@shawnstack/quickforge 1.3.21 → 1.3.22

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 (78) hide show
  1. package/README.md +10 -10
  2. package/dist/assets/{anthropic--qj3xmqE.js → anthropic-CDKnv1FQ.js} +1 -1
  3. package/dist/assets/{azure-openai-responses-DDRaS-MZ.js → azure-openai-responses-BnUwVl-8.js} +1 -1
  4. package/dist/assets/{google-C-8-FIZS.js → google-DOEyCDZy.js} +1 -1
  5. package/dist/assets/{google-vertex-Dw2y_nqS.js → google-vertex-BPPf3car.js} +1 -1
  6. package/dist/assets/{icons-BHkxP7oT.js → icons-WD3UkVNM.js} +1 -1
  7. package/dist/assets/{index-DRGbHzkd.js → index-CjTN0qaQ.js} +586 -554
  8. package/dist/assets/index-eeLjaV06.css +3 -0
  9. package/dist/assets/{mistral-u_5S4wj6.js → mistral-Ber29mja.js} +1 -1
  10. package/dist/assets/{openai-codex-responses-CWZGpchs.js → openai-codex-responses-D8gq8a3l.js} +1 -1
  11. package/dist/assets/{openai-completions-C_DdwPuH.js → openai-completions-CATWPFBp.js} +1 -1
  12. package/dist/assets/{openai-responses-CMp0ziUV.js → openai-responses-DxcB6Ksu.js} +1 -1
  13. package/dist/assets/{openai-responses-shared-CORWeerT.js → openai-responses-shared-a_PAPxTO.js} +1 -1
  14. package/dist/assets/{react-vendor-CmyL2roG.js → react-vendor-BcQaTQ90.js} +1 -1
  15. package/dist/index.html +4 -4
  16. package/node_modules/@aws-sdk/client-bedrock-runtime/dist-cjs/index.js +1 -0
  17. package/node_modules/@aws-sdk/client-bedrock-runtime/dist-es/models/enums.js +1 -0
  18. package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +11 -11
  19. package/node_modules/@aws-sdk/core/package.json +3 -3
  20. package/node_modules/@aws-sdk/credential-provider-env/package.json +3 -3
  21. package/node_modules/@aws-sdk/credential-provider-http/package.json +5 -5
  22. package/node_modules/@aws-sdk/credential-provider-ini/package.json +11 -11
  23. package/node_modules/@aws-sdk/credential-provider-login/package.json +4 -4
  24. package/node_modules/@aws-sdk/credential-provider-node/package.json +9 -9
  25. package/node_modules/@aws-sdk/credential-provider-process/package.json +3 -3
  26. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +4 -4
  27. package/node_modules/@aws-sdk/credential-provider-sso/package.json +5 -5
  28. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +4 -4
  29. package/node_modules/@aws-sdk/eventstream-handler-node/package.json +2 -2
  30. package/node_modules/@aws-sdk/middleware-eventstream/package.json +2 -2
  31. package/node_modules/@aws-sdk/middleware-websocket/package.json +5 -5
  32. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
  33. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
  34. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
  35. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
  36. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
  37. package/node_modules/@aws-sdk/nested-clients/package.json +6 -6
  38. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +2 -2
  39. package/node_modules/@aws-sdk/token-providers/package.json +4 -4
  40. package/node_modules/@nodable/entities/package.json +1 -1
  41. package/node_modules/@nodable/entities/src/EntityDecoder.js +1 -1
  42. package/node_modules/@nodable/entities/src/entities.js +0 -18
  43. package/node_modules/@smithy/credential-provider-imds/dist-cjs/index.js +14 -13
  44. package/node_modules/@smithy/credential-provider-imds/dist-es/fromContainerMetadata.js +14 -13
  45. package/node_modules/@smithy/credential-provider-imds/package.json +1 -1
  46. package/node_modules/hasown/CHANGELOG.md +7 -0
  47. package/node_modules/hasown/package.json +4 -5
  48. package/node_modules/protobufjs/dist/light/protobuf.js +7 -5
  49. package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
  50. package/node_modules/protobufjs/dist/minimal/protobuf.js +3 -3
  51. package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
  52. package/node_modules/protobufjs/dist/protobuf.js +7 -5
  53. package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
  54. package/node_modules/protobufjs/package.json +1 -1
  55. package/node_modules/protobufjs/src/converter.js +4 -2
  56. package/node_modules/protobufjs/src/roots.js +1 -1
  57. package/node_modules/typebox/build/type/script/mapping.mjs +15 -8
  58. package/node_modules/typebox/build/type/script/parser.mjs +2 -1
  59. package/node_modules/typebox/package.json +29 -29
  60. package/package.json +1 -1
  61. package/server/agent-manager.mjs +63 -25
  62. package/server/agent-profiles.mjs +191 -0
  63. package/server/auto-compaction.mjs +20 -0
  64. package/server/index.mjs +32 -8
  65. package/server/mcp/registry.mjs +13 -2
  66. package/server/routes/agent-profiles.mjs +172 -0
  67. package/server/routes/lan-access.mjs +20 -0
  68. package/server/routes/scheduled-tasks.mjs +161 -47
  69. package/server/routes/shared-conversation.mjs +14 -0
  70. package/server/routes/storage.mjs +10 -0
  71. package/server/routes/terminal.mjs +13 -3
  72. package/server/session-utils.mjs +2 -2
  73. package/server/storage.mjs +3 -4
  74. package/server/system-prompt.mjs +10 -5
  75. package/server/terminal/terminal-manager.mjs +9 -1
  76. package/server/tools/definitions.mjs +2 -7
  77. package/server/utils/response.mjs +4 -0
  78. package/dist/assets/index-B-WkttzD.css +0 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typebox",
3
3
  "description": "Json Schema Type Builder with Static Type Resolution for TypeScript",
4
- "version": "1.1.38",
4
+ "version": "1.1.39",
5
5
  "keywords": [
6
6
  "typescript",
7
7
  "jsonschema"
@@ -16,22 +16,6 @@
16
16
  "types": "./build/index.d.mts",
17
17
  "module": "./build/index.mjs",
18
18
  "exports": {
19
- "./guard": {
20
- "import": "./build/guard/index.mjs",
21
- "default": "./build/guard/index.mjs"
22
- },
23
- "./error": {
24
- "import": "./build/error/index.mjs",
25
- "default": "./build/error/index.mjs"
26
- },
27
- "./compile": {
28
- "import": "./build/compile/index.mjs",
29
- "default": "./build/compile/index.mjs"
30
- },
31
- "./system": {
32
- "import": "./build/system/index.mjs",
33
- "default": "./build/system/index.mjs"
34
- },
35
19
  "./format": {
36
20
  "import": "./build/format/index.mjs",
37
21
  "default": "./build/format/index.mjs"
@@ -44,10 +28,26 @@
44
28
  "import": "./build/schema/index.mjs",
45
29
  "default": "./build/schema/index.mjs"
46
30
  },
31
+ "./compile": {
32
+ "import": "./build/compile/index.mjs",
33
+ "default": "./build/compile/index.mjs"
34
+ },
47
35
  "./value": {
48
36
  "import": "./build/value/index.mjs",
49
37
  "default": "./build/value/index.mjs"
50
38
  },
39
+ "./guard": {
40
+ "import": "./build/guard/index.mjs",
41
+ "default": "./build/guard/index.mjs"
42
+ },
43
+ "./system": {
44
+ "import": "./build/system/index.mjs",
45
+ "default": "./build/system/index.mjs"
46
+ },
47
+ "./error": {
48
+ "import": "./build/error/index.mjs",
49
+ "default": "./build/error/index.mjs"
50
+ },
51
51
  ".": {
52
52
  "import": "./build/index.mjs",
53
53
  "default": "./build/index.mjs"
@@ -55,18 +55,6 @@
55
55
  },
56
56
  "typesVersions": {
57
57
  "*": {
58
- "guard": [
59
- "./build/guard/index.d.mts"
60
- ],
61
- "error": [
62
- "./build/error/index.d.mts"
63
- ],
64
- "compile": [
65
- "./build/compile/index.d.mts"
66
- ],
67
- "system": [
68
- "./build/system/index.d.mts"
69
- ],
70
58
  "format": [
71
59
  "./build/format/index.d.mts"
72
60
  ],
@@ -76,9 +64,21 @@
76
64
  "schema": [
77
65
  "./build/schema/index.d.mts"
78
66
  ],
67
+ "compile": [
68
+ "./build/compile/index.d.mts"
69
+ ],
79
70
  "value": [
80
71
  "./build/value/index.d.mts"
81
72
  ],
73
+ "guard": [
74
+ "./build/guard/index.d.mts"
75
+ ],
76
+ "system": [
77
+ "./build/system/index.d.mts"
78
+ ],
79
+ "error": [
80
+ "./build/error/index.d.mts"
81
+ ],
82
82
  ".": [
83
83
  "./build/index.d.mts"
84
84
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shawnstack/quickforge",
3
- "version": "1.3.21",
3
+ "version": "1.3.22",
4
4
  "description": "AI chat application with YOLO-mode local workspace tools. React + Vite + Tailwind CSS frontend, local Node.js storage server.",
5
5
  "keywords": [
6
6
  "ai",
@@ -8,8 +8,8 @@ import { callMcpTool, createMcpToolDefinitions, isMcpToolName } from './mcp/regi
8
8
  import {
9
9
  composeSubagentSystemPrompt,
10
10
  formatSubagentTask,
11
- getSubagentDefinition,
12
11
  } from './subagents.mjs'
12
+ import { agentProfileSnapshot, getAgentProfile } from './agent-profiles.mjs'
13
13
  import { projectContextFromId, readProjectConfig } from './project-config.mjs'
14
14
  import { readStore, atomicUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
15
15
  import { logger } from './utils/logger.mjs'
@@ -22,6 +22,7 @@ import {
22
22
  } from './conversation-compaction.mjs'
23
23
  import {
24
24
  buildAutoCompactLoopMessages,
25
+ estimateSessionContextUsage,
25
26
  maybeAutoCompactSession,
26
27
  } from './auto-compaction.mjs'
27
28
  import {
@@ -366,8 +367,12 @@ function estimateTokenReduction(originalChars, finalChars) {
366
367
  }
367
368
 
368
369
  function emitSessionEvent(session, event) {
369
- session.eventBus.emit('agent_event', event)
370
- agentEvents.emit('agent_event', { sessionId: session.sessionId, ...event })
370
+ const enrichedEvent = (event?.type === 'message_end' || event?.type === 'agent_end' || event?.type === 'messages_replaced' || event?.type === 'auto_compact_completed')
371
+ && event.contextUsage === undefined
372
+ ? { ...event, contextUsage: getSessionContextUsage(session) }
373
+ : event
374
+ session.eventBus.emit('agent_event', enrichedEvent)
375
+ agentEvents.emit('agent_event', { sessionId: session.sessionId, ...enrichedEvent })
371
376
  }
372
377
 
373
378
  function addToolTimingToEvent(session, event) {
@@ -681,12 +686,13 @@ function lastAssistantText(messages) {
681
686
  }
682
687
 
683
688
  async function runSubagent(parentSession, params, parentSignal, onUpdate) {
684
- const definition = getSubagentDefinition(params?.subagent)
685
- if (!definition) {
686
- const error = new Error(`Unknown subagent: ${params?.subagent || ''}`)
689
+ const profile = await getAgentProfile(params?.subagent)
690
+ if (!profile || !profile.enabledAsSubagent) {
691
+ const error = new Error(`Unknown or disabled subagent: ${params?.subagent || ''}`)
687
692
  error.statusCode = 400
688
693
  throw error
689
694
  }
695
+ const definition = profile
690
696
 
691
697
  const task = String(params?.task || '').trim()
692
698
  if (!task) {
@@ -774,7 +780,7 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
774
780
  const toolName = context.toolCall?.name
775
781
  toolCalls += 1
776
782
  emitSubagentTrace()
777
- if (toolCalls > Number(definition.maxToolCalls || 12)) {
783
+ if (toolCalls > Number(definition.maxToolCalls || 300)) {
778
784
  return { block: true, reason: `Subagent ${definition.name} exceeded its tool-call budget.` }
779
785
  }
780
786
  if (!definition.allowedTools.includes(toolName)) {
@@ -954,6 +960,7 @@ export async function createAgent(sessionId, config = {}) {
954
960
  createdAt = new Date().toISOString(),
955
961
  lastModified = null,
956
962
  contextCompaction = null,
963
+ agentProfile = null,
957
964
  } = config
958
965
 
959
966
  // Resolve project context for tool calls
@@ -975,7 +982,8 @@ export async function createAgent(sessionId, config = {}) {
975
982
  globalSkillNames: projectConfig.globalSkills,
976
983
  projectSkillNames: configuredProject?.skills,
977
984
  }
978
- const resolvedSystemPrompt = systemPrompt ?? (await buildSystemPrompt(projectId))
985
+ const profileSystemPrompt = agentProfile?.systemPrompt ? `\n\n<agent_profile_instructions>\nAgent Profile: ${agentProfile.label || agentProfile.name}\n${agentProfile.systemPrompt}\n</agent_profile_instructions>` : ''
986
+ const resolvedSystemPrompt = systemPrompt ?? `${await buildSystemPrompt(projectId)}${profileSystemPrompt}`
979
987
 
980
988
  // Resolve model
981
989
  let resolvedModel = model
@@ -991,16 +999,25 @@ export async function createAgent(sessionId, config = {}) {
991
999
  }
992
1000
 
993
1001
  // Build skills tools for enabled skills, plus workspace tools when a project is available.
1002
+ const profileToolNames = Array.isArray(agentProfile?.allowedTools) ? agentProfile.allowedTools : null
994
1003
  const tools = await createServerTools(
995
1004
  projectId,
996
1005
  projectContext,
997
1006
  skillsContext,
998
1007
  !!(projectId && projectContext),
999
1008
  (toolName) => {
1009
+ if (profileToolNames && !profileToolNames.includes(toolName)) return `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.`
1000
1010
  const session = agentSessions.get(sessionId)
1001
1011
  return session ? createCommandToolPermissions(session)(toolName) : null
1002
1012
  },
1003
- { parentSessionId: sessionId },
1013
+ agentProfile
1014
+ ? {
1015
+ allowedToolNames: profileToolNames,
1016
+ includeSubagentTool: false,
1017
+ includeMcpTools: false,
1018
+ parentSessionId: sessionId,
1019
+ }
1020
+ : { parentSessionId: sessionId },
1004
1021
  )
1005
1022
 
1006
1023
  // Resolve API key
@@ -1013,6 +1030,7 @@ export async function createAgent(sessionId, config = {}) {
1013
1030
  }
1014
1031
  }
1015
1032
 
1033
+ let session
1016
1034
  const agent = new Agent({
1017
1035
  initialState: {
1018
1036
  systemPrompt: resolvedSystemPrompt,
@@ -1035,6 +1053,7 @@ export async function createAgent(sessionId, config = {}) {
1035
1053
  const isSkillTool = toolName === 'activate_skill' || toolName === 'read_skill_resource'
1036
1054
  if (isSkillTool) return undefined
1037
1055
  const currentSession = agentSessions.get(sessionId)
1056
+ if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
1038
1057
  if (toolName === 'run_subagent') return undefined
1039
1058
  if (isMcpToolName(toolName)) {
1040
1059
  if (!currentSession?.yoloMode) return createApprovalPromise(currentSession, toolCallId, toolName, context.args)
@@ -1055,7 +1074,7 @@ export async function createAgent(sessionId, config = {}) {
1055
1074
  const eventBus = new EventEmitter()
1056
1075
  eventBus.setMaxListeners(100)
1057
1076
 
1058
- const session = {
1077
+ session = {
1059
1078
  sessionId,
1060
1079
  agent,
1061
1080
  projectContext,
@@ -1081,6 +1100,7 @@ export async function createAgent(sessionId, config = {}) {
1081
1100
  toolTimings: new Map(),
1082
1101
  getApiKey,
1083
1102
  contextCompaction,
1103
+ agentProfile: agentProfile ? agentProfileSnapshot(agentProfile) : null,
1084
1104
  lastTransformedContextMessages: null,
1085
1105
  autoCompacting: false,
1086
1106
  lastAutoCompactAt: null,
@@ -1102,8 +1122,7 @@ export async function createAgent(sessionId, config = {}) {
1102
1122
  : timedEvent
1103
1123
 
1104
1124
  // Forward all events to the session event bus and the global bus.
1105
- eventBus.emit('agent_event', forwardEvent)
1106
- agentEvents.emit('agent_event', { sessionId, ...forwardEvent })
1125
+ emitSessionEvent(session, forwardEvent)
1107
1126
 
1108
1127
  // Track status
1109
1128
  if (event.type === 'agent_start') {
@@ -1263,7 +1282,10 @@ async function persistSession(session) {
1263
1282
  try {
1264
1283
  await writeSessionValue(sessionId, sessionData)
1265
1284
  await atomicUpdate('sessions-metadata', (data) => {
1266
- data[sessionId] = metadata
1285
+ data[sessionId] = {
1286
+ ...metadata,
1287
+ pinnedAt: data[sessionId]?.pinnedAt,
1288
+ }
1267
1289
  return data
1268
1290
  })
1269
1291
  } catch (err) {
@@ -1320,6 +1342,8 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
1320
1342
  reason: 'rollback',
1321
1343
  rollbackIndex,
1322
1344
  messages: session.agent.state.messages,
1345
+ contextCompaction: session.contextCompaction,
1346
+ contextUsage: getSessionContextUsage(session),
1323
1347
  }
1324
1348
  emitSessionEvent(session, replacedEvent)
1325
1349
  emitSessionEvent(session, { type: 'message_end', messages: session.agent.state.messages })
@@ -1340,8 +1364,9 @@ export async function replaceSessionMessages(sessionId, messages) {
1340
1364
  session.finishedAt = new Date().toISOString()
1341
1365
  await persistSession(session)
1342
1366
  const nextMessages = session.agent.state.messages
1343
- emitSessionEvent(session, { type: 'message_end', messages: nextMessages })
1344
- emitSessionEvent(session, { type: 'agent_end', messages: nextMessages })
1367
+ const contextUsage = getSessionContextUsage(session)
1368
+ emitSessionEvent(session, { type: 'message_end', messages: nextMessages, contextUsage })
1369
+ emitSessionEvent(session, { type: 'agent_end', messages: nextMessages, contextUsage })
1345
1370
  return getSessionState(sessionId)
1346
1371
  }
1347
1372
 
@@ -1375,10 +1400,8 @@ export async function runPrompt(sessionId, message) {
1375
1400
  ]
1376
1401
  await persistSession(session)
1377
1402
  const messages = session.agent.state.messages
1378
- session.eventBus.emit('agent_event', { type: 'message_end', messages })
1379
- session.eventBus.emit('agent_event', { type: 'agent_end', messages })
1380
- agentEvents.emit('agent_event', { sessionId, type: 'message_end', messages })
1381
- agentEvents.emit('agent_event', { sessionId, type: 'agent_end', messages })
1403
+ emitSessionEvent(session, { type: 'message_end', messages })
1404
+ emitSessionEvent(session, { type: 'agent_end', messages })
1382
1405
  return { sessionId, status: session.status }
1383
1406
  }
1384
1407
 
@@ -1403,8 +1426,7 @@ export async function runPrompt(sessionId, message) {
1403
1426
  if (aiTitle && aiTitle !== 'New chat') {
1404
1427
  session.title = aiTitle
1405
1428
  await persistSession(session)
1406
- session.eventBus.emit('agent_event', { type: 'title_updated', title: aiTitle })
1407
- agentEvents.emit('agent_event', { sessionId, type: 'title_updated', title: aiTitle })
1429
+ emitSessionEvent(session, { type: 'title_updated', title: aiTitle })
1408
1430
  }
1409
1431
  }).catch((err) => {
1410
1432
  logger.warn(`Title generation failed for session ${sessionId}:`, err.message || err, { sessionId })
@@ -1422,8 +1444,7 @@ export async function runPrompt(sessionId, message) {
1422
1444
  type: 'error',
1423
1445
  error: err.message || 'Unknown error',
1424
1446
  }
1425
- session.eventBus.emit('agent_event', event)
1426
- agentEvents.emit('agent_event', { sessionId, ...event })
1447
+ emitSessionEvent(session, event)
1427
1448
  }).finally(() => {
1428
1449
  session.activeCommandName = null
1429
1450
  session.activeCommandPermissions = null
@@ -1468,8 +1489,7 @@ export async function abortRun(sessionId) {
1468
1489
  type: 'agent_end',
1469
1490
  messages: session.agent.state.messages,
1470
1491
  }
1471
- session.eventBus.emit('agent_event', event)
1472
- agentEvents.emit('agent_event', { sessionId, ...event })
1492
+ emitSessionEvent(session, event)
1473
1493
  }
1474
1494
 
1475
1495
  return { sessionId, aborted: true }
@@ -1509,6 +1529,15 @@ export function followUpAgent(sessionId, message) {
1509
1529
  return { sessionId, followUp: true }
1510
1530
  }
1511
1531
 
1532
+ function getSessionContextUsage(session) {
1533
+ try {
1534
+ return estimateSessionContextUsage(session)
1535
+ } catch (error) {
1536
+ logger.warn(`Failed to estimate context usage for session ${session?.sessionId}:`, error?.message || error, { sessionId: session?.sessionId })
1537
+ return null
1538
+ }
1539
+ }
1540
+
1512
1541
  /**
1513
1542
  * Get the current state of a session (for page refresh recovery).
1514
1543
  */
@@ -1533,6 +1562,7 @@ export function getSessionState(sessionId) {
1533
1562
  tools: session.agent.state.tools,
1534
1563
  messages: session.agent.state.messages,
1535
1564
  contextCompaction: session.contextCompaction,
1565
+ contextUsage: getSessionContextUsage(session),
1536
1566
  isStreaming: session.agent.state.isStreaming,
1537
1567
  errorMessage: session.agent.state.errorMessage,
1538
1568
  }
@@ -1591,6 +1621,14 @@ export async function destroyAgent(sessionId) {
1591
1621
  // ignore
1592
1622
  }
1593
1623
 
1624
+ // Clean up any pending approvals for this session before removing it.
1625
+ for (const [toolCallId, approval] of pendingApprovals) {
1626
+ if (approval.sessionId === sessionId) approval.reject(new Error('Session destroyed'))
1627
+ }
1628
+ for (const [approvalId, approval] of pendingAutoCompactApprovals) {
1629
+ if (approval.sessionId === sessionId) approval.reject(new Error('Session destroyed'))
1630
+ }
1631
+
1594
1632
  // Final persist (empty sessions are cleaned up by persistSession)
1595
1633
  try {
1596
1634
  await persistSession(session)
@@ -0,0 +1,191 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { readStore, atomicUpdate } from './storage.mjs'
3
+ import { subagentDefinitions } from './subagents.mjs'
4
+ import { workspaceTools } from './tools/definitions.mjs'
5
+
6
+ const STORE = 'custom-agents'
7
+ const RESERVED_NAMES = new Set(subagentDefinitions.map((definition) => definition.name))
8
+ export const AGENT_PROFILE_TOOL_NAMES = ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command']
9
+ const allowedToolNames = new Set(AGENT_PROFILE_TOOL_NAMES)
10
+ const nameRegex = /^[a-z][a-z0-9_-]{1,39}$/
11
+ const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000
12
+ const DEFAULT_MAX_TOOL_CALLS = 300
13
+
14
+ function requestError(message, statusCode = 400) {
15
+ const error = new Error(message)
16
+ error.statusCode = statusCode
17
+ return error
18
+ }
19
+
20
+ function uniqueStrings(value) {
21
+ if (!Array.isArray(value)) return []
22
+ const result = []
23
+ const seen = new Set()
24
+ for (const item of value) {
25
+ const text = String(item || '').trim()
26
+ if (!text || seen.has(text)) continue
27
+ seen.add(text)
28
+ result.push(text)
29
+ }
30
+ return result
31
+ }
32
+
33
+ function normalizeOptionalPositiveInteger(value, fallback, max) {
34
+ if (value === undefined || value === null || value === '') return fallback
35
+ const parsed = Number(value)
36
+ if (!Number.isInteger(parsed) || parsed <= 0) throw requestError('maxToolCalls must be a positive integer')
37
+ return Math.min(parsed, max)
38
+ }
39
+
40
+ function normalizeOptionalRuntime(value, fallback) {
41
+ if (value === undefined || value === null || value === '') return fallback
42
+ const parsed = Number(value)
43
+ if (!Number.isFinite(parsed) || parsed <= 0) throw requestError('maxRuntimeMs must be a positive number')
44
+ return Math.min(Math.max(Math.round(parsed), 1000), DEFAULT_MAX_RUNTIME_MS)
45
+ }
46
+
47
+ function builtinProfileFromSubagent(definition) {
48
+ return {
49
+ id: definition.name,
50
+ name: definition.name,
51
+ label: definition.label || definition.name,
52
+ description: definition.description || '',
53
+ systemPrompt: definition.systemPrompt || '',
54
+ allowedTools: [...definition.allowedTools],
55
+ maxRuntimeMs: definition.maxRuntimeMs || DEFAULT_MAX_RUNTIME_MS,
56
+ maxToolCalls: definition.maxToolCalls || DEFAULT_MAX_TOOL_CALLS,
57
+ enabledAsSubagent: true,
58
+ builtin: true,
59
+ createdAt: 'builtin',
60
+ updatedAt: 'builtin',
61
+ }
62
+ }
63
+
64
+ export function listBuiltinAgentProfiles() {
65
+ return subagentDefinitions.map(builtinProfileFromSubagent)
66
+ }
67
+
68
+ function normalizeProfileInput(input, existing = null, { creating = false } = {}) {
69
+ const now = new Date().toISOString()
70
+ const name = String(input?.name ?? existing?.name ?? '').trim().toLowerCase()
71
+ if (!nameRegex.test(name)) throw requestError('name must start with a letter and contain only lowercase letters, numbers, underscores, or hyphens')
72
+ if (creating && RESERVED_NAMES.has(name)) throw requestError(`Agent name is reserved: ${name}`, 409)
73
+ if (!creating && existing?.builtin) throw requestError('Built-in agents cannot be modified', 403)
74
+
75
+ const label = String(input?.label ?? existing?.label ?? name).trim().slice(0, 80)
76
+ if (!label) throw requestError('label is required')
77
+
78
+ const allowedTools = uniqueStrings(input?.allowedTools ?? existing?.allowedTools ?? ['read_file', 'grep_files'])
79
+ if (allowedTools.length === 0) throw requestError('allowedTools must contain at least one tool')
80
+ for (const toolName of allowedTools) {
81
+ if (!allowedToolNames.has(toolName)) throw requestError(`Unsupported tool for custom agent: ${toolName}`)
82
+ }
83
+
84
+ return {
85
+ id: existing?.id || `agent-${randomUUID()}`,
86
+ name,
87
+ label,
88
+ description: String(input?.description ?? existing?.description ?? '').trim().slice(0, 500),
89
+ systemPrompt: String(input?.systemPrompt ?? existing?.systemPrompt ?? '').trim(),
90
+ allowedTools,
91
+ maxRuntimeMs: normalizeOptionalRuntime(input?.maxRuntimeMs ?? existing?.maxRuntimeMs, DEFAULT_MAX_RUNTIME_MS),
92
+ maxToolCalls: normalizeOptionalPositiveInteger(input?.maxToolCalls ?? existing?.maxToolCalls, DEFAULT_MAX_TOOL_CALLS, 300),
93
+ enabledAsSubagent: input?.enabledAsSubagent === undefined ? Boolean(existing?.enabledAsSubagent ?? true) : input.enabledAsSubagent === true,
94
+ builtin: false,
95
+ createdAt: existing?.createdAt || now,
96
+ updatedAt: now,
97
+ }
98
+ }
99
+
100
+ async function readCustomAgentMap() {
101
+ const data = await readStore(STORE)
102
+ return data && typeof data === 'object' ? data : {}
103
+ }
104
+
105
+ export async function listAgentProfiles(options = {}) {
106
+ const custom = Object.values(await readCustomAgentMap())
107
+ const profiles = [...listBuiltinAgentProfiles(), ...custom]
108
+ return options.includeDisabled ? profiles : profiles.filter((profile) => profile.enabledAsSubagent || profile.builtin || profile.enabledAsSubagent === false)
109
+ }
110
+
111
+ export async function listSubagentProfiles() {
112
+ return (await listAgentProfiles({ includeDisabled: true })).filter((profile) => profile.enabledAsSubagent)
113
+ }
114
+
115
+ export async function getAgentProfile(idOrName) {
116
+ const key = String(idOrName || '').trim().toLowerCase()
117
+ if (!key) return null
118
+ return (await listAgentProfiles({ includeDisabled: true })).find((profile) => profile.id === key || profile.name === key) || null
119
+ }
120
+
121
+ export async function createCustomAgentProfile(input) {
122
+ let created = null
123
+ await atomicUpdate(STORE, (data) => {
124
+ const map = data && typeof data === 'object' ? data : {}
125
+ const profile = normalizeProfileInput(input, null, { creating: true })
126
+ if (Object.values(map).some((item) => item?.name === profile.name)) throw requestError(`Agent name already exists: ${profile.name}`, 409)
127
+ created = profile
128
+ map[profile.id] = profile
129
+ return map
130
+ })
131
+ return created
132
+ }
133
+
134
+ export async function updateCustomAgentProfile(id, patch) {
135
+ let updated = null
136
+ await atomicUpdate(STORE, (data) => {
137
+ const map = data && typeof data === 'object' ? data : {}
138
+ const current = map[id]
139
+ if (!current) throw requestError('Agent not found', 404)
140
+ const next = normalizeProfileInput(patch, current)
141
+ if (RESERVED_NAMES.has(next.name)) throw requestError(`Agent name is reserved: ${next.name}`, 409)
142
+ if (Object.values(map).some((item) => item?.id !== id && item?.name === next.name)) throw requestError(`Agent name already exists: ${next.name}`, 409)
143
+ updated = next
144
+ map[id] = next
145
+ return map
146
+ })
147
+ return updated
148
+ }
149
+
150
+ export async function deleteCustomAgentProfile(id) {
151
+ await atomicUpdate(STORE, (data) => {
152
+ const map = data && typeof data === 'object' ? data : {}
153
+ if (!map[id]) throw requestError('Agent not found', 404)
154
+ delete map[id]
155
+ return map
156
+ })
157
+ }
158
+
159
+ export function agentProfileSnapshot(profile) {
160
+ if (!profile) return null
161
+ return {
162
+ id: profile.id,
163
+ name: profile.name,
164
+ label: profile.label,
165
+ description: profile.description,
166
+ systemPrompt: profile.systemPrompt,
167
+ allowedTools: [...profile.allowedTools],
168
+ maxRuntimeMs: profile.maxRuntimeMs,
169
+ maxToolCalls: profile.maxToolCalls,
170
+ builtin: profile.builtin === true,
171
+ }
172
+ }
173
+
174
+ export function listAvailableAgentTools() {
175
+ const labels = {
176
+ read_file: 'Read file',
177
+ grep_files: 'Search files',
178
+ write_file: 'Write file',
179
+ edit_file: 'Edit file',
180
+ run_command: 'Run command',
181
+ }
182
+ const risks = new Set(['write_file', 'edit_file', 'run_command'])
183
+ return workspaceTools
184
+ .filter((tool) => allowedToolNames.has(tool.name))
185
+ .map((tool) => ({
186
+ name: tool.name,
187
+ label: tool.label || labels[tool.name] || tool.name,
188
+ description: tool.description || '',
189
+ riskLevel: risks.has(tool.name) ? 'dangerous' : 'safe',
190
+ }))
191
+ }
@@ -215,6 +215,24 @@ export function buildAutoCompactLoopMessages(session, messages) {
215
215
  return [summaryMessage, ...source.slice(compactedUpToIndex)]
216
216
  }
217
217
 
218
+ export function estimateSessionContextUsage(session, messages = session?.agent?.state?.messages ?? []) {
219
+ if (!session?.agent?.state) return null
220
+ const sourceMessages = Array.isArray(messages) ? messages : []
221
+ const contextWindow = Number(session.model?.contextWindow) || 0
222
+ if (sourceMessages.length === 0) {
223
+ return { inputTokens: 0, estimatedInputTokens: 0, knownInputTokens: 0, reservedOutputTokens: 0, totalTokens: 0, contextWindow, percent: 0 }
224
+ }
225
+ const loopMessages = buildAutoCompactLoopMessages(session, sourceMessages)
226
+ const knownInputTokens = latestKnownInputTokens(sourceMessages, latestCompactTimestampMs(session))
227
+ return estimateContextUsage({
228
+ systemPrompt: session.agent.state.systemPrompt,
229
+ messages: loopMessages,
230
+ tools: session.agent.state.tools,
231
+ model: session.model,
232
+ knownInputTokens,
233
+ })
234
+ }
235
+
218
236
  export async function maybeAutoCompactSession({ session, messages, signal, emitSessionEvent, persistSession, logger, confirmAutoCompact }) {
219
237
  if (!session || session.autoCompacting) return { compacted: false }
220
238
  const settings = await readAutoCompactSettings()
@@ -300,12 +318,14 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
300
318
  usage,
301
319
  thresholdPercent: settings.thresholdPercent,
302
320
  contextCompaction: session.contextCompaction,
321
+ contextUsage: estimateSessionContextUsage(session, messages),
303
322
  })
304
323
  emitSessionEvent(session, {
305
324
  type: 'messages_replaced',
306
325
  reason: 'auto_compact',
307
326
  messages,
308
327
  contextCompaction: session.contextCompaction,
328
+ contextUsage: estimateSessionContextUsage(session, messages),
309
329
  })
310
330
  return { compacted: true, usage }
311
331
  } catch (error) {
package/server/index.mjs CHANGED
@@ -16,6 +16,7 @@ import { handleToolApi, handleGetTools } from './routes/tools.mjs'
16
16
  import { handleInstructionsApi } from './routes/instructions.mjs'
17
17
  import { handleSkillsApi } from './routes/skills.mjs'
18
18
  import { handleAgentApi } from './routes/agent.mjs'
19
+ import { handleAgentProfilesApi } from './routes/agent-profiles.mjs'
19
20
  import { handleScheduledTasksApi, startScheduledTaskRunner, stopScheduledTaskRunner } from './routes/scheduled-tasks.mjs'
20
21
  import { handleBackupApi } from './routes/backup.mjs'
21
22
  import { handleSystemApi } from './routes/system.mjs'
@@ -208,6 +209,12 @@ async function handleApi(req, res, url) {
208
209
  return
209
210
  }
210
211
 
212
+ // Agent profiles
213
+ if (pathname === '/api/agent-profiles' || pathname.startsWith('/api/agent-profiles/')) {
214
+ await handleAgentProfilesApi(req, res, url)
215
+ return
216
+ }
217
+
211
218
  // Skills
212
219
  if (pathname === '/api/skills' || pathname.startsWith('/api/skills/')) {
213
220
  await handleSkillsApi(req, res, url)
@@ -319,6 +326,10 @@ function startVite() {
319
326
  shell: false,
320
327
  env: { ...process.env, QUICKFORGE_SERVER_PORT: String(port) },
321
328
  })
329
+ viteChild.on('error', (error) => {
330
+ logger.error('Failed to start Vite dev server:', error)
331
+ process.exitCode = 1
332
+ })
322
333
  viteChild.on('exit', (code) => {
323
334
  if (code && code !== 0) process.exitCode = code
324
335
  })
@@ -485,10 +496,17 @@ const server = createServer(async (req, res) => {
485
496
  }
486
497
  })
487
498
 
499
+ function writeAndDestroySocket(socket, statusLine) {
500
+ socket.on('error', () => {})
501
+ if (!socket.destroyed) {
502
+ try { socket.write(`${statusLine}\r\n\r\n`) } catch { /* ignore */ }
503
+ }
504
+ socket.destroy()
505
+ }
506
+
488
507
  server.on('upgrade', (req, socket, head) => {
489
508
  if (!isAllowedHostHeader(req.headers.host)) {
490
- socket.write('HTTP/1.1 403 Forbidden\r\n\r\n')
491
- socket.destroy()
509
+ writeAndDestroySocket(socket, 'HTTP/1.1 403 Forbidden')
492
510
  return
493
511
  }
494
512
 
@@ -500,11 +518,9 @@ server.on('upgrade', (req, socket, head) => {
500
518
  })
501
519
  return
502
520
  }
503
- socket.write('HTTP/1.1 404 Not Found\r\n\r\n')
504
- socket.destroy()
521
+ writeAndDestroySocket(socket, 'HTTP/1.1 404 Not Found')
505
522
  } catch {
506
- socket.write('HTTP/1.1 400 Bad Request\r\n\r\n')
507
- socket.destroy()
523
+ writeAndDestroySocket(socket, 'HTTP/1.1 400 Bad Request')
508
524
  }
509
525
  })
510
526
 
@@ -546,5 +562,13 @@ async function gracefulShutdown(signal) {
546
562
  process.exit(0)
547
563
  }
548
564
 
549
- process.on('SIGINT', (signal) => gracefulShutdown(signal))
550
- process.on('SIGTERM', (signal) => gracefulShutdown(signal))
565
+ function handleShutdownSignal(signal) {
566
+ void gracefulShutdown(signal).catch((error) => {
567
+ logger.error('Graceful shutdown failed:', error)
568
+ flushLogger()
569
+ process.exit(1)
570
+ })
571
+ }
572
+
573
+ process.on('SIGINT', handleShutdownSignal)
574
+ process.on('SIGTERM', handleShutdownSignal)