@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
package/server/storage.mjs
CHANGED
|
@@ -8,8 +8,17 @@ export const stores = new Set([
|
|
|
8
8
|
'custom-providers',
|
|
9
9
|
'sessions',
|
|
10
10
|
'sessions-metadata',
|
|
11
|
+
'scheduled-tasks',
|
|
11
12
|
])
|
|
12
13
|
|
|
14
|
+
// --- In-memory session bucket index ---
|
|
15
|
+
// Avoids O(n) directory scanning in findSessionBucket() by caching
|
|
16
|
+
// sessionId → { scope, projectId } lookups. Populated lazily on first
|
|
17
|
+
// lookup and kept up-to-date by write/delete paths.
|
|
18
|
+
/** @type {Map<string, { scope: string, projectId?: string }>} */
|
|
19
|
+
const sessionBucketIndex = new Map()
|
|
20
|
+
let bucketIndexBuilt = false
|
|
21
|
+
|
|
13
22
|
const configStores = new Set(['settings', 'provider-keys', 'custom-providers'])
|
|
14
23
|
const sessionStores = new Set(['sessions', 'sessions-metadata'])
|
|
15
24
|
|
|
@@ -73,6 +82,7 @@ function legacyNestedProjectConfigFile() {
|
|
|
73
82
|
function defaultProjectConfig() {
|
|
74
83
|
return {
|
|
75
84
|
activeProjectId: null,
|
|
85
|
+
globalSkills: [],
|
|
76
86
|
projects: [],
|
|
77
87
|
}
|
|
78
88
|
}
|
|
@@ -94,11 +104,33 @@ function defaultConfig() {
|
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
function normalizeStringArray(value) {
|
|
108
|
+
if (!Array.isArray(value)) return []
|
|
109
|
+
const result = []
|
|
110
|
+
const seen = new Set()
|
|
111
|
+
for (const item of value) {
|
|
112
|
+
if (typeof item !== 'string') continue
|
|
113
|
+
const text = item.trim()
|
|
114
|
+
if (!text || seen.has(text)) continue
|
|
115
|
+
seen.add(text)
|
|
116
|
+
result.push(text)
|
|
117
|
+
}
|
|
118
|
+
return result
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
function normalizeProjectConfig(value) {
|
|
98
|
-
|
|
122
|
+
const base = defaultProjectConfig()
|
|
123
|
+
if (!value || typeof value !== 'object') return base
|
|
124
|
+
const projects = Array.isArray(value.projects)
|
|
125
|
+
? value.projects.map((project) => ({
|
|
126
|
+
...project,
|
|
127
|
+
skills: normalizeStringArray(project?.skills),
|
|
128
|
+
}))
|
|
129
|
+
: base.projects
|
|
99
130
|
return {
|
|
100
|
-
activeProjectId: typeof value.activeProjectId === 'string' ? value.activeProjectId :
|
|
101
|
-
|
|
131
|
+
activeProjectId: typeof value.activeProjectId === 'string' ? value.activeProjectId : base.activeProjectId,
|
|
132
|
+
globalSkills: normalizeStringArray(value.globalSkills),
|
|
133
|
+
projects,
|
|
102
134
|
}
|
|
103
135
|
}
|
|
104
136
|
|
|
@@ -169,10 +201,24 @@ function sessionStoreFile(storeName, bucket) {
|
|
|
169
201
|
return path.join(storageDir, 'conversations', 'global', `${storeName}.json`)
|
|
170
202
|
}
|
|
171
203
|
|
|
204
|
+
function sessionDataDir(bucket) {
|
|
205
|
+
if (bucket.scope === 'project') {
|
|
206
|
+
assertSafePathSegment(bucket.projectId)
|
|
207
|
+
return path.join(storageDir, 'conversations', 'projects', bucket.projectId, 'sessions')
|
|
208
|
+
}
|
|
209
|
+
return path.join(storageDir, 'conversations', 'global', 'sessions')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sessionDataFile(sessionId, bucket) {
|
|
213
|
+
assertSafePathSegment(sessionId)
|
|
214
|
+
return path.join(sessionDataDir(bucket), `${sessionId}.json`)
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
export async function ensureProjectCache(projectId) {
|
|
173
218
|
const safeProjectId = String(projectId || '')
|
|
174
219
|
assertSafePathSegment(safeProjectId)
|
|
175
220
|
const projectCacheDir = path.join(cacheDir, 'projects', safeProjectId)
|
|
221
|
+
const projectStorageDir = path.join(storageDir, 'conversations', 'projects', safeProjectId)
|
|
176
222
|
|
|
177
223
|
await Promise.all([
|
|
178
224
|
fs.mkdir(path.join(projectCacheDir, 'workspace', 'file-index'), { recursive: true }),
|
|
@@ -181,6 +227,8 @@ export async function ensureProjectCache(projectId) {
|
|
|
181
227
|
fs.mkdir(path.join(projectCacheDir, 'llm', 'reasoning'), { recursive: true }),
|
|
182
228
|
fs.mkdir(path.join(projectCacheDir, 'assets'), { recursive: true }),
|
|
183
229
|
fs.mkdir(path.join(projectCacheDir, 'tmp'), { recursive: true }),
|
|
230
|
+
fs.mkdir(path.join(projectStorageDir, 'sessions'), { recursive: true }),
|
|
231
|
+
ensureJsonFile(path.join(projectStorageDir, 'sessions-metadata.json')),
|
|
184
232
|
])
|
|
185
233
|
|
|
186
234
|
return projectCacheDir
|
|
@@ -194,7 +242,8 @@ async function ensureJsonFile(file, defaultValue = {}) {
|
|
|
194
242
|
async function readJsonFile(file, defaultValue = {}) {
|
|
195
243
|
try {
|
|
196
244
|
const text = await fs.readFile(file, 'utf8')
|
|
197
|
-
|
|
245
|
+
const json = text.trimStart()
|
|
246
|
+
return json ? JSON.parse(json) : defaultValue
|
|
198
247
|
} catch (error) {
|
|
199
248
|
if (error?.code === 'ENOENT') return defaultValue
|
|
200
249
|
throw error
|
|
@@ -233,6 +282,132 @@ async function listProjectSessionFiles(storeName) {
|
|
|
233
282
|
.map((entry) => path.join(projectsDir, entry.name, `${storeName}.json`))
|
|
234
283
|
}
|
|
235
284
|
|
|
285
|
+
async function listProjectIds() {
|
|
286
|
+
const projectsDir = path.join(storageDir, 'conversations', 'projects')
|
|
287
|
+
let entries = []
|
|
288
|
+
try {
|
|
289
|
+
entries = await fs.readdir(projectsDir, { withFileTypes: true })
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error?.code !== 'ENOENT') throw error
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function listSessionDataFiles(bucket) {
|
|
298
|
+
const dir = sessionDataDir(bucket)
|
|
299
|
+
let entries = []
|
|
300
|
+
try {
|
|
301
|
+
entries = await fs.readdir(dir, { withFileTypes: true })
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error?.code !== 'ENOENT') throw error
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return entries
|
|
307
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
308
|
+
.map((entry) => path.join(dir, entry.name))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function readSessionValuesScoped(scope, projectId) {
|
|
312
|
+
const bucket = scope === 'project' ? { scope: 'project', projectId } : { scope: 'global' }
|
|
313
|
+
const files = await listSessionDataFiles(bucket)
|
|
314
|
+
const result = {}
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
const value = await readJsonFile(file, null)
|
|
317
|
+
if (value?.id) result[value.id] = value
|
|
318
|
+
}
|
|
319
|
+
return result
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function readAllSessionValues() {
|
|
323
|
+
const result = await readSessionValuesScoped('global')
|
|
324
|
+
for (const projectId of await listProjectIds()) {
|
|
325
|
+
Object.assign(result, await readSessionValuesScoped('project', projectId))
|
|
326
|
+
}
|
|
327
|
+
return result
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function writeSessionValueFile(sessionId, value) {
|
|
331
|
+
await writeJsonAtomic(sessionDataFile(sessionId, sessionBucket(value)), value)
|
|
332
|
+
// Keep in-memory index current
|
|
333
|
+
if (value) sessionBucketIndex.set(sessionId, sessionBucket(value))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function writeSessionValues(data) {
|
|
337
|
+
const nextIds = new Set(Object.keys(data || {}))
|
|
338
|
+
const existingFiles = [
|
|
339
|
+
...(await listSessionDataFiles({ scope: 'global' })),
|
|
340
|
+
...(await Promise.all(
|
|
341
|
+
(await listProjectIds()).map((projectId) => listSessionDataFiles({ scope: 'project', projectId })),
|
|
342
|
+
)).flat(),
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
await Promise.all(
|
|
346
|
+
existingFiles.map(async (file) => {
|
|
347
|
+
const sessionId = path.basename(file, '.json')
|
|
348
|
+
if (!nextIds.has(sessionId)) {
|
|
349
|
+
await fs.rm(file, { force: true })
|
|
350
|
+
sessionBucketIndex.delete(sessionId)
|
|
351
|
+
}
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
await Promise.all(
|
|
356
|
+
Object.entries(data || {}).map(([sessionId, value]) => writeSessionValueFile(sessionId, value)),
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function rebuildBucketIndex() {
|
|
361
|
+
sessionBucketIndex.clear()
|
|
362
|
+
// Global bucket
|
|
363
|
+
try {
|
|
364
|
+
const globalMeta = await readJsonFile(sessionStoreFile('sessions-metadata', { scope: 'global' }), {})
|
|
365
|
+
for (const [id, meta] of Object.entries(globalMeta)) {
|
|
366
|
+
if (meta && typeof meta === 'object') sessionBucketIndex.set(id, sessionBucket(meta))
|
|
367
|
+
}
|
|
368
|
+
} catch { /* ignore */ }
|
|
369
|
+
// Project buckets
|
|
370
|
+
for (const projectId of await listProjectIds()) {
|
|
371
|
+
try {
|
|
372
|
+
const meta = await readJsonFile(sessionStoreFile('sessions-metadata', { scope: 'project', projectId }), {})
|
|
373
|
+
for (const [id, entry] of Object.entries(meta)) {
|
|
374
|
+
if (entry && typeof entry === 'object') sessionBucketIndex.set(id, sessionBucket(entry))
|
|
375
|
+
}
|
|
376
|
+
} catch { /* ignore */ }
|
|
377
|
+
}
|
|
378
|
+
bucketIndexBuilt = true
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function findSessionBucket(sessionId) {
|
|
382
|
+
if (!bucketIndexBuilt) {
|
|
383
|
+
await ensureStorage()
|
|
384
|
+
await rebuildBucketIndex()
|
|
385
|
+
}
|
|
386
|
+
return sessionBucketIndex.get(sessionId) ?? null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function readSessionValue(sessionId) {
|
|
390
|
+
const bucket = await findSessionBucket(sessionId)
|
|
391
|
+
if (!bucket) return null
|
|
392
|
+
return readJsonFile(sessionDataFile(sessionId, bucket), null)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function writeSessionValue(sessionId, value) {
|
|
396
|
+
return enqueueWrite('sessions', async () => {
|
|
397
|
+
await ensureStorage()
|
|
398
|
+
await writeSessionValueFile(sessionId, value)
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function deleteSessionValue(sessionId) {
|
|
403
|
+
return enqueueWrite('sessions', async () => {
|
|
404
|
+
const bucket = await findSessionBucket(sessionId)
|
|
405
|
+
if (!bucket) return
|
|
406
|
+
await fs.rm(sessionDataFile(sessionId, bucket), { force: true })
|
|
407
|
+
sessionBucketIndex.delete(sessionId)
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
236
411
|
async function listSessionStoreFiles(storeName) {
|
|
237
412
|
return [
|
|
238
413
|
sessionStoreFile(storeName, { scope: 'global' }),
|
|
@@ -241,6 +416,8 @@ async function listSessionStoreFiles(storeName) {
|
|
|
241
416
|
}
|
|
242
417
|
|
|
243
418
|
async function readSessionStore(storeName) {
|
|
419
|
+
if (storeName === 'sessions') return readAllSessionValues()
|
|
420
|
+
|
|
244
421
|
const files = await listSessionStoreFiles(storeName)
|
|
245
422
|
const result = {}
|
|
246
423
|
for (const file of files) {
|
|
@@ -249,7 +426,20 @@ async function readSessionStore(storeName) {
|
|
|
249
426
|
return result
|
|
250
427
|
}
|
|
251
428
|
|
|
429
|
+
export async function readSessionStoreScoped(storeName, scope, projectId) {
|
|
430
|
+
await ensureStorage()
|
|
431
|
+
if (storeName === 'sessions') return readSessionValuesScoped(scope, projectId)
|
|
432
|
+
|
|
433
|
+
const file = sessionStoreFile(storeName, { scope, projectId })
|
|
434
|
+
return readJsonFile(file, {})
|
|
435
|
+
}
|
|
436
|
+
|
|
252
437
|
async function writeSessionStore(storeName, data) {
|
|
438
|
+
if (storeName === 'sessions') {
|
|
439
|
+
await writeSessionValues(data)
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
|
|
253
443
|
const buckets = new Map()
|
|
254
444
|
|
|
255
445
|
for (const [key, value] of Object.entries(data || {})) {
|
|
@@ -270,6 +460,13 @@ async function writeSessionStore(storeName, data) {
|
|
|
270
460
|
await writeJsonAtomic(file, bucketEntry?.data ?? {})
|
|
271
461
|
}),
|
|
272
462
|
)
|
|
463
|
+
|
|
464
|
+
// Keep in-memory bucket index current for metadata writes
|
|
465
|
+
if (storeName === 'sessions-metadata') {
|
|
466
|
+
for (const [sessionId, meta] of Object.entries(data || {})) {
|
|
467
|
+
if (meta && typeof meta === 'object') sessionBucketIndex.set(sessionId, sessionBucket(meta))
|
|
468
|
+
}
|
|
469
|
+
}
|
|
273
470
|
}
|
|
274
471
|
|
|
275
472
|
async function migrateLegacySessionStore(storeName) {
|
|
@@ -318,7 +515,6 @@ async function migrateUnifiedConfig() {
|
|
|
318
515
|
await writeConfigFile(current)
|
|
319
516
|
|
|
320
517
|
if (!existsSync(legacyStorageMigrationMarkerFile)) {
|
|
321
|
-
await migrateLegacySessionStore('sessions')
|
|
322
518
|
await migrateLegacySessionStore('sessions-metadata')
|
|
323
519
|
await fs.mkdir(path.dirname(legacyStorageMigrationMarkerFile), { recursive: true })
|
|
324
520
|
await fs.writeFile(legacyStorageMigrationMarkerFile, `${new Date().toISOString()}\n`, 'utf8')
|
|
@@ -339,6 +535,50 @@ function enqueueWrite(queueName, operation) {
|
|
|
339
535
|
return next
|
|
340
536
|
}
|
|
341
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Atomically read-modify-write a store within its serialized write queue.
|
|
540
|
+
* Eliminates the race condition where concurrent read-modify-write operations
|
|
541
|
+
* from multiple browser tabs would overwrite each other.
|
|
542
|
+
*
|
|
543
|
+
* @param {string} storeName
|
|
544
|
+
* @param {(data: object) => object} updateFn — receives current data, returns updated data
|
|
545
|
+
* @returns {Promise<object>} the updated data
|
|
546
|
+
*/
|
|
547
|
+
export async function atomicUpdate(storeName, updateFn) {
|
|
548
|
+
assertStore(storeName)
|
|
549
|
+
const queueName = configStores.has(storeName) ? 'config' : storeName
|
|
550
|
+
return enqueueWrite(queueName, async () => {
|
|
551
|
+
await ensureStorage()
|
|
552
|
+
if (configStores.has(storeName)) {
|
|
553
|
+
const config = await readConfigFile()
|
|
554
|
+
const data = configSection(config, storeName)
|
|
555
|
+
const updated = updateFn(data)
|
|
556
|
+
setConfigSection(config, storeName, updated)
|
|
557
|
+
await writeConfigFile(config)
|
|
558
|
+
return updated
|
|
559
|
+
}
|
|
560
|
+
const data = await readSessionStore(storeName)
|
|
561
|
+
const updated = updateFn(data)
|
|
562
|
+
await writeSessionStore(storeName, updated)
|
|
563
|
+
return updated
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Atomically read-modify-write the project config within the config queue.
|
|
569
|
+
*/
|
|
570
|
+
export async function atomicProjectConfigUpdate(updateFn) {
|
|
571
|
+
return enqueueWrite('config', async () => {
|
|
572
|
+
await ensureStorage()
|
|
573
|
+
const config = await readConfigFile()
|
|
574
|
+
const projectConfig = normalizeProjectConfig(config.projects)
|
|
575
|
+
const updated = updateFn(projectConfig)
|
|
576
|
+
config.projects = normalizeProjectConfig(updated)
|
|
577
|
+
await writeConfigFile(config)
|
|
578
|
+
return updated
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
342
582
|
export async function ensureStorage() {
|
|
343
583
|
await fs.mkdir(configDir, { recursive: true })
|
|
344
584
|
await fs.mkdir(storageDir, { recursive: true })
|
|
@@ -348,6 +588,7 @@ export async function ensureStorage() {
|
|
|
348
588
|
fs.mkdir(path.join(cacheDir, 'global', 'llm'), { recursive: true }),
|
|
349
589
|
fs.mkdir(path.join(cacheDir, 'global', 'tmp'), { recursive: true }),
|
|
350
590
|
fs.mkdir(path.join(cacheDir, 'projects'), { recursive: true }),
|
|
591
|
+
fs.mkdir(path.join(storageDir, 'conversations', 'global', 'sessions'), { recursive: true }),
|
|
351
592
|
fs.mkdir(path.join(storageDir, 'conversations', 'projects'), { recursive: true }),
|
|
352
593
|
])
|
|
353
594
|
|
|
@@ -355,7 +596,7 @@ export async function ensureStorage() {
|
|
|
355
596
|
|
|
356
597
|
await Promise.all([
|
|
357
598
|
ensureJsonFile(quickForgeConfigFile, defaultConfig()),
|
|
358
|
-
|
|
599
|
+
ensureJsonFile(sessionStoreFile('sessions-metadata', { scope: 'global' })),
|
|
359
600
|
])
|
|
360
601
|
}
|
|
361
602
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const BASE_SYSTEM_PROMPT = `You are a pragmatic coding assistant.
|
|
2
|
+
|
|
3
|
+
For project tasks:
|
|
4
|
+
- Inspect the workspace before changing files.
|
|
5
|
+
- Make minimal, focused changes.
|
|
6
|
+
- Prefer dedicated workspace tools for reading, editing, and searching files.
|
|
7
|
+
- If dedicated tools are unavailable or insufficient, use the shell/command tool.
|
|
8
|
+
- Use Python through the shell for reliable scripting, data processing, or file transformations.
|
|
9
|
+
- Stay within the current workspace unless the user explicitly asks otherwise.
|
|
10
|
+
- Verify changes with relevant tests, build, lint, or targeted checks.
|
|
11
|
+
- If no suitable tool is available, say so clearly.`
|
|
12
|
+
|
|
13
|
+
function escapeXml(value) {
|
|
14
|
+
return String(value ?? '')
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatSkillCatalogItem(skill) {
|
|
21
|
+
const details = [
|
|
22
|
+
` <name>${escapeXml(skill.name)}</name>`,
|
|
23
|
+
` <description>${escapeXml(skill.description)}</description>`,
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
if (skill.compatibility) details.push(` <compatibility>${escapeXml(skill.compatibility)}</compatibility>`)
|
|
27
|
+
if (skill.allowedTools) details.push(` <allowed_tools>${escapeXml(skill.allowedTools)}</allowed_tools>`)
|
|
28
|
+
|
|
29
|
+
return ` <skill>\n${details.join('\n')}\n </skill>`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function appendSkillsCatalog(parts, skills) {
|
|
33
|
+
if (!Array.isArray(skills) || skills.length === 0) return
|
|
34
|
+
|
|
35
|
+
const skillParts = skills.map(formatSkillCatalogItem)
|
|
36
|
+
parts.push(`
|
|
37
|
+
<available_skills>
|
|
38
|
+
The following Agent Skills provide specialized instructions for specific tasks. Use progressive disclosure: this catalog is available now, but full skill instructions are loaded only when needed.
|
|
39
|
+
|
|
40
|
+
When the user's task matches a skill description, call activate_skill with that skill's name before proceeding. If a loaded skill references bundled files under scripts/, references/, or assets/, call read_skill_resource with the skill name and the relative path. Do not assume resources are already loaded.
|
|
41
|
+
|
|
42
|
+
${skillParts.join('\n')}
|
|
43
|
+
</available_skills>`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function composeSystemPrompt(instructions = {}) {
|
|
47
|
+
const parts = [BASE_SYSTEM_PROMPT]
|
|
48
|
+
|
|
49
|
+
if (instructions.global) {
|
|
50
|
+
parts.push(`\n<user_instructions>\n${instructions.global}\n</user_instructions>`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (instructions.project) {
|
|
54
|
+
parts.push(`\n<project_instructions>\n${instructions.project}\n</project_instructions>`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const skills = Array.isArray(instructions.skills)
|
|
58
|
+
? instructions.skills
|
|
59
|
+
: [
|
|
60
|
+
...(Array.isArray(instructions.globalSkills) ? instructions.globalSkills : []),
|
|
61
|
+
...(Array.isArray(instructions.projectSkills) ? instructions.projectSkills : []),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
appendSkillsCatalog(parts, skills)
|
|
65
|
+
|
|
66
|
+
return parts.join('\n')
|
|
67
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Type } from 'typebox'
|
|
2
|
+
import { loadSelectedGlobalSkills, loadSelectedProjectSkills, mergeSkills } from '../skills.mjs'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Canonical workspace tool definitions.
|
|
6
|
+
// These are the single source of truth for tool metadata (name, label,
|
|
7
|
+
// description, parameters). Both the server agent-manager (which wraps them
|
|
8
|
+
// with execute handlers) and the GET /api/tools endpoint (which returns them
|
|
9
|
+
// as JSON) import from here.
|
|
10
|
+
//
|
|
11
|
+
// When adding a new tool, add its definition here. The agent-manager connects
|
|
12
|
+
// it to a handler, and the frontend can fetch definitions from /api/tools.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export const workspaceTools = [
|
|
16
|
+
{
|
|
17
|
+
name: 'get_project_info',
|
|
18
|
+
label: 'Project info',
|
|
19
|
+
description: 'Get the project directory bound to this chat.',
|
|
20
|
+
parameters: Type.Object({}),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'list_dir',
|
|
24
|
+
label: 'List directory',
|
|
25
|
+
description: 'List files and folders inside the project bound to this chat. Paths are relative to that project root.',
|
|
26
|
+
parameters: Type.Object({
|
|
27
|
+
path: Type.Optional(Type.String({ description: 'Directory path relative to the workspace root. Defaults to .', default: '.' })),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'read_file',
|
|
32
|
+
label: 'Read file',
|
|
33
|
+
description: 'Read a UTF-8 text file inside the project bound to this chat. Use offset and limit for large files.',
|
|
34
|
+
parameters: Type.Object({
|
|
35
|
+
path: Type.String({ description: 'File path relative to the workspace root.' }),
|
|
36
|
+
offset: Type.Optional(Type.Number({ description: '1-based line offset.', default: 1 })),
|
|
37
|
+
limit: Type.Optional(Type.Number({ description: 'Maximum number of lines to return.', default: 200 })),
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'grep_files',
|
|
42
|
+
label: 'Search files',
|
|
43
|
+
description: 'Search text in the project files bound to this chat. Returns matching file paths and line numbers.',
|
|
44
|
+
parameters: Type.Object({
|
|
45
|
+
query: Type.String({ description: 'Plain text or regular expression to search for.' }),
|
|
46
|
+
path: Type.Optional(Type.String({ description: 'Directory path relative to the workspace root. Defaults to .', default: '.' })),
|
|
47
|
+
regex: Type.Optional(Type.Boolean({ description: 'Treat query as a regular expression.', default: false })),
|
|
48
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: 'Use case-sensitive matching.', default: false })),
|
|
49
|
+
limit: Type.Optional(Type.Number({ description: 'Maximum matches to return.', default: 200 })),
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'write_file',
|
|
54
|
+
label: 'Write file',
|
|
55
|
+
description: 'Create or overwrite a UTF-8 text file inside the project bound to this chat.',
|
|
56
|
+
parameters: Type.Object({
|
|
57
|
+
path: Type.String({ description: 'File path relative to the workspace root.' }),
|
|
58
|
+
content: Type.String({ description: 'Complete file content to write.' }),
|
|
59
|
+
}),
|
|
60
|
+
executionMode: 'sequential',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'edit_file',
|
|
64
|
+
label: 'Edit file',
|
|
65
|
+
description: 'Edit a text file in the project bound to this chat by replacing exact text. oldText must match exactly once.',
|
|
66
|
+
parameters: Type.Object({
|
|
67
|
+
path: Type.String({ description: 'File path relative to the workspace root.' }),
|
|
68
|
+
oldText: Type.String({ description: 'Exact existing text to replace. Must be unique in the file.' }),
|
|
69
|
+
newText: Type.String({ description: 'Replacement text.' }),
|
|
70
|
+
}),
|
|
71
|
+
executionMode: 'sequential',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'run_command',
|
|
75
|
+
label: 'Run command',
|
|
76
|
+
description: 'Run a shell command in the project bound to this chat. Use this for lint, build, tests, git status, and diagnostics.',
|
|
77
|
+
parameters: Type.Object({
|
|
78
|
+
command: Type.String({ description: 'Command to execute in the workspace.' }),
|
|
79
|
+
timeoutSeconds: Type.Optional(Type.Number({ description: 'Timeout in seconds. Defaults to 60.', default: 60 })),
|
|
80
|
+
}),
|
|
81
|
+
executionMode: 'sequential',
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
function activeSkillSchema(skills) {
|
|
86
|
+
const names = skills.map((skill) => skill.name).filter(Boolean)
|
|
87
|
+
return names.length ? Type.String({ enum: names }) : Type.String()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function createSkillTools(config = {}) {
|
|
91
|
+
const globalSkills = await loadSelectedGlobalSkills(config.globalSkillNames)
|
|
92
|
+
const projectSkills = config.workspaceRoot
|
|
93
|
+
? await loadSelectedProjectSkills(config.projectSkillNames, config.workspaceRoot)
|
|
94
|
+
: []
|
|
95
|
+
const skills = mergeSkills(globalSkills, projectSkills)
|
|
96
|
+
if (skills.length === 0) return []
|
|
97
|
+
|
|
98
|
+
const skillNameSchema = activeSkillSchema(skills)
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
name: 'activate_skill',
|
|
102
|
+
label: 'Activate skill',
|
|
103
|
+
description: 'Load the full instructions for an enabled Agent Skill when the current task matches its description.',
|
|
104
|
+
parameters: Type.Object({
|
|
105
|
+
name: skillNameSchema,
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'read_skill_resource',
|
|
110
|
+
label: 'Read skill resource',
|
|
111
|
+
description: 'Read a text resource bundled with an activated Agent Skill. Paths are relative to that skill directory.',
|
|
112
|
+
parameters: Type.Object({
|
|
113
|
+
skill: skillNameSchema,
|
|
114
|
+
path: Type.String({ description: 'Relative path inside the skill directory, for example references/REFERENCE.md or scripts/helper.py.' }),
|
|
115
|
+
offset: Type.Optional(Type.Number({ description: '1-based line offset.', default: 1 })),
|
|
116
|
+
limit: Type.Optional(Type.Number({ description: 'Maximum number of lines to return.', default: 200 })),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
]
|
|
120
|
+
}
|