@shawnstack/quickforge 1.1.0 → 1.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/README.md +1 -1
- package/bin/quickforge.mjs +72 -7
- package/dist/assets/{anthropic-By-wpU1w.js → anthropic-DLvtwHL2.js} +2 -2
- package/dist/assets/{azure-openai-responses-C8spS__i.js → azure-openai-responses-D68z7hLN.js} +1 -1
- package/dist/assets/css-utils-rkE68RDy.js +1 -0
- package/dist/assets/{google-DiIcyajo.js → google-B_sSaRBM.js} +1 -1
- package/dist/assets/{google-gemini-cli-BXZFGMXD.js → google-gemini-cli-CYqGXjGi.js} +1 -1
- package/dist/assets/google-shared-XhYUKiGZ.js +11 -0
- package/dist/assets/{google-vertex-D93MV5Cx.js → google-vertex-DSMuB4YB.js} +1 -1
- package/dist/assets/icons-BsZ9PlYY.js +1 -0
- package/dist/assets/index-BqFfVQJM.css +3 -0
- package/dist/assets/index-DoraECXN.js +3187 -0
- package/dist/assets/lit-vendor-1dsGB-Iy.js +2 -0
- package/dist/assets/{mistral-BAJNGYqd.js → mistral-BZngRB4x.js} +2 -2
- package/dist/assets/{openai-codex-responses-BHHCy65K.js → openai-codex-responses-Niu7xDYK.js} +1 -1
- package/dist/assets/openai-completions-B2bhb9k0.js +5 -0
- package/dist/assets/{openai-responses-CP9-AyAD.js → openai-responses-CDYDv8yL.js} +1 -1
- package/dist/assets/{openai-responses-shared-_z7sua8J.js → openai-responses-shared-BIKPTpEQ.js} +1 -1
- package/dist/assets/react-vendor-Ds3ovY0w.js +9 -0
- package/dist/assets/rolldown-runtime-CkqCuyE9.js +1 -0
- package/dist/index.html +7 -3
- package/package.json +14 -13
- package/server/agent-manager.mjs +1053 -0
- package/server/conversation-compaction.mjs +302 -0
- package/server/custom-commands.mjs +344 -0
- package/server/index.mjs +322 -32
- package/server/project-config.mjs +80 -31
- package/server/reasoning-cache.mjs +51 -0
- package/server/restart-supervisor.mjs +38 -0
- package/server/routes/agent.mjs +323 -0
- package/server/routes/backup.mjs +250 -0
- package/server/routes/instructions.mjs +6 -17
- package/server/routes/project.mjs +46 -10
- package/server/routes/scheduled-tasks.mjs +424 -0
- package/server/routes/shared-conversation.mjs +404 -0
- package/server/routes/shares.mjs +84 -0
- package/server/routes/skills.mjs +145 -0
- package/server/routes/static.mjs +4 -3
- package/server/routes/storage.mjs +58 -10
- package/server/routes/system.mjs +35 -0
- package/server/routes/tools.mjs +53 -2
- package/server/session-utils.mjs +102 -0
- package/server/share-store.mjs +468 -0
- package/server/skills.mjs +539 -0
- package/server/storage.mjs +247 -6
- package/server/system-prompt.mjs +67 -0
- package/server/tools/definitions.mjs +120 -0
- package/server/tools/index.mjs +167 -46
- package/server/utils/logger.mjs +34 -0
- package/server/utils/network.mjs +38 -0
- package/server/utils/platform.mjs +30 -0
- package/server/utils/response.mjs +8 -1
- package/skills/ai-context-package/SKILL.md +104 -0
- package/skills/ai-context-package/skill.json +9 -0
- package/skills/code-review/SKILL.md +23 -0
- package/skills/code-review/skill.json +9 -0
- package/skills/frontend-react/SKILL.md +22 -0
- package/skills/frontend-react/skill.json +9 -0
- package/skills/quickforge-project/SKILL.md +22 -0
- package/skills/quickforge-project/skill.json +9 -0
- package/dist/assets/chunk-62oNxeRG.js +0 -1
- package/dist/assets/confirm-dialog-4mZt9XEq.js +0 -1
- package/dist/assets/google-shared-CXUHW-9O.js +0 -11
- package/dist/assets/index-Bq6VHkyY.js +0 -3048
- package/dist/assets/index-D7uXa1RT.css +0 -3
- package/dist/assets/openai-completions-BtZAvOiJ.js +0 -5
- package/dist/assets/prompt-dialog-BGMKszUz.js +0 -1
- /package/dist/assets/{github-copilot-headers-C0toI16e.js → github-copilot-headers-CrI0CIJ7.js} +0 -0
- /package/dist/assets/{hash-fDQBJsbb.js → hash-Bt1aVMQ3.js} +0 -0
- /package/dist/assets/{headers-Drkm68SQ.js → headers-5EYI0_pl.js} +0 -0
- /package/dist/assets/{openai-CuiHR4mv.js → openai-Cn7eGqwa.js} +0 -0
- /package/dist/assets/{transform-messages-BFwlToJ0.js → transform-messages-CV4kCtBB.js} +0 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { existsSync, promises as fs } from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { dataDir } from './storage.mjs'
|
|
5
|
+
|
|
6
|
+
const userSkillsDir = path.join(dataDir, 'skills')
|
|
7
|
+
const sharedUserSkillsDir = path.join(os.homedir(), '.agents', 'skills')
|
|
8
|
+
const defaultEntry = 'SKILL.md'
|
|
9
|
+
const resourceDirs = ['scripts', 'references', 'assets']
|
|
10
|
+
const maxResourceFiles = 200
|
|
11
|
+
|
|
12
|
+
function isValidSkillName(value) {
|
|
13
|
+
return (
|
|
14
|
+
typeof value === 'string' &&
|
|
15
|
+
value.length >= 1 &&
|
|
16
|
+
value.length <= 64 &&
|
|
17
|
+
/^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value)
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeString(value) {
|
|
22
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeStringArray(value) {
|
|
26
|
+
if (!Array.isArray(value)) return []
|
|
27
|
+
const result = []
|
|
28
|
+
const seen = new Set()
|
|
29
|
+
for (const item of value) {
|
|
30
|
+
const text = normalizeString(item)
|
|
31
|
+
if (!text || seen.has(text)) continue
|
|
32
|
+
seen.add(text)
|
|
33
|
+
result.push(text)
|
|
34
|
+
}
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeSkillNames(value) {
|
|
39
|
+
return normalizeStringArray(value).filter(isValidSkillName)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isPathInside(root, target) {
|
|
43
|
+
const relative = path.relative(root, target)
|
|
44
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readJsonFile(file, defaultValue = {}) {
|
|
48
|
+
try {
|
|
49
|
+
const text = await fs.readFile(file, 'utf8')
|
|
50
|
+
const json = text.trimStart()
|
|
51
|
+
return json ? JSON.parse(json) : defaultValue
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error?.code === 'ENOENT') return defaultValue
|
|
54
|
+
throw error
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readOptionalText(file) {
|
|
59
|
+
try {
|
|
60
|
+
return await fs.readFile(file, 'utf8')
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error?.code === 'ENOENT') return null
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseFrontmatter(text) {
|
|
68
|
+
const normalized = String(text || '').replace(/^\uFEFF/, '')
|
|
69
|
+
const match = normalized.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/)
|
|
70
|
+
if (!match) return null
|
|
71
|
+
return {
|
|
72
|
+
frontmatter: match[1],
|
|
73
|
+
body: match[2].trim(),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function leadingIndent(line) {
|
|
78
|
+
const match = line.match(/^\s*/)
|
|
79
|
+
return match ? match[0].length : 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stripInlineComment(value) {
|
|
83
|
+
const trimmed = value.trim()
|
|
84
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) return trimmed
|
|
85
|
+
const index = trimmed.indexOf(' #')
|
|
86
|
+
return index >= 0 ? trimmed.slice(0, index).trimEnd() : trimmed
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseYamlScalar(value) {
|
|
90
|
+
const trimmed = stripInlineComment(String(value ?? ''))
|
|
91
|
+
if (!trimmed) return ''
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
95
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
96
|
+
) {
|
|
97
|
+
return trimmed
|
|
98
|
+
.slice(1, -1)
|
|
99
|
+
.replace(/\\"/g, '"')
|
|
100
|
+
.replace(/\\'/g, "'")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return trimmed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collectIndentedBlock(lines, startIndex, parentIndent) {
|
|
107
|
+
const block = []
|
|
108
|
+
let index = startIndex
|
|
109
|
+
while (index < lines.length) {
|
|
110
|
+
const line = lines[index]
|
|
111
|
+
if (!line.trim()) {
|
|
112
|
+
block.push(line)
|
|
113
|
+
index++
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if (leadingIndent(line) <= parentIndent) break
|
|
117
|
+
block.push(line)
|
|
118
|
+
index++
|
|
119
|
+
}
|
|
120
|
+
return { block, nextIndex: index }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseBlockScalar(lines, style) {
|
|
124
|
+
const nonEmpty = lines.filter((line) => line.trim())
|
|
125
|
+
const minIndent = nonEmpty.length
|
|
126
|
+
? Math.min(...nonEmpty.map((line) => leadingIndent(line)))
|
|
127
|
+
: 0
|
|
128
|
+
const unindented = lines.map((line) => line.slice(Math.min(minIndent, line.length)))
|
|
129
|
+
return style === '>'
|
|
130
|
+
? unindented.join(' ').replace(/\s+/g, ' ').trim()
|
|
131
|
+
: unindented.join('\n').trim()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseSimpleYamlMap(text) {
|
|
135
|
+
const result = {}
|
|
136
|
+
const lines = String(text || '').split(/\r?\n/)
|
|
137
|
+
let index = 0
|
|
138
|
+
|
|
139
|
+
while (index < lines.length) {
|
|
140
|
+
const line = lines[index]
|
|
141
|
+
const trimmed = line.trim()
|
|
142
|
+
if (!trimmed || trimmed.startsWith('#') || leadingIndent(line) > 0) {
|
|
143
|
+
index++
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const match = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/)
|
|
148
|
+
if (!match) {
|
|
149
|
+
index++
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const [, key, rawValue = ''] = match
|
|
154
|
+
const value = rawValue.trim()
|
|
155
|
+
|
|
156
|
+
if (value === '|' || value === '>') {
|
|
157
|
+
const { block, nextIndex } = collectIndentedBlock(lines, index + 1, 0)
|
|
158
|
+
result[key] = parseBlockScalar(block, value)
|
|
159
|
+
index = nextIndex
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (value) {
|
|
164
|
+
result[key] = parseYamlScalar(value)
|
|
165
|
+
index++
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const nested = {}
|
|
170
|
+
let nestedIndex = index + 1
|
|
171
|
+
while (nestedIndex < lines.length) {
|
|
172
|
+
const nestedLine = lines[nestedIndex]
|
|
173
|
+
const nestedTrimmed = nestedLine.trim()
|
|
174
|
+
if (!nestedTrimmed || nestedTrimmed.startsWith('#')) {
|
|
175
|
+
nestedIndex++
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const indent = leadingIndent(nestedLine)
|
|
180
|
+
if (indent <= 0) break
|
|
181
|
+
|
|
182
|
+
const nestedMatch = nestedLine.slice(indent).match(/^([A-Za-z0-9_.-]+):(?:\s*(.*))?$/)
|
|
183
|
+
if (!nestedMatch) {
|
|
184
|
+
nestedIndex++
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const [, nestedKey, nestedRawValue = ''] = nestedMatch
|
|
189
|
+
nested[nestedKey] = parseYamlScalar(nestedRawValue.trim())
|
|
190
|
+
nestedIndex++
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
result[key] = Object.keys(nested).length ? nested : ''
|
|
194
|
+
index = Object.keys(nested).length ? nestedIndex : index + 1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeMetadata(value) {
|
|
201
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
202
|
+
const metadata = {}
|
|
203
|
+
for (const [key, item] of Object.entries(value)) {
|
|
204
|
+
if (!key || item === undefined || item === null || typeof item === 'object') continue
|
|
205
|
+
metadata[key] = String(item)
|
|
206
|
+
}
|
|
207
|
+
return metadata
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function listSkillDirectories(root) {
|
|
211
|
+
if (!root || !existsSync(root)) return []
|
|
212
|
+
const entries = await fs.readdir(root, { withFileTypes: true })
|
|
213
|
+
return entries
|
|
214
|
+
.filter((entry) => entry.isDirectory())
|
|
215
|
+
.map((entry) => path.join(root, entry.name))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function splitMetadataList(value) {
|
|
219
|
+
return String(value || '')
|
|
220
|
+
.split(',')
|
|
221
|
+
.map((item) => item.trim())
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function metadataValue(metadata, key) {
|
|
226
|
+
return normalizeString(metadata[key])
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function skillFromStandardMarkdown(rootDir, source, text) {
|
|
230
|
+
const parsed = parseFrontmatter(text)
|
|
231
|
+
if (!parsed?.body) return null
|
|
232
|
+
|
|
233
|
+
const rawManifest = parseSimpleYamlMap(parsed.frontmatter)
|
|
234
|
+
const name = normalizeString(rawManifest.name)
|
|
235
|
+
const description = normalizeString(rawManifest.description)
|
|
236
|
+
if (!name || !isValidSkillName(name)) return null
|
|
237
|
+
if (name !== path.basename(rootDir)) return null
|
|
238
|
+
if (!description || description.length > 1024) return null
|
|
239
|
+
|
|
240
|
+
const metadata = normalizeMetadata(rawManifest.metadata)
|
|
241
|
+
const compatibility = normalizeString(rawManifest.compatibility)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
name,
|
|
245
|
+
displayName: metadataValue(metadata, 'displayName') || metadataValue(metadata, 'title'),
|
|
246
|
+
description,
|
|
247
|
+
license: normalizeString(rawManifest.license),
|
|
248
|
+
compatibility: compatibility && compatibility.length <= 500 ? compatibility : undefined,
|
|
249
|
+
metadata,
|
|
250
|
+
allowedTools: normalizeString(rawManifest['allowed-tools']),
|
|
251
|
+
version: metadataValue(metadata, 'version'),
|
|
252
|
+
tags: splitMetadataList(metadata.tags),
|
|
253
|
+
triggers: splitMetadataList(metadata.triggers),
|
|
254
|
+
entry: defaultEntry,
|
|
255
|
+
source,
|
|
256
|
+
rootDir,
|
|
257
|
+
location: path.join(rootDir, defaultEntry),
|
|
258
|
+
instructions: parsed.body,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function loadLegacySkillDirectory(rootDir, source) {
|
|
263
|
+
const rawManifest = await readJsonFile(path.join(rootDir, 'skill.json'), null)
|
|
264
|
+
if (!rawManifest || typeof rawManifest !== 'object') return null
|
|
265
|
+
if (rawManifest.enabled === false) return null
|
|
266
|
+
|
|
267
|
+
const name = normalizeString(rawManifest.name) || path.basename(rootDir)
|
|
268
|
+
if (!isValidSkillName(name)) return null
|
|
269
|
+
|
|
270
|
+
const entry = normalizeString(rawManifest.entry) || defaultEntry
|
|
271
|
+
const entryPath = path.resolve(rootDir, entry)
|
|
272
|
+
if (!isPathInside(rootDir, entryPath)) return null
|
|
273
|
+
|
|
274
|
+
const instructions = (await readOptionalText(entryPath))?.trim()
|
|
275
|
+
const description = normalizeString(rawManifest.description)
|
|
276
|
+
if (!instructions || !description) return null
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
name,
|
|
280
|
+
displayName: normalizeString(rawManifest.displayName) || normalizeString(rawManifest.title),
|
|
281
|
+
description,
|
|
282
|
+
license: normalizeString(rawManifest.license),
|
|
283
|
+
compatibility: normalizeString(rawManifest.compatibility),
|
|
284
|
+
metadata: rawManifest.metadata && typeof rawManifest.metadata === 'object' ? rawManifest.metadata : {},
|
|
285
|
+
allowedTools: normalizeString(rawManifest.allowedTools) || normalizeString(rawManifest['allowed-tools']),
|
|
286
|
+
version: normalizeString(rawManifest.version),
|
|
287
|
+
tags: normalizeStringArray(rawManifest.tags),
|
|
288
|
+
triggers: normalizeStringArray(rawManifest.triggers),
|
|
289
|
+
entry,
|
|
290
|
+
source,
|
|
291
|
+
rootDir,
|
|
292
|
+
location: entryPath,
|
|
293
|
+
instructions,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function loadSkillDirectory(rootDir, source) {
|
|
298
|
+
const skillText = await readOptionalText(path.join(rootDir, defaultEntry))
|
|
299
|
+
if (skillText) {
|
|
300
|
+
const standardSkill = skillFromStandardMarkdown(rootDir, source, skillText)
|
|
301
|
+
if (standardSkill) return standardSkill
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return loadLegacySkillDirectory(rootDir, source)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function projectClientSkillsDir(workspaceRoot) {
|
|
308
|
+
return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.quickforge', 'skills') : ''
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function projectSharedSkillsDir(workspaceRoot) {
|
|
312
|
+
return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.agents', 'skills') : ''
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function loadSkillsFromSources(sources) {
|
|
316
|
+
const skillsByName = new Map()
|
|
317
|
+
|
|
318
|
+
for (const source of sources) {
|
|
319
|
+
for (const skillDir of await listSkillDirectories(source.dir)) {
|
|
320
|
+
try {
|
|
321
|
+
const skill = await loadSkillDirectory(skillDir, source.name)
|
|
322
|
+
if (!skill) continue
|
|
323
|
+
if (skillsByName.has(skill.name)) skillsByName.delete(skill.name)
|
|
324
|
+
skillsByName.set(skill.name, skill)
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.warn(`Failed to load skill from ${skillDir}:`, error.message || error)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return [...skillsByName.values()].sort((a, b) => {
|
|
332
|
+
const left = (a.displayName || a.name).toLowerCase()
|
|
333
|
+
const right = (b.displayName || b.name).toLowerCase()
|
|
334
|
+
return left.localeCompare(right)
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function searchDirsForList(value) {
|
|
339
|
+
return value.length === 1 ? value[0] : value.slice()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function summarizeSkills(skills) {
|
|
343
|
+
return skills.map(({ instructions: _instructions, rootDir: _rootDir, location: _location, ...summary }) => summary)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function filterKnownNames(skillNames, skills) {
|
|
347
|
+
const selected = normalizeSkillNames(skillNames)
|
|
348
|
+
if (selected.length === 0) return []
|
|
349
|
+
|
|
350
|
+
const known = new Set(skills.map((skill) => skill.name))
|
|
351
|
+
return selected.filter((name) => known.has(name))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function selectSkills(skillNames, skills) {
|
|
355
|
+
const selected = normalizeSkillNames(skillNames)
|
|
356
|
+
if (selected.length === 0) return []
|
|
357
|
+
|
|
358
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]))
|
|
359
|
+
return selected.map((name) => byName.get(name)).filter(Boolean)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function mergeSkills(...skillLists) {
|
|
363
|
+
const skillsByName = new Map()
|
|
364
|
+
for (const skillList of skillLists) {
|
|
365
|
+
for (const skill of Array.isArray(skillList) ? skillList : []) {
|
|
366
|
+
if (!skill?.name) continue
|
|
367
|
+
if (skillsByName.has(skill.name)) skillsByName.delete(skill.name)
|
|
368
|
+
skillsByName.set(skill.name, skill)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return [...skillsByName.values()]
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export const skillSearchPaths = {
|
|
375
|
+
global: searchDirsForList([sharedUserSkillsDir, userSkillsDir]),
|
|
376
|
+
project: ['<project>/.agents/skills', '<project>/.quickforge/skills'],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function projectSkillSearchPaths(workspaceRoot) {
|
|
380
|
+
if (!workspaceRoot) return skillSearchPaths.project.slice()
|
|
381
|
+
return searchDirsForList([projectSharedSkillsDir(workspaceRoot), projectClientSkillsDir(workspaceRoot)])
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function loadGlobalSkills() {
|
|
385
|
+
return loadSkillsFromSources([
|
|
386
|
+
{ dir: sharedUserSkillsDir, name: 'user-shared' },
|
|
387
|
+
{ dir: userSkillsDir, name: 'user' },
|
|
388
|
+
])
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function loadProjectSkills(workspaceRoot) {
|
|
392
|
+
return loadSkillsFromSources([
|
|
393
|
+
{ dir: projectSharedSkillsDir(workspaceRoot), name: 'project-shared' },
|
|
394
|
+
{ dir: projectClientSkillsDir(workspaceRoot), name: 'project' },
|
|
395
|
+
])
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function loadSkills() {
|
|
399
|
+
return loadGlobalSkills()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function listGlobalSkillSummaries() {
|
|
403
|
+
return summarizeSkills(await loadGlobalSkills())
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function listProjectSkillSummaries(workspaceRoot) {
|
|
407
|
+
return summarizeSkills(await loadProjectSkills(workspaceRoot))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function listSkillSummaries() {
|
|
411
|
+
return listGlobalSkillSummaries()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function filterKnownGlobalSkillNames(skillNames) {
|
|
415
|
+
return filterKnownNames(skillNames, await loadGlobalSkills())
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function filterKnownProjectSkillNames(skillNames, workspaceRoot) {
|
|
419
|
+
return filterKnownNames(skillNames, await loadProjectSkills(workspaceRoot))
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function filterKnownSkillNames(skillNames) {
|
|
423
|
+
return filterKnownGlobalSkillNames(skillNames)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function loadSelectedGlobalSkills(skillNames) {
|
|
427
|
+
return selectSkills(skillNames, await loadGlobalSkills())
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function loadSelectedProjectSkills(skillNames, workspaceRoot) {
|
|
431
|
+
return selectSkills(skillNames, await loadProjectSkills(workspaceRoot))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function loadSelectedSkills(skillNames) {
|
|
435
|
+
return loadSelectedGlobalSkills(skillNames)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function walkResourceFiles(rootDir, currentDir, files, maxFiles) {
|
|
439
|
+
if (files.length >= maxFiles) return
|
|
440
|
+
let entries
|
|
441
|
+
try {
|
|
442
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true })
|
|
443
|
+
} catch {
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
448
|
+
for (const entry of entries) {
|
|
449
|
+
if (files.length >= maxFiles) return
|
|
450
|
+
const fullPath = path.join(currentDir, entry.name)
|
|
451
|
+
if (entry.isDirectory()) {
|
|
452
|
+
await walkResourceFiles(rootDir, fullPath, files, maxFiles)
|
|
453
|
+
} else if (entry.isFile()) {
|
|
454
|
+
files.push(path.relative(rootDir, fullPath).replace(/\\/g, '/'))
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function listSkillResourceFiles(skill, maxFiles = maxResourceFiles) {
|
|
460
|
+
const rootDir = skill?.rootDir
|
|
461
|
+
if (!rootDir) return []
|
|
462
|
+
|
|
463
|
+
const files = []
|
|
464
|
+
for (const dirName of resourceDirs) {
|
|
465
|
+
await walkResourceFiles(rootDir, path.join(rootDir, dirName), files, maxFiles)
|
|
466
|
+
if (files.length >= maxFiles) break
|
|
467
|
+
}
|
|
468
|
+
return files
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function escapeAttribute(value) {
|
|
472
|
+
return String(value ?? '')
|
|
473
|
+
.replace(/&/g, '&')
|
|
474
|
+
.replace(/"/g, '"')
|
|
475
|
+
.replace(/</g, '<')
|
|
476
|
+
.replace(/>/g, '>')
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export async function formatSkillActivation(skill) {
|
|
480
|
+
const resources = await listSkillResourceFiles(skill)
|
|
481
|
+
const resourceBlock = resources.length
|
|
482
|
+
? `\n\n<skill_resources>\n${resources.map((file) => ` <file>${file}</file>`).join('\n')}\n</skill_resources>`
|
|
483
|
+
: ''
|
|
484
|
+
|
|
485
|
+
return `<skill_content name="${escapeAttribute(skill.name)}">\n${skill.instructions}\n\nSkill directory: ${skill.rootDir}\nRelative paths in this skill are relative to the skill directory. Use read_skill_resource with the skill name and a relative path when you need a bundled reference, script, or asset.${resourceBlock}\n</skill_content>`
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export async function readSkillResource(skill, resourcePath, options = {}) {
|
|
489
|
+
const rootDir = skill?.rootDir
|
|
490
|
+
const input = normalizeString(resourcePath)
|
|
491
|
+
if (!rootDir || !input) {
|
|
492
|
+
const error = new Error('skill name and resource path are required')
|
|
493
|
+
error.statusCode = 400
|
|
494
|
+
throw error
|
|
495
|
+
}
|
|
496
|
+
if (path.isAbsolute(input)) {
|
|
497
|
+
const error = new Error('resource path must be relative to the skill directory')
|
|
498
|
+
error.statusCode = 400
|
|
499
|
+
throw error
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const file = path.resolve(rootDir, input)
|
|
503
|
+
if (!isPathInside(rootDir, file)) {
|
|
504
|
+
const error = new Error(`resource path is outside the skill directory: ${input}`)
|
|
505
|
+
error.statusCode = 403
|
|
506
|
+
throw error
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const stat = await fs.stat(file).catch(() => null)
|
|
510
|
+
if (!stat || !stat.isFile()) {
|
|
511
|
+
const error = new Error(`skill resource not found: ${input}`)
|
|
512
|
+
error.statusCode = 404
|
|
513
|
+
throw error
|
|
514
|
+
}
|
|
515
|
+
if (stat.size > 1024 * 1024) {
|
|
516
|
+
const error = new Error(`skill resource is too large to read as text: ${input}`)
|
|
517
|
+
error.statusCode = 413
|
|
518
|
+
throw error
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const text = await fs.readFile(file, 'utf8')
|
|
522
|
+
const lines = text.split(/\r?\n/)
|
|
523
|
+
const offset = Math.max(1, Number(options.offset || 1))
|
|
524
|
+
const limit = Math.min(2000, Math.max(1, Number(options.limit || 200)))
|
|
525
|
+
const selected = lines.slice(offset - 1, offset - 1 + limit)
|
|
526
|
+
const content = selected.map((line, index) => `${offset + index}: ${line}`).join('\n')
|
|
527
|
+
const suffix = offset - 1 + limit < lines.length ? `\n\n[showing ${selected.length} of ${lines.length} lines]` : ''
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
content: `${content}${suffix}`,
|
|
531
|
+
details: {
|
|
532
|
+
skill: skill.name,
|
|
533
|
+
path: path.relative(rootDir, file).replace(/\\/g, '/'),
|
|
534
|
+
totalLines: lines.length,
|
|
535
|
+
offset,
|
|
536
|
+
limit,
|
|
537
|
+
},
|
|
538
|
+
}
|
|
539
|
+
}
|