@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,332 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { NdjsonStreamAdapter } from "./ndjson-stream.js";
5
+ import { wrapEngineAdapter, } from "./engine.js";
6
+ import { firstExistingPath, readCommandVersion, resolveCommandOnPath, } from "./probe.js";
7
+ // 权限姿态:daemon 驱动的 Codex run 非交互、无审批中继,且每个 run 的工作区已隔离,
8
+ // 因此默认 danger-full-access + approval_policy=never;需要收紧可用 extraArgs 覆盖。
9
+ const CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
10
+ /** Codex 会话 id 是 36 位带连字符的 UUID;拒绝其他形状以保证 argv 安全。 */
11
+ const CODEX_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
12
+ const CODEX_FOREIGN_EXTRA_FLAGS_WITH_VALUE = new Set([
13
+ "--append-system-prompt",
14
+ "--permission-mode",
15
+ ]);
16
+ const CODEX_SANDBOX_MODES = new Set(["read-only", "workspace-write", "danger-full-access"]);
17
+ function extraFlagName(arg) {
18
+ if (!arg.startsWith("-"))
19
+ return arg;
20
+ const eq = arg.indexOf("=");
21
+ return eq === -1 ? arg : arg.slice(0, eq);
22
+ }
23
+ /** 把不可信 JSON 值收敛为有限非负数,否则 undefined。 */
24
+ function numOrUndefined(value) {
25
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
26
+ }
27
+ function nextExtraValue(args, index) {
28
+ const next = args[index + 1];
29
+ if (typeof next !== "string")
30
+ return undefined;
31
+ if (!next.startsWith("-"))
32
+ return next;
33
+ return /^-\d/.test(next) ? next : undefined;
34
+ }
35
+ export function sanitizeCodexExtraArgs(extraArgs) {
36
+ if (!extraArgs?.length)
37
+ return [];
38
+ const out = [];
39
+ for (let i = 0; i < extraArgs.length; i += 1) {
40
+ const arg = extraArgs[i];
41
+ const name = extraFlagName(arg);
42
+ if (CODEX_FOREIGN_EXTRA_FLAGS_WITH_VALUE.has(name)) {
43
+ if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined)
44
+ i += 1;
45
+ continue;
46
+ }
47
+ // `codex exec resume` 不接受 -s/--sandbox;改写成两个子命令都接受的 -c 覆盖。
48
+ if (name === "-s" || name === "--sandbox") {
49
+ const value = arg.includes("=")
50
+ ? arg.slice(arg.indexOf("=") + 1)
51
+ : nextExtraValue(extraArgs, i);
52
+ if (!arg.includes("=") && value !== undefined)
53
+ i += 1;
54
+ if (value && CODEX_SANDBOX_MODES.has(value)) {
55
+ out.push("-c", `sandbox_mode="${value}"`);
56
+ }
57
+ continue;
58
+ }
59
+ if (arg === "--full-auto") {
60
+ out.push("--dangerously-bypass-approvals-and-sandbox");
61
+ continue;
62
+ }
63
+ out.push(arg);
64
+ }
65
+ return out;
66
+ }
67
+ function hasCodexSandboxOverride(args) {
68
+ for (let i = 0; i < args.length; i += 1) {
69
+ const arg = args[i];
70
+ if (arg === "--dangerously-bypass-approvals-and-sandbox" ||
71
+ arg.startsWith("-c sandbox_mode=") ||
72
+ arg.startsWith("-csandbox_mode=") ||
73
+ arg.startsWith("--config=sandbox_mode=")) {
74
+ return true;
75
+ }
76
+ if ((arg === "-c" || arg === "--config") && args[i + 1]?.startsWith("sandbox_mode=")) {
77
+ return true;
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ /** 通过 PATH 或 macOS 桌面 bundle 解析 Codex CLI 可执行文件。 */
83
+ export function resolveCodexCommand(deps = {}) {
84
+ const onPath = resolveCommandOnPath("codex", deps);
85
+ if (onPath)
86
+ return onPath;
87
+ return firstExistingPath([CODEX_DESKTOP_BUNDLE_PATH], deps);
88
+ }
89
+ function resolveCodexGlobalNpmEntry() {
90
+ try {
91
+ const globalRoot = execFileSync("npm", ["root", "-g"], {
92
+ encoding: "utf8",
93
+ stdio: ["ignore", "pipe", "ignore"],
94
+ timeout: 5000,
95
+ }).trim();
96
+ if (!globalRoot)
97
+ return null;
98
+ const candidate = path.join(globalRoot, "@openai", "codex", "bin", "codex.js");
99
+ return existsSync(candidate) ? candidate : null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ /** 探测 Codex CLI 是否安装并读取版本。 */
106
+ export function probeCodex(deps = {}) {
107
+ const command = resolveCodexCommand(deps);
108
+ if (command) {
109
+ return {
110
+ available: true,
111
+ path: command,
112
+ version: readCommandVersion(command, [], deps) ?? undefined,
113
+ };
114
+ }
115
+ // npm 全局安装但 bin 不在 PATH 上:报告 .js 入口,但 resolveBinary 不用它
116
+ // (.js 无法直接 spawn)。
117
+ const npmEntry = resolveCodexGlobalNpmEntry();
118
+ if (npmEntry) {
119
+ return {
120
+ available: true,
121
+ path: npmEntry,
122
+ version: readCommandVersion(process.execPath, [npmEntry], deps) ?? undefined,
123
+ };
124
+ }
125
+ return { available: false };
126
+ }
127
+ /**
128
+ * Codex adapter — spawn `codex exec [resume <sid>] --json ...` 并解析 JSONL 事件流。
129
+ *
130
+ * 事件形状(节选):
131
+ * {"type":"thread.started","thread_id":"<uuid>"}
132
+ * {"type":"turn.started"}
133
+ * {"type":"item.started","item":{"type":"command_execution", ...}}
134
+ * {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
135
+ * {"type":"turn.completed","usage":{...}}
136
+ *
137
+ * `codex exec` 不报告 USD 成本,只报 token 用量,所以 costUsd 恒空。
138
+ * systemContext 走 prompt 前缀(Codex 没有 --append-system-prompt;本包单轮
139
+ * 执行、不复用 transcript,前缀不会跨轮累积)。
140
+ */
141
+ export class CodexAdapter extends NdjsonStreamAdapter {
142
+ id = "codex";
143
+ explicitBinary;
144
+ resolvedBinary = null;
145
+ constructor(opts) {
146
+ super(opts?.logger);
147
+ this.explicitBinary = opts?.binary ?? process.env.BOTLEARN_CODEX_BIN;
148
+ }
149
+ async run(opts) {
150
+ if (opts.sessionId && !CODEX_SESSION_ID_RE.test(opts.sessionId)) {
151
+ throw new Error(`codex: invalid sessionId "${opts.sessionId}" (expected UUID)`);
152
+ }
153
+ return super.run(opts);
154
+ }
155
+ resolveBinary() {
156
+ if (this.explicitBinary)
157
+ return this.explicitBinary;
158
+ if (this.resolvedBinary)
159
+ return this.resolvedBinary;
160
+ this.resolvedBinary = resolveCodexCommand() ?? "codex";
161
+ return this.resolvedBinary;
162
+ }
163
+ /**
164
+ * 新会话:`exec <tail> -- <prompt>`;resume:`exec resume <sid> <tail> -- <prompt>`。
165
+ * 两条路径共享同一个 tail(sandbox/approval 用 -c 表达以兼容 resume)。
166
+ * `--` 隔开 flags 与 positionals,防止以 `-` 开头的 prompt 被解析成选项。
167
+ */
168
+ buildArgs(opts) {
169
+ const tail = [];
170
+ const extraArgs = sanitizeCodexExtraArgs(opts.extraArgs);
171
+ const hasSandboxOverride = hasCodexSandboxOverride(extraArgs);
172
+ if (!hasSandboxOverride) {
173
+ tail.push("-c", 'sandbox_mode="danger-full-access"', "-c", 'approval_policy="never"');
174
+ }
175
+ tail.push("--skip-git-repo-check", "--json");
176
+ if (extraArgs.length)
177
+ tail.push(...extraArgs);
178
+ const prompt = composeCodexPrompt(opts.text, opts.systemContext);
179
+ if (opts.sessionId) {
180
+ return ["exec", "resume", opts.sessionId, ...tail, "--", prompt];
181
+ }
182
+ return ["exec", ...tail, "--", prompt];
183
+ }
184
+ spawnEnv(_opts) {
185
+ return {
186
+ ...process.env,
187
+ // 保证 JSONL 输出不混入 ANSI 转义。
188
+ FORCE_COLOR: "0",
189
+ NO_COLOR: "1",
190
+ };
191
+ }
192
+ handleEvent(raw, ctx) {
193
+ const obj = raw;
194
+ const status = codexStatusEvent(obj);
195
+ if (status)
196
+ ctx.emitStatus(status);
197
+ ctx.emitBlock(normalizeBlock(obj, ctx.seq));
198
+ if (obj.type === "thread.started") {
199
+ if (typeof obj.thread_id === "string") {
200
+ ctx.state.newSessionId = obj.thread_id;
201
+ }
202
+ return;
203
+ }
204
+ if (obj.type === "item.completed" && obj.item?.type === "agent_message") {
205
+ if (typeof obj.item.text === "string") {
206
+ ctx.appendAssistantText(obj.item.text);
207
+ // 最后一条 agent_message 即最终回复。
208
+ ctx.state.finalText = obj.item.text;
209
+ }
210
+ return;
211
+ }
212
+ if (obj.type === "turn.completed") {
213
+ // usage 成功与失败的 turn 都会报。input_tokens 含缓存部分,
214
+ // miss = input_tokens - cached_input_tokens。仅本地诊断用。
215
+ const usage = obj.usage;
216
+ if (usage && typeof usage === "object") {
217
+ const input = numOrUndefined(usage.input_tokens);
218
+ const cached = numOrUndefined(usage.cached_input_tokens);
219
+ const output = numOrUndefined(usage.output_tokens);
220
+ const hit = cached;
221
+ const miss = input !== undefined ? Math.max(0, input - (cached ?? 0)) : undefined;
222
+ if (hit !== undefined || miss !== undefined || output !== undefined) {
223
+ ctx.state.usage = {
224
+ ...(hit !== undefined ? { inputCacheHitTokens: hit } : {}),
225
+ ...(miss !== undefined ? { inputCacheMissTokens: miss } : {}),
226
+ ...(output !== undefined ? { outputTokens: output } : {}),
227
+ };
228
+ }
229
+ }
230
+ if (obj.turn?.status === "failed") {
231
+ ctx.state.errorText =
232
+ obj.turn.error?.message?.trim() || summarizeCodexErrorEvent(raw) || "codex turn failed";
233
+ }
234
+ return;
235
+ }
236
+ if (obj.type === "error") {
237
+ const err = obj.error;
238
+ const fromMessage = typeof err === "string" ? err.trim() : (err?.message?.trim() ?? "");
239
+ ctx.state.errorText = fromMessage || summarizeCodexErrorEvent(raw) || "codex error";
240
+ }
241
+ }
242
+ }
243
+ /** systemContext 前缀 + 分隔线;空/纯空白时原样返回 text。 */
244
+ function composeCodexPrompt(text, systemContext) {
245
+ const sc = systemContext?.trim();
246
+ if (!sc)
247
+ return text;
248
+ return `${sc}\n\n---\n\n${text}`;
249
+ }
250
+ /**
251
+ * codex `error` / 失败 `turn.completed` 事件的兜底摘要:error.message 缺失时
252
+ * 用紧凑 JSON 代替不可诊断的 "codex error",截断防失控。
253
+ */
254
+ function summarizeCodexErrorEvent(raw) {
255
+ try {
256
+ const json = JSON.stringify(raw);
257
+ if (!json || json === "{}" || json === "null")
258
+ return "";
259
+ return json.length > 800 ? `${json.slice(0, 800)}…` : json;
260
+ }
261
+ catch {
262
+ return "";
263
+ }
264
+ }
265
+ function codexStatusEvent(obj) {
266
+ if (obj.type === "thread.started") {
267
+ return { kind: "thinking", phase: "started", label: "Starting session" };
268
+ }
269
+ if (obj.type === "turn.started") {
270
+ return { kind: "thinking", phase: "started", label: "Thinking" };
271
+ }
272
+ if (obj.type === "item.started" && typeof obj.item?.type === "string") {
273
+ const tool = obj.item.type;
274
+ if (tool === "command_execution" ||
275
+ tool === "file_change" ||
276
+ tool === "mcp_tool_call" ||
277
+ tool === "web_search") {
278
+ return { kind: "thinking", phase: "updated", label: codexToolLabel(tool) };
279
+ }
280
+ }
281
+ if (obj.type === "item.completed" && obj.item?.type === "agent_message") {
282
+ return { kind: "thinking", phase: "stopped" };
283
+ }
284
+ if (obj.type === "turn.completed") {
285
+ return { kind: "thinking", phase: "stopped" };
286
+ }
287
+ return undefined;
288
+ }
289
+ function codexToolLabel(tool) {
290
+ switch (tool) {
291
+ case "command_execution":
292
+ return "Running command";
293
+ case "file_change":
294
+ return "Editing files";
295
+ case "mcp_tool_call":
296
+ return "Calling tool";
297
+ case "web_search":
298
+ return "Searching web";
299
+ default:
300
+ return tool;
301
+ }
302
+ }
303
+ function normalizeBlock(obj, seq) {
304
+ let kind = "other";
305
+ const type = obj?.type;
306
+ const itemType = obj?.item?.type;
307
+ if (type === "thread.started" || type === "turn.started" || type === "turn.completed") {
308
+ kind = "system";
309
+ }
310
+ else if (type === "item.completed" && itemType === "agent_message") {
311
+ kind = "assistant_text";
312
+ }
313
+ else if (type === "item.started" || type === "item.completed") {
314
+ if (itemType === "command_execution" ||
315
+ itemType === "file_change" ||
316
+ itemType === "mcp_tool_call" ||
317
+ itemType === "web_search") {
318
+ kind = type === "item.completed" ? "tool_result" : "tool_use";
319
+ }
320
+ }
321
+ return { raw: obj, kind, seq };
322
+ }
323
+ export const codexModule = {
324
+ id: "codex",
325
+ displayName: "Codex CLI",
326
+ binary: "codex",
327
+ probe: async () => probeCodex(),
328
+ create: () => wrapEngineAdapter("codex", new CodexAdapter(), {
329
+ // codex 没有一等 --model flag;模型走 -c 覆盖(值带引号,与 sandbox_mode 一致)。
330
+ modelArgs: (m) => ["-c", `model="${m}"`],
331
+ }),
332
+ };
@@ -0,0 +1,50 @@
1
+ import { spawn } from "node:child_process";
2
+ import { type ProbeDeps } from "./probe.js";
3
+ import { type EngineAdapter, type EngineRunOptions, type EngineRunResult } from "./engine.js";
4
+ import type { RuntimeModule, RuntimeProbe } from "../types.js";
5
+ interface DeepseekAdapterDeps {
6
+ binary?: string;
7
+ /** 测试注入:使用现成的兼容 server,不 spawn `deepseek`。 */
8
+ serverUrl?: string;
9
+ authToken?: string;
10
+ fetchFn?: typeof fetch;
11
+ spawnFn?: typeof spawn;
12
+ }
13
+ /** 解析 PATH 上的 `deepseek` dispatcher CLI。 */
14
+ export declare function resolveDeepseekCommand(deps?: ProbeDeps): string | null;
15
+ export declare function probeDeepseekTui(deps?: ProbeDeps): RuntimeProbe;
16
+ /**
17
+ * DeepSeek TUI 引擎。驱动 `deepseek serve --http` 暴露的 headless runtime
18
+ * API(HTTP/SSE),不是交互式 TUI 也不是 ACP —— HTTP/SSE 是其文档化的完整
19
+ * runtime 面;ACP 目前只是保守的编辑器基线。
20
+ *
21
+ * 权限姿态:course run 在 owner 本机的隔离工作区执行,线程一律
22
+ * `allow_shell/trust_mode/auto_approve = true`(owner 信任,无审批人可交互)。
23
+ */
24
+ export declare class DeepseekTuiAdapter implements EngineAdapter {
25
+ readonly id: "deepseek-tui";
26
+ private readonly explicitBinary;
27
+ private readonly explicitServerUrl;
28
+ private readonly explicitAuthToken;
29
+ private readonly fetchFn;
30
+ private readonly spawnFn;
31
+ private resolvedBinary;
32
+ constructor(deps?: DeepseekAdapterDeps);
33
+ run(opts: EngineRunOptions): Promise<EngineRunResult>;
34
+ private resolveBinary;
35
+ private acquireHandle;
36
+ /**
37
+ * 不设置 DEEPSEEK_RUNTIME_DIR:server 跨 run 池化共享,per-run 目录不成立;
38
+ * BYOA 直接用用户本机 deepseek 自身的默认状态目录(含已登录凭据)。
39
+ */
40
+ private spawnEnv;
41
+ private createThread;
42
+ private patchThreadSystemContext;
43
+ private startTurnAndReadEvents;
44
+ private readEvents;
45
+ private requestJson;
46
+ }
47
+ /** 仅测试用:清空进程池。 */
48
+ export declare function __resetDeepseekTuiPoolForTests(): void;
49
+ export declare const deepseekTuiModule: RuntimeModule;
50
+ export {};