@shawnstack/quickforge 1.7.12 → 1.8.0
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/dist/assets/AgentProfilesPage-lKGIE5Dj.js +1 -0
- package/dist/assets/ChatPanelHost-CWUYIkXG.js +11 -0
- package/dist/assets/CloudAccountSettingsPage-BhXH0ups.js +1 -0
- package/dist/assets/{PluginsPage-BIbIDfWY.js → PluginsPage-DHWF13mN.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-BOqmBNCx.js → ScheduledTasksPage-DLAg-i8c.js} +2 -2
- package/dist/assets/{SettingsWorkspacePage-BCRtycRN.js → SettingsWorkspacePage-BhUzL5oi.js} +128 -128
- package/dist/assets/ShareLinksSettingsPage-DogGa3lH.js +1 -0
- package/dist/assets/SharedConversationPage-7QpeaG_P.js +1 -0
- package/dist/assets/{TerminalDock-Cqz_9mTC.js → TerminalDock-CWSKcuiU.js} +2 -2
- package/dist/assets/WorkspaceInspector-CEfyYqWy.js +36 -0
- package/dist/assets/{index-BWf6y3Kh.css → index-CX36XjVA.css} +1 -1
- package/dist/assets/index-CgVTwN15.js +66 -0
- package/dist/assets/local-tools-CJVNO1aW.js +388 -0
- package/dist/assets/{mcp-servers-dialog-7Xwu2RVW.js → mcp-servers-dialog-xf3UR49J.js} +2 -2
- package/dist/assets/{skills-dialog-C_JvWbAa.js → skills-dialog-Bbqtuy2h.js} +1 -1
- package/dist/index.html +4 -4
- package/dist/licenses/material-icon-theme.txt +8 -8
- package/package.json +1 -1
- package/server/agent-manager.mjs +175 -47
- package/server/context-references.mjs +139 -0
- package/server/custom-commands.mjs +43 -0
- package/server/index.mjs +1 -1
- package/server/project-config.mjs +17 -5
- package/server/routes/agent-profiles.mjs +16 -1
- package/server/routes/agent.mjs +1 -1
- package/server/routes/shared-conversation.mjs +13 -1
- package/server/routes/skills.mjs +22 -0
- package/server/routes/workspace.mjs +166 -2
- package/server/selected-capabilities.mjs +76 -0
- package/server/skills.mjs +1 -1
- package/server/system-prompt.mjs +1 -3
- package/server/tools/definitions.mjs +0 -10
- package/server/utils/workspace.mjs +13 -4
- package/skills/skill-creator/SKILL.md +485 -485
- package/skills/skill-creator/agents/analyzer.md +274 -274
- package/skills/skill-creator/agents/comparator.md +202 -202
- package/skills/skill-creator/agents/grader.md +223 -223
- package/skills/skill-creator/assets/eval_review.html +146 -146
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -1325
- package/skills/skill-creator/references/schemas.md +430 -430
- package/dist/assets/AgentProfilesPage-CApCpsDd.js +0 -1
- package/dist/assets/ChatPanelHost-COTO7swS.js +0 -48
- package/dist/assets/CloudAccountSettingsPage-Be5r3dtv.js +0 -1
- package/dist/assets/ShareLinksSettingsPage-BrxQacJn.js +0 -1
- package/dist/assets/SharedConversationPage-DJa0M0El.js +0 -1
- package/dist/assets/WorkspaceInspector-C-GOK81D.js +0 -36
- package/dist/assets/index-D3GWkXJR.js +0 -66
- package/dist/assets/local-tools-DY0yEU3T.js +0 -350
|
@@ -33,6 +33,12 @@ const builtinCommandCatalog = [
|
|
|
33
33
|
argumentHint: '[scope]',
|
|
34
34
|
permissionNote: 'no edits',
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
name: 'commit',
|
|
38
|
+
description: 'Validate and commit the current task changes as one local commit.',
|
|
39
|
+
argumentHint: '[message]',
|
|
40
|
+
permissionNote: 'commands allowed; no edits or subagents',
|
|
41
|
+
},
|
|
36
42
|
{
|
|
37
43
|
name: 'summary',
|
|
38
44
|
description: 'Create a new chat with this conversation summarized to reduce context usage.',
|
|
@@ -361,6 +367,29 @@ ${expandedBody}
|
|
|
361
367
|
</custom_command_invocation>`
|
|
362
368
|
}
|
|
363
369
|
|
|
370
|
+
export function formatSkillCommandPrompt(name, task) {
|
|
371
|
+
const taskBlock = String(task || '').trim() || '(none — call activate_skill first, then ask the user what they would like to do)'
|
|
372
|
+
return `<skill_invocation name="${escapeXml(name)}" source="slash">
|
|
373
|
+
This slash command applies only to the current user request. Follow it unless it conflicts with higher-priority system, safety, user, or project instructions.
|
|
374
|
+
|
|
375
|
+
First call the activate_skill tool with name="${escapeXml(name)}", then carry out the task below following the skill's instructions.
|
|
376
|
+
|
|
377
|
+
Task:
|
|
378
|
+
${taskBlock}
|
|
379
|
+
</skill_invocation>`
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function formatAgentCommandPrompt(name, task) {
|
|
383
|
+
return `<subagent_invocation name="${escapeXml(name)}" source="slash">
|
|
384
|
+
This slash command applies only to the current user request. Follow it unless it conflicts with higher-priority system, safety, user, or project instructions.
|
|
385
|
+
|
|
386
|
+
Call the run_subagent tool with subagent="${escapeXml(name)}" and the task below. Do not perform the task yourself unless the subagent is unavailable. When it completes, summarize its findings for the user.
|
|
387
|
+
|
|
388
|
+
Task:
|
|
389
|
+
${String(task || '').trim()}
|
|
390
|
+
</subagent_invocation>`
|
|
391
|
+
}
|
|
392
|
+
|
|
364
393
|
function escapeXml(value) {
|
|
365
394
|
return String(value ?? '')
|
|
366
395
|
.replace(/&/g, '&')
|
|
@@ -384,12 +413,21 @@ export function parseInternalCommandInvocation(message) {
|
|
|
384
413
|
const reviewMatch = text.match(/^\/review(?:\s+([\s\S]*))?$/i)
|
|
385
414
|
if (reviewMatch) return { type: 'review', args: (reviewMatch[1] || '').trim() }
|
|
386
415
|
|
|
416
|
+
const commitMatch = text.match(/^\/commit(?:\s+([\s\S]*))?$/i)
|
|
417
|
+
if (commitMatch) return { type: 'commit', args: (commitMatch[1] || '').trim() }
|
|
418
|
+
|
|
387
419
|
const compactMatch = text.match(/^\/compact(?:\s+([\s\S]*))?$/i)
|
|
388
420
|
if (compactMatch) return { type: 'compact', args: (compactMatch[1] || '').trim() }
|
|
389
421
|
|
|
390
422
|
const summaryMatch = text.match(/^\/summary(?:\s+([\s\S]*))?$/i)
|
|
391
423
|
if (summaryMatch) return { type: 'summary', args: (summaryMatch[1] || '').trim() }
|
|
392
424
|
|
|
425
|
+
const skillMatch = text.match(/^\/skill(?:\s+([\s\S]*))?$/i)
|
|
426
|
+
if (skillMatch) return { type: 'skill', args: (skillMatch[1] || '').trim() }
|
|
427
|
+
|
|
428
|
+
const agentMatch = text.match(/^\/agent(?:\s+([\s\S]*))?$/i)
|
|
429
|
+
if (agentMatch) return { type: 'agent', args: (agentMatch[1] || '').trim() }
|
|
430
|
+
|
|
393
431
|
const createMatch = text.match(/^\/command\s+new\s+([A-Za-z0-9][A-Za-z0-9-]*)\s*$/i)
|
|
394
432
|
if (createMatch) {
|
|
395
433
|
const name = normalizeCommandName(createMatch[1])
|
|
@@ -429,6 +467,11 @@ export async function handleInternalCommand(invocation, workspaceRoot, commandDi
|
|
|
429
467
|
return { review: true, args: invocation.args || '' }
|
|
430
468
|
}
|
|
431
469
|
|
|
470
|
+
if (invocation.type === 'commit') {
|
|
471
|
+
if (!workspaceRoot) return 'Commit requires an active project chat.'
|
|
472
|
+
return { commit: true, args: invocation.args || '' }
|
|
473
|
+
}
|
|
474
|
+
|
|
432
475
|
if (invocation.type === 'clear') {
|
|
433
476
|
return { clear: true }
|
|
434
477
|
}
|
package/server/index.mjs
CHANGED
|
@@ -475,7 +475,7 @@ async function handleApi(req, res, url, requestContext = {}) {
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
// Project workspace inspector routes
|
|
478
|
-
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/children' || pathname === '/api/workspace/search' || pathname === '/api/workspace/file' || pathname === '/api/workspace/resolve-path' || pathname === '/api/workspace/open-external' || pathname.startsWith('/api/workspace/preview/')) {
|
|
478
|
+
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/children' || pathname === '/api/workspace/search' || pathname === '/api/workspace/mention-children' || pathname === '/api/workspace/mention-search' || pathname === '/api/workspace/file' || pathname === '/api/workspace/resolve-path' || pathname === '/api/workspace/open-external' || pathname.startsWith('/api/workspace/preview/')) {
|
|
479
479
|
await handleWorkspaceApi(req, res, url, requestContext)
|
|
480
480
|
return
|
|
481
481
|
}
|
|
@@ -304,14 +304,14 @@ export async function initializeActiveProject() {
|
|
|
304
304
|
setWorkspaceRoot(defaultWorkspaceRoot)
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
export async function
|
|
307
|
+
export async function registeredProjectContextFromId(projectId) {
|
|
308
308
|
const config = await readProjectConfig()
|
|
309
309
|
const project = config.projects.find((item) => item.id === projectId)
|
|
310
310
|
if (!project) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
311
|
+
const error = new Error('Unknown project')
|
|
312
|
+
error.statusCode = 404
|
|
313
|
+
error.errorCode = 'PROJECT_NOT_FOUND'
|
|
314
|
+
throw error
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
await assertDirectory(project.path)
|
|
@@ -319,6 +319,18 @@ export async function projectContextFromId(projectId) {
|
|
|
319
319
|
return { project, workspaceRoot: path.resolve(project.path) }
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
export async function projectContextFromId(projectId) {
|
|
323
|
+
try {
|
|
324
|
+
return await registeredProjectContextFromId(projectId)
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error?.errorCode !== 'PROJECT_NOT_FOUND') throw error
|
|
327
|
+
// Unknown or removed project (e.g. a global conversation's synthetic id) —
|
|
328
|
+
// fall back to the default workspace so workspace/git REST endpoints keep
|
|
329
|
+
// working for global conversations.
|
|
330
|
+
return defaultGlobalWorkspaceContext()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
322
334
|
export async function readInstructionsFile(filePath) {
|
|
323
335
|
const candidates = filePath.endsWith('AGENTS.md')
|
|
324
336
|
? [filePath, path.join(path.dirname(filePath), 'agents.md')]
|
|
@@ -43,6 +43,17 @@ import {
|
|
|
43
43
|
updateBuiltinAgentOverrides,
|
|
44
44
|
updateCustomAgentProfile,
|
|
45
45
|
} from '../agent-profiles.mjs'
|
|
46
|
+
import { projectContextFromId } from '../project-config.mjs'
|
|
47
|
+
|
|
48
|
+
async function projectWorkspaceRoot(projectId) {
|
|
49
|
+
if (!projectId) return null
|
|
50
|
+
try {
|
|
51
|
+
const context = await projectContextFromId(projectId)
|
|
52
|
+
return context.workspaceRoot
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
46
57
|
|
|
47
58
|
function requestError(message, statusCode = 400) {
|
|
48
59
|
const error = new Error(message)
|
|
@@ -158,7 +169,11 @@ export async function handleAgentProfilesApi(req, res, url, context = {}) {
|
|
|
158
169
|
const parts = url.pathname.split('/').filter(Boolean)
|
|
159
170
|
|
|
160
171
|
if (req.method === 'GET' && url.pathname === '/api/agent-profiles') {
|
|
161
|
-
|
|
172
|
+
// Optional projectId makes project-level agent files (.claude/agents,
|
|
173
|
+
// .quickforge/agents) appear alongside built-in and user profiles; an
|
|
174
|
+
// unknown/absent projectId keeps the previous default behavior.
|
|
175
|
+
const workspaceRoot = await projectWorkspaceRoot(url.searchParams.get('projectId'))
|
|
176
|
+
const agents = await listAgentProfiles({ includeDisabled: true, ...(workspaceRoot ? { workspaceRoot } : {}) })
|
|
162
177
|
sendJson(res, 200, { agents: agents.map(agentProfileSnapshot) })
|
|
163
178
|
return
|
|
164
179
|
}
|
package/server/routes/agent.mjs
CHANGED
|
@@ -92,7 +92,7 @@ export async function handleAgentApi(req, res, url, context = {}) {
|
|
|
92
92
|
error.statusCode = 400
|
|
93
93
|
throw error
|
|
94
94
|
}
|
|
95
|
-
const result = await runPrompt(sessionId, message, body?.selectedCapabilities, body?.command, null, context)
|
|
95
|
+
const result = await runPrompt(sessionId, message, body?.selectedCapabilities, body?.command, null, context, body?.contextReferences)
|
|
96
96
|
sendJson(res, 200, result)
|
|
97
97
|
return
|
|
98
98
|
}
|
|
@@ -58,7 +58,13 @@ function publicSharePayload(record) {
|
|
|
58
58
|
function sanitizeMessage(message) {
|
|
59
59
|
if (!message || typeof message !== 'object') return message
|
|
60
60
|
if (message.role === 'system') return null
|
|
61
|
-
return message
|
|
61
|
+
if (!message.details || typeof message.details !== 'object' || Array.isArray(message.details)) return message
|
|
62
|
+
const details = { ...message.details }
|
|
63
|
+
delete details.contextReferences
|
|
64
|
+
const sanitized = { ...message }
|
|
65
|
+
if (Object.keys(details).length > 0) sanitized.details = details
|
|
66
|
+
else delete sanitized.details
|
|
67
|
+
return sanitized
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
function sanitizeContextCompaction(compaction) {
|
|
@@ -369,6 +375,12 @@ export async function handleSharedConversationApi(req, res, url, context = {}) {
|
|
|
369
375
|
if (req.method === 'POST' && action === 'message') {
|
|
370
376
|
assertOperate(record)
|
|
371
377
|
const body = await readJsonBody(req)
|
|
378
|
+
if (Array.isArray(body?.contextReferences) && body.contextReferences.length > 0) {
|
|
379
|
+
throw Object.assign(new Error('Shared conversations do not support file context references'), {
|
|
380
|
+
statusCode: 409,
|
|
381
|
+
errorCode: 'CONTEXT_REFERENCES_UNSUPPORTED_SHARED',
|
|
382
|
+
})
|
|
383
|
+
}
|
|
372
384
|
await restoreAgent(record.sessionId)
|
|
373
385
|
const result = await runPrompt(
|
|
374
386
|
record.sessionId,
|
package/server/routes/skills.mjs
CHANGED
|
@@ -6,10 +6,14 @@ import {
|
|
|
6
6
|
filterKnownProjectSkillNames,
|
|
7
7
|
findGlobalSkill,
|
|
8
8
|
findProjectSkill,
|
|
9
|
+
loadSelectedGlobalSkills,
|
|
10
|
+
loadSelectedProjectSkills,
|
|
11
|
+
mergeSkills,
|
|
9
12
|
projectSkillSearchPaths,
|
|
10
13
|
listGlobalSkillSummaries,
|
|
11
14
|
listProjectSkillSummaries,
|
|
12
15
|
skillSearchPaths,
|
|
16
|
+
summarizeSkills,
|
|
13
17
|
} from '../skills.mjs'
|
|
14
18
|
|
|
15
19
|
function getProject(config, projectId) {
|
|
@@ -46,6 +50,24 @@ export async function handleSkillsApi(req, res, url) {
|
|
|
46
50
|
const scope = url.searchParams.get('scope') === 'global' ? 'global' : 'project'
|
|
47
51
|
|
|
48
52
|
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
53
|
+
if (url.searchParams.get('available') === 'true') {
|
|
54
|
+
// Merged view of what the current session can activate: global enabled
|
|
55
|
+
// skills always, plus project enabled skills (including plugin
|
|
56
|
+
// contributions) when a valid project is in scope. Mirrors the merge in
|
|
57
|
+
// loadSkillToolContext / activeSkillsForContext (project overrides global).
|
|
58
|
+
const project = projectId ? getProject(config, projectId) : null
|
|
59
|
+
const workspaceRoot = await projectWorkspaceRoot(project?.id)
|
|
60
|
+
const globalSkills = await loadSelectedGlobalSkills(selectedGlobalSkills(config))
|
|
61
|
+
const projectSkills = workspaceRoot
|
|
62
|
+
? await loadSelectedProjectSkills(selectedSkillsForProject(project), workspaceRoot)
|
|
63
|
+
: []
|
|
64
|
+
sendJson(res, 200, {
|
|
65
|
+
available: true,
|
|
66
|
+
skills: summarizeSkills(mergeSkills(globalSkills, projectSkills)),
|
|
67
|
+
})
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
if (scope === 'global') {
|
|
50
72
|
sendJson(res, 200, {
|
|
51
73
|
scope: 'global',
|
|
@@ -5,7 +5,7 @@ import { streamSimpleWithAiHttpLogging } from '../ai-http-logger.mjs'
|
|
|
5
5
|
import { resolveModelBinding } from '../model-catalog.mjs'
|
|
6
6
|
import { DEFAULT_AI_MAX_RETRIES } from '../ai-provider-options.mjs'
|
|
7
7
|
import { sendJson, readJsonBody } from '../utils/response.mjs'
|
|
8
|
-
import { projectContextFromId } from '../project-config.mjs'
|
|
8
|
+
import { projectContextFromId, registeredProjectContextFromId } from '../project-config.mjs'
|
|
9
9
|
import { readStore } from '../storage.mjs'
|
|
10
10
|
import { logger } from '../utils/logger.mjs'
|
|
11
11
|
import { openPathInFileManager, openPathInIDEA, openPathInVSCode } from '../utils/platform.mjs'
|
|
@@ -29,6 +29,8 @@ const DEFAULT_CHILDREN_LIMIT = 200
|
|
|
29
29
|
const MAX_CHILDREN_LIMIT = 500
|
|
30
30
|
const DEFAULT_SEARCH_LIMIT = 100
|
|
31
31
|
const MAX_SEARCH_LIMIT = 200
|
|
32
|
+
const DEFAULT_MENTION_SEARCH_LIMIT = 20
|
|
33
|
+
const MAX_MENTION_SEARCH_LIMIT = 50
|
|
32
34
|
const MAX_SEARCH_VISITED_ENTRIES = 50000
|
|
33
35
|
const SKIP_DIRS = new Set(['.git', 'node_modules'])
|
|
34
36
|
|
|
@@ -897,7 +899,7 @@ async function handleWorkspaceChildren(req, res, url) {
|
|
|
897
899
|
export async function readValidatedWorkspaceSearchDirectory(directory, validateWorkspacePath, visitedDirectories, io = {}) {
|
|
898
900
|
const realpath = io.realpath || fs.realpath
|
|
899
901
|
const readdir = io.readdir || fs.readdir
|
|
900
|
-
await validateWorkspacePath(directory, { allowSensitive:
|
|
902
|
+
await validateWorkspacePath(directory, { allowSensitive: io.allowSensitive !== false })
|
|
901
903
|
const directoryReal = await realpath(directory)
|
|
902
904
|
const directoryKey = process.platform === 'win32' ? directoryReal.toLocaleLowerCase() : directoryReal
|
|
903
905
|
if (visitedDirectories.has(directoryKey)) return null
|
|
@@ -973,6 +975,160 @@ async function handleWorkspaceSearch(req, res, url) {
|
|
|
973
975
|
}))
|
|
974
976
|
}
|
|
975
977
|
|
|
978
|
+
function mentionSearchRank(entry, normalizedQuery) {
|
|
979
|
+
const name = entry.name.toLocaleLowerCase()
|
|
980
|
+
const relativePath = entry.path.toLocaleLowerCase()
|
|
981
|
+
if (name === normalizedQuery) return 0
|
|
982
|
+
if (name.startsWith(normalizedQuery)) return 1
|
|
983
|
+
if (name.includes(normalizedQuery)) return 2
|
|
984
|
+
return relativePath.startsWith(normalizedQuery) ? 3 : 4
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function compareMentionSearchEntries(left, right, normalizedQuery) {
|
|
988
|
+
return mentionSearchRank(left, normalizedQuery) - mentionSearchRank(right, normalizedQuery)
|
|
989
|
+
|| left.path.localeCompare(right.path, undefined, { sensitivity: 'base' })
|
|
990
|
+
|| left.path.localeCompare(right.path, undefined, { sensitivity: 'variant' })
|
|
991
|
+
|| (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async function mentionSearchEntryFromDirent(directory, dirent, context, validateWorkspacePath) {
|
|
995
|
+
if (dirent.isDirectory() && SKIP_DIRS.has(dirent.name)) return null
|
|
996
|
+
const fullPath = path.join(directory, dirent.name)
|
|
997
|
+
const relativePath = toWorkspaceRelative(fullPath, context)
|
|
998
|
+
try {
|
|
999
|
+
await validateWorkspacePath(fullPath)
|
|
1000
|
+
const stat = await fs.stat(fullPath)
|
|
1001
|
+
if (stat.isDirectory()) {
|
|
1002
|
+
if (SKIP_DIRS.has(dirent.name)) return null
|
|
1003
|
+
if (dirent.isSymbolicLink()) {
|
|
1004
|
+
const workspaceReal = await fs.realpath(context.workspaceRoot)
|
|
1005
|
+
const targetPaths = [
|
|
1006
|
+
[workspaceReal, await fs.realpath(fullPath)],
|
|
1007
|
+
[context.workspaceRoot, path.resolve(directory, await fs.readlink(fullPath))],
|
|
1008
|
+
]
|
|
1009
|
+
if (targetPaths.some(([root, target]) => path.relative(root, target).split(path.sep).some((part) => SKIP_DIRS.has(part)))) return null
|
|
1010
|
+
}
|
|
1011
|
+
return { name: dirent.name, path: relativePath, type: 'directory', fullPath }
|
|
1012
|
+
}
|
|
1013
|
+
if (stat.isFile()) return { name: dirent.name, path: relativePath, type: 'file' }
|
|
1014
|
+
} catch {
|
|
1015
|
+
// Sensitive, external, broken, or inaccessible entries are omitted.
|
|
1016
|
+
}
|
|
1017
|
+
return null
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function readMentionWorkspaceDirectory(context, relativePath) {
|
|
1021
|
+
const requestedPath = relativePath?.trim() || '.'
|
|
1022
|
+
const directory = resolveWorkspacePath(requestedPath, context)
|
|
1023
|
+
const validateWorkspacePath = await createWorkspacePathValidator(context)
|
|
1024
|
+
await validateWorkspacePath(directory)
|
|
1025
|
+
const stat = await fs.stat(directory)
|
|
1026
|
+
if (!stat.isDirectory()) {
|
|
1027
|
+
const error = new Error('Path is not a directory')
|
|
1028
|
+
error.statusCode = 400
|
|
1029
|
+
throw error
|
|
1030
|
+
}
|
|
1031
|
+
return { directory, requestedPath: toWorkspaceRelative(directory, context), validateWorkspacePath }
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export async function listWorkspaceMentionChildren(context, relativePath = '.') {
|
|
1035
|
+
const { directory, requestedPath, validateWorkspacePath } = await readMentionWorkspaceDirectory(context, relativePath)
|
|
1036
|
+
const dirents = await fs.readdir(directory, { withFileTypes: true })
|
|
1037
|
+
const entries = []
|
|
1038
|
+
for (const dirent of dirents) {
|
|
1039
|
+
const entry = await mentionSearchEntryFromDirent(directory, dirent, context, validateWorkspacePath)
|
|
1040
|
+
if (!entry) continue
|
|
1041
|
+
entries.push({ name: entry.name, path: entry.path, type: entry.type })
|
|
1042
|
+
}
|
|
1043
|
+
entries.sort(compareWorkspaceEntries)
|
|
1044
|
+
return {
|
|
1045
|
+
root: context.project.name,
|
|
1046
|
+
path: requestedPath,
|
|
1047
|
+
entries,
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async function handleWorkspaceMentionChildren(req, res, url) {
|
|
1052
|
+
const projectId = url.searchParams.get('projectId')
|
|
1053
|
+
if (!projectId) {
|
|
1054
|
+
const error = new Error('projectId is required')
|
|
1055
|
+
error.statusCode = 400
|
|
1056
|
+
throw error
|
|
1057
|
+
}
|
|
1058
|
+
const context = await registeredProjectContextFromId(projectId)
|
|
1059
|
+
sendJson(res, 200, await listWorkspaceMentionChildren(context, url.searchParams.get('path') || '.'))
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export async function searchWorkspaceMentions(context, rawQuery, options = {}) {
|
|
1063
|
+
const query = typeof rawQuery === 'string' ? rawQuery.trim() : ''
|
|
1064
|
+
if (query.length < 2) {
|
|
1065
|
+
const error = new Error('query must contain at least 2 characters')
|
|
1066
|
+
error.statusCode = 400
|
|
1067
|
+
throw error
|
|
1068
|
+
}
|
|
1069
|
+
const limit = parseBoundedLimit(options.limit ?? null, DEFAULT_MENTION_SEARCH_LIMIT, MAX_MENTION_SEARCH_LIMIT)
|
|
1070
|
+
const validateWorkspacePath = await createWorkspacePathValidator(context)
|
|
1071
|
+
await validateWorkspacePath(context.workspaceRoot)
|
|
1072
|
+
const normalizedQuery = query.toLocaleLowerCase()
|
|
1073
|
+
const results = []
|
|
1074
|
+
const pending = [context.workspaceRoot]
|
|
1075
|
+
const visitedDirectories = new Set()
|
|
1076
|
+
let visited = 0
|
|
1077
|
+
let traversalTruncated = false
|
|
1078
|
+
|
|
1079
|
+
while (pending.length > 0 && !traversalTruncated) {
|
|
1080
|
+
const directory = pending.pop()
|
|
1081
|
+
let validated
|
|
1082
|
+
try {
|
|
1083
|
+
validated = await readValidatedWorkspaceSearchDirectory(directory, validateWorkspacePath, visitedDirectories, { allowSensitive: false })
|
|
1084
|
+
} catch {
|
|
1085
|
+
continue
|
|
1086
|
+
}
|
|
1087
|
+
if (!validated) continue
|
|
1088
|
+
const { dirents } = validated
|
|
1089
|
+
dirents.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' })
|
|
1090
|
+
|| left.name.localeCompare(right.name, undefined, { sensitivity: 'variant' })
|
|
1091
|
+
|| (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
|
|
1092
|
+
const childDirectories = []
|
|
1093
|
+
for (const dirent of dirents) {
|
|
1094
|
+
if (visited >= MAX_SEARCH_VISITED_ENTRIES) {
|
|
1095
|
+
traversalTruncated = true
|
|
1096
|
+
break
|
|
1097
|
+
}
|
|
1098
|
+
visited += 1
|
|
1099
|
+
const entry = await mentionSearchEntryFromDirent(directory, dirent, context, validateWorkspacePath)
|
|
1100
|
+
if (!entry) continue
|
|
1101
|
+
if (entry.type === 'directory') {
|
|
1102
|
+
childDirectories.push(entry.fullPath)
|
|
1103
|
+
} else if (entry.path.toLocaleLowerCase().includes(normalizedQuery) || entry.name.toLocaleLowerCase().includes(normalizedQuery)) {
|
|
1104
|
+
results.push(entry)
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
for (let index = childDirectories.length - 1; index >= 0; index -= 1) pending.push(childDirectories[index])
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
results.sort((left, right) => compareMentionSearchEntries(left, right, normalizedQuery))
|
|
1111
|
+
return {
|
|
1112
|
+
root: context.project.name,
|
|
1113
|
+
query,
|
|
1114
|
+
entries: results.slice(0, limit),
|
|
1115
|
+
truncated: traversalTruncated || results.length > limit,
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async function handleWorkspaceMentionSearch(req, res, url) {
|
|
1120
|
+
const projectId = url.searchParams.get('projectId')
|
|
1121
|
+
if (!projectId) {
|
|
1122
|
+
const error = new Error('projectId is required')
|
|
1123
|
+
error.statusCode = 400
|
|
1124
|
+
throw error
|
|
1125
|
+
}
|
|
1126
|
+
const context = await registeredProjectContextFromId(projectId)
|
|
1127
|
+
sendJson(res, 200, await searchWorkspaceMentions(context, url.searchParams.get('query') || '', {
|
|
1128
|
+
limit: url.searchParams.get('limit'),
|
|
1129
|
+
}))
|
|
1130
|
+
}
|
|
1131
|
+
|
|
976
1132
|
async function handleWorkspaceFile(req, res, url) {
|
|
977
1133
|
const context = await projectContextFromUrl(url)
|
|
978
1134
|
const relativePath = url.searchParams.get('path') || ''
|
|
@@ -1414,6 +1570,14 @@ export async function handleWorkspaceApi(req, res, url, requestContext = {}) {
|
|
|
1414
1570
|
await handleWorkspaceSearch(req, res, url)
|
|
1415
1571
|
return
|
|
1416
1572
|
}
|
|
1573
|
+
if (req.method === 'GET' && url.pathname === '/api/workspace/mention-children') {
|
|
1574
|
+
await handleWorkspaceMentionChildren(req, res, url)
|
|
1575
|
+
return
|
|
1576
|
+
}
|
|
1577
|
+
if (req.method === 'GET' && url.pathname === '/api/workspace/mention-search') {
|
|
1578
|
+
await handleWorkspaceMentionSearch(req, res, url)
|
|
1579
|
+
return
|
|
1580
|
+
}
|
|
1417
1581
|
if (req.method === 'GET' && url.pathname === '/api/workspace/file') {
|
|
1418
1582
|
await handleWorkspaceFile(req, res, url)
|
|
1419
1583
|
return
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const MAX_SELECTED_CAPABILITIES = 4
|
|
2
|
+
export const SELECTED_CAPABILITY_PLUGIN_NAME_MAX_LENGTH = 120
|
|
3
|
+
export const SELECTED_CAPABILITY_NAME_MAX_LENGTH = 120
|
|
4
|
+
export const SELECTED_CAPABILITY_LABEL_MAX_LENGTH = 160
|
|
5
|
+
export const SELECTED_CAPABILITY_DESCRIPTION_MAX_LENGTH = 400
|
|
6
|
+
export const SELECTED_CAPABILITIES_DETAILS_KEY = 'selectedCapabilities'
|
|
7
|
+
|
|
8
|
+
const SELECTED_CAPABILITY_TYPES = new Set(['plugin', 'skill', 'tool', 'command'])
|
|
9
|
+
|
|
10
|
+
function normalizedText(value, maxLength) {
|
|
11
|
+
if (typeof value !== 'string') return undefined
|
|
12
|
+
const normalized = value.trim().slice(0, maxLength)
|
|
13
|
+
return normalized || undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function selectedCapabilityKey(capability) {
|
|
17
|
+
return `${capability.type}:${capability.pluginName}:${capability.name}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeSelectedCapabilities(value) {
|
|
21
|
+
if (!Array.isArray(value)) return []
|
|
22
|
+
const result = []
|
|
23
|
+
const seen = new Set()
|
|
24
|
+
for (const item of value) {
|
|
25
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) || !SELECTED_CAPABILITY_TYPES.has(item.type)) continue
|
|
26
|
+
const pluginName = normalizedText(item.pluginName, SELECTED_CAPABILITY_PLUGIN_NAME_MAX_LENGTH)
|
|
27
|
+
const name = normalizedText(item.name, SELECTED_CAPABILITY_NAME_MAX_LENGTH)
|
|
28
|
+
const label = normalizedText(item.label, SELECTED_CAPABILITY_LABEL_MAX_LENGTH)
|
|
29
|
+
if (!pluginName || !name || !label) continue
|
|
30
|
+
const capability = { type: item.type, pluginName, name, label }
|
|
31
|
+
const description = normalizedText(item.description, SELECTED_CAPABILITY_DESCRIPTION_MAX_LENGTH)
|
|
32
|
+
if (description) capability.description = description
|
|
33
|
+
const key = selectedCapabilityKey(capability)
|
|
34
|
+
if (seen.has(key)) continue
|
|
35
|
+
seen.add(key)
|
|
36
|
+
result.push(capability)
|
|
37
|
+
if (result.length >= MAX_SELECTED_CAPABILITIES) break
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function selectedCapabilitySnapshots(value) {
|
|
43
|
+
return normalizeSelectedCapabilities(value).map(({ type, pluginName, name, label }) => ({ type, pluginName, name, label }))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function selectedCapabilitiesFromMessage(message) {
|
|
47
|
+
const details = message?.details
|
|
48
|
+
if (!details || typeof details !== 'object' || Array.isArray(details)) return []
|
|
49
|
+
return selectedCapabilitySnapshots(details[SELECTED_CAPABILITIES_DETAILS_KEY])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function withCanonicalSelectedCapabilities(message, capabilities) {
|
|
53
|
+
if (!message || typeof message !== 'object') return message
|
|
54
|
+
const snapshots = selectedCapabilitySnapshots(capabilities)
|
|
55
|
+
const details = message.details && typeof message.details === 'object' && !Array.isArray(message.details)
|
|
56
|
+
? message.details
|
|
57
|
+
: {}
|
|
58
|
+
const nextDetails = { ...details }
|
|
59
|
+
if (snapshots.length > 0) nextDetails[SELECTED_CAPABILITIES_DETAILS_KEY] = snapshots
|
|
60
|
+
else delete nextDetails[SELECTED_CAPABILITIES_DETAILS_KEY]
|
|
61
|
+
const next = { ...message }
|
|
62
|
+
if (Object.keys(nextDetails).length > 0) next.details = nextDetails
|
|
63
|
+
else delete next.details
|
|
64
|
+
return next
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function selectedCapabilityPrompt(capabilities) {
|
|
68
|
+
const normalized = normalizeSelectedCapabilities(capabilities)
|
|
69
|
+
if (normalized.length === 0) return null
|
|
70
|
+
const lines = normalized.map((capability) => {
|
|
71
|
+
const toolHint = capability.type === 'tool' ? ` Tool name: plugin__${capability.pluginName}__${capability.name}.` : ''
|
|
72
|
+
const description = capability.description ? ` Description: ${capability.description}` : ''
|
|
73
|
+
return `- ${capability.label} (${capability.type}, plugin: ${capability.pluginName}, name: ${capability.name}).${toolHint}${description}`
|
|
74
|
+
}).join('\n')
|
|
75
|
+
return `The user selected the following QuickForge plugin capability mentions for this turn. Treat them as an explicit preference for routing and context. Use the selected capability when relevant, but do not force it if it is unrelated to the actual request.\n\n${lines}`
|
|
76
|
+
}
|
package/server/skills.mjs
CHANGED
|
@@ -392,7 +392,7 @@ function searchDirsForList(value) {
|
|
|
392
392
|
return value.length === 1 ? value[0] : value.slice()
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
function summarizeSkills(skills) {
|
|
395
|
+
export function summarizeSkills(skills) {
|
|
396
396
|
return skills.map(({ instructions: _instructions, rootDir: _rootDir, location: _location, ...summary }) => summary)
|
|
397
397
|
}
|
|
398
398
|
|
package/server/system-prompt.mjs
CHANGED
|
@@ -2,8 +2,7 @@ export const BASE_SYSTEM_PROMPT = `You are a pragmatic coding assistant.
|
|
|
2
2
|
|
|
3
3
|
For project tasks:
|
|
4
4
|
- Do not assume requirements. If ambiguous, state assumptions or ask.
|
|
5
|
-
-
|
|
6
|
-
- Make surgical changes only. Do not refactor unrelated code.
|
|
5
|
+
- Do not refactor unrelated code.
|
|
7
6
|
- Match existing style.
|
|
8
7
|
- When content has room for visual explanation, first consider whether an SVG diagram can improve understanding.
|
|
9
8
|
- For multi-step work, use a brief plan.
|
|
@@ -11,7 +10,6 @@ For project tasks:
|
|
|
11
10
|
- Before taking action, confirm with the user.
|
|
12
11
|
- Unless the change is trivial and localized to an already-known file, use Explore first for read-only repository research before implementation decisions; prefer Explore for file discovery, source location, broad searches, call-chain lookup, pattern lookup, impact analysis, and locating related tests, docs, wiki pages, or build scripts.
|
|
13
12
|
- For complex multi-step work, use General only for bounded assistance; the parent assistant remains responsible for final decisions, minimal edits, and verification.
|
|
14
|
-
- Make minimal, focused changes.
|
|
15
13
|
- Prefer dedicated workspace tools for reading, editing, and searching files.
|
|
16
14
|
- If dedicated tools are unavailable or insufficient, use the shell/command tool.
|
|
17
15
|
- Use Python through the shell for reliable scripting, data processing, or file transformations.
|
|
@@ -134,16 +134,6 @@ export const workspaceTools = [
|
|
|
134
134
|
}),
|
|
135
135
|
executionMode: 'sequential',
|
|
136
136
|
},
|
|
137
|
-
{
|
|
138
|
-
name: 'generate_image',
|
|
139
|
-
label: 'Generate image',
|
|
140
|
-
description: 'Generate images with the configured OpenRouter provider and save them as assets owned by the current conversation. Use this when the user explicitly asks for a generated bitmap image. The operation may incur provider charges.',
|
|
141
|
-
parameters: Type.Object({
|
|
142
|
-
prompt: Type.String({ description: 'Detailed image-generation prompt.' }),
|
|
143
|
-
model: Type.Optional(Type.String({ description: 'Optional OpenRouter image model ID. Defaults to google/gemini-2.5-flash-image.' })),
|
|
144
|
-
}),
|
|
145
|
-
executionMode: 'sequential',
|
|
146
|
-
},
|
|
147
137
|
{
|
|
148
138
|
name: 'present_files',
|
|
149
139
|
label: 'Present files',
|
|
@@ -41,7 +41,7 @@ export function toWorkspaceRelative(fullPath, context) {
|
|
|
41
41
|
|
|
42
42
|
export function isSensitiveWorkspacePath(fullPath, context) {
|
|
43
43
|
const relative = toWorkspaceRelative(fullPath, context)
|
|
44
|
-
const parts = relative.split('/')
|
|
44
|
+
const parts = relative.split('/').map((part) => part.toLocaleLowerCase())
|
|
45
45
|
const name = parts.at(-1) || ''
|
|
46
46
|
return (
|
|
47
47
|
parts.includes('.git') ||
|
|
@@ -61,6 +61,13 @@ export function isSensitiveWorkspacePath(fullPath, context) {
|
|
|
61
61
|
)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function sensitivePathError(fullPath, context) {
|
|
65
|
+
return Object.assign(new Error(`Access to sensitive path is blocked: ${toWorkspaceRelative(fullPath, context)}`), {
|
|
66
|
+
statusCode: 403,
|
|
67
|
+
errorCode: 'WORKSPACE_SENSITIVE_PATH',
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
async function realpathNearestExistingParent(inputPath) {
|
|
65
72
|
let current = path.resolve(inputPath)
|
|
66
73
|
while (true) {
|
|
@@ -77,9 +84,7 @@ async function realpathNearestExistingParent(inputPath) {
|
|
|
77
84
|
|
|
78
85
|
async function assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal, options = {}) {
|
|
79
86
|
if (!options.allowSensitive && isSensitiveWorkspacePath(fullPath, context)) {
|
|
80
|
-
|
|
81
|
-
error.statusCode = 403
|
|
82
|
-
throw error
|
|
87
|
+
throw sensitivePathError(fullPath, context)
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
let targetReal
|
|
@@ -98,8 +103,12 @@ async function assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal,
|
|
|
98
103
|
if (!isInside(workspaceReal, targetReal)) {
|
|
99
104
|
const error = new Error(`Path resolves outside the selected project: ${toWorkspaceRelative(fullPath, context)}`)
|
|
100
105
|
error.statusCode = 403
|
|
106
|
+
error.errorCode = 'WORKSPACE_PATH_ESCAPE'
|
|
101
107
|
throw error
|
|
102
108
|
}
|
|
109
|
+
if (!options.allowSensitive && isSensitiveWorkspacePath(targetReal, { workspaceRoot: workspaceReal })) {
|
|
110
|
+
throw sensitivePathError(fullPath, context)
|
|
111
|
+
}
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
export async function createWorkspacePathValidator(context) {
|