@shawnstack/quickforge 1.4.0 → 1.4.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 +12 -12
- package/dist/assets/{AgentProfilesPage-C79teCgh.js → AgentProfilesPage-CNK5PxA3.js} +1 -1
- package/dist/assets/ChatPanelHost-FqPQwwMO.js +217 -0
- package/dist/assets/PluginsPage-BCu1Ept0.js +1 -0
- package/dist/assets/{ScheduledTasksPage-C047y3p3.js → ScheduledTasksPage-Bx04rjui.js} +2 -2
- package/dist/assets/SharedConversationPage-55vX9sqe.js +1 -0
- package/dist/assets/TerminalDock-DLN_pLkJ.js +2 -0
- package/dist/assets/WorkspaceInspector-DoemHHnY.js +3 -0
- package/dist/assets/{WorkspaceReaderDialog-bTeERaGd.js → WorkspaceReaderDialog-C6xUHBCw.js} +2 -2
- package/dist/assets/{icons-Dsc5yL3l.js → icons-BWtivFsx.js} +1 -1
- package/dist/assets/index-CxOHP41X.css +3 -0
- package/dist/assets/index-Dcf73EL8.js +895 -0
- package/dist/assets/{monaco-DG4TcBMc.js → monaco-evITXh-m.js} +1 -1
- package/dist/assets/{react-vendor-CiCXOLb5.js → react-vendor-Mthyt1p4.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/agent-manager.mjs +85 -13
- package/server/approval-store.mjs +13 -1
- package/server/auto-compaction.mjs +29 -73
- package/server/context-usage.mjs +108 -0
- package/server/custom-commands.mjs +145 -28
- package/server/mcp/registry.mjs +40 -0
- package/server/routes/agent.mjs +1 -1
- package/server/routes/mcp.mjs +7 -1
- package/server/routes/project.mjs +32 -2
- package/server/routes/shared-conversation.mjs +1 -1
- package/server/storage.mjs +1 -0
- package/server/subagents.mjs +8 -6
- package/server/system-prompt.mjs +2 -2
- package/server/tools/definitions.mjs +1 -1
- package/dist/assets/ChatPanelHost-BjdIshtX.js +0 -195
- package/dist/assets/PluginsPage-Dt7Iiddo.js +0 -1
- package/dist/assets/SharedConversationPage-8X8kfztQ.js +0 -1
- package/dist/assets/TerminalDock-CEuJNf0m.js +0 -2
- package/dist/assets/WorkspaceInspector-BIa5gLVs.js +0 -3
- package/dist/assets/index-CPAWYhzz.css +0 -3
- package/dist/assets/index-YTL26wyJ.js +0 -814
|
@@ -1,11 +1,59 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { getEnabledPluginCommandSources } from './plugins/registry.mjs'
|
|
4
|
+
import { userCommandsDir } from './storage.mjs'
|
|
4
5
|
|
|
5
6
|
const commandsRelativeDirs = ['.claude/commands', '.opencode/commands', '.ai/commands']
|
|
6
7
|
const commandsRelativeDir = '.ai/commands'
|
|
7
8
|
const commandNamePattern = /^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Centralized metadata for all built-in slash commands.
|
|
12
|
+
* This is the single source of truth used by /help output.
|
|
13
|
+
* Front-end i18n descriptions (i18n.ts) should be kept in sync with the
|
|
14
|
+
* `description` values here.
|
|
15
|
+
*/
|
|
16
|
+
const builtinCommandCatalog = [
|
|
17
|
+
{
|
|
18
|
+
name: 'plan',
|
|
19
|
+
description: 'Create a plan first; this turn cannot edit files or run commands.',
|
|
20
|
+
argumentHint: '[task]',
|
|
21
|
+
permissionNote: 'read-only',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'review',
|
|
25
|
+
description: 'Review pending code changes before commit; this turn cannot edit files.',
|
|
26
|
+
argumentHint: '[scope]',
|
|
27
|
+
permissionNote: 'no edits',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'compact',
|
|
31
|
+
description: 'Create a new chat with this conversation compacted to reduce context usage.',
|
|
32
|
+
argumentHint: '',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'clear',
|
|
36
|
+
description: 'Clear the current chat history and context without calling the model.',
|
|
37
|
+
argumentHint: '',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'commands',
|
|
41
|
+
description: 'List custom commands (project, user-level, and plugin).',
|
|
42
|
+
argumentHint: '',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'command new',
|
|
46
|
+
description: 'Create a project custom command template.',
|
|
47
|
+
argumentHint: '<name>',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'help',
|
|
51
|
+
aliases: ['?'],
|
|
52
|
+
description: 'Show available commands and their usage.',
|
|
53
|
+
argumentHint: '',
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
9
57
|
function normalizeCommandName(value) {
|
|
10
58
|
const name = String(value || '').trim().toLowerCase()
|
|
11
59
|
return commandNamePattern.test(name) ? name : null
|
|
@@ -103,7 +151,7 @@ function firstOptionalBoolean(...values) {
|
|
|
103
151
|
return undefined
|
|
104
152
|
}
|
|
105
153
|
|
|
106
|
-
function commandFromFile(file, text, options = {}) {
|
|
154
|
+
export function commandFromFile(file, text, options = {}) {
|
|
107
155
|
const parsed = parseFrontmatter(text)
|
|
108
156
|
if (!parsed.body) return null
|
|
109
157
|
|
|
@@ -229,13 +277,27 @@ async function listPluginCommands(workspaceRoot) {
|
|
|
229
277
|
return commands
|
|
230
278
|
}
|
|
231
279
|
|
|
280
|
+
async function listUserCommands() {
|
|
281
|
+
return listCommandsFromDirectory(userCommandsDir, {
|
|
282
|
+
source: 'user',
|
|
283
|
+
relativeRoot: '~/.quickforge/commands',
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
232
287
|
export async function listProjectCommands(workspaceRoot, commandDir) {
|
|
233
288
|
const byName = new Map()
|
|
234
289
|
|
|
290
|
+
// 1. Plugin commands (lowest priority)
|
|
235
291
|
for (const command of (await listPluginCommands(workspaceRoot)).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
236
292
|
byName.set(command.name, command)
|
|
237
293
|
}
|
|
238
294
|
|
|
295
|
+
// 2. User-level commands (~/.quickforge/commands/) — override plugins, overridden by project
|
|
296
|
+
for (const command of (await listUserCommands()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
297
|
+
byName.set(command.name, command)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 3. Project directories: .claude → .opencode → .ai → configured (highest priority)
|
|
239
301
|
for (const dir of commandDirectories(workspaceRoot, commandDir)) {
|
|
240
302
|
const commands = await listCommandsFromDirectory(dir)
|
|
241
303
|
for (const command of commands.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
@@ -297,6 +359,7 @@ function escapeXml(value) {
|
|
|
297
359
|
|
|
298
360
|
export function parseInternalCommandInvocation(message) {
|
|
299
361
|
const text = textFromUserMessage(message).trim()
|
|
362
|
+
if (/^\/(?:help|\?)(?:\s+.*)?$/i.test(text)) return { type: 'help' }
|
|
300
363
|
if (/^\/commands(?:\s+.*)?$/i.test(text)) return { type: 'list' }
|
|
301
364
|
if (/^\/clear\s*$/i.test(text)) return { type: 'clear' }
|
|
302
365
|
if (/^\/clear(?:\s+[\s\S]+)$/i.test(text)) return { type: 'invalid-clear-args' }
|
|
@@ -344,14 +407,19 @@ export async function handleInternalCommand(invocation, workspaceRoot, commandDi
|
|
|
344
407
|
return 'Usage: /clear'
|
|
345
408
|
}
|
|
346
409
|
|
|
347
|
-
if (
|
|
348
|
-
return
|
|
410
|
+
if (invocation.type === 'help') {
|
|
411
|
+
return formatHelpText(await listProjectCommands(workspaceRoot, commandDir))
|
|
349
412
|
}
|
|
350
413
|
|
|
414
|
+
// /commands and /help work without a project (user-level commands are global)
|
|
351
415
|
if (invocation.type === 'list') {
|
|
352
416
|
return formatCommandList(await listProjectCommands(workspaceRoot, commandDir))
|
|
353
417
|
}
|
|
354
418
|
|
|
419
|
+
if (!workspaceRoot) {
|
|
420
|
+
return 'Custom commands require an active project chat.'
|
|
421
|
+
}
|
|
422
|
+
|
|
355
423
|
if (invocation.type === 'new') {
|
|
356
424
|
return createCommandTemplate(workspaceRoot, invocation.name)
|
|
357
425
|
}
|
|
@@ -367,17 +435,17 @@ function formatPermission(value) {
|
|
|
367
435
|
return value === false ? 'false' : 'true'
|
|
368
436
|
}
|
|
369
437
|
|
|
370
|
-
function formatCommandList(commands) {
|
|
438
|
+
export function formatCommandList(commands) {
|
|
371
439
|
if (commands.length === 0) {
|
|
372
440
|
return [
|
|
373
|
-
'No
|
|
441
|
+
'No custom commands found.',
|
|
374
442
|
'',
|
|
375
443
|
'Create one with:',
|
|
376
444
|
'```text',
|
|
377
445
|
'/command new review',
|
|
378
446
|
'```',
|
|
379
447
|
'',
|
|
380
|
-
'Or add Markdown files under `.claude/commands/`, `.opencode/commands/`,
|
|
448
|
+
'Or add Markdown files under `~/.quickforge/commands/` (user-level), or `.claude/commands/`, `.opencode/commands/`, `.ai/commands/` (project-level), for example `.ai/commands/review.md`.',
|
|
381
449
|
].join('\n')
|
|
382
450
|
}
|
|
383
451
|
|
|
@@ -389,37 +457,59 @@ function formatCommandList(commands) {
|
|
|
389
457
|
})
|
|
390
458
|
|
|
391
459
|
return [
|
|
392
|
-
'
|
|
460
|
+
'Custom commands:',
|
|
393
461
|
'',
|
|
394
462
|
...rows,
|
|
395
463
|
'',
|
|
396
|
-
'Command files live in `.claude/commands/*.md`, `.opencode/commands/*.md`, `.ai/commands/*.md`, or configured directories. Use `$ARGUMENTS` inside a command file to insert invocation arguments.',
|
|
464
|
+
'Command files live in `~/.quickforge/commands/*.md` (user-level), `.claude/commands/*.md`, `.opencode/commands/*.md`, `.ai/commands/*.md`, or configured directories. Use `$ARGUMENTS` inside a command file to insert invocation arguments.',
|
|
397
465
|
].join('\n')
|
|
398
466
|
}
|
|
399
467
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
468
|
+
function formatBuiltinCommandRows() {
|
|
469
|
+
return builtinCommandCatalog.map((cmd) => {
|
|
470
|
+
const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : ''
|
|
471
|
+
const aliases = cmd.aliases?.length
|
|
472
|
+
? ` (alias: ${cmd.aliases.map((alias) => `/${alias}`).join(', ')})`
|
|
473
|
+
: ''
|
|
474
|
+
const perm = cmd.permissionNote ? ` \[${cmd.permissionNote}\]` : ''
|
|
475
|
+
return `- \`/${cmd.name}${hint}\`${aliases} — ${cmd.description}${perm}`
|
|
476
|
+
})
|
|
477
|
+
}
|
|
405
478
|
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
479
|
+
export function formatHelpText(customCommands = []) {
|
|
480
|
+
const sections = [
|
|
481
|
+
'QuickForge command reference',
|
|
482
|
+
'',
|
|
483
|
+
'Built-in commands:',
|
|
484
|
+
'',
|
|
485
|
+
...formatBuiltinCommandRows(),
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
if (customCommands.length > 0) {
|
|
489
|
+
sections.push('', formatCommandList(customCommands))
|
|
490
|
+
} else {
|
|
491
|
+
sections.push(
|
|
492
|
+
'',
|
|
493
|
+
'No custom commands found. Add Markdown files under `~/.quickforge/commands/` (user-level) or `.claude/commands/`, `.opencode/commands/`, `.ai/commands/` (project-level).',
|
|
494
|
+
)
|
|
416
495
|
}
|
|
417
496
|
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
497
|
+
return sections.join('\n')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function createCommandTemplate(workspaceRoot, name) {
|
|
501
|
+
const result = await createCommandFile(workspaceRoot, name)
|
|
502
|
+
if (result.ok) {
|
|
503
|
+
return [
|
|
504
|
+
`Created custom command: ${result.relativePath}`,
|
|
505
|
+
'',
|
|
506
|
+
`Run it with: /${result.name} your arguments`,
|
|
507
|
+
].join('\n')
|
|
508
|
+
}
|
|
509
|
+
if (result.reason === 'exists') {
|
|
510
|
+
return `Custom command already exists: ${commandsRelativeDir}/${result.name}.md`
|
|
511
|
+
}
|
|
512
|
+
return `Invalid command name: ${name}\n\nUse lowercase letters, numbers, and hyphens.`
|
|
423
513
|
}
|
|
424
514
|
|
|
425
515
|
function commandTemplate(name) {
|
|
@@ -436,3 +526,30 @@ allow_commands: false
|
|
|
436
526
|
$ARGUMENTS
|
|
437
527
|
`
|
|
438
528
|
}
|
|
529
|
+
|
|
530
|
+
export async function createCommandFile(workspaceRoot, name) {
|
|
531
|
+
const commandName = normalizeCommandName(name)
|
|
532
|
+
if (!commandName) {
|
|
533
|
+
return { ok: false, reason: 'invalid' }
|
|
534
|
+
}
|
|
535
|
+
const dir = commandDirectory(workspaceRoot)
|
|
536
|
+
if (!dir) {
|
|
537
|
+
return { ok: false, reason: 'no-project' }
|
|
538
|
+
}
|
|
539
|
+
const file = path.join(dir, `${commandName}.md`)
|
|
540
|
+
try {
|
|
541
|
+
await fs.mkdir(dir, { recursive: true })
|
|
542
|
+
await fs.writeFile(file, commandTemplate(commandName), { encoding: 'utf8', flag: 'wx' })
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error?.code === 'EEXIST') {
|
|
545
|
+
return { ok: false, reason: 'exists', name: commandName, filePath: file }
|
|
546
|
+
}
|
|
547
|
+
throw error
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
ok: true,
|
|
551
|
+
name: commandName,
|
|
552
|
+
filePath: file,
|
|
553
|
+
relativePath: `${commandsRelativeDir}/${commandName}.md`,
|
|
554
|
+
}
|
|
555
|
+
}
|
package/server/mcp/registry.mjs
CHANGED
|
@@ -231,6 +231,46 @@ export async function refreshMcpConnections() {
|
|
|
231
231
|
return refreshPromise
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
export async function reconnectMcpServer(name) {
|
|
235
|
+
const normalizedName = String(name || '').trim()
|
|
236
|
+
// close any existing connection first (bypasses retry backoff)
|
|
237
|
+
const existing = connections.get(normalizedName)
|
|
238
|
+
if (existing) {
|
|
239
|
+
connections.delete(normalizedName)
|
|
240
|
+
await closeConnection(existing)
|
|
241
|
+
}
|
|
242
|
+
const servers = await readMcpServers()
|
|
243
|
+
const config = servers.find((server) => server.name === normalizedName)
|
|
244
|
+
if (!config) {
|
|
245
|
+
const error = new Error(`MCP server not found: ${normalizedName}`)
|
|
246
|
+
error.statusCode = 404
|
|
247
|
+
throw error
|
|
248
|
+
}
|
|
249
|
+
if (!config.enabled) {
|
|
250
|
+
const error = new Error(`MCP server is disabled: ${normalizedName}`)
|
|
251
|
+
error.statusCode = 400
|
|
252
|
+
throw error
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const connection = await connectServer(config)
|
|
256
|
+
connections.set(config.name, connection)
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logger.error(`Failed to reconnect MCP server ${config.name}:`, error)
|
|
259
|
+
connections.set(config.name, {
|
|
260
|
+
config,
|
|
261
|
+
client: null,
|
|
262
|
+
transport: null,
|
|
263
|
+
status: 'error',
|
|
264
|
+
error: error?.message || 'Failed to connect MCP server',
|
|
265
|
+
tools: [],
|
|
266
|
+
connectedAt: null,
|
|
267
|
+
stderr: '',
|
|
268
|
+
lastAttemptAt: Date.now(),
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
return connections
|
|
272
|
+
}
|
|
273
|
+
|
|
234
274
|
export async function getMcpStatus() {
|
|
235
275
|
await refreshMcpConnections()
|
|
236
276
|
const servers = await readMcpServers()
|
package/server/routes/agent.mjs
CHANGED
|
@@ -83,7 +83,7 @@ export async function handleAgentApi(req, res, url) {
|
|
|
83
83
|
error.statusCode = 400
|
|
84
84
|
throw error
|
|
85
85
|
}
|
|
86
|
-
const result = await runPrompt(sessionId, message, body?.selectedCapabilities)
|
|
86
|
+
const result = await runPrompt(sessionId, message, body?.selectedCapabilities, body?.command)
|
|
87
87
|
sendJson(res, 200, result)
|
|
88
88
|
return
|
|
89
89
|
}
|
package/server/routes/mcp.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sendJson, readJsonBody } from '../utils/response.mjs'
|
|
2
2
|
import { refreshAllSessionTools } from '../agent-manager.mjs'
|
|
3
3
|
import { deleteMcpServer, normalizeMcpServers, readMcpServers, setMcpServerEnabled, upsertMcpServer, writeMcpServers } from '../mcp/config.mjs'
|
|
4
|
-
import { getMcpStatus, refreshMcpConnections } from '../mcp/registry.mjs'
|
|
4
|
+
import { getMcpStatus, reconnectMcpServer, refreshMcpConnections } from '../mcp/registry.mjs'
|
|
5
5
|
|
|
6
6
|
async function refreshMcpAndAgentTools() {
|
|
7
7
|
await refreshMcpConnections()
|
|
@@ -54,6 +54,12 @@ export async function handleMcpApi(req, res, url) {
|
|
|
54
54
|
return
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
if (req.method === 'POST' && parts[0] === 'api' && parts[1] === 'mcp' && parts[2] === 'reconnect' && parts[3]) {
|
|
58
|
+
await reconnectMcpServer(decodeURIComponent(parts[3]))
|
|
59
|
+
sendJson(res, 200, await refreshMcpAndAgentTools())
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
if (req.method === 'DELETE' && parts[0] === 'api' && parts[1] === 'mcp' && parts[2] === 'servers' && parts[3]) {
|
|
58
64
|
await deleteMcpServer(decodeURIComponent(parts[3]))
|
|
59
65
|
sendJson(res, 200, await refreshMcpAndAgentTools())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
2
|
import { getActiveProject, setActiveProjectPath, readProjectConfig } from '../project-config.mjs'
|
|
3
|
-
import { listProjectCommands } from '../custom-commands.mjs'
|
|
3
|
+
import { listProjectCommands, createCommandFile } from '../custom-commands.mjs'
|
|
4
4
|
import { atomicProjectConfigUpdate } from '../storage.mjs'
|
|
5
5
|
import { getWorkspaceRoot, setWorkspaceRoot } from '../utils/workspace.mjs'
|
|
6
6
|
import { selectDirectoryDialog, openPathInFileManager } from '../utils/platform.mjs'
|
|
@@ -20,7 +20,7 @@ export async function handleProjectApi(req, res, url) {
|
|
|
20
20
|
const project = projectId
|
|
21
21
|
? config.projects.find((item) => item.id === projectId)
|
|
22
22
|
: getActiveProject(config)
|
|
23
|
-
const commands =
|
|
23
|
+
const commands = await listProjectCommands(project?.path, project?.commandDir)
|
|
24
24
|
sendJson(res, 200, {
|
|
25
25
|
commands: commands.map((command) => ({
|
|
26
26
|
name: command.name,
|
|
@@ -29,6 +29,7 @@ export async function handleProjectApi(req, res, url) {
|
|
|
29
29
|
allowEdit: command.allowEdit,
|
|
30
30
|
allowCommands: command.allowCommands,
|
|
31
31
|
relativePath: command.relativePath,
|
|
32
|
+
filePath: command.filePath,
|
|
32
33
|
source: command.source,
|
|
33
34
|
pluginName: command.pluginName,
|
|
34
35
|
})),
|
|
@@ -107,6 +108,35 @@ export async function handleProjectApi(req, res, url) {
|
|
|
107
108
|
return
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
if (req.method === 'POST' && url.pathname === '/api/project/open-path') {
|
|
112
|
+
const body = await readJsonBody(req)
|
|
113
|
+
const active = body?.projectId
|
|
114
|
+
? config.projects.find((project) => project.id === body.projectId) ?? getActiveProject(config)
|
|
115
|
+
: getActiveProject(config)
|
|
116
|
+
const target = String(body?.path || '')
|
|
117
|
+
const resolved = target && path.isAbsolute(target)
|
|
118
|
+
? path.resolve(target)
|
|
119
|
+
: path.resolve(active?.path || '', target)
|
|
120
|
+
await openPathInFileManager(resolved)
|
|
121
|
+
sendJson(res, 200, { ok: true })
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (req.method === 'POST' && url.pathname === '/api/project/command') {
|
|
126
|
+
const body = await readJsonBody(req)
|
|
127
|
+
const active = body?.projectId
|
|
128
|
+
? config.projects.find((project) => project.id === body.projectId) ?? getActiveProject(config)
|
|
129
|
+
: getActiveProject(config)
|
|
130
|
+
if (!active?.path) {
|
|
131
|
+
const error = new Error('No active project')
|
|
132
|
+
error.statusCode = 400
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
const result = await createCommandFile(active.path, body?.name)
|
|
136
|
+
sendJson(res, 200, result)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
if (req.method === 'DELETE' && url.pathname.startsWith('/api/project/')) {
|
|
111
141
|
const id = decodeSegment(url.pathname.split('/').filter(Boolean)[2])
|
|
112
142
|
const updated = await atomicProjectConfigUpdate((cfg) => {
|
|
@@ -363,7 +363,7 @@ export async function handleSharedConversationApi(req, res, url) {
|
|
|
363
363
|
assertOperate(record)
|
|
364
364
|
const body = await readJsonBody(req)
|
|
365
365
|
await restoreAgent(record.sessionId)
|
|
366
|
-
const result = await runPrompt(record.sessionId, messageFromBody(body, record, req))
|
|
366
|
+
const result = await runPrompt(record.sessionId, messageFromBody(body, record, req), [], body?.command)
|
|
367
367
|
sendJson(res, 200, result)
|
|
368
368
|
return
|
|
369
369
|
}
|
package/server/storage.mjs
CHANGED
|
@@ -94,6 +94,7 @@ export const configDir = path.join(dataDir, 'config')
|
|
|
94
94
|
export const storageDir = path.join(dataDir, 'storage')
|
|
95
95
|
export const cacheDir = path.join(dataDir, 'cache')
|
|
96
96
|
export const logsDir = path.join(dataDir, 'logs')
|
|
97
|
+
export const userCommandsDir = path.join(dataDir, 'commands')
|
|
97
98
|
|
|
98
99
|
const quickForgeConfigFile = path.join(configDir, 'config.json')
|
|
99
100
|
const configMigrationMarkerFile = path.join(configDir, '.layout-migrated')
|
package/server/subagents.mjs
CHANGED
|
@@ -21,23 +21,23 @@ export const subagentDefinitions = [
|
|
|
21
21
|
name: 'general',
|
|
22
22
|
label: 'General',
|
|
23
23
|
mode: 'subagent',
|
|
24
|
-
description: 'A general-purpose agent for
|
|
24
|
+
description: 'A general-purpose agent for bounded complex multi-step implementation or broader independent work. It has full built-in workspace tool access, excluding MCP tools and Agent Skills, so it can modify files when needed. Prefer Explore for focused read-only repository discovery, source search, call-chain lookup, tests/docs discovery, and impact analysis.',
|
|
25
25
|
allowedTools: ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command'],
|
|
26
26
|
allowFileMutations: true,
|
|
27
27
|
maxRuntimeMs: 30 * 60 * 1000,
|
|
28
28
|
maxToolCalls: 300,
|
|
29
|
-
systemPrompt: `You are General, a general-purpose subagent for complex
|
|
29
|
+
systemPrompt: `You are General, a general-purpose subagent for bounded complex multi-step implementation tasks and broader independent work. You may inspect, edit, write files, and run commands using the built-in workspace tools when needed. You do not have MCP tools or Agent Skills. Prefer Explore for focused read-only repository discovery, source search, call-chain lookup, tests/docs discovery, and impact analysis. Make focused, minimal changes that satisfy the delegated task, and verify your changes when appropriate.`,
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
name: 'explore',
|
|
33
33
|
label: 'Explore',
|
|
34
34
|
mode: 'subagent',
|
|
35
|
-
description: '
|
|
36
|
-
allowedTools: ['read_file', 'grep_files'],
|
|
35
|
+
description: 'The preferred subagent for focused read-only repository exploration, file discovery, source search, call-chain lookup, related tests/docs/wiki discovery, safe inspection commands, pattern lookup, and impact analysis before non-trivial implementation. It cannot modify files.',
|
|
36
|
+
allowedTools: ['read_file', 'grep_files', 'run_command'],
|
|
37
37
|
allowFileMutations: false,
|
|
38
38
|
maxRuntimeMs: 30 * 60 * 1000,
|
|
39
39
|
maxToolCalls: 300,
|
|
40
|
-
systemPrompt: `You are Explore,
|
|
40
|
+
systemPrompt: `You are Explore, the preferred read-only repository exploration subagent. Use read_file, grep_files, and safe read-only run_command calls to locate files, inspect project structure, search source, trace call chains, find related tests/docs/wiki pages, run diagnostics, identify patterns, assess impact, and answer focused questions before non-trivial implementation. You cannot modify files.`,
|
|
41
41
|
},
|
|
42
42
|
]
|
|
43
43
|
|
|
@@ -80,7 +80,9 @@ export function composeSubagentSystemPrompt({ definition, parentSystemPrompt, pr
|
|
|
80
80
|
'- run_subagent is not available to subagents.',
|
|
81
81
|
definition.allowFileMutations
|
|
82
82
|
? '- File modification tools are available when needed, subject to the parent session approval/YOLO policy.'
|
|
83
|
-
:
|
|
83
|
+
: definition.allowedTools.includes('run_command')
|
|
84
|
+
? '- This subagent is read-only. Do not modify files. Use run_command only for safe inspection or diagnostic commands.'
|
|
85
|
+
: '- This subagent is read-only. Do not modify files or run commands.',
|
|
84
86
|
workspaceLines.length ? `\nWorkspace context:\n${workspaceLines.join('\n')}` : '',
|
|
85
87
|
'</subagent_instructions>',
|
|
86
88
|
].filter(Boolean).join('\n')
|
package/server/system-prompt.mjs
CHANGED
|
@@ -8,7 +8,7 @@ For project tasks:
|
|
|
8
8
|
- For multi-step work, use a brief plan.
|
|
9
9
|
- Before changing files, gather sufficient context: relevant files, entry points or call chains, existing patterns, tests or validation commands, and docs/wiki impact.
|
|
10
10
|
- Before taking action, confirm with the user.
|
|
11
|
-
- Unless the change is trivial and localized to an already-known file, use Explore first for read-only repository research; prefer Explore for broad searches, pattern lookup, impact analysis, and locating related tests, docs, or build scripts.
|
|
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.
|
|
12
12
|
- For complex multi-step work, use General only for bounded assistance; the parent assistant remains responsible for final decisions, minimal edits, and verification.
|
|
13
13
|
- Make minimal, focused changes.
|
|
14
14
|
- Prefer dedicated workspace tools for reading, editing, and searching files.
|
|
@@ -63,7 +63,7 @@ function appendSubagentCatalog(parts, subagents) {
|
|
|
63
63
|
const subagentParts = subagents.map(formatSubagentCatalogItem)
|
|
64
64
|
parts.push(`
|
|
65
65
|
<available_subagents>
|
|
66
|
-
The run_subagent tool can delegate work to an enabled temporary Agent Profile.
|
|
66
|
+
The run_subagent tool can delegate work to an enabled temporary Agent Profile. Prefer Explore for focused read-only repository discovery before implementation decisions, including locating files, searching source, tracing call chains, finding related tests/docs/wiki pages, and impact analysis. Use General for bounded complex multi-step implementation or broader independent work; custom profiles may also be available when enabled.
|
|
67
67
|
|
|
68
68
|
Choose the most appropriate subagent by name, keep delegation concrete, and include relevant context. Treat subagent output as advisory; you remain responsible for the final answer.
|
|
69
69
|
|
|
@@ -15,7 +15,7 @@ import { loadSelectedGlobalSkills, loadSelectedProjectSkills, mergeSkills } from
|
|
|
15
15
|
export const subagentTool = {
|
|
16
16
|
name: 'run_subagent',
|
|
17
17
|
label: 'Run subagent',
|
|
18
|
-
description: 'Delegate a bounded task to an enabled temporary Agent Profile.
|
|
18
|
+
description: 'Delegate a bounded task to an enabled temporary Agent Profile. Prefer explore for focused read-only repository discovery before implementation decisions, including locating files, searching source, tracing call chains, finding related tests/docs/wiki pages, and impact analysis. Use general for bounded complex multi-step implementation or broader independent work. Custom profiles can also be enabled as subagents. Subagents are short-lived and do not receive MCP or Agent Skill tools.',
|
|
19
19
|
parameters: Type.Object({
|
|
20
20
|
subagent: Type.String({ description: 'Agent Profile name to invoke.' }),
|
|
21
21
|
task: Type.String({ description: 'Concrete, bounded task for the subagent. Do not delegate vague or open-ended work.' }),
|