@shawnstack/quickforge 1.6.8 → 1.6.10

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 (164) hide show
  1. package/README.md +452 -452
  2. package/dist/assets/AgentProfilesPage-BtZpDV-3.js +1 -0
  3. package/dist/assets/ChatPanelHost-BF1V-xOV.js +244 -0
  4. package/dist/assets/{PluginsPage-3pZG7D7e.js → PluginsPage-CCIj8-Mn.js} +1 -1
  5. package/dist/assets/ScheduledTasksPage-DIMewQ3i.js +2 -0
  6. package/dist/assets/{SettingsWorkspacePage-DWkPyGP_.js → SettingsWorkspacePage-BPD495WT.js} +296 -290
  7. package/dist/assets/SharedConversationPage-B_oMTp7B.js +1 -0
  8. package/dist/assets/TerminalDock-wgW7kSC4.js +2 -0
  9. package/dist/assets/WorkspaceInspector-CdX45r3u.js +13 -0
  10. package/dist/assets/abnfDiagram-VRR7QNED-Dl_Fvlqg.js +1 -0
  11. package/dist/assets/arc-Bhg4c0ht.js +1 -0
  12. package/dist/assets/architecture-TIHT7OUA-BgP-IMeo.js +1 -0
  13. package/dist/assets/architectureDiagram-ZJ3FMSHR-BuojN27B.js +36 -0
  14. package/dist/assets/array-CYkMkqnU.js +1 -0
  15. package/dist/assets/blockDiagram-677ZJIJ3-alDLbrR1.js +132 -0
  16. package/dist/assets/c4Diagram-LMCZKHZV-D1lKa4VT.js +10 -0
  17. package/dist/assets/channel-Rz20dKTK.js +1 -0
  18. package/dist/assets/chunk-2Q5K7J3B-Cb14ZF_k.js +1 -0
  19. package/dist/assets/chunk-32BRIVSS-CwWLNjMZ.js +1 -0
  20. package/dist/assets/chunk-52WLFC77-Be569m7d.js +10 -0
  21. package/dist/assets/chunk-5FCAYU7R-BSvoiWV1.js +1 -0
  22. package/dist/assets/chunk-5HE753X5-CRgsy8WS.js +1 -0
  23. package/dist/assets/chunk-5JV3BV7I-CdW477UR.js +1 -0
  24. package/dist/assets/chunk-5TONJI2A-CdtKzSmp.js +2 -0
  25. package/dist/assets/chunk-5VM5RSS4-BKjjssLZ.js +15 -0
  26. package/dist/assets/chunk-7BUUIJ7U-ByAXiXdn.js +1 -0
  27. package/dist/assets/chunk-BIQX33UG-DeG8SCT1.js +1 -0
  28. package/dist/assets/chunk-C7G6YPKG-D_zHT68Q.js +1 -0
  29. package/dist/assets/chunk-CQNSW5MT-CHpHoUSu.js +1 -0
  30. package/dist/assets/chunk-CYSBUYHQ-CRP50Vov.js +1 -0
  31. package/dist/assets/chunk-EMLP6XTP-DX6uQQ2v.js +1 -0
  32. package/dist/assets/chunk-EX3LRPZG-DAJRNkHR.js +231 -0
  33. package/dist/assets/chunk-FWX5IMBZ-XbM5yomC.js +2 -0
  34. package/dist/assets/chunk-HOUHSVGY-Ci0ljnb1.js +1 -0
  35. package/dist/assets/chunk-ICXQ74PX-Dy2WJ71z.js +2 -0
  36. package/dist/assets/chunk-JG7HCLWE-2PrkzHkt.js +2 -0
  37. package/dist/assets/chunk-JWPE2WC7-D32uacSM.js +1 -0
  38. package/dist/assets/chunk-KEIR6QF5-77hsebd7.js +161 -0
  39. package/dist/assets/chunk-MOJQB5TN-DIAFLtiS.js +88 -0
  40. package/dist/assets/chunk-MOZMSUNE-BbscOaEz.js +1 -0
  41. package/dist/assets/chunk-OGEWGWER-ivBVxzKe.js +1 -0
  42. package/dist/assets/chunk-OSBZ3O6U-frD0hkdx.js +1 -0
  43. package/dist/assets/chunk-PUDLZKDR-B-37Rxae.js +156 -0
  44. package/dist/assets/chunk-Q4XR5HBZ-Y90m2RTe.js +11 -0
  45. package/dist/assets/chunk-QBLGF6JB-DixBqslB.js +1 -0
  46. package/dist/assets/chunk-R7FJI6CG-DXS1UXn_.js +1 -0
  47. package/dist/assets/chunk-RYQCIY6F-n4JxhNd0.js +1 -0
  48. package/dist/assets/chunk-U6XO7XAA-yHlgxMmn.js +2 -0
  49. package/dist/assets/chunk-V7JOEXUC-Z6kuZfbl.js +206 -0
  50. package/dist/assets/chunk-VAUOI2AC-BOZvd2Sn.js +1 -0
  51. package/dist/assets/chunk-VR4S4FIN-Cs_SIQma.js +1 -0
  52. package/dist/assets/chunk-WYO6CB5R-CGExYbV-.js +128 -0
  53. package/dist/assets/chunk-XXDRQBXY-CD_EGCtT.js +1 -0
  54. package/dist/assets/chunk-Y2CYZVJY-CQltPwYk.js +1 -0
  55. package/dist/assets/chunk-YOTPTUD7-DopPXWSl.js +1 -0
  56. package/dist/assets/chunk-ZGVPDNZ5-CJHOeqMb.js +62 -0
  57. package/dist/assets/chunk-ZIRB5QZD-BBVc5mf6.js +32 -0
  58. package/dist/assets/classDiagram-OUVF2IWQ-C_ozTKCt.js +1 -0
  59. package/dist/assets/classDiagram-v2-EOCWNBFH-FOfFbbZF.js +1 -0
  60. package/dist/assets/cose-bilkent-JH36ORCC-DLjV-dTm.js +1 -0
  61. package/dist/assets/cynefin-VYW2F7L2-BzPhLtd5.js +1 -0
  62. package/dist/assets/cynefinDiagram-TSTJHNR4-BfH3zVS-.js +62 -0
  63. package/dist/assets/cytoscape.esm-DwdZnGOm.js +321 -0
  64. package/dist/assets/dagre-CY3D84Cl.js +1 -0
  65. package/dist/assets/dagre-VKFMJZFB-BIhwZCIZ.js +4 -0
  66. package/dist/assets/defaultLocale-Beh6XjaL.js +1 -0
  67. package/dist/assets/diagram-FQU43EPY-BHnzNM2w.js +3 -0
  68. package/dist/assets/diagram-G47NLZAW-C7-ZlmFT.js +24 -0
  69. package/dist/assets/diagram-NH7WQ7WH-C-P9Y7IR.js +24 -0
  70. package/dist/assets/diagram-OA4YK3LP-Ci5lZ_T5.js +30 -0
  71. package/dist/assets/diagram-WEI45ONY-C1xuK2bs.js +41 -0
  72. package/dist/assets/dist-DelZBZpK.js +1 -0
  73. package/dist/assets/ebnfDiagram-CCIWWBDH-DKQ5NWMD.js +1 -0
  74. package/dist/assets/erDiagram-Q63AITRT-HAWySadP.js +85 -0
  75. package/dist/assets/eventmodeling-45OFAUF4-BcgGbVJ-.js +1 -0
  76. package/dist/assets/flowDiagram-23GEKE2U-dtkzcC8E.js +1 -0
  77. package/dist/assets/ganttDiagram-NO4QXBWP-C1hwIcOA.js +292 -0
  78. package/dist/assets/gitGraph-TEB2WS4Q-VByWboBU.js +1 -0
  79. package/dist/assets/gitGraphDiagram-IHSO6WYX-DYUYxs1R.js +106 -0
  80. package/dist/assets/graphlib-CAISHZh4.js +1 -0
  81. package/dist/assets/icons-C7j5jdKo.js +1 -0
  82. package/dist/assets/index-Bxg5fT2p.css +3 -0
  83. package/dist/assets/index-DKmdePUP.js +63 -0
  84. package/dist/assets/info-DKCQHKI2-CZeDLZxx.js +1 -0
  85. package/dist/assets/infoDiagram-FWYZ7A6U-CnIL6tv4.js +2 -0
  86. package/dist/assets/init-Rr1s_RiX.js +1 -0
  87. package/dist/assets/ishikawaDiagram-FXEZZL3T-B7WXNycQ.js +70 -0
  88. package/dist/assets/journeyDiagram-5HDEW3XC-D4HvioyY.js +139 -0
  89. package/dist/assets/kanban-definition-HUTT4EX6-CDs763eS.js +89 -0
  90. package/dist/assets/line-ikKkQiFp.js +1 -0
  91. package/dist/assets/linear-GqPyxQkN.js +1 -0
  92. package/dist/assets/map-CJwjCvwr.js +1 -0
  93. package/dist/assets/mcp-servers-dialog-jHlDR9jP.js +5 -0
  94. package/dist/assets/mermaid-parser.core-CoE8cccr.js +4 -0
  95. package/dist/assets/mermaid-renderer-BmbSAmpG.js +2 -0
  96. package/dist/assets/mermaid.core-DlFSL90j.js +11 -0
  97. package/dist/assets/mindmap-definition-LN4V7U3C-DLdq0K9C.js +96 -0
  98. package/dist/assets/{monaco-CzxeAjEw.js → monaco-CdOyGlWD.js} +1 -1
  99. package/dist/assets/ordinal-CibZU3PM.js +1 -0
  100. package/dist/assets/{otel-DQf3c6v2.js → otel-BNGM50C-.js} +1 -1
  101. package/dist/assets/packet-7NZHBO7P-decrrxyX.js +1 -0
  102. package/dist/assets/path-BAQ3hXlG.js +1 -0
  103. package/dist/assets/pegDiagram-2B236MQR-Cm1kPRC2.js +1 -0
  104. package/dist/assets/{pi-ai-DjJwSQBQ.js → pi-ai-Cr3rpCXT.js} +2 -2
  105. package/dist/assets/{pi-web-ui-DqxtBlZL.js → pi-web-ui-DoRGtREc.js} +371 -371
  106. package/dist/assets/pie-RZYD4A2V-J1CasQZl.js +1 -0
  107. package/dist/assets/pieDiagram-ENE6RG2P-Df05wjjo.js +39 -0
  108. package/dist/assets/quadrantDiagram-ABIIQ3AL-BjCwGxJC.js +7 -0
  109. package/dist/assets/radar-I7S5WNFK-B-sZytUA.js +1 -0
  110. package/dist/assets/railroad-3IZDKUUU-BSaqNPm8.js +1 -0
  111. package/dist/assets/railroad-abnf-AHOZXSZD-lzZR7PxT.js +1 -0
  112. package/dist/assets/railroad-ebnf-EBAXGLYW-DQtz1Fh9.js +1 -0
  113. package/dist/assets/railroad-peg-LSFZ7HO6-CpMsuGY3.js +1 -0
  114. package/dist/assets/railroadDiagram-RFXS5EU6-CTUvpKkv.js +1 -0
  115. package/dist/assets/{react-vendor-BR9MG2D-.js → react-vendor-VqdnQHHS.js} +1 -1
  116. package/dist/assets/requirementDiagram-TGXJPOKE-BU1kvEku.js +84 -0
  117. package/dist/assets/rough.esm-Djo4Abte.js +1 -0
  118. package/dist/assets/sankeyDiagram-HTMAVEWB-BHmvxVqH.js +40 -0
  119. package/dist/assets/sequenceDiagram-DBY2YBRQ-CTpwICgg.js +162 -0
  120. package/dist/assets/sizeCapture-X5ZJPWSS-BbLhGaa4.js +1 -0
  121. package/dist/assets/{skills-dialog-B0jNVGZW.js → skills-dialog-DvSnPPzu.js} +1 -1
  122. package/dist/assets/src-B4Tvg3gT.js +1 -0
  123. package/dist/assets/stateDiagram-2N3HPSRC-BxuDwIgT.js +1 -0
  124. package/dist/assets/stateDiagram-v2-6OUMAXLB-BGLshxcY.js +1 -0
  125. package/dist/assets/swimlanes-5IMT3BWC-EkZsiylV.js +2 -0
  126. package/dist/assets/swimlanesDiagram-G3AALYLV-CqdqxA-b.js +8 -0
  127. package/dist/assets/timeline-definition-FHXFAJF6-C9ewOCTa.js +120 -0
  128. package/dist/assets/treeView-QDETBFTQ-Byb71heW.js +1 -0
  129. package/dist/assets/treemap-6X3UGDF4-CMV1SJsq.js +1 -0
  130. package/dist/assets/vennDiagram-L72KCM5P-CR49Y4Lp.js +34 -0
  131. package/dist/assets/wardley-OPB4EBWU-DtCHtr2V.js +1 -0
  132. package/dist/assets/wardleyDiagram-EHGQE667-Cn0GgwWi.js +78 -0
  133. package/dist/assets/xychartDiagram-FW5EYKEG-DlmPNAvd.js +7 -0
  134. package/dist/index.html +8 -8
  135. package/package.json +2 -1
  136. package/server/agent-manager.mjs +174 -9
  137. package/server/agent-profile-files.mjs +86 -6
  138. package/server/agent-profile-schema.mjs +197 -0
  139. package/server/agent-profiles.mjs +172 -41
  140. package/server/auto-compaction.mjs +2 -2
  141. package/server/project-route-context.mjs +25 -0
  142. package/server/routes/agent-profiles.mjs +9 -3
  143. package/server/routes/backup.mjs +135 -48
  144. package/server/routes/project.mjs +6 -10
  145. package/server/routes/workspace.mjs +240 -11
  146. package/server/storage.mjs +34 -0
  147. package/server/subagents.mjs +64 -0
  148. package/server/tools/definitions.mjs +17 -1
  149. package/server/utils/platform.mjs +4 -4
  150. package/dist/assets/AgentProfilesPage-R7N7eDq5.js +0 -1
  151. package/dist/assets/ChatPanelHost-C4orgd5l.js +0 -242
  152. package/dist/assets/ScheduledTasksPage-BAqoK4xk.js +0 -2
  153. package/dist/assets/SharedConversationPage-C-1n_MmB.js +0 -1
  154. package/dist/assets/TerminalDock-B6KAXkIU.js +0 -2
  155. package/dist/assets/WorkspaceInspector-CkXyD_k4.js +0 -3
  156. package/dist/assets/WorkspaceReaderDialog-Dp2QAwBo.js +0 -1
  157. package/dist/assets/diff-line-counts-Dm5kKQOo.js +0 -10
  158. package/dist/assets/icons-DHoaB5uq.js +0 -1
  159. package/dist/assets/index-C77QCHE9.css +0 -3
  160. package/dist/assets/index-CZb_mGGF.js +0 -63
  161. package/dist/assets/mcp-servers-dialog-VbGvNekJ.js +0 -5
  162. package/dist/assets/useAppTheme-DsabFF9D.js +0 -1
  163. /package/dist/assets/{plugin-api-Ds5xGIL7.js → plugin-api-BbUkYm3Y.js} +0 -0
  164. /package/dist/assets/{xterm-aPHkK8RR.js → xterm-CGiQzDiR.js} +0 -0
@@ -1,17 +1,36 @@
1
+ import { existsSync, promises as fs } from 'node:fs'
2
+ import path from 'node:path'
1
3
  import { randomUUID } from 'node:crypto'
2
- import { readStore, atomicUpdate } from './storage.mjs'
3
- import { subagentDefinitions } from './subagents.mjs'
4
+ import { readStore, userAgentsDir, writeStore } from './storage.mjs'
5
+ import { builtinSubagentProfilePath, ensureBuiltinSubagentMarkdownFiles, subagentDefinitions } from './subagents.mjs'
4
6
  import { workspaceTools } from './tools/definitions.mjs'
5
7
  import { defaultGlobalWorkspaceContext, projectContextFromId } from './project-config.mjs'
6
- import { loadFileAgentProfiles } from './agent-profile-files.mjs'
8
+ import {
9
+ deleteUserAgentProfileMarkdown,
10
+ loadFileAgentProfiles,
11
+ userAgentProfileFilePath,
12
+ writeUserAgentProfileMarkdown,
13
+ } from './agent-profile-files.mjs'
14
+ import {
15
+ AGENT_PROFILE_TOOL_NAMES,
16
+ inferCapabilityPolicy,
17
+ modelReferenceSnapshot,
18
+ normalizeAgentProfileThinkingLevel,
19
+ normalizeCapabilityPolicy,
20
+ normalizeModelReference,
21
+ validateAgentProfileTools,
22
+ validateModelReference,
23
+ } from './agent-profile-schema.mjs'
7
24
 
8
25
  const STORE = 'custom-agents'
26
+ const BUILTIN_OVERRIDES_STORE = 'agent-profile-overrides'
9
27
  const RESERVED_NAMES = new Set(subagentDefinitions.map((definition) => definition.name))
10
- export const AGENT_PROFILE_TOOL_NAMES = ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command']
11
28
  const allowedToolNames = new Set(AGENT_PROFILE_TOOL_NAMES)
12
29
  const nameRegex = /^[a-z][a-z0-9_-]{1,39}$/
13
30
  const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000
14
31
  const DEFAULT_MAX_TOOL_CALLS = 300
32
+ const MIGRATION_MARKER_FILE = path.join(userAgentsDir, '.custom-agents-migrated.json')
33
+ let customAgentMigrationPromise = null
15
34
 
16
35
  function requestError(message, statusCode = 400) {
17
36
  const error = new Error(message)
@@ -46,7 +65,7 @@ function normalizeOptionalRuntime(value, fallback) {
46
65
  return Math.min(Math.max(Math.round(parsed), 1000), DEFAULT_MAX_RUNTIME_MS)
47
66
  }
48
67
 
49
- function builtinProfileFromSubagent(definition) {
68
+ function builtinProfileFromSubagent(definition, override = null) {
50
69
  return {
51
70
  id: definition.name,
52
71
  name: definition.name,
@@ -54,20 +73,28 @@ function builtinProfileFromSubagent(definition) {
54
73
  description: definition.description || '',
55
74
  systemPrompt: definition.systemPrompt || '',
56
75
  allowedTools: [...definition.allowedTools],
76
+ capabilityPolicy: definition.capabilityPolicy || inferCapabilityPolicy(definition.allowedTools),
77
+ model: modelReferenceSnapshot(override?.model || definition.model),
78
+ thinkingLevel: 'inherit',
79
+ lifecycle: 'builtin',
80
+ managed: true,
57
81
  maxRuntimeMs: definition.maxRuntimeMs || DEFAULT_MAX_RUNTIME_MS,
58
82
  maxToolCalls: definition.maxToolCalls || DEFAULT_MAX_TOOL_CALLS,
59
83
  enabledAsSubagent: true,
60
84
  builtin: true,
61
85
  source: 'builtin',
62
86
  readonly: true,
87
+ filePath: builtinSubagentProfilePath(definition.name),
88
+ relativePath: `~/.quickforge/agents/builtin/${definition.name}.md`,
63
89
  allowFileMutations: definition.allowFileMutations === true,
64
90
  createdAt: 'builtin',
65
91
  updatedAt: 'builtin',
66
92
  }
67
93
  }
68
94
 
69
- export function listBuiltinAgentProfiles() {
70
- return subagentDefinitions.map(builtinProfileFromSubagent)
95
+ export async function listBuiltinAgentProfiles() {
96
+ const overrides = await readStore(BUILTIN_OVERRIDES_STORE).catch(() => ({}))
97
+ return subagentDefinitions.map((definition) => builtinProfileFromSubagent(definition, overrides?.[definition.name]))
71
98
  }
72
99
 
73
100
  function normalizeProfileInput(input, existing = null, { creating = false } = {}) {
@@ -85,6 +112,13 @@ function normalizeProfileInput(input, existing = null, { creating = false } = {}
85
112
  for (const toolName of allowedTools) {
86
113
  if (!allowedToolNames.has(toolName)) throw requestError(`Unsupported tool for custom agent: ${toolName}`)
87
114
  }
115
+ const capabilityPolicy = normalizeCapabilityPolicy(
116
+ input?.capabilityPolicy ?? existing?.capabilityPolicy,
117
+ allowedTools,
118
+ )
119
+ validateAgentProfileTools(allowedTools, capabilityPolicy)
120
+ const model = validateModelReference(normalizeModelReference(input?.model ?? existing?.model))
121
+ const thinkingLevel = normalizeAgentProfileThinkingLevel(input?.thinkingLevel ?? existing?.thinkingLevel)
88
122
 
89
123
  return {
90
124
  id: existing?.id || `agent-${randomUUID()}`,
@@ -93,12 +127,18 @@ function normalizeProfileInput(input, existing = null, { creating = false } = {}
93
127
  description: String(input?.description ?? existing?.description ?? '').trim().slice(0, 500),
94
128
  systemPrompt: String(input?.systemPrompt ?? existing?.systemPrompt ?? '').trim(),
95
129
  allowedTools,
130
+ capabilityPolicy,
131
+ model,
132
+ thinkingLevel,
133
+ lifecycle: existing?.lifecycle || 'persistent',
96
134
  maxRuntimeMs: normalizeOptionalRuntime(input?.maxRuntimeMs ?? existing?.maxRuntimeMs, DEFAULT_MAX_RUNTIME_MS),
97
135
  maxToolCalls: normalizeOptionalPositiveInteger(input?.maxToolCalls ?? existing?.maxToolCalls, DEFAULT_MAX_TOOL_CALLS, 300),
98
136
  enabledAsSubagent: input?.enabledAsSubagent === undefined ? Boolean(existing?.enabledAsSubagent ?? true) : input.enabledAsSubagent === true,
99
137
  builtin: false,
100
- source: 'store',
138
+ source: 'user',
101
139
  readonly: false,
140
+ managed: true,
141
+ managedBy: 'quickforge',
102
142
  allowFileMutations: allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file'),
103
143
  createdAt: existing?.createdAt || now,
104
144
  updatedAt: now,
@@ -110,6 +150,74 @@ async function readCustomAgentMap() {
110
150
  return data && typeof data === 'object' ? data : {}
111
151
  }
112
152
 
153
+ function migrationReportEntry(profile, file, extra = {}) {
154
+ return {
155
+ id: profile.id,
156
+ fromName: profile.name,
157
+ toName: path.basename(file, '.md'),
158
+ file,
159
+ ...extra,
160
+ }
161
+ }
162
+
163
+ function uniqueMigratedName(name, usedNames) {
164
+ const base = String(name || '').trim().toLowerCase()
165
+ const candidates = [base, `${base}-store`, `${base}-custom`]
166
+ for (const candidate of candidates) {
167
+ if (nameRegex.test(candidate) && !usedNames.has(candidate) && !RESERVED_NAMES.has(candidate) && !existsSync(userAgentProfileFilePath(candidate))) return candidate
168
+ }
169
+ for (let index = 2; index < 100; index += 1) {
170
+ const candidate = `${base.slice(0, 34)}-${index}`
171
+ if (nameRegex.test(candidate) && !usedNames.has(candidate) && !RESERVED_NAMES.has(candidate) && !existsSync(userAgentProfileFilePath(candidate))) return candidate
172
+ }
173
+ return null
174
+ }
175
+
176
+ async function migrateCustomAgentStoreToFiles() {
177
+ if (existsSync(MIGRATION_MARKER_FILE)) return
178
+ await fs.mkdir(userAgentsDir, { recursive: true })
179
+ const custom = Object.values(await readCustomAgentMap())
180
+ const report = { migratedAt: new Date().toISOString(), sourceStore: STORE, migrated: [], conflicts: [], skipped: [] }
181
+ if (custom.length === 0) {
182
+ await fs.writeFile(MIGRATION_MARKER_FILE, JSON.stringify(report, null, 2), 'utf8')
183
+ return
184
+ }
185
+
186
+ const usedNames = new Set()
187
+ for (const item of custom) {
188
+ try {
189
+ const profile = normalizeProfileInput({ ...item, id: item.id }, item)
190
+ let targetName = profile.name
191
+ let conflictReason = ''
192
+ if (RESERVED_NAMES.has(targetName) || existsSync(userAgentProfileFilePath(targetName)) || usedNames.has(targetName)) {
193
+ conflictReason = RESERVED_NAMES.has(targetName) ? 'reserved builtin name or duplicate' : 'target file already exists or duplicate'
194
+ targetName = uniqueMigratedName(targetName, usedNames)
195
+ }
196
+ if (!targetName) {
197
+ report.skipped.push({ id: item?.id, fromName: item?.name, reason: 'unable to allocate unique markdown file name' })
198
+ continue
199
+ }
200
+ const migrated = { ...profile, name: targetName, source: 'user', readonly: false, managed: true, managedBy: 'quickforge' }
201
+ const file = await writeUserAgentProfileMarkdown(migrated)
202
+ usedNames.add(targetName)
203
+ const entry = migrationReportEntry(item, file, conflictReason ? { reason: conflictReason } : {})
204
+ if (conflictReason) report.conflicts.push(entry)
205
+ else report.migrated.push(entry)
206
+ } catch (error) {
207
+ report.skipped.push({ id: item?.id, fromName: item?.name, reason: error?.message || String(error) })
208
+ }
209
+ }
210
+ await fs.writeFile(MIGRATION_MARKER_FILE, JSON.stringify(report, null, 2), 'utf8')
211
+ }
212
+
213
+ async function ensureCustomAgentStoreMigrated() {
214
+ if (existsSync(MIGRATION_MARKER_FILE)) return
215
+ customAgentMigrationPromise ||= migrateCustomAgentStoreToFiles().finally(() => {
216
+ customAgentMigrationPromise = null
217
+ })
218
+ await customAgentMigrationPromise
219
+ }
220
+
113
221
  async function resolveWorkspaceRoot(options = {}) {
114
222
  if (options.workspaceRoot) return options.workspaceRoot
115
223
  if (options.projectId) {
@@ -146,10 +254,11 @@ function mergeProfiles({ builtin = [], file = [], custom = [] }) {
146
254
  }
147
255
 
148
256
  export async function listAgentProfiles(options = {}) {
149
- const custom = Object.values(await readCustomAgentMap())
257
+ await ensureBuiltinSubagentMarkdownFiles()
258
+ await ensureCustomAgentStoreMigrated()
150
259
  const workspaceRoot = await resolveWorkspaceRoot(options)
151
260
  const file = await loadFileAgentProfiles(workspaceRoot, { reservedNames: RESERVED_NAMES })
152
- const profiles = mergeProfiles({ builtin: listBuiltinAgentProfiles(), file, custom })
261
+ const profiles = mergeProfiles({ builtin: await listBuiltinAgentProfiles(), file, custom: [] })
153
262
  return options.includeDisabled ? profiles : profiles.filter((profile) => profile.enabledAsSubagent || profile.builtin || profile.enabledAsSubagent === false)
154
263
  }
155
264
 
@@ -163,46 +272,60 @@ export async function getAgentProfile(idOrName, options = {}) {
163
272
  const profiles = await listAgentProfiles({ ...options, includeDisabled: true })
164
273
  const byName = profiles.find((profile) => profile.name === key)
165
274
  if (byName) return byName
166
- const custom = Object.values(await readCustomAgentMap())
167
- return custom.find((profile) => profile?.id === key) || profiles.find((profile) => profile.id === key) || null
275
+ return profiles.find((profile) => String(profile.id || '').toLowerCase() === key) || null
276
+ }
277
+
278
+ function assertEditableUserProfile(profile) {
279
+ if (!profile) throw requestError('Agent not found', 404)
280
+ if (profile.builtin) throw requestError('Built-in agents cannot be modified', 403)
281
+ if (profile.readonly) throw requestError('Read-only agents cannot be modified from the API', 403)
282
+ if (profile.source !== 'user' || profile.managedBy !== 'quickforge') throw requestError('Only QuickForge-managed user agents can be modified from the API', 403)
283
+ }
284
+
285
+ async function assertNameAvailable(name, currentId = null) {
286
+ if (RESERVED_NAMES.has(name)) throw requestError(`Agent name is reserved: ${name}`, 409)
287
+ const existing = await listAgentProfiles({ includeDisabled: true })
288
+ const conflict = existing.find((profile) => profile.name === name && profile.id !== currentId)
289
+ if (conflict) throw requestError(`Agent name already exists: ${name}`, 409)
168
290
  }
169
291
 
170
292
  export async function createCustomAgentProfile(input) {
171
- let created = null
172
- await atomicUpdate(STORE, (data) => {
173
- const map = data && typeof data === 'object' ? data : {}
174
- const profile = normalizeProfileInput(input, null, { creating: true })
175
- if (Object.values(map).some((item) => item?.name === profile.name)) throw requestError(`Agent name already exists: ${profile.name}`, 409)
176
- created = profile
177
- map[profile.id] = profile
178
- return map
179
- })
180
- return created
293
+ await ensureCustomAgentStoreMigrated()
294
+ const profile = normalizeProfileInput(input, null, { creating: true })
295
+ await assertNameAvailable(profile.name)
296
+ const file = await writeUserAgentProfileMarkdown(profile)
297
+ return { ...profile, filePath: file, relativePath: `~/.quickforge/agents/${profile.name}.md` }
181
298
  }
182
299
 
183
300
  export async function updateCustomAgentProfile(id, patch) {
184
- let updated = null
185
- await atomicUpdate(STORE, (data) => {
186
- const map = data && typeof data === 'object' ? data : {}
187
- const current = map[id]
188
- if (!current) throw requestError('Agent not found', 404)
189
- const next = normalizeProfileInput(patch, current)
190
- if (RESERVED_NAMES.has(next.name)) throw requestError(`Agent name is reserved: ${next.name}`, 409)
191
- if (Object.values(map).some((item) => item?.id !== id && item?.name === next.name)) throw requestError(`Agent name already exists: ${next.name}`, 409)
192
- updated = next
193
- map[id] = next
194
- return map
195
- })
196
- return updated
301
+ await ensureCustomAgentStoreMigrated()
302
+ const current = await getAgentProfile(id)
303
+ assertEditableUserProfile(current)
304
+ const next = normalizeProfileInput(patch, current)
305
+ await assertNameAvailable(next.name, current.id)
306
+ const file = await writeUserAgentProfileMarkdown(next)
307
+ if (next.name !== current.name) await deleteUserAgentProfileMarkdown(current)
308
+ return { ...next, filePath: file, relativePath: `~/.quickforge/agents/${next.name}.md` }
309
+ }
310
+
311
+ export async function updateBuiltinAgentModelOverride(id, modelInput) {
312
+ const current = await getAgentProfile(id)
313
+ if (!current) throw requestError('Agent not found', 404)
314
+ if (!current.builtin) throw requestError('Only built-in agents support model overrides', 400)
315
+ const model = validateModelReference(normalizeModelReference(modelInput))
316
+ const overrides = await readStore(BUILTIN_OVERRIDES_STORE).catch(() => ({}))
317
+ const next = { ...(overrides && typeof overrides === 'object' ? overrides : {}) }
318
+ if (model.mode === 'fixed') next[current.name] = { model }
319
+ else delete next[current.name]
320
+ await writeStore(BUILTIN_OVERRIDES_STORE, next)
321
+ return { ...current, model: modelReferenceSnapshot(model) }
197
322
  }
198
323
 
199
324
  export async function deleteCustomAgentProfile(id) {
200
- await atomicUpdate(STORE, (data) => {
201
- const map = data && typeof data === 'object' ? data : {}
202
- if (!map[id]) throw requestError('Agent not found', 404)
203
- delete map[id]
204
- return map
205
- })
325
+ await ensureCustomAgentStoreMigrated()
326
+ const current = await getAgentProfile(id)
327
+ assertEditableUserProfile(current)
328
+ await deleteUserAgentProfileMarkdown(current)
206
329
  }
207
330
 
208
331
  export function agentProfileSnapshot(profile) {
@@ -214,6 +337,11 @@ export function agentProfileSnapshot(profile) {
214
337
  description: profile.description,
215
338
  systemPrompt: profile.systemPrompt,
216
339
  allowedTools: [...profile.allowedTools],
340
+ capabilityPolicy: profile.capabilityPolicy || inferCapabilityPolicy(profile.allowedTools || []),
341
+ model: modelReferenceSnapshot(profile.model),
342
+ thinkingLevel: normalizeAgentProfileThinkingLevel(profile.thinkingLevel),
343
+ lifecycle: profile.lifecycle || (profile.builtin ? 'builtin' : 'persistent'),
344
+ managed: profile.managed === true,
217
345
  maxRuntimeMs: profile.maxRuntimeMs,
218
346
  maxToolCalls: profile.maxToolCalls,
219
347
  enabledAsSubagent: profile.enabledAsSubagent === true,
@@ -222,9 +350,12 @@ export function agentProfileSnapshot(profile) {
222
350
  readonly: profile.readonly === true || profile.builtin === true,
223
351
  filePath: profile.filePath,
224
352
  relativePath: profile.relativePath,
353
+ warnings: Array.isArray(profile.warnings) ? [...profile.warnings] : undefined,
225
354
  }
226
355
  }
227
356
 
357
+ export { AGENT_PROFILE_TOOL_NAMES } from './agent-profile-schema.mjs'
358
+
228
359
  export function listAvailableAgentTools() {
229
360
  const labels = {
230
361
  read_file: 'Read file',
@@ -5,7 +5,7 @@ import { estimateContextUsage, shouldCompactContextByPercent } from './context-u
5
5
  export const AUTO_COMPACT_SETTINGS_KEY = 'auto-compact-settings'
6
6
 
7
7
  export const DEFAULT_AUTO_COMPACT_SETTINGS = {
8
- enabled: false,
8
+ enabled: true,
9
9
  thresholdPercent: 80,
10
10
  keepRecentTurns: 2,
11
11
  minSourceChars: 1600,
@@ -24,7 +24,7 @@ function clampNumber(value, fallback, min, max) {
24
24
  export function normalizeAutoCompactSettings(value) {
25
25
  if (!value || typeof value !== 'object') return { ...DEFAULT_AUTO_COMPACT_SETTINGS }
26
26
  return {
27
- enabled: value.enabled === true,
27
+ enabled: value.enabled !== false,
28
28
  thresholdPercent: clampNumber(value.thresholdPercent, DEFAULT_AUTO_COMPACT_SETTINGS.thresholdPercent, 50, 95),
29
29
  keepRecentTurns: clampNumber(value.keepRecentTurns, DEFAULT_AUTO_COMPACT_SETTINGS.keepRecentTurns, 1, 20),
30
30
  minSourceChars: clampNumber(value.minSourceChars, DEFAULT_AUTO_COMPACT_SETTINGS.minSourceChars, 0, 200000),
@@ -0,0 +1,25 @@
1
+ import path from 'node:path'
2
+
3
+ export function resolveRequestedProject(config, projectId, defaultWorkspaceRoot) {
4
+ if (!projectId) return config.projects.find((project) => project.id === config.activeProjectId) ?? config.projects[0]
5
+ if (projectId === 'default') {
6
+ return {
7
+ id: 'default',
8
+ name: 'workspace',
9
+ path: defaultWorkspaceRoot,
10
+ lastOpenedAt: '',
11
+ sortOrder: 0,
12
+ skills: [],
13
+ commandDir: '',
14
+ }
15
+ }
16
+ const project = config.projects.find((item) => item.id === projectId)
17
+ if (project) return project
18
+ const error = new Error('Unknown project')
19
+ error.statusCode = 404
20
+ throw error
21
+ }
22
+
23
+ export function workspaceRootAfterProjectDeletion(activeProject, defaultWorkspaceRoot) {
24
+ return path.resolve(activeProject?.path || defaultWorkspaceRoot)
25
+ }
@@ -9,6 +9,7 @@ import {
9
9
  getAgentProfile,
10
10
  listAgentProfiles,
11
11
  listAvailableAgentTools,
12
+ updateBuiltinAgentModelOverride,
12
13
  updateCustomAgentProfile,
13
14
  } from '../agent-profiles.mjs'
14
15
 
@@ -155,9 +156,14 @@ export async function handleAgentProfilesApi(req, res, url) {
155
156
 
156
157
  if (req.method === 'PATCH' || req.method === 'PUT') {
157
158
  const current = await getAgentProfile(id)
158
- if (current?.builtin) throw requestError('Built-in agents cannot be modified', 403)
159
- if (current?.readonly) throw requestError('File-based agents cannot be modified from the API', 403)
160
159
  const body = await readJsonBody(req)
160
+ if (current?.builtin) {
161
+ const keys = Object.keys(body || {})
162
+ if (keys.length !== 1 || keys[0] !== 'model') throw requestError('Built-in agents only allow model updates', 403)
163
+ sendJson(res, 200, { agent: agentProfileSnapshot(await updateBuiltinAgentModelOverride(id, body.model)) })
164
+ return
165
+ }
166
+ if (current?.readonly) throw requestError('Read-only agents cannot be modified from the API', 403)
161
167
  sendJson(res, 200, { agent: await updateCustomAgentProfile(id, body || {}) })
162
168
  return
163
169
  }
@@ -165,7 +171,7 @@ export async function handleAgentProfilesApi(req, res, url) {
165
171
  if (req.method === 'DELETE') {
166
172
  const current = await getAgentProfile(id)
167
173
  if (current?.builtin) throw requestError('Built-in agents cannot be deleted', 403)
168
- if (current?.readonly) throw requestError('File-based agents cannot be deleted from the API', 403)
174
+ if (current?.readonly) throw requestError('Read-only agents cannot be deleted from the API', 403)
169
175
  await deleteCustomAgentProfile(id)
170
176
  sendJson(res, 200, { ok: true })
171
177
  return
@@ -18,12 +18,17 @@ const BACKUP_VERSION = 1
18
18
  const BACKUP_APP = 'quickforge'
19
19
  const IMPORT_UPLOAD_MAX_BYTES = Number(process.env.QUICKFORGE_IMPORT_UPLOAD_MAX_BYTES || 1024 * 1024 * 1024)
20
20
  const backupScopes = new Set(['all', 'config', 'sessions'])
21
- const restoreSectionIds = new Set(['settings', 'mcp', 'providerKeys', 'customProviders', 'projects', 'scheduledTasks', 'conversations'])
21
+ const settingsSectionIds = ['settings', 'mcp', 'providerKeys', 'customProviders', 'projects', 'scheduledTasks']
22
+ const exportSectionIds = new Set(settingsSectionIds)
23
+ const restoreSectionIds = new Set([...settingsSectionIds, 'conversations'])
22
24
  const restoreModes = new Set(['replace', 'merge'])
23
25
 
24
26
  function normalizeMode(value) {
25
27
  const mode = String(value || 'replace')
26
- return restoreModes.has(mode) ? mode : 'replace'
28
+ if (restoreModes.has(mode)) return mode
29
+ const error = new Error(`Invalid restore mode: ${mode}`)
30
+ error.statusCode = 400
31
+ throw error
27
32
  }
28
33
 
29
34
  function normalizeScope(value) {
@@ -36,25 +41,27 @@ function parseBoolean(value) {
36
41
  return text === '1' || text === 'true' || text === 'yes'
37
42
  }
38
43
 
39
- function extractCoreBackupFromText(text) {
40
- const backup = JSON.parse(text, (key, value) => (
41
- key === 'sessions' || key === 'sessionsMetadata' || key === 'sessions-metadata'
42
- ? undefined
43
- : value
44
- ))
45
- if (backup && typeof backup === 'object' && !Array.isArray(backup)) {
46
- if (backup.data && typeof backup.data === 'object' && !Array.isArray(backup.data)) {
47
- backup.scope = 'config'
48
- return backup
49
- }
50
- return {
51
- app: backup.app ?? BACKUP_APP,
52
- version: backup.version ?? BACKUP_VERSION,
53
- exportedAt: backup.exportedAt ?? null,
54
- scope: 'config',
55
- includeSecrets: backup.includeSecrets === true,
56
- data: backup,
44
+ function extractSettingsBackupFromText(text) {
45
+ let ignoredConversations = false
46
+ const backup = JSON.parse(text, (key, value) => {
47
+ if (key === 'sessions' || key === 'sessionsMetadata' || key === 'sessions-metadata') {
48
+ ignoredConversations = true
49
+ return undefined
57
50
  }
51
+ return value
52
+ })
53
+ if (backup && typeof backup === 'object' && !Array.isArray(backup)) {
54
+ const normalized = backup.data && typeof backup.data === 'object' && !Array.isArray(backup.data)
55
+ ? { ...backup, scope: 'config' }
56
+ : {
57
+ app: backup.app ?? BACKUP_APP,
58
+ version: backup.version ?? BACKUP_VERSION,
59
+ exportedAt: backup.exportedAt ?? null,
60
+ scope: 'config',
61
+ includeSecrets: backup.includeSecrets === true,
62
+ data: backup,
63
+ }
64
+ return { backup: normalized, ignoredConversations }
58
65
  }
59
66
  const error = new Error('Invalid backup file')
60
67
  error.statusCode = 400
@@ -92,9 +99,22 @@ async function readPendingImportBackup(token) {
92
99
  throw error
93
100
  }
94
101
  const file = path.join(storageDir, 'pending-imports', `${token}.json`)
95
- const backup = JSON.parse(await fs.readFile(file, 'utf8'))
102
+ try {
103
+ return JSON.parse(await fs.readFile(file, 'utf8'))
104
+ } catch (cause) {
105
+ if (cause?.code === 'ENOENT') {
106
+ const error = new Error('Import preview has expired. Please select the backup file again.')
107
+ error.statusCode = 400
108
+ throw error
109
+ }
110
+ throw cause
111
+ }
112
+ }
113
+
114
+ async function deletePendingImportBackup(token) {
115
+ if (!/^[0-9a-f-]{36}$/i.test(String(token || ''))) return
116
+ const file = path.join(storageDir, 'pending-imports', `${token}.json`)
96
117
  await fs.rm(file, { force: true })
97
- return backup
98
118
  }
99
119
 
100
120
  function backupTimestamp(date = new Date()) {
@@ -170,30 +190,48 @@ function normalizeSessionMetadata(sessions, metadata) {
170
190
  return nextMetadata
171
191
  }
172
192
 
193
+ function normalizeExportSections(value) {
194
+ if (value === null || value === undefined || value === '') return null
195
+ const items = Array.isArray(value) ? value : String(value).split(',')
196
+ const selected = new Set()
197
+ for (const item of items) {
198
+ const id = String(item).trim()
199
+ if (!exportSectionIds.has(id)) {
200
+ const error = new Error(`Invalid export section: ${id}`)
201
+ error.statusCode = 400
202
+ throw error
203
+ }
204
+ selected.add(id)
205
+ }
206
+ if (selected.size === 0) {
207
+ const error = new Error('No export sections selected')
208
+ error.statusCode = 400
209
+ throw error
210
+ }
211
+ return selected
212
+ }
213
+
173
214
  async function buildBackup(scope = 'all', options = {}) {
174
215
  const normalizedScope = normalizeScope(scope)
175
- const includeConfig = normalizedScope === 'all' || normalizedScope === 'config'
176
- const includeSessions = normalizedScope === 'all' || normalizedScope === 'sessions'
177
- const includeSecrets = Boolean(options.includeSecrets && includeConfig)
216
+ const selected = normalizeExportSections(options.sections)
217
+ const includeConfig = selected ? true : normalizedScope === 'all' || normalizedScope === 'config'
218
+ const includeSessions = selected ? false : normalizedScope === 'all' || normalizedScope === 'sessions'
219
+ const includeSecrets = selected ? selected.has('providerKeys') : Boolean(options.includeSecrets && includeConfig)
178
220
  const data = {}
179
221
 
180
222
  if (includeConfig) {
181
- const [settings, mcp, providerKeys, customProviders, projects, scheduledTasks] = await Promise.all([
182
- readStore('settings'),
183
- readStore('mcp'),
184
- includeSecrets ? readStore('provider-keys') : Promise.resolve(undefined),
185
- readStore('custom-providers'),
186
- readProjectConfigData(),
187
- readStore('scheduled-tasks'),
223
+ const shouldInclude = (id) => !selected || selected.has(id)
224
+ const entries = await Promise.all([
225
+ shouldInclude('settings') ? readStore('settings').then((value) => ['settings', value]) : null,
226
+ shouldInclude('mcp') ? readStore('mcp').then((value) => ['mcp', value]) : null,
227
+ shouldInclude('providerKeys') ? readStore('provider-keys').then((value) => ['providerKeys', value]) : null,
228
+ shouldInclude('customProviders') ? readStore('custom-providers').then((value) => ['customProviders', value]) : null,
229
+ shouldInclude('projects') ? readProjectConfigData().then((value) => ['projects', value]) : null,
230
+ shouldInclude('scheduledTasks') ? readStore('scheduled-tasks').then((value) => ['scheduledTasks', value]) : null,
188
231
  ])
189
- Object.assign(data, {
190
- settings,
191
- mcp,
192
- customProviders,
193
- projects,
194
- scheduledTasks,
195
- })
196
- if (includeSecrets) data.providerKeys = providerKeys
232
+ for (const entry of entries) {
233
+ if (entry) data[entry[0]] = entry[1]
234
+ }
197
235
  }
198
236
 
199
237
  if (includeSessions) {
@@ -211,7 +249,8 @@ async function buildBackup(scope = 'all', options = {}) {
211
249
  app: BACKUP_APP,
212
250
  version: BACKUP_VERSION,
213
251
  exportedAt: new Date().toISOString(),
214
- scope: normalizedScope,
252
+ scope: selected ? 'config' : normalizedScope,
253
+ exportedSections: selected ? settingsSectionIds.filter((id) => selected.has(id)) : undefined,
215
254
  includeSecrets,
216
255
  data,
217
256
  }
@@ -267,6 +306,42 @@ function normalizeBackupPayload(payload) {
267
306
  }
268
307
  }
269
308
 
309
+ function validateSettingsImportBackup(payload) {
310
+ const normalized = normalizeBackupPayload(payload)
311
+ const validSections = {}
312
+ const invalidSections = {}
313
+
314
+ for (const id of settingsSectionIds) {
315
+ const value = normalized.sections[id]
316
+ if (value === undefined) continue
317
+ try {
318
+ validSections[id] = id === 'projects'
319
+ ? assertProjectConfig(value)
320
+ : assertObjectSection(value, id)
321
+ } catch (error) {
322
+ invalidSections[id] = error instanceof Error ? error.message : `Invalid backup section: ${id}`
323
+ }
324
+ }
325
+
326
+ if (Object.keys(validSections).length === 0) {
327
+ const error = new Error('Backup does not contain any valid settings sections')
328
+ error.statusCode = 400
329
+ throw error
330
+ }
331
+
332
+ return {
333
+ backup: {
334
+ app: normalized.app,
335
+ version: normalized.version,
336
+ exportedAt: normalized.exportedAt,
337
+ scope: 'config',
338
+ includeSecrets: normalized.includeSecrets,
339
+ data: validSections,
340
+ },
341
+ invalidSections,
342
+ }
343
+ }
344
+
270
345
  function validateBackupPayload(payload) {
271
346
  const backup = normalizeBackupPayload(payload)
272
347
  const { sections } = backup
@@ -349,11 +424,20 @@ function backupWithSelectedSections(backup, selected) {
349
424
 
350
425
  function parseImportPayload(body) {
351
426
  const payload = body?.backup && typeof body.backup === 'object' ? body.backup : body
352
- const backup = validateBackupPayload(payload)
427
+ const normalized = normalizeBackupPayload(payload)
353
428
  const requestedSections = body?.backup && typeof body === 'object' ? body.sections : undefined
354
- const selected = normalizeRestoreSections(requestedSections, backup.sections)
429
+ const selected = normalizeRestoreSections(requestedSections, normalized.sections)
430
+ const filtered = selected ? backupWithSelectedSections(normalized, selected) : normalized
431
+ const backup = validateBackupPayload({
432
+ app: filtered.app,
433
+ version: filtered.version,
434
+ exportedAt: filtered.exportedAt,
435
+ scope: filtered.scope,
436
+ includeSecrets: filtered.includeSecrets,
437
+ data: filtered.sections,
438
+ })
355
439
  const mode = normalizeMode(body?.mode)
356
- return { backup: backupWithSelectedSections(backup, selected), mode }
440
+ return { backup, mode }
357
441
  }
358
442
 
359
443
  function countKeys(value) {
@@ -494,6 +578,7 @@ export async function handleBackupApi(req, res, url) {
494
578
  if (req.method === 'GET' && url.pathname === '/api/backup/export') {
495
579
  await ensureStorage()
496
580
  sendJson(res, 200, await buildBackup(url.searchParams.get('scope'), {
581
+ sections: url.searchParams.get('sections'),
497
582
  includeSecrets: parseBoolean(url.searchParams.get('includeSecrets')),
498
583
  }))
499
584
  return
@@ -509,10 +594,11 @@ export async function handleBackupApi(req, res, url) {
509
594
  if (req.method === 'POST' && url.pathname === '/api/backup/inspect-file') {
510
595
  await ensureStorage()
511
596
  const text = await readTextBody(req)
512
- const backup = extractCoreBackupFromText(text)
513
- const inspect = inspectBackup(backup)
514
- const token = await writePendingImportBackup(backup)
515
- sendJson(res, 200, { ...inspect, importToken: token })
597
+ const { backup, ignoredConversations } = extractSettingsBackupFromText(text)
598
+ const { backup: validBackup, invalidSections } = validateSettingsImportBackup(backup)
599
+ const inspect = inspectBackup(validBackup)
600
+ const token = await writePendingImportBackup(validBackup)
601
+ sendJson(res, 200, { ...inspect, invalidSections, ignoredConversations, importToken: token })
516
602
  return
517
603
  }
518
604
 
@@ -523,6 +609,7 @@ export async function handleBackupApi(req, res, url) {
523
609
  const { backup, mode } = parseImportPayload(importBody)
524
610
  const safetyBackupPath = await writeSafetyBackup(backup.sections.sessions !== undefined || backup.sections.sessionsMetadata !== undefined ? 'all' : 'config')
525
611
  const summary = await restoreValidatedBackup(backup, mode)
612
+ if (body?.importToken) await deletePendingImportBackup(body.importToken)
526
613
  sendJson(res, 200, { ok: true, safetyBackupPath, summary })
527
614
  return
528
615
  }