mocode-ai 1.3.9 → 1.4.0
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 +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桌宠命令组:/pet quit · /pet skin · /pet
|
|
3
|
+
*
|
|
4
|
+
* 顺序敏感:`/pet quit`、`/pet skin` 必须排在裸 `/pet` 之前(后者是完整匹配,
|
|
5
|
+
* 前两者也是完整匹配,但语义上前者更具体)。保持原 if 链的相对顺序。
|
|
6
|
+
*/
|
|
7
|
+
import * as layout from '../../ui/layout.js';
|
|
8
|
+
import { ui } from '../../ui/theme.js';
|
|
9
|
+
import { togglePet, killPetProcess, listSkins, setSkin } from '../../pet/bridge.js';
|
|
10
|
+
import { promptThemePicker } from '../../ui/prompt.js';
|
|
11
|
+
import { unhandled, next } from './types.js';
|
|
12
|
+
export const petCommands = [
|
|
13
|
+
// /pet quit:完全关闭桌宠进程(区别于 /pet 的仅断开本连接)。方案C的 CLI 侧退出入口,
|
|
14
|
+
// 另一入口是桌宠托盘菜单"退出桌宠"(见 packages/pet-app/src/main.ts)。
|
|
15
|
+
async (ctx) => {
|
|
16
|
+
if (ctx.line !== '/pet quit')
|
|
17
|
+
return unhandled();
|
|
18
|
+
const { ok, reason } = await killPetProcess();
|
|
19
|
+
layout.contentWrite(`${ui.dim}(${ok ? '已关闭桌宠进程' : (reason ?? '关闭失败')})${ui.reset}\n`);
|
|
20
|
+
return next();
|
|
21
|
+
},
|
|
22
|
+
// /pet skin:菜单选皮(↑↓ 选,Enter 切换,Esc 取消),仿 /theme 的交互。要求桌宠已在运行
|
|
23
|
+
// (未运行则先提示 /pet 打开;不在此处自动 spawn,避免选皮命令产生"顺带开桌宠"的意外副作用)。
|
|
24
|
+
async (ctx) => {
|
|
25
|
+
if (ctx.line !== '/pet skin')
|
|
26
|
+
return unhandled();
|
|
27
|
+
let skinList;
|
|
28
|
+
try {
|
|
29
|
+
skinList = await listSkins();
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
layout.contentWrite(`${ui.dim}(${e instanceof Error ? e.message : '获取皮肤列表失败'})${ui.reset}\n`);
|
|
33
|
+
return next();
|
|
34
|
+
}
|
|
35
|
+
const items = [
|
|
36
|
+
{ id: 'default', title: '默认(mascot)', subtitle: skinList.currentSkinId === 'default' ? '当前' : '' },
|
|
37
|
+
...skinList.skins.map((s) => ({
|
|
38
|
+
id: s.id,
|
|
39
|
+
title: s.name,
|
|
40
|
+
subtitle: skinList.currentSkinId === s.id ? '当前' : '',
|
|
41
|
+
})),
|
|
42
|
+
];
|
|
43
|
+
let pick;
|
|
44
|
+
try {
|
|
45
|
+
pick = await promptThemePicker(items);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return next(); // Ctrl+C(SIGINT)→ 取消
|
|
49
|
+
}
|
|
50
|
+
if (pick === null)
|
|
51
|
+
return next(); // Esc / Ctrl+D 取消
|
|
52
|
+
setSkin(pick.id);
|
|
53
|
+
layout.contentWrite(`${ui.dim}(已切换桌宠皮肤:${pick.title})${ui.reset}\n`);
|
|
54
|
+
return next();
|
|
55
|
+
},
|
|
56
|
+
// /pet:开关桌宠。已连接→断开;未连接→探测端口(已有实例则直连)或 spawn 拉起 + 退避重试连接。
|
|
57
|
+
// togglePet 不抛异常,所有失败路径转为返回值——桌宠是可选增强,任何异常都不能影响 REPL 主流程。
|
|
58
|
+
async (ctx) => {
|
|
59
|
+
if (ctx.line !== '/pet')
|
|
60
|
+
return unhandled();
|
|
61
|
+
const { connected, reason } = await togglePet();
|
|
62
|
+
if (connected) {
|
|
63
|
+
layout.contentWrite(`${ui.dim}(桌宠已连接)${ui.reset}\n`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
layout.contentWrite(`${ui.dim}(${reason ?? '桌宠已断开'})${ui.reset}\n`);
|
|
67
|
+
}
|
|
68
|
+
return next();
|
|
69
|
+
},
|
|
70
|
+
];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { unhandled } from './types.js';
|
|
2
|
+
import { systemCommands } from './system.js';
|
|
3
|
+
import { modeCommands } from './mode.js';
|
|
4
|
+
import { petCommands } from './pet.js';
|
|
5
|
+
import { imageCommands } from './image.js';
|
|
6
|
+
import { contextCommands } from './context.js';
|
|
7
|
+
import { sessionCommands } from './session.js';
|
|
8
|
+
import { memoryCommands } from './memory.js';
|
|
9
|
+
import { skillCommands } from './skill.js';
|
|
10
|
+
import { compactCommands } from './compact.js';
|
|
11
|
+
import { appearanceCommands } from './appearance.js';
|
|
12
|
+
import { modelCommands } from './model.js';
|
|
13
|
+
import { toolGroupCommands } from './tool-group.js';
|
|
14
|
+
/**
|
|
15
|
+
* 命令注册表。顺序即匹配优先级 —— 更具体的分支放前面。
|
|
16
|
+
* 组内顺序由各文件自己保证(见文件头注释);组间顺序互不影响(条件互斥)。
|
|
17
|
+
*
|
|
18
|
+
* dispatch 返回 unhandled = 没有这条命令,runtime.ts 据此走未知命令兜底。
|
|
19
|
+
*/
|
|
20
|
+
export const commandHandlers = [
|
|
21
|
+
...systemCommands,
|
|
22
|
+
...modeCommands,
|
|
23
|
+
...petCommands,
|
|
24
|
+
...imageCommands,
|
|
25
|
+
...contextCommands,
|
|
26
|
+
...sessionCommands,
|
|
27
|
+
...memoryCommands,
|
|
28
|
+
...skillCommands,
|
|
29
|
+
...compactCommands,
|
|
30
|
+
...appearanceCommands,
|
|
31
|
+
...modelCommands,
|
|
32
|
+
...toolGroupCommands,
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* 依次询问 handler,返回第一个非 unhandled 的结果。
|
|
36
|
+
* 全部 unhandled 时返回 unhandled —— 调用方(runtime.ts)据此走未知命令兜底
|
|
37
|
+
* (isCommandShape + suggestCommand),或把非命令输入当普通消息发给 agent。
|
|
38
|
+
*/
|
|
39
|
+
export async function dispatchCommand(ctx) {
|
|
40
|
+
for (const handler of commandHandlers) {
|
|
41
|
+
const outcome = await handler(ctx);
|
|
42
|
+
if (outcome.kind !== 'unhandled')
|
|
43
|
+
return outcome;
|
|
44
|
+
}
|
|
45
|
+
return unhandled();
|
|
46
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话命令组:/clear · /sessions · /resume · /rollback
|
|
3
|
+
*
|
|
4
|
+
* /clear 是这批里唯一**写**闭包状态的命令(currentSessionId / turnCount / lastTurnUsage),
|
|
5
|
+
* 全部经 ctx.state 访问器写回 startRepl 的 `let`。
|
|
6
|
+
*/
|
|
7
|
+
import * as layout from '../../ui/layout.js';
|
|
8
|
+
import { ui } from '../../ui/theme.js';
|
|
9
|
+
import { bannerLines } from '../../ui/render.js';
|
|
10
|
+
import { t } from '../../i18n/index.js';
|
|
11
|
+
import { newSessionId, listSessions } from '../../session/index.js';
|
|
12
|
+
import { resetState } from '../../rollback/index.js';
|
|
13
|
+
import { setCurrentSessionId } from '../../session/state.js';
|
|
14
|
+
import { promptSessionPicker } from '../../ui/prompt.js';
|
|
15
|
+
import { unhandled, next } from './types.js';
|
|
16
|
+
export const sessionCommands = [
|
|
17
|
+
(ctx) => {
|
|
18
|
+
if (ctx.line !== '/clear')
|
|
19
|
+
return unhandled();
|
|
20
|
+
ctx.history.length = 1; // 保留 system 提示
|
|
21
|
+
resetState(); // 同步清空回滚轮次/快照
|
|
22
|
+
// /clear 立刻换新会话 id:之前延后到 turn 收尾分配,但 runAgent 期间模型
|
|
23
|
+
// 调 plan_update / note_append 会命中「no active session」——它们写 notes.md 靠
|
|
24
|
+
// getNotesFilePath(getCurrentSessionId()),而下一个 turn 还没走完。改为与启动对齐
|
|
25
|
+
// 立即分配:notes.md 由写入时按需建文件,这里预分配 id 不触盘。
|
|
26
|
+
ctx.state.currentSessionId = newSessionId();
|
|
27
|
+
setCurrentSessionId(ctx.state.currentSessionId, process.cwd()); // 同步到 session/state
|
|
28
|
+
ctx.state.turnCount = 0; // 反思 cadence 重新计数
|
|
29
|
+
ctx.contextState.lastUsage = undefined;
|
|
30
|
+
ctx.contextState.lifecycleStats = undefined;
|
|
31
|
+
ctx.contextState.ephemeralText = undefined;
|
|
32
|
+
ctx.state.lastTurnUsage = undefined; // 清空旧轮的 token 累计
|
|
33
|
+
ctx.state.lastToolGroups = []; // 新会话不得继承旧会话的路由回退
|
|
34
|
+
ctx.attachments.clear(); // 一并清空待发图片
|
|
35
|
+
layout.clearContent();
|
|
36
|
+
layout.writeBanner(bannerLines(ctx.banner()));
|
|
37
|
+
layout.contentWrite(`${ui.dim}${t('repl.historyCleared')}${ui.reset}\n`);
|
|
38
|
+
layout.writeWelcomeBlock(ctx.welcomeLines()); // 回到空会话状态,欢迎引导重新出现
|
|
39
|
+
return next();
|
|
40
|
+
},
|
|
41
|
+
// /sessions:浏览全部已保存会话(慢路径,readdir+全量 JSON.parse,目录 N 大时会有可感知卡顿)。
|
|
42
|
+
// 默认走 /resume(仅最近 10 条,瞬开);要翻历史续接更早的会话才用这条。
|
|
43
|
+
// picker 走全显(cap=items.length,无 a 展开提示),靠 picker 自身开窗(以选中为中心分屏)。
|
|
44
|
+
async (ctx) => {
|
|
45
|
+
if (ctx.line !== '/sessions')
|
|
46
|
+
return unhandled();
|
|
47
|
+
const sessions = listSessions(); // 不传 limit = 全量
|
|
48
|
+
if (sessions.length === 0) {
|
|
49
|
+
layout.contentWrite(`${ui.dim}(没有已保存的会话)${ui.reset}\n`);
|
|
50
|
+
return next();
|
|
51
|
+
}
|
|
52
|
+
const items = sessions.map((s) => ({
|
|
53
|
+
id: s.id,
|
|
54
|
+
title: s.firstUser || '(无)',
|
|
55
|
+
subtitle: `${s.id} ${s.model}`,
|
|
56
|
+
}));
|
|
57
|
+
let pick;
|
|
58
|
+
try {
|
|
59
|
+
pick = await promptSessionPicker(items, items.length);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return next(); // Ctrl+C(SIGINT)→ 取消
|
|
63
|
+
}
|
|
64
|
+
await ctx.resumeFromPick(pick);
|
|
65
|
+
return next();
|
|
66
|
+
},
|
|
67
|
+
// /resume:打开会话菜单(↑/↓ 选,Enter 续接,Esc 取消)。只加载最近 10 条,
|
|
68
|
+
// 避免 sessions 目录堆了几百个会话时 readdir+全量 JSON.parse 卡顿。
|
|
69
|
+
// 仿 /rollback 菜单化(promptSessionPicker);选中项 cyan+bold + ▸ 高亮。
|
|
70
|
+
// 要续接更早的会话请用 /sessions 翻全表,或 CLI `mocode --resume <id>`。
|
|
71
|
+
async (ctx) => {
|
|
72
|
+
if (ctx.line !== '/resume')
|
|
73
|
+
return unhandled();
|
|
74
|
+
const sessions = listSessions(10);
|
|
75
|
+
if (sessions.length === 0) {
|
|
76
|
+
layout.contentWrite(`${ui.dim}(没有已保存的会话)${ui.reset}\n`);
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
const items = sessions.map((s) => ({
|
|
80
|
+
id: s.id,
|
|
81
|
+
title: s.firstUser || '(无)',
|
|
82
|
+
subtitle: `${s.id} ${s.model}`,
|
|
83
|
+
}));
|
|
84
|
+
let pick;
|
|
85
|
+
try {
|
|
86
|
+
pick = await promptSessionPicker(items);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return next(); // Ctrl+C(SIGINT)→ 取消
|
|
90
|
+
}
|
|
91
|
+
await ctx.resumeFromPick(pick);
|
|
92
|
+
return next();
|
|
93
|
+
},
|
|
94
|
+
// /rollback:打开轮次菜单(↑/↓ 选,Enter 回滚到该轮并预填其输入,再 Enter 重新跑)。
|
|
95
|
+
// 忽略任何数字参数(原「输数字选回滚」已删,统一走菜单)。无快照的旧轮次(/resume 重建)文件改动不可撤销。
|
|
96
|
+
async (ctx) => {
|
|
97
|
+
if (ctx.line !== '/rollback' && !ctx.line.startsWith('/rollback '))
|
|
98
|
+
return unhandled();
|
|
99
|
+
await ctx.rollbackFlow();
|
|
100
|
+
return next();
|
|
101
|
+
},
|
|
102
|
+
];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill 命令组:/skills · /skill <name> [args-json]
|
|
3
|
+
*
|
|
4
|
+
* 顺序敏感:`/skills` 必须排在 `/skill` 之前——`/skill` 用 startsWith('/skill ')
|
|
5
|
+
* 匹配带参形式,裸 '/skills' 不会命中它,但保持原 if 链的相对顺序最稳妥。
|
|
6
|
+
*
|
|
7
|
+
* /skill 的 inline 分支只预览:正文不进模型 history,不激活工具面约束。
|
|
8
|
+
*/
|
|
9
|
+
import * as layout from '../../ui/layout.js';
|
|
10
|
+
import { ui } from '../../ui/theme.js';
|
|
11
|
+
import { listSkills, findSkill } from '../../skills/index.js';
|
|
12
|
+
import { runSkill, renderSkillBody } from '../../skills/runner.js';
|
|
13
|
+
import { isSkillTrusted } from '../../skills/trust.js';
|
|
14
|
+
import { unhandled, next } from './types.js';
|
|
15
|
+
export const skillCommands = [
|
|
16
|
+
(ctx) => {
|
|
17
|
+
if (ctx.line !== '/skills')
|
|
18
|
+
return unhandled();
|
|
19
|
+
const skills = listSkills();
|
|
20
|
+
if (skills.length === 0) {
|
|
21
|
+
layout.contentWrite(`${ui.dim}(没有已发现的 skill)${ui.reset}\n`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
layout.contentWrite(`${ui.dim}已发现 ${skills.length} 个 skill:${ui.reset}\n`);
|
|
25
|
+
for (const s of skills) {
|
|
26
|
+
const badges = [];
|
|
27
|
+
if (s.context === 'fork')
|
|
28
|
+
badges.push('fork');
|
|
29
|
+
if (!s.modelInvocable)
|
|
30
|
+
badges.push('manual-only');
|
|
31
|
+
badges.push(s.origin);
|
|
32
|
+
if (s.origin === 'project') {
|
|
33
|
+
badges.push(isSkillTrusted(s) ? 'trusted' : 'untrusted');
|
|
34
|
+
}
|
|
35
|
+
const badgeStr = badges.length ? ` ${ui.dim}[${badges.join('|')}]${ui.reset}` : '';
|
|
36
|
+
layout.contentWrite(` ${ui.accent}${s.name}${ui.reset}${badgeStr} ${ui.dim}${s.description}${ui.reset}\n`);
|
|
37
|
+
if (s.allowedTools?.length) {
|
|
38
|
+
layout.contentWrite(` ${ui.dim}allowed: ${s.allowedTools.join(', ')}${ui.reset}\n`);
|
|
39
|
+
}
|
|
40
|
+
if (s.disallowedTools?.length) {
|
|
41
|
+
layout.contentWrite(` ${ui.dim}disallowed: ${s.disallowedTools.join(', ')}${ui.reset}\n`);
|
|
42
|
+
}
|
|
43
|
+
if (s.warnings.length) {
|
|
44
|
+
layout.contentWrite(` ${ui.dim}warnings: ${s.warnings.join('; ')}${ui.reset}\n`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
layout.contentWrite(`${ui.dim}(用 use_skill 加载指令; fork 类用 run_skill 执行; 也支持 /skill <name> [args-json])${ui.reset}\n`);
|
|
48
|
+
}
|
|
49
|
+
return next();
|
|
50
|
+
},
|
|
51
|
+
// /skill <name> [args-json]:直接执行一个 skill(fork 走 run_skill,inline 打印渲染后正文)。
|
|
52
|
+
async (ctx) => {
|
|
53
|
+
const { line } = ctx;
|
|
54
|
+
if (line !== '/skill' && !line.startsWith('/skill '))
|
|
55
|
+
return unhandled();
|
|
56
|
+
const rest = line.slice('/skill'.length).trim();
|
|
57
|
+
const sp = rest.indexOf(' ');
|
|
58
|
+
const name = sp === -1 ? rest : rest.slice(0, sp);
|
|
59
|
+
const argStr = sp === -1 ? '' : rest.slice(sp + 1).trim();
|
|
60
|
+
if (!name) {
|
|
61
|
+
layout.contentWrite(`${ui.dim}用法: /skill <name> [args-json]${ui.reset}\n`);
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
let args;
|
|
65
|
+
if (argStr) {
|
|
66
|
+
try {
|
|
67
|
+
args = JSON.parse(argStr);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
layout.contentWrite(`${ui.dim}args 不是合法 JSON,已忽略。${ui.reset}\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const skill = findSkill(name);
|
|
74
|
+
if (!skill) {
|
|
75
|
+
layout.contentWrite(`错误:未找到 skill "${name}"。\n`);
|
|
76
|
+
return next();
|
|
77
|
+
}
|
|
78
|
+
if (skill.context === 'fork') {
|
|
79
|
+
layout.contentWrite(`${ui.dim}执行 fork skill "${name}"…${ui.reset}\n`);
|
|
80
|
+
const out = await runSkill({ name, args });
|
|
81
|
+
layout.contentWrite((out.status === 'success' ? '' : `[${out.status}] `) + out.output + '\n');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const body = await renderSkillBody(skill, args);
|
|
85
|
+
if (body === null)
|
|
86
|
+
layout.contentWrite(`错误:未找到 skill "${name}" 的正文。\n`);
|
|
87
|
+
else {
|
|
88
|
+
// 仅预览:inline 正文未进入模型 history,不激活工具面约束;
|
|
89
|
+
// 要让模型按此 skill 工作,请让它调 use_skill(name) 或在下条消息里提及该 skill。
|
|
90
|
+
layout.contentWrite(`# Skill: ${name}(预览,模型尚未看到此内容)\n\n${body}\n`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return next();
|
|
94
|
+
},
|
|
95
|
+
];
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统命令组:/help · /language · /init · /upgrade
|
|
3
|
+
*
|
|
4
|
+
* 控制流改写说明(/upgrade now 成功路径):
|
|
5
|
+
* 原分支在 try 块内直接 `break` 跳出 while 主循环(finally 里 stopRunningListener 仍执行)。
|
|
6
|
+
* 搬进 handler 后无法 break 调用方的循环,改为置 `exitAfter = true`,handler 返回
|
|
7
|
+
* outcome.kind === 'exit',runtime.ts 的 switch 负责 break——finally 时序不变。
|
|
8
|
+
*/
|
|
9
|
+
import * as layout from '../../ui/layout.js';
|
|
10
|
+
import { ui } from '../../ui/theme.js';
|
|
11
|
+
import { bannerLines } from '../../ui/render.js';
|
|
12
|
+
import { t, getLanguage, normalizeLanguage } from '../../i18n/index.js';
|
|
13
|
+
import { updateLanguageConfig, languageFromShell, hasCodegraphIndex } from '../../config/index.js';
|
|
14
|
+
import { updateConfigKey } from '../../config/file.js';
|
|
15
|
+
import { getAgentMode } from '../../agent/mode.js';
|
|
16
|
+
import { buildSlashCommands, slashHelpLines, HELP_GROUPS } from '../commands.js';
|
|
17
|
+
import { startRunningListener, stopRunningListener } from '../running-input.js';
|
|
18
|
+
import { checkVersion, fetchLatestVersion, getCurrentVersion, runUpgradeForeground } from '../../commands/upgrade.js';
|
|
19
|
+
import { unhandled, next, exit, forward } from './types.js';
|
|
20
|
+
/**
|
|
21
|
+
* /init 指令:发给 agent 扫描项目并生成 AGENTS.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
22
|
+
*
|
|
23
|
+
* 函数化(非 const):.codegraph/ 索引是否存在的探测放在调用瞬间,没索引时不提 codegraph,
|
|
24
|
+
* 避免 LLM 调出失败。/init 是冷启动动作,IO 开销可忽略。
|
|
25
|
+
*/
|
|
26
|
+
function buildInitPrompt() {
|
|
27
|
+
const cg = hasCodegraphIndex()
|
|
28
|
+
? '- 若有 .codegraph/:用 use_skill 加载 codegraph skill 后用 run_command 调 codegraph explore "<架构或入口符号>" 一次拿相关源码+调用路径,别逐文件读!!!\n'
|
|
29
|
+
: '';
|
|
30
|
+
return `分析当前项目(process.cwd()),生成 AGENTS.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
|
|
31
|
+
|
|
32
|
+
先探查(尽量少调用拿全貌):
|
|
33
|
+
${cg}- read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
|
|
34
|
+
- glob 顶层目录;read_file 入口文件 + 各子系统 index.ts/README。
|
|
35
|
+
- 若 AGENTS.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
|
|
36
|
+
|
|
37
|
+
AGENTS.md 按以下结构写(每节简短,只写稳定、非显然的事实):
|
|
38
|
+
## 项目
|
|
39
|
+
一两句:是什么、技术栈、运行环境。
|
|
40
|
+
## 命令
|
|
41
|
+
install / dev / build / test / typecheck / lint 等——从 package.json scripts 提炼,写原样命令行(如 \`npm run typecheck\`);没有的注明"无测试"/"无 lint"。
|
|
42
|
+
## 目录结构
|
|
43
|
+
顶层各目录与子系统职责,一句话/个;不逐文件列。
|
|
44
|
+
## 约定
|
|
45
|
+
从代码与现有文档提炼的硬约定:模块系统(ESM?)、命名、错误处理、工具/函数契约、易踩坑点。只写非显然、会让人踩坑的;不写"保持简洁"这种正确废话。
|
|
46
|
+
## 扩展点
|
|
47
|
+
加工具/命令/provider/模块的接缝(改哪个文件、加在哪)。
|
|
48
|
+
|
|
49
|
+
硬要求:
|
|
50
|
+
- 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
|
|
51
|
+
- 总长 ≤ 3000 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
|
|
52
|
+
- 用 write_file 写入项目根 AGENTS.md。
|
|
53
|
+
- 写完简述:写了哪几节 + 从代码里发现的 2-3 条非显然关键约定(供用户校验)。`;
|
|
54
|
+
}
|
|
55
|
+
export const systemCommands = [
|
|
56
|
+
(ctx) => {
|
|
57
|
+
if (ctx.line !== '/help')
|
|
58
|
+
return unhandled();
|
|
59
|
+
const nodes = buildSlashCommands();
|
|
60
|
+
layout.contentWrite(`${ui.bold}${t('help.title')}${ui.reset}\n`);
|
|
61
|
+
layout.contentWrite(`${ui.dim}${t('help.hint')}${ui.reset}\n`);
|
|
62
|
+
// 按使用场景分组输出,常用置顶;未归组命令(如 /exit)兜底进「其他」。
|
|
63
|
+
let first = true;
|
|
64
|
+
for (const group of HELP_GROUPS) {
|
|
65
|
+
const picked = nodes.filter((n) => group.names.includes(n.name));
|
|
66
|
+
if (!picked.length)
|
|
67
|
+
continue;
|
|
68
|
+
if (!first)
|
|
69
|
+
layout.contentWrite('\n');
|
|
70
|
+
first = false;
|
|
71
|
+
layout.contentWrite(`${ui.accent}${ui.bold}${t(group.key)}${ui.reset}\n`);
|
|
72
|
+
layout.contentWrite(`${slashHelpLines(picked).join('\n')}\n`);
|
|
73
|
+
}
|
|
74
|
+
const rest = nodes.filter((n) => !HELP_GROUPS.some((g) => g.names.includes(n.name)));
|
|
75
|
+
if (rest.length) {
|
|
76
|
+
if (!first)
|
|
77
|
+
layout.contentWrite('\n');
|
|
78
|
+
layout.contentWrite(`${ui.accent}${ui.bold}${t('help.groupOther')}${ui.reset}\n`);
|
|
79
|
+
layout.contentWrite(`${slashHelpLines(rest).join('\n')}\n`);
|
|
80
|
+
}
|
|
81
|
+
return next();
|
|
82
|
+
},
|
|
83
|
+
(ctx) => {
|
|
84
|
+
const { line } = ctx;
|
|
85
|
+
if (line !== '/language' && !line.startsWith('/language '))
|
|
86
|
+
return unhandled();
|
|
87
|
+
const arg = line === '/language' ? '' : line.slice('/language '.length).trim();
|
|
88
|
+
if (!arg) {
|
|
89
|
+
const currentName = getLanguage() === 'zh-CN' ? t('language.zh') : t('language.en');
|
|
90
|
+
layout.contentWrite(`${ui.dim}${t('language.current', { language: currentName })}${ui.reset}\n`);
|
|
91
|
+
layout.contentWrite(`${ui.dim}${t('language.usage')}${ui.reset}\n\n`);
|
|
92
|
+
return next();
|
|
93
|
+
}
|
|
94
|
+
const nextLang = normalizeLanguage(arg);
|
|
95
|
+
if (!nextLang) {
|
|
96
|
+
layout.contentWrite(`${ui.yellow}${t('language.invalid', { value: arg })}${ui.reset}\n\n`);
|
|
97
|
+
return next();
|
|
98
|
+
}
|
|
99
|
+
updateLanguageConfig(nextLang);
|
|
100
|
+
updateConfigKey('MOCODE_LANGUAGE', nextLang);
|
|
101
|
+
ctx.history[0] = { role: 'system', content: ctx.buildSystemMessage(getAgentMode() === 'plan') };
|
|
102
|
+
ctx.refreshStatusBase(ctx.history);
|
|
103
|
+
// 横幅是内容缓冲顶部的固定区域;语言切换后等长原地替换,不移动后续对话。
|
|
104
|
+
layout.rewriteBanner(bannerLines(ctx.banner()));
|
|
105
|
+
layout.contentWrite(`${ui.cyan}${t('language.changed')}${ui.reset}\n`);
|
|
106
|
+
if (languageFromShell) {
|
|
107
|
+
layout.contentWrite(`${ui.dim}${t('language.shellOverride')}${ui.reset}\n`);
|
|
108
|
+
}
|
|
109
|
+
layout.contentWrite('\n');
|
|
110
|
+
return next();
|
|
111
|
+
},
|
|
112
|
+
(ctx) => {
|
|
113
|
+
if (ctx.line !== '/init')
|
|
114
|
+
return unhandled();
|
|
115
|
+
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 AGENTS.md),fall through 走 runAgent
|
|
116
|
+
return forward(buildInitPrompt());
|
|
117
|
+
},
|
|
118
|
+
async (ctx) => {
|
|
119
|
+
const { line } = ctx;
|
|
120
|
+
if (line !== '/upgrade' && !line.startsWith('/upgrade '))
|
|
121
|
+
return unhandled();
|
|
122
|
+
const arg = line === '/upgrade' ? '' : line.slice('/upgrade '.length).trim().toLowerCase();
|
|
123
|
+
// /upgrade check — 联网检查当前版本与最新版本差异
|
|
124
|
+
if (arg === 'check') {
|
|
125
|
+
startRunningListener(t('running.upgrading'));
|
|
126
|
+
try {
|
|
127
|
+
const info = await checkVersion();
|
|
128
|
+
if (!info.latest) {
|
|
129
|
+
layout.contentWrite(`${ui.yellow}⚠ ${t('upgrade.fetchFailed')}${ui.reset}\n`);
|
|
130
|
+
}
|
|
131
|
+
else if (info.hasUpdate) {
|
|
132
|
+
layout.contentWrite(`${ui.cyan}● ${t('upgrade.hasUpdate', { current: info.current, latest: info.latest })}${ui.reset}\n`);
|
|
133
|
+
layout.contentWrite(`${ui.dim}${t('upgrade.checkHint')}${ui.reset}\n`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
layout.contentWrite(`${ui.green}✓ ${t('upgrade.noUpdate', { version: info.current })}${ui.reset}\n`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
141
|
+
layout.contentWrite(`${ui.red}${t('upgrade.failed', { message: msg })}${ui.reset}\n`);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
stopRunningListener();
|
|
145
|
+
}
|
|
146
|
+
return next();
|
|
147
|
+
}
|
|
148
|
+
// /upgrade status — 只显示本地当前版本(不联网)
|
|
149
|
+
if (arg === 'status') {
|
|
150
|
+
const current = getCurrentVersion();
|
|
151
|
+
layout.contentWrite(`${ui.dim}${t('upgrade.currentVersion', { version: `v${current}` })}${ui.reset}\n`);
|
|
152
|
+
layout.contentWrite(`${ui.dim}${t('upgrade.pkgName')}${ui.reset}\n`);
|
|
153
|
+
return next();
|
|
154
|
+
}
|
|
155
|
+
// /upgrade 或 /upgrade now — 前台执行升级,实时显示 npm 输出
|
|
156
|
+
if (arg === '' || arg === 'now') {
|
|
157
|
+
// 原实现升级成功后在 try 内 `break` 跳出 REPL 主循环;handler 无法 break 调用方,
|
|
158
|
+
// 改为置标志 + 返回 exit,由 runtime.ts 的 switch 执行 break(finally 时序不变)。
|
|
159
|
+
let exitAfterUpgrade = false;
|
|
160
|
+
startRunningListener(t('running.upgrading'));
|
|
161
|
+
try {
|
|
162
|
+
const latest = await fetchLatestVersion();
|
|
163
|
+
const current = getCurrentVersion();
|
|
164
|
+
if (latest && latest === current) {
|
|
165
|
+
layout.contentWrite(`${ui.green}✓ ${t('upgrade.noUpdate', { version: current })}${ui.reset}\n`);
|
|
166
|
+
return next();
|
|
167
|
+
}
|
|
168
|
+
const target = latest ?? 'latest';
|
|
169
|
+
layout.contentWrite(`${ui.cyan}● ${t('upgrade.begin', { version: target })}${ui.reset}\n`);
|
|
170
|
+
const result = await runUpgradeForeground((chunk) => {
|
|
171
|
+
layout.contentWrite(chunk);
|
|
172
|
+
});
|
|
173
|
+
if (result.ok) {
|
|
174
|
+
layout.contentWrite(`\n${ui.green}${ui.bold}✓ ${t('upgrade.completed')}${ui.reset}\n`);
|
|
175
|
+
// 给用户 2.5 秒看清提示,然后自动退出,下次启动即使用新版本。
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
177
|
+
exitAfterUpgrade = true;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
layout.contentWrite(`${ui.red}✗ ${t('upgrade.failedWithCode', { code: String(result.exitCode ?? 'unknown') })}${ui.reset}\n`);
|
|
181
|
+
if (result.output.includes('ETARGET') || result.output.includes('No matching version found')) {
|
|
182
|
+
layout.contentWrite(`${ui.yellow}${t('upgrade.etargetHint')}${ui.reset}\n`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
layout.contentWrite(`${ui.dim}${t('upgrade.manualHint')}${ui.reset}\n`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
191
|
+
layout.contentWrite(`${ui.red}${t('upgrade.failed', { message: msg })}${ui.reset}\n`);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
stopRunningListener();
|
|
195
|
+
}
|
|
196
|
+
return exitAfterUpgrade ? exit() : next();
|
|
197
|
+
}
|
|
198
|
+
// 未知子命令
|
|
199
|
+
layout.contentWrite(`${ui.yellow}${t('upgrade.usage')}${ui.reset}\n`);
|
|
200
|
+
return next();
|
|
201
|
+
},
|
|
202
|
+
];
|