@shawnstack/quickforge 1.3.25 → 1.3.26
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 +20 -14
- package/dist/assets/{anthropic-B1_Yrokl.js → anthropic-BcnDL7hi.js} +1 -1
- package/dist/assets/{azure-openai-responses-UMiOBCBd.js → azure-openai-responses-BEfdv0qd.js} +1 -1
- package/dist/assets/{google-BLE_Gcd1.js → google-C2y985rW.js} +1 -1
- package/dist/assets/{google-vertex-6_sIZLVc.js → google-vertex-Jf9zNsCF.js} +1 -1
- package/dist/assets/{icons-Bs7OG8yi.js → icons-BVM5--R9.js} +1 -1
- package/dist/assets/{index-C3bc5C3k.js → index-8Q1Ovled.js} +595 -547
- package/dist/assets/index-ZYbEKGUp.css +3 -0
- package/dist/assets/{mistral-DmZEmRkv.js → mistral-qYbgRY3z.js} +1 -1
- package/dist/assets/{openai-codex-responses-i_SmQGzQ.js → openai-codex-responses--aAgyYJM.js} +1 -1
- package/dist/assets/{openai-completions-BmmZFDDY.js → openai-completions-CHDluyXM.js} +1 -1
- package/dist/assets/{openai-responses-C8tPdeE9.js → openai-responses-UtRriBXu.js} +1 -1
- package/dist/assets/{openai-responses-shared-DchtjQNp.js → openai-responses-shared-G6WDDqJ8.js} +1 -1
- package/dist/assets/{openrouter-CcTv1G_v.js → openrouter-Dz9zwzUG.js} +1 -1
- package/dist/assets/{react-vendor-Cu-7p9CI.js → react-vendor-DAoL5p8_.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +2 -1
- package/server/agent-manager.mjs +102 -22
- package/server/approval-store.mjs +1 -1
- package/server/custom-commands.mjs +67 -9
- package/server/index.mjs +7 -0
- package/server/plugins/loader.mjs +56 -0
- package/server/plugins/manifest.mjs +174 -0
- package/server/plugins/registry.mjs +304 -0
- package/server/project-config.mjs +53 -4
- package/server/routes/agent.mjs +1 -16
- package/server/routes/plugins.mjs +63 -0
- package/server/routes/project.mjs +2 -0
- package/server/routes/tools.mjs +12 -1
- package/server/skills.mjs +64 -5
- package/server/storage.mjs +13 -6
- package/server/system-prompt.mjs +27 -5
- package/server/tool-wiring.mjs +26 -0
- package/dist/assets/index-C7oT9Rdw.css +0 -3
package/server/routes/tools.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
|
2
2
|
import { readStore } from '../storage.mjs'
|
|
3
3
|
import { toolHandlers, loadSkillToolContext } from '../tools/index.mjs'
|
|
4
4
|
import { createSkillTools, workspaceTools } from '../tools/definitions.mjs'
|
|
5
|
+
import { createMcpToolDefinitions } from '../mcp/registry.mjs'
|
|
6
|
+
import { callPluginTool, createPluginToolDefinitions, isPluginToolName } from '../plugins/registry.mjs'
|
|
5
7
|
import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
|
|
6
8
|
|
|
7
9
|
const directRouteDisabledTools = new Set(['run_subagent'])
|
|
@@ -17,7 +19,9 @@ export async function handleGetTools(_req, res) {
|
|
|
17
19
|
projectSkillNames: activeProject?.skills,
|
|
18
20
|
workspaceRoot: activeProject?.path,
|
|
19
21
|
})
|
|
20
|
-
|
|
22
|
+
const pluginTools = await createPluginToolDefinitions(activeProject ? { workspaceRoot: activeProject.path, project: activeProject } : null)
|
|
23
|
+
const mcpTools = await createMcpToolDefinitions()
|
|
24
|
+
sendJson(res, 200, { tools: [...skillTools, ...workspaceTools, ...mcpTools, ...pluginTools] })
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
const workspaceToolNames = new Set(workspaceTools.map((tool) => tool.name))
|
|
@@ -69,6 +73,13 @@ export async function handleToolApi(req, res, url) {
|
|
|
69
73
|
name = decodeSegment(parts[4])
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
if (isPluginToolName(name)) {
|
|
77
|
+
const params = await readJsonBody(req)
|
|
78
|
+
const result = await callPluginTool(name, params || {}, context)
|
|
79
|
+
sendJson(res, 200, result)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
const handler = toolHandlers[name]
|
|
73
84
|
if (!handler || directRouteDisabledTools.has(name)) {
|
|
74
85
|
const error = new Error(`Unknown tool: ${name}`)
|
package/server/skills.mjs
CHANGED
|
@@ -2,9 +2,12 @@ import { existsSync, promises as fs } from 'node:fs'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { dataDir } from './storage.mjs'
|
|
5
|
+
import { getEnabledPluginSkillSources } from './plugins/registry.mjs'
|
|
5
6
|
|
|
6
7
|
const userSkillsDir = path.join(dataDir, 'skills')
|
|
7
8
|
const sharedUserSkillsDir = path.join(os.homedir(), '.agents', 'skills')
|
|
9
|
+
const claudeUserSkillsDir = path.join(os.homedir(), '.claude', 'skills')
|
|
10
|
+
const opencodeUserSkillsDir = path.join(os.homedir(), '.opencode', 'skills')
|
|
8
11
|
const defaultEntry = 'SKILL.md'
|
|
9
12
|
const resourceDirs = ['scripts', 'references', 'assets']
|
|
10
13
|
const maxResourceFiles = 200
|
|
@@ -312,6 +315,14 @@ function projectSharedSkillsDir(workspaceRoot) {
|
|
|
312
315
|
return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.agents', 'skills') : ''
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
function projectClaudeSkillsDir(workspaceRoot) {
|
|
319
|
+
return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.claude', 'skills') : ''
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function projectOpencodeSkillsDir(workspaceRoot) {
|
|
323
|
+
return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.opencode', 'skills') : ''
|
|
324
|
+
}
|
|
325
|
+
|
|
315
326
|
async function loadSkillsFromSources(sources) {
|
|
316
327
|
const skillsByName = new Map()
|
|
317
328
|
|
|
@@ -335,6 +346,30 @@ async function loadSkillsFromSources(sources) {
|
|
|
335
346
|
})
|
|
336
347
|
}
|
|
337
348
|
|
|
349
|
+
async function loadSkillsFromExplicitSources(sources) {
|
|
350
|
+
const skillsByName = new Map()
|
|
351
|
+
|
|
352
|
+
for (const source of sources) {
|
|
353
|
+
const candidateDirs = [source.dir, ...(await listSkillDirectories(source.dir))]
|
|
354
|
+
for (const skillDir of candidateDirs) {
|
|
355
|
+
try {
|
|
356
|
+
const skill = await loadSkillDirectory(skillDir, source.name)
|
|
357
|
+
if (!skill) continue
|
|
358
|
+
if (skillsByName.has(skill.name)) skillsByName.delete(skill.name)
|
|
359
|
+
skillsByName.set(skill.name, skill)
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.warn(`Failed to load skill from ${skillDir}:`, error.message || error)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return [...skillsByName.values()].sort((a, b) => {
|
|
367
|
+
const left = (a.displayName || a.name).toLowerCase()
|
|
368
|
+
const right = (b.displayName || b.name).toLowerCase()
|
|
369
|
+
return left.localeCompare(right)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
338
373
|
function searchDirsForList(value) {
|
|
339
374
|
return value.length === 1 ? value[0] : value.slice()
|
|
340
375
|
}
|
|
@@ -372,27 +407,49 @@ export function mergeSkills(...skillLists) {
|
|
|
372
407
|
}
|
|
373
408
|
|
|
374
409
|
export const skillSearchPaths = {
|
|
375
|
-
global: searchDirsForList([sharedUserSkillsDir, userSkillsDir]),
|
|
376
|
-
project: ['<project>/.agents/skills', '<project>/.quickforge/skills'],
|
|
410
|
+
global: searchDirsForList([claudeUserSkillsDir, opencodeUserSkillsDir, sharedUserSkillsDir, userSkillsDir]),
|
|
411
|
+
project: ['<project>/.claude/skills', '<project>/.opencode/skills', '<project>/.agents/skills', '<project>/.quickforge/skills'],
|
|
377
412
|
}
|
|
378
413
|
|
|
379
414
|
export function projectSkillSearchPaths(workspaceRoot) {
|
|
380
415
|
if (!workspaceRoot) return skillSearchPaths.project.slice()
|
|
381
|
-
return searchDirsForList([
|
|
416
|
+
return searchDirsForList([
|
|
417
|
+
projectClaudeSkillsDir(workspaceRoot),
|
|
418
|
+
projectOpencodeSkillsDir(workspaceRoot),
|
|
419
|
+
projectSharedSkillsDir(workspaceRoot),
|
|
420
|
+
projectClientSkillsDir(workspaceRoot),
|
|
421
|
+
'<enabled-plugin>/skills',
|
|
422
|
+
])
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function loadPluginSkills(workspaceRoot) {
|
|
426
|
+
if (!workspaceRoot) return []
|
|
427
|
+
const sources = await getEnabledPluginSkillSources({ workspaceRoot })
|
|
428
|
+
return loadSkillsFromExplicitSources(sources.map((source) => ({ dir: source.dir, name: source.source })))
|
|
382
429
|
}
|
|
383
430
|
|
|
384
431
|
export async function loadGlobalSkills() {
|
|
385
432
|
return loadSkillsFromSources([
|
|
433
|
+
{ dir: claudeUserSkillsDir, name: 'user-claude' },
|
|
434
|
+
{ dir: opencodeUserSkillsDir, name: 'user-opencode' },
|
|
386
435
|
{ dir: sharedUserSkillsDir, name: 'user-shared' },
|
|
387
436
|
{ dir: userSkillsDir, name: 'user' },
|
|
388
437
|
])
|
|
389
438
|
}
|
|
390
439
|
|
|
391
440
|
export async function loadProjectSkills(workspaceRoot) {
|
|
392
|
-
|
|
441
|
+
const pluginSkills = await loadPluginSkills(workspaceRoot)
|
|
442
|
+
const projectSkills = await loadSkillsFromSources([
|
|
443
|
+
{ dir: projectClaudeSkillsDir(workspaceRoot), name: 'project-claude' },
|
|
444
|
+
{ dir: projectOpencodeSkillsDir(workspaceRoot), name: 'project-opencode' },
|
|
393
445
|
{ dir: projectSharedSkillsDir(workspaceRoot), name: 'project-shared' },
|
|
394
446
|
{ dir: projectClientSkillsDir(workspaceRoot), name: 'project' },
|
|
395
447
|
])
|
|
448
|
+
return mergeSkills(projectSkills, pluginSkills).sort((a, b) => {
|
|
449
|
+
const left = (a.displayName || a.name).toLowerCase()
|
|
450
|
+
const right = (b.displayName || b.name).toLowerCase()
|
|
451
|
+
return left.localeCompare(right)
|
|
452
|
+
})
|
|
396
453
|
}
|
|
397
454
|
|
|
398
455
|
export async function loadSkills() {
|
|
@@ -442,7 +499,9 @@ export async function loadSelectedGlobalSkills(skillNames) {
|
|
|
442
499
|
}
|
|
443
500
|
|
|
444
501
|
export async function loadSelectedProjectSkills(skillNames, workspaceRoot) {
|
|
445
|
-
|
|
502
|
+
const skills = await loadProjectSkills(workspaceRoot)
|
|
503
|
+
const pluginSkills = skills.filter((skill) => String(skill.source || '').startsWith('plugin:'))
|
|
504
|
+
return mergeSkills(pluginSkills, selectSkills(skillNames, skills))
|
|
446
505
|
}
|
|
447
506
|
|
|
448
507
|
export async function loadSelectedSkills(skillNames) {
|
package/server/storage.mjs
CHANGED
|
@@ -48,6 +48,7 @@ export const stores = new Set([
|
|
|
48
48
|
'settings',
|
|
49
49
|
'provider-keys',
|
|
50
50
|
'custom-providers',
|
|
51
|
+
'plugins',
|
|
51
52
|
'sessions',
|
|
52
53
|
'sessions-metadata',
|
|
53
54
|
'scheduled-tasks',
|
|
@@ -73,13 +74,14 @@ export function getStoreRevision(storeName) {
|
|
|
73
74
|
return storeRevisions.get(storeName) || 0
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const configStores = new Set(['settings', 'provider-keys', 'custom-providers'])
|
|
77
|
+
const configStores = new Set(['settings', 'provider-keys', 'custom-providers', 'plugins'])
|
|
77
78
|
const sessionStores = new Set(['sessions', 'sessions-metadata'])
|
|
78
79
|
|
|
79
80
|
const configStoreSections = {
|
|
80
81
|
settings: ['app', 'settings'],
|
|
81
82
|
'provider-keys': ['credentials', 'providerKeys'],
|
|
82
83
|
'custom-providers': ['providers', 'customProviders'],
|
|
84
|
+
plugins: ['extensions', 'plugins'],
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export function getDataDir() {
|
|
@@ -107,11 +109,6 @@ export function configFile() {
|
|
|
107
109
|
return quickForgeConfigFile
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
// Compatibility export for older modules/imports. Project config now lives inside config/config.json -> projects.
|
|
111
|
-
export function projectConfigFile() {
|
|
112
|
-
return quickForgeConfigFile
|
|
113
|
-
}
|
|
114
|
-
|
|
115
112
|
function legacyFlatStoreFile(storeName) {
|
|
116
113
|
return path.join(storageDir, `${storeName}.json`)
|
|
117
114
|
}
|
|
@@ -154,6 +151,9 @@ function defaultConfig() {
|
|
|
154
151
|
credentials: {
|
|
155
152
|
providerKeys: {},
|
|
156
153
|
},
|
|
154
|
+
extensions: {
|
|
155
|
+
plugins: {},
|
|
156
|
+
},
|
|
157
157
|
projects: defaultProjectConfig(),
|
|
158
158
|
}
|
|
159
159
|
}
|
|
@@ -217,6 +217,13 @@ function normalizeConfig(value) {
|
|
|
217
217
|
? input.credentials.providerKeys
|
|
218
218
|
: base.credentials.providerKeys,
|
|
219
219
|
},
|
|
220
|
+
extensions: {
|
|
221
|
+
...(input.extensions && typeof input.extensions === 'object' ? input.extensions : {}),
|
|
222
|
+
plugins:
|
|
223
|
+
input.extensions?.plugins && typeof input.extensions.plugins === 'object'
|
|
224
|
+
? input.extensions.plugins
|
|
225
|
+
: base.extensions.plugins,
|
|
226
|
+
},
|
|
220
227
|
projects: normalizeProjectConfig(input.projects),
|
|
221
228
|
}
|
|
222
229
|
}
|
package/server/system-prompt.mjs
CHANGED
|
@@ -27,6 +27,10 @@ function escapeXml(value) {
|
|
|
27
27
|
.replace(/>/g, '>')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function escapeAttribute(value) {
|
|
31
|
+
return escapeXml(value).replace(/"/g, '"')
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
function formatAllowedTools(value) {
|
|
31
35
|
if (Array.isArray(value)) return value.join(', ')
|
|
32
36
|
return value
|
|
@@ -83,6 +87,22 @@ ${skillParts.join('\n')}
|
|
|
83
87
|
</available_skills>`)
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
function appendInstructionSources(parts, tagName, sources, fallback) {
|
|
91
|
+
const instructionSources = Array.isArray(sources)
|
|
92
|
+
? sources.filter((source) => source?.content)
|
|
93
|
+
: []
|
|
94
|
+
|
|
95
|
+
if (instructionSources.length > 0) {
|
|
96
|
+
for (const source of instructionSources) {
|
|
97
|
+
const sourceAttribute = source.source ? ` source="${escapeAttribute(source.source)}"` : ''
|
|
98
|
+
parts.push(`\n<${tagName}${sourceAttribute}>\n${source.content}\n</${tagName}>`)
|
|
99
|
+
}
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (fallback) parts.push(`\n<${tagName}>\n${fallback}\n</${tagName}>`)
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
export function composeSystemPrompt(instructions = {}) {
|
|
87
107
|
const parts = [BASE_SYSTEM_PROMPT]
|
|
88
108
|
|
|
@@ -103,12 +123,14 @@ ${lines.join('\n')}
|
|
|
103
123
|
}
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
126
|
+
appendInstructionSources(parts, 'user_instructions', instructions.globalSources, instructions.global)
|
|
127
|
+
appendInstructionSources(parts, 'project_instructions', instructions.projectSources, instructions.project)
|
|
109
128
|
|
|
110
|
-
if (instructions.
|
|
111
|
-
parts.push(
|
|
129
|
+
if (instructions.globalSources?.length || instructions.projectSources?.length) {
|
|
130
|
+
parts.push(`
|
|
131
|
+
<instruction_precedence>
|
|
132
|
+
When instructions from different sources conflict, prefer project-specific instructions over global instructions, and prefer QuickForge project instructions over external compatibility instructions.
|
|
133
|
+
</instruction_precedence>`)
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
const skills = Array.isArray(instructions.skills)
|
package/server/tool-wiring.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { toolHandlers } from './tools/index.mjs'
|
|
9
9
|
import { callMcpTool } from './mcp/registry.mjs'
|
|
10
|
+
import { callPluginTool } from './plugins/registry.mjs'
|
|
10
11
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Helpers
|
|
@@ -75,6 +76,31 @@ export function wrapMcpToolDefinition(definition, toolPermissions) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
export function wrapPluginToolDefinition(definition, context, toolPermissions) {
|
|
80
|
+
return {
|
|
81
|
+
...definition,
|
|
82
|
+
execute: async (_toolCallId, params, signal, onUpdate) => {
|
|
83
|
+
if (toolPermissions) {
|
|
84
|
+
const permissionError = toolPermissions(definition.name)
|
|
85
|
+
if (permissionError) throw new Error(permissionError)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const startedAt = Date.now()
|
|
89
|
+
const startedAtPerf = performance.now()
|
|
90
|
+
const result = await callPluginTool(definition.name, params || {}, { ...context, signal, onUpdate, toolCallId: _toolCallId })
|
|
91
|
+
const finishedAt = Date.now()
|
|
92
|
+
const durationMs = Math.max(0, Math.round(performance.now() - startedAtPerf))
|
|
93
|
+
if (result.isError) {
|
|
94
|
+
throw new Error(result.content || `Plugin tool failed: ${definition.name}`)
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: result.content }],
|
|
98
|
+
details: mergeQuickForgeTiming(result.details, { startedAt, finishedAt, durationMs }),
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// ---------------------------------------------------------------------------
|
|
79
105
|
// Skill context
|
|
80
106
|
// ---------------------------------------------------------------------------
|