Merge branch 'main' into commands-list-clean
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
||||||
|
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).
|
||||||
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
|
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
|
||||||
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
|
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
|
||||||
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
|
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
|
||||||
|
|||||||
86
docs/debugging.md
Normal file
86
docs/debugging.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
summary: "Debugging tools: watch mode, raw model streams, and tracing reasoning leakage"
|
||||||
|
read_when:
|
||||||
|
- You need to inspect raw model output for reasoning leakage
|
||||||
|
- You want to run the Gateway in watch mode while iterating
|
||||||
|
- You need a repeatable debugging workflow
|
||||||
|
---
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
|
||||||
|
This page covers debugging helpers for streaming output, especially when a
|
||||||
|
provider mixes reasoning into normal text.
|
||||||
|
|
||||||
|
## Gateway watch mode
|
||||||
|
|
||||||
|
For fast iteration, run the gateway under the file watcher:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm gateway:watch --force
|
||||||
|
```
|
||||||
|
|
||||||
|
This maps to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tsx watch src/entry.ts gateway --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Add any gateway CLI flags after `gateway:watch` and they will be passed through
|
||||||
|
on each restart.
|
||||||
|
|
||||||
|
## Raw stream logging (Clawdbot)
|
||||||
|
|
||||||
|
Clawdbot can log the **raw assistant stream** before any filtering/formatting.
|
||||||
|
This is the best way to see whether reasoning is arriving as plain text deltas
|
||||||
|
(or as separate thinking blocks).
|
||||||
|
|
||||||
|
Enable it via CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm gateway:watch --force --raw-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional path override:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm gateway:watch --force --raw-stream --raw-stream-path ~/.clawdbot/logs/raw-stream.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalent env vars:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLAWDBOT_RAW_STREAM=1
|
||||||
|
CLAWDBOT_RAW_STREAM_PATH=~/.clawdbot/logs/raw-stream.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
Default file:
|
||||||
|
|
||||||
|
`~/.clawdbot/logs/raw-stream.jsonl`
|
||||||
|
|
||||||
|
## Raw chunk logging (pi-mono)
|
||||||
|
|
||||||
|
To capture **raw OpenAI-compat chunks** before they are parsed into blocks,
|
||||||
|
pi-mono exposes a separate logger:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PI_RAW_STREAM=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PI_RAW_STREAM_PATH=~/.pi-mono/logs/raw-openai-completions.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
Default file:
|
||||||
|
|
||||||
|
`~/.pi-mono/logs/raw-openai-completions.jsonl`
|
||||||
|
|
||||||
|
> Note: this is only emitted by processes using pi-mono’s
|
||||||
|
> `openai-completions` provider.
|
||||||
|
|
||||||
|
## Safety notes
|
||||||
|
|
||||||
|
- Raw stream logs can include full prompts, tool output, and user data.
|
||||||
|
- Keep logs local and delete them after debugging.
|
||||||
|
- If you share logs, scrub secrets and PII first.
|
||||||
332
src/agents/claude-cli-runner.ts
Normal file
332
src/agents/claude-cli-runner.ts
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||||
|
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
||||||
|
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import { createSubsystemLogger } from "../logging.js";
|
||||||
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
|
import { resolveUserPath } from "../utils.js";
|
||||||
|
import {
|
||||||
|
buildBootstrapContextFiles,
|
||||||
|
type EmbeddedContextFile,
|
||||||
|
} from "./pi-embedded-helpers.js";
|
||||||
|
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||||
|
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||||
|
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||||
|
|
||||||
|
const log = createSubsystemLogger("agent/claude-cli");
|
||||||
|
|
||||||
|
type ClaudeCliUsage = {
|
||||||
|
input?: number;
|
||||||
|
output?: number;
|
||||||
|
cacheRead?: number;
|
||||||
|
cacheWrite?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ClaudeCliOutput = {
|
||||||
|
text: string;
|
||||||
|
sessionId?: string;
|
||||||
|
usage?: ClaudeCliUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveUserTimezone(configured?: string): string {
|
||||||
|
const trimmed = configured?.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
try {
|
||||||
|
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
return trimmed;
|
||||||
|
} catch {
|
||||||
|
// ignore invalid timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const host = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
return host?.trim() || "UTC";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUserTime(date: Date, timeZone: string): string | undefined {
|
||||||
|
try {
|
||||||
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone,
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hourCycle: "h23",
|
||||||
|
}).formatToParts(date);
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.type !== "literal") map[part.type] = part.value;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!map.weekday ||
|
||||||
|
!map.year ||
|
||||||
|
!map.month ||
|
||||||
|
!map.day ||
|
||||||
|
!map.hour ||
|
||||||
|
!map.minute
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
||||||
|
const models = cfg?.agent?.models ?? {};
|
||||||
|
const entries: Array<{ alias: string; model: string }> = [];
|
||||||
|
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
||||||
|
const model = String(keyRaw ?? "").trim();
|
||||||
|
if (!model) continue;
|
||||||
|
const alias = String(
|
||||||
|
(entryRaw as { alias?: string } | undefined)?.alias ?? "",
|
||||||
|
).trim();
|
||||||
|
if (!alias) continue;
|
||||||
|
entries.push({ alias, model });
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
.sort((a, b) => a.alias.localeCompare(b.alias))
|
||||||
|
.map((entry) => `- ${entry.alias}: ${entry.model}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSystemPrompt(params: {
|
||||||
|
workspaceDir: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
defaultThinkLevel?: ThinkLevel;
|
||||||
|
extraSystemPrompt?: string;
|
||||||
|
ownerNumbers?: string[];
|
||||||
|
tools: AgentTool[];
|
||||||
|
contextFiles?: EmbeddedContextFile[];
|
||||||
|
modelDisplay: string;
|
||||||
|
}) {
|
||||||
|
const userTimezone = resolveUserTimezone(params.config?.agent?.userTimezone);
|
||||||
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
|
return buildAgentSystemPrompt({
|
||||||
|
workspaceDir: params.workspaceDir,
|
||||||
|
defaultThinkLevel: params.defaultThinkLevel,
|
||||||
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
|
ownerNumbers: params.ownerNumbers,
|
||||||
|
reasoningTagHint: false,
|
||||||
|
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||||
|
params.config?.agent?.heartbeat?.prompt,
|
||||||
|
),
|
||||||
|
runtimeInfo: {
|
||||||
|
host: "clawdbot",
|
||||||
|
os: `${os.type()} ${os.release()}`,
|
||||||
|
arch: os.arch(),
|
||||||
|
node: process.version,
|
||||||
|
model: params.modelDisplay,
|
||||||
|
},
|
||||||
|
toolNames: params.tools.map((tool) => tool.name),
|
||||||
|
modelAliasLines: buildModelAliasLines(params.config),
|
||||||
|
userTimezone,
|
||||||
|
userTime,
|
||||||
|
contextFiles: params.contextFiles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeClaudeCliModel(modelId: string): string {
|
||||||
|
const trimmed = modelId.trim();
|
||||||
|
if (!trimmed) return "opus";
|
||||||
|
const lower = trimmed.toLowerCase();
|
||||||
|
if (lower.startsWith("opus")) return "opus";
|
||||||
|
if (lower.startsWith("sonnet")) return "sonnet";
|
||||||
|
if (lower.startsWith("haiku")) return "haiku";
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUsage(raw: Record<string, unknown>): ClaudeCliUsage | undefined {
|
||||||
|
const pick = (key: string) =>
|
||||||
|
typeof raw[key] === "number" && raw[key] > 0
|
||||||
|
? (raw[key] as number)
|
||||||
|
: undefined;
|
||||||
|
const input = pick("input_tokens") ?? pick("inputTokens");
|
||||||
|
const output = pick("output_tokens") ?? pick("outputTokens");
|
||||||
|
const cacheRead = pick("cache_read_input_tokens") ?? pick("cacheRead");
|
||||||
|
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite");
|
||||||
|
const total = pick("total_tokens") ?? pick("total");
|
||||||
|
if (!input && !output && !cacheRead && !cacheWrite && !total)
|
||||||
|
return undefined;
|
||||||
|
return { input, output, cacheRead, cacheWrite, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectText(value: unknown): string {
|
||||||
|
if (!value) return "";
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((entry) => collectText(entry)).join("");
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) return "";
|
||||||
|
if (typeof value.text === "string") return value.text;
|
||||||
|
if (typeof value.content === "string") return value.content;
|
||||||
|
if (Array.isArray(value.content)) {
|
||||||
|
return value.content.map((entry) => collectText(entry)).join("");
|
||||||
|
}
|
||||||
|
if (isRecord(value.message)) return collectText(value.message);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseClaudeCliJson(raw: string): ClaudeCliOutput | null {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(trimmed);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isRecord(parsed)) return null;
|
||||||
|
const sessionId =
|
||||||
|
(typeof parsed.session_id === "string" && parsed.session_id) ||
|
||||||
|
(typeof parsed.sessionId === "string" && parsed.sessionId) ||
|
||||||
|
(typeof parsed.conversation_id === "string" && parsed.conversation_id) ||
|
||||||
|
undefined;
|
||||||
|
const usage = isRecord(parsed.usage) ? toUsage(parsed.usage) : undefined;
|
||||||
|
const text =
|
||||||
|
collectText(parsed.message) ||
|
||||||
|
collectText(parsed.content) ||
|
||||||
|
collectText(parsed.result) ||
|
||||||
|
collectText(parsed);
|
||||||
|
return { text: text.trim(), sessionId, usage };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runClaudeCliOnce(params: {
|
||||||
|
prompt: string;
|
||||||
|
workspaceDir: string;
|
||||||
|
modelId: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
timeoutMs: number;
|
||||||
|
resumeSessionId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}): Promise<ClaudeCliOutput> {
|
||||||
|
const args = [
|
||||||
|
"-p",
|
||||||
|
"--output-format",
|
||||||
|
"json",
|
||||||
|
"--model",
|
||||||
|
normalizeClaudeCliModel(params.modelId),
|
||||||
|
"--append-system-prompt",
|
||||||
|
params.systemPrompt,
|
||||||
|
"--dangerously-skip-permissions",
|
||||||
|
"--permission-mode",
|
||||||
|
"dontAsk",
|
||||||
|
"--tools",
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
if (params.resumeSessionId) {
|
||||||
|
args.push("--resume", params.resumeSessionId);
|
||||||
|
} else if (params.sessionId) {
|
||||||
|
args.push("--session-id", params.sessionId);
|
||||||
|
}
|
||||||
|
args.push(params.prompt);
|
||||||
|
|
||||||
|
const result = await runCommandWithTimeout(["claude", ...args], {
|
||||||
|
timeoutMs: params.timeoutMs,
|
||||||
|
cwd: params.workspaceDir,
|
||||||
|
});
|
||||||
|
const stdout = result.stdout.trim();
|
||||||
|
if (result.code !== 0) {
|
||||||
|
const err = result.stderr.trim() || stdout || "Claude CLI failed.";
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
const parsed = parseClaudeCliJson(stdout);
|
||||||
|
if (parsed) return parsed;
|
||||||
|
return { text: stdout };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runClaudeCliAgent(params: {
|
||||||
|
sessionId: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
sessionFile: string;
|
||||||
|
workspaceDir: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
prompt: string;
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
thinkLevel?: ThinkLevel;
|
||||||
|
timeoutMs: number;
|
||||||
|
runId: string;
|
||||||
|
extraSystemPrompt?: string;
|
||||||
|
ownerNumbers?: string[];
|
||||||
|
resumeSessionId?: string;
|
||||||
|
}): Promise<EmbeddedPiRunResult> {
|
||||||
|
const started = Date.now();
|
||||||
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
|
const workspaceDir = resolvedWorkspace;
|
||||||
|
|
||||||
|
const modelId = (params.model ?? "opus").trim() || "opus";
|
||||||
|
const modelDisplay = `${params.provider ?? "claude-cli"}/${modelId}`;
|
||||||
|
|
||||||
|
const extraSystemPrompt = [
|
||||||
|
params.extraSystemPrompt?.trim(),
|
||||||
|
"Tools are disabled in this session. Do not call tools.",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const bootstrapFiles = await loadWorkspaceBootstrapFiles(workspaceDir);
|
||||||
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
|
const systemPrompt = buildSystemPrompt({
|
||||||
|
workspaceDir,
|
||||||
|
config: params.config,
|
||||||
|
defaultThinkLevel: params.thinkLevel,
|
||||||
|
extraSystemPrompt,
|
||||||
|
ownerNumbers: params.ownerNumbers,
|
||||||
|
tools: [],
|
||||||
|
contextFiles,
|
||||||
|
modelDisplay,
|
||||||
|
});
|
||||||
|
|
||||||
|
let output: ClaudeCliOutput;
|
||||||
|
try {
|
||||||
|
output = await runClaudeCliOnce({
|
||||||
|
prompt: params.prompt,
|
||||||
|
workspaceDir,
|
||||||
|
modelId,
|
||||||
|
systemPrompt,
|
||||||
|
timeoutMs: params.timeoutMs,
|
||||||
|
resumeSessionId: params.resumeSessionId,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!params.resumeSessionId) throw err;
|
||||||
|
log.warn(
|
||||||
|
`claude-cli resume failed for ${params.resumeSessionId}; retrying with --session-id (${params.sessionId})`,
|
||||||
|
);
|
||||||
|
output = await runClaudeCliOnce({
|
||||||
|
prompt: params.prompt,
|
||||||
|
workspaceDir,
|
||||||
|
modelId,
|
||||||
|
systemPrompt,
|
||||||
|
timeoutMs: params.timeoutMs,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = output.text?.trim();
|
||||||
|
const payloads = text ? [{ text }] : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
payloads,
|
||||||
|
meta: {
|
||||||
|
durationMs: Date.now() - started,
|
||||||
|
agentMeta: {
|
||||||
|
sessionId: output.sessionId ?? params.sessionId,
|
||||||
|
provider: params.provider ?? "claude-cli",
|
||||||
|
model: modelId,
|
||||||
|
usage: output.usage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||||
import type { ReasoningLevel } from "../auto-reply/thinking.js";
|
import type { ReasoningLevel } from "../auto-reply/thinking.js";
|
||||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||||
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||||
import { createSubsystemLogger } from "../logging.js";
|
import { createSubsystemLogger } from "../logging.js";
|
||||||
import { splitMediaFromOutput } from "../media/parse.js";
|
import { splitMediaFromOutput } from "../media/parse.js";
|
||||||
@@ -23,6 +26,31 @@ const THINKING_OPEN_GLOBAL_RE = /<\s*think(?:ing)?\s*>/gi;
|
|||||||
const THINKING_CLOSE_GLOBAL_RE = /<\s*\/\s*think(?:ing)?\s*>/gi;
|
const THINKING_CLOSE_GLOBAL_RE = /<\s*\/\s*think(?:ing)?\s*>/gi;
|
||||||
const TOOL_RESULT_MAX_CHARS = 8000;
|
const TOOL_RESULT_MAX_CHARS = 8000;
|
||||||
const log = createSubsystemLogger("agent/embedded");
|
const log = createSubsystemLogger("agent/embedded");
|
||||||
|
const RAW_STREAM_ENABLED = process.env.CLAWDBOT_RAW_STREAM === "1";
|
||||||
|
const RAW_STREAM_PATH =
|
||||||
|
process.env.CLAWDBOT_RAW_STREAM_PATH?.trim() ||
|
||||||
|
path.join(resolveStateDir(), "logs", "raw-stream.jsonl");
|
||||||
|
let rawStreamReady = false;
|
||||||
|
|
||||||
|
const appendRawStream = (payload: Record<string, unknown>) => {
|
||||||
|
if (!RAW_STREAM_ENABLED) return;
|
||||||
|
if (!rawStreamReady) {
|
||||||
|
rawStreamReady = true;
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(RAW_STREAM_PATH), { recursive: true });
|
||||||
|
} catch {
|
||||||
|
// ignore raw stream mkdir failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
void fs.promises.appendFile(
|
||||||
|
RAW_STREAM_PATH,
|
||||||
|
`${JSON.stringify(payload)}\n`,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore raw stream write failures
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
|
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
|
||||||
|
|
||||||
@@ -664,6 +692,15 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
typeof assistantRecord?.content === "string"
|
typeof assistantRecord?.content === "string"
|
||||||
? assistantRecord.content
|
? assistantRecord.content
|
||||||
: "";
|
: "";
|
||||||
|
appendRawStream({
|
||||||
|
ts: Date.now(),
|
||||||
|
event: "assistant_text_stream",
|
||||||
|
runId: params.runId,
|
||||||
|
sessionId: (params.session as { id?: string }).id,
|
||||||
|
evtType,
|
||||||
|
delta,
|
||||||
|
content,
|
||||||
|
});
|
||||||
let chunk = "";
|
let chunk = "";
|
||||||
if (evtType === "text_delta") {
|
if (evtType === "text_delta") {
|
||||||
chunk = delta;
|
chunk = delta;
|
||||||
@@ -756,6 +793,14 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
if (msg?.role === "assistant") {
|
if (msg?.role === "assistant") {
|
||||||
const assistantMessage = msg as AssistantMessage;
|
const assistantMessage = msg as AssistantMessage;
|
||||||
const rawText = extractAssistantText(assistantMessage);
|
const rawText = extractAssistantText(assistantMessage);
|
||||||
|
appendRawStream({
|
||||||
|
ts: Date.now(),
|
||||||
|
event: "assistant_message_end",
|
||||||
|
runId: params.runId,
|
||||||
|
sessionId: (params.session as { id?: string }).id,
|
||||||
|
rawText,
|
||||||
|
rawThinking: extractAssistantThinking(assistantMessage),
|
||||||
|
});
|
||||||
const cleaned = params.enforceFinalTag
|
const cleaned = params.enforceFinalTag
|
||||||
? stripThinkingSegments(stripUnpairedThinkingTags(rawText))
|
? stripThinkingSegments(stripUnpairedThinkingTags(rawText))
|
||||||
: stripThinkingSegments(rawText);
|
: stripThinkingSegments(rawText);
|
||||||
|
|||||||
@@ -599,7 +599,7 @@ describe("directive behavior", () => {
|
|||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("Elevated mode disabled.");
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
expect(text).toContain("status agent:main:main");
|
expect(text).toContain("Session: agent:main:main");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,8 +92,7 @@ describe("buildStatusMessage", () => {
|
|||||||
|
|
||||||
expect(text).toContain("🦞 ClawdBot");
|
expect(text).toContain("🦞 ClawdBot");
|
||||||
expect(text).toContain("🧠 Model: anthropic/pi:opus · 🔑 api-key");
|
expect(text).toContain("🧠 Model: anthropic/pi:opus · 🔑 api-key");
|
||||||
expect(text).toContain("🧮 Tokens: 1.2k in / 800 out");
|
expect(text).toContain("🧮 Tokens: 1.2k in / 800 out · 💵 Cost: $0.0020");
|
||||||
expect(text).toContain("💵 Cost: $0.0020");
|
|
||||||
expect(text).toContain("Context: 16k/32k (50%)");
|
expect(text).toContain("Context: 16k/32k (50%)");
|
||||||
expect(text).toContain("🧹 Compactions: 2");
|
expect(text).toContain("🧹 Compactions: 2");
|
||||||
expect(text).toContain("Session: agent:main:main");
|
expect(text).toContain("Session: agent:main:main");
|
||||||
|
|||||||
@@ -297,7 +297,10 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
const activationLine = activationParts.filter(Boolean).join(" · ");
|
const activationLine = activationParts.filter(Boolean).join(" · ");
|
||||||
|
|
||||||
const authMode = resolveModelAuthMode(provider, args.config);
|
const authMode = resolveModelAuthMode(provider, args.config);
|
||||||
const showCost = authMode === "api-key";
|
const authLabelValue =
|
||||||
|
args.modelAuth ??
|
||||||
|
(authMode && authMode !== "unknown" ? authMode : undefined);
|
||||||
|
const showCost = authLabelValue === "api-key" || authLabelValue === "mixed";
|
||||||
const costConfig = showCost
|
const costConfig = showCost
|
||||||
? resolveModelCostConfig({
|
? resolveModelCostConfig({
|
||||||
provider,
|
provider,
|
||||||
@@ -320,21 +323,19 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
||||||
|
|
||||||
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
||||||
const authLabelValue =
|
|
||||||
args.modelAuth ??
|
|
||||||
(authMode && authMode !== "unknown" ? authMode : undefined);
|
|
||||||
const authLabel = authLabelValue ? ` · 🔑 ${authLabelValue}` : "";
|
const authLabel = authLabelValue ? ` · 🔑 ${authLabelValue}` : "";
|
||||||
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
||||||
const commit = resolveCommitHash();
|
const commit = resolveCommitHash();
|
||||||
const versionLine = `🦞 ClawdBot ${VERSION}${commit ? ` (${commit})` : ""}`;
|
const versionLine = `🦞 ClawdBot ${VERSION}${commit ? ` (${commit})` : ""}`;
|
||||||
const usagePair = formatUsagePair(inputTokens, outputTokens);
|
const usagePair = formatUsagePair(inputTokens, outputTokens);
|
||||||
const costLine = costLabel ? `💵 Cost: ${costLabel}` : null;
|
const costLine = costLabel ? `💵 Cost: ${costLabel}` : null;
|
||||||
|
const usageCostLine =
|
||||||
|
usagePair && costLine ? `${usagePair} · ${costLine}` : usagePair ?? costLine;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
versionLine,
|
versionLine,
|
||||||
modelLine,
|
modelLine,
|
||||||
usagePair,
|
usageCostLine,
|
||||||
costLine,
|
|
||||||
`📚 ${contextLine}`,
|
`📚 ${contextLine}`,
|
||||||
args.usageLine,
|
args.usageLine,
|
||||||
`🧵 ${sessionLine}`,
|
`🧵 ${sessionLine}`,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||||
import { getResolvedLoggerSettings } from "../logging.js";
|
import { getResolvedLoggerSettings } from "../logging.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||||
import { createDefaultDeps } from "./deps.js";
|
import { createDefaultDeps } from "./deps.js";
|
||||||
import { withProgress } from "./progress.js";
|
import { withProgress } from "./progress.js";
|
||||||
|
|
||||||
@@ -523,100 +524,146 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rich = isRich();
|
||||||
|
const label = (value: string) => colorize(rich, theme.muted, value);
|
||||||
|
const accent = (value: string) => colorize(rich, theme.accent, value);
|
||||||
|
const infoText = (value: string) => colorize(rich, theme.info, value);
|
||||||
|
const okText = (value: string) => colorize(rich, theme.success, value);
|
||||||
|
const warnText = (value: string) => colorize(rich, theme.warn, value);
|
||||||
|
const errorText = (value: string) => colorize(rich, theme.error, value);
|
||||||
|
const spacer = () => defaultRuntime.log("");
|
||||||
|
|
||||||
const { service, rpc, legacyServices, extraServices } = status;
|
const { service, rpc, legacyServices, extraServices } = status;
|
||||||
|
const serviceStatus = service.loaded
|
||||||
|
? okText(service.loadedText)
|
||||||
|
: warnText(service.notLoadedText);
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
`Service: ${service.label} (${service.loaded ? service.loadedText : service.notLoadedText})`,
|
`${label("Service:")} ${accent(service.label)} (${serviceStatus})`,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const logFile = getResolvedLoggerSettings().file;
|
const logFile = getResolvedLoggerSettings().file;
|
||||||
defaultRuntime.log(`File logs: ${logFile}`);
|
defaultRuntime.log(`${label("File logs:")} ${infoText(logFile)}`);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore missing config/log resolution
|
// ignore missing config/log resolution
|
||||||
}
|
}
|
||||||
if (service.command?.programArguments?.length) {
|
if (service.command?.programArguments?.length) {
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
`Command: ${service.command.programArguments.join(" ")}`,
|
`${label("Command:")} ${infoText(service.command.programArguments.join(" "))}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (service.command?.sourcePath) {
|
if (service.command?.sourcePath) {
|
||||||
defaultRuntime.log(`Service file: ${service.command.sourcePath}`);
|
defaultRuntime.log(
|
||||||
|
`${label("Service file:")} ${infoText(service.command.sourcePath)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (service.command?.workingDirectory) {
|
if (service.command?.workingDirectory) {
|
||||||
defaultRuntime.log(`Working dir: ${service.command.workingDirectory}`);
|
defaultRuntime.log(
|
||||||
|
`${label("Working dir:")} ${infoText(service.command.workingDirectory)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const daemonEnvLines = safeDaemonEnv(service.command?.environment);
|
const daemonEnvLines = safeDaemonEnv(service.command?.environment);
|
||||||
if (daemonEnvLines.length > 0) {
|
if (daemonEnvLines.length > 0) {
|
||||||
defaultRuntime.log(`Daemon env: ${daemonEnvLines.join(" ")}`);
|
defaultRuntime.log(`${label("Daemon env:")} ${daemonEnvLines.join(" ")}`);
|
||||||
}
|
}
|
||||||
|
spacer();
|
||||||
if (service.configAudit?.issues.length) {
|
if (service.configAudit?.issues.length) {
|
||||||
defaultRuntime.error("Service config looks out of date or non-standard.");
|
defaultRuntime.error(
|
||||||
|
warnText("Service config looks out of date or non-standard."),
|
||||||
|
);
|
||||||
for (const issue of service.configAudit.issues) {
|
for (const issue of service.configAudit.issues) {
|
||||||
const detail = issue.detail ? ` (${issue.detail})` : "";
|
const detail = issue.detail ? ` (${issue.detail})` : "";
|
||||||
defaultRuntime.error(`Service config issue: ${issue.message}${detail}`);
|
defaultRuntime.error(
|
||||||
|
`${warnText("Service config issue:")} ${issue.message}${detail}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").',
|
warnText(
|
||||||
|
'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (status.config) {
|
if (status.config) {
|
||||||
const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`;
|
const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`;
|
||||||
defaultRuntime.log(`Config (cli): ${cliCfg}`);
|
defaultRuntime.log(`${label("Config (cli):")} ${infoText(cliCfg)}`);
|
||||||
if (!status.config.cli.valid && status.config.cli.issues?.length) {
|
if (!status.config.cli.valid && status.config.cli.issues?.length) {
|
||||||
for (const issue of status.config.cli.issues.slice(0, 5)) {
|
for (const issue of status.config.cli.issues.slice(0, 5)) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`Config issue: ${issue.path || "<root>"}: ${issue.message}`,
|
`${errorText("Config issue:")} ${issue.path || "<root>"}: ${issue.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status.config.daemon) {
|
if (status.config.daemon) {
|
||||||
const daemonCfg = `${status.config.daemon.path}${status.config.daemon.exists ? "" : " (missing)"}${status.config.daemon.valid ? "" : " (invalid)"}`;
|
const daemonCfg = `${status.config.daemon.path}${status.config.daemon.exists ? "" : " (missing)"}${status.config.daemon.valid ? "" : " (invalid)"}`;
|
||||||
defaultRuntime.log(`Config (daemon): ${daemonCfg}`);
|
defaultRuntime.log(`${label("Config (daemon):")} ${infoText(daemonCfg)}`);
|
||||||
if (!status.config.daemon.valid && status.config.daemon.issues?.length) {
|
if (!status.config.daemon.valid && status.config.daemon.issues?.length) {
|
||||||
for (const issue of status.config.daemon.issues.slice(0, 5)) {
|
for (const issue of status.config.daemon.issues.slice(0, 5)) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`Daemon config issue: ${issue.path || "<root>"}: ${issue.message}`,
|
`${errorText("Daemon config issue:")} ${issue.path || "<root>"}: ${issue.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status.config.mismatch) {
|
if (status.config.mismatch) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
"Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).",
|
errorText(
|
||||||
|
"Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
"Fix: rerun `clawdbot daemon install --force` from the same --profile / CLAWDBOT_STATE_DIR you expect.",
|
errorText(
|
||||||
|
"Fix: rerun `clawdbot daemon install --force` from the same --profile / CLAWDBOT_STATE_DIR you expect.",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
if (status.gateway) {
|
if (status.gateway) {
|
||||||
const bindHost = status.gateway.bindHost ?? "n/a";
|
const bindHost = status.gateway.bindHost ?? "n/a";
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
`Gateway: bind=${status.gateway.bindMode} (${bindHost}), port=${status.gateway.port} (${status.gateway.portSource})`,
|
`${label("Gateway:")} bind=${infoText(status.gateway.bindMode)} (${infoText(bindHost)}), port=${infoText(String(status.gateway.port))} (${infoText(status.gateway.portSource)})`,
|
||||||
|
);
|
||||||
|
defaultRuntime.log(
|
||||||
|
`${label("Probe target:")} ${infoText(status.gateway.probeUrl)}`,
|
||||||
);
|
);
|
||||||
defaultRuntime.log(`Probe target: ${status.gateway.probeUrl}`);
|
|
||||||
const controlUiEnabled = status.config?.daemon?.controlUi?.enabled ?? true;
|
const controlUiEnabled = status.config?.daemon?.controlUi?.enabled ?? true;
|
||||||
if (!controlUiEnabled) {
|
if (!controlUiEnabled) {
|
||||||
defaultRuntime.log("Dashboard: disabled");
|
defaultRuntime.log(`${label("Dashboard:")} ${warnText("disabled")}`);
|
||||||
} else {
|
} else {
|
||||||
const links = resolveControlUiLinks({
|
const links = resolveControlUiLinks({
|
||||||
port: status.gateway.port,
|
port: status.gateway.port,
|
||||||
bind: status.gateway.bindMode,
|
bind: status.gateway.bindMode,
|
||||||
basePath: status.config?.daemon?.controlUi?.basePath,
|
basePath: status.config?.daemon?.controlUi?.basePath,
|
||||||
});
|
});
|
||||||
defaultRuntime.log(`Dashboard: ${links.httpUrl}`);
|
defaultRuntime.log(`${label("Dashboard:")} ${infoText(links.httpUrl)}`);
|
||||||
}
|
}
|
||||||
if (status.gateway.probeNote) {
|
if (status.gateway.probeNote) {
|
||||||
defaultRuntime.log(`Probe note: ${status.gateway.probeNote}`);
|
defaultRuntime.log(
|
||||||
|
`${label("Probe note:")} ${infoText(status.gateway.probeNote)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (status.gateway.bindMode === "tailnet" && !status.gateway.bindHost) {
|
if (status.gateway.bindMode === "tailnet" && !status.gateway.bindHost) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
"Root cause: gateway bind=tailnet but no tailnet interface was found.",
|
errorText(
|
||||||
|
"Root cause: gateway bind=tailnet but no tailnet interface was found.",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
const runtimeLine = formatRuntimeStatus(service.runtime);
|
const runtimeLine = formatRuntimeStatus(service.runtime);
|
||||||
if (runtimeLine) {
|
if (runtimeLine) {
|
||||||
defaultRuntime.log(`Runtime: ${runtimeLine}`);
|
const runtimeStatus = service.runtime?.status ?? "unknown";
|
||||||
|
const runtimeColor =
|
||||||
|
runtimeStatus === "running"
|
||||||
|
? theme.success
|
||||||
|
: runtimeStatus === "stopped"
|
||||||
|
? theme.error
|
||||||
|
: runtimeStatus === "unknown"
|
||||||
|
? theme.muted
|
||||||
|
: theme.warn;
|
||||||
|
defaultRuntime.log(
|
||||||
|
`${label("Runtime:")} ${colorize(rich, runtimeColor, runtimeLine)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
rpc &&
|
rpc &&
|
||||||
@@ -625,44 +672,50 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
service.runtime?.status === "running"
|
service.runtime?.status === "running"
|
||||||
) {
|
) {
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
"Warm-up: launch agents can take a few seconds. Try again shortly.",
|
warnText("Warm-up: launch agents can take a few seconds. Try again shortly."),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (rpc) {
|
if (rpc) {
|
||||||
if (rpc.ok) {
|
if (rpc.ok) {
|
||||||
defaultRuntime.log("RPC probe: ok");
|
defaultRuntime.log(`${label("RPC probe:")} ${okText("ok")}`);
|
||||||
} else {
|
} else {
|
||||||
defaultRuntime.error("RPC probe: failed");
|
defaultRuntime.error(`${label("RPC probe:")} ${errorText("failed")}`);
|
||||||
if (rpc.url) defaultRuntime.error(`RPC target: ${rpc.url}`);
|
if (rpc.url)
|
||||||
|
defaultRuntime.error(`${label("RPC target:")} ${rpc.url}`);
|
||||||
const lines = String(rpc.error ?? "unknown")
|
const lines = String(rpc.error ?? "unknown")
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
for (const line of lines.slice(0, 12)) {
|
for (const line of lines.slice(0, 12)) {
|
||||||
defaultRuntime.error(` ${line}`);
|
defaultRuntime.error(` ${errorText(line)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
if (service.runtime?.missingUnit) {
|
if (service.runtime?.missingUnit) {
|
||||||
defaultRuntime.error("Service unit not found.");
|
defaultRuntime.error(errorText("Service unit not found."));
|
||||||
for (const hint of renderRuntimeHints(service.runtime)) {
|
for (const hint of renderRuntimeHints(service.runtime)) {
|
||||||
defaultRuntime.error(hint);
|
defaultRuntime.error(errorText(hint));
|
||||||
}
|
}
|
||||||
} else if (service.loaded && service.runtime?.status === "stopped") {
|
} else if (service.loaded && service.runtime?.status === "stopped") {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
"Service is loaded but not running (likely exited immediately).",
|
errorText("Service is loaded but not running (likely exited immediately)."),
|
||||||
);
|
);
|
||||||
for (const hint of renderRuntimeHints(
|
for (const hint of renderRuntimeHints(
|
||||||
service.runtime,
|
service.runtime,
|
||||||
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
|
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
|
||||||
)) {
|
)) {
|
||||||
defaultRuntime.error(hint);
|
defaultRuntime.error(errorText(hint));
|
||||||
}
|
}
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
if (service.runtime?.cachedLabel) {
|
if (service.runtime?.cachedLabel) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
errorText(
|
||||||
|
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
defaultRuntime.error("Then reinstall: clawdbot daemon install");
|
defaultRuntime.error(errorText("Then reinstall: clawdbot daemon install"));
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
if (status.port && shouldReportPortUsage(status.port.status, rpc?.ok)) {
|
if (status.port && shouldReportPortUsage(status.port.status, rpc?.ok)) {
|
||||||
for (const line of formatPortDiagnostics({
|
for (const line of formatPortDiagnostics({
|
||||||
@@ -671,7 +724,7 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
listeners: status.port.listeners,
|
listeners: status.port.listeners,
|
||||||
hints: status.port.hints,
|
hints: status.port.hints,
|
||||||
})) {
|
})) {
|
||||||
defaultRuntime.error(line);
|
defaultRuntime.error(errorText(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status.port) {
|
if (status.port) {
|
||||||
@@ -683,12 +736,12 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (addrs.length > 0) {
|
if (addrs.length > 0) {
|
||||||
defaultRuntime.log(`Listening: ${addrs.join(", ")}`);
|
defaultRuntime.log(`${label("Listening:")} ${infoText(addrs.join(", "))}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status.portCli && status.portCli.port !== status.port?.port) {
|
if (status.portCli && status.portCli.port !== status.port?.port) {
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
`Note: CLI config resolves gateway port=${status.portCli.port} (${status.portCli.status}).`,
|
`${label("Note:")} CLI config resolves gateway port=${status.portCli.port} (${status.portCli.status}).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -698,52 +751,72 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
status.port.status !== "busy"
|
status.port.status !== "busy"
|
||||||
) {
|
) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`Gateway port ${status.port.port} is not listening (service appears running).`,
|
errorText(
|
||||||
|
`Gateway port ${status.port.port} is not listening (service appears running).`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (status.lastError) {
|
if (status.lastError) {
|
||||||
defaultRuntime.error(`Last gateway error: ${status.lastError}`);
|
defaultRuntime.error(
|
||||||
|
`${errorText("Last gateway error:")} ${status.lastError}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`Logs: journalctl --user -u ${GATEWAY_SYSTEMD_SERVICE_NAME}.service -n 200 --no-pager`,
|
errorText(
|
||||||
|
`Logs: journalctl --user -u ${GATEWAY_SYSTEMD_SERVICE_NAME}.service -n 200 --no-pager`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
const logs = resolveGatewayLogPaths(
|
const logs = resolveGatewayLogPaths(
|
||||||
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
|
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
|
||||||
);
|
);
|
||||||
defaultRuntime.error(`Logs: ${logs.stdoutPath}`);
|
defaultRuntime.error(`${errorText("Logs:")} ${logs.stdoutPath}`);
|
||||||
defaultRuntime.error(`Errors: ${logs.stderrPath}`);
|
defaultRuntime.error(`${errorText("Errors:")} ${logs.stderrPath}`);
|
||||||
}
|
}
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (legacyServices.length > 0) {
|
if (legacyServices.length > 0) {
|
||||||
defaultRuntime.error("Legacy Clawdis services detected:");
|
defaultRuntime.error(errorText("Legacy Clawdis services detected:"));
|
||||||
for (const svc of legacyServices) {
|
for (const svc of legacyServices) {
|
||||||
defaultRuntime.error(`- ${svc.label} (${svc.detail})`);
|
defaultRuntime.error(`- ${errorText(svc.label)} (${svc.detail})`);
|
||||||
}
|
}
|
||||||
defaultRuntime.error("Cleanup: clawdbot doctor");
|
defaultRuntime.error(errorText("Cleanup: clawdbot doctor"));
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraServices.length > 0) {
|
if (extraServices.length > 0) {
|
||||||
defaultRuntime.error("Other gateway-like services detected (best effort):");
|
defaultRuntime.error(
|
||||||
|
errorText("Other gateway-like services detected (best effort):"),
|
||||||
|
);
|
||||||
for (const svc of extraServices) {
|
for (const svc of extraServices) {
|
||||||
defaultRuntime.error(`- ${svc.label} (${svc.scope}, ${svc.detail})`);
|
defaultRuntime.error(
|
||||||
|
`- ${errorText(svc.label)} (${svc.scope}, ${svc.detail})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (const hint of renderGatewayServiceCleanupHints()) {
|
for (const hint of renderGatewayServiceCleanupHints()) {
|
||||||
defaultRuntime.error(`Cleanup hint: ${hint}`);
|
defaultRuntime.error(`${errorText("Cleanup hint:")} ${hint}`);
|
||||||
}
|
}
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (legacyServices.length > 0 || extraServices.length > 0) {
|
if (legacyServices.length > 0 || extraServices.length > 0) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
"Recommendation: run a single gateway per machine. One gateway supports multiple agents.",
|
errorText(
|
||||||
|
"Recommendation: run a single gateway per machine. One gateway supports multiple agents.",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
"If you need multiple gateways, isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host).",
|
errorText(
|
||||||
|
"If you need multiple gateways, isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host).",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
spacer();
|
||||||
}
|
}
|
||||||
defaultRuntime.log("Troubles: run clawdbot status");
|
defaultRuntime.log(`${label("Troubles:")} run clawdbot status`);
|
||||||
defaultRuntime.log("Troubleshooting: https://docs.clawd.bot/troubleshooting");
|
defaultRuntime.log(
|
||||||
|
`${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runDaemonStatus(opts: DaemonStatusOptions) {
|
export async function runDaemonStatus(opts: DaemonStatusOptions) {
|
||||||
@@ -755,7 +828,10 @@ export async function runDaemonStatus(opts: DaemonStatusOptions) {
|
|||||||
});
|
});
|
||||||
printDaemonStatus(status, { json: Boolean(opts.json) });
|
printDaemonStatus(status, { json: Boolean(opts.json) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
defaultRuntime.error(`Daemon status failed: ${String(err)}`);
|
const rich = isRich();
|
||||||
|
defaultRuntime.error(
|
||||||
|
colorize(rich, theme.error, `Daemon status failed: ${String(err)}`),
|
||||||
|
);
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ type GatewayRunOpts = {
|
|||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
wsLog?: unknown;
|
wsLog?: unknown;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
rawStream?: boolean;
|
||||||
|
rawStreamPath?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GatewayRunParams = {
|
type GatewayRunParams = {
|
||||||
@@ -300,6 +302,14 @@ async function runGatewayCommand(
|
|||||||
}
|
}
|
||||||
setGatewayWsLogStyle(wsLogStyle);
|
setGatewayWsLogStyle(wsLogStyle);
|
||||||
|
|
||||||
|
if (opts.rawStream) {
|
||||||
|
process.env.CLAWDBOT_RAW_STREAM = "1";
|
||||||
|
}
|
||||||
|
const rawStreamPath = toOptionString(opts.rawStreamPath);
|
||||||
|
if (rawStreamPath) {
|
||||||
|
process.env.CLAWDBOT_RAW_STREAM_PATH = rawStreamPath;
|
||||||
|
}
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const portOverride = parsePort(opts.port);
|
const portOverride = parsePort(opts.port);
|
||||||
if (opts.port !== undefined && portOverride === null) {
|
if (opts.port !== undefined && portOverride === null) {
|
||||||
@@ -565,6 +575,8 @@ function addGatewayRunCommand(
|
|||||||
"auto",
|
"auto",
|
||||||
)
|
)
|
||||||
.option("--compact", 'Alias for "--ws-log compact"', false)
|
.option("--compact", 'Alias for "--ws-log compact"', false)
|
||||||
|
.option("--raw-stream", "Log raw model stream events to jsonl", false)
|
||||||
|
.option("--raw-stream-path <path>", "Raw stream jsonl path")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await runGatewayCommand(opts, params);
|
await runGatewayCommand(opts, params);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
} from "../agents/agent-scope.js";
|
} from "../agents/agent-scope.js";
|
||||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
|
import { runClaudeCliAgent } from "../agents/claude-cli-runner.js";
|
||||||
import { lookupContextTokens } from "../agents/context.js";
|
import { lookupContextTokens } from "../agents/context.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONTEXT_TOKENS,
|
DEFAULT_CONTEXT_TOKENS,
|
||||||
@@ -347,7 +348,11 @@ export async function agentCommand(
|
|||||||
const overrideModel = sessionEntry.modelOverride?.trim();
|
const overrideModel = sessionEntry.modelOverride?.trim();
|
||||||
if (overrideModel) {
|
if (overrideModel) {
|
||||||
const key = modelKey(overrideProvider, overrideModel);
|
const key = modelKey(overrideProvider, overrideModel);
|
||||||
if (allowedModelKeys.size > 0 && !allowedModelKeys.has(key)) {
|
if (
|
||||||
|
overrideProvider !== "claude-cli" &&
|
||||||
|
allowedModelKeys.size > 0 &&
|
||||||
|
!allowedModelKeys.has(key)
|
||||||
|
) {
|
||||||
delete sessionEntry.providerOverride;
|
delete sessionEntry.providerOverride;
|
||||||
delete sessionEntry.modelOverride;
|
delete sessionEntry.modelOverride;
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
@@ -362,7 +367,11 @@ export async function agentCommand(
|
|||||||
if (storedModelOverride) {
|
if (storedModelOverride) {
|
||||||
const candidateProvider = storedProviderOverride || defaultProvider;
|
const candidateProvider = storedProviderOverride || defaultProvider;
|
||||||
const key = modelKey(candidateProvider, storedModelOverride);
|
const key = modelKey(candidateProvider, storedModelOverride);
|
||||||
if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) {
|
if (
|
||||||
|
candidateProvider === "claude-cli" ||
|
||||||
|
allowedModelKeys.size === 0 ||
|
||||||
|
allowedModelKeys.has(key)
|
||||||
|
) {
|
||||||
provider = candidateProvider;
|
provider = candidateProvider;
|
||||||
model = storedModelOverride;
|
model = storedModelOverride;
|
||||||
}
|
}
|
||||||
@@ -401,6 +410,7 @@ export async function agentCommand(
|
|||||||
let result: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
let result: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||||
let fallbackProvider = provider;
|
let fallbackProvider = provider;
|
||||||
let fallbackModel = model;
|
let fallbackModel = model;
|
||||||
|
const claudeResumeId = sessionEntry?.claudeCliSessionId?.trim();
|
||||||
try {
|
try {
|
||||||
const messageProvider = resolveMessageProvider(
|
const messageProvider = resolveMessageProvider(
|
||||||
opts.messageProvider,
|
opts.messageProvider,
|
||||||
@@ -410,8 +420,25 @@ export async function agentCommand(
|
|||||||
cfg,
|
cfg,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
run: (providerOverride, modelOverride) =>
|
run: (providerOverride, modelOverride) => {
|
||||||
runEmbeddedPiAgent({
|
if (providerOverride === "claude-cli") {
|
||||||
|
return runClaudeCliAgent({
|
||||||
|
sessionId,
|
||||||
|
sessionKey,
|
||||||
|
sessionFile,
|
||||||
|
workspaceDir,
|
||||||
|
config: cfg,
|
||||||
|
prompt: body,
|
||||||
|
provider: providerOverride,
|
||||||
|
model: modelOverride,
|
||||||
|
thinkLevel: resolvedThinkLevel,
|
||||||
|
timeoutMs,
|
||||||
|
runId,
|
||||||
|
extraSystemPrompt: opts.extraSystemPrompt,
|
||||||
|
resumeSessionId: claudeResumeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return runEmbeddedPiAgent({
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
messageProvider,
|
messageProvider,
|
||||||
@@ -445,7 +472,8 @@ export async function agentCommand(
|
|||||||
data: evt.data,
|
data: evt.data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
result = fallbackResult.result;
|
result = fallbackResult.result;
|
||||||
fallbackProvider = fallbackResult.provider;
|
fallbackProvider = fallbackResult.provider;
|
||||||
@@ -501,6 +529,10 @@ export async function agentCommand(
|
|||||||
model: modelUsed,
|
model: modelUsed,
|
||||||
contextTokens,
|
contextTokens,
|
||||||
};
|
};
|
||||||
|
if (providerUsed === "claude-cli") {
|
||||||
|
const cliSessionId = result.meta.agentMeta?.sessionId?.trim();
|
||||||
|
if (cliSessionId) next.claudeCliSessionId = cliSessionId;
|
||||||
|
}
|
||||||
next.abortedLastRun = result.meta.aborted ?? false;
|
next.abortedLastRun = result.meta.aborted ?? false;
|
||||||
if (hasNonzeroUsage(usage)) {
|
if (hasNonzeroUsage(usage)) {
|
||||||
const input = usage.input ?? 0;
|
const input = usage.input ?? 0;
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export type SessionEntry = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
contextTokens?: number;
|
contextTokens?: number;
|
||||||
compactionCount?: number;
|
compactionCount?: number;
|
||||||
|
claudeCliSessionId?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import { runClaudeCliAgent } from "../agents/claude-cli-runner.js";
|
||||||
import { lookupContextTokens } from "../agents/context.js";
|
import { lookupContextTokens } from "../agents/context.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONTEXT_TOKENS,
|
DEFAULT_CONTEXT_TOKENS,
|
||||||
@@ -422,12 +423,29 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
});
|
});
|
||||||
const messageProvider = resolvedDelivery.provider;
|
const messageProvider = resolvedDelivery.provider;
|
||||||
|
const claudeResumeId = cronSession.sessionEntry.claudeCliSessionId?.trim();
|
||||||
const fallbackResult = await runWithModelFallback({
|
const fallbackResult = await runWithModelFallback({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
run: (providerOverride, modelOverride) =>
|
run: (providerOverride, modelOverride) => {
|
||||||
runEmbeddedPiAgent({
|
if (providerOverride === "claude-cli") {
|
||||||
|
return runClaudeCliAgent({
|
||||||
|
sessionId: cronSession.sessionEntry.sessionId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
sessionFile,
|
||||||
|
workspaceDir,
|
||||||
|
config: params.cfg,
|
||||||
|
prompt: commandBody,
|
||||||
|
provider: providerOverride,
|
||||||
|
model: modelOverride,
|
||||||
|
thinkLevel,
|
||||||
|
timeoutMs,
|
||||||
|
runId: cronSession.sessionEntry.sessionId,
|
||||||
|
resumeSessionId: claudeResumeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return runEmbeddedPiAgent({
|
||||||
sessionId: cronSession.sessionEntry.sessionId,
|
sessionId: cronSession.sessionEntry.sessionId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
messageProvider,
|
messageProvider,
|
||||||
@@ -448,7 +466,8 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
(agentCfg?.verboseDefault as "on" | "off" | undefined),
|
(agentCfg?.verboseDefault as "on" | "off" | undefined),
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
runId: cronSession.sessionEntry.sessionId,
|
runId: cronSession.sessionEntry.sessionId,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
runResult = fallbackResult.result;
|
runResult = fallbackResult.result;
|
||||||
fallbackProvider = fallbackResult.provider;
|
fallbackProvider = fallbackResult.provider;
|
||||||
@@ -473,6 +492,12 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
cronSession.sessionEntry.modelProvider = providerUsed;
|
cronSession.sessionEntry.modelProvider = providerUsed;
|
||||||
cronSession.sessionEntry.model = modelUsed;
|
cronSession.sessionEntry.model = modelUsed;
|
||||||
cronSession.sessionEntry.contextTokens = contextTokens;
|
cronSession.sessionEntry.contextTokens = contextTokens;
|
||||||
|
if (providerUsed === "claude-cli") {
|
||||||
|
const cliSessionId = runResult.meta.agentMeta?.sessionId?.trim();
|
||||||
|
if (cliSessionId) {
|
||||||
|
cronSession.sessionEntry.claudeCliSessionId = cliSessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (hasNonzeroUsage(usage)) {
|
if (hasNonzeroUsage(usage)) {
|
||||||
const input = usage.input ?? 0;
|
const input = usage.input ?? 0;
|
||||||
const output = usage.output ?? 0;
|
const output = usage.output ?? 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user