@botlearn-course/daemon 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/agent-service-client.d.ts +18 -0
  4. package/dist/agent-service-client.js +108 -0
  5. package/dist/auth-store.d.ts +16 -0
  6. package/dist/auth-store.js +106 -0
  7. package/dist/cli.d.ts +24 -0
  8. package/dist/cli.js +354 -0
  9. package/dist/course-client.d.ts +46 -0
  10. package/dist/course-client.js +143 -0
  11. package/dist/doctor.d.ts +15 -0
  12. package/dist/doctor.js +85 -0
  13. package/dist/file-candidates.d.ts +34 -0
  14. package/dist/file-candidates.js +173 -0
  15. package/dist/index.d.ts +19 -0
  16. package/dist/index.js +19 -0
  17. package/dist/log.d.ts +20 -0
  18. package/dist/log.js +154 -0
  19. package/dist/path-env.d.ts +8 -0
  20. package/dist/path-env.js +42 -0
  21. package/dist/redaction.d.ts +24 -0
  22. package/dist/redaction.js +158 -0
  23. package/dist/run-dispatcher.d.ts +43 -0
  24. package/dist/run-dispatcher.js +294 -0
  25. package/dist/run-queue.d.ts +11 -0
  26. package/dist/run-queue.js +26 -0
  27. package/dist/runtime-capabilities.d.ts +3 -0
  28. package/dist/runtime-capabilities.js +42 -0
  29. package/dist/runtime-profile.d.ts +8 -0
  30. package/dist/runtime-profile.js +213 -0
  31. package/dist/runtimes/acp-stream.d.ts +96 -0
  32. package/dist/runtimes/acp-stream.js +488 -0
  33. package/dist/runtimes/claude-code.d.ts +41 -0
  34. package/dist/runtimes/claude-code.js +353 -0
  35. package/dist/runtimes/codex.d.ts +44 -0
  36. package/dist/runtimes/codex.js +332 -0
  37. package/dist/runtimes/deepseek-tui.d.ts +50 -0
  38. package/dist/runtimes/deepseek-tui.js +701 -0
  39. package/dist/runtimes/engine.d.ts +52 -0
  40. package/dist/runtimes/engine.js +127 -0
  41. package/dist/runtimes/fake.d.ts +13 -0
  42. package/dist/runtimes/fake.js +45 -0
  43. package/dist/runtimes/gemini.d.ts +39 -0
  44. package/dist/runtimes/gemini.js +251 -0
  45. package/dist/runtimes/hermes-agent.d.ts +61 -0
  46. package/dist/runtimes/hermes-agent.js +173 -0
  47. package/dist/runtimes/index.d.ts +15 -0
  48. package/dist/runtimes/index.js +74 -0
  49. package/dist/runtimes/kimi.d.ts +35 -0
  50. package/dist/runtimes/kimi.js +335 -0
  51. package/dist/runtimes/ndjson-stream.d.ts +51 -0
  52. package/dist/runtimes/ndjson-stream.js +207 -0
  53. package/dist/runtimes/openclaw-acp.d.ts +52 -0
  54. package/dist/runtimes/openclaw-acp.js +872 -0
  55. package/dist/runtimes/probe.d.ts +17 -0
  56. package/dist/runtimes/probe.js +54 -0
  57. package/dist/runtimes/runtime-errors.d.ts +20 -0
  58. package/dist/runtimes/runtime-errors.js +95 -0
  59. package/dist/runtimes/text-cap.d.ts +7 -0
  60. package/dist/runtimes/text-cap.js +25 -0
  61. package/dist/transcript.d.ts +13 -0
  62. package/dist/transcript.js +46 -0
  63. package/dist/types.d.ts +199 -0
  64. package/dist/types.js +17 -0
  65. package/dist/workspace.d.ts +23 -0
  66. package/dist/workspace.js +54 -0
  67. package/package.json +40 -0
@@ -0,0 +1,353 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { NdjsonStreamAdapter } from "./ndjson-stream.js";
4
+ import { wrapEngineAdapter, } from "./engine.js";
5
+ import { looksLikeRuntimeAuthFailure } from "./runtime-errors.js";
6
+ import { firstExistingPath, readCommandVersion, resolveCommandOnPath, resolveHomePath, } from "./probe.js";
7
+ // 权限姿态:daemon 驱动的 Claude Code run 非交互、无审批中继,且工作区已按 run 隔离,
8
+ // 因此默认 --permission-mode bypassPermissions;需要收紧可用 extraArgs 覆盖。
9
+ const CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
10
+ const CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
11
+ /** 这些 env 会覆盖 ~/.claude 的存量登录凭据;spawn 前必须剔除。 */
12
+ const CLAUDE_CODE_AUTH_ENV_DENYLIST = [
13
+ "ANTHROPIC_API_KEY",
14
+ "ANTHROPIC_AUTH_TOKEN",
15
+ "ANTHROPIC_BASE_URL",
16
+ "ANTHROPIC_CUSTOM_HEADERS",
17
+ "CLAUDE_CODE_OAUTH_TOKEN",
18
+ ];
19
+ export function scrubClaudeCodeAuthEnv(env) {
20
+ const out = { ...env };
21
+ for (const key of CLAUDE_CODE_AUTH_ENV_DENYLIST) {
22
+ delete out[key];
23
+ }
24
+ return out;
25
+ }
26
+ function isValidClaudeSessionId(sessionId) {
27
+ if (sessionId.length === 0 || sessionId.length > 512)
28
+ return false;
29
+ if (sessionId.startsWith("-"))
30
+ return false;
31
+ for (const ch of sessionId) {
32
+ const code = ch.codePointAt(0);
33
+ if (code === undefined || code < 0x20 || code === 0x7f)
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ function invalidClaudeSessionIdError() {
39
+ return "claude-code: invalid sessionId (expected non-control text not starting with '-')";
40
+ }
41
+ const CLAUDE_FOREIGN_FLAGS_WITH_VALUE = new Set([
42
+ "--color",
43
+ "--config",
44
+ "--disable",
45
+ "--enable",
46
+ "--image",
47
+ "--local-provider",
48
+ "--output-last-message",
49
+ "--output-schema",
50
+ "--profile",
51
+ "--sandbox",
52
+ "-i",
53
+ "-o",
54
+ "-p",
55
+ "-s",
56
+ ]);
57
+ const CLAUDE_FOREIGN_BOOLEAN_FLAGS = new Set([
58
+ "--all",
59
+ "--dangerously-bypass-approvals-and-sandbox",
60
+ "--ephemeral",
61
+ "--full-auto",
62
+ "--ignore-rules",
63
+ "--ignore-user-config",
64
+ "--json",
65
+ "--last",
66
+ "--oss",
67
+ "--print",
68
+ "--skip-git-repo-check",
69
+ ]);
70
+ function extraFlagName(arg) {
71
+ if (!arg.startsWith("-"))
72
+ return arg;
73
+ const eq = arg.indexOf("=");
74
+ return eq === -1 ? arg : arg.slice(0, eq);
75
+ }
76
+ function nextExtraValue(args, index) {
77
+ const next = args[index + 1];
78
+ if (typeof next !== "string")
79
+ return undefined;
80
+ if (!next.startsWith("-"))
81
+ return next;
82
+ return /^-\d/.test(next) ? next : undefined;
83
+ }
84
+ export function sanitizeClaudeExtraArgs(extraArgs) {
85
+ if (!extraArgs?.length)
86
+ return [];
87
+ const out = [];
88
+ for (let i = 0; i < extraArgs.length; i += 1) {
89
+ const arg = extraArgs[i];
90
+ const name = extraFlagName(arg);
91
+ // codex 的裸 -c(TOML 覆盖)总带一个值。
92
+ if (arg === "-c") {
93
+ const value = nextExtraValue(extraArgs, i);
94
+ if (value !== undefined)
95
+ i += 1;
96
+ continue;
97
+ }
98
+ if (name === "--config" || name === "--sandbox") {
99
+ if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined)
100
+ i += 1;
101
+ continue;
102
+ }
103
+ if (CLAUDE_FOREIGN_FLAGS_WITH_VALUE.has(name)) {
104
+ if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined)
105
+ i += 1;
106
+ continue;
107
+ }
108
+ if (CLAUDE_FOREIGN_BOOLEAN_FLAGS.has(name)) {
109
+ continue;
110
+ }
111
+ out.push(arg);
112
+ }
113
+ return out;
114
+ }
115
+ /** 通过 PATH 或 macOS 桌面 bundle 兜底解析 Claude Code CLI。 */
116
+ export function resolveClaudeCommand(deps = {}) {
117
+ const onPath = resolveCommandOnPath("claude", deps);
118
+ if (onPath)
119
+ return onPath;
120
+ if ((deps.platform ?? process.platform) !== "darwin")
121
+ return null;
122
+ return firstExistingPath([resolveHomePath(CLAUDE_DESKTOP_CLI_RELATIVE_PATH, deps), CLAUDE_DESKTOP_CLI_SYSTEM_PATH], deps);
123
+ }
124
+ /** 探测 Claude Code CLI 是否安装并读取版本。 */
125
+ export function probeClaude(deps = {}) {
126
+ const command = resolveClaudeCommand(deps);
127
+ if (!command)
128
+ return { available: false };
129
+ return {
130
+ available: true,
131
+ path: command,
132
+ version: readCommandVersion(command, [], deps) ?? undefined,
133
+ };
134
+ }
135
+ /** 用 `claude -p ping` 探测登录态;Claude Code 会把认证失败伪装成 success result。 */
136
+ export function probeClaudeAuth(deps = {}) {
137
+ const command = resolveClaudeCommand(deps);
138
+ if (!command)
139
+ return { checked: false, ok: false, message: "claude command not found" };
140
+ return runClaudeAuthProbe(command, deps);
141
+ }
142
+ function runClaudeAuthProbe(command, deps = {}) {
143
+ const execFn = deps.execFileSyncFn ?? execFileSync;
144
+ const env = scrubClaudeCodeAuthEnv(deps.env ?? process.env);
145
+ try {
146
+ // 新版 claude 要求 --print + stream-json 必须带 --verbose,否则探测恒失败。
147
+ const raw = execFn(command, ["-p", "ping", "--output-format", "stream-json", "--verbose"], {
148
+ stdio: ["ignore", "pipe", "pipe"],
149
+ env,
150
+ timeout: 20_000,
151
+ });
152
+ const output = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
153
+ const authFailure = claudeAuthFailureFromOutput(output);
154
+ if (authFailure)
155
+ return { checked: true, ok: false, message: authFailure };
156
+ return { checked: true, ok: true, message: "claude-code auth ok" };
157
+ }
158
+ catch (err) {
159
+ const e = err;
160
+ const output = `${bufferishToString(e.stdout)}\n${bufferishToString(e.stderr)}`.trim();
161
+ const authFailure = claudeAuthFailureFromOutput(output);
162
+ return {
163
+ checked: true,
164
+ ok: false,
165
+ message: authFailure || e.message || "claude-code auth probe failed",
166
+ };
167
+ }
168
+ }
169
+ function bufferishToString(raw) {
170
+ return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
171
+ }
172
+ function claudeAuthFailureFromOutput(output) {
173
+ for (const line of output.split(/\r?\n/)) {
174
+ const s = line.trim();
175
+ if (!s)
176
+ continue;
177
+ try {
178
+ const obj = JSON.parse(s);
179
+ if (obj.type === "result" &&
180
+ typeof obj.result === "string" &&
181
+ looksLikeRuntimeAuthFailure(obj.result)) {
182
+ return obj.result;
183
+ }
184
+ }
185
+ catch {
186
+ if (looksLikeRuntimeAuthFailure(s))
187
+ return s;
188
+ }
189
+ }
190
+ return looksLikeRuntimeAuthFailure(output) ? output : null;
191
+ }
192
+ /**
193
+ * Claude Code adapter — spawn `claude -p "<text>" --output-format stream-json`
194
+ * (有会话时加 `--resume <sid>`)并解析 ndjson 流。
195
+ *
196
+ * stream-json 形状(节选):
197
+ * {type:"system", subtype:"init", session_id:"...", ...}
198
+ * {type:"assistant", message:{content:[{type:"text", text:"..."} | {type:"tool_use", ...}]}}
199
+ * {type:"user", message:{content:[{type:"tool_result", ...}]}}
200
+ * {type:"result", subtype:"success", session_id:"...", total_cost_usd: 0.01, result:"final text"}
201
+ */
202
+ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
203
+ id = "claude-code";
204
+ explicitBinary;
205
+ resolvedBinary = null;
206
+ constructor(opts) {
207
+ super(opts?.logger);
208
+ this.explicitBinary = opts?.binary ?? process.env.BOTLEARN_CLAUDE_BIN;
209
+ }
210
+ async run(opts) {
211
+ // 非法 sessionId 不抛:返回空 newSessionId 作为「删除存量会话」信号。
212
+ if (opts.sessionId && !isValidClaudeSessionId(opts.sessionId)) {
213
+ return { text: "", newSessionId: "", error: invalidClaudeSessionIdError() };
214
+ }
215
+ return super.run(opts);
216
+ }
217
+ resolveBinary() {
218
+ if (this.explicitBinary)
219
+ return this.explicitBinary;
220
+ if (this.resolvedBinary)
221
+ return this.resolvedBinary;
222
+ this.resolvedBinary = resolveClaudeCommand() ?? "claude";
223
+ return this.resolvedBinary;
224
+ }
225
+ buildArgs(opts) {
226
+ const extraArgs = sanitizeClaudeExtraArgs(opts.extraArgs);
227
+ const args = ["-p", opts.text, "--output-format", "stream-json", "--verbose"];
228
+ // headless `-p` 默认不加载项目 .claude/;显式 opt-in 让工作区内种子 skills 可见。
229
+ if (!extraArgs.some((a) => a.startsWith("--setting-sources"))) {
230
+ args.push("--setting-sources", "project");
231
+ }
232
+ if (opts.sessionId) {
233
+ if (!isValidClaudeSessionId(opts.sessionId))
234
+ throw new Error(invalidClaudeSessionIdError());
235
+ args.push("--resume", opts.sessionId);
236
+ }
237
+ if (!extraArgs.some((a) => a.startsWith("--permission-mode"))) {
238
+ args.push("--permission-mode", "bypassPermissions");
239
+ }
240
+ // --append-system-prompt 按次生效、不进 resume transcript。
241
+ const systemPrompt = opts.systemContext?.trim() ? opts.systemContext : undefined;
242
+ if (systemPrompt && !extraArgs.includes("--append-system-prompt")) {
243
+ args.push("--append-system-prompt", systemPrompt);
244
+ }
245
+ if (extraArgs.length)
246
+ args.push(...extraArgs);
247
+ return args;
248
+ }
249
+ spawnEnv(opts) {
250
+ return scrubClaudeCodeAuthEnv(super.spawnEnv(opts));
251
+ }
252
+ handleEvent(raw, ctx) {
253
+ const obj = raw;
254
+ const status = claudeStatusEvent(obj);
255
+ if (status)
256
+ ctx.emitStatus(status);
257
+ ctx.emitBlock(normalizeBlock(obj, ctx.seq));
258
+ if (obj.type === "system" && obj.session_id) {
259
+ ctx.state.newSessionId = String(obj.session_id);
260
+ return;
261
+ }
262
+ if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
263
+ for (const c of obj.message.content) {
264
+ if (c?.type === "text" && typeof c.text === "string") {
265
+ ctx.appendAssistantText(c.text);
266
+ }
267
+ }
268
+ return;
269
+ }
270
+ if (obj.type === "result") {
271
+ if (typeof obj.total_cost_usd === "number")
272
+ ctx.state.costUsd = obj.total_cost_usd;
273
+ if (obj.subtype === "success") {
274
+ const result = typeof obj.result === "string" ? obj.result : "";
275
+ // 认证失败会以 cost=0 的 success result 出现,必须重新归类为错误。
276
+ const looksLikeAuthFailure = obj.total_cost_usd === 0 && looksLikeRuntimeAuthFailure(result);
277
+ if (looksLikeAuthFailure) {
278
+ this.log.error("claude-code authentication failed; check ~/.claude login or unset stale Anthropic env vars", { error: result });
279
+ ctx.state.newSessionId = "";
280
+ ctx.state.finalText = "";
281
+ ctx.state.assistantTextChunks = [];
282
+ ctx.state.assistantTextBytes = 0;
283
+ ctx.state.errorText = result;
284
+ }
285
+ else {
286
+ if (typeof obj.session_id === "string")
287
+ ctx.state.newSessionId = obj.session_id;
288
+ if (typeof obj.result === "string")
289
+ ctx.state.finalText = obj.result;
290
+ }
291
+ }
292
+ else {
293
+ // 非 success(如 resume 目标缺失):CLI 仍会为空会话发新 session_id,
294
+ // 持久化它会永远 resume 一个无用 UUID —— 清空让上层删除存量条目。
295
+ // CLI 同时非零退出,obj.result 缺失时基类会从 stderr 合成 errorText。
296
+ ctx.state.newSessionId = "";
297
+ if (typeof obj.result === "string")
298
+ ctx.state.errorText = obj.result;
299
+ }
300
+ }
301
+ }
302
+ }
303
+ function claudeStatusEvent(obj) {
304
+ if (obj.type === "system" && obj.subtype === "init") {
305
+ return { kind: "thinking", phase: "started", label: "Starting session" };
306
+ }
307
+ // assistant 事件可能混合 text 与 tool_use:有 text 视为 thinking 结束,
308
+ // 只有 tool_use 时用工具名作为 label。
309
+ if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
310
+ const contents = obj.message.content;
311
+ const hasText = contents.some((c) => c?.type === "text" && typeof c.text === "string" && c.text.length > 0);
312
+ if (hasText)
313
+ return { kind: "thinking", phase: "stopped" };
314
+ const tool = contents.find((c) => c?.type === "tool_use");
315
+ if (tool) {
316
+ const name = typeof tool.name === "string" && tool.name ? tool.name : "tool";
317
+ return { kind: "thinking", phase: "updated", label: name };
318
+ }
319
+ }
320
+ if (obj.type === "result") {
321
+ return { kind: "thinking", phase: "stopped" };
322
+ }
323
+ return undefined;
324
+ }
325
+ function normalizeBlock(obj, seq) {
326
+ let kind = "other";
327
+ if (obj?.type === "assistant") {
328
+ const contents = Array.isArray(obj.message?.content) ? obj.message.content : [];
329
+ if (contents.some((c) => c?.type === "tool_use"))
330
+ kind = "tool_use";
331
+ else if (contents.some((c) => c?.type === "text"))
332
+ kind = "assistant_text";
333
+ }
334
+ else if (obj?.type === "user") {
335
+ const contents = Array.isArray(obj.message?.content) ? obj.message.content : [];
336
+ if (contents.some((c) => c?.type === "tool_result"))
337
+ kind = "tool_result";
338
+ }
339
+ else if (obj?.type === "system") {
340
+ kind = "system";
341
+ }
342
+ return { raw: obj, kind, seq };
343
+ }
344
+ export const claudeCodeModule = {
345
+ id: "claude-code",
346
+ displayName: "Claude Code",
347
+ binary: "claude",
348
+ envVar: "BOTLEARN_CLAUDE_BIN",
349
+ probe: async () => probeClaude(),
350
+ // auth 探测会真实执行一次 `claude -p ping`(慢且消耗 token),只留给 doctor 按需调用。
351
+ probeAuth: async () => probeClaudeAuth(),
352
+ create: () => wrapEngineAdapter("claude-code", new ClaudeCodeAdapter()),
353
+ };
@@ -0,0 +1,44 @@
1
+ import { NdjsonStreamAdapter, type NdjsonEventCtx } from "./ndjson-stream.js";
2
+ import { type EngineRunOptions } from "./engine.js";
3
+ import { type ProbeDeps } from "./probe.js";
4
+ import type { Logger } from "../log.js";
5
+ import type { RuntimeModule, RuntimeProbe } from "../types.js";
6
+ export declare function sanitizeCodexExtraArgs(extraArgs: string[] | undefined): string[];
7
+ /** 通过 PATH 或 macOS 桌面 bundle 解析 Codex CLI 可执行文件。 */
8
+ export declare function resolveCodexCommand(deps?: ProbeDeps): string | null;
9
+ /** 探测 Codex CLI 是否安装并读取版本。 */
10
+ export declare function probeCodex(deps?: ProbeDeps): RuntimeProbe;
11
+ /**
12
+ * Codex adapter — spawn `codex exec [resume <sid>] --json ...` 并解析 JSONL 事件流。
13
+ *
14
+ * 事件形状(节选):
15
+ * {"type":"thread.started","thread_id":"<uuid>"}
16
+ * {"type":"turn.started"}
17
+ * {"type":"item.started","item":{"type":"command_execution", ...}}
18
+ * {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
19
+ * {"type":"turn.completed","usage":{...}}
20
+ *
21
+ * `codex exec` 不报告 USD 成本,只报 token 用量,所以 costUsd 恒空。
22
+ * systemContext 走 prompt 前缀(Codex 没有 --append-system-prompt;本包单轮
23
+ * 执行、不复用 transcript,前缀不会跨轮累积)。
24
+ */
25
+ export declare class CodexAdapter extends NdjsonStreamAdapter {
26
+ readonly id: "codex";
27
+ private readonly explicitBinary;
28
+ private resolvedBinary;
29
+ constructor(opts?: {
30
+ binary?: string;
31
+ logger?: Logger;
32
+ });
33
+ run(opts: EngineRunOptions): Promise<import("./engine.js").EngineRunResult>;
34
+ protected resolveBinary(): string;
35
+ /**
36
+ * 新会话:`exec <tail> -- <prompt>`;resume:`exec resume <sid> <tail> -- <prompt>`。
37
+ * 两条路径共享同一个 tail(sandbox/approval 用 -c 表达以兼容 resume)。
38
+ * `--` 隔开 flags 与 positionals,防止以 `-` 开头的 prompt 被解析成选项。
39
+ */
40
+ protected buildArgs(opts: EngineRunOptions): string[];
41
+ protected spawnEnv(_opts: EngineRunOptions): NodeJS.ProcessEnv;
42
+ protected handleEvent(raw: unknown, ctx: NdjsonEventCtx): void;
43
+ }
44
+ export declare const codexModule: RuntimeModule;