@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.
Files changed (34) hide show
  1. package/README.md +20 -14
  2. package/dist/assets/{anthropic-B1_Yrokl.js → anthropic-BcnDL7hi.js} +1 -1
  3. package/dist/assets/{azure-openai-responses-UMiOBCBd.js → azure-openai-responses-BEfdv0qd.js} +1 -1
  4. package/dist/assets/{google-BLE_Gcd1.js → google-C2y985rW.js} +1 -1
  5. package/dist/assets/{google-vertex-6_sIZLVc.js → google-vertex-Jf9zNsCF.js} +1 -1
  6. package/dist/assets/{icons-Bs7OG8yi.js → icons-BVM5--R9.js} +1 -1
  7. package/dist/assets/{index-C3bc5C3k.js → index-8Q1Ovled.js} +595 -547
  8. package/dist/assets/index-ZYbEKGUp.css +3 -0
  9. package/dist/assets/{mistral-DmZEmRkv.js → mistral-qYbgRY3z.js} +1 -1
  10. package/dist/assets/{openai-codex-responses-i_SmQGzQ.js → openai-codex-responses--aAgyYJM.js} +1 -1
  11. package/dist/assets/{openai-completions-BmmZFDDY.js → openai-completions-CHDluyXM.js} +1 -1
  12. package/dist/assets/{openai-responses-C8tPdeE9.js → openai-responses-UtRriBXu.js} +1 -1
  13. package/dist/assets/{openai-responses-shared-DchtjQNp.js → openai-responses-shared-G6WDDqJ8.js} +1 -1
  14. package/dist/assets/{openrouter-CcTv1G_v.js → openrouter-Dz9zwzUG.js} +1 -1
  15. package/dist/assets/{react-vendor-Cu-7p9CI.js → react-vendor-DAoL5p8_.js} +1 -1
  16. package/dist/index.html +4 -4
  17. package/package.json +2 -1
  18. package/server/agent-manager.mjs +102 -22
  19. package/server/approval-store.mjs +1 -1
  20. package/server/custom-commands.mjs +67 -9
  21. package/server/index.mjs +7 -0
  22. package/server/plugins/loader.mjs +56 -0
  23. package/server/plugins/manifest.mjs +174 -0
  24. package/server/plugins/registry.mjs +304 -0
  25. package/server/project-config.mjs +53 -4
  26. package/server/routes/agent.mjs +1 -16
  27. package/server/routes/plugins.mjs +63 -0
  28. package/server/routes/project.mjs +2 -0
  29. package/server/routes/tools.mjs +12 -1
  30. package/server/skills.mjs +64 -5
  31. package/server/storage.mjs +13 -6
  32. package/server/system-prompt.mjs +27 -5
  33. package/server/tool-wiring.mjs +26 -0
  34. package/dist/assets/index-C7oT9Rdw.css +0 -3
@@ -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
- sendJson(res, 200, { tools: [...skillTools, ...workspaceTools] })
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([projectSharedSkillsDir(workspaceRoot), projectClientSkillsDir(workspaceRoot)])
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
- return loadSkillsFromSources([
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
- return selectSkills(skillNames, await loadProjectSkills(workspaceRoot))
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) {
@@ -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
  }
@@ -27,6 +27,10 @@ function escapeXml(value) {
27
27
  .replace(/>/g, '&gt;')
28
28
  }
29
29
 
30
+ function escapeAttribute(value) {
31
+ return escapeXml(value).replace(/"/g, '&quot;')
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
- if (instructions.global) {
107
- parts.push(`\n<user_instructions>\n${instructions.global}\n</user_instructions>`)
108
- }
126
+ appendInstructionSources(parts, 'user_instructions', instructions.globalSources, instructions.global)
127
+ appendInstructionSources(parts, 'project_instructions', instructions.projectSources, instructions.project)
109
128
 
110
- if (instructions.project) {
111
- parts.push(`\n<project_instructions>\n${instructions.project}\n</project_instructions>`)
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)
@@ -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
  // ---------------------------------------------------------------------------