@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.
- package/README.md +452 -452
- package/dist/assets/AgentProfilesPage-BtZpDV-3.js +1 -0
- package/dist/assets/ChatPanelHost-BF1V-xOV.js +244 -0
- package/dist/assets/{PluginsPage-3pZG7D7e.js → PluginsPage-CCIj8-Mn.js} +1 -1
- package/dist/assets/ScheduledTasksPage-DIMewQ3i.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-DWkPyGP_.js → SettingsWorkspacePage-BPD495WT.js} +296 -290
- package/dist/assets/SharedConversationPage-B_oMTp7B.js +1 -0
- package/dist/assets/TerminalDock-wgW7kSC4.js +2 -0
- package/dist/assets/WorkspaceInspector-CdX45r3u.js +13 -0
- package/dist/assets/abnfDiagram-VRR7QNED-Dl_Fvlqg.js +1 -0
- package/dist/assets/arc-Bhg4c0ht.js +1 -0
- package/dist/assets/architecture-TIHT7OUA-BgP-IMeo.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-BuojN27B.js +36 -0
- package/dist/assets/array-CYkMkqnU.js +1 -0
- package/dist/assets/blockDiagram-677ZJIJ3-alDLbrR1.js +132 -0
- package/dist/assets/c4Diagram-LMCZKHZV-D1lKa4VT.js +10 -0
- package/dist/assets/channel-Rz20dKTK.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-Cb14ZF_k.js +1 -0
- package/dist/assets/chunk-32BRIVSS-CwWLNjMZ.js +1 -0
- package/dist/assets/chunk-52WLFC77-Be569m7d.js +10 -0
- package/dist/assets/chunk-5FCAYU7R-BSvoiWV1.js +1 -0
- package/dist/assets/chunk-5HE753X5-CRgsy8WS.js +1 -0
- package/dist/assets/chunk-5JV3BV7I-CdW477UR.js +1 -0
- package/dist/assets/chunk-5TONJI2A-CdtKzSmp.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-BKjjssLZ.js +15 -0
- package/dist/assets/chunk-7BUUIJ7U-ByAXiXdn.js +1 -0
- package/dist/assets/chunk-BIQX33UG-DeG8SCT1.js +1 -0
- package/dist/assets/chunk-C7G6YPKG-D_zHT68Q.js +1 -0
- package/dist/assets/chunk-CQNSW5MT-CHpHoUSu.js +1 -0
- package/dist/assets/chunk-CYSBUYHQ-CRP50Vov.js +1 -0
- package/dist/assets/chunk-EMLP6XTP-DX6uQQ2v.js +1 -0
- package/dist/assets/chunk-EX3LRPZG-DAJRNkHR.js +231 -0
- package/dist/assets/chunk-FWX5IMBZ-XbM5yomC.js +2 -0
- package/dist/assets/chunk-HOUHSVGY-Ci0ljnb1.js +1 -0
- package/dist/assets/chunk-ICXQ74PX-Dy2WJ71z.js +2 -0
- package/dist/assets/chunk-JG7HCLWE-2PrkzHkt.js +2 -0
- package/dist/assets/chunk-JWPE2WC7-D32uacSM.js +1 -0
- package/dist/assets/chunk-KEIR6QF5-77hsebd7.js +161 -0
- package/dist/assets/chunk-MOJQB5TN-DIAFLtiS.js +88 -0
- package/dist/assets/chunk-MOZMSUNE-BbscOaEz.js +1 -0
- package/dist/assets/chunk-OGEWGWER-ivBVxzKe.js +1 -0
- package/dist/assets/chunk-OSBZ3O6U-frD0hkdx.js +1 -0
- package/dist/assets/chunk-PUDLZKDR-B-37Rxae.js +156 -0
- package/dist/assets/chunk-Q4XR5HBZ-Y90m2RTe.js +11 -0
- package/dist/assets/chunk-QBLGF6JB-DixBqslB.js +1 -0
- package/dist/assets/chunk-R7FJI6CG-DXS1UXn_.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-n4JxhNd0.js +1 -0
- package/dist/assets/chunk-U6XO7XAA-yHlgxMmn.js +2 -0
- package/dist/assets/chunk-V7JOEXUC-Z6kuZfbl.js +206 -0
- package/dist/assets/chunk-VAUOI2AC-BOZvd2Sn.js +1 -0
- package/dist/assets/chunk-VR4S4FIN-Cs_SIQma.js +1 -0
- package/dist/assets/chunk-WYO6CB5R-CGExYbV-.js +128 -0
- package/dist/assets/chunk-XXDRQBXY-CD_EGCtT.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-CQltPwYk.js +1 -0
- package/dist/assets/chunk-YOTPTUD7-DopPXWSl.js +1 -0
- package/dist/assets/chunk-ZGVPDNZ5-CJHOeqMb.js +62 -0
- package/dist/assets/chunk-ZIRB5QZD-BBVc5mf6.js +32 -0
- package/dist/assets/classDiagram-OUVF2IWQ-C_ozTKCt.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-FOfFbbZF.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-DLjV-dTm.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-BzPhLtd5.js +1 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-BfH3zVS-.js +62 -0
- package/dist/assets/cytoscape.esm-DwdZnGOm.js +321 -0
- package/dist/assets/dagre-CY3D84Cl.js +1 -0
- package/dist/assets/dagre-VKFMJZFB-BIhwZCIZ.js +4 -0
- package/dist/assets/defaultLocale-Beh6XjaL.js +1 -0
- package/dist/assets/diagram-FQU43EPY-BHnzNM2w.js +3 -0
- package/dist/assets/diagram-G47NLZAW-C7-ZlmFT.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-C-P9Y7IR.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-Ci5lZ_T5.js +30 -0
- package/dist/assets/diagram-WEI45ONY-C1xuK2bs.js +41 -0
- package/dist/assets/dist-DelZBZpK.js +1 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-DKQ5NWMD.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-HAWySadP.js +85 -0
- package/dist/assets/eventmodeling-45OFAUF4-BcgGbVJ-.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-dtkzcC8E.js +1 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-C1hwIcOA.js +292 -0
- package/dist/assets/gitGraph-TEB2WS4Q-VByWboBU.js +1 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DYUYxs1R.js +106 -0
- package/dist/assets/graphlib-CAISHZh4.js +1 -0
- package/dist/assets/icons-C7j5jdKo.js +1 -0
- package/dist/assets/index-Bxg5fT2p.css +3 -0
- package/dist/assets/index-DKmdePUP.js +63 -0
- package/dist/assets/info-DKCQHKI2-CZeDLZxx.js +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-CnIL6tv4.js +2 -0
- package/dist/assets/init-Rr1s_RiX.js +1 -0
- package/dist/assets/ishikawaDiagram-FXEZZL3T-B7WXNycQ.js +70 -0
- package/dist/assets/journeyDiagram-5HDEW3XC-D4HvioyY.js +139 -0
- package/dist/assets/kanban-definition-HUTT4EX6-CDs763eS.js +89 -0
- package/dist/assets/line-ikKkQiFp.js +1 -0
- package/dist/assets/linear-GqPyxQkN.js +1 -0
- package/dist/assets/map-CJwjCvwr.js +1 -0
- package/dist/assets/mcp-servers-dialog-jHlDR9jP.js +5 -0
- package/dist/assets/mermaid-parser.core-CoE8cccr.js +4 -0
- package/dist/assets/mermaid-renderer-BmbSAmpG.js +2 -0
- package/dist/assets/mermaid.core-DlFSL90j.js +11 -0
- package/dist/assets/mindmap-definition-LN4V7U3C-DLdq0K9C.js +96 -0
- package/dist/assets/{monaco-CzxeAjEw.js → monaco-CdOyGlWD.js} +1 -1
- package/dist/assets/ordinal-CibZU3PM.js +1 -0
- package/dist/assets/{otel-DQf3c6v2.js → otel-BNGM50C-.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-decrrxyX.js +1 -0
- package/dist/assets/path-BAQ3hXlG.js +1 -0
- package/dist/assets/pegDiagram-2B236MQR-Cm1kPRC2.js +1 -0
- package/dist/assets/{pi-ai-DjJwSQBQ.js → pi-ai-Cr3rpCXT.js} +2 -2
- package/dist/assets/{pi-web-ui-DqxtBlZL.js → pi-web-ui-DoRGtREc.js} +371 -371
- package/dist/assets/pie-RZYD4A2V-J1CasQZl.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-Df05wjjo.js +39 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-BjCwGxJC.js +7 -0
- package/dist/assets/radar-I7S5WNFK-B-sZytUA.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BSaqNPm8.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-lzZR7PxT.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-DQtz1Fh9.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-CpMsuGY3.js +1 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-CTUvpKkv.js +1 -0
- package/dist/assets/{react-vendor-BR9MG2D-.js → react-vendor-VqdnQHHS.js} +1 -1
- package/dist/assets/requirementDiagram-TGXJPOKE-BU1kvEku.js +84 -0
- package/dist/assets/rough.esm-Djo4Abte.js +1 -0
- package/dist/assets/sankeyDiagram-HTMAVEWB-BHmvxVqH.js +40 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-CTpwICgg.js +162 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-BbLhGaa4.js +1 -0
- package/dist/assets/{skills-dialog-B0jNVGZW.js → skills-dialog-DvSnPPzu.js} +1 -1
- package/dist/assets/src-B4Tvg3gT.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-BxuDwIgT.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BGLshxcY.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-EkZsiylV.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-CqdqxA-b.js +8 -0
- package/dist/assets/timeline-definition-FHXFAJF6-C9ewOCTa.js +120 -0
- package/dist/assets/treeView-QDETBFTQ-Byb71heW.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CMV1SJsq.js +1 -0
- package/dist/assets/vennDiagram-L72KCM5P-CR49Y4Lp.js +34 -0
- package/dist/assets/wardley-OPB4EBWU-DtCHtr2V.js +1 -0
- package/dist/assets/wardleyDiagram-EHGQE667-Cn0GgwWi.js +78 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-DlmPNAvd.js +7 -0
- package/dist/index.html +8 -8
- package/package.json +2 -1
- package/server/agent-manager.mjs +174 -9
- package/server/agent-profile-files.mjs +86 -6
- package/server/agent-profile-schema.mjs +197 -0
- package/server/agent-profiles.mjs +172 -41
- package/server/auto-compaction.mjs +2 -2
- package/server/project-route-context.mjs +25 -0
- package/server/routes/agent-profiles.mjs +9 -3
- package/server/routes/backup.mjs +135 -48
- package/server/routes/project.mjs +6 -10
- package/server/routes/workspace.mjs +240 -11
- package/server/storage.mjs +34 -0
- package/server/subagents.mjs +64 -0
- package/server/tools/definitions.mjs +17 -1
- package/server/utils/platform.mjs +4 -4
- package/dist/assets/AgentProfilesPage-R7N7eDq5.js +0 -1
- package/dist/assets/ChatPanelHost-C4orgd5l.js +0 -242
- package/dist/assets/ScheduledTasksPage-BAqoK4xk.js +0 -2
- package/dist/assets/SharedConversationPage-C-1n_MmB.js +0 -1
- package/dist/assets/TerminalDock-B6KAXkIU.js +0 -2
- package/dist/assets/WorkspaceInspector-CkXyD_k4.js +0 -3
- package/dist/assets/WorkspaceReaderDialog-Dp2QAwBo.js +0 -1
- package/dist/assets/diff-line-counts-Dm5kKQOo.js +0 -10
- package/dist/assets/icons-DHoaB5uq.js +0 -1
- package/dist/assets/index-C77QCHE9.css +0 -3
- package/dist/assets/index-CZb_mGGF.js +0 -63
- package/dist/assets/mcp-servers-dialog-VbGvNekJ.js +0 -5
- package/dist/assets/useAppTheme-DsabFF9D.js +0 -1
- /package/dist/assets/{plugin-api-Ds5xGIL7.js → plugin-api-BbUkYm3Y.js} +0 -0
- /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,
|
|
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 {
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
185
|
-
await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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:
|
|
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
|
|
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('
|
|
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
|
package/server/routes/backup.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
176
|
-
const
|
|
177
|
-
const
|
|
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
|
|
182
|
-
|
|
183
|
-
readStore('
|
|
184
|
-
|
|
185
|
-
readStore('
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
427
|
+
const normalized = normalizeBackupPayload(payload)
|
|
353
428
|
const requestedSections = body?.backup && typeof body === 'object' ? body.sections : undefined
|
|
354
|
-
const selected = normalizeRestoreSections(requestedSections,
|
|
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
|
|
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 =
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
|
|
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
|
}
|