mocode-ai 1.2.3 → 1.2.5
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/dist/agent/index.js +3 -2
- package/dist/agent/spawn.js +25 -1
- package/dist/config/index.js +3 -2
- package/dist/context/classifier.js +1 -0
- package/dist/context/pipeline.js +1 -1
- package/dist/i18n/index.js +8 -0
- package/dist/memory/graph.js +409 -0
- package/dist/memory/index.js +2 -1
- package/dist/memory/reflect.js +25 -6
- package/dist/repl/index.js +71 -5
- package/dist/sandbox/policy.js +1 -1
- package/dist/skills/activation.js +26 -0
- package/dist/skills/builtin-skills.js +5 -0
- package/dist/skills/discover.js +167 -21
- package/dist/skills/index.js +32 -7
- package/dist/skills/runner.js +191 -33
- package/dist/skills/toolmap.js +56 -0
- package/dist/skills/trust.js +134 -0
- package/dist/tools/builtins/index.js +8 -1
- package/dist/tools/builtins/memory-graph.js +118 -0
- package/dist/tools/builtins/memory-save.js +42 -5
- package/dist/tools/builtins/memory-search.js +26 -5
- package/dist/tools/builtins/run-skill.js +42 -0
- package/dist/tools/builtins/use-skill.js +43 -5
- package/dist/tools/constants.js +14 -0
- package/dist/ui/layout.js +2 -2
- package/dist/ui/theme.js +11 -11
- package/package.json +1 -1
package/dist/repl/index.js
CHANGED
|
@@ -26,7 +26,10 @@ import { computePruneStats } from '../context/relevance.js';
|
|
|
26
26
|
import { formatArtifactTokenSources } from '../context/artifacts.js';
|
|
27
27
|
import { manualCompact, contextState, newSessionId, saveSession, loadSession, listSessions, appendCurrentSessionRuntimeEvent, hashTraceValue, } from '../session/index.js';
|
|
28
28
|
import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, getCurrentTurnId, } from '../rollback/index.js';
|
|
29
|
-
import { listSkills, effectiveSystemPrompt, } from '../skills/index.js';
|
|
29
|
+
import { listSkills, effectiveSystemPrompt, findSkill, } from '../skills/index.js';
|
|
30
|
+
import { clearSkillActivation } from '../skills/activation.js';
|
|
31
|
+
import { runSkill, renderSkillBody } from '../skills/runner.js';
|
|
32
|
+
import { isSkillTrusted } from '../skills/trust.js';
|
|
30
33
|
import { buildMemoryIndexSection, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, loadAll, } from '../memory/index.js';
|
|
31
34
|
import fs from 'node:fs';
|
|
32
35
|
import path from 'node:path';
|
|
@@ -50,6 +53,7 @@ function buildSlashCommands() {
|
|
|
50
53
|
{ name: '/clear', desc: d('commands.clear') },
|
|
51
54
|
{ name: '/context', desc: d('commands.context') },
|
|
52
55
|
{ name: '/skills', desc: d('commands.skills') },
|
|
56
|
+
{ name: '/skill', desc: '执行某个 skill(/skill <name> [args-json])' },
|
|
53
57
|
{ name: '/compact', desc: d('commands.compact') },
|
|
54
58
|
{ name: '/resume', desc: d('commands.sessionResume') },
|
|
55
59
|
{ name: '/sessions', desc: d('commands.sessionBrowse') },
|
|
@@ -998,6 +1002,8 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
998
1002
|
// 入口设定本轮初始模式(合成执行轮传 false→auto;用户轮传当前 mode)。
|
|
999
1003
|
// setAgentMode 触发 listener 重写 history[0];LLM 可在轮中调 switch_mode 切模式,runAgent 每步读实时值。
|
|
1000
1004
|
setAgentMode(planMode ? 'plan' : 'auto');
|
|
1005
|
+
// 新用户轮开始:清除上一轮 inline skill 的激活态(允许/disallowed 约束一 turn 有效)。
|
|
1006
|
+
clearSkillActivation();
|
|
1001
1007
|
// 运行中每步 chat() 返回后刷新状态行 context 用量条(用 fresh lastUsage / 估算),
|
|
1002
1008
|
// 否则整轮冻结在轮首 refreshStatusBase 的值,「执行 grep」时 2k/1000k 不动。
|
|
1003
1009
|
const result = await runAgent(history, userInput, signal, () => {
|
|
@@ -1455,9 +1461,69 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1455
1461
|
else {
|
|
1456
1462
|
layout.contentWrite(`${ui.dim}已发现 ${skills.length} 个 skill:${ui.reset}\n`);
|
|
1457
1463
|
for (const s of skills) {
|
|
1458
|
-
|
|
1464
|
+
const badges = [];
|
|
1465
|
+
if (s.context === 'fork')
|
|
1466
|
+
badges.push('fork');
|
|
1467
|
+
if (!s.modelInvocable)
|
|
1468
|
+
badges.push('manual-only');
|
|
1469
|
+
badges.push(s.origin);
|
|
1470
|
+
if (s.origin === 'project') {
|
|
1471
|
+
badges.push(isSkillTrusted(s) ? 'trusted' : 'untrusted');
|
|
1472
|
+
}
|
|
1473
|
+
const badgeStr = badges.length ? ` ${ui.dim}[${badges.join('|')}]${ui.reset}` : '';
|
|
1474
|
+
layout.contentWrite(` ${ui.accent}${s.name}${ui.reset}${badgeStr} ${ui.dim}${s.description}${ui.reset}\n`);
|
|
1475
|
+
if (s.allowedTools?.length) {
|
|
1476
|
+
layout.contentWrite(` ${ui.dim}allowed: ${s.allowedTools.join(', ')}${ui.reset}\n`);
|
|
1477
|
+
}
|
|
1478
|
+
if (s.disallowedTools?.length) {
|
|
1479
|
+
layout.contentWrite(` ${ui.dim}disallowed: ${s.disallowedTools.join(', ')}${ui.reset}\n`);
|
|
1480
|
+
}
|
|
1481
|
+
if (s.warnings.length) {
|
|
1482
|
+
layout.contentWrite(` ${ui.dim}warnings: ${s.warnings.join('; ')}${ui.reset}\n`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
layout.contentWrite(`${ui.dim}(用 use_skill 加载指令; fork 类用 run_skill 执行; 也支持 /skill <name> [args-json])${ui.reset}\n`);
|
|
1486
|
+
}
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
// /skill <name> [args-json]:直接执行一个 skill(fork 走 run_skill,inline 打印渲染后正文)。
|
|
1490
|
+
if (line === '/skill' || line.startsWith('/skill ')) {
|
|
1491
|
+
const rest = line.slice('/skill'.length).trim();
|
|
1492
|
+
const sp = rest.indexOf(' ');
|
|
1493
|
+
const name = sp === -1 ? rest : rest.slice(0, sp);
|
|
1494
|
+
const argStr = sp === -1 ? '' : rest.slice(sp + 1).trim();
|
|
1495
|
+
if (!name) {
|
|
1496
|
+
layout.contentWrite(`${ui.dim}用法: /skill <name> [args-json]${ui.reset}\n`);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
let args;
|
|
1500
|
+
if (argStr) {
|
|
1501
|
+
try {
|
|
1502
|
+
args = JSON.parse(argStr);
|
|
1503
|
+
}
|
|
1504
|
+
catch {
|
|
1505
|
+
layout.contentWrite(`${ui.dim}args 不是合法 JSON,已忽略。${ui.reset}\n`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
const skill = findSkill(name);
|
|
1509
|
+
if (!skill) {
|
|
1510
|
+
layout.contentWrite(`错误:未找到 skill "${name}"。\n`);
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (skill.context === 'fork') {
|
|
1514
|
+
layout.contentWrite(`${ui.dim}执行 fork skill "${name}"…${ui.reset}\n`);
|
|
1515
|
+
const out = await runSkill({ name, args });
|
|
1516
|
+
layout.contentWrite((out.status === 'success' ? '' : `[${out.status}] `) + out.output + '\n');
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
const body = await renderSkillBody(skill, args);
|
|
1520
|
+
if (body === null)
|
|
1521
|
+
layout.contentWrite(`错误:未找到 skill "${name}" 的正文。\n`);
|
|
1522
|
+
else {
|
|
1523
|
+
// 仅预览:inline 正文未进入模型 history,不激活工具面约束;
|
|
1524
|
+
// 要让模型按此 skill 工作,请让它调 use_skill(name) 或在下条消息里提及该 skill。
|
|
1525
|
+
layout.contentWrite(`# Skill: ${name}(预览,模型尚未看到此内容)\n\n${body}\n`);
|
|
1459
1526
|
}
|
|
1460
|
-
layout.contentWrite(`${ui.dim}(用 use_skill 工具加载某 skill 的完整指令)${ui.reset}\n`);
|
|
1461
1527
|
}
|
|
1462
1528
|
continue;
|
|
1463
1529
|
}
|
|
@@ -2120,9 +2186,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
2120
2186
|
// 写盘:mode 文件 values,/~/.mocode/config;writeConfigKeys 不会动其它键(主题 / 模型等)
|
|
2121
2187
|
updateConfigKey('MEMORY_ENABLED', nextEnabled ? 'true' : 'false');
|
|
2122
2188
|
const note = nextEnabled
|
|
2123
|
-
? `${ui.green}已开启记忆子系统${ui.reset} — memory_save/search/list/update/forget 进入工具表;` +
|
|
2189
|
+
? `${ui.green}已开启记忆子系统${ui.reset} — memory_save/search/list/update/forget/graph 进入工具表;` +
|
|
2124
2190
|
`Memory Index 段会在下次拼 system message 时注入。工具表本身的快照需要重启 REPL 才完整刷新。`
|
|
2125
|
-
: `${ui.yellow}已关闭记忆子系统${ui.reset} —
|
|
2191
|
+
: `${ui.yellow}已关闭记忆子系统${ui.reset} — 六个 memory_* 工具将在下次拼 system message 时从工具表过滤;` +
|
|
2126
2192
|
`Memory Index 段不再出现;plan-mode 提示词里的 memory_* 字样消失。重启 REPL 后工具表完全不出现。`;
|
|
2127
2193
|
layout.contentWrite(`${note}\n`);
|
|
2128
2194
|
layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MEMORY_ENABLED=${nextEnabled ? 'true' : 'false'};${ui.reset}` +
|
package/dist/sandbox/policy.js
CHANGED
|
@@ -14,7 +14,7 @@ import { jailResolve, jailGlobPattern } from './jail.js';
|
|
|
14
14
|
* Shift+Tab 等用户面触发,不经工具路径。
|
|
15
15
|
*/
|
|
16
16
|
export const SANDBOX_EXEMPT_TOOLS = new Set([
|
|
17
|
-
'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list',
|
|
17
|
+
'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list', 'memory_graph',
|
|
18
18
|
'use_skill',
|
|
19
19
|
'web_search', 'web_fetch',
|
|
20
20
|
'ask_human',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// inline skill 激活态(设计 §3.6):use_skill 成功加载 inline skill 时置位,
|
|
2
|
+
// 该 turn 内模型的 disallowed-tools 约束生效(constants.getRuntimeDisabledTools 消费)。
|
|
3
|
+
// 一 turn 语义:下一次用户轮开始时由 repl 调 clearSkillActivation() 清除。
|
|
4
|
+
//
|
|
5
|
+
// 刻意不设 allowed 授权:inline 加载不过信任门禁,自动放行 confirm 级工具会让
|
|
6
|
+
// 未信任的项目 skill 绕过权限确认。allowed-tools 只在 fork 模式下经 spawnAgent
|
|
7
|
+
// 的工具白名单生效(runner.runSkill)。
|
|
8
|
+
//
|
|
9
|
+
// 叶子模块:仅依赖 toolmap(亦叶子),不引 tools/permissions/agent,避免环。
|
|
10
|
+
import { mapSkillToolName } from './toolmap.js';
|
|
11
|
+
let active = null;
|
|
12
|
+
/** use_skill 成功加载 inline skill 时调用。 */
|
|
13
|
+
export function activateSkill(skill) {
|
|
14
|
+
const disallowed = skill.disallowedTools?.map(mapSkillToolName).filter((t) => t != null);
|
|
15
|
+
active = {
|
|
16
|
+
name: skill.name,
|
|
17
|
+
disallowed: disallowed && disallowed.length ? new Set(disallowed) : null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** 用户新轮开始时清激活态。 */
|
|
21
|
+
export function clearSkillActivation() {
|
|
22
|
+
active = null;
|
|
23
|
+
}
|
|
24
|
+
export function getActiveSkill() {
|
|
25
|
+
return active;
|
|
26
|
+
}
|
|
@@ -50,6 +50,11 @@ export const builtinSkills = [
|
|
|
50
50
|
body: CODEGRAPH_BODY,
|
|
51
51
|
dir: 'builtin',
|
|
52
52
|
skillMdPath: 'builtin',
|
|
53
|
+
// 内置 skill:恒信任、内联、模型可自动触发。
|
|
54
|
+
context: 'inline',
|
|
55
|
+
modelInvocable: true,
|
|
56
|
+
origin: 'builtin',
|
|
57
|
+
warnings: [],
|
|
53
58
|
},
|
|
54
59
|
];
|
|
55
60
|
/** 内置 skill 名字集,供 discoverSkills 跳过/覆盖时使用。 */
|
package/dist/skills/discover.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// skills 发现子系统。
|
|
2
2
|
// 仅依赖 node 标准库,是叶子模块:不依赖 config/agent/llm/tools,避免环。
|
|
3
3
|
//
|
|
4
|
-
// 约定:每个 skill 是一个目录 <skill-name>/SKILL.md,顶部 YAML frontmatter
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
4
|
+
// 约定:每个 skill 是一个目录 <skill-name>/SKILL.md,顶部 YAML frontmatter。
|
|
5
|
+
// 元数据始终注入系统提示,正文由 use_skill 工具按需加载(渐进式披露)。
|
|
6
|
+
// frontmatter 兼容 Agent Skills 开放标准(name/description/allowed-tools/context/agent…),
|
|
7
|
+
// 解析器是受限 YAML 子集(自写,不引 yaml 依赖),越界字段一律忽略 + 记 warning。
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
9
|
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
import { builtinSkillNames } from './builtin-skills.js';
|
|
@@ -39,10 +40,85 @@ export function resolveSkillsDirs() {
|
|
|
39
40
|
path.join(process.cwd(), '.mocode', 'skills'),
|
|
40
41
|
];
|
|
41
42
|
}
|
|
43
|
+
/** 数组型 frontmatter 键(这些键的整行值按空白切分为 token 列表)。 */
|
|
44
|
+
const ARRAY_KEYS = new Set(['allowed-tools', 'disallowed-tools']);
|
|
45
|
+
/** 行内数组 / 方括号数组:按空白或逗号切分,去空。 */
|
|
46
|
+
function splitList(s) {
|
|
47
|
+
return s
|
|
48
|
+
.split(/[\s,]+/)
|
|
49
|
+
.map((x) => x.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
/** 取标量值(数组值返回 undefined)。 */
|
|
53
|
+
function scalar(meta, key) {
|
|
54
|
+
const v = meta[key];
|
|
55
|
+
return typeof v === 'string' ? v : undefined;
|
|
56
|
+
}
|
|
57
|
+
/** 取可能为数组的值,统一成 string[] | undefined。 */
|
|
58
|
+
function asArray(meta, key) {
|
|
59
|
+
const v = meta[key];
|
|
60
|
+
if (Array.isArray(v))
|
|
61
|
+
return v.length ? v : undefined;
|
|
62
|
+
if (typeof v === 'string' && v)
|
|
63
|
+
return splitList(v);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
function truthy(meta, key) {
|
|
67
|
+
const v = scalar(meta, key);
|
|
68
|
+
return v != null && (v.toLowerCase() === 'true' || v === '1');
|
|
69
|
+
}
|
|
70
|
+
function num(meta, key) {
|
|
71
|
+
const v = scalar(meta, key);
|
|
72
|
+
if (v == null)
|
|
73
|
+
return undefined;
|
|
74
|
+
const n = Number(v);
|
|
75
|
+
return Number.isFinite(n) ? n : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** 去掉首尾配对引号(YAML 标量基本语义;`"use when: foo"` → `use when: foo`)。 */
|
|
78
|
+
function unquote(s) {
|
|
79
|
+
if (s.length >= 2 && (s[0] === '"' || s[0] === "'") && s[s.length - 1] === s[0]) {
|
|
80
|
+
return s.slice(1, -1);
|
|
81
|
+
}
|
|
82
|
+
return s;
|
|
83
|
+
}
|
|
84
|
+
/** `context: fork` 或兼容别名 `mode: fork` → 'fork';其余 'inline'。 */
|
|
85
|
+
function resolveContext(meta) {
|
|
86
|
+
if (scalar(meta, 'context') === 'fork')
|
|
87
|
+
return 'fork';
|
|
88
|
+
if (scalar(meta, 'mode') === 'fork')
|
|
89
|
+
return 'fork'; // 旧内部别名
|
|
90
|
+
return 'inline';
|
|
91
|
+
}
|
|
92
|
+
/** `agent:` 映射到 mocode 的 read/write 双态;未知值记 warning 并保守按 read。 */
|
|
93
|
+
function resolveAgentMode(meta, warnings) {
|
|
94
|
+
const a = scalar(meta, 'agent');
|
|
95
|
+
if (!a)
|
|
96
|
+
return undefined;
|
|
97
|
+
const low = a.toLowerCase();
|
|
98
|
+
if (['explore', 'plan', 'research', 'read'].includes(low))
|
|
99
|
+
return 'read';
|
|
100
|
+
if (['general-purpose', 'write'].includes(low))
|
|
101
|
+
return 'write';
|
|
102
|
+
warnings.push(`未知 agent 值 "${a}",按 read(只读,保守)处理`);
|
|
103
|
+
return 'read';
|
|
104
|
+
}
|
|
105
|
+
/** 收集本轮不支持、已忽略的字段,用于 /skills 提示(不阻断加载)。 */
|
|
106
|
+
function collectWarnings(meta) {
|
|
107
|
+
const warnings = [];
|
|
108
|
+
for (const k of ['hooks', 'model', 'effort', 'paths']) {
|
|
109
|
+
if (k in meta)
|
|
110
|
+
warnings.push(`字段 "${k}" 当前不支持,已忽略`);
|
|
111
|
+
}
|
|
112
|
+
return warnings;
|
|
113
|
+
}
|
|
42
114
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
115
|
+
* 受限 YAML 子集解析(不引入 yaml 依赖)。
|
|
116
|
+
* 支持四种值形态:
|
|
117
|
+
* - 标量 `key: value`
|
|
118
|
+
* - 行内数组 `allowed-tools: a, b` / 方括号 `allowed-tools: [a, b]`
|
|
119
|
+
* - 块序列(下一行起 ` - item`)
|
|
120
|
+
* - 空格分隔字符串(标准形态 `allowed-tools: Read grep run_command`,仅数组型键)
|
|
121
|
+
* 其余(嵌套 map / 块标量)一律忽略,保持攻击面与维护成本可控。
|
|
46
122
|
*
|
|
47
123
|
* 返回 { meta, body }:无 frontmatter 时 meta 为空、body 为原文。
|
|
48
124
|
*/
|
|
@@ -70,7 +146,16 @@ export function parseFrontmatter(content) {
|
|
|
70
146
|
if (!closed)
|
|
71
147
|
return { meta, body: content }; // 无闭合:视为无 frontmatter
|
|
72
148
|
i++; // 跳过闭 ---
|
|
73
|
-
|
|
149
|
+
const isListItem = (l) => {
|
|
150
|
+
const t = l.trim();
|
|
151
|
+
return t === '-' || t.startsWith('- ');
|
|
152
|
+
};
|
|
153
|
+
const itemText = (l) => {
|
|
154
|
+
const t = l.trim();
|
|
155
|
+
return t === '-' ? '' : t.slice(2).trim();
|
|
156
|
+
};
|
|
157
|
+
for (let k = 0; k < fmLines.length; k++) {
|
|
158
|
+
const line = fmLines[k];
|
|
74
159
|
const trimmed = line.trim();
|
|
75
160
|
if (trimmed === '' || trimmed.startsWith('#'))
|
|
76
161
|
continue;
|
|
@@ -78,15 +163,30 @@ export function parseFrontmatter(content) {
|
|
|
78
163
|
if (colon === -1)
|
|
79
164
|
continue;
|
|
80
165
|
const key = line.slice(0, colon).trim();
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
if (value.length
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
166
|
+
const value = line.slice(colon + 1).trim();
|
|
167
|
+
// 块序列:当前值为空且下一行是列表项
|
|
168
|
+
if (value === '' && k + 1 < fmLines.length && isListItem(fmLines[k + 1])) {
|
|
169
|
+
const arr = [];
|
|
170
|
+
let j = k + 1;
|
|
171
|
+
while (j < fmLines.length && isListItem(fmLines[j])) {
|
|
172
|
+
arr.push(itemText(fmLines[j]));
|
|
173
|
+
j++;
|
|
174
|
+
}
|
|
175
|
+
meta[key] = arr;
|
|
176
|
+
k = j - 1; // for 循环会再 +1
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// 方括号数组
|
|
180
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
181
|
+
meta[key] = splitList(value.slice(1, -1));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// 数组型键:整行按空白切分为 token(标准形态 Bash(git:*) Read)
|
|
185
|
+
if (ARRAY_KEYS.has(key)) {
|
|
186
|
+
meta[key] = value ? splitList(value) : [];
|
|
187
|
+
continue;
|
|
87
188
|
}
|
|
88
|
-
|
|
89
|
-
meta[key] = value;
|
|
189
|
+
meta[key] = unquote(value);
|
|
90
190
|
}
|
|
91
191
|
const body = lines.slice(i).join('\n').replace(/^\n+/, '');
|
|
92
192
|
return { meta, body };
|
|
@@ -99,6 +199,10 @@ export function parseFrontmatter(content) {
|
|
|
99
199
|
*/
|
|
100
200
|
export function discoverSkills() {
|
|
101
201
|
const dirs = resolveSkillsDirs();
|
|
202
|
+
// 记录本次发现的所有 SKILL.md 路径,供 skillsScanSignature 做廉价热重载探测
|
|
203
|
+
// (只 stat 已知文件,不每次全量重扫)。新增 skill 目录会改目录 mtime → 触发全量重扫。
|
|
204
|
+
const seenMdPaths = [];
|
|
205
|
+
const projDir = path.join(process.cwd(), '.mocode', 'skills');
|
|
102
206
|
const byName = new Map();
|
|
103
207
|
for (const dir of dirs) {
|
|
104
208
|
let entries = [];
|
|
@@ -110,6 +214,8 @@ export function discoverSkills() {
|
|
|
110
214
|
catch {
|
|
111
215
|
continue; // 目录不存在或无权限,静默跳过
|
|
112
216
|
}
|
|
217
|
+
// SKILLS_DIRS 自定义目录无法归类 project,统一按 user(免门禁)处理。
|
|
218
|
+
const origin = dir === projDir ? 'project' : 'user';
|
|
113
219
|
for (const name of entries) {
|
|
114
220
|
// 跳过内置 skill 同名目录(项目级用户版可放在 ~/.mocode/skills/<name>/
|
|
115
221
|
// 覆盖内置版;cwd/.mocode/skills/<name>/ 也算项目级覆盖)。discoverSkills 按
|
|
@@ -122,19 +228,32 @@ export function discoverSkills() {
|
|
|
122
228
|
try {
|
|
123
229
|
if (!existsSync(skillMdPath))
|
|
124
230
|
continue;
|
|
231
|
+
seenMdPaths.push(skillMdPath);
|
|
125
232
|
const content = readFileSync(skillMdPath, 'utf8');
|
|
126
|
-
const { meta } = parseFrontmatter(content);
|
|
127
|
-
const skillName = (meta
|
|
128
|
-
const description = (meta
|
|
233
|
+
const { meta, body } = parseFrontmatter(content);
|
|
234
|
+
const skillName = (scalar(meta, 'name') || name).trim();
|
|
235
|
+
const description = (scalar(meta, 'description') || '').trim();
|
|
129
236
|
if (!description)
|
|
130
237
|
continue; // 缺 description 跳过(它是最触发机制)
|
|
238
|
+
const warnings = collectWarnings(meta);
|
|
131
239
|
byName.set(skillName, {
|
|
132
240
|
name: skillName,
|
|
133
241
|
description,
|
|
134
|
-
version: meta
|
|
135
|
-
license: meta
|
|
242
|
+
version: scalar(meta, 'version')?.trim() || undefined,
|
|
243
|
+
license: scalar(meta, 'license')?.trim() || undefined,
|
|
136
244
|
dir: skillDir,
|
|
137
245
|
skillMdPath,
|
|
246
|
+
// body 留空:用户/项目 skill 由 getSkillBody 走文件读取(保留热重载语义)。
|
|
247
|
+
context: resolveContext(meta),
|
|
248
|
+
agentMode: resolveAgentMode(meta, warnings),
|
|
249
|
+
allowedTools: asArray(meta, 'allowed-tools'),
|
|
250
|
+
disallowedTools: asArray(meta, 'disallowed-tools'),
|
|
251
|
+
modelInvocable: !truthy(meta, 'disable-model-invocation'),
|
|
252
|
+
maxSteps: num(meta, 'max-steps'),
|
|
253
|
+
argumentHint: scalar(meta, 'argument-hint')?.trim() || undefined,
|
|
254
|
+
whenToUse: scalar(meta, 'when_to_use')?.trim() || undefined,
|
|
255
|
+
origin,
|
|
256
|
+
warnings,
|
|
138
257
|
});
|
|
139
258
|
}
|
|
140
259
|
catch {
|
|
@@ -142,5 +261,32 @@ export function discoverSkills() {
|
|
|
142
261
|
}
|
|
143
262
|
}
|
|
144
263
|
}
|
|
264
|
+
// 首次全量扫描后,把发现的 SKILL.md 路径交给热重载探测器,后续只 stat 这些已知文件。
|
|
265
|
+
lastKnownMdPaths = seenMdPaths;
|
|
145
266
|
return Array.from(byName.values());
|
|
146
267
|
}
|
|
268
|
+
/** 上一次 discoverSkills 发现的 SKILL.md 路径;skillsScanSignature 复用,避免每次全量重扫。 */
|
|
269
|
+
let lastKnownMdPaths = [];
|
|
270
|
+
/** 供热重载签名计算:各 skills 目录 mtime + 已知 SKILL.md 的 mtime。
|
|
271
|
+
* 目录 mtime 变化(新增/删除 skill 目录)或任一已知文件变动 → 签名变更 → 重扫。
|
|
272
|
+
* 首轮 lastKnownMdPaths 为空时退化为只测目录 mtime(首轮 listSkills 后即填充)。 */
|
|
273
|
+
export function skillsScanSignature() {
|
|
274
|
+
const parts = [];
|
|
275
|
+
for (const dir of resolveSkillsDirs()) {
|
|
276
|
+
try {
|
|
277
|
+
parts.push('D' + dir + statSync(dir).mtimeMs);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
parts.push('D' + dir + 'x');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const p of lastKnownMdPaths) {
|
|
284
|
+
try {
|
|
285
|
+
parts.push('F' + p + statSync(p).mtimeMs);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
parts.push('F' + p + 'x');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return parts.join('\0');
|
|
292
|
+
}
|
package/dist/skills/index.js
CHANGED
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
// 优先级:内置 skill < 用户/系统 skill < 项目级 skill(后注册覆盖先注册,
|
|
6
6
|
// 项目级最后扫描,自然在 Map 中胜出)。同名用户 skill 完全替换内置版。
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
-
import { discoverSkills, parseFrontmatter } from './discover.js';
|
|
8
|
+
import { discoverSkills, parseFrontmatter, skillsScanSignature, } from './discover.js';
|
|
9
9
|
import { builtinSkills } from './builtin-skills.js';
|
|
10
10
|
let cache = null;
|
|
11
|
+
let cacheKey = null;
|
|
12
|
+
/** 当前扫描签名(各 skills 目录 + 各 SKILL.md 的 mtime 拼接);用于热重载失效判断。 */
|
|
13
|
+
function currentKey() {
|
|
14
|
+
return skillsScanSignature();
|
|
15
|
+
}
|
|
11
16
|
/** 已发现的 skill 列表(懒加载,首次调用触发扫描;启动期 repl 调一次)。
|
|
12
|
-
* 合并策略:内置 skill 作兜底,discoverSkills 的输出(按目录优先级后写覆盖)优先生效。
|
|
17
|
+
* 合并策略:内置 skill 作兜底,discoverSkills 的输出(按目录优先级后写覆盖)优先生效。
|
|
18
|
+
* 热重载:签名变更(任一 SKILL.md 或目录被增删/改动)即重扫,无需重启。 */
|
|
13
19
|
export function listSkills() {
|
|
14
|
-
|
|
20
|
+
const key = currentKey();
|
|
21
|
+
if (cache !== null && cacheKey === key)
|
|
15
22
|
return cache;
|
|
16
23
|
const discovered = discoverSkills();
|
|
17
24
|
const byName = new Map();
|
|
@@ -22,15 +29,25 @@ export function listSkills() {
|
|
|
22
29
|
for (const skill of discovered)
|
|
23
30
|
byName.set(skill.name, skill);
|
|
24
31
|
cache = Array.from(byName.values());
|
|
32
|
+
cacheKey = key;
|
|
25
33
|
return cache;
|
|
26
34
|
}
|
|
35
|
+
/** 强制下次 listSkills 重扫(外部触发热重载用)。 */
|
|
36
|
+
export function invalidateSkillsCache() {
|
|
37
|
+
cache = null;
|
|
38
|
+
cacheKey = null;
|
|
39
|
+
}
|
|
40
|
+
/** 按名字取单个 skill(区分大小写;找不到返 null)。 */
|
|
41
|
+
export function findSkill(name) {
|
|
42
|
+
return listSkills().find((s) => s.name === name) ?? null;
|
|
43
|
+
}
|
|
27
44
|
/**
|
|
28
45
|
* 读取某 skill 的 SKILL.md 正文(去掉 frontmatter)。
|
|
29
46
|
* 纯函数:找不到 / 读失败返 null(错误字符串交给调用方工具层生成)。
|
|
30
47
|
* 内置 skill 直接返回 body 字段(无 fs 依赖)。
|
|
31
48
|
*/
|
|
32
49
|
export function getSkillBody(name) {
|
|
33
|
-
const skill =
|
|
50
|
+
const skill = findSkill(name);
|
|
34
51
|
if (!skill)
|
|
35
52
|
return null;
|
|
36
53
|
if (skill.body) {
|
|
@@ -49,12 +66,20 @@ export function getSkillBody(name) {
|
|
|
49
66
|
return null;
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
|
-
/** 拼进系统提示的 skill 段;无 skill 返空串(零行为变化)。
|
|
69
|
+
/** 拼进系统提示的 skill 段;无 skill 返空串(零行为变化)。
|
|
70
|
+
* - disable-model-invocation: true 的 skill 不进列表(模型看不见,仅 /skill 可触发)。
|
|
71
|
+
* - context: fork 的 skill 追加 [fork] 徽标 + 一句调用指引,提示模型它可被「执行」而非「阅读」。 */
|
|
53
72
|
export function buildSkillsSection() {
|
|
54
|
-
const skills = listSkills();
|
|
73
|
+
const skills = listSkills().filter((s) => s.modelInvocable);
|
|
55
74
|
if (skills.length === 0)
|
|
56
75
|
return '';
|
|
57
|
-
const lines = skills.map((s) =>
|
|
76
|
+
const lines = skills.map((s) => {
|
|
77
|
+
const badge = s.context === 'fork' ? ' [fork]' : '';
|
|
78
|
+
const hint = s.context === 'fork'
|
|
79
|
+
? ' (call run_skill to execute it as an isolated workflow)'
|
|
80
|
+
: ' (call use_skill to load its instructions)';
|
|
81
|
+
return `- ${s.name}${badge}: ${s.description}${hint}`;
|
|
82
|
+
});
|
|
58
83
|
return [
|
|
59
84
|
'',
|
|
60
85
|
'',
|