mocode-ai 1.6.5 → 1.6.6

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.
package/README.md CHANGED
@@ -117,9 +117,15 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
117
117
  - **Streaming output + visible reasoning** — Responses render as they're generated; when the model supports reasoning, the thinking process is visible in real time and auto-collapses to save screen space.
118
118
  - **Full-screen TUI** — Alt-screen mode with a fixed status bar, scrollback (PgUp/PgDn), typeahead while the agent is running, and auto-prefill for the next turn.
119
119
  - **Session persistence** — Every turn is saved automatically; `--resume` / `/resume` picks up a past session.
120
+ - **Background jobs** — `mocode run --bg "task"` spawns a detached process that survives terminal close; state and logs land in `.mocode/jobs/`, and `/jobs` lists, tails logs, or kills them. Set `MOCODE_NOTIFY_WEBHOOK` to push a finish notification (ntfy/Bark/Telegram/generic). When an unattended job hits an unauthorised confirm/dangerous action it parks (status `paused`) and pings you; approve in another terminal with `mocode approve <id>` (`mocode deny` to reject, or `/jobs approve` inside the TUI), and it resumes in the same process. Follow a running (or finished) job live with `mocode attach <id>`. Long jobs checkpoint their history after every tool batch; if the process dies or the machine reboots, `mocode resume-job <id>` replays from the last checkpoint (in-flight work is re-run). `MOCODE_JOB_MAX_MS` / `MOCODE_JOB_MAX_TOKENS` add hard wall-clock/token caps.
121
+ - **Named bots** — `mocode bots add` defines role-based bots (job-specific system prompt + optional exact-tool whitelist + sandbox scope) at project/global level; run with `--bot <name>`, combine with `run --bg` or schedules.
122
+ - **Arena** — The `arena` tool runs the same task N times (2-6) as parallel independent workers, then a judge model ranks every candidate against your criteria and returns the winner with the full ranking. Useful for design/exploration/problem-solving where several attempts beat one.
123
+ - **Persistent bot messaging** — The `message_bus` tool gives named bots a durable, asynchronous message store (`send` / `inbox` / `ack` / `history`): a supervisor bot can hand off work to another bot that is not currently running; the worker reads its inbox later (e.g. when a schedule wakes it), does the job, and replies. Identity follows the `--bot` identity, and each bot only sees/acks its own messages.
124
+ - **Scheduled tasks** — `mocode schedule add` registers cron and/or webhook triggers; a local detached daemon (`schedule start`, loopback-only) fires background jobs on time or on `POST /trigger/<token>`, with per-minute dedup. A stateless `schedule tick` is also available for OS task schedulers.
125
+ - **Headless one-shot mode** — `mocode -p "task"` or piped `echo "task" | mocode`, with optional `--json` structured output; confirm/dangerous actions are denied by default when non-interactive (opt in with `--dangerously-skip-permissions`; `--verbose` adds tool-result summaries, `--session-dir <dir>`, `--worktree` ephemeral git worktree), and sessions are still saved for `--resume`.
120
126
  - **Skills system** — Scans directories like `~/.mocode/skills/` automatically; each skill's description is injected into the system prompt, and the model calls `use_skill` to load the full instructions only when relevant (progressive disclosure: skim the summary first, load the body only if needed).
121
127
  - **Optional desktop pet** — A small floating window (`/pet`) shows a stateful character that mirrors agent activity (idle / thinking / tool running / waiting for human). Works as a separate process over WebSocket; quit it with `/pet quit`. Sits beside the terminal, never blocks it.
122
- - **Slash commands** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/memory` `/reflect` `/init` `/theme` `/model` `/effort` `/stats` `/plan` `/auto` `/pet`, with dropdown filtering as you type.
128
+ - **Slash commands** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/jobs` `/schedules` `/bots` `/memory` `/reflect` `/init` `/theme` `/model` `/effort` `/stats` `/plan` `/auto` `/pet`, with dropdown filtering as you type.
123
129
 
124
130
  ## Documentation
125
131
 
package/README.zh-CN.md CHANGED
@@ -116,9 +116,15 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
116
116
  - **流式输出 + 思考可见** — 回复边生成边显示;模型支持 reasoning 时思考过程实时可见,思考段自动折叠(不占屏)
117
117
  - **全屏 TUI** — 备用屏(alt screen)+ 固定底栏状态行 + 滚动回看(PgUp/PgDn),运行中可打字(typeahead),下一轮自动预填
118
118
  - **会话持久化** — 每轮自动落盘,`--resume` / `/resume` 续接历史会话
119
+ - **后台任务** — `mocode run --bg "任务"` 派 detached 子进程,关终端不死;状态与日志落 `.mocode/jobs/`,`/jobs` 可列表/看日志/kill。设 `MOCODE_NOTIFY_WEBHOOK` 后,任务结束自动推送通知(ntfy/Bark/Telegram/generic)。无人值守任务遇到未授权的 confirm/dangerous 动作时会挂起(状态 `paused`)并通知你;在另一终端敲 `mocode approve <id>` 批准(`mocode deny` 拒绝,或 TUI 内 `/jobs approve`),任务在同一进程内续跑。用 `mocode attach <id>` 可实时跟随运行中(或已结束)的任务。长任务在每个工具批次后落 checkpoint;进程挂掉或机器重启后,`mocode resume-job <id>` 从最后 checkpoint 重放(在途动作会重跑)。`MOCODE_JOB_MAX_MS` / `MOCODE_JOB_MAX_TOKENS` 提供硬性时长/token 上限。
120
+ - **具名 Bot** — `mocode bots add` 定义岗位 Bot(专属系统提示 + 可选工具白名单 + 沙箱范围),项目/全局两级;`--bot <name>` 按名运行,可与后台/schedules 组合。
121
+ - **Arena 竞技场** — `arena` 工具把同一任务并行跑 N 遍(2-6,各自独立),再由 judge 模型按你给的标准对所有候选打分排序,返回排名和最优方案。适合设计/探索/解题这类“多跑几遍选最好”的场景。
122
+ - **持久 Bot 消息协作** — `message_bus` 工具给具名 bot 提供持久的异步消息存储(`send`/`inbox`/`ack`/`history`):主管 bot 可把活交给当前没在运行的 bot;worker 之后(如被计划任务唤起)拉 inbox、干活、回复。身份随 `--bot` 走,每个 bot 只能看到/确认发给自己的消息。
123
+ - **计划任务** — `mocode schedule add` 注册 cron / webhook 触发;本地 detached 守护(`schedule start`,仅回环)到点或被 `POST /trigger/<token>` 触发即派后台 job,分钟去重;另有 `schedule tick` 供系统任务计划调用。
124
+ - **Headless 一次性执行** — `mocode -p "任务"` 或管道 `echo "任务" | mocode`,可选 `--json` 结构化输出;非交互下 confirm/dangerous 操作默认拒绝(`--dangerously-skip-permissions` 显式放开;`--verbose` 追加工具结果摘要、`--session-dir <目录>`、`--worktree` git 隔离工作树),会话仍自动落盘可 `--resume`
119
125
  - **Skills 系统** — 自动扫描 `~/.mocode/skills/` 等目录,description 注入系统提示,模型按需调 `use_skill` 加载完整指令(渐进式披露:先看简介,任务相关才加载正文)
120
126
  - **可选桌宠** — 独立悬浮窗(`/pet`)显示一个小角色,镜像 agent 活动(空闲 / 思考 / 跑工具 / 等人工),独立进程走 WebSocket,`/pet quit` 完全关闭。挂在终端外,绝不挡终端。
121
- - **斜杠命令** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/memory` `/reflect` `/init` `/theme` `/model` `/effort` `/stats` `/plan` `/auto` `/pet`,输入时下拉过滤
127
+ - **斜杠命令** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/jobs` `/schedules` `/bots` `/memory` `/reflect` `/init` `/theme` `/model` `/effort` `/stats` `/plan` `/auto` `/pet`,输入时下拉过滤
122
128
 
123
129
  ## 使用文档
124
130
 
@@ -64,7 +64,8 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
64
64
  const turnLifecycle = createTurnLifecycle(opts, ctx, stages, savedMode);
65
65
  const { usageMeter, emitTrace, traceTurnId } = turnLifecycle;
66
66
  const toolTurnPlanState = { stepsSincePlanTouch: 0 };
67
- historyManager.appendUserTurn(userInput);
67
+ if (!opts.continueFromHistory)
68
+ historyManager.appendUserTurn(userInput);
68
69
  // P2:每用户 turn 一个重复读 scope,经 dispatcher → ToolContext 透传给 read_file。
69
70
  const readDedup = createReadDedup();
70
71
  // The initial cancellation checkpoint is captured after the user turn and before any model/tool work.
@@ -78,6 +78,8 @@ export async function runToolTurn(input) {
78
78
  rebuildHistoryIndexes();
79
79
  throw error;
80
80
  }
81
+ // D3: batch committed (staged shadow pushed into backing) -> persist full history.
82
+ hooks.onCheckpoint?.(opts.history);
81
83
  hooks.onToolBatchEnd?.();
82
84
  cancellationLifecycle.checkpoint();
83
85
  const batchDecision = terminationPolicy.decide({
@@ -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
+ }
@@ -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。