@kkelly-offical/kkcode 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +41 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +1 -1
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/engine.mjs +227 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1081 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +884 -1462
- package/src/session/project-context.mjs +30 -0
- package/src/session/store.mjs +510 -503
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
package/src/skill/registry.mjs
CHANGED
|
@@ -1,336 +1,390 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import { access, readdir, readFile } from "node:fs/promises"
|
|
3
|
-
import { pathToFileURL, fileURLToPath } from "node:url"
|
|
4
|
-
import { exec } 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
const
|
|
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
|
-
}
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { access, readdir, readFile } from "node:fs/promises"
|
|
3
|
+
import { pathToFileURL, fileURLToPath } from "node:url"
|
|
4
|
+
import { exec } 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
|
+
|
|
12
|
+
const execAsync = promisify(exec)
|
|
13
|
+
|
|
14
|
+
const DEFAULT_ALLOWED_COMMANDS = ["git", "node", "npm", "ls", "cat", "date", "pwd", "echo", "which"]
|
|
15
|
+
let _allowedCommands = null
|
|
16
|
+
let _allowedCommandsSig = null
|
|
17
|
+
|
|
18
|
+
function getAllowedCommands(config) {
|
|
19
|
+
const extra = config?.skills?.allowed_commands || []
|
|
20
|
+
const sig = extra.join(",")
|
|
21
|
+
if (_allowedCommands && _allowedCommandsSig === sig) return _allowedCommands
|
|
22
|
+
_allowedCommands = new Set([...DEFAULT_ALLOWED_COMMANDS, ...extra])
|
|
23
|
+
_allowedCommandsSig = sig
|
|
24
|
+
return _allowedCommands
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isCommandAllowed(cmdString, config) {
|
|
28
|
+
const allowed = getAllowedCommands(config)
|
|
29
|
+
const trimmed = cmdString.trim()
|
|
30
|
+
// Extract the base command (first token, strip path)
|
|
31
|
+
const firstToken = trimmed.split(/\s+/)[0] || ""
|
|
32
|
+
const baseName = path.basename(firstToken)
|
|
33
|
+
return allowed.has(baseName)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function exists(target) {
|
|
37
|
+
try {
|
|
38
|
+
await access(target)
|
|
39
|
+
return true
|
|
40
|
+
} catch {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse YAML frontmatter from SKILL.md content.
|
|
47
|
+
* Returns { meta: {}, body: string }
|
|
48
|
+
*/
|
|
49
|
+
function parseFrontmatter(raw) {
|
|
50
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
|
|
51
|
+
if (!match) return { meta: {}, body: raw.trim() }
|
|
52
|
+
try {
|
|
53
|
+
return { meta: parseYaml(match[1]) || {}, body: match[2].trim() }
|
|
54
|
+
} catch {
|
|
55
|
+
return { meta: {}, body: raw.trim() }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Replace !`command` patterns with command stdout.
|
|
61
|
+
* Commands are checked against a whitelist before execution.
|
|
62
|
+
*/
|
|
63
|
+
async function injectDynamicContext(template, cwd, config) {
|
|
64
|
+
const pattern = /!\`([^`]+)\`/g
|
|
65
|
+
const matches = [...template.matchAll(pattern)]
|
|
66
|
+
if (!matches.length) return template
|
|
67
|
+
let result = template
|
|
68
|
+
for (const m of matches) {
|
|
69
|
+
if (!isCommandAllowed(m[1], config)) {
|
|
70
|
+
result = result.replace(m[0], `[blocked: ${m[1]}]`)
|
|
71
|
+
EventBus.emit({
|
|
72
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
73
|
+
payload: { kind: "skill_command_blocked", command: m[1] }
|
|
74
|
+
}).catch(() => {})
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const { stdout } = await execAsync(m[1], { cwd, timeout: 10000 })
|
|
79
|
+
result = result.replace(m[0], stdout.trim())
|
|
80
|
+
} catch {
|
|
81
|
+
result = result.replace(m[0], `[command failed: ${m[1]}]`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load SKILL.md directory-format skills from a directory.
|
|
89
|
+
* Scans for <dir>/<name>/SKILL.md
|
|
90
|
+
*/
|
|
91
|
+
async function loadAuxFiles(skillDir) {
|
|
92
|
+
const aux = {}
|
|
93
|
+
const resolvedSkillDir = path.resolve(skillDir)
|
|
94
|
+
try {
|
|
95
|
+
const entries = await readdir(skillDir, { withFileTypes: true })
|
|
96
|
+
for (const e of entries) {
|
|
97
|
+
if (!e.isFile() || e.name === "SKILL.md") continue
|
|
98
|
+
const filePath = path.resolve(skillDir, e.name)
|
|
99
|
+
// Path traversal protection: ensure file is within skillDir
|
|
100
|
+
if (!filePath.startsWith(resolvedSkillDir + path.sep) && filePath !== resolvedSkillDir) {
|
|
101
|
+
EventBus.emit({
|
|
102
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
103
|
+
payload: { kind: "skill_path_traversal", file: e.name, skillDir }
|
|
104
|
+
}).catch(() => {})
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
aux[e.name] = filePath
|
|
108
|
+
}
|
|
109
|
+
} catch { /* ignore */ }
|
|
110
|
+
return aux
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function loadSkillDirs(dir, scope) {
|
|
114
|
+
if (!(await exists(dir))) return []
|
|
115
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
116
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort()
|
|
117
|
+
const skills = []
|
|
118
|
+
for (const name of dirs) {
|
|
119
|
+
const skillDir = path.join(dir, name)
|
|
120
|
+
const mdPath = path.join(skillDir, "SKILL.md")
|
|
121
|
+
if (!(await exists(mdPath))) continue
|
|
122
|
+
try {
|
|
123
|
+
const raw = await readFile(mdPath, "utf8")
|
|
124
|
+
const { meta, body } = parseFrontmatter(raw)
|
|
125
|
+
const auxFiles = await loadAuxFiles(skillDir)
|
|
126
|
+
skills.push({
|
|
127
|
+
name: meta.name || name,
|
|
128
|
+
description: meta.description || name,
|
|
129
|
+
type: "skill_md",
|
|
130
|
+
scope,
|
|
131
|
+
source: mdPath,
|
|
132
|
+
skillDir,
|
|
133
|
+
template: body,
|
|
134
|
+
auxFiles,
|
|
135
|
+
disableModelInvocation: !!meta["disable-model-invocation"],
|
|
136
|
+
userInvocable: meta["user-invocable"] !== false,
|
|
137
|
+
allowedTools: meta["allowed-tools"] || null,
|
|
138
|
+
model: meta.model || null,
|
|
139
|
+
contextFork: !!meta["context-fork"]
|
|
140
|
+
})
|
|
141
|
+
} catch { /* skip broken */ }
|
|
142
|
+
}
|
|
143
|
+
return skills
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Load .mjs programmable skills from a directory.
|
|
148
|
+
* Each .mjs file should export: { name, description, run(ctx) }
|
|
149
|
+
* run() returns a string prompt to send to the model.
|
|
150
|
+
*/
|
|
151
|
+
async function loadMjsSkills(dir, scope) {
|
|
152
|
+
if (!(await exists(dir))) return []
|
|
153
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
154
|
+
const files = entries
|
|
155
|
+
.filter((e) => e.isFile() && e.name.endsWith(".mjs"))
|
|
156
|
+
.map((e) => e.name)
|
|
157
|
+
.sort()
|
|
158
|
+
|
|
159
|
+
const skills = []
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const full = path.join(dir, file)
|
|
162
|
+
try {
|
|
163
|
+
const mod = await import(pathToFileURL(full).href)
|
|
164
|
+
const name = mod.name || path.basename(file, ".mjs")
|
|
165
|
+
skills.push({
|
|
166
|
+
name,
|
|
167
|
+
description: mod.description || name,
|
|
168
|
+
type: "mjs",
|
|
169
|
+
scope,
|
|
170
|
+
source: full,
|
|
171
|
+
run: typeof mod.run === "function" ? mod.run : null
|
|
172
|
+
})
|
|
173
|
+
} catch {
|
|
174
|
+
// Skip broken skill files silently
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return skills
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Convert custom commands (.md templates) to skill format.
|
|
182
|
+
*/
|
|
183
|
+
function customCommandsToSkills(commands) {
|
|
184
|
+
return commands.map((cmd) => ({
|
|
185
|
+
name: cmd.name,
|
|
186
|
+
description: `custom command (${cmd.scope})`,
|
|
187
|
+
type: "template",
|
|
188
|
+
scope: cmd.scope,
|
|
189
|
+
source: cmd.source,
|
|
190
|
+
template: cmd.template
|
|
191
|
+
}))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Convert MCP prompts to skill format.
|
|
196
|
+
*/
|
|
197
|
+
function mcpPromptsToSkills(prompts) {
|
|
198
|
+
return prompts.map((p) => ({
|
|
199
|
+
name: p.name,
|
|
200
|
+
description: p.description || `${p.server}:${p.name}`,
|
|
201
|
+
type: "mcp_prompt",
|
|
202
|
+
scope: "mcp",
|
|
203
|
+
server: p.server,
|
|
204
|
+
promptId: p.id,
|
|
205
|
+
arguments: p.arguments || []
|
|
206
|
+
}))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const state = {
|
|
210
|
+
skills: new Map(),
|
|
211
|
+
loaded: false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const SkillRegistry = {
|
|
215
|
+
/**
|
|
216
|
+
* Load all skills from all sources.
|
|
217
|
+
*/
|
|
218
|
+
async initialize(config, cwd = process.cwd()) {
|
|
219
|
+
state.skills.clear()
|
|
220
|
+
|
|
221
|
+
// Source 0: Built-in skills (shipped with kkcode)
|
|
222
|
+
const builtinDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "builtin")
|
|
223
|
+
const builtinSkills = await loadMjsSkills(builtinDir, "builtin")
|
|
224
|
+
for (const skill of builtinSkills) {
|
|
225
|
+
state.skills.set(skill.name, skill)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Source 1: Custom commands (.md templates)
|
|
229
|
+
const customCommands = await loadCustomCommands(cwd)
|
|
230
|
+
for (const skill of customCommandsToSkills(customCommands)) {
|
|
231
|
+
state.skills.set(skill.name, skill)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Source 2: Programmable skills (.mjs)
|
|
235
|
+
const userRoot = process.env.USERPROFILE || process.env.HOME || cwd
|
|
236
|
+
const globalSkillDir = path.join(userRoot, ".kkcode", "skills")
|
|
237
|
+
const projectSkillDir = path.join(cwd, ".kkcode", "skills")
|
|
238
|
+
const [globalSkills, projectSkills, globalSkillMds, projectSkillMds] = await Promise.all([
|
|
239
|
+
loadMjsSkills(globalSkillDir, "global"),
|
|
240
|
+
loadMjsSkills(projectSkillDir, "project"),
|
|
241
|
+
loadSkillDirs(globalSkillDir, "global"),
|
|
242
|
+
loadSkillDirs(projectSkillDir, "project")
|
|
243
|
+
])
|
|
244
|
+
// Project skills override global skills with same name
|
|
245
|
+
for (const skill of [...globalSkills, ...projectSkills, ...globalSkillMds, ...projectSkillMds]) {
|
|
246
|
+
state.skills.set(skill.name, skill)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Source 3: MCP prompts (if MCP is initialized)
|
|
250
|
+
if (McpRegistry.isReady()) {
|
|
251
|
+
const prompts = McpRegistry.listPrompts()
|
|
252
|
+
for (const skill of mcpPromptsToSkills(prompts)) {
|
|
253
|
+
// Prefix MCP skills to avoid name collisions
|
|
254
|
+
const key = `mcp:${skill.name}`
|
|
255
|
+
state.skills.set(key, { ...skill, name: key })
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
state.loaded = true
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
isReady() {
|
|
263
|
+
return state.loaded
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
list() {
|
|
267
|
+
return [...state.skills.values()]
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
get(name) {
|
|
271
|
+
return state.skills.get(name) || null
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Execute a skill and return the expanded prompt string.
|
|
276
|
+
*/
|
|
277
|
+
async execute(name, args = "", context = {}) {
|
|
278
|
+
const skill = state.skills.get(name)
|
|
279
|
+
if (!skill) return null
|
|
280
|
+
|
|
281
|
+
if (skill.type === "mjs" && skill.run) {
|
|
282
|
+
// Programmable skill — call run() to get prompt
|
|
283
|
+
try {
|
|
284
|
+
const result = await skill.run({
|
|
285
|
+
args,
|
|
286
|
+
cwd: context.cwd || process.cwd(),
|
|
287
|
+
mode: context.mode || "agent",
|
|
288
|
+
model: context.model || "",
|
|
289
|
+
provider: context.provider || ""
|
|
290
|
+
})
|
|
291
|
+
return result == null ? "" : typeof result === "string" ? result : JSON.stringify(result)
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return `skill execution error (${name}): ${error?.message || String(error)}`
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (skill.type === "template" && skill.template) {
|
|
298
|
+
// Template skill — expand $ARGUMENTS, $1, $2, etc.
|
|
299
|
+
return applyCommandTemplate(skill.template, args, {
|
|
300
|
+
path: context.cwd || process.cwd(),
|
|
301
|
+
mode: context.mode || "agent",
|
|
302
|
+
provider: context.provider || "",
|
|
303
|
+
cwd: context.cwd || process.cwd(),
|
|
304
|
+
project: path.basename(context.cwd || process.cwd())
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (skill.type === "skill_md" && skill.template) {
|
|
309
|
+
const cwd = context.cwd || process.cwd()
|
|
310
|
+
let prompt = applyCommandTemplate(skill.template, args, {
|
|
311
|
+
path: cwd, mode: context.mode || "agent",
|
|
312
|
+
provider: context.provider || "", cwd, project: path.basename(cwd)
|
|
313
|
+
})
|
|
314
|
+
// Resolve $FILE{name} references to auxiliary file contents
|
|
315
|
+
if (skill.auxFiles) {
|
|
316
|
+
const resolvedSkillDir = path.resolve(skill.skillDir)
|
|
317
|
+
const filePattern = /\$FILE\{([^}]+)\}/g
|
|
318
|
+
const fileMatches = [...prompt.matchAll(filePattern)]
|
|
319
|
+
for (const m of fileMatches) {
|
|
320
|
+
const filePath = skill.auxFiles[m[1]]
|
|
321
|
+
if (filePath) {
|
|
322
|
+
// Path traversal protection for $FILE{} references
|
|
323
|
+
const resolvedFile = path.resolve(filePath)
|
|
324
|
+
if (!resolvedFile.startsWith(resolvedSkillDir + path.sep)) {
|
|
325
|
+
prompt = prompt.replace(m[0], `[blocked: path traversal: ${m[1]}]`)
|
|
326
|
+
EventBus.emit({
|
|
327
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
328
|
+
payload: { kind: "skill_path_traversal", file: m[1], skillDir: skill.skillDir }
|
|
329
|
+
}).catch(() => {})
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const content = await readFile(filePath, "utf8")
|
|
334
|
+
prompt = prompt.replace(m[0], content.trim())
|
|
335
|
+
} catch {
|
|
336
|
+
prompt = prompt.replace(m[0], `[file not found: ${m[1]}]`)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
prompt = await injectDynamicContext(prompt, cwd, context.config)
|
|
342
|
+
if (skill.contextFork) {
|
|
343
|
+
return { prompt, contextFork: true, model: skill.model }
|
|
344
|
+
}
|
|
345
|
+
return prompt
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (skill.type === "mcp_prompt" && skill.promptId) {
|
|
349
|
+
// MCP prompt — fetch from server
|
|
350
|
+
const promptArgs = {}
|
|
351
|
+
if (args) {
|
|
352
|
+
// Simple: pass entire args string as first argument
|
|
353
|
+
const argDefs = skill.arguments || []
|
|
354
|
+
if (argDefs.length === 1) {
|
|
355
|
+
promptArgs[argDefs[0].name] = args
|
|
356
|
+
} else if (argDefs.length > 1) {
|
|
357
|
+
// Split args by spaces for multiple arguments
|
|
358
|
+
const tokens = args.split(/\s+/)
|
|
359
|
+
for (let i = 0; i < argDefs.length && i < tokens.length; i++) {
|
|
360
|
+
promptArgs[argDefs[i].name] = tokens[i]
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const result = await McpRegistry.getPrompt(skill.promptId, promptArgs)
|
|
365
|
+
// MCP prompt result: { messages: [{ role, content: { type, text } }] }
|
|
366
|
+
if (result?.messages) {
|
|
367
|
+
return result.messages
|
|
368
|
+
.map((m) => {
|
|
369
|
+
if (typeof m.content === "string") return m.content
|
|
370
|
+
if (m.content?.text) return m.content.text
|
|
371
|
+
return ""
|
|
372
|
+
})
|
|
373
|
+
.filter(Boolean)
|
|
374
|
+
.join("\n\n")
|
|
375
|
+
}
|
|
376
|
+
return JSON.stringify(result)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return null
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Return skill metadata for system prompt inclusion.
|
|
384
|
+
*/
|
|
385
|
+
listForSystemPrompt() {
|
|
386
|
+
return [...state.skills.values()]
|
|
387
|
+
.filter((s) => !s.disableModelInvocation)
|
|
388
|
+
.map((s) => ({ name: s.name, description: s.description }))
|
|
389
|
+
}
|
|
390
|
+
}
|