@shawnstack/quickforge 1.6.12 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/assets/AgentProfilesPage-DQ8URegq.js +1 -0
- package/dist/assets/ChatPanelHost-vi1tDqlJ.js +291 -0
- package/dist/assets/{PluginsPage-qXTjDrpd.js → PluginsPage-B8bHMwfv.js} +1 -1
- package/dist/assets/ScheduledTasksPage-AaUz5el2.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-CnFDmGvW.js → SettingsWorkspacePage-BOP9CtoI.js} +112 -105
- package/dist/assets/{SharedConversationPage-CkQ-XwYS.js → SharedConversationPage-9A4L9UcQ.js} +1 -1
- package/dist/assets/TerminalDock-DQOXpaDe.js +2 -0
- package/dist/assets/WorkspaceInspector-DaM7TJHb.js +13 -0
- package/dist/assets/icons-ko_i0WpN.js +1 -0
- package/dist/assets/index-B2eauKpo.css +3 -0
- package/dist/assets/index-B8Awq5FV.js +63 -0
- package/dist/assets/{mcp-servers-dialog-CsRQxx7A.js → mcp-servers-dialog-XE4K8PCO.js} +2 -2
- package/dist/assets/{monaco-CdOyGlWD.js → monaco-C13M09wD.js} +1 -1
- package/dist/assets/{react-vendor-VqdnQHHS.js → react-vendor-2RKYr-A4.js} +1 -1
- package/dist/assets/{skills-dialog-CmVLi_h6.js → skills-dialog-MpoxntiV.js} +1 -1
- package/dist/index.html +6 -6
- package/dist/licenses/material-icon-theme.txt +8 -0
- package/package.json +1 -1
- package/server/agent-manager.mjs +182 -39
- package/server/auto-archive.mjs +137 -0
- package/server/auto-compaction.mjs +15 -18
- package/server/conversation-compaction.mjs +69 -3
- package/server/custom-commands.mjs +17 -0
- package/server/image-generation.mjs +130 -0
- package/server/index.mjs +12 -1
- package/server/provider-config.mjs +78 -0
- package/server/routes/agent.mjs +9 -0
- package/server/routes/session-assets.mjs +49 -0
- package/server/routes/shared-conversation.mjs +10 -0
- package/server/routes/storage.mjs +4 -0
- package/server/routes/tools.mjs +1 -1
- package/server/routes/workspace.mjs +5 -2
- package/server/session-assets.mjs +134 -0
- package/server/session-persistence-lock.mjs +9 -0
- package/server/storage.mjs +24 -1
- package/server/tools/definitions.mjs +10 -0
- package/server/tools/index.mjs +2 -0
- package/dist/assets/AgentProfilesPage-CooGLweI.js +0 -1
- package/dist/assets/ChatPanelHost-TEbv0lnL.js +0 -260
- package/dist/assets/ScheduledTasksPage-BBor8Yb2.js +0 -2
- package/dist/assets/TerminalDock-CvqBBMPg.js +0 -2
- package/dist/assets/WorkspaceInspector-6JGc6VGb.js +0 -13
- package/dist/assets/icons-C7j5jdKo.js +0 -1
- package/dist/assets/index-DxNzlPmc.js +0 -63
- package/dist/assets/index-HVxfqXfB.css +0 -3
|
@@ -6,6 +6,16 @@ import { cacheDir } from './storage.mjs'
|
|
|
6
6
|
export const DEFAULT_COMPACT_KEEP_TURNS = 0
|
|
7
7
|
const MAX_COMPACT_KEEP_TURNS = 20
|
|
8
8
|
const MIN_SUMMARY_SOURCE_CHARS = 1600
|
|
9
|
+
const COMPACT_SUMMARY_OPEN_TAG = '<compact_summary>'
|
|
10
|
+
const COMPACT_SUMMARY_CLOSE_TAG = '</compact_summary>'
|
|
11
|
+
const COMPACTION_DETAILS_KEY = 'quickforgeCompaction'
|
|
12
|
+
const LEGACY_COMPACT_SUMMARY_INTROS = [
|
|
13
|
+
'The previous conversation has been compacted.',
|
|
14
|
+
'The previous conversation has been automatically compacted.',
|
|
15
|
+
'Existing rolling compact summary from earlier conversation history:',
|
|
16
|
+
]
|
|
17
|
+
const LEGACY_COMPACTION_NOTICE_PREFIX = '已基于当前对话创建压缩后的新对话:'
|
|
18
|
+
const LEGACY_COMPACTION_NOTICE_SUFFIX = '压缩前历史已保存到本地备份。'
|
|
9
19
|
|
|
10
20
|
export const COMPACT_SYSTEM_PROMPT = `你是 QuickForge 的“历史对话压缩器”。你的任务是把一段较长的 AI 助手对话压缩成后续模型继续工作所需的最小充分上下文。
|
|
11
21
|
|
|
@@ -72,8 +82,60 @@ function normalizeKeepTurns(value) {
|
|
|
72
82
|
return Math.min(MAX_COMPACT_KEEP_TURNS, Math.max(0, Math.floor(parsed)))
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
function messageContentText(message) {
|
|
86
|
+
const content = message?.content
|
|
87
|
+
if (typeof content === 'string') return content
|
|
88
|
+
if (!Array.isArray(content)) return ''
|
|
89
|
+
return content
|
|
90
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
91
|
+
.map((block) => block.text)
|
|
92
|
+
.join('\n')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function compactionMessageKind(message) {
|
|
96
|
+
const kind = message?.details?.[COMPACTION_DETAILS_KEY]?.kind
|
|
97
|
+
if (kind === 'summary' || kind === 'notice') return kind
|
|
98
|
+
|
|
99
|
+
const text = messageContentText(message).trim()
|
|
100
|
+
if (
|
|
101
|
+
message?.role === 'user'
|
|
102
|
+
&& LEGACY_COMPACT_SUMMARY_INTROS.some((intro) => text.startsWith(intro))
|
|
103
|
+
&& text.includes(COMPACT_SUMMARY_OPEN_TAG)
|
|
104
|
+
&& text.lastIndexOf(COMPACT_SUMMARY_CLOSE_TAG) > text.indexOf(COMPACT_SUMMARY_OPEN_TAG)
|
|
105
|
+
) return 'summary'
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
message?.role === 'assistant'
|
|
109
|
+
&& text.startsWith(LEGACY_COMPACTION_NOTICE_PREFIX)
|
|
110
|
+
&& text.includes(LEGACY_COMPACTION_NOTICE_SUFFIX)
|
|
111
|
+
) return 'notice'
|
|
112
|
+
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function compactionMessageDetails(kind) {
|
|
117
|
+
return { [COMPACTION_DETAILS_KEY]: { version: 1, kind } }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isCompactSummaryMessage(message) {
|
|
121
|
+
return compactionMessageKind(message) === 'summary'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isCompactionNoticeMessage(message) {
|
|
125
|
+
return compactionMessageKind(message) === 'notice'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function extractCompactSummaryText(message) {
|
|
129
|
+
const text = messageContentText(message).trim()
|
|
130
|
+
const openIndex = text.indexOf(COMPACT_SUMMARY_OPEN_TAG)
|
|
131
|
+
const closeIndex = text.lastIndexOf(COMPACT_SUMMARY_CLOSE_TAG)
|
|
132
|
+
if (openIndex < 0 || closeIndex <= openIndex) return text
|
|
133
|
+
return text.slice(openIndex + COMPACT_SUMMARY_OPEN_TAG.length, closeIndex).trim()
|
|
134
|
+
}
|
|
135
|
+
|
|
75
136
|
function isUserMessage(message) {
|
|
76
|
-
return message?.role === 'user' || message?.role === 'user-with-attachments'
|
|
137
|
+
return (message?.role === 'user' || message?.role === 'user-with-attachments')
|
|
138
|
+
&& !isCompactSummaryMessage(message)
|
|
77
139
|
}
|
|
78
140
|
|
|
79
141
|
function truncateMiddle(value, maxLength) {
|
|
@@ -200,7 +262,7 @@ export function splitMessagesForCompaction(messages, options = {}) {
|
|
|
200
262
|
if (keepTurns <= 0) {
|
|
201
263
|
return {
|
|
202
264
|
keepTurns,
|
|
203
|
-
compactRange: sourceMessages.
|
|
265
|
+
compactRange: sourceMessages.filter((message) => !isCompactionNoticeMessage(message)),
|
|
204
266
|
recentTail: [],
|
|
205
267
|
tailStart: sourceMessages.length,
|
|
206
268
|
}
|
|
@@ -219,9 +281,13 @@ export function splitMessagesForCompaction(messages, options = {}) {
|
|
|
219
281
|
|
|
220
282
|
if (seenUserTurns < keepTurns) tailStart = 0
|
|
221
283
|
|
|
284
|
+
const compactRange = sourceMessages
|
|
285
|
+
.slice(0, tailStart)
|
|
286
|
+
.filter((message) => !isCompactionNoticeMessage(message))
|
|
287
|
+
|
|
222
288
|
return {
|
|
223
289
|
keepTurns,
|
|
224
|
-
compactRange
|
|
290
|
+
compactRange,
|
|
225
291
|
recentTail: sourceMessages.slice(tailStart),
|
|
226
292
|
tailStart,
|
|
227
293
|
}
|
|
@@ -15,6 +15,12 @@ const commandNamePattern = /^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
|
|
|
15
15
|
* `description` values here.
|
|
16
16
|
*/
|
|
17
17
|
const builtinCommandCatalog = [
|
|
18
|
+
{
|
|
19
|
+
name: 'init',
|
|
20
|
+
description: 'Generate or update a concise AGENTS.md contributor guide for the current repository.',
|
|
21
|
+
argumentHint: '',
|
|
22
|
+
permissionNote: 'may edit files and run commands',
|
|
23
|
+
},
|
|
18
24
|
{
|
|
19
25
|
name: 'plan',
|
|
20
26
|
description: 'Create a plan first; this turn cannot edit files or run commands.',
|
|
@@ -367,6 +373,8 @@ export function parseInternalCommandInvocation(message) {
|
|
|
367
373
|
const text = textFromUserMessage(message).trim()
|
|
368
374
|
if (/^\/(?:help|\?)(?:\s+.*)?$/i.test(text)) return { type: 'help' }
|
|
369
375
|
if (/^\/commands(?:\s+.*)?$/i.test(text)) return { type: 'list' }
|
|
376
|
+
if (/^\/init\s*$/i.test(text)) return { type: 'init' }
|
|
377
|
+
if (/^\/init(?:\s+[\s\S]+)$/i.test(text)) return { type: 'invalid-init-args' }
|
|
370
378
|
if (/^\/clear\s*$/i.test(text)) return { type: 'clear' }
|
|
371
379
|
if (/^\/clear(?:\s+[\s\S]+)$/i.test(text)) return { type: 'invalid-clear-args' }
|
|
372
380
|
|
|
@@ -407,6 +415,15 @@ export async function handleInternalCommand(invocation, workspaceRoot, commandDi
|
|
|
407
415
|
return { plan: true, args: invocation.args }
|
|
408
416
|
}
|
|
409
417
|
|
|
418
|
+
if (invocation.type === 'init') {
|
|
419
|
+
if (!workspaceRoot) return 'Initialization requires an active project chat.'
|
|
420
|
+
return { init: true }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (invocation.type === 'invalid-init-args') {
|
|
424
|
+
return 'Usage: /init'
|
|
425
|
+
}
|
|
426
|
+
|
|
410
427
|
if (invocation.type === 'review') {
|
|
411
428
|
if (!workspaceRoot) return 'Review requires an active project chat.'
|
|
412
429
|
return { review: true, args: invocation.args || '' }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { builtinImagesModels } from '@earendil-works/pi-ai/providers/all'
|
|
2
|
+
import { resolveOpenRouterConfig } from './provider-config.mjs'
|
|
3
|
+
import { deleteSessionAsset, writeSessionAsset } from './session-assets.mjs'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_IMAGE_MODEL = 'google/gemini-2.5-flash-image'
|
|
6
|
+
export const MAX_GENERATED_IMAGES = 4
|
|
7
|
+
export const MAX_GENERATED_IMAGE_BYTES = 50 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
function requestError(message, statusCode = 400) {
|
|
10
|
+
const error = new Error(message)
|
|
11
|
+
error.statusCode = statusCode
|
|
12
|
+
return error
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeBucket(value) {
|
|
16
|
+
if (value?.scope === 'project') return { scope: 'project', projectId: value.projectId }
|
|
17
|
+
return { scope: 'global' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveImagesModels(runtime) {
|
|
21
|
+
const candidate = runtime.imagesModels
|
|
22
|
+
if (candidate && typeof candidate.getModel === 'function') return candidate
|
|
23
|
+
if (typeof candidate === 'function') return candidate()
|
|
24
|
+
return builtinImagesModels()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function imageByteLength(data) {
|
|
28
|
+
const normalized = typeof data === 'string' ? data.replace(/\s+/g, '') : ''
|
|
29
|
+
if (!normalized || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
|
30
|
+
throw requestError('Image provider returned invalid base64 data')
|
|
31
|
+
}
|
|
32
|
+
const buffer = Buffer.from(normalized, 'base64')
|
|
33
|
+
if (buffer.toString('base64') !== normalized) {
|
|
34
|
+
throw requestError('Image provider returned invalid base64 data')
|
|
35
|
+
}
|
|
36
|
+
return buffer.byteLength
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resultText(output) {
|
|
40
|
+
return output
|
|
41
|
+
.filter((item) => item?.type === 'text' && typeof item.text === 'string' && item.text.trim())
|
|
42
|
+
.map((item) => item.text.trim())
|
|
43
|
+
.join('\n\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function contentSummary(count, text) {
|
|
47
|
+
const generated = `Generated ${count} image${count === 1 ? '' : 's'}.`
|
|
48
|
+
return text ? `${generated}\n\n${text}` : generated
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function generateSessionImages(params, context = {}, runtime = {}) {
|
|
52
|
+
const prompt = typeof params?.prompt === 'string' ? params.prompt.trim() : ''
|
|
53
|
+
if (!prompt) throw requestError('prompt is required')
|
|
54
|
+
|
|
55
|
+
const sessionId = typeof context.sessionId === 'string' ? context.sessionId : params?.sessionId
|
|
56
|
+
if (!sessionId) throw requestError('sessionId is required')
|
|
57
|
+
const bucket = normalizeBucket(context.bucket || context.sessionBucket || params?.bucket || {
|
|
58
|
+
scope: context.scope,
|
|
59
|
+
projectId: context.projectId,
|
|
60
|
+
})
|
|
61
|
+
const requestedModel = typeof params?.model === 'string' && params.model.trim()
|
|
62
|
+
? params.model.trim()
|
|
63
|
+
: DEFAULT_IMAGE_MODEL
|
|
64
|
+
|
|
65
|
+
const imagesModels = resolveImagesModels(runtime)
|
|
66
|
+
const model = imagesModels.getModel('openrouter', requestedModel)
|
|
67
|
+
if (!model || model.api !== 'openrouter-images') {
|
|
68
|
+
throw requestError(`Unsupported image model: ${requestedModel}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resolveConfig = runtime.resolveOpenRouterConfig || resolveOpenRouterConfig
|
|
72
|
+
const config = await resolveConfig(runtime)
|
|
73
|
+
const requestModel = config.baseUrl ? { ...model, baseUrl: config.baseUrl } : model
|
|
74
|
+
const response = await imagesModels.generateImages(
|
|
75
|
+
requestModel,
|
|
76
|
+
{ input: [{ type: 'text', text: prompt }] },
|
|
77
|
+
{
|
|
78
|
+
apiKey: config.apiKey,
|
|
79
|
+
...(config.headers ? { headers: config.headers } : {}),
|
|
80
|
+
...(runtime.signal || context.signal ? { signal: runtime.signal || context.signal } : {}),
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (response?.stopReason !== 'stop') {
|
|
85
|
+
throw new Error(response?.errorMessage || `Image generation stopped: ${response?.stopReason || 'unknown'}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const output = Array.isArray(response.output) ? response.output : []
|
|
89
|
+
const images = output.filter((item) => item?.type === 'image')
|
|
90
|
+
if (!images.length) throw new Error('Image generation returned no images')
|
|
91
|
+
if (images.length > MAX_GENERATED_IMAGES) {
|
|
92
|
+
throw requestError(`Image generation returned more than ${MAX_GENERATED_IMAGES} images`, 413)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let totalBytes = 0
|
|
96
|
+
for (const image of images) {
|
|
97
|
+
totalBytes += imageByteLength(image.data)
|
|
98
|
+
if (totalBytes > MAX_GENERATED_IMAGE_BYTES) {
|
|
99
|
+
throw requestError(`Generated images exceed the ${MAX_GENERATED_IMAGE_BYTES} byte limit`, 413)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const writeAsset = runtime.writeSessionAsset || writeSessionAsset
|
|
104
|
+
const deleteAsset = runtime.deleteSessionAsset || deleteSessionAsset
|
|
105
|
+
const assets = []
|
|
106
|
+
try {
|
|
107
|
+
for (const image of images) {
|
|
108
|
+
assets.push(await writeAsset(bucket, sessionId, image))
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await Promise.allSettled(assets.map((asset) => deleteAsset(bucket, sessionId, asset.assetId)))
|
|
112
|
+
throw error
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const text = resultText(output)
|
|
116
|
+
return {
|
|
117
|
+
content: contentSummary(assets.length, text),
|
|
118
|
+
details: {
|
|
119
|
+
type: 'generated_image_result',
|
|
120
|
+
sessionId,
|
|
121
|
+
prompt,
|
|
122
|
+
model: requestedModel,
|
|
123
|
+
assets,
|
|
124
|
+
text,
|
|
125
|
+
usage: response.usage,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const generateImages = generateSessionImages
|
package/server/index.mjs
CHANGED
|
@@ -19,10 +19,12 @@ import { handleSkillsApi } from './routes/skills.mjs'
|
|
|
19
19
|
import { handleAgentApi } from './routes/agent.mjs'
|
|
20
20
|
import { handleAgentProfilesApi } from './routes/agent-profiles.mjs'
|
|
21
21
|
import { handleScheduledTasksApi, startScheduledTaskRunner, stopScheduledTaskRunner } from './routes/scheduled-tasks.mjs'
|
|
22
|
+
import { startAutoArchiveRunner, stopAutoArchiveRunner } from './auto-archive.mjs'
|
|
22
23
|
import { handleBackupApi } from './routes/backup.mjs'
|
|
23
24
|
import { handleSystemApi } from './routes/system.mjs'
|
|
24
25
|
import { handleSharesApi } from './routes/shares.mjs'
|
|
25
26
|
import { handleSharedConversationApi } from './routes/shared-conversation.mjs'
|
|
27
|
+
import { handleSessionAssetsApi } from './routes/session-assets.mjs'
|
|
26
28
|
import { handleLanAccessApi, renderLanUnlockPage } from './routes/lan-access.mjs'
|
|
27
29
|
import { handleMcpApi } from './routes/mcp.mjs'
|
|
28
30
|
import { handlePluginsApi } from './routes/plugins.mjs'
|
|
@@ -146,6 +148,7 @@ async function performRestart() {
|
|
|
146
148
|
logger.info(`Restart supervisor started (PID ${supervisorPid}).`)
|
|
147
149
|
|
|
148
150
|
stopScheduledTaskRunner()
|
|
151
|
+
stopAutoArchiveRunner()
|
|
149
152
|
stopVite()
|
|
150
153
|
await shutdownAgentManager()
|
|
151
154
|
await shutdownMcpConnections()
|
|
@@ -220,6 +223,7 @@ function spawnUpdateSupervisor(update) {
|
|
|
220
223
|
async function shutdownForUpdate() {
|
|
221
224
|
logger.info('Shutting down QuickForge for external updater.')
|
|
222
225
|
stopScheduledTaskRunner()
|
|
226
|
+
stopAutoArchiveRunner()
|
|
223
227
|
stopVite()
|
|
224
228
|
await shutdownAgentManager()
|
|
225
229
|
await shutdownMcpConnections()
|
|
@@ -288,6 +292,11 @@ async function handleApi(req, res, url) {
|
|
|
288
292
|
return
|
|
289
293
|
}
|
|
290
294
|
|
|
295
|
+
if (pathname.startsWith('/api/session-assets/')) {
|
|
296
|
+
await handleSessionAssetsApi(req, res, url)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
291
300
|
if (pathname === '/api/lan-access/status' || pathname === '/api/lan-access/settings' || pathname === '/api/lan-access/unlock' || pathname === '/api/lan-access/logout' || pathname === '/api/lan-access/revoke-all') {
|
|
292
301
|
await handleLanAccessApi(req, res, url, {
|
|
293
302
|
port,
|
|
@@ -364,7 +373,7 @@ async function handleApi(req, res, url) {
|
|
|
364
373
|
}
|
|
365
374
|
|
|
366
375
|
// Project workspace inspector routes
|
|
367
|
-
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/file' || pathname === '/api/workspace/resolve-path' || pathname.startsWith('/api/workspace/preview/')) {
|
|
376
|
+
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/file' || pathname === '/api/workspace/resolve-path' || pathname === '/api/workspace/open-external' || pathname.startsWith('/api/workspace/preview/')) {
|
|
368
377
|
await handleWorkspaceApi(req, res, url)
|
|
369
378
|
return
|
|
370
379
|
}
|
|
@@ -665,6 +674,7 @@ await resetStaleTaskStatuses()
|
|
|
665
674
|
await initializeActiveProject()
|
|
666
675
|
setActiveWorkspaceRootForFilesystem(getWorkspaceRoot())
|
|
667
676
|
startScheduledTaskRunner()
|
|
677
|
+
startAutoArchiveRunner()
|
|
668
678
|
|
|
669
679
|
server.on('error', (error) => {
|
|
670
680
|
// Handle listen errors (most commonly EADDRINUSE). Without this, Node would
|
|
@@ -703,6 +713,7 @@ server.listen(port, host, () => {
|
|
|
703
713
|
async function gracefulShutdown(signal) {
|
|
704
714
|
logger.info(`Received ${signal}, shutting down gracefully...`)
|
|
705
715
|
stopScheduledTaskRunner()
|
|
716
|
+
stopAutoArchiveRunner()
|
|
706
717
|
stopVite()
|
|
707
718
|
await shutdownAgentManager()
|
|
708
719
|
await shutdownMcpConnections()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readStore } from './storage.mjs'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'
|
|
4
|
+
|
|
5
|
+
function requestError(message, statusCode = 400) {
|
|
6
|
+
const error = new Error(message)
|
|
7
|
+
error.statusCode = statusCode
|
|
8
|
+
return error
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function providersFromStore(store) {
|
|
12
|
+
return Array.isArray(store)
|
|
13
|
+
? store
|
|
14
|
+
: Object.entries(store || {}).map(([id, provider]) => (
|
|
15
|
+
provider && typeof provider === 'object' ? { id, ...provider } : provider
|
|
16
|
+
))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isOpenRouterProvider(provider) {
|
|
20
|
+
if (!provider || typeof provider !== 'object') return false
|
|
21
|
+
const id = String(provider.id || '').trim().toLowerCase()
|
|
22
|
+
const name = String(provider.name || '').trim().toLowerCase()
|
|
23
|
+
if (id === 'openrouter' || name === 'openrouter') return true
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
return new URL(String(provider.baseUrl || '')).hostname.toLowerCase() === 'openrouter.ai'
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeHeaders(provider) {
|
|
33
|
+
const source = provider?.headers || provider?.models?.find((model) => model?.headers)?.headers
|
|
34
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return undefined
|
|
35
|
+
const headers = {}
|
|
36
|
+
for (const [key, value] of Object.entries(source)) {
|
|
37
|
+
if (typeof value === 'string' && key.trim()) headers[key.trim()] = value
|
|
38
|
+
}
|
|
39
|
+
return Object.keys(headers).length ? headers : undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findProviderKey(keys, providerName) {
|
|
43
|
+
if (!keys || typeof keys !== 'object' || Array.isArray(keys)) return undefined
|
|
44
|
+
if (typeof keys[providerName] === 'string' && keys[providerName].trim()) return keys[providerName].trim()
|
|
45
|
+
|
|
46
|
+
const wanted = String(providerName || '').trim().toLowerCase()
|
|
47
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
48
|
+
const normalized = key.trim().toLowerCase()
|
|
49
|
+
if ((normalized === wanted || normalized === 'openrouter') && typeof value === 'string' && value.trim()) {
|
|
50
|
+
return value.trim()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function resolveOpenRouterConfig(runtime = {}) {
|
|
57
|
+
const read = runtime.readStore || readStore
|
|
58
|
+
const [providerStore, providerKeys] = await Promise.all([
|
|
59
|
+
read('custom-providers'),
|
|
60
|
+
read('provider-keys'),
|
|
61
|
+
])
|
|
62
|
+
const provider = providersFromStore(providerStore).find(isOpenRouterProvider)
|
|
63
|
+
if (!provider) throw requestError('OpenRouter provider is not configured')
|
|
64
|
+
|
|
65
|
+
const providerName = String(provider.name || provider.id || 'openrouter').trim()
|
|
66
|
+
const apiKey = findProviderKey(providerKeys, providerName)
|
|
67
|
+
if (!apiKey) throw requestError('OpenRouter API key is not configured')
|
|
68
|
+
|
|
69
|
+
const baseUrl = typeof provider.baseUrl === 'string' && provider.baseUrl.trim()
|
|
70
|
+
? provider.baseUrl.trim().replace(/\/$/, '')
|
|
71
|
+
: DEFAULT_OPENROUTER_BASE_URL
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
apiKey,
|
|
75
|
+
baseUrl,
|
|
76
|
+
headers: normalizeHeaders(provider),
|
|
77
|
+
}
|
|
78
|
+
}
|
package/server/routes/agent.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
touchSession,
|
|
17
17
|
listSessions,
|
|
18
18
|
updateSessionAccessMode,
|
|
19
|
+
updateSessionTitle,
|
|
19
20
|
updateSessionYoloMode,
|
|
20
21
|
updateSessionModel,
|
|
21
22
|
updateSessionThinkingLevel,
|
|
@@ -153,6 +154,14 @@ export async function handleAgentApi(req, res, url) {
|
|
|
153
154
|
return
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// POST /api/agents/:sessionId/title — update the authoritative session title
|
|
158
|
+
if (req.method === 'POST' && subPath === 'title') {
|
|
159
|
+
const body = await readJsonBody(req)
|
|
160
|
+
const result = await updateSessionTitle(sessionId, body?.title)
|
|
161
|
+
sendJson(res, 200, result)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
156
165
|
// POST /api/agents/:sessionId/access-mode — update session Agent access mode
|
|
157
166
|
if (req.method === 'POST' && subPath === 'access-mode') {
|
|
158
167
|
const body = await readJsonBody(req)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { findSessionBucket } from '../storage.mjs'
|
|
2
|
+
import { readSessionAsset } from '../session-assets.mjs'
|
|
3
|
+
import { decodeSegment } from '../utils/response.mjs'
|
|
4
|
+
|
|
5
|
+
function notFound(message = 'Image asset not found') {
|
|
6
|
+
const error = new Error(message)
|
|
7
|
+
error.statusCode = 404
|
|
8
|
+
return error
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function sendSessionAsset(res, bucket, sessionId, assetId) {
|
|
12
|
+
let asset
|
|
13
|
+
try {
|
|
14
|
+
asset = await readSessionAsset(bucket, sessionId, assetId)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error?.code === 'ENOENT') throw notFound()
|
|
17
|
+
throw error
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
res.writeHead(200, {
|
|
21
|
+
'content-type': asset.mimeType,
|
|
22
|
+
'content-length': String(asset.size),
|
|
23
|
+
'cache-control': 'private, max-age=31536000, immutable',
|
|
24
|
+
'content-disposition': 'inline',
|
|
25
|
+
'x-content-type-options': 'nosniff',
|
|
26
|
+
})
|
|
27
|
+
res.end(asset.data)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleSessionAssetsApi(req, res, url) {
|
|
31
|
+
if (req.method !== 'GET') {
|
|
32
|
+
const error = new Error('Session asset endpoints require GET')
|
|
33
|
+
error.statusCode = 405
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const parts = url.pathname.split('/').filter(Boolean)
|
|
38
|
+
const sessionId = decodeSegment(parts[2])
|
|
39
|
+
const assetId = decodeSegment(parts[3])
|
|
40
|
+
if (!sessionId || !assetId || parts.length !== 4) {
|
|
41
|
+
const error = new Error('Missing session image asset path')
|
|
42
|
+
error.statusCode = 400
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bucket = await findSessionBucket(sessionId)
|
|
47
|
+
if (!bucket) throw notFound('Session not found')
|
|
48
|
+
await sendSessionAsset(res, bucket, sessionId, assetId)
|
|
49
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
2
|
import { readSessionValue, readStore } from '../storage.mjs'
|
|
3
|
+
import { sendSessionAsset } from './session-assets.mjs'
|
|
3
4
|
import { abortRun, restoreAgent, runPrompt, getSessionState, getSessionEventBus, updateSessionModel, updateSessionThinkingLevel } from '../agent-manager.mjs'
|
|
4
5
|
import {
|
|
5
6
|
assertShareActive,
|
|
@@ -307,6 +308,7 @@ export async function handleSharedConversationApi(req, res, url) {
|
|
|
307
308
|
const parts = url.pathname.split('/').filter(Boolean)
|
|
308
309
|
const shareId = decodeSegment(parts[2])
|
|
309
310
|
const action = parts[3]
|
|
311
|
+
const actionId = parts[4]
|
|
310
312
|
|
|
311
313
|
if (!shareId) {
|
|
312
314
|
const error = new Error('Missing share id')
|
|
@@ -343,6 +345,14 @@ export async function handleSharedConversationApi(req, res, url) {
|
|
|
343
345
|
const record = await requireShareAuth(req, shareId)
|
|
344
346
|
if (record.permission === 'operate' && !record.passwordHash) throw passwordRequiredError()
|
|
345
347
|
|
|
348
|
+
if (req.method === 'GET' && action === 'assets' && actionId) {
|
|
349
|
+
const bucket = record.scope === 'project'
|
|
350
|
+
? { scope: 'project', projectId: record.projectId }
|
|
351
|
+
: { scope: 'global' }
|
|
352
|
+
await sendSessionAsset(res, bucket, record.sessionId, decodeSegment(actionId))
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
|
|
346
356
|
if (req.method === 'GET' && action === 'session') {
|
|
347
357
|
sendJson(res, 200, await sharedSessionPayload(record))
|
|
348
358
|
return
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
2
|
import { readStore, writeStore, atomicUpdate, getComparable, getStoreRevision, readSessionStoreScoped, readSessionValue, writeSessionValue, deleteSessionValue, ensureStorage, dataDir, configDir, storageDir, cacheDir, logsDir } from '../storage.mjs'
|
|
3
|
+
import { AUTO_ARCHIVE_SETTINGS_KEY, archiveInactiveSessions, normalizeAutoArchiveSettings } from '../auto-archive.mjs'
|
|
3
4
|
import { directorySize } from '../utils/workspace.mjs'
|
|
4
5
|
|
|
5
6
|
const metadataIndexCache = new Map()
|
|
@@ -194,6 +195,9 @@ export async function handleStorageApi(req, res, url) {
|
|
|
194
195
|
data[key] = body?.value
|
|
195
196
|
return data
|
|
196
197
|
})
|
|
198
|
+
if (store === 'settings' && key === AUTO_ARCHIVE_SETTINGS_KEY && normalizeAutoArchiveSettings(body?.value).enabled) {
|
|
199
|
+
await archiveInactiveSessions()
|
|
200
|
+
}
|
|
197
201
|
sendJson(res, 200, { ok: true })
|
|
198
202
|
return
|
|
199
203
|
}
|
package/server/routes/tools.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { callPluginTool, createPluginToolDefinitions, isPluginToolName } from '.
|
|
|
8
8
|
import { safeReadTools } from '../approval-store.mjs'
|
|
9
9
|
import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
|
|
10
10
|
|
|
11
|
-
const directRouteDisabledTools = new Set(['run_subagent', 'manage_global_memory'])
|
|
11
|
+
const directRouteDisabledTools = new Set(['run_subagent', 'manage_global_memory', 'generate_image'])
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* GET /api/tools — returns canonical tool definitions (no project context needed).
|
|
@@ -304,9 +304,12 @@ async function countWorkspaceLines(context, relativePath) {
|
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
async function listGitStatus(context) {
|
|
307
|
+
export async function listGitStatus(context) {
|
|
308
308
|
if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, files: [] }
|
|
309
|
-
const result = await git(
|
|
309
|
+
const result = await git(
|
|
310
|
+
['status', '--porcelain=v1', '-z', '--untracked-files=all'],
|
|
311
|
+
context.workspaceRoot,
|
|
312
|
+
)
|
|
310
313
|
const files = parseGitStatus(result.stdout)
|
|
311
314
|
const numstat = await collectNumstat(context)
|
|
312
315
|
for (const file of files) {
|