mocode-ai 1.6.5 → 1.6.7

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 (45) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +57 -30
  3. package/dist/agent/run-coordinator.js +2 -1
  4. package/dist/agent/tool-turn.js +2 -0
  5. package/dist/bots/bus.js +117 -0
  6. package/dist/bots/cli.js +102 -0
  7. package/dist/bots/store.js +100 -0
  8. package/dist/config/index.js +2 -1
  9. package/dist/headless.js +302 -0
  10. package/dist/index.js +171 -3
  11. package/dist/jobs/approval.js +153 -0
  12. package/dist/jobs/approve-cli.js +30 -0
  13. package/dist/jobs/attach.js +61 -0
  14. package/dist/jobs/checkpoint.js +33 -0
  15. package/dist/jobs/launch.js +107 -0
  16. package/dist/jobs/runner.js +59 -0
  17. package/dist/jobs/store.js +101 -0
  18. package/dist/jobs/worktree.js +63 -0
  19. package/dist/notify/index.js +144 -0
  20. package/dist/repl/commands/bots.js +32 -0
  21. package/dist/repl/commands/jobs.js +109 -0
  22. package/dist/repl/commands/model.js +5 -20
  23. package/dist/repl/commands/registry.js +6 -0
  24. package/dist/repl/commands/schedule.js +37 -0
  25. package/dist/repl/commands.js +3 -58
  26. package/dist/schedule/cli.js +114 -0
  27. package/dist/schedule/cron.js +79 -0
  28. package/dist/schedule/daemon.js +166 -0
  29. package/dist/schedule/store.js +98 -0
  30. package/dist/session/scheduler.js +0 -3
  31. package/dist/session/store.js +1 -1
  32. package/dist/tools/builtins/arena.js +156 -0
  33. package/dist/tools/builtins/glob.js +64 -16
  34. package/dist/tools/builtins/grep.js +176 -122
  35. package/dist/tools/builtins/index.js +13 -2
  36. package/dist/tools/builtins/message-bus.js +98 -0
  37. package/dist/tools/builtins/run-command.js +2 -1
  38. package/dist/tools/constants.js +11 -0
  39. package/dist/tools/ripgrep-client.js +180 -0
  40. package/dist/ui/batch.js +31 -1
  41. package/dist/ui/composer.js +4 -18
  42. package/dist/ui/fuzzy-picker.js +1 -3
  43. package/dist/ui/intervention.js +6 -0
  44. package/dist/ui/prompt-internal/editor.js +10 -27
  45. package/package.json +3 -2
@@ -0,0 +1,117 @@
1
+ // D1 持久 Bot 消息总线:bot 之间(以及 main 用户与 bot 之间)跨时间的异步消息。
2
+ // 与同步 sub-agent 的区别:收发双方不必同时在线——消息落盘,worker bot 之后由
3
+ // scheduler / bg 唤起时拉 inbox 处理并回结果。
4
+ //
5
+ // 存储(<sandboxRoot>/.mocode/bus/):
6
+ // <msgId>.json 一条消息(只创建、永不改写 → 并发安全,无丢消息)
7
+ // <receiver>.read.json 该接收者已读消息 id 数组(低频 RMW,原子写)
8
+ //
9
+ // 身份用 AsyncLocalStorage 注入(runAsIdentity):headless --bot X 时整段运行为 X,
10
+ // 其内派生的 sub-agent 自动继承;缺省身份为 'main'(用户主会话)。
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { AsyncLocalStorage } from 'node:async_hooks';
14
+ import { randomBytes } from 'node:crypto';
15
+ import { getSandboxRoot } from '../sandbox/index.js';
16
+ export const DEFAULT_IDENTITY = 'main';
17
+ const identityStorage = new AsyncLocalStorage();
18
+ /** 在指定 bot 身份下执行 fn;子 agent / 工具内 getIdentity() 都返回该身份。 */
19
+ export function runAsIdentity(identity, fn) {
20
+ return identityStorage.run(identity, fn);
21
+ }
22
+ export function getIdentity() {
23
+ return identityStorage.getStore() ?? DEFAULT_IDENTITY;
24
+ }
25
+ function busRoot() {
26
+ return path.join(getSandboxRoot() ?? process.cwd(), '.mocode', 'bus');
27
+ }
28
+ function messagePath(id) {
29
+ return path.join(busRoot(), `${id}.json`);
30
+ }
31
+ function readMarkerPath(receiver) {
32
+ // receiver 只允许安全文件名段,杜绝路径穿越。
33
+ const safe = receiver.replace(/[^a-zA-Z0-9_.-]/g, '_');
34
+ return path.join(busRoot(), `${safe}.read.json`);
35
+ }
36
+ export function sendMessage(input) {
37
+ const to = input.to.trim();
38
+ const body = input.body;
39
+ if (!to)
40
+ throw new Error('message recipient (to) is required');
41
+ if (!body)
42
+ throw new Error('message body is required');
43
+ const msg = {
44
+ id: `msg-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}`,
45
+ from: (input.from ?? getIdentity()).trim() || DEFAULT_IDENTITY,
46
+ to,
47
+ body,
48
+ createdAt: new Date().toISOString(),
49
+ ...(input.inReplyTo ? { inReplyTo: input.inReplyTo } : {}),
50
+ };
51
+ fs.mkdirSync(busRoot(), { recursive: true });
52
+ const target = messagePath(msg.id);
53
+ fs.writeFileSync(`${target}.tmp`, `${JSON.stringify(msg, null, 2)}\n`, 'utf8');
54
+ fs.renameSync(`${target}.tmp`, target);
55
+ return msg;
56
+ }
57
+ function getReadSet(receiver) {
58
+ try {
59
+ const parsed = JSON.parse(fs.readFileSync(readMarkerPath(receiver), 'utf8'));
60
+ return new Set(Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : []);
61
+ }
62
+ catch {
63
+ return new Set();
64
+ }
65
+ }
66
+ function saveReadSet(receiver, set) {
67
+ const target = readMarkerPath(receiver);
68
+ fs.writeFileSync(`${target}.tmp`, `${JSON.stringify([...set], null, 2)}\n`, 'utf8');
69
+ fs.renameSync(`${target}.tmp`, target);
70
+ }
71
+ function allMessages() {
72
+ let files;
73
+ try {
74
+ files = fs.readdirSync(busRoot());
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ const out = [];
80
+ for (const f of files) {
81
+ if (!f.endsWith('.json') || f.endsWith('.read.json'))
82
+ continue;
83
+ try {
84
+ out.push(JSON.parse(fs.readFileSync(path.join(busRoot(), f), 'utf8')));
85
+ }
86
+ catch {
87
+ // skip corrupt
88
+ }
89
+ }
90
+ return out.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
91
+ }
92
+ /** 某身份的未读收件箱。 */
93
+ export function inbox(identity = getIdentity()) {
94
+ const read = getReadSet(identity);
95
+ return allMessages().filter((m) => m.to === identity && !read.has(m.id));
96
+ }
97
+ /** 标记消息已读(ack)。只能 ack 发给自己的消息。 */
98
+ export function markRead(ids, identity = getIdentity()) {
99
+ const read = getReadSet(identity);
100
+ const mine = new Set(allMessages()
101
+ .filter((m) => m.to === identity)
102
+ .map((m) => m.id));
103
+ const marked = [];
104
+ for (const id of ids) {
105
+ if (mine.has(id) && !read.has(id)) {
106
+ read.add(id);
107
+ marked.push(id);
108
+ }
109
+ }
110
+ if (marked.length)
111
+ saveReadSet(identity, read);
112
+ return marked;
113
+ }
114
+ /** 与某身份相关的全部消息(发出或收到),按时间排序。 */
115
+ export function messageHistory(identity = getIdentity()) {
116
+ return allMessages().filter((m) => m.from === identity || m.to === identity);
117
+ }
@@ -0,0 +1,102 @@
1
+ // `mocode bots …` 管理:
2
+ // add --name <n> (--prompt "<text>" | --prompt-file <path>) [--description <d>]
3
+ // [--tools "a,b,c"] [--sandbox-path <p>] [--global]
4
+ // list | show <name> | rm <name> [--global]
5
+ // 作用域默认项目(.mocode/bots);--global 写 ~/.mocode/bots。
6
+ import fs from 'node:fs';
7
+ import { saveBot, listBots, getBot, deleteBot } from './store.js';
8
+ function flagValue(args, flag) {
9
+ const i = args.indexOf(flag);
10
+ if (i === -1)
11
+ return undefined;
12
+ const v = args[i + 1];
13
+ return v && !v.startsWith('-') ? v : undefined;
14
+ }
15
+ export async function runBotsCli(rawArgs) {
16
+ const sub = rawArgs[0] ?? 'list';
17
+ const args = rawArgs.slice(1);
18
+ switch (sub) {
19
+ case 'add': {
20
+ const name = flagValue(args, '--name');
21
+ const description = flagValue(args, '--description') ?? '';
22
+ const inline = flagValue(args, '--prompt');
23
+ const promptFile = flagValue(args, '--prompt-file');
24
+ const toolsRaw = flagValue(args, '--tools');
25
+ const sandboxPath = flagValue(args, '--sandbox-path');
26
+ const scope = args.includes('--global') ? 'global' : 'project';
27
+ if (!name) {
28
+ process.stderr.write('usage: mocode bots add --name <n> (--prompt "<text>" | --prompt-file <path>) ' +
29
+ '[--description <d>] [--tools "a,b"] [--sandbox-path <p>] [--global]\n');
30
+ return 1;
31
+ }
32
+ let systemPrompt = inline ?? '';
33
+ if (promptFile) {
34
+ try {
35
+ systemPrompt = fs.readFileSync(promptFile, 'utf8');
36
+ }
37
+ catch (e) {
38
+ process.stderr.write(`mocode: cannot read prompt file: ${e instanceof Error ? e.message : String(e)}\n`);
39
+ return 1;
40
+ }
41
+ }
42
+ if (!systemPrompt.trim()) {
43
+ process.stderr.write('mocode: bot prompt is empty (use --prompt or --prompt-file)\n');
44
+ return 1;
45
+ }
46
+ const tools = toolsRaw
47
+ ? toolsRaw
48
+ .split(',')
49
+ .map((t) => t.trim())
50
+ .filter(Boolean)
51
+ : undefined;
52
+ try {
53
+ const rec = saveBot({ name, description, systemPrompt, tools, sandboxPath, scope });
54
+ process.stdout.write(`Bot saved: ${rec.name} (${rec.scope})\n`);
55
+ return 0;
56
+ }
57
+ catch (e) {
58
+ process.stderr.write(`mocode: ${e instanceof Error ? e.message : String(e)}\n`);
59
+ return 1;
60
+ }
61
+ }
62
+ case 'show': {
63
+ const name = args[0];
64
+ if (!name) {
65
+ process.stderr.write('usage: mocode bots show <name>\n');
66
+ return 1;
67
+ }
68
+ const rec = getBot(name);
69
+ if (!rec) {
70
+ process.stderr.write(`mocode: bot "${name}" not found\n`);
71
+ return 1;
72
+ }
73
+ process.stdout.write(`${JSON.stringify(rec, null, 2)}\n`);
74
+ return 0;
75
+ }
76
+ case 'rm':
77
+ case 'remove': {
78
+ const name = args[0];
79
+ if (!name) {
80
+ process.stderr.write('usage: mocode bots rm <name> [--global]\n');
81
+ return 1;
82
+ }
83
+ const scope = args.includes('--global') ? 'global' : 'project';
84
+ const ok = deleteBot(name, scope);
85
+ process.stdout.write(ok ? `Removed bot ${name} (${scope})\n` : `mocode: bot ${name} (${scope}) not found\n`);
86
+ return ok ? 0 : 1;
87
+ }
88
+ case 'list':
89
+ default: {
90
+ const all = listBots();
91
+ if (all.length === 0) {
92
+ process.stdout.write('(no bots; create one with `mocode bots add …`)\n');
93
+ return 0;
94
+ }
95
+ for (const b of all) {
96
+ process.stdout.write(`${b.scope === 'global' ? 'g' : 'p'} ${b.name}` +
97
+ `${b.tools && b.tools.length ? ` tools:${b.tools.length}` : ''} ${b.description}\n`);
98
+ }
99
+ return 0;
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,100 @@
1
+ // 具名 Bot 存储:一个 Bot = 岗位系统提示 + 可选工具白名单 + 可选沙箱范围。
2
+ //
3
+ // 两级存储(与 AGENTS.md 发现规则同风格):
4
+ // - 全局:~/.mocode/bots/<name>.json
5
+ // - 项目:<sandboxRoot>/.mocode/bots/<name>.json
6
+ // 合并时全局先、项目后,项目同名覆盖全局。
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { getSandboxRoot } from '../sandbox/index.js';
11
+ const NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
12
+ export function globalBotsDir() {
13
+ return path.join(os.homedir(), '.mocode', 'bots');
14
+ }
15
+ export function projectBotsDir() {
16
+ return path.join(getSandboxRoot() ?? process.cwd(), '.mocode', 'bots');
17
+ }
18
+ function botPath(dir, name) {
19
+ return path.join(dir, `${name}.json`);
20
+ }
21
+ function assertValidName(name) {
22
+ if (!NAME_RE.test(name)) {
23
+ throw new Error(`bad bot name "${name}": use 1-32 chars, lowercase letters/digits/dash, cannot start with dash`);
24
+ }
25
+ }
26
+ /** 校验并落盘一个 bot(scope 决定写全局还是项目)。 */
27
+ export function saveBot(input) {
28
+ assertValidName(input.name);
29
+ if (!input.systemPrompt.trim())
30
+ throw new Error('bot systemPrompt must not be empty');
31
+ const scope = input.scope ?? 'project';
32
+ const record = {
33
+ name: input.name,
34
+ description: input.description ?? '',
35
+ systemPrompt: input.systemPrompt,
36
+ ...(input.tools && input.tools.length ? { tools: input.tools } : {}),
37
+ ...(input.sandboxPath ? { sandboxPath: input.sandboxPath } : {}),
38
+ scope,
39
+ createdAt: new Date().toISOString(),
40
+ };
41
+ const dir = scope === 'global' ? globalBotsDir() : projectBotsDir();
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ const target = botPath(dir, record.name);
44
+ const tmp = `${target}.tmp`;
45
+ fs.writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
46
+ fs.renameSync(tmp, target);
47
+ return record;
48
+ }
49
+ function readOne(dir, file) {
50
+ try {
51
+ return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function readDir(dir) {
58
+ let files;
59
+ try {
60
+ files = fs.readdirSync(dir);
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ const out = [];
66
+ for (const f of files) {
67
+ if (!f.endsWith('.json'))
68
+ continue;
69
+ const rec = readOne(dir, f);
70
+ if (rec)
71
+ out.push(rec);
72
+ }
73
+ return out;
74
+ }
75
+ /** 列出全部 bot(全局 + 项目,项目同名覆盖)。 */
76
+ export function listBots() {
77
+ const byName = new Map();
78
+ for (const b of readDir(globalBotsDir()))
79
+ byName.set(b.name, b);
80
+ for (const b of readDir(projectBotsDir()))
81
+ byName.set(b.name, b);
82
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
83
+ }
84
+ export function getBot(name) {
85
+ // 项目优先(覆盖语义):先找项目再找全局。
86
+ const project = readOne(projectBotsDir(), `${name}.json`);
87
+ if (project)
88
+ return project;
89
+ return readOne(globalBotsDir(), `${name}.json`);
90
+ }
91
+ export function deleteBot(name, scope = 'project') {
92
+ assertValidName(name);
93
+ try {
94
+ fs.rmSync(botPath(scope === 'global' ? globalBotsDir() : projectBotsDir(), name));
95
+ return true;
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
@@ -495,7 +495,7 @@ ${buildVoiceSection()}
495
495
  - Stop immediately when no more tools are needed; give conclusions directly.
496
496
  - **Do not stop prematurely during exploration**: if you started investigating but haven't gathered enough information to answer the user's question, keep calling tools. Only stop when you have sufficient evidence or hit a dead end.
497
497
  - **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
498
- - Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.
498
+ - Close with a brief explanation of what you did and why, not just a verdict, so the user doesn't have to re-read the tool trace: when files changed, group them by file or logical change — what changed and the behavior/fix it delivers, one or two lines each with "path:line" references, noting renames/deletions and skipping purely mechanical edits; for investigation or analysis, state the conclusion and the key evidence. Say success when successful, where you're stuck when failing, and anything skipped. Keep it concise.
499
499
  - Definition of done: never declare a multi-feature request finished just because the code exists. Each distinct user request listed in the plan must be actually verified (the relevant check run / observed result), not merely implemented; mark plan steps \`[x]\` only after that verification. If the user supplied an acceptance list, every item on it must pass. Leave unverified items explicitly listed as pending instead of collapsing them into "done".`;
500
500
  // 动态段(置于末尾):AGENTS.md 项目记忆 + notepad 索引/说明。工具簇特定指导由
501
501
  // ToolPolicyController.reminder() 按当前 turn 的 route 注入,避免旧全局 profile 与真实 schema 分裂。
@@ -618,6 +618,7 @@ export const config = {
618
618
  llmKeysFromShell,
619
619
  permissionEnabled: process.env.MOCODE_PERMISSION !== 'false',
620
620
  permissionNonInteractiveAllow: process.env.MOCODE_PERMISSION_NON_INTERACTIVE_ALLOW === 'true',
621
+ notifyWebhook: process.env.MOCODE_NOTIFY_WEBHOOK || undefined,
621
622
  };
622
623
  /**
623
624
  * 创建一份独立 Config 快照,不重新读取环境变量、配置文件或 preset。
@@ -0,0 +1,302 @@
1
+ // Headless(一次性 / 非交互)模式:mocode -p "任务" 或 echo "任务" | mocode。
2
+ // 与 REPL 共用同一套装配(builtins 工具包 + MCP + runAgentCore),区别只在展示:
3
+ // 没有全屏 TUI,正文流式写 stdout,进度写 stderr(保 stdout 可被管道解析)。
4
+ //
5
+ // 工具进度:默认每个调用打一行「[工具名] 关键参数」(summarizeToolCall,与 TUI
6
+ // 同源;browser fill 等敏感值刻意不显示)。--verbose 追加工具结果摘要。
7
+ //
8
+ // 具名 Bot(C2):--bot <name> 时用 Bot 的岗位提示作为角色前缀,并按其工具白名单
9
+ // 过滤可调用工具(toolsOverride + runtimeAllowedToolNames,schema 即上限)。
10
+ //
11
+ // 隔离:--session-dir <path> 会话独立目录;--worktree 在 git detached worktree 中
12
+ // 执行并自动销毁。两者走独立 Runtime(不碰全局单例)。
13
+ //
14
+ // 权限:非交互默认 fail-closed(permissions/index.ts 的 !isTTY 分支)。
15
+ // --dangerously-skip-permissions 显式关闭权限闸。
16
+ import path from 'node:path';
17
+ import { setSandboxRoot } from './sandbox/root.js';
18
+ import { registerToolsExtension } from './tools/registry.js';
19
+ // 装配官方默认工具包:registry 不顶层 import builtins(破模块循环),入口须显式装配。
20
+ import './tools/builtins/index.js';
21
+ import { initializeAllMcp, getMcpTools, closeAllMcp } from './mcp/index.js';
22
+ import { config, buildBasePrompt, isMemoryEnabled } from './config/index.js';
23
+ import { effectiveSystemPrompt } from './skills/index.js';
24
+ import { buildMemoryIndexSection } from './memory/store.js';
25
+ import { defaultRuntime, Runtime } from './runtime/index.js';
26
+ import { runAgentCore } from './agent/core.js';
27
+ import { summarizeToolCall, summarizeToolResult } from './ui/render.js';
28
+ import { getToolChatSchema } from './tools/policy.js';
29
+ import { createWorktree, removeWorktree } from './jobs/worktree.js';
30
+ import { runAsIdentity } from './bots/bus.js';
31
+ import { createAsyncApprovalChecker } from './jobs/approval.js';
32
+ import { writeCheckpoint, readCheckpoint } from './jobs/checkpoint.js';
33
+ import { getBot } from './bots/store.js';
34
+ /** 读取 stdin 全部内容作为 prompt(管道场景)。 */
35
+ async function readStdin() {
36
+ let data = '';
37
+ process.stdin.setEncoding('utf8');
38
+ for await (const chunk of process.stdin)
39
+ data += chunk;
40
+ return data;
41
+ }
42
+ /**
43
+ * 解析 Bot:查记录、解析其沙箱范围、校验白名单。
44
+ * 返回 null 表示未使用 Bot;找不到 Bot 抛错(不静默用默认身份,避免误以为岗位生效)。
45
+ */
46
+ function resolveBot(botName, baseSandbox) {
47
+ if (!botName)
48
+ return null;
49
+ const bot = getBot(botName);
50
+ if (!bot)
51
+ throw new Error(`bot "${botName}" not found (project .mocode/bots or global ~/.mocode/bots)`);
52
+ let sandboxRoot = baseSandbox;
53
+ if (bot.sandboxPath) {
54
+ // project bot 相对其工作区;global bot 相对当前 cwd。
55
+ const anchor = bot.scope === 'project' ? baseSandbox : process.cwd();
56
+ sandboxRoot = path.resolve(anchor, bot.sandboxPath);
57
+ }
58
+ return { bot, sandboxRoot };
59
+ }
60
+ /** 从工具目录按白名单构造 schema 集 + 允许名集合。 */
61
+ function buildBotToolFilter(catalog, allowNames) {
62
+ const wanted = new Set(allowNames);
63
+ const toolsOverride = catalog
64
+ .filter((t) => wanted.has(t.name))
65
+ .map((t) => getToolChatSchema(t.name, catalog))
66
+ .filter((s) => s !== null);
67
+ return { toolsOverride, allowed: new Set(toolsOverride.map((s) => s.function.name)) };
68
+ }
69
+ export async function runHeadless(opts) {
70
+ const prompt = (opts.prompt ?? '').trim();
71
+ if (!prompt) {
72
+ process.stderr.write('mocode: empty prompt (use -p "..." or pipe via stdin)\n');
73
+ return 1;
74
+ }
75
+ if (opts.skipPermissions)
76
+ config.permissionEnabled = false;
77
+ // worktree 先建(失败明确报错,不静默回退主工作区)。
78
+ let wt = null;
79
+ if (opts.worktree) {
80
+ try {
81
+ wt = createWorktree();
82
+ }
83
+ catch (e) {
84
+ process.stderr.write(`mocode: worktree setup failed: ${e instanceof Error ? e.message : String(e)}\n`);
85
+ return 1;
86
+ }
87
+ }
88
+ const initialSandbox = wt?.path ?? opts.sandboxRootOverride ?? config.sandboxRoot ?? process.cwd();
89
+ // Bot 解析(在选沙箱之后:bot.sandboxPath 可进一步收窄)。
90
+ let botInfo;
91
+ try {
92
+ botInfo = resolveBot(opts.botName, initialSandbox);
93
+ }
94
+ catch (e) {
95
+ if (wt) {
96
+ try {
97
+ removeWorktree(wt);
98
+ }
99
+ catch {
100
+ // ignore
101
+ }
102
+ }
103
+ process.stderr.write(`mocode: ${e instanceof Error ? e.message : String(e)}\n`);
104
+ return 1;
105
+ }
106
+ const effectiveSandboxRoot = botInfo?.sandboxRoot ?? initialSandbox;
107
+ const isolated = wt !== null || opts.sessionDir !== undefined || opts.jobId !== undefined;
108
+ // MCP 先初始化注册:独立 Runtime 创建时复制当前 builtinTools(含 MCP),须早于 new Runtime。
109
+ await initializeAllMcp();
110
+ registerToolsExtension('mcp', getMcpTools());
111
+ let runtime;
112
+ if (isolated) {
113
+ runtime = new Runtime({
114
+ sandboxRoot: effectiveSandboxRoot,
115
+ ...(opts.sessionDir ? { configOverrides: { sessionDir: path.resolve(opts.sessionDir) } } : {}),
116
+ // bg job:注入异步审批 checker(命中需授权动作时挂起,等 mocode approve)。
117
+ ...(opts.jobId
118
+ ? {
119
+ services: {
120
+ checkPermission: createAsyncApprovalChecker({
121
+ jobId: opts.jobId,
122
+ sandboxRoot: effectiveSandboxRoot,
123
+ getPrompt: () => prompt,
124
+ }),
125
+ },
126
+ }
127
+ : {}),
128
+ });
129
+ }
130
+ else {
131
+ setSandboxRoot(effectiveSandboxRoot);
132
+ runtime = defaultRuntime;
133
+ }
134
+ await runtime.start();
135
+ const sessionId = runtime.session.create();
136
+ // Bot 工具白名单:从该 runtime 实际工具目录过滤。
137
+ let toolFilter = null;
138
+ if (botInfo?.bot.tools && botInfo.bot.tools.length) {
139
+ toolFilter = buildBotToolFilter(runtime.context.toolRuntime.tools, botInfo.bot.tools);
140
+ if (toolFilter.toolsOverride.length === 0) {
141
+ process.stderr.write('mocode: bot tool whitelist matched no available tools\n');
142
+ await runtime.close();
143
+ if (wt) {
144
+ try {
145
+ removeWorktree(wt);
146
+ }
147
+ catch {
148
+ // ignore
149
+ }
150
+ }
151
+ return 1;
152
+ }
153
+ }
154
+ // 系统提示:Bot 岗位角色在前,基础约定在后;白名单存在时补一句工具边界。
155
+ const baseConventions = buildBasePrompt(sessionId) + buildMemoryIndexSection(isMemoryEnabled());
156
+ const systemContent = botInfo
157
+ ? `# Role\n${botInfo.bot.systemPrompt}\n\n` +
158
+ `${botInfo.bot.description ? `**Position:** ${botInfo.bot.description}\n\n` : ''}` +
159
+ `---\n\n${baseConventions}` +
160
+ (toolFilter ? `\n\n## Tool boundary\nOnly these tools are available: ${[...toolFilter.allowed].join(', ')}.` : '')
161
+ : baseConventions;
162
+ const history = [{ role: 'system', content: effectiveSystemPrompt(systemContent) }];
163
+ let resumedFromCheckpoint = false;
164
+ if (opts.resumeFromCheckpoint && opts.jobId) {
165
+ const snap = readCheckpoint(opts.jobId);
166
+ if (snap && snap.length) {
167
+ history.length = 0;
168
+ history.push(...snap);
169
+ resumedFromCheckpoint = true;
170
+ }
171
+ else {
172
+ process.stderr.write('mocode: no checkpoint to resume from\n');
173
+ await runtime.close();
174
+ return 1;
175
+ }
176
+ }
177
+ let textBuffer = '';
178
+ // D3 runaway guard (bg jobs only): hard wall-clock / token caps.
179
+ let jobSignal;
180
+ let jobGuard;
181
+ if (opts.jobId) {
182
+ const maxMs = Number(process.env.MOCODE_JOB_MAX_MS ?? 0);
183
+ const maxTokens = Number(process.env.MOCODE_JOB_MAX_TOKENS ?? 0);
184
+ if (maxMs > 0 || maxTokens > 0) {
185
+ const controller = new AbortController();
186
+ const state = { tokens: 0, maxTokens, controller, timer: undefined };
187
+ if (maxMs > 0)
188
+ state.timer = setTimeout(() => {
189
+ process.stderr.write(`\n[guard] job exceeded ${maxMs}ms; aborting\n`);
190
+ controller.abort();
191
+ }, maxMs);
192
+ jobGuard = state;
193
+ jobSignal = controller.signal;
194
+ }
195
+ }
196
+ const hooks = {
197
+ onLiveUsage: (u) => {
198
+ if (!jobGuard)
199
+ return;
200
+ jobGuard.tokens = Math.max(jobGuard.tokens, u.totalTokens);
201
+ if (jobGuard.maxTokens && jobGuard.tokens > jobGuard.maxTokens) {
202
+ process.stderr.write(`\n[guard] job exceeded ${jobGuard.maxTokens} tokens; aborting\n`);
203
+ jobGuard.controller?.abort();
204
+ }
205
+ },
206
+ onText: (delta) => {
207
+ textBuffer += delta;
208
+ if (!opts.json)
209
+ process.stdout.write(delta);
210
+ },
211
+ onToolHeader: (tc) => {
212
+ if (!opts.json && textBuffer && !textBuffer.endsWith('\n')) {
213
+ process.stdout.write('\n');
214
+ textBuffer += '\n';
215
+ }
216
+ process.stderr.write(`[${tc.name}] ${summarizeToolCall(tc.name, tc.arguments)}\n`);
217
+ },
218
+ onToolResult: (tc, output) => {
219
+ if (!opts.verbose)
220
+ return;
221
+ process.stderr.write(` → ${summarizeToolResult(tc.name, output)}\n`);
222
+ },
223
+ ...(opts.jobId
224
+ ? {
225
+ onCheckpoint: (h) => writeCheckpoint(opts.jobId, h),
226
+ }
227
+ : {}),
228
+ onModelRetry: (info) => {
229
+ process.stderr.write(`[retry] model error (${info.code}), retry ${info.nextAttempt} in ${info.waitMs}ms\n`);
230
+ },
231
+ };
232
+ const startedAt = Date.now();
233
+ let exitCode = 0;
234
+ let result;
235
+ const invokeCore = () => runAgentCore({
236
+ history,
237
+ userInput: prompt,
238
+ hooks,
239
+ ...(jobSignal ? { signal: jobSignal } : {}),
240
+ runtimeContext: runtime.context,
241
+ ...(resumedFromCheckpoint ? { continueFromHistory: true } : {}),
242
+ ...(toolFilter ? { toolsOverride: toolFilter.toolsOverride, runtimeAllowedToolNames: toolFilter.allowed } : {}),
243
+ });
244
+ try {
245
+ result = opts.botName ? await runAsIdentity(opts.botName, () => invokeCore()) : await invokeCore();
246
+ }
247
+ finally {
248
+ if (jobGuard?.timer)
249
+ clearTimeout(jobGuard.timer);
250
+ try {
251
+ runtime.session.save(history, sessionId, [prompt], []);
252
+ }
253
+ catch {
254
+ // 落盘失败不覆盖主结果
255
+ }
256
+ closeAllMcp();
257
+ await runtime.close();
258
+ if (wt) {
259
+ try {
260
+ removeWorktree(wt);
261
+ }
262
+ catch {
263
+ process.stderr.write(`mocode: failed to remove worktree ${wt.path}(手动清理)\n`);
264
+ }
265
+ }
266
+ }
267
+ const elapsedMs = Date.now() - startedAt;
268
+ const finalText = result.finalText ?? (opts.json ? null : textBuffer);
269
+ if (opts.json) {
270
+ const payload = {
271
+ terminationReason: result.terminationReason,
272
+ text: finalText,
273
+ sessionId,
274
+ changedFiles: result.changedFiles ?? [],
275
+ usage: result.usage,
276
+ elapsedMs,
277
+ model: config.model,
278
+ ...(opts.botName ? { bot: opts.botName } : {}),
279
+ };
280
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
281
+ }
282
+ else if (finalText && !textBuffer) {
283
+ process.stdout.write(finalText);
284
+ }
285
+ if (!opts.json && process.stdout.isTTY && finalText && !finalText.endsWith('\n'))
286
+ process.stdout.write('\n');
287
+ if (result.terminationReason === 'aborted')
288
+ exitCode = 130;
289
+ else if (result.terminationReason === 'max_steps' || !result.completed)
290
+ exitCode = 1;
291
+ return exitCode;
292
+ }
293
+ /** 无 -p 且 stdin 被重定向时,从 stdin 取 prompt;返回 null 表示无可用输入。 */
294
+ export async function resolvePrompt(inlinePrompt) {
295
+ if (inlinePrompt !== undefined)
296
+ return inlinePrompt;
297
+ if (!process.stdin.isTTY) {
298
+ const piped = (await readStdin()).trim();
299
+ return piped || null;
300
+ }
301
+ return null;
302
+ }