@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.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/bin/quickforge.mjs +72 -7
  3. package/dist/assets/{anthropic-By-wpU1w.js → anthropic-DLvtwHL2.js} +2 -2
  4. package/dist/assets/{azure-openai-responses-C8spS__i.js → azure-openai-responses-D68z7hLN.js} +1 -1
  5. package/dist/assets/css-utils-rkE68RDy.js +1 -0
  6. package/dist/assets/{google-DiIcyajo.js → google-B_sSaRBM.js} +1 -1
  7. package/dist/assets/{google-gemini-cli-BXZFGMXD.js → google-gemini-cli-CYqGXjGi.js} +1 -1
  8. package/dist/assets/google-shared-XhYUKiGZ.js +11 -0
  9. package/dist/assets/{google-vertex-D93MV5Cx.js → google-vertex-DSMuB4YB.js} +1 -1
  10. package/dist/assets/icons-BsZ9PlYY.js +1 -0
  11. package/dist/assets/index-BqFfVQJM.css +3 -0
  12. package/dist/assets/index-DoraECXN.js +3187 -0
  13. package/dist/assets/lit-vendor-1dsGB-Iy.js +2 -0
  14. package/dist/assets/{mistral-BAJNGYqd.js → mistral-BZngRB4x.js} +2 -2
  15. package/dist/assets/{openai-codex-responses-BHHCy65K.js → openai-codex-responses-Niu7xDYK.js} +1 -1
  16. package/dist/assets/openai-completions-B2bhb9k0.js +5 -0
  17. package/dist/assets/{openai-responses-CP9-AyAD.js → openai-responses-CDYDv8yL.js} +1 -1
  18. package/dist/assets/{openai-responses-shared-_z7sua8J.js → openai-responses-shared-BIKPTpEQ.js} +1 -1
  19. package/dist/assets/react-vendor-Ds3ovY0w.js +9 -0
  20. package/dist/assets/rolldown-runtime-CkqCuyE9.js +1 -0
  21. package/dist/index.html +7 -3
  22. package/package.json +14 -13
  23. package/server/agent-manager.mjs +1053 -0
  24. package/server/conversation-compaction.mjs +302 -0
  25. package/server/custom-commands.mjs +344 -0
  26. package/server/index.mjs +322 -32
  27. package/server/project-config.mjs +80 -31
  28. package/server/reasoning-cache.mjs +51 -0
  29. package/server/restart-supervisor.mjs +38 -0
  30. package/server/routes/agent.mjs +323 -0
  31. package/server/routes/backup.mjs +250 -0
  32. package/server/routes/instructions.mjs +6 -17
  33. package/server/routes/project.mjs +46 -10
  34. package/server/routes/scheduled-tasks.mjs +424 -0
  35. package/server/routes/shared-conversation.mjs +404 -0
  36. package/server/routes/shares.mjs +84 -0
  37. package/server/routes/skills.mjs +145 -0
  38. package/server/routes/static.mjs +4 -3
  39. package/server/routes/storage.mjs +58 -10
  40. package/server/routes/system.mjs +35 -0
  41. package/server/routes/tools.mjs +53 -2
  42. package/server/session-utils.mjs +102 -0
  43. package/server/share-store.mjs +468 -0
  44. package/server/skills.mjs +539 -0
  45. package/server/storage.mjs +247 -6
  46. package/server/system-prompt.mjs +67 -0
  47. package/server/tools/definitions.mjs +120 -0
  48. package/server/tools/index.mjs +167 -46
  49. package/server/utils/logger.mjs +34 -0
  50. package/server/utils/network.mjs +38 -0
  51. package/server/utils/platform.mjs +30 -0
  52. package/server/utils/response.mjs +8 -1
  53. package/skills/ai-context-package/SKILL.md +104 -0
  54. package/skills/ai-context-package/skill.json +9 -0
  55. package/skills/code-review/SKILL.md +23 -0
  56. package/skills/code-review/skill.json +9 -0
  57. package/skills/frontend-react/SKILL.md +22 -0
  58. package/skills/frontend-react/skill.json +9 -0
  59. package/skills/quickforge-project/SKILL.md +22 -0
  60. package/skills/quickforge-project/skill.json +9 -0
  61. package/dist/assets/chunk-62oNxeRG.js +0 -1
  62. package/dist/assets/confirm-dialog-4mZt9XEq.js +0 -1
  63. package/dist/assets/google-shared-CXUHW-9O.js +0 -11
  64. package/dist/assets/index-Bq6VHkyY.js +0 -3048
  65. package/dist/assets/index-D7uXa1RT.css +0 -3
  66. package/dist/assets/openai-completions-BtZAvOiJ.js +0 -5
  67. package/dist/assets/prompt-dialog-BGMKszUz.js +0 -1
  68. /package/dist/assets/{github-copilot-headers-C0toI16e.js → github-copilot-headers-CrI0CIJ7.js} +0 -0
  69. /package/dist/assets/{hash-fDQBJsbb.js → hash-Bt1aVMQ3.js} +0 -0
  70. /package/dist/assets/{headers-Drkm68SQ.js → headers-5EYI0_pl.js} +0 -0
  71. /package/dist/assets/{openai-CuiHR4mv.js → openai-Cn7eGqwa.js} +0 -0
  72. /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 data = await readStore(store)
41
- const values = Object.values(data)
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
- sendJson(res, 200, { values })
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
- const data = await readStore(store)
85
- data[key] = body?.value
86
- await writeStore(store, data)
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
- const data = await readStore(store)
93
- delete data[key]
94
- await writeStore(store, data)
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
+ }
@@ -1,6 +1,36 @@
1
1
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
- import { toolHandlers } from '../tools/index.mjs'
3
- import { projectContextFromId } from '../project-config.mjs'
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
+ }