@kkelly-offical/kkcode 0.1.6 → 0.2.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/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
package/src/skill/registry.mjs
CHANGED
|
@@ -1,390 +1,642 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import { access, readdir, readFile } from "node:fs/promises"
|
|
3
|
-
import { pathToFileURL, fileURLToPath } from "node:url"
|
|
4
|
-
import {
|
|
5
|
-
import { promisify } from "node:util"
|
|
6
|
-
import { parse as parseYaml } from "yaml"
|
|
7
|
-
import { McpRegistry } from "../mcp/registry.mjs"
|
|
8
|
-
import { loadCustomCommands, applyCommandTemplate } from "../command/custom-commands.mjs"
|
|
9
|
-
import { EventBus } from "../core/events.mjs"
|
|
10
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
async function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import { pathToFileURL, fileURLToPath } from "node:url"
|
|
4
|
+
import { execFile } from "node:child_process"
|
|
5
|
+
import { promisify } from "node:util"
|
|
6
|
+
import { parse as parseYaml } from "yaml"
|
|
7
|
+
import { McpRegistry } from "../mcp/registry.mjs"
|
|
8
|
+
import { loadCustomCommands, applyCommandTemplate } from "../command/custom-commands.mjs"
|
|
9
|
+
import { EventBus } from "../core/events.mjs"
|
|
10
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
11
|
+
import { discoverLocalPluginManifests, pluginComponentDirs } from "../plugin/manifest-loader.mjs"
|
|
12
|
+
import { userRootDir } from "../storage/paths.mjs"
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile)
|
|
15
|
+
|
|
16
|
+
const DEFAULT_SKILL_DIR_README = `# kkcode Skills\n\nThis directory stores reusable skills for kkcode.\n\nWhen kkcode starts, skills in this directory are loaded automatically and can be invoked as slash commands (for example: \/code-review).\n\nDefault skill packs include:\n- code-review: structured review checklist for changed files\n- test-plan: lightweight test planning support\n\nAdd your own skills as:\n- .md files (simple templates)\n- .mjs files (programmable)\n- directories with SKILL.md (metadata + templates)`
|
|
17
|
+
|
|
18
|
+
const DEFAULT_SKILL_PACKS = [
|
|
19
|
+
{
|
|
20
|
+
dir: "code-review",
|
|
21
|
+
content: `---\nname: code-review\ndescription: Review code changes and provide risk-oriented recommendations.\nuser-invocable: true\ncontext-fork: false\n---\n请基于以下内容做结构化代码评审:\n- 先给出风险分级(高/中/低)与范围\n- 列出 3 条以内关键问题\n- 给出最小可执行修复建议\n\n输入:\n$ARGUMENTS\n\n如果是目录或文件路径,请先说明覆盖范围后再给建议。`
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
dir: "test-plan",
|
|
25
|
+
content: `---\nname: test-plan\ndescription: Generate a compact test plan for task execution.\nuser-invocable: true\ncontext-fork: false\n---\n请为下列任务生成测试计划:\n$ARGUMENTS\n\n请输出:\n1) 关键测试场景(按优先级)\n2) 可执行命令顺序\n3) 关键验证标准\n4) 回归风险与缓解建议`
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
function toText(v) {
|
|
30
|
+
if (v === undefined || v === null) return ""
|
|
31
|
+
return String(v)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toArray(value) {
|
|
35
|
+
if (Array.isArray(value)) return value
|
|
36
|
+
if (value === undefined || value === null || value === "") return []
|
|
37
|
+
return [value]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toStringArray(value) {
|
|
41
|
+
return toArray(value)
|
|
42
|
+
.flatMap((item) => typeof item === "string" ? item.split(",") : [item])
|
|
43
|
+
.map((item) => typeof item === "string" ? item.trim() : "")
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function exists(target) {
|
|
48
|
+
try {
|
|
49
|
+
await access(target)
|
|
50
|
+
return true
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function writeIfMissing(filePath, content, force = false) {
|
|
57
|
+
if (!force && await exists(filePath)) return false
|
|
58
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
59
|
+
await writeFile(filePath, toText(content), "utf8")
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function ensureDefaultSkillPack({
|
|
64
|
+
cwd = process.cwd(),
|
|
65
|
+
force = false,
|
|
66
|
+
includeProject = true,
|
|
67
|
+
includeGlobal = true
|
|
68
|
+
} = {}) {
|
|
69
|
+
const targets = []
|
|
70
|
+
if (includeGlobal) {
|
|
71
|
+
targets.push({ scope: "global", dir: path.join(userRootDir(), "skills") })
|
|
72
|
+
}
|
|
73
|
+
if (includeProject) {
|
|
74
|
+
targets.push({ scope: "project", dir: path.join(cwd, ".kkcode", "skills") })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const created = []
|
|
78
|
+
for (const target of targets) {
|
|
79
|
+
const createdFiles = []
|
|
80
|
+
const skippedFiles = []
|
|
81
|
+
|
|
82
|
+
if (await writeIfMissing(path.join(target.dir, "README.md"), DEFAULT_SKILL_DIR_README, force)) {
|
|
83
|
+
createdFiles.push("README.md")
|
|
84
|
+
} else {
|
|
85
|
+
skippedFiles.push("README.md")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const pack of DEFAULT_SKILL_PACKS) {
|
|
89
|
+
const filePath = path.join(target.dir, pack.dir, "SKILL.md")
|
|
90
|
+
if (await writeIfMissing(filePath, pack.content, force)) {
|
|
91
|
+
createdFiles.push(`${pack.dir}/SKILL.md`)
|
|
92
|
+
} else {
|
|
93
|
+
skippedFiles.push(`${pack.dir}/SKILL.md`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
created.push({
|
|
98
|
+
scope: target.scope,
|
|
99
|
+
dir: target.dir,
|
|
100
|
+
created: createdFiles,
|
|
101
|
+
skipped: skippedFiles
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return created
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const DEFAULT_ALLOWED_COMMANDS = ["git", "node", "npm", "ls", "cat", "date", "pwd", "echo", "which"]
|
|
109
|
+
let _allowedCommands = null
|
|
110
|
+
let _allowedCommandsSig = null
|
|
111
|
+
|
|
112
|
+
function getAllowedCommands(config) {
|
|
113
|
+
const extra = config?.skills?.allowed_commands || []
|
|
114
|
+
const sig = extra.join(",")
|
|
115
|
+
if (_allowedCommands && _allowedCommandsSig === sig) return _allowedCommands
|
|
116
|
+
_allowedCommands = new Set([...DEFAULT_ALLOWED_COMMANDS, ...extra])
|
|
117
|
+
_allowedCommandsSig = sig
|
|
118
|
+
return _allowedCommands
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Shell metacharacters and control chars that enable command chaining / injection
|
|
122
|
+
const SHELL_INJECTION_RE = /[;|&`$(){}]|>\s*>|<\s*<|[\n\r]/
|
|
123
|
+
|
|
124
|
+
function isCommandAllowed(cmdString, config) {
|
|
125
|
+
const allowed = getAllowedCommands(config)
|
|
126
|
+
const trimmed = cmdString.trim()
|
|
127
|
+
if (!trimmed) return false
|
|
128
|
+
// Reject any shell control characters — prevents chaining like `git status; rm -rf /`
|
|
129
|
+
if (SHELL_INJECTION_RE.test(trimmed)) return false
|
|
130
|
+
// Extract the base command (first token, strip path)
|
|
131
|
+
const firstToken = trimmed.split(/\s+/)[0] || ""
|
|
132
|
+
const baseName = path.basename(firstToken)
|
|
133
|
+
return allowed.has(baseName)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse YAML frontmatter from SKILL.md content.
|
|
138
|
+
* Returns { meta: {}, body: string }
|
|
139
|
+
*/
|
|
140
|
+
function parseFrontmatter(raw) {
|
|
141
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
|
|
142
|
+
if (!match) return { meta: {}, body: raw.trim() }
|
|
143
|
+
try {
|
|
144
|
+
return { meta: parseYaml(match[1]) || {}, body: match[2].trim() }
|
|
145
|
+
} catch {
|
|
146
|
+
return { meta: {}, body: raw.trim() }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeSkillMeta(meta = {}, defaults = {}) {
|
|
151
|
+
const contextValue = typeof meta.context === "string" ? meta.context.trim().toLowerCase() : ""
|
|
152
|
+
const explicitContextFork = meta["context-fork"] === true || meta.contextFork === true
|
|
153
|
+
const contextFork = explicitContextFork || contextValue === "fork"
|
|
154
|
+
const rawModel = typeof meta.model === "string" ? meta.model.trim() : meta.model
|
|
155
|
+
const model = rawModel && rawModel.toLowerCase() === "inherit" ? null : rawModel || null
|
|
156
|
+
const allowedTools = toStringArray(meta["allowed-tools"] ?? meta.allowedTools ?? meta.tools)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
disableModelInvocation: !!meta["disable-model-invocation"],
|
|
160
|
+
userInvocable: meta["user-invocable"] !== false,
|
|
161
|
+
allowedTools: allowedTools.length ? allowedTools : null,
|
|
162
|
+
model,
|
|
163
|
+
contextFork,
|
|
164
|
+
context: contextFork ? "fork" : contextValue === "inline" ? "inline" : null,
|
|
165
|
+
whenToUse: meta.when_to_use || meta["when-to-use"] || null,
|
|
166
|
+
argumentHint: meta.argument_hint || meta["argument-hint"] || null,
|
|
167
|
+
arguments: Array.isArray(meta.arguments) ? meta.arguments : [],
|
|
168
|
+
agent: meta.agent || null,
|
|
169
|
+
effort: meta.effort || null,
|
|
170
|
+
shell: meta.shell || null,
|
|
171
|
+
hooks: meta.hooks || null,
|
|
172
|
+
paths: toStringArray(meta.paths),
|
|
173
|
+
skillRoot: defaults.skillDir || path.dirname(defaults.source || process.cwd()),
|
|
174
|
+
plugin: defaults.plugin || null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Load plain .md skills from skill directories.
|
|
180
|
+
* Supports optional YAML frontmatter.
|
|
181
|
+
*/
|
|
182
|
+
async function loadMarkdownSkills(dir, scope, plugin = null) {
|
|
183
|
+
if (!(await exists(dir))) return []
|
|
184
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
185
|
+
const mdFiles = entries
|
|
186
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
|
|
187
|
+
.map((entry) => entry.name)
|
|
188
|
+
.sort()
|
|
189
|
+
|
|
190
|
+
const skills = []
|
|
191
|
+
for (const name of mdFiles) {
|
|
192
|
+
const lowerName = name.toLowerCase()
|
|
193
|
+
if (lowerName === "skill.md" || lowerName === "readme.md") continue
|
|
194
|
+
const filePath = path.join(dir, name)
|
|
195
|
+
try {
|
|
196
|
+
const raw = await readFile(filePath, "utf8")
|
|
197
|
+
const trimmed = raw.trim()
|
|
198
|
+
if (!trimmed) continue
|
|
199
|
+
const { meta, body } = parseFrontmatter(trimmed)
|
|
200
|
+
const normalized = normalizeSkillMeta(meta, {
|
|
201
|
+
skillDir: path.dirname(filePath),
|
|
202
|
+
source: filePath,
|
|
203
|
+
plugin
|
|
204
|
+
})
|
|
205
|
+
skills.push({
|
|
206
|
+
name: meta.name || path.basename(name, ".md"),
|
|
207
|
+
description: meta.description || path.basename(name, ".md"),
|
|
208
|
+
type: "skill_md",
|
|
209
|
+
scope,
|
|
210
|
+
source: filePath,
|
|
211
|
+
skillDir: path.dirname(filePath),
|
|
212
|
+
template: body,
|
|
213
|
+
auxFiles: {},
|
|
214
|
+
...normalized
|
|
215
|
+
})
|
|
216
|
+
} catch {
|
|
217
|
+
// skip invalid markdown skill files
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return skills
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Load .mjs programmable skills from a directory.
|
|
225
|
+
* Each .mjs file should export: { name, description, run(ctx) }
|
|
226
|
+
* run() returns a string prompt to send to the model.
|
|
227
|
+
*/
|
|
228
|
+
async function loadMjsSkills(dir, scope, plugin = null) {
|
|
229
|
+
if (!(await exists(dir))) return []
|
|
230
|
+
const resolvedDir = path.resolve(dir)
|
|
231
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
232
|
+
const files = entries
|
|
233
|
+
.filter((e) => e.isFile() && e.name.endsWith(".mjs"))
|
|
234
|
+
.map((e) => e.name)
|
|
235
|
+
.sort()
|
|
236
|
+
|
|
237
|
+
const skills = []
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
const full = path.resolve(dir, file)
|
|
240
|
+
// Path boundary check: ensure resolved path is within expected directory
|
|
241
|
+
if (!full.startsWith(resolvedDir + path.sep) && full !== resolvedDir) continue
|
|
242
|
+
try {
|
|
243
|
+
const mod = await import(pathToFileURL(full).href)
|
|
244
|
+
const name = mod.name || path.basename(file, ".mjs")
|
|
245
|
+
skills.push({
|
|
246
|
+
name,
|
|
247
|
+
description: mod.description || name,
|
|
248
|
+
type: "mjs",
|
|
249
|
+
scope,
|
|
250
|
+
source: full,
|
|
251
|
+
run: typeof mod.run === "function" ? mod.run : null,
|
|
252
|
+
skillRoot: path.dirname(full),
|
|
253
|
+
plugin
|
|
254
|
+
})
|
|
255
|
+
} catch {
|
|
256
|
+
// Skip broken skill files silently
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return skills
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function shellTokenize(input) {
|
|
263
|
+
const tokens = []
|
|
264
|
+
let current = ""
|
|
265
|
+
let inQuote = null
|
|
266
|
+
for (const ch of input) {
|
|
267
|
+
if (inQuote) {
|
|
268
|
+
if (ch === inQuote) { inQuote = null; continue }
|
|
269
|
+
current += ch
|
|
270
|
+
} else if (ch === '"' || ch === "'") {
|
|
271
|
+
inQuote = ch
|
|
272
|
+
} else if (/\s/.test(ch)) {
|
|
273
|
+
if (current) { tokens.push(current); current = "" }
|
|
274
|
+
} else {
|
|
275
|
+
current += ch
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (current) tokens.push(current)
|
|
279
|
+
return tokens
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Replace !`command` patterns with command stdout.
|
|
284
|
+
* Commands are checked against a whitelist before execution.
|
|
285
|
+
*/
|
|
286
|
+
async function injectDynamicContext(template, cwd, config) {
|
|
287
|
+
const pattern = /!\`([^`]+)\`/g
|
|
288
|
+
const matches = [...template.matchAll(pattern)]
|
|
289
|
+
if (!matches.length) return template
|
|
290
|
+
let result = template
|
|
291
|
+
for (const m of matches) {
|
|
292
|
+
if (!isCommandAllowed(m[1], config)) {
|
|
293
|
+
result = result.replace(m[0], `[blocked: ${m[1]}]`)
|
|
294
|
+
EventBus.emit({
|
|
295
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
296
|
+
payload: { kind: "skill_command_blocked", command: m[1] }
|
|
297
|
+
}).catch(() => {})
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const tokens = shellTokenize(m[1].trim())
|
|
302
|
+
const cmd = tokens[0]
|
|
303
|
+
const cmdArgs = tokens.slice(1)
|
|
304
|
+
const { stdout } = await execFileAsync(cmd, cmdArgs, { cwd, timeout: 10000 })
|
|
305
|
+
result = result.replace(m[0], stdout.trim())
|
|
306
|
+
} catch {
|
|
307
|
+
result = result.replace(m[0], `[command failed: ${m[1]}]`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Load SKILL.md directory-format skills from a directory.
|
|
315
|
+
* Scans for <dir>/<name>/SKILL.md
|
|
316
|
+
*/
|
|
317
|
+
async function loadAuxFiles(skillDir) {
|
|
318
|
+
const aux = {}
|
|
319
|
+
const resolvedSkillDir = path.resolve(skillDir)
|
|
320
|
+
try {
|
|
321
|
+
const entries = await readdir(skillDir, { withFileTypes: true })
|
|
322
|
+
for (const e of entries) {
|
|
323
|
+
if (!e.isFile() || e.name === "SKILL.md") continue
|
|
324
|
+
const filePath = path.resolve(skillDir, e.name)
|
|
325
|
+
// Path traversal protection: ensure file is within skillDir
|
|
326
|
+
if (!filePath.startsWith(resolvedSkillDir + path.sep) && filePath !== resolvedSkillDir) {
|
|
327
|
+
EventBus.emit({
|
|
328
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
329
|
+
payload: { kind: "skill_path_traversal", file: e.name, skillDir }
|
|
330
|
+
}).catch(() => {})
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
aux[e.name] = filePath
|
|
334
|
+
}
|
|
335
|
+
} catch { /* ignore */ }
|
|
336
|
+
return aux
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function loadSkillDirs(dir, scope, plugin = null) {
|
|
340
|
+
if (!(await exists(dir))) return []
|
|
341
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
342
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort()
|
|
343
|
+
const skills = []
|
|
344
|
+
for (const name of dirs) {
|
|
345
|
+
const skillDir = path.join(dir, name)
|
|
346
|
+
const mdPath = path.join(skillDir, "SKILL.md")
|
|
347
|
+
if (!(await exists(mdPath))) continue
|
|
348
|
+
try {
|
|
349
|
+
const raw = await readFile(mdPath, "utf8")
|
|
350
|
+
const { meta, body } = parseFrontmatter(raw)
|
|
351
|
+
const auxFiles = await loadAuxFiles(skillDir)
|
|
352
|
+
const normalized = normalizeSkillMeta(meta, {
|
|
353
|
+
skillDir,
|
|
354
|
+
source: mdPath,
|
|
355
|
+
plugin
|
|
356
|
+
})
|
|
357
|
+
skills.push({
|
|
358
|
+
name: meta.name || name,
|
|
359
|
+
description: meta.description || name,
|
|
360
|
+
type: "skill_md",
|
|
361
|
+
scope,
|
|
362
|
+
source: mdPath,
|
|
363
|
+
skillDir,
|
|
364
|
+
template: body,
|
|
365
|
+
auxFiles,
|
|
366
|
+
...normalized
|
|
367
|
+
})
|
|
368
|
+
} catch { /* skip broken */ }
|
|
369
|
+
}
|
|
370
|
+
return skills
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Convert custom commands (.md templates) to skill format.
|
|
375
|
+
*/
|
|
376
|
+
function customCommandsToSkills(commands) {
|
|
377
|
+
return commands.map((cmd) => ({
|
|
378
|
+
name: cmd.name,
|
|
379
|
+
description: `custom command (${cmd.scope})`,
|
|
380
|
+
type: "template",
|
|
381
|
+
scope: cmd.scope,
|
|
382
|
+
source: cmd.source,
|
|
383
|
+
template: cmd.template
|
|
384
|
+
}))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Convert MCP prompts to skill format.
|
|
389
|
+
*/
|
|
390
|
+
function mcpPromptsToSkills(prompts) {
|
|
391
|
+
return prompts.map((p) => ({
|
|
392
|
+
name: p.name,
|
|
393
|
+
description: p.description || `${p.server}:${p.name}`,
|
|
394
|
+
type: "mcp_prompt",
|
|
395
|
+
scope: "mcp",
|
|
396
|
+
server: p.server,
|
|
397
|
+
promptId: p.id,
|
|
398
|
+
arguments: p.arguments || []
|
|
399
|
+
}))
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const state = {
|
|
403
|
+
skills: new Map(),
|
|
404
|
+
loaded: false,
|
|
405
|
+
plugins: [],
|
|
406
|
+
pluginErrors: []
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export const SkillRegistry = {
|
|
410
|
+
/**
|
|
411
|
+
* Load all skills from all sources.
|
|
412
|
+
*/
|
|
413
|
+
async initialize(config, cwd = process.cwd()) {
|
|
414
|
+
state.skills.clear()
|
|
415
|
+
state.plugins = []
|
|
416
|
+
state.pluginErrors = []
|
|
417
|
+
const autoSeed = config?.skills?.auto_seed !== false
|
|
418
|
+
if (autoSeed) {
|
|
419
|
+
try {
|
|
420
|
+
await ensureDefaultSkillPack({ cwd, force: false, includeProject: true, includeGlobal: true })
|
|
421
|
+
} catch {
|
|
422
|
+
// Ignore seed failures (e.g., read-only mode)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Respect skills.enabled config — if explicitly false, skip all loading
|
|
427
|
+
if (config?.skills?.enabled === false) {
|
|
428
|
+
state.loaded = true
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Source 0: Built-in skills (shipped with kkcode)
|
|
433
|
+
const builtinDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "builtin")
|
|
434
|
+
const builtinSkills = await loadMjsSkills(builtinDir, "builtin")
|
|
435
|
+
for (const skill of builtinSkills) {
|
|
436
|
+
state.skills.set(skill.name, skill)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Source 1: Custom commands (.md templates)
|
|
440
|
+
const customCommands = await loadCustomCommands(cwd)
|
|
441
|
+
for (const skill of customCommandsToSkills(customCommands)) {
|
|
442
|
+
state.skills.set(skill.name, skill)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Source 2: Programmable skills (.mjs) + SKILL.md directories
|
|
446
|
+
const userRoot = userRootDir()
|
|
447
|
+
const pluginManifestState = await discoverLocalPluginManifests(cwd)
|
|
448
|
+
state.plugins = pluginManifestState.plugins
|
|
449
|
+
state.pluginErrors = pluginManifestState.errors
|
|
450
|
+
const rawCustomDirs = Array.isArray(config?.skills?.dirs) ? config.skills.dirs : []
|
|
451
|
+
// Default directories: global (~/.kkcode/skills) + project (.kkcode/skills)
|
|
452
|
+
const defaultDirs = [
|
|
453
|
+
{ dir: path.join(userRoot, "skills"), scope: "global" },
|
|
454
|
+
{ dir: path.join(cwd, ".kkcode", "skills"), scope: "project" }
|
|
455
|
+
]
|
|
456
|
+
const pluginDirs = pluginComponentDirs(state.plugins, "skills")
|
|
457
|
+
// Custom dirs from config (resolve relative to cwd)
|
|
458
|
+
const extraDirs = rawCustomDirs
|
|
459
|
+
.filter((d) => typeof d === "string" && d.trim().length > 0)
|
|
460
|
+
.map((d) => {
|
|
461
|
+
const trimmed = d.trim()
|
|
462
|
+
return {
|
|
463
|
+
dir: path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed),
|
|
464
|
+
scope: "custom"
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
const seenDirSet = new Set()
|
|
468
|
+
const allSkillDirs = [...pluginDirs, ...defaultDirs, ...extraDirs].filter((entry) => {
|
|
469
|
+
const resolved = path.resolve(entry.dir)
|
|
470
|
+
if (seenDirSet.has(resolved)) return false
|
|
471
|
+
seenDirSet.add(resolved)
|
|
472
|
+
return true
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
const loadPromises = allSkillDirs.flatMap(({ dir, scope, plugin = null }) => [
|
|
476
|
+
loadMarkdownSkills(dir, scope, plugin),
|
|
477
|
+
loadMjsSkills(dir, scope, plugin),
|
|
478
|
+
loadSkillDirs(dir, scope, plugin)
|
|
479
|
+
])
|
|
480
|
+
const results = await Promise.all(loadPromises)
|
|
481
|
+
for (const skills of results) {
|
|
482
|
+
for (const skill of skills) {
|
|
483
|
+
state.skills.set(skill.name, skill)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Source 3: MCP prompts (if MCP is initialized)
|
|
488
|
+
if (McpRegistry.isReady()) {
|
|
489
|
+
const prompts = McpRegistry.listPrompts()
|
|
490
|
+
for (const skill of mcpPromptsToSkills(prompts)) {
|
|
491
|
+
// Include server name to avoid cross-server name collisions
|
|
492
|
+
const key = `mcp:${skill.server}:${skill.name}`
|
|
493
|
+
state.skills.set(key, { ...skill, name: key })
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
state.loaded = true
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
isReady() {
|
|
501
|
+
return state.loaded
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
list() {
|
|
505
|
+
return [...state.skills.values()]
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
get(name) {
|
|
509
|
+
return state.skills.get(name) || null
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Execute a skill and return the expanded prompt string.
|
|
514
|
+
*/
|
|
515
|
+
async execute(name, args = "", context = {}) {
|
|
516
|
+
const skill = state.skills.get(name)
|
|
517
|
+
if (!skill) return null
|
|
518
|
+
|
|
519
|
+
if (skill.type === "mjs" && skill.run) {
|
|
520
|
+
// Programmable skill — call run() to get prompt
|
|
521
|
+
try {
|
|
522
|
+
const result = await skill.run({
|
|
523
|
+
args,
|
|
524
|
+
cwd: context.cwd || process.cwd(),
|
|
525
|
+
mode: context.mode || "agent",
|
|
526
|
+
model: context.model || "",
|
|
527
|
+
provider: context.provider || "",
|
|
528
|
+
config: context.config || null
|
|
529
|
+
})
|
|
530
|
+
return result == null ? "" : typeof result === "string" ? result : JSON.stringify(result)
|
|
531
|
+
} catch (error) {
|
|
532
|
+
return `skill execution error (${name}): ${error?.message || String(error)}`
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (skill.type === "template" && skill.template) {
|
|
537
|
+
// Template skill — expand $ARGUMENTS, $1, $2, etc.
|
|
538
|
+
return applyCommandTemplate(skill.template, args, {
|
|
539
|
+
path: context.cwd || process.cwd(),
|
|
540
|
+
mode: context.mode || "agent",
|
|
541
|
+
provider: context.provider || "",
|
|
542
|
+
cwd: context.cwd || process.cwd(),
|
|
543
|
+
project: path.basename(context.cwd || process.cwd())
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (skill.type === "skill_md" && skill.template) {
|
|
548
|
+
const cwd = context.cwd || process.cwd()
|
|
549
|
+
let prompt = applyCommandTemplate(skill.template, args, {
|
|
550
|
+
path: cwd, mode: context.mode || "agent",
|
|
551
|
+
provider: context.provider || "", cwd, project: path.basename(cwd),
|
|
552
|
+
SKILL_ROOT: skill.skillRoot || skill.skillDir || path.dirname(skill.source),
|
|
553
|
+
SKILL_DIR: skill.skillRoot || skill.skillDir || path.dirname(skill.source),
|
|
554
|
+
SKILL_NAME: skill.name,
|
|
555
|
+
ARGUMENT_HINT: skill.argumentHint || "",
|
|
556
|
+
WHEN_TO_USE: skill.whenToUse || ""
|
|
557
|
+
})
|
|
558
|
+
// Resolve $FILE{name} references to auxiliary file contents
|
|
559
|
+
if (skill.auxFiles) {
|
|
560
|
+
const resolvedSkillDir = path.resolve(skill.skillDir)
|
|
561
|
+
const filePattern = /\$FILE\{([^}]+)\}/g
|
|
562
|
+
const fileMatches = [...prompt.matchAll(filePattern)]
|
|
563
|
+
for (const m of fileMatches) {
|
|
564
|
+
const filePath = skill.auxFiles[m[1]]
|
|
565
|
+
if (filePath) {
|
|
566
|
+
// Path traversal protection for $FILE{} references
|
|
567
|
+
const resolvedFile = path.resolve(filePath)
|
|
568
|
+
if (!resolvedFile.startsWith(resolvedSkillDir + path.sep)) {
|
|
569
|
+
prompt = prompt.replace(m[0], `[blocked: path traversal: ${m[1]}]`)
|
|
570
|
+
EventBus.emit({
|
|
571
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
572
|
+
payload: { kind: "skill_path_traversal", file: m[1], skillDir: skill.skillDir }
|
|
573
|
+
}).catch(() => {})
|
|
574
|
+
continue
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const content = await readFile(filePath, "utf8")
|
|
578
|
+
prompt = prompt.replace(m[0], content.trim())
|
|
579
|
+
} catch {
|
|
580
|
+
prompt = prompt.replace(m[0], `[file not found: ${m[1]}]`)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
prompt = await injectDynamicContext(prompt, cwd, context.config)
|
|
586
|
+
if (skill.contextFork) {
|
|
587
|
+
return { prompt, contextFork: true, model: skill.model }
|
|
588
|
+
}
|
|
589
|
+
return prompt
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (skill.type === "mcp_prompt" && skill.promptId) {
|
|
593
|
+
// MCP prompt — fetch from server
|
|
594
|
+
const promptArgs = {}
|
|
595
|
+
if (args) {
|
|
596
|
+
// Simple: pass entire args string as first argument
|
|
597
|
+
const argDefs = skill.arguments || []
|
|
598
|
+
if (argDefs.length === 1) {
|
|
599
|
+
promptArgs[argDefs[0].name] = args
|
|
600
|
+
} else if (argDefs.length > 1) {
|
|
601
|
+
// Split args by spaces for multiple arguments
|
|
602
|
+
const tokens = args.split(/\s+/)
|
|
603
|
+
for (let i = 0; i < argDefs.length && i < tokens.length; i++) {
|
|
604
|
+
promptArgs[argDefs[i].name] = tokens[i]
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const result = await McpRegistry.getPrompt(skill.promptId, promptArgs)
|
|
609
|
+
// MCP prompt result: { messages: [{ role, content: { type, text } }] }
|
|
610
|
+
if (result?.messages) {
|
|
611
|
+
return result.messages
|
|
612
|
+
.map((m) => {
|
|
613
|
+
if (typeof m.content === "string") return m.content
|
|
614
|
+
if (m.content?.text) return m.content.text
|
|
615
|
+
return ""
|
|
616
|
+
})
|
|
617
|
+
.filter(Boolean)
|
|
618
|
+
.join("\n\n")
|
|
619
|
+
}
|
|
620
|
+
return JSON.stringify(result)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return null
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Return skill metadata for system prompt inclusion.
|
|
628
|
+
*/
|
|
629
|
+
listForSystemPrompt() {
|
|
630
|
+
return [...state.skills.entries()]
|
|
631
|
+
.filter(([key, s]) => !s.disableModelInvocation && !key.startsWith("mcp:"))
|
|
632
|
+
.map(([, s]) => ({ name: s.name, description: s.description }))
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
listPluginManifests() {
|
|
636
|
+
return [...state.plugins]
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
pluginErrors() {
|
|
640
|
+
return [...state.pluginErrors]
|
|
641
|
+
}
|
|
642
|
+
}
|