@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
3
|
-
import { readStore, writeStore, getComparable, dataDir, configDir, storageDir, cacheDir, logsDir } from '../storage.mjs'
|
|
3
|
+
import { readStore, writeStore, atomicUpdate, getComparable, readSessionStoreScoped, readSessionValue, writeSessionValue, deleteSessionValue, ensureStorage, dataDir, configDir, storageDir, cacheDir, logsDir } from '../storage.mjs'
|
|
4
4
|
import { directorySize } from '../utils/workspace.mjs'
|
|
5
5
|
|
|
6
6
|
export async function handleStorageApi(req, res, url) {
|
|
@@ -37,8 +37,24 @@ export async function handleStorageApi(req, res, url) {
|
|
|
37
37
|
if (req.method === 'GET' && parts[3] === 'index') {
|
|
38
38
|
const indexName = decodeSegment(parts[4])
|
|
39
39
|
const direction = url.searchParams.get('direction') === 'desc' ? 'desc' : 'asc'
|
|
40
|
-
const
|
|
41
|
-
const
|
|
40
|
+
const scope = url.searchParams.get('scope')
|
|
41
|
+
const projectId = url.searchParams.get('projectId')
|
|
42
|
+
const limitParam = url.searchParams.get('limit')
|
|
43
|
+
const offsetParam = url.searchParams.get('offset')
|
|
44
|
+
|
|
45
|
+
await ensureStorage()
|
|
46
|
+
|
|
47
|
+
let data
|
|
48
|
+
if (scope && (store === 'sessions' || store === 'sessions-metadata')) {
|
|
49
|
+
data = await readSessionStoreScoped(store, scope, scope === 'project' ? projectId : undefined)
|
|
50
|
+
} else {
|
|
51
|
+
data = await readStore(store)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let values = Object.values(data)
|
|
55
|
+
if (store === 'sessions-metadata') {
|
|
56
|
+
values = values.filter((value) => value?.messageCount !== 0)
|
|
57
|
+
}
|
|
42
58
|
values.sort((a, b) => {
|
|
43
59
|
const left = getComparable(a, indexName)
|
|
44
60
|
const right = getComparable(b, indexName)
|
|
@@ -48,7 +64,16 @@ export async function handleStorageApi(req, res, url) {
|
|
|
48
64
|
const result = String(left).localeCompare(String(right))
|
|
49
65
|
return direction === 'desc' ? -result : result
|
|
50
66
|
})
|
|
51
|
-
|
|
67
|
+
|
|
68
|
+
const total = values.length
|
|
69
|
+
const limit = limitParam ? parseInt(limitParam, 10) : undefined
|
|
70
|
+
const offset = offsetParam ? parseInt(offsetParam, 10) : 0
|
|
71
|
+
|
|
72
|
+
if (limit && limit > 0) {
|
|
73
|
+
sendJson(res, 200, { values: values.slice(offset, offset + limit), total })
|
|
74
|
+
} else {
|
|
75
|
+
sendJson(res, 200, { values, total })
|
|
76
|
+
}
|
|
52
77
|
return
|
|
53
78
|
}
|
|
54
79
|
|
|
@@ -74,6 +99,11 @@ export async function handleStorageApi(req, res, url) {
|
|
|
74
99
|
}
|
|
75
100
|
|
|
76
101
|
if (req.method === 'GET') {
|
|
102
|
+
if (store === 'sessions') {
|
|
103
|
+
sendJson(res, 200, { value: await readSessionValue(key) })
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
const data = await readStore(store)
|
|
78
108
|
sendJson(res, 200, { value: Object.prototype.hasOwnProperty.call(data, key) ? data[key] : null })
|
|
79
109
|
return
|
|
@@ -81,17 +111,35 @@ export async function handleStorageApi(req, res, url) {
|
|
|
81
111
|
|
|
82
112
|
if (req.method === 'PUT') {
|
|
83
113
|
const body = await readJsonBody(req)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
if (store === 'sessions') {
|
|
115
|
+
await writeSessionValue(key, body?.value)
|
|
116
|
+
sendJson(res, 200, { ok: true })
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await atomicUpdate(store, (data) => {
|
|
121
|
+
data[key] = body?.value
|
|
122
|
+
return data
|
|
123
|
+
})
|
|
87
124
|
sendJson(res, 200, { ok: true })
|
|
88
125
|
return
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
if (req.method === 'DELETE') {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
if (store === 'sessions') {
|
|
130
|
+
await deleteSessionValue(key)
|
|
131
|
+
await atomicUpdate('sessions-metadata', (data) => {
|
|
132
|
+
delete data[key]
|
|
133
|
+
return data
|
|
134
|
+
})
|
|
135
|
+
sendJson(res, 200, { ok: true })
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await atomicUpdate(store, (data) => {
|
|
140
|
+
delete data[key]
|
|
141
|
+
return data
|
|
142
|
+
})
|
|
95
143
|
sendJson(res, 200, { ok: true })
|
|
96
144
|
return
|
|
97
145
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { sendJson } from '../utils/response.mjs'
|
|
2
|
+
import { getLanUrls } from '../utils/network.mjs'
|
|
3
|
+
|
|
4
|
+
export async function handleSystemApi(req, res, url, context) {
|
|
5
|
+
if (req.method === 'POST' && url.pathname === '/api/system/restart') {
|
|
6
|
+
if (req.headers['x-quickforge-action'] !== 'restart') {
|
|
7
|
+
const error = new Error('Forbidden action')
|
|
8
|
+
error.statusCode = 403
|
|
9
|
+
throw error
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const result = await context.requestRestart()
|
|
13
|
+
sendJson(res, 202, result)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (req.method === 'GET' && url.pathname === '/api/system/status') {
|
|
18
|
+
sendJson(res, 200, await context.getSystemStatus())
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (req.method === 'GET' && url.pathname === '/api/system/network') {
|
|
23
|
+
sendJson(res, 200, {
|
|
24
|
+
host: context.host,
|
|
25
|
+
port: context.port,
|
|
26
|
+
lanUrls: getLanUrls(context.port),
|
|
27
|
+
remoteEnabled: context.remoteEnabled === true,
|
|
28
|
+
})
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const error = new Error('Not found')
|
|
33
|
+
error.statusCode = 404
|
|
34
|
+
throw error
|
|
35
|
+
}
|
package/server/routes/tools.mjs
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { readStore } from '../storage.mjs'
|
|
3
|
+
import { toolHandlers, loadSkillToolContext } from '../tools/index.mjs'
|
|
4
|
+
import { createSkillTools, workspaceTools } from '../tools/definitions.mjs'
|
|
5
|
+
import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/tools — returns canonical tool definitions (no project context needed).
|
|
9
|
+
*/
|
|
10
|
+
export async function handleGetTools(_req, res) {
|
|
11
|
+
const config = await readProjectConfig()
|
|
12
|
+
const activeProject = config.projects.find((project) => project.id === config.activeProjectId) || config.projects[0]
|
|
13
|
+
const skillTools = await createSkillTools({
|
|
14
|
+
globalSkillNames: config.globalSkills,
|
|
15
|
+
projectSkillNames: activeProject?.skills,
|
|
16
|
+
workspaceRoot: activeProject?.path,
|
|
17
|
+
})
|
|
18
|
+
sendJson(res, 200, { tools: [...skillTools, ...workspaceTools] })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dangerousTools = new Set(['write_file', 'edit_file', 'run_command'])
|
|
22
|
+
|
|
23
|
+
async function assertYoloEnabledForTool(name) {
|
|
24
|
+
if (!dangerousTools.has(name)) return
|
|
25
|
+
|
|
26
|
+
const settings = await readStore('settings')
|
|
27
|
+
const yoloMode = settings?.['yolo-mode'] === true || settings?.['yolo-mode'] === 'true'
|
|
28
|
+
if (!yoloMode) {
|
|
29
|
+
const error = new Error('YOLO mode is disabled. Enable it to use this tool.')
|
|
30
|
+
error.statusCode = 403
|
|
31
|
+
throw error
|
|
32
|
+
}
|
|
33
|
+
}
|
|
4
34
|
|
|
5
35
|
export async function handleToolApi(req, res, url) {
|
|
6
36
|
if (req.method !== 'POST') {
|
|
@@ -13,8 +43,27 @@ export async function handleToolApi(req, res, url) {
|
|
|
13
43
|
let name = decodeSegment(parts[2])
|
|
14
44
|
let context
|
|
15
45
|
|
|
46
|
+
if (name === 'activate_skill' || name === 'read_skill_resource') {
|
|
47
|
+
const config = await readProjectConfig()
|
|
48
|
+
const activeProject = config.projects.find((project) => project.id === config.activeProjectId) || config.projects[0]
|
|
49
|
+
context = await loadSkillToolContext({
|
|
50
|
+
globalSkillNames: config.globalSkills,
|
|
51
|
+
projectSkillNames: activeProject?.skills,
|
|
52
|
+
workspaceRoot: activeProject?.path,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
16
56
|
if (parts[1] === 'projects' && parts[3] === 'tools') {
|
|
17
57
|
context = await projectContextFromId(decodeSegment(parts[2]))
|
|
58
|
+
const config = await readProjectConfig()
|
|
59
|
+
context = {
|
|
60
|
+
...context,
|
|
61
|
+
...(await loadSkillToolContext({
|
|
62
|
+
globalSkillNames: config.globalSkills,
|
|
63
|
+
projectSkillNames: context.project?.skills,
|
|
64
|
+
workspaceRoot: context.workspaceRoot,
|
|
65
|
+
})),
|
|
66
|
+
}
|
|
18
67
|
name = decodeSegment(parts[4])
|
|
19
68
|
}
|
|
20
69
|
|
|
@@ -25,6 +74,8 @@ export async function handleToolApi(req, res, url) {
|
|
|
25
74
|
throw error
|
|
26
75
|
}
|
|
27
76
|
|
|
77
|
+
await assertYoloEnabledForTool(name)
|
|
78
|
+
|
|
28
79
|
const params = await readJsonBody(req)
|
|
29
80
|
const result = await handler(params || {}, context)
|
|
30
81
|
sendJson(res, 200, result)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { streamSimple } from '@mariozechner/pi-ai'
|
|
2
|
+
import { buildInstructionsPayload } from './project-config.mjs'
|
|
3
|
+
import { composeSystemPrompt } from './system-prompt.mjs'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// System prompt
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export async function buildSystemPrompt(projectId) {
|
|
10
|
+
return composeSystemPrompt(await buildInstructionsPayload(projectId))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Simple title generation (from first user message)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export function generateTitle(messages) {
|
|
18
|
+
const firstUser = messages.find(
|
|
19
|
+
(m) => m.role === 'user' || m.role === 'user-with-attachments',
|
|
20
|
+
)
|
|
21
|
+
if (!firstUser) return 'New chat'
|
|
22
|
+
const content = firstUser.content
|
|
23
|
+
const text = typeof content === 'string' ? content : Array.isArray(content)
|
|
24
|
+
? content.filter((b) => b.type === 'text').map((b) => b.text ?? '').join(' ')
|
|
25
|
+
: ''
|
|
26
|
+
const normalized = text.trim().replace(/\s+/g, ' ')
|
|
27
|
+
if (!normalized) return 'New chat'
|
|
28
|
+
return normalized.length > 46 ? `${normalized.slice(0, 43)}...` : normalized
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// AI title generation
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function normalizeAiTitle(value) {
|
|
36
|
+
return value
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/^[[\s"'""''`]+|[\]`\s"'""''.。,!!??,,::;;]+$/g, '')
|
|
39
|
+
.replace(/\s+/g, ' ')
|
|
40
|
+
.slice(0, 80)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function generateAiTitle(messages, model, thinkingLevel, getApiKey) {
|
|
44
|
+
const firstUser = messages.find((m) => m.role === 'user' || m.role === 'user-with-attachments')
|
|
45
|
+
if (!firstUser) return null
|
|
46
|
+
|
|
47
|
+
const userText = typeof firstUser.content === 'string'
|
|
48
|
+
? firstUser.content
|
|
49
|
+
: Array.isArray(firstUser.content)
|
|
50
|
+
? firstUser.content.filter((b) => b.type === 'text').map((b) => b.text ?? '').join(' ')
|
|
51
|
+
: ''
|
|
52
|
+
|
|
53
|
+
if (!userText.trim()) return null
|
|
54
|
+
|
|
55
|
+
const firstAssistant = messages.find((m) => m.role === 'assistant')
|
|
56
|
+
let assistantReply = ''
|
|
57
|
+
if (firstAssistant) {
|
|
58
|
+
const content = firstAssistant.content
|
|
59
|
+
if (typeof content === 'string') {
|
|
60
|
+
assistantReply = content.slice(0, 2000)
|
|
61
|
+
} else if (Array.isArray(content)) {
|
|
62
|
+
assistantReply = content
|
|
63
|
+
.filter((b) => b.type === 'text')
|
|
64
|
+
.map((b) => b.text ?? '')
|
|
65
|
+
.join(' ')
|
|
66
|
+
.slice(0, 2000)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const conversationText = assistantReply
|
|
71
|
+
? `User: ${userText.trim()}\n\nAssistant: ${assistantReply}`
|
|
72
|
+
: `User: ${userText.trim()}`
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const apiKey = getApiKey ? await getApiKey(model.provider) : undefined
|
|
76
|
+
const stream = streamSimple(
|
|
77
|
+
model,
|
|
78
|
+
{
|
|
79
|
+
systemPrompt: '你是对话标题生成器。请用和用户相同的语言,根据对话主题生成 3 到 5 个词的短标题。只输出标题,不要解释,不要标点。',
|
|
80
|
+
messages: [{ role: 'user', content: conversationText, timestamp: Date.now() }],
|
|
81
|
+
tools: [],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
apiKey,
|
|
85
|
+
maxTokens: 160,
|
|
86
|
+
temperature: 0.2,
|
|
87
|
+
reasoning: thinkingLevel === 'off' ? undefined : 'medium',
|
|
88
|
+
maxRetryDelayMs: 60000,
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
const titleMessage = await stream.result()
|
|
92
|
+
const titleText = Array.isArray(titleMessage.content)
|
|
93
|
+
? titleMessage.content.filter((b) => b.type === 'text').map((b) => b.text ?? '').join(' ').trim()
|
|
94
|
+
: ''
|
|
95
|
+
if (!titleText) return null
|
|
96
|
+
const title = normalizeAiTitle(titleText)
|
|
97
|
+
return title || null
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn('Failed to generate AI title:', error.message || error)
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
}
|