mocode-ai 0.7.0 → 0.7.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.
- package/README.md +4 -3
- package/README.zh-CN.md +3 -3
- package/dist/agent/core.js +162 -108
- package/dist/agent/index.js +20 -0
- package/dist/agent/spawn.js +9 -1
- package/dist/config/index.js +12 -0
- package/dist/i18n/index.js +34 -0
- package/dist/llm/index.js +6 -2
- package/dist/mcp/index.js +15 -2
- package/dist/permissions/index.js +110 -93
- package/dist/repl/index.js +39 -5
- package/dist/rollback/index.js +31 -0
- package/dist/session/index.js +1 -0
- package/dist/session/trace.js +26 -0
- package/dist/tools/builtins/index.js +35 -4
- package/dist/tools/builtins/run-command.js +115 -66
- package/dist/tools/builtins/task.js +5 -2
- package/dist/tools/constants.js +5 -1
- package/dist/tools/registry.js +108 -25
- package/dist/verification/affected.js +149 -0
- package/dist/verification/discovery.js +40 -0
- package/dist/verification/index.js +163 -0
- package/dist/verification/profile.js +237 -0
- package/dist/verification/types.js +1 -0
- package/package.json +5 -2
package/dist/i18n/index.js
CHANGED
|
@@ -19,6 +19,10 @@ const zhCN = {
|
|
|
19
19
|
'commands.memoryStatus': '查看当前开关与原理',
|
|
20
20
|
'commands.memoryReflect': '手动触发后台记忆反思',
|
|
21
21
|
'commands.memoryInit': '扫描项目生成 MOCODE.md 项目记忆',
|
|
22
|
+
'commands.subagent': '子 Agent 开关(默认关闭)',
|
|
23
|
+
'commands.subagentOn': '开启子 Agent',
|
|
24
|
+
'commands.subagentOff': '关闭子 Agent',
|
|
25
|
+
'commands.subagentStatus': '查看子 Agent 状态',
|
|
22
26
|
'commands.skill': '项目专属 Skill 管理',
|
|
23
27
|
'commands.toggle': '切换开关',
|
|
24
28
|
'commands.skillOn': '开启项目 Skill',
|
|
@@ -84,6 +88,7 @@ const zhCN = {
|
|
|
84
88
|
'running.memory': '切记忆开关',
|
|
85
89
|
'running.switching': '切换中…',
|
|
86
90
|
'running.memoryStatus': '查记忆状态',
|
|
91
|
+
'running.subagent': '子 Agent',
|
|
87
92
|
'running.skill': '项目 Skill',
|
|
88
93
|
'running.language': '切语言',
|
|
89
94
|
'running.chooseLanguage': '选择语言…',
|
|
@@ -160,6 +165,9 @@ const zhCN = {
|
|
|
160
165
|
'agent.noReply': '(无回复)',
|
|
161
166
|
'agent.maxSteps': '达到最大步数({count}),本轮停止。',
|
|
162
167
|
'agent.aborted': '(已中断)',
|
|
168
|
+
'agent.validating': '自动验证 {command}',
|
|
169
|
+
'agent.validationNoCommand': '未发现验证命令',
|
|
170
|
+
'agent.validationResult': '自动验证 {command} → {status}',
|
|
163
171
|
'agent.workedFor': '耗时 {elapsed}',
|
|
164
172
|
'toolSummary.lines': '{count} 行',
|
|
165
173
|
'toolSummary.files': '{count} 个文件',
|
|
@@ -191,13 +199,22 @@ const zhCN = {
|
|
|
191
199
|
'permission.deny': '拒绝',
|
|
192
200
|
'permission.allow': '允许',
|
|
193
201
|
'permission.allowSession': '本次会话始终允许此工具',
|
|
202
|
+
'permission.allowSessionResource': '本次会话允许相同命令/资源',
|
|
203
|
+
'permission.allowProjectResource': '对此项目永久允许相同命令/资源',
|
|
194
204
|
'askHuman.cancelled': '用户取消了选择。请考虑是否有不依赖用户输入的替代方案,或换个角度重新提问。',
|
|
195
205
|
'askHuman.submitted': '用户回答:{value}',
|
|
196
206
|
'askHuman.selected': '用户选择:{value}',
|
|
197
207
|
'task.missingPrompt': '错误:缺少 prompt(子任务指令)。',
|
|
208
|
+
'task.disabled': '错误:子 Agent 当前已关闭。需要时请先运行 /subagent on。',
|
|
198
209
|
'task.interrupted': '子 agent 被中断,未完成。',
|
|
199
210
|
'task.noSummary': '子 agent 完成但未返回文本摘要(可能只调了工具或达到步数上限)。',
|
|
200
211
|
'task.summaryTruncated': '…(子 agent 摘要已截断 {count} 字符)',
|
|
212
|
+
'subagent.status': '子 Agent:{state}',
|
|
213
|
+
'subagent.stateOn': '开启',
|
|
214
|
+
'subagent.stateOff': '关闭',
|
|
215
|
+
'subagent.changedOn': '已开启子 Agent;task 将从下一次模型请求起可用。',
|
|
216
|
+
'subagent.changedOff': '已关闭子 Agent;task 已从模型工具表移除。',
|
|
217
|
+
'subagent.usage': '用法:/subagent on|off|status',
|
|
201
218
|
'plan.ready': '计划已就绪',
|
|
202
219
|
'plan.approvalDetail': '切换到 auto 模式按上述计划执行?(plan 模式只读探查,执行需切 auto)',
|
|
203
220
|
'plan.execute': '切 auto 执行',
|
|
@@ -228,6 +245,10 @@ const en = {
|
|
|
228
245
|
'commands.memoryStatus': 'Show current state and behavior',
|
|
229
246
|
'commands.memoryReflect': 'Run background memory reflection',
|
|
230
247
|
'commands.memoryInit': 'Scan project and generate MOCODE.md',
|
|
248
|
+
'commands.subagent': 'Sub-agent controls (disabled by default)',
|
|
249
|
+
'commands.subagentOn': 'Enable sub-agents',
|
|
250
|
+
'commands.subagentOff': 'Disable sub-agents',
|
|
251
|
+
'commands.subagentStatus': 'Show sub-agent status',
|
|
231
252
|
'commands.skill': 'Manage the project-specific Skill',
|
|
232
253
|
'commands.toggle': 'Toggle the feature',
|
|
233
254
|
'commands.skillOn': 'Enable the project Skill',
|
|
@@ -293,6 +314,7 @@ const en = {
|
|
|
293
314
|
'running.memory': 'Memory',
|
|
294
315
|
'running.switching': 'Switching…',
|
|
295
316
|
'running.memoryStatus': 'Memory status',
|
|
317
|
+
'running.subagent': 'Sub-agent',
|
|
296
318
|
'running.skill': 'Project Skill',
|
|
297
319
|
'running.language': 'Language',
|
|
298
320
|
'running.chooseLanguage': 'Choose a language…',
|
|
@@ -369,6 +391,9 @@ const en = {
|
|
|
369
391
|
'agent.noReply': '(no reply)',
|
|
370
392
|
'agent.maxSteps': 'Maximum steps reached ({count}); this turn has stopped.',
|
|
371
393
|
'agent.aborted': '(aborted)',
|
|
394
|
+
'agent.validating': 'Validating {command}',
|
|
395
|
+
'agent.validationNoCommand': 'no validation command',
|
|
396
|
+
'agent.validationResult': 'Automatic validation {command} → {status}',
|
|
372
397
|
'agent.workedFor': 'Worked for {elapsed}',
|
|
373
398
|
'toolSummary.lines': '{count} lines',
|
|
374
399
|
'toolSummary.files': '{count} files',
|
|
@@ -400,13 +425,22 @@ const en = {
|
|
|
400
425
|
'permission.deny': 'Deny',
|
|
401
426
|
'permission.allow': 'Allow',
|
|
402
427
|
'permission.allowSession': 'Always allow this tool for this session',
|
|
428
|
+
'permission.allowSessionResource': 'Allow this command/resource for this session',
|
|
429
|
+
'permission.allowProjectResource': 'Always allow this command/resource for this project',
|
|
403
430
|
'askHuman.cancelled': 'The user cancelled the choice. Consider an alternative that does not require user input, or ask from a different angle.',
|
|
404
431
|
'askHuman.submitted': 'User response: {value}',
|
|
405
432
|
'askHuman.selected': 'User selected: {value}',
|
|
406
433
|
'task.missingPrompt': 'Error: missing prompt (sub-task instruction).',
|
|
434
|
+
'task.disabled': 'Error: sub-agents are disabled. Run /subagent on first if needed.',
|
|
407
435
|
'task.interrupted': 'The sub-agent was interrupted before completion.',
|
|
408
436
|
'task.noSummary': 'The sub-agent completed without a text summary (it may only have used tools or reached its step limit).',
|
|
409
437
|
'task.summaryTruncated': '…(sub-agent summary truncated by {count} characters)',
|
|
438
|
+
'subagent.status': 'Sub-agent: {state}',
|
|
439
|
+
'subagent.stateOn': 'enabled',
|
|
440
|
+
'subagent.stateOff': 'disabled',
|
|
441
|
+
'subagent.changedOn': 'Sub-agents enabled; task will be available from the next model request.',
|
|
442
|
+
'subagent.changedOff': 'Sub-agents disabled; task has been removed from the model tool list.',
|
|
443
|
+
'subagent.usage': 'Usage: /subagent on|off|status',
|
|
410
444
|
'plan.ready': 'Plan ready',
|
|
411
445
|
'plan.approvalDetail': 'Switch to auto mode and execute the plan above? (Plan mode is read-only; execution requires auto mode.)',
|
|
412
446
|
'plan.execute': 'Switch to auto and execute',
|
package/dist/llm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import { config } from '../config/index.js';
|
|
2
|
+
import { config, isSubAgentEnabled } from '../config/index.js';
|
|
3
3
|
import { tools } from '../tools/registry.js';
|
|
4
4
|
import { getPlanDisabledTools } from '../tools/constants.js';
|
|
5
5
|
import { ThinkTagFilter } from './think-filter.js';
|
|
@@ -149,7 +149,11 @@ export function __setChatCreateImpl(impl) {
|
|
|
149
149
|
export const chatTools = [];
|
|
150
150
|
export const planChatTools = [];
|
|
151
151
|
export function refreshChatTools() {
|
|
152
|
-
|
|
152
|
+
// task 常驻内部 registry,运行时开关只控制模型可见 schema,因而 on/off 可即时生效。
|
|
153
|
+
const visibleTools = isSubAgentEnabled()
|
|
154
|
+
? tools
|
|
155
|
+
: tools.filter((tool) => tool.name !== 'task');
|
|
156
|
+
const next = visibleTools.map((t) => ({
|
|
153
157
|
type: 'function',
|
|
154
158
|
function: {
|
|
155
159
|
name: t.name,
|
package/dist/mcp/index.js
CHANGED
|
@@ -41,9 +41,22 @@ export function getMcpTools() {
|
|
|
41
41
|
parameters: remote.inputSchema && typeof remote.inputSchema === 'object'
|
|
42
42
|
? remote.inputSchema
|
|
43
43
|
: { type: 'object', properties: {} },
|
|
44
|
-
//
|
|
44
|
+
// MCP 协议没有可靠副作用声明:未知能力、workspace 串行、每次确认。
|
|
45
45
|
risk: 'dangerous',
|
|
46
|
-
|
|
46
|
+
capabilities: {
|
|
47
|
+
effect: 'unknown',
|
|
48
|
+
concurrency: 'serial',
|
|
49
|
+
retry: 'never',
|
|
50
|
+
resources: () => ['workspace'],
|
|
51
|
+
supportsAbort: true,
|
|
52
|
+
},
|
|
53
|
+
execute: async (args, ctx) => {
|
|
54
|
+
const result = await client.callTool(remote.name, args, ctx?.signal);
|
|
55
|
+
const output = formatToolResult(result);
|
|
56
|
+
return result.isError
|
|
57
|
+
? { status: 'error', code: 'MCP_ERROR', retryable: false, output }
|
|
58
|
+
: { status: 'success', code: 'OK', retryable: false, output };
|
|
59
|
+
},
|
|
47
60
|
});
|
|
48
61
|
}
|
|
49
62
|
}
|
|
@@ -1,142 +1,159 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { promptIntervention } from '../ui/intervention.js';
|
|
5
6
|
import { config } from '../config/index.js';
|
|
7
|
+
import { getSandboxRoot } from '../sandbox/index.js';
|
|
6
8
|
import { t } from '../i18n/index.js';
|
|
7
|
-
/**
|
|
8
|
-
* 工具权限系统:基于 risk 字段在执行前拦截确认。
|
|
9
|
-
*
|
|
10
|
-
* 设计:
|
|
11
|
-
* - safe → 直接放行(只读工具,零交互)
|
|
12
|
-
* - confirm → 弹面板确认,同工具同会话缓存(避免重复打断)
|
|
13
|
-
* - dangerous → 每次都弹(高风险,命令内容不可预测)
|
|
14
|
-
*
|
|
15
|
-
* 三层允许(优先级从高到低):
|
|
16
|
-
* 1. 永久允许(permanentAllow,跨会话持久化到 ~/.mocode/permissions.json)
|
|
17
|
-
* 2. 会话允许(approvedTools,本次进程内缓存)
|
|
18
|
-
* 3. 面板询问(promptIntervention,复用 ask_human UI)
|
|
19
|
-
*
|
|
20
|
-
* 复用 promptIntervention:统一 UI 面板,非 TTY 自动降级(第一项 = 允许)。
|
|
21
|
-
*/
|
|
22
|
-
/** 跨会话持久化允许列表路径 */
|
|
23
9
|
const PERMISSIONS_PATH = path.join(os.homedir(), '.mocode', 'permissions.json');
|
|
24
|
-
|
|
25
|
-
let permanentAllow = new Set();
|
|
10
|
+
let permanentGrants = [];
|
|
26
11
|
let permanentLoaded = false;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
const sessionGrants = [];
|
|
13
|
+
function stable(value) {
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return value.map(stable);
|
|
16
|
+
if (value && typeof value === 'object') {
|
|
17
|
+
return Object.fromEntries(Object.entries(value)
|
|
18
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
19
|
+
.map(([key, item]) => [key, stable(item)]));
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function canonicalProjectRoot(root) {
|
|
24
|
+
const resolved = path.resolve(root);
|
|
25
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
26
|
+
}
|
|
27
|
+
/** A grant is bound to the actual command or logical resources, never merely a tool name. */
|
|
28
|
+
export function permissionFingerprint(tool, args) {
|
|
29
|
+
let subject;
|
|
30
|
+
if (tool.name === 'run_command' && typeof args.command === 'string') {
|
|
31
|
+
subject = { command: args.command.trim() };
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const resources = tool.capabilities?.resources?.(args).filter(Boolean).sort();
|
|
35
|
+
// File mutations may be granted by their concrete resource. Coarse resources such as
|
|
36
|
+
// "workspace" must retain arguments so task/process-like calls cannot become tool-wide.
|
|
37
|
+
subject = resources?.length && typeof args.path === 'string'
|
|
38
|
+
? { resources }
|
|
39
|
+
: stable(args);
|
|
40
|
+
}
|
|
41
|
+
return crypto.createHash('sha256').update(JSON.stringify(subject)).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
function validGrant(value) {
|
|
44
|
+
if (!value || typeof value !== 'object')
|
|
45
|
+
return false;
|
|
46
|
+
const grant = value;
|
|
47
|
+
return typeof grant.tool === 'string'
|
|
48
|
+
&& typeof grant.fingerprint === 'string'
|
|
49
|
+
&& grant.fingerprint.length > 0
|
|
50
|
+
&& (grant.scope === 'project' || grant.scope === 'session' || grant.scope === 'once');
|
|
51
|
+
}
|
|
30
52
|
function loadPermanent() {
|
|
31
53
|
if (permanentLoaded)
|
|
32
|
-
return
|
|
54
|
+
return permanentGrants;
|
|
33
55
|
permanentLoaded = true;
|
|
34
56
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
57
|
+
const parsed = JSON.parse(fs.readFileSync(PERMISSIONS_PATH, 'utf8'));
|
|
58
|
+
permanentGrants = Array.isArray(parsed.grants)
|
|
59
|
+
? parsed.grants.filter(validGrant).filter((grant) => grant.scope === 'project')
|
|
60
|
+
: [];
|
|
40
61
|
}
|
|
41
62
|
catch {
|
|
42
|
-
|
|
63
|
+
permanentGrants = [];
|
|
43
64
|
}
|
|
44
|
-
return
|
|
65
|
+
return permanentGrants;
|
|
45
66
|
}
|
|
46
|
-
/** 写入永久允许列表(覆盖写;失败静默,下次启动丢失但不阻断当前会话) */
|
|
47
67
|
function savePermanent() {
|
|
48
68
|
try {
|
|
49
69
|
fs.mkdirSync(path.dirname(PERMISSIONS_PATH), { recursive: true });
|
|
50
|
-
fs.writeFileSync(PERMISSIONS_PATH, JSON.stringify({
|
|
70
|
+
fs.writeFileSync(PERMISSIONS_PATH, `${JSON.stringify({ version: 2, grants: permanentGrants }, null, 2)}\n`, 'utf8');
|
|
51
71
|
}
|
|
52
72
|
catch {
|
|
53
|
-
//
|
|
73
|
+
// A failed persistence write must never turn into broader authorization.
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
|
-
/** 从 Tool 解析 risk,缺省返 'safe'(只读工具无需标注)。 */
|
|
57
76
|
export function getToolRisk(tool) {
|
|
58
77
|
return tool.risk ?? 'safe';
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
function summarizeArgs(tool, args) {
|
|
79
|
+
function summarizeArgs(args) {
|
|
62
80
|
const lines = [];
|
|
63
81
|
if (typeof args.path === 'string')
|
|
64
82
|
lines.push(t('permission.path', { value: args.path }));
|
|
65
83
|
if (typeof args.command === 'string')
|
|
66
84
|
lines.push(t('permission.command', { value: args.command }));
|
|
67
85
|
if (typeof args.prompt === 'string') {
|
|
68
|
-
const preview =
|
|
69
|
-
lines.push(t('permission.task', { value: `${preview}${
|
|
86
|
+
const preview = args.prompt.slice(0, 100);
|
|
87
|
+
lines.push(t('permission.task', { value: `${preview}${args.prompt.length > 100 ? '…' : ''}` }));
|
|
70
88
|
}
|
|
71
|
-
return lines.length
|
|
89
|
+
return lines.length ? lines.join('\n') : t('permission.noArgs');
|
|
72
90
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// 总开关关闭 → 全部放行(零行为变化,向后兼容)
|
|
81
|
-
if (!config.permissionEnabled)
|
|
82
|
-
return 'allow';
|
|
83
|
-
const risk = getToolRisk(tool);
|
|
84
|
-
if (risk === 'safe')
|
|
91
|
+
function matches(grant, tool, fingerprint, projectRoot) {
|
|
92
|
+
return grant.tool === tool
|
|
93
|
+
&& grant.fingerprint === fingerprint
|
|
94
|
+
&& (grant.scope !== 'project' || grant.projectRoot === projectRoot);
|
|
95
|
+
}
|
|
96
|
+
export async function checkPermission(tool, args, signal, options = {}) {
|
|
97
|
+
if (!config.permissionEnabled || getToolRisk(tool) === 'safe')
|
|
85
98
|
return 'allow';
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
if (signal?.aborted)
|
|
100
|
+
return 'deny';
|
|
101
|
+
const fingerprint = permissionFingerprint(tool, args);
|
|
102
|
+
const projectRoot = canonicalProjectRoot(options.projectRoot ?? getSandboxRoot() ?? process.cwd());
|
|
103
|
+
if (sessionGrants.some((grant) => matches(grant, tool.name, fingerprint, projectRoot)))
|
|
88
104
|
return 'allow';
|
|
89
|
-
|
|
90
|
-
if (risk === 'confirm' && approvedTools.has(tool.name))
|
|
105
|
+
if (loadPermanent().some((grant) => matches(grant, tool.name, fingerprint, projectRoot)))
|
|
91
106
|
return 'allow';
|
|
92
|
-
//
|
|
93
|
-
|
|
107
|
+
// CI/pipes must fail closed. Operators can deliberately restore unattended behavior.
|
|
108
|
+
if (!process.stdin.isTTY && !config.permissionNonInteractiveAllow && !options.prompt)
|
|
109
|
+
return 'deny';
|
|
110
|
+
if (signal?.aborted)
|
|
111
|
+
return 'deny';
|
|
112
|
+
const onceOption = t('permission.allow');
|
|
113
|
+
const sessionOption = t('permission.allowSessionResource');
|
|
114
|
+
const projectOption = t('permission.allowProjectResource');
|
|
94
115
|
const denyOption = t('permission.deny');
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
? t('permission.dangerTitle', { tool: tool.name })
|
|
99
|
-
: t('permission.confirmTitle', { tool: tool.name });
|
|
100
|
-
const detail = summarizeArgs(tool, args) + (isDangerous ? `\n\n${t('permission.dangerWarning')}` : '');
|
|
101
|
-
// 选项统一结构:dangerous 也提供"以后不再询问"(用户明确授权即尊重,即使 run_command)
|
|
102
|
-
const options = isDangerous
|
|
103
|
-
? [t('permission.confirmExecute'), foreverOption, denyOption]
|
|
104
|
-
: [t('permission.allow'), sessionOption, foreverOption, denyOption];
|
|
105
|
-
// 弹面板(阻塞直到用户选择;signal 中断时 promptIntervention 内部处理)
|
|
106
|
-
const result = await promptIntervention({
|
|
116
|
+
const dangerous = getToolRisk(tool) === 'dangerous';
|
|
117
|
+
const choices = [onceOption, sessionOption, projectOption, denyOption];
|
|
118
|
+
const result = await (options.prompt ?? promptIntervention)({
|
|
107
119
|
type: 'choice',
|
|
108
|
-
title
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
title: dangerous
|
|
121
|
+
? t('permission.dangerTitle', { tool: tool.name })
|
|
122
|
+
: t('permission.confirmTitle', { tool: tool.name }),
|
|
123
|
+
detail: summarizeArgs(args) + (dangerous ? `\n\n${t('permission.dangerWarning')}` : ''),
|
|
124
|
+
options: choices,
|
|
111
125
|
allowCustom: false,
|
|
112
126
|
});
|
|
113
|
-
|
|
114
|
-
if (result.action === 'cancelled')
|
|
115
|
-
return 'deny';
|
|
116
|
-
// 解析选择
|
|
117
|
-
const value = result.value ?? '';
|
|
118
|
-
if (value === denyOption)
|
|
127
|
+
if (signal?.aborted || result.action === 'cancelled' || result.value === denyOption)
|
|
119
128
|
return 'deny';
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
permanentAllow.add(tool.name);
|
|
123
|
-
savePermanent();
|
|
124
|
-
return 'allow';
|
|
129
|
+
if (result.value === sessionOption) {
|
|
130
|
+
sessionGrants.push({ tool: tool.name, fingerprint, scope: 'session' });
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
else if (result.value === projectOption) {
|
|
133
|
+
const grant = { tool: tool.name, fingerprint, scope: 'project', projectRoot };
|
|
134
|
+
permanentGrants = loadPermanent().filter((item) => !matches(item, tool.name, fingerprint, projectRoot));
|
|
135
|
+
permanentGrants.push(grant);
|
|
136
|
+
if (options.persistProjectGrant !== false)
|
|
137
|
+
savePermanent();
|
|
129
138
|
}
|
|
130
139
|
return 'allow';
|
|
131
140
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
permanentAllow.delete(toolName);
|
|
136
|
-
savePermanent();
|
|
137
|
-
}
|
|
141
|
+
export function revokePermanentAllow(toolName, fingerprint) {
|
|
142
|
+
permanentGrants = loadPermanent().filter((grant) => grant.tool !== toolName || (fingerprint !== undefined && grant.fingerprint !== fingerprint));
|
|
143
|
+
savePermanent();
|
|
138
144
|
}
|
|
139
|
-
|
|
145
|
+
export function listPermanentGrants() {
|
|
146
|
+
return loadPermanent().map((grant) => ({ ...grant }));
|
|
147
|
+
}
|
|
148
|
+
/** Compatibility API: returns tools having at least one project-scoped grant. */
|
|
140
149
|
export function listPermanentAllow() {
|
|
141
|
-
return
|
|
150
|
+
return [...new Set(loadPermanent().map((grant) => grant.tool))];
|
|
151
|
+
}
|
|
152
|
+
export function clearSessionPermissionGrants() {
|
|
153
|
+
sessionGrants.length = 0;
|
|
154
|
+
}
|
|
155
|
+
export function resetPermissionGrantsForTests() {
|
|
156
|
+
sessionGrants.length = 0;
|
|
157
|
+
permanentGrants = [];
|
|
158
|
+
permanentLoaded = true;
|
|
142
159
|
}
|
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
5
5
|
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
6
6
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
7
7
|
import { deletePreset, getPreset, isValidPresetName, listPresets, migrateCurrentToPreset, savePreset, } from '../config/presets.js';
|
|
@@ -16,9 +16,9 @@ import * as mouse from '../ui/mouse.js';
|
|
|
16
16
|
import * as batch from '../ui/batch.js';
|
|
17
17
|
import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
|
|
18
18
|
import { promptIntervention } from '../ui/intervention.js';
|
|
19
|
-
import {
|
|
19
|
+
import { registerToolsExtension } from '../tools/registry.js';
|
|
20
20
|
import { initializeAllMcp, getMcpTools, closeAllMcp } from '../mcp/index.js';
|
|
21
|
-
import { estimateMessagesTokens, reconfigureClient, refreshChatTools, } from '../llm/index.js';
|
|
21
|
+
import { estimateMessagesTokens, reconfigureClient, refreshChatTools, chatTools, } from '../llm/index.js';
|
|
22
22
|
import { loadImageAttachment, renderChip, MAX_INLINE_BYTES_DEFAULT, } from '../attachments/image.js';
|
|
23
23
|
import { modelSupportsVision } from '../llm/capabilities.js';
|
|
24
24
|
import { computePruneStats } from '../context/relevance.js';
|
|
@@ -63,6 +63,13 @@ function buildSlashCommands() {
|
|
|
63
63
|
{ name: 'init', value: '/init', desc: d('commands.memoryInit') },
|
|
64
64
|
],
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: '/subagent', desc: d('commands.subagent'), children: [
|
|
68
|
+
{ name: 'on', value: '/subagent on', desc: d('commands.subagentOn') },
|
|
69
|
+
{ name: 'off', value: '/subagent off', desc: d('commands.subagentOff') },
|
|
70
|
+
{ name: 'status', value: '/subagent status', desc: d('commands.subagentStatus') },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
66
73
|
{
|
|
67
74
|
name: '/project_skill', desc: d('commands.skill'), children: [
|
|
68
75
|
{ name: 'toggle', value: '/project_skill', desc: d('commands.toggle') },
|
|
@@ -322,6 +329,8 @@ function runningStateFor(cmd) {
|
|
|
322
329
|
return { status: t('running.memory'), placeholder: t('running.switching') };
|
|
323
330
|
case '/memory_status':
|
|
324
331
|
return { status: t('running.memoryStatus'), placeholder: '…' };
|
|
332
|
+
case '/subagent':
|
|
333
|
+
return { status: t('running.subagent'), placeholder: t('running.switching') };
|
|
325
334
|
case '/project_skill':
|
|
326
335
|
return { status: t('running.skill'), placeholder: t('running.processing') };
|
|
327
336
|
case '/snapshot':
|
|
@@ -734,12 +743,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
734
743
|
// 本轮 token 累计:runAgent 返回后写入,供底栏模式 chip 右边显示。undefined=无实测
|
|
735
744
|
// (后端不开 include_usage / 后端失败时)。
|
|
736
745
|
let lastTurnUsage;
|
|
737
|
-
const toolsLine = tools.map((t) => t.name).join(' · ');
|
|
738
746
|
const banner = () => ({
|
|
739
747
|
model: config.model,
|
|
740
748
|
baseURL: config.baseURL,
|
|
741
749
|
cwd: process.cwd(),
|
|
742
|
-
tools:
|
|
750
|
+
tools: chatTools.map((tool) => tool.function.name).join(' · '),
|
|
743
751
|
memoryEnabled: isMemoryEnabled(),
|
|
744
752
|
});
|
|
745
753
|
// 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
|
|
@@ -1858,6 +1866,32 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1858
1866
|
await rollbackFlow();
|
|
1859
1867
|
continue;
|
|
1860
1868
|
}
|
|
1869
|
+
if (line === '/subagent' || line.startsWith('/subagent ')) {
|
|
1870
|
+
const arg = line.startsWith('/subagent ')
|
|
1871
|
+
? line.slice('/subagent '.length).trim().toLowerCase()
|
|
1872
|
+
: 'status';
|
|
1873
|
+
if (arg === '' || arg === 'status') {
|
|
1874
|
+
const enabled = isSubAgentEnabled();
|
|
1875
|
+
const state = t(enabled ? 'subagent.stateOn' : 'subagent.stateOff');
|
|
1876
|
+
layout.contentWrite(`${ui.accent}${t('subagent.status', { state })}${ui.reset}\n` +
|
|
1877
|
+
`${ui.dim}MOCODE_SUBAGENT_ENABLED=${enabled ? 'true' : 'false'} · ${CONFIG_PATH}${ui.reset}\n`);
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
1881
|
+
layout.contentWrite(`${ui.yellow}${t('subagent.usage')}${ui.reset}\n`);
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const enabled = arg === 'on';
|
|
1885
|
+
if (enabled !== isSubAgentEnabled()) {
|
|
1886
|
+
updateSubAgentConfig(enabled);
|
|
1887
|
+
updateConfigKey('MOCODE_SUBAGENT_ENABLED', enabled ? 'true' : 'false');
|
|
1888
|
+
refreshChatTools();
|
|
1889
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1890
|
+
layout.rewriteBanner(bannerLines(banner()));
|
|
1891
|
+
}
|
|
1892
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t(enabled ? 'subagent.changedOn' : 'subagent.changedOff')}${ui.reset}\n`);
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1861
1895
|
if (line === '/memory_switch' ||
|
|
1862
1896
|
line.startsWith('/memory_switch ') ||
|
|
1863
1897
|
line === '/memory_status' ||
|
package/dist/rollback/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { toText } from '../context/utils.js';
|
|
|
6
6
|
let turnIdCounter = 0;
|
|
7
7
|
let currentTurnId = 0;
|
|
8
8
|
let sequenceCounter = 0;
|
|
9
|
+
/** Monotonic process-local generation; repeated writes to one path still invalidate validation. */
|
|
10
|
+
let mutationVersion = 0;
|
|
9
11
|
let turns = [];
|
|
10
12
|
let snapshots = [];
|
|
11
13
|
const rootDir = () => path.resolve(process.cwd());
|
|
@@ -128,17 +130,22 @@ export function endPathMutation(capture, op) {
|
|
|
128
130
|
const full = safeFullPath(capture.path);
|
|
129
131
|
if (!full)
|
|
130
132
|
return;
|
|
133
|
+
let changed = false;
|
|
131
134
|
const after = readState(full);
|
|
132
135
|
if (!sameState(capture.before, after)) {
|
|
136
|
+
changed = true;
|
|
133
137
|
addSnapshot(snapshotFromState(capture.path, capture.before, capture.sequence, op, capture.createdParents));
|
|
134
138
|
}
|
|
135
139
|
// write_file 会递归创建父目录;即使最终写文件失败,这些目录也是本轮真实副作用。
|
|
136
140
|
for (const parentRel of capture.createdParents) {
|
|
137
141
|
const parent = safeFullPath(parentRel);
|
|
138
142
|
if (parent && readState(parent).kind !== 'missing') {
|
|
143
|
+
changed = true;
|
|
139
144
|
addSnapshot(snapshotFromState(parentRel, { kind: 'missing' }, capture.sequence, op));
|
|
140
145
|
}
|
|
141
146
|
}
|
|
147
|
+
if (changed)
|
|
148
|
+
mutationVersion += 1;
|
|
142
149
|
}
|
|
143
150
|
function isWorkspaceExcluded(full) {
|
|
144
151
|
const base = path.basename(full).toLowerCase();
|
|
@@ -183,13 +190,37 @@ export function beginWorkspaceMutation() {
|
|
|
183
190
|
export function endWorkspaceMutation(capture, op) {
|
|
184
191
|
const after = scanWorkspace();
|
|
185
192
|
const paths = new Set([...capture.entries.keys(), ...after.keys()]);
|
|
193
|
+
let changed = false;
|
|
186
194
|
for (const rel of paths) {
|
|
187
195
|
const beforeState = capture.entries.get(rel) ?? { kind: 'missing' };
|
|
188
196
|
const afterState = after.get(rel) ?? { kind: 'missing' };
|
|
189
197
|
if (sameState(beforeState, afterState))
|
|
190
198
|
continue;
|
|
199
|
+
changed = true;
|
|
191
200
|
addSnapshot(snapshotFromState(rel, beforeState, capture.sequence, op));
|
|
192
201
|
}
|
|
202
|
+
if (changed)
|
|
203
|
+
mutationVersion += 1;
|
|
204
|
+
}
|
|
205
|
+
/** Current main turn changes, deduplicated by path, plus a generation for validation invalidation. */
|
|
206
|
+
export function getCurrentTurnMutationState() {
|
|
207
|
+
const order = [];
|
|
208
|
+
const byPath = new Map();
|
|
209
|
+
for (const snapshot of snapshots) {
|
|
210
|
+
if (snapshot.turnId !== currentTurnId)
|
|
211
|
+
continue;
|
|
212
|
+
let change = byPath.get(snapshot.path);
|
|
213
|
+
if (!change) {
|
|
214
|
+
change = { path: snapshot.path, ops: [], snapshotAvailable: true };
|
|
215
|
+
byPath.set(snapshot.path, change);
|
|
216
|
+
order.push(snapshot.path);
|
|
217
|
+
}
|
|
218
|
+
for (const op of snapshot.ops ?? ['file_change']) {
|
|
219
|
+
if (!change.ops.includes(op))
|
|
220
|
+
change.ops.push(op);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { version: mutationVersion, changedFiles: order.map((item) => byPath.get(item)) };
|
|
193
224
|
}
|
|
194
225
|
export function listTurns() {
|
|
195
226
|
return turns.slice();
|
package/dist/session/index.js
CHANGED
|
@@ -12,3 +12,4 @@ export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, con
|
|
|
12
12
|
export { runScheduler, manualCompact, createBudgetScheduler, } from './scheduler.js';
|
|
13
13
|
export { dropContextFromHistory, formatDropResult, } from './drop.js';
|
|
14
14
|
export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
|
|
15
|
+
export { appendCurrentSessionTrace } from './trace.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { config } from '../config/index.js';
|
|
4
|
+
import { getCurrentSessionId } from './state.js';
|
|
5
|
+
function boundedValidation(result) {
|
|
6
|
+
if (!result)
|
|
7
|
+
return undefined;
|
|
8
|
+
const max = 4000;
|
|
9
|
+
return result.output.length <= max
|
|
10
|
+
? result
|
|
11
|
+
: { ...result, output: `${result.output.slice(0, 2000)}\n…[trace output truncated]…\n${result.output.slice(-1900)}` };
|
|
12
|
+
}
|
|
13
|
+
/** Best-effort JSONL trace. Failure must never affect the agent turn. */
|
|
14
|
+
export function appendCurrentSessionTrace(trace) {
|
|
15
|
+
const sessionId = getCurrentSessionId();
|
|
16
|
+
if (!sessionId)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
const dir = path.join(config.sessionDir, sessionId);
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
appendFileSync(path.join(dir, 'trace.jsonl'), `${JSON.stringify({ ...trace, validation: boundedValidation(trace.validation) })}\n`, 'utf8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Observability is best-effort and cannot block coding work.
|
|
25
|
+
}
|
|
26
|
+
}
|