@shawnstack/quickforge 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/quickforge.mjs +72 -7
- package/dist/assets/{anthropic-By-wpU1w.js → anthropic-DLvtwHL2.js} +2 -2
- package/dist/assets/{azure-openai-responses-C8spS__i.js → azure-openai-responses-D68z7hLN.js} +1 -1
- package/dist/assets/css-utils-rkE68RDy.js +1 -0
- package/dist/assets/{google-DiIcyajo.js → google-B_sSaRBM.js} +1 -1
- package/dist/assets/{google-gemini-cli-BXZFGMXD.js → google-gemini-cli-CYqGXjGi.js} +1 -1
- package/dist/assets/google-shared-XhYUKiGZ.js +11 -0
- package/dist/assets/{google-vertex-D93MV5Cx.js → google-vertex-DSMuB4YB.js} +1 -1
- package/dist/assets/icons-BsZ9PlYY.js +1 -0
- package/dist/assets/index-BqFfVQJM.css +3 -0
- package/dist/assets/index-DoraECXN.js +3187 -0
- package/dist/assets/lit-vendor-1dsGB-Iy.js +2 -0
- package/dist/assets/{mistral-BAJNGYqd.js → mistral-BZngRB4x.js} +2 -2
- package/dist/assets/{openai-codex-responses-BHHCy65K.js → openai-codex-responses-Niu7xDYK.js} +1 -1
- package/dist/assets/openai-completions-B2bhb9k0.js +5 -0
- package/dist/assets/{openai-responses-CP9-AyAD.js → openai-responses-CDYDv8yL.js} +1 -1
- package/dist/assets/{openai-responses-shared-_z7sua8J.js → openai-responses-shared-BIKPTpEQ.js} +1 -1
- package/dist/assets/react-vendor-Ds3ovY0w.js +9 -0
- package/dist/assets/rolldown-runtime-CkqCuyE9.js +1 -0
- package/dist/index.html +7 -3
- package/package.json +14 -13
- package/server/agent-manager.mjs +1053 -0
- package/server/conversation-compaction.mjs +302 -0
- package/server/custom-commands.mjs +344 -0
- package/server/index.mjs +322 -32
- package/server/project-config.mjs +80 -31
- package/server/reasoning-cache.mjs +51 -0
- package/server/restart-supervisor.mjs +38 -0
- package/server/routes/agent.mjs +323 -0
- package/server/routes/backup.mjs +250 -0
- package/server/routes/instructions.mjs +6 -17
- package/server/routes/project.mjs +46 -10
- package/server/routes/scheduled-tasks.mjs +424 -0
- package/server/routes/shared-conversation.mjs +404 -0
- package/server/routes/shares.mjs +84 -0
- package/server/routes/skills.mjs +145 -0
- package/server/routes/static.mjs +4 -3
- package/server/routes/storage.mjs +58 -10
- package/server/routes/system.mjs +35 -0
- package/server/routes/tools.mjs +53 -2
- package/server/session-utils.mjs +102 -0
- package/server/share-store.mjs +468 -0
- package/server/skills.mjs +539 -0
- package/server/storage.mjs +247 -6
- package/server/system-prompt.mjs +67 -0
- package/server/tools/definitions.mjs +120 -0
- package/server/tools/index.mjs +167 -46
- package/server/utils/logger.mjs +34 -0
- package/server/utils/network.mjs +38 -0
- package/server/utils/platform.mjs +30 -0
- package/server/utils/response.mjs +8 -1
- package/skills/ai-context-package/SKILL.md +104 -0
- package/skills/ai-context-package/skill.json +9 -0
- package/skills/code-review/SKILL.md +23 -0
- package/skills/code-review/skill.json +9 -0
- package/skills/frontend-react/SKILL.md +22 -0
- package/skills/frontend-react/skill.json +9 -0
- package/skills/quickforge-project/SKILL.md +22 -0
- package/skills/quickforge-project/skill.json +9 -0
- package/dist/assets/chunk-62oNxeRG.js +0 -1
- package/dist/assets/confirm-dialog-4mZt9XEq.js +0 -1
- package/dist/assets/google-shared-CXUHW-9O.js +0 -11
- package/dist/assets/index-Bq6VHkyY.js +0 -3048
- package/dist/assets/index-D7uXa1RT.css +0 -3
- package/dist/assets/openai-completions-BtZAvOiJ.js +0 -5
- package/dist/assets/prompt-dialog-BGMKszUz.js +0 -1
- /package/dist/assets/{github-copilot-headers-C0toI16e.js → github-copilot-headers-CrI0CIJ7.js} +0 -0
- /package/dist/assets/{hash-fDQBJsbb.js → hash-Bt1aVMQ3.js} +0 -0
- /package/dist/assets/{headers-Drkm68SQ.js → headers-5EYI0_pl.js} +0 -0
- /package/dist/assets/{openai-CuiHR4mv.js → openai-Cn7eGqwa.js} +0 -0
- /package/dist/assets/{transform-messages-BFwlToJ0.js → transform-messages-CV4kCtBB.js} +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
|
+
import { readSessionValue, readStore } from '../storage.mjs'
|
|
3
|
+
import { abortRun, restoreAgent, runPrompt, getSessionState, getSessionEventBus, updateSessionModel, updateSessionThinkingLevel } from '../agent-manager.mjs'
|
|
4
|
+
import {
|
|
5
|
+
assertShareActive,
|
|
6
|
+
issueConversationShareToken,
|
|
7
|
+
parseCookies,
|
|
8
|
+
readConversationShare,
|
|
9
|
+
rollbackSharedSessionMessages,
|
|
10
|
+
shareCookieName,
|
|
11
|
+
verifySharePassword,
|
|
12
|
+
verifyShareToken,
|
|
13
|
+
} from '../share-store.mjs'
|
|
14
|
+
|
|
15
|
+
const MAX_SHARED_MESSAGE_BYTES = 64 * 1024
|
|
16
|
+
const CLIENT_MESSAGE_ID_FIELD = 'quickforgeClientMessageId'
|
|
17
|
+
const CLIENT_MESSAGE_ID_MAX_LENGTH = 128
|
|
18
|
+
|
|
19
|
+
function sanitizedClientMessageId(value) {
|
|
20
|
+
if (typeof value !== 'string') return undefined
|
|
21
|
+
const trimmed = value.trim()
|
|
22
|
+
if (!trimmed || trimmed.length > CLIENT_MESSAGE_ID_MAX_LENGTH) return undefined
|
|
23
|
+
return /^[A-Za-z0-9._:-]+$/.test(trimmed) ? trimmed : undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function objectMetadata(value) {
|
|
27
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function attachClientMessageId(message, clientMessageId) {
|
|
31
|
+
if (!clientMessageId) return message
|
|
32
|
+
return {
|
|
33
|
+
...message,
|
|
34
|
+
metadata: {
|
|
35
|
+
...objectMetadata(message.metadata),
|
|
36
|
+
[CLIENT_MESSAGE_ID_FIELD]: clientMessageId,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function publicSharePayload(record) {
|
|
42
|
+
return {
|
|
43
|
+
id: record.id,
|
|
44
|
+
permission: record.permission,
|
|
45
|
+
title: record.titleSnapshot,
|
|
46
|
+
expiresAt: record.expiresAt,
|
|
47
|
+
revokedAt: record.revokedAt,
|
|
48
|
+
scope: record.scope,
|
|
49
|
+
projectId: record.projectId,
|
|
50
|
+
hasPassword: Boolean(record.passwordHash),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sanitizeMessage(message) {
|
|
55
|
+
if (!message || typeof message !== 'object') return message
|
|
56
|
+
if (message.role === 'system') return null
|
|
57
|
+
return message
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sanitizeSession(session, record) {
|
|
61
|
+
const messages = Array.isArray(session?.messages) ? session.messages.map(sanitizeMessage).filter(Boolean) : []
|
|
62
|
+
return {
|
|
63
|
+
id: record.id,
|
|
64
|
+
shareId: record.id,
|
|
65
|
+
sessionId: record.sessionId,
|
|
66
|
+
title: session?.title || record.titleSnapshot || 'New chat',
|
|
67
|
+
permission: record.permission,
|
|
68
|
+
expiresAt: record.expiresAt,
|
|
69
|
+
scope: record.scope,
|
|
70
|
+
projectId: record.projectId,
|
|
71
|
+
systemPrompt: '',
|
|
72
|
+
model: sanitizeModel(session?.model),
|
|
73
|
+
thinkingLevel: session?.thinkingLevel || 'off',
|
|
74
|
+
tools: Array.isArray(session?.tools) ? session.tools : [],
|
|
75
|
+
yoloMode: Boolean(session?.yoloMode),
|
|
76
|
+
messages,
|
|
77
|
+
isStreaming: Boolean(session?.isStreaming || session?.taskStatus === 'running'),
|
|
78
|
+
taskStatus: session?.taskStatus || session?.status,
|
|
79
|
+
errorMessage: session?.errorMessage,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizeModel(model) {
|
|
84
|
+
if (!model || typeof model !== 'object') return { provider: 'shared', id: 'shared' }
|
|
85
|
+
return {
|
|
86
|
+
...model,
|
|
87
|
+
apiKey: undefined,
|
|
88
|
+
key: undefined,
|
|
89
|
+
headers: undefined,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sanitizeIncomingModel(model) {
|
|
94
|
+
if (!model || typeof model !== 'object') return null
|
|
95
|
+
const sanitized = sanitizeModel(model)
|
|
96
|
+
if (!sanitized.id || !sanitized.provider) return null
|
|
97
|
+
return sanitized
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sanitizeEvent(event) {
|
|
101
|
+
if (!event || typeof event !== 'object') return event
|
|
102
|
+
const next = { ...event }
|
|
103
|
+
if (next.message) next.message = sanitizeMessage(next.message)
|
|
104
|
+
if (Array.isArray(next.messages)) next.messages = next.messages.map(sanitizeMessage).filter(Boolean)
|
|
105
|
+
return next
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeSseEvent(res, event, data) {
|
|
109
|
+
const payload = JSON.stringify(data)
|
|
110
|
+
res.write(`event: ${event}\n`)
|
|
111
|
+
for (const line of payload.split('\n')) {
|
|
112
|
+
res.write(`data: ${line}\n`)
|
|
113
|
+
}
|
|
114
|
+
res.write('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function handleSharedEvents(req, res, record) {
|
|
118
|
+
await restoreAgent(record.sessionId)
|
|
119
|
+
const eventBus = getSessionEventBus(record.sessionId)
|
|
120
|
+
if (!eventBus) {
|
|
121
|
+
const error = new Error('Session not found')
|
|
122
|
+
error.statusCode = 404
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
res.writeHead(200, {
|
|
127
|
+
'content-type': 'text/event-stream',
|
|
128
|
+
'cache-control': 'no-cache, no-transform',
|
|
129
|
+
'connection': 'keep-alive',
|
|
130
|
+
'x-accel-buffering': 'no',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
writeSseEvent(res, 'state', await sharedSessionPayload(record))
|
|
134
|
+
|
|
135
|
+
const keepAlive = setInterval(() => {
|
|
136
|
+
try {
|
|
137
|
+
res.write(': ping\n\n')
|
|
138
|
+
} catch {
|
|
139
|
+
cleanup()
|
|
140
|
+
}
|
|
141
|
+
}, 15000)
|
|
142
|
+
|
|
143
|
+
const onAgentEvent = (event) => {
|
|
144
|
+
try {
|
|
145
|
+
const payload = sanitizeEvent(event)
|
|
146
|
+
writeSseEvent(res, payload.type || 'agent_event', payload)
|
|
147
|
+
} catch {
|
|
148
|
+
cleanup()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
clearInterval(keepAlive)
|
|
154
|
+
eventBus.removeListener('agent_event', onAgentEvent)
|
|
155
|
+
if (!res.writableEnded) res.end()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
eventBus.on('agent_event', onAgentEvent)
|
|
159
|
+
req.on('close', cleanup)
|
|
160
|
+
req.on('error', cleanup)
|
|
161
|
+
res.on('error', cleanup)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function passwordRequiredError() {
|
|
165
|
+
const error = new Error('Editable shares require a non-empty password')
|
|
166
|
+
error.statusCode = 403
|
|
167
|
+
return error
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function setShareCookie(res, shareId, token) {
|
|
171
|
+
const maxAge = 60 * 60 * 24 * 7
|
|
172
|
+
const cookie = [
|
|
173
|
+
`${shareCookieName(shareId)}=${encodeURIComponent(token)}`,
|
|
174
|
+
'HttpOnly',
|
|
175
|
+
'SameSite=Lax',
|
|
176
|
+
`Max-Age=${maxAge}`,
|
|
177
|
+
`Path=/`,
|
|
178
|
+
].join('; ')
|
|
179
|
+
res.setHeader('Set-Cookie', cookie)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function requireShareAuth(req, shareId) {
|
|
183
|
+
const record = await readConversationShare(shareId)
|
|
184
|
+
assertShareActive(record)
|
|
185
|
+
if (!record.passwordHash) return record
|
|
186
|
+
const token = parseCookies(req.headers.cookie).get(shareCookieName(shareId))
|
|
187
|
+
if (!verifyShareToken(record, token)) {
|
|
188
|
+
const error = new Error('Share authentication required')
|
|
189
|
+
error.statusCode = 401
|
|
190
|
+
throw error
|
|
191
|
+
}
|
|
192
|
+
return record
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function assertOperate(record) {
|
|
196
|
+
if (record.permission !== 'operate') {
|
|
197
|
+
const error = new Error('This shared conversation is read-only.')
|
|
198
|
+
error.statusCode = 403
|
|
199
|
+
throw error
|
|
200
|
+
}
|
|
201
|
+
if (!record.passwordHash) {
|
|
202
|
+
throw passwordRequiredError()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function messageFromBody(body, record, req) {
|
|
207
|
+
const content = typeof body?.content === 'string'
|
|
208
|
+
? body.content
|
|
209
|
+
: typeof body?.message === 'string'
|
|
210
|
+
? body.message
|
|
211
|
+
: typeof body?.message?.content === 'string'
|
|
212
|
+
? body.message.content
|
|
213
|
+
: ''
|
|
214
|
+
const attachments = Array.isArray(body?.message?.attachments) ? body.message.attachments : undefined
|
|
215
|
+
if (!content.trim() && !attachments?.length) {
|
|
216
|
+
const error = new Error('Missing message content')
|
|
217
|
+
error.statusCode = 400
|
|
218
|
+
throw error
|
|
219
|
+
}
|
|
220
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_SHARED_MESSAGE_BYTES) {
|
|
221
|
+
const error = new Error('Message is too large')
|
|
222
|
+
error.statusCode = 413
|
|
223
|
+
throw error
|
|
224
|
+
}
|
|
225
|
+
const clientMessageId = sanitizedClientMessageId(body?.clientMessageId)
|
|
226
|
+
|| sanitizedClientMessageId(body?.message?.metadata?.[CLIENT_MESSAGE_ID_FIELD])
|
|
227
|
+
const metadata = {
|
|
228
|
+
...objectMetadata(body?.message?.metadata),
|
|
229
|
+
source: 'lan-share',
|
|
230
|
+
shareId: record.id,
|
|
231
|
+
permission: record.permission,
|
|
232
|
+
remoteAddress: req.socket.remoteAddress,
|
|
233
|
+
}
|
|
234
|
+
if (clientMessageId) metadata[CLIENT_MESSAGE_ID_FIELD] = clientMessageId
|
|
235
|
+
const message = {
|
|
236
|
+
role: attachments?.length ? 'user-with-attachments' : 'user',
|
|
237
|
+
content,
|
|
238
|
+
timestamp: body?.message?.timestamp || new Date().toISOString(),
|
|
239
|
+
metadata,
|
|
240
|
+
}
|
|
241
|
+
if (attachments?.length) message.attachments = attachments
|
|
242
|
+
return attachClientMessageId(message, clientMessageId)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function sharedSessionPayload(record) {
|
|
246
|
+
const activeState = getSessionState(record.sessionId)
|
|
247
|
+
if (activeState) return sanitizeSession(activeState, record)
|
|
248
|
+
const session = await readSessionValue(record.sessionId)
|
|
249
|
+
if (!session) {
|
|
250
|
+
const error = new Error('Session not found')
|
|
251
|
+
error.statusCode = 404
|
|
252
|
+
throw error
|
|
253
|
+
}
|
|
254
|
+
return sanitizeSession(session, record)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function sanitizeProvider(provider) {
|
|
258
|
+
if (!provider || typeof provider !== 'object') return null
|
|
259
|
+
const models = Array.isArray(provider.models)
|
|
260
|
+
? provider.models.map(sanitizeModel).filter((model) => model?.id && model?.provider && model?.api)
|
|
261
|
+
: []
|
|
262
|
+
if (!models.length) return null
|
|
263
|
+
return {
|
|
264
|
+
id: provider.id || provider.name || models[0].provider,
|
|
265
|
+
name: provider.name || models[0].provider,
|
|
266
|
+
type: provider.type || models[0].api,
|
|
267
|
+
baseUrl: provider.baseUrl,
|
|
268
|
+
models,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function listSharedModelProviders() {
|
|
273
|
+
const providers = await readStore('custom-providers')
|
|
274
|
+
return Object.values(providers || {}).map(sanitizeProvider).filter(Boolean)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function readConfiguredModel(model) {
|
|
278
|
+
if (!model || typeof model !== 'object') return null
|
|
279
|
+
const providers = await readStore('custom-providers')
|
|
280
|
+
for (const provider of Object.values(providers || {})) {
|
|
281
|
+
if (!provider || typeof provider !== 'object') continue
|
|
282
|
+
const matched = Array.isArray(provider.models)
|
|
283
|
+
? provider.models.find((candidate) => {
|
|
284
|
+
return candidate?.id === model.id && candidate?.provider === model.provider && candidate?.api === model.api
|
|
285
|
+
})
|
|
286
|
+
: undefined
|
|
287
|
+
if (matched) return matched
|
|
288
|
+
}
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function handleSharedConversationApi(req, res, url) {
|
|
293
|
+
const parts = url.pathname.split('/').filter(Boolean)
|
|
294
|
+
const shareId = decodeSegment(parts[2])
|
|
295
|
+
const action = parts[3]
|
|
296
|
+
|
|
297
|
+
if (!shareId) {
|
|
298
|
+
const error = new Error('Missing share id')
|
|
299
|
+
error.statusCode = 400
|
|
300
|
+
throw error
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (req.method === 'POST' && action === 'unlock') {
|
|
304
|
+
const body = await readJsonBody(req)
|
|
305
|
+
const record = await readConversationShare(shareId)
|
|
306
|
+
assertShareActive(record)
|
|
307
|
+
if (record.permission === 'operate' && !record.passwordHash) {
|
|
308
|
+
throw passwordRequiredError()
|
|
309
|
+
}
|
|
310
|
+
const ok = await verifySharePassword(record, body?.password)
|
|
311
|
+
if (!ok) {
|
|
312
|
+
const error = new Error('Invalid share password')
|
|
313
|
+
error.statusCode = 401
|
|
314
|
+
throw error
|
|
315
|
+
}
|
|
316
|
+
const { token, share } = await issueConversationShareToken(shareId)
|
|
317
|
+
setShareCookie(res, shareId, token)
|
|
318
|
+
sendJson(res, 200, { ok: true, share: publicSharePayload(share), permission: share.permission, title: share.titleSnapshot, expiresAt: share.expiresAt })
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (req.method === 'GET' && action === 'meta') {
|
|
323
|
+
const record = await readConversationShare(shareId)
|
|
324
|
+
assertShareActive(record)
|
|
325
|
+
sendJson(res, 200, { share: publicSharePayload(record) })
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const record = await requireShareAuth(req, shareId)
|
|
330
|
+
if (record.permission === 'operate' && !record.passwordHash) throw passwordRequiredError()
|
|
331
|
+
|
|
332
|
+
if (req.method === 'GET' && action === 'session') {
|
|
333
|
+
sendJson(res, 200, await sharedSessionPayload(record))
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (req.method === 'GET' && action === 'models') {
|
|
338
|
+
assertOperate(record)
|
|
339
|
+
sendJson(res, 200, { providers: await listSharedModelProviders() })
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (req.method === 'GET' && action === 'events') {
|
|
344
|
+
await handleSharedEvents(req, res, record)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (req.method === 'POST' && action === 'message') {
|
|
349
|
+
assertOperate(record)
|
|
350
|
+
const body = await readJsonBody(req)
|
|
351
|
+
await restoreAgent(record.sessionId)
|
|
352
|
+
const result = await runPrompt(record.sessionId, messageFromBody(body, record, req))
|
|
353
|
+
sendJson(res, 200, result)
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (req.method === 'POST' && action === 'model') {
|
|
358
|
+
assertOperate(record)
|
|
359
|
+
const body = await readJsonBody(req)
|
|
360
|
+
const model = sanitizeIncomingModel(body?.model)
|
|
361
|
+
if (!model) {
|
|
362
|
+
const error = new Error('Missing model in request body')
|
|
363
|
+
error.statusCode = 400
|
|
364
|
+
throw error
|
|
365
|
+
}
|
|
366
|
+
await restoreAgent(record.sessionId)
|
|
367
|
+
const configured = await readConfiguredModel(model)
|
|
368
|
+
sendJson(res, 200, updateSessionModel(record.sessionId, configured ? sanitizeModel(configured) : model))
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (req.method === 'POST' && action === 'thinking-level') {
|
|
373
|
+
assertOperate(record)
|
|
374
|
+
const body = await readJsonBody(req)
|
|
375
|
+
const thinkingLevel = body?.thinkingLevel
|
|
376
|
+
if (!thinkingLevel || typeof thinkingLevel !== 'string') {
|
|
377
|
+
const error = new Error('Missing thinkingLevel in request body')
|
|
378
|
+
error.statusCode = 400
|
|
379
|
+
throw error
|
|
380
|
+
}
|
|
381
|
+
await restoreAgent(record.sessionId)
|
|
382
|
+
sendJson(res, 200, updateSessionThinkingLevel(record.sessionId, thinkingLevel))
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (req.method === 'POST' && action === 'abort') {
|
|
387
|
+
assertOperate(record)
|
|
388
|
+
const result = await abortRun(record.sessionId)
|
|
389
|
+
sendJson(res, 200, result)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (req.method === 'POST' && action === 'rollback') {
|
|
394
|
+
assertOperate(record)
|
|
395
|
+
const body = await readJsonBody(req)
|
|
396
|
+
const result = await rollbackSharedSessionMessages(record, body?.messageIndex)
|
|
397
|
+
sendJson(res, 200, { ok: true, rollbackIndex: result.rollbackIndex, session: sanitizeSession(result.session, record) })
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const error = new Error('This operation is not allowed for shared conversations.')
|
|
402
|
+
error.statusCode = 403
|
|
403
|
+
throw error
|
|
404
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
|
+
import { readSessionValue } from '../storage.mjs'
|
|
3
|
+
import { getLanUrls } from '../utils/network.mjs'
|
|
4
|
+
import {
|
|
5
|
+
createConversationShare,
|
|
6
|
+
listConversationShares,
|
|
7
|
+
revokeConversationShare,
|
|
8
|
+
} from '../share-store.mjs'
|
|
9
|
+
|
|
10
|
+
function localBaseUrl(req, port) {
|
|
11
|
+
const forwardedProto = req.headers['x-forwarded-proto']
|
|
12
|
+
const protocol = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto || 'http'
|
|
13
|
+
const host = req.headers.host || `127.0.0.1:${port}`
|
|
14
|
+
return `${protocol}://${host}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clipboardText({ url }) {
|
|
18
|
+
return url
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shareUrlForRequest(req, shareId, port) {
|
|
22
|
+
const lanBase = getLanUrls(port)[0]
|
|
23
|
+
const baseUrl = lanBase || localBaseUrl(req, port)
|
|
24
|
+
return `${baseUrl}/share/${encodeURIComponent(shareId)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleSharesApi(req, res, url, context = {}) {
|
|
28
|
+
const parts = url.pathname.split('/').filter(Boolean)
|
|
29
|
+
|
|
30
|
+
if (req.method === 'GET' && url.pathname === '/api/shares') {
|
|
31
|
+
const sessionId = url.searchParams.get('sessionId') || undefined
|
|
32
|
+
sendJson(res, 200, { shares: await listConversationShares(sessionId) })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method === 'POST' && url.pathname === '/api/shares') {
|
|
37
|
+
const body = await readJsonBody(req)
|
|
38
|
+
const sessionId = body?.sessionId
|
|
39
|
+
const permission = body?.permission
|
|
40
|
+
const passwordProvided = typeof body?.password === 'string'
|
|
41
|
+
const password = passwordProvided ? body.password.trim() : undefined
|
|
42
|
+
const expiresAt = typeof body?.expiresAt === 'string' && body.expiresAt ? body.expiresAt : undefined
|
|
43
|
+
|
|
44
|
+
const session = sessionId ? await readSessionValue(sessionId) : null
|
|
45
|
+
if (!session) {
|
|
46
|
+
const error = new Error('Session not found')
|
|
47
|
+
error.statusCode = 404
|
|
48
|
+
throw error
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const share = await createConversationShare({
|
|
52
|
+
sessionId,
|
|
53
|
+
permission,
|
|
54
|
+
password: passwordProvided ? password : undefined,
|
|
55
|
+
expiresAt,
|
|
56
|
+
titleSnapshot: session.title,
|
|
57
|
+
scope: session.scope,
|
|
58
|
+
projectId: session.projectId,
|
|
59
|
+
createdFromHost: req.socket.remoteAddress,
|
|
60
|
+
})
|
|
61
|
+
const shareUrl = shareUrlForRequest(req, share.id, context.port)
|
|
62
|
+
const text = clipboardText({ url: shareUrl })
|
|
63
|
+
sendJson(res, 201, {
|
|
64
|
+
ok: true,
|
|
65
|
+
share,
|
|
66
|
+
url: shareUrl,
|
|
67
|
+
password: passwordProvided ? password : undefined,
|
|
68
|
+
clipboardText: text,
|
|
69
|
+
lanUrls: getLanUrls(context.port),
|
|
70
|
+
})
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (req.method === 'DELETE' && parts.length === 3 && parts[0] === 'api' && parts[1] === 'shares') {
|
|
75
|
+
const shareId = decodeSegment(parts[2])
|
|
76
|
+
const share = await revokeConversationShare(shareId)
|
|
77
|
+
sendJson(res, 200, { ok: true, share })
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const error = new Error('Not found')
|
|
82
|
+
error.statusCode = 404
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { sendJson, readJsonBody } from '../utils/response.mjs'
|
|
2
|
+
import { getActiveProject, projectContextFromId, readProjectConfig } from '../project-config.mjs'
|
|
3
|
+
import { atomicProjectConfigUpdate } from '../storage.mjs'
|
|
4
|
+
import {
|
|
5
|
+
filterKnownGlobalSkillNames,
|
|
6
|
+
filterKnownProjectSkillNames,
|
|
7
|
+
projectSkillSearchPaths,
|
|
8
|
+
listGlobalSkillSummaries,
|
|
9
|
+
listProjectSkillSummaries,
|
|
10
|
+
skillSearchPaths,
|
|
11
|
+
} from '../skills.mjs'
|
|
12
|
+
|
|
13
|
+
function getProject(config, projectId) {
|
|
14
|
+
if (!projectId) return getActiveProject(config)
|
|
15
|
+
return config.projects.find((project) => project.id === projectId)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function selectedSkillsForProject(project) {
|
|
19
|
+
return Array.isArray(project?.skills) ? project.skills : []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function selectedGlobalSkills(config) {
|
|
23
|
+
return Array.isArray(config.globalSkills) ? config.globalSkills : []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function filterSelectedSkills(selectedSkills, skills) {
|
|
27
|
+
const known = new Set(skills.map((skill) => skill.name))
|
|
28
|
+
return selectedSkills.filter((name) => known.has(name))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function projectWorkspaceRoot(projectId) {
|
|
32
|
+
if (!projectId) return null
|
|
33
|
+
try {
|
|
34
|
+
const context = await projectContextFromId(projectId)
|
|
35
|
+
return context.workspaceRoot
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function handleSkillsApi(req, res, url) {
|
|
42
|
+
const config = await readProjectConfig()
|
|
43
|
+
const projectId = url.searchParams.get('projectId')
|
|
44
|
+
const scope = url.searchParams.get('scope') === 'global' ? 'global' : 'project'
|
|
45
|
+
|
|
46
|
+
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
47
|
+
if (scope === 'global') {
|
|
48
|
+
sendJson(res, 200, {
|
|
49
|
+
scope: 'global',
|
|
50
|
+
skills: await listGlobalSkillSummaries(),
|
|
51
|
+
selectedSkills: selectedGlobalSkills(config),
|
|
52
|
+
searchPaths: skillSearchPaths.global,
|
|
53
|
+
})
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const project = getProject(config, projectId)
|
|
58
|
+
const workspaceRoot = await projectWorkspaceRoot(project?.id)
|
|
59
|
+
const skills = workspaceRoot ? await listProjectSkillSummaries(workspaceRoot) : []
|
|
60
|
+
sendJson(res, 200, {
|
|
61
|
+
scope: 'project',
|
|
62
|
+
skills,
|
|
63
|
+
projectId: project?.id ?? null,
|
|
64
|
+
selectedSkills: filterSelectedSkills(selectedSkillsForProject(project), skills),
|
|
65
|
+
searchPaths: workspaceRoot ? projectSkillSearchPaths(workspaceRoot) : skillSearchPaths.project,
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (req.method === 'GET' && url.pathname === '/api/skills/global') {
|
|
71
|
+
sendJson(res, 200, {
|
|
72
|
+
scope: 'global',
|
|
73
|
+
selectedSkills: selectedGlobalSkills(config),
|
|
74
|
+
})
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (req.method === 'GET' && url.pathname === '/api/skills/project') {
|
|
79
|
+
const project = getProject(config, projectId)
|
|
80
|
+
sendJson(res, 200, {
|
|
81
|
+
scope: 'project',
|
|
82
|
+
projectId: project?.id ?? null,
|
|
83
|
+
selectedSkills: selectedSkillsForProject(project),
|
|
84
|
+
})
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (req.method === 'PUT' && url.pathname === '/api/skills/global') {
|
|
89
|
+
const body = await readJsonBody(req)
|
|
90
|
+
const selectedSkills = await filterKnownGlobalSkillNames(body?.selectedSkills)
|
|
91
|
+
const updated = await atomicProjectConfigUpdate((cfg) => {
|
|
92
|
+
cfg.globalSkills = selectedSkills
|
|
93
|
+
return cfg
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
sendJson(res, 200, {
|
|
97
|
+
scope: 'global',
|
|
98
|
+
selectedSkills: selectedGlobalSkills(updated),
|
|
99
|
+
projects: updated.projects,
|
|
100
|
+
})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (req.method === 'PUT' && url.pathname === '/api/skills/project') {
|
|
105
|
+
const body = await readJsonBody(req)
|
|
106
|
+
const targetProjectId = body?.projectId || projectId
|
|
107
|
+
if (!targetProjectId) {
|
|
108
|
+
const error = new Error('Missing projectId')
|
|
109
|
+
error.statusCode = 400
|
|
110
|
+
throw error
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const workspaceRoot = await projectWorkspaceRoot(targetProjectId)
|
|
114
|
+
if (!workspaceRoot) {
|
|
115
|
+
const error = new Error('Unknown project')
|
|
116
|
+
error.statusCode = 404
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const selectedSkills = await filterKnownProjectSkillNames(body?.selectedSkills, workspaceRoot)
|
|
121
|
+
const updated = await atomicProjectConfigUpdate((cfg) => {
|
|
122
|
+
const project = cfg.projects.find((item) => item.id === targetProjectId)
|
|
123
|
+
if (!project) {
|
|
124
|
+
const error = new Error('Unknown project')
|
|
125
|
+
error.statusCode = 404
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
128
|
+
project.skills = selectedSkills
|
|
129
|
+
return cfg
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const project = updated.projects.find((item) => item.id === targetProjectId)
|
|
133
|
+
sendJson(res, 200, {
|
|
134
|
+
scope: 'project',
|
|
135
|
+
projectId: targetProjectId,
|
|
136
|
+
selectedSkills: selectedSkillsForProject(project),
|
|
137
|
+
projects: updated.projects,
|
|
138
|
+
})
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const error = new Error('Not found')
|
|
143
|
+
error.statusCode = 404
|
|
144
|
+
throw error
|
|
145
|
+
}
|
package/server/routes/static.mjs
CHANGED
|
@@ -27,10 +27,11 @@ function getContentType(filePath) {
|
|
|
27
27
|
export async function serveStatic(req, res, url) {
|
|
28
28
|
const distDir = path.join(projectRoot, 'dist')
|
|
29
29
|
const requested = decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname)
|
|
30
|
-
const normalized = path.normalize(requested).replace(/^([.][.][\/])+/, '')
|
|
31
|
-
let filePath = path.
|
|
30
|
+
const normalized = path.normalize(requested).replace(/^([.][.][\/])+/, '').replace(/^[/\\]+/, '')
|
|
31
|
+
let filePath = path.resolve(distDir, normalized)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const relative = path.relative(distDir, filePath)
|
|
34
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
34
35
|
res.writeHead(403)
|
|
35
36
|
res.end('Forbidden')
|
|
36
37
|
return
|