@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,10 +1,36 @@
1
1
  import { promises as fs } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { spawn } from 'node:child_process'
4
- import { resolveWorkspacePath, toWorkspaceRelative, assertSafeWorkspacePath, truncateText, splitLines, shouldSkipSearchDir, shouldSearchFile, isSensitiveWorkspacePath } from '../utils/workspace.mjs'
4
+ import { resolveWorkspacePath, toWorkspaceRelative, assertSafeWorkspacePath, truncateText, splitLines, walkFiles } from '../utils/workspace.mjs'
5
5
  import { readProjectConfig, getActiveProject } from '../project-config.mjs'
6
+ import {
7
+ formatSkillActivation,
8
+ loadSelectedGlobalSkills,
9
+ loadSelectedProjectSkills,
10
+ mergeSkills,
11
+ readSkillResource,
12
+ } from '../skills.mjs'
6
13
  import { getWorkspaceRoot, getToolWorkspaceRoot } from '../utils/workspace.mjs'
7
14
 
15
+ // --- get_project_info ---
16
+ export async function toolGetProjectInfo(_params, context) {
17
+ const config = context?.project ? null : await readProjectConfig()
18
+ const project = context?.project || getActiveProject(config)
19
+ const workspaceRoot = context?.workspaceRoot || project?.path || getWorkspaceRoot()
20
+
21
+ if (!project) {
22
+ return {
23
+ content: 'No active project is configured.',
24
+ details: { project: null, workspaceRoot },
25
+ }
26
+ }
27
+
28
+ return {
29
+ content: [`Project: ${project.name}`, `Path: ${workspaceRoot}`, `ID: ${project.id}`].join('\n'),
30
+ details: { project, workspaceRoot },
31
+ }
32
+ }
33
+
8
34
  // --- list_dir ---
9
35
  export async function toolListDir(params, context) {
10
36
  const dir = resolveWorkspacePath(params?.path || '.', context)
@@ -54,6 +80,31 @@ export async function toolReadFile(params, context) {
54
80
  }
55
81
 
56
82
  // --- grep_files ---
83
+
84
+ /**
85
+ * Process items with bounded concurrency. Returns results in input order.
86
+ * @template T, R
87
+ * @param {T[]} items
88
+ * @param {(item: T, index: number) => Promise<R>} fn
89
+ * @param {number} concurrency
90
+ * @returns {Promise<R[]>}
91
+ */
92
+ async function poolMap(items, fn, concurrency = 20) {
93
+ const results = new Array(items.length)
94
+ let cursor = 0
95
+
96
+ async function worker() {
97
+ while (cursor < items.length) {
98
+ const index = cursor++
99
+ results[index] = await fn(items[index], index)
100
+ }
101
+ }
102
+
103
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
104
+ await Promise.all(workers)
105
+ return results
106
+ }
107
+
57
108
  export async function toolGrepFiles(params, context) {
58
109
  const root = resolveWorkspacePath(params?.path || '.', context)
59
110
  assertSafeWorkspacePath(root, context)
@@ -67,24 +118,62 @@ export async function toolGrepFiles(params, context) {
67
118
 
68
119
  const limit = Math.min(1000, Math.max(1, Number(params?.limit || 200)))
69
120
  const flags = params?.caseSensitive ? 'g' : 'gi'
70
- const matcher = params?.regex
71
- ? new RegExp(query, flags)
72
- : new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags)
121
+ let matcher
122
+ try {
123
+ matcher = params?.regex
124
+ ? new RegExp(query, flags)
125
+ : new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags)
126
+ } catch {
127
+ const error = new Error('Invalid regular expression')
128
+ error.statusCode = 400
129
+ throw error
130
+ }
73
131
 
74
132
  const files = await walkFiles(root, [], context)
75
133
  const matches = []
76
134
 
77
- for (const file of files) {
78
- if (matches.length >= limit) break
79
- const stat = await fs.stat(file)
80
- if (stat.size > 1024 * 1024) continue
81
-
82
- const text = await fs.readFile(file, 'utf8').catch(() => '')
83
- const lines = splitLines(text)
84
- for (let index = 0; index < lines.length && matches.length < limit; index++) {
85
- matcher.lastIndex = 0
86
- if (matcher.test(lines[index])) {
87
- matches.push(`${toWorkspaceRelative(file, context)}:${index + 1}: ${lines[index]}`)
135
+ // Stat and filter files in parallel, then grep in parallel batches
136
+ const candidateResults = await poolMap(files, async (file) => {
137
+ try {
138
+ const stat = await fs.stat(file)
139
+ if (stat.size > 1024 * 1024) return { file, skip: true }
140
+ return { file, skip: false }
141
+ } catch {
142
+ return { file, skip: true }
143
+ }
144
+ })
145
+
146
+ const candidates = candidateResults.filter((r) => !r.skip).map((r) => r.file)
147
+
148
+ // Grep with bounded concurrency — short-circuit when limit reached
149
+ let matchCount = 0
150
+ for (let batchStart = 0; batchStart < candidates.length && matchCount < limit; batchStart += 20) {
151
+ const batch = candidates.slice(batchStart, batchStart + 20)
152
+ const batchMatches = await Promise.all(
153
+ batch.map(async (file) => {
154
+ if (matchCount >= limit) return []
155
+ try {
156
+ const text = await fs.readFile(file, 'utf8')
157
+ const lines = splitLines(text)
158
+ const fileMatches = []
159
+ for (let index = 0; index < lines.length && (matchCount + fileMatches.length) < limit; index++) {
160
+ matcher.lastIndex = 0
161
+ if (matcher.test(lines[index])) {
162
+ fileMatches.push(`${toWorkspaceRelative(file, context)}:${index + 1}: ${lines[index]}`)
163
+ }
164
+ }
165
+ return fileMatches
166
+ } catch {
167
+ return []
168
+ }
169
+ }),
170
+ )
171
+ for (const fm of batchMatches) {
172
+ if (matchCount >= limit) break
173
+ for (const m of fm) {
174
+ if (matchCount >= limit) break
175
+ matches.push(m)
176
+ matchCount++
88
177
  }
89
178
  }
90
179
  }
@@ -144,6 +233,59 @@ export async function toolEditFile(params, context) {
144
233
  }
145
234
  }
146
235
 
236
+ // --- run_command ---
237
+ function activeSkillsForContext(context) {
238
+ return mergeSkills(context?.globalSkills, context?.projectSkills)
239
+ }
240
+
241
+ function activeSkillByName(context, name) {
242
+ const skillName = String(name || '')
243
+ return activeSkillsForContext(context).find((skill) => skill.name === skillName)
244
+ }
245
+
246
+ export async function loadSkillToolContext(config = {}) {
247
+ const globalSkills = await loadSelectedGlobalSkills(config.globalSkillNames)
248
+ const projectSkills = config.workspaceRoot
249
+ ? await loadSelectedProjectSkills(config.projectSkillNames, config.workspaceRoot)
250
+ : []
251
+ return { globalSkills, projectSkills }
252
+ }
253
+
254
+ // --- activate_skill ---
255
+ export async function toolActivateSkill(params, context) {
256
+ const skill = activeSkillByName(context, params?.name)
257
+ if (!skill) {
258
+ const error = new Error(`Unknown or disabled skill: ${params?.name || ''}`)
259
+ error.statusCode = 404
260
+ throw error
261
+ }
262
+
263
+ return {
264
+ content: truncateText(await formatSkillActivation(skill)),
265
+ details: {
266
+ skill: skill.name,
267
+ source: skill.source,
268
+ directory: skill.rootDir,
269
+ },
270
+ }
271
+ }
272
+
273
+ // --- read_skill_resource ---
274
+ export async function toolReadSkillResource(params, context) {
275
+ const skill = activeSkillByName(context, params?.skill)
276
+ if (!skill) {
277
+ const error = new Error(`Unknown or disabled skill: ${params?.skill || ''}`)
278
+ error.statusCode = 404
279
+ throw error
280
+ }
281
+
282
+ const result = await readSkillResource(skill, params?.path, params)
283
+ return {
284
+ content: truncateText(result.content),
285
+ details: result.details,
286
+ }
287
+ }
288
+
147
289
  // --- run_command ---
148
290
  export async function toolRunCommand(params, context) {
149
291
  const command = String(params?.command || '')
@@ -191,40 +333,17 @@ export async function toolRunCommand(params, context) {
191
333
  ].join('\n')
192
334
  resolve({ content: truncateText(content), details: { command, project: context?.project, cwd: getToolWorkspaceRoot(context), code, signal, timedOut } })
193
335
  })
336
+ child.on('error', (err) => {
337
+ clearTimeout(timer)
338
+ resolve({
339
+ isError: true,
340
+ content: truncateText(`Error running command: ${err.message}`),
341
+ details: { command, project: context?.project, error: err.message },
342
+ })
343
+ })
194
344
  })
195
345
  }
196
346
 
197
- // --- get_project_info ---
198
- export async function toolGetProjectInfo(_params, context) {
199
- if (context?.project) {
200
- return {
201
- content: `Project: ${context.project.name}\nRoot: ${context.project.path}`,
202
- details: { project: context.project, workspaceRoot: context.workspaceRoot },
203
- }
204
- }
205
-
206
- const config = await readProjectConfig()
207
- const project = getActiveProject(config)
208
- return {
209
- content: `Active project: ${project.name}\nRoot: ${project.path}`,
210
- details: { project, workspaceRoot: getWorkspaceRoot() },
211
- }
212
- }
213
-
214
- // Helper for grep
215
- async function walkFiles(root, files = [], context) {
216
- const entries = await fs.readdir(root, { withFileTypes: true })
217
- for (const entry of entries) {
218
- const fullPath = path.join(root, entry.name)
219
- if (entry.isDirectory()) {
220
- if (!shouldSkipSearchDir(entry.name)) await walkFiles(fullPath, files, context)
221
- } else if (entry.isFile() && shouldSearchFile(entry.name) && !isSensitiveWorkspacePath(fullPath, context)) {
222
- files.push(fullPath)
223
- }
224
- }
225
- return files
226
- }
227
-
228
347
  export const toolHandlers = {
229
348
  get_project_info: toolGetProjectInfo,
230
349
  list_dir: toolListDir,
@@ -233,4 +352,6 @@ export const toolHandlers = {
233
352
  write_file: toolWriteFile,
234
353
  edit_file: toolEditFile,
235
354
  run_command: toolRunCommand,
355
+ activate_skill: toolActivateSkill,
356
+ read_skill_resource: toolReadSkillResource,
236
357
  }
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { logsDir } from '../storage.mjs'
4
+
5
+ function timestamp() {
6
+ return new Date().toISOString()
7
+ }
8
+
9
+ function logFile() {
10
+ const date = new Date().toISOString().slice(0, 10)
11
+ return path.join(logsDir, `server-${date}.log`)
12
+ }
13
+
14
+ function formatArgs(args) {
15
+ return args.map((a) =>
16
+ typeof a === 'string' ? a : a instanceof Error ? a.stack : JSON.stringify(a),
17
+ ).join(' ')
18
+ }
19
+
20
+ function writeLog(level, ...args) {
21
+ const line = `${timestamp()} [${level}] ${formatArgs(args)}\n`
22
+ process.stderr.write(line)
23
+ try {
24
+ fs.appendFileSync(logFile(), line)
25
+ } catch {
26
+ // ignore write errors
27
+ }
28
+ }
29
+
30
+ export const logger = {
31
+ info: (...args) => writeLog('INFO', ...args),
32
+ warn: (...args) => writeLog('WARN', ...args),
33
+ error: (...args) => writeLog('ERROR', ...args),
34
+ }
@@ -0,0 +1,38 @@
1
+ import os from 'node:os'
2
+
3
+ export function isPrivateIpv4(hostname) {
4
+ if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname || '')) return false
5
+ const parts = hostname.split('.').map((part) => Number(part))
6
+ if (parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false
7
+ const [a, b] = parts
8
+ return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168)
9
+ }
10
+
11
+ export function isLoopbackAddress(address) {
12
+ if (!address) return false
13
+ const normalized = address.replace(/^::ffff:/, '')
14
+ return normalized === '127.0.0.1' || normalized === '::1' || normalized === 'localhost'
15
+ }
16
+
17
+ export function getLanIpv4Addresses() {
18
+ const result = []
19
+ const seen = new Set()
20
+ const interfaces = os.networkInterfaces()
21
+
22
+ for (const entries of Object.values(interfaces)) {
23
+ for (const entry of entries || []) {
24
+ if (entry.family !== 'IPv4' || entry.internal) continue
25
+ if (!isPrivateIpv4(entry.address)) continue
26
+ if (seen.has(entry.address)) continue
27
+ seen.add(entry.address)
28
+ result.push(entry.address)
29
+ }
30
+ }
31
+
32
+ return result
33
+ }
34
+
35
+ export function getLanUrls(port, protocol = 'http') {
36
+ const safePort = Number(port)
37
+ return getLanIpv4Addresses().map((address) => `${protocol}://${address}${safePort ? `:${safePort}` : ''}`)
38
+ }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process'
2
+ import { promises as fs } from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import os from 'node:os'
4
5
 
@@ -121,6 +122,35 @@ try {
121
122
  throw error
122
123
  }
123
124
 
125
+ export async function openPathInFileManager(targetPath) {
126
+ const resolved = path.resolve(String(targetPath || ''))
127
+ const stat = await fs.stat(resolved).catch(() => null)
128
+ if (!stat || !stat.isDirectory()) {
129
+ const error = new Error(`Directory does not exist: ${resolved}`)
130
+ error.statusCode = 400
131
+ throw error
132
+ }
133
+
134
+ const command = process.platform === 'win32' ? 'explorer.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open'
135
+ const args = [resolved]
136
+ await new Promise((resolve, reject) => {
137
+ const child = spawn(command, args, {
138
+ detached: true,
139
+ stdio: 'ignore',
140
+ windowsHide: false,
141
+ shell: false,
142
+ })
143
+ child.once('error', (error) => {
144
+ error.statusCode = 500
145
+ reject(error)
146
+ })
147
+ child.once('spawn', () => {
148
+ child.unref()
149
+ resolve()
150
+ })
151
+ })
152
+ }
153
+
124
154
  export function openBrowser(url) {
125
155
  if (process.env.QUICKFORGE_NO_OPEN === '1') return
126
156
 
@@ -27,7 +27,14 @@ export async function readJsonBody(req, maxBodyBytes = DEFAULT_MAX_BODY_BYTES) {
27
27
  chunks.push(chunk)
28
28
  }
29
29
  const text = Buffer.concat(chunks).toString('utf8')
30
- return text ? JSON.parse(text) : null
30
+ if (!text) return null
31
+ try {
32
+ return JSON.parse(text.trimStart())
33
+ } catch {
34
+ const error = new Error('Invalid JSON request body')
35
+ error.statusCode = 400
36
+ throw error
37
+ }
31
38
  }
32
39
 
33
40
  export function decodeSegment(value) {
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: ai-context-package
3
+ description: Use this skill when the user wants to prepare, fill, review, or enforce an AI task context package before coding, including task scope, constraints, validation commands, stop conditions, and expected output format.
4
+ metadata:
5
+ displayName: AI Context Package
6
+ version: "1.0.0"
7
+ tags: context, planning, template
8
+ ---
9
+ # AI Context Package Skill
10
+
11
+ ## Purpose
12
+
13
+ Produce a clear, bounded context package that helps an AI coding assistant understand the project, task goal, accepted scope, constraints, validation commands, and expected delivery format.
14
+
15
+ ## Template
16
+
17
+ ```md
18
+ # AI 上下文包:{TASK_NAME}
19
+
20
+ ## 1. 项目概况
21
+ - 项目名称:{PROJECT_NAME}
22
+ - 项目定位:{PROJECT_SUMMARY}
23
+ - 技术栈:{TECH_STACK}
24
+ - 当前环境:{ENV}
25
+
26
+ ## 2. 相关文档
27
+ - 项目知识地图:{PROJECT_KNOWLEDGE_DOC_PATH}
28
+ - 技术栈学习地图:{TECH_STACK_DOC_PATH}
29
+ - 模块设计文档:{MODULE_DOC_PATH}
30
+ - 解决方案文档:{SOLUTION_DOC_PATH}
31
+ - 接口文档/产品文档:{REQUIREMENT_DOC_PATH}
32
+
33
+ ## 3. 任务目标
34
+ {BUSINESS_GOAL}
35
+
36
+ ## 4. 验收标准
37
+ {ACCEPTANCE_CRITERIA}
38
+
39
+ ## 5. 变更范围
40
+ 允许修改:
41
+ {ALLOWED_FILES_OR_MODULES}
42
+
43
+ 禁止修改:
44
+ {FORBIDDEN_FILES_OR_MODULES}
45
+
46
+ ## 6. 相关代码入口
47
+ | 类型 | 路径 | 说明 |
48
+ |---|---|---|
49
+ | 入口/API | | |
50
+ | Service | | |
51
+ | DAO/Repository | | |
52
+ | 配置 | | |
53
+ | 测试 | | |
54
+
55
+ ## 7. 已确定方案
56
+ {SOLUTION_SUMMARY}
57
+
58
+ ## 8. 约束条件
59
+ - 不做无关重构。
60
+ - 不引入新依赖,除非明确说明。
61
+ - 不改变公共接口契约,除非有兼容方案。
62
+ - 不修改数据库结构,除非有迁移和回滚。
63
+ - 不泄露密钥和敏感数据。
64
+ - {OTHER_CONSTRAINTS}
65
+
66
+ ## 9. 验证方式
67
+ - 测试命令:{TEST_COMMAND}
68
+ - 构建命令:{BUILD_COMMAND}
69
+ - 手工验证步骤:{MANUAL_VALIDATION}
70
+
71
+ ## 10. 停止条件
72
+ 遇到以下情况先停止,不要继续改代码:
73
+ - 代码事实与方案文档冲突。
74
+ - 需要扩大修改范围。
75
+ - 涉及数据库破坏性变更。
76
+ - 涉及权限扩大或敏感数据。
77
+ - 测试命令不可用且无法判断影响。
78
+ - {OTHER_STOP_CONDITIONS}
79
+
80
+ ## 11. 期望 AI 输出
81
+ - 修改前理解。
82
+ - 修改文件清单。
83
+ - 每个文件变更说明。
84
+ - 测试/构建结果。
85
+ - 风险点。
86
+ - 回滚方式。
87
+ - 未完成项。
88
+ ```
89
+
90
+ ## Workflow
91
+
92
+ 1. If the user provides only the template, treat it as a reusable task-context skill/template.
93
+ 2. If fields are missing, ask only for the fields needed to proceed, or fill safe placeholders like `待补充` when the user wants a draft.
94
+ 3. Before coding, restate the task understanding and explicitly list allowed and forbidden change scope.
95
+ 4. Inspect the relevant docs/code entries before changing files.
96
+ 5. Stop and ask for confirmation when any stop condition is met.
97
+ 6. After changes, report using the expected AI output sections.
98
+
99
+ ## Output Rules
100
+
101
+ - Keep the context package structured and copyable as Markdown.
102
+ - Do not invent requirements. Mark unknown values as `待补充`.
103
+ - Prefer concrete file/module paths over broad descriptions.
104
+ - Keep validation commands explicit.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "ai-context-package",
3
+ "displayName": "AI Context Package",
4
+ "description": "Create a structured task context package for AI coding work, including goals, docs, scope, constraints, validation, and expected output.",
5
+ "version": "1.0.0",
6
+ "tags": ["context", "planning", "task", "requirements", "ai"],
7
+ "triggers": ["上下文包", "AI 上下文", "context package", "任务上下文", "验收标准", "变更范围"],
8
+ "entry": "SKILL.md"
9
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: code-review
3
+ description: Use this skill when the user asks for code review, PR review, bug finding, quality checks, security/performance/maintainability review, or wants feedback on code changes.
4
+ metadata:
5
+ displayName: Code Review
6
+ version: "1.0.0"
7
+ tags: code, review, quality
8
+ ---
9
+ # Code Review Skill
10
+
11
+ ## Workflow
12
+
13
+ 1. Understand the requested change and inspect the relevant files before judging.
14
+ 2. Focus on correctness, regressions, security, data loss, performance, and maintainability.
15
+ 3. Prefer concrete findings over generic advice.
16
+ 4. If proposing changes, keep them small and directly related to the review.
17
+ 5. Verify with targeted tests, lint, typecheck, build, or a focused command when practical.
18
+
19
+ ## Output
20
+
21
+ - Start with the highest-risk findings.
22
+ - For each finding include the file/location, risk, why it matters, and a concise fix.
23
+ - If no major issues are found, say so clearly and mention any checks performed.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "code-review",
3
+ "displayName": "Code Review",
4
+ "description": "Review code changes for correctness, bugs, security, performance, and maintainability.",
5
+ "version": "1.0.0",
6
+ "tags": ["code", "review", "quality"],
7
+ "triggers": ["code review", "review code", "pr review", "代码审查", "检查代码"],
8
+ "entry": "SKILL.md"
9
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: frontend-react
3
+ description: Use this skill for React, TypeScript, frontend UI, layout, component work, accessibility, styling, or browser interaction changes.
4
+ metadata:
5
+ displayName: React Frontend
6
+ version: "1.0.0"
7
+ tags: react, frontend, typescript, ui
8
+ ---
9
+ # React Frontend Skill
10
+
11
+ ## Guidelines
12
+
13
+ - Match the existing component style and design language.
14
+ - Prefer simple controlled state and small components over broad rewrites.
15
+ - Keep accessibility in mind: labels, aria attributes, keyboard escape/submit behavior, and focus states.
16
+ - Preserve existing behavior unless the user explicitly asks to change it.
17
+ - Avoid adding new dependencies for basic UI interactions.
18
+ - After changes, run typecheck/build or the smallest relevant verification command.
19
+
20
+ ## Output
21
+
22
+ Briefly summarize what changed and what verification was run.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "frontend-react",
3
+ "displayName": "React Frontend",
4
+ "description": "Build and modify React UI with TypeScript, accessible interactions, and minimal focused changes.",
5
+ "version": "1.0.0",
6
+ "tags": ["react", "frontend", "typescript", "ui"],
7
+ "triggers": ["react", "frontend", "ui", "组件", "前端", "界面"],
8
+ "entry": "SKILL.md"
9
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: quickforge-project
3
+ description: Use this skill when modifying the QuickForge application, including its React/Vite frontend, local Node.js backend, storage, project chats, workspace tools, skills, or local-only safety behavior.
4
+ metadata:
5
+ displayName: QuickForge Project
6
+ version: "1.0.0"
7
+ tags: quickforge, project, local-agent
8
+ ---
9
+ # QuickForge Project Skill
10
+
11
+ ## Project Rules
12
+
13
+ - Make surgical changes only; avoid broad refactors.
14
+ - Keep server API changes small and local to `server/routes` or dedicated modules.
15
+ - Keep frontend state changes explicit and close to the component/hook that owns them.
16
+ - Preserve local-only safety assumptions for workspace access.
17
+ - For project-scoped features, persist configuration in the project config where possible.
18
+ - Verify with `npm run build` or a targeted command before finishing.
19
+
20
+ ## Notes
21
+
22
+ QuickForge has a React/Vite frontend and a local Node.js backend. Project chats can use workspace tools when YOLO mode is enabled.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "quickforge-project",
3
+ "displayName": "QuickForge Project",
4
+ "description": "Work within QuickForge conventions: local server, project chats, YOLO workspace tools, and minimal surgical changes.",
5
+ "version": "1.0.0",
6
+ "tags": ["quickforge", "project", "workspace"],
7
+ "triggers": ["quickforge", "项目", "本项目"],
8
+ "entry": "SKILL.md"
9
+ }
@@ -1 +0,0 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),u=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")});export{l as i,s as n,u as r,o as t};
@@ -1 +0,0 @@
1
- import{i as e}from"./chunk-62oNxeRG.js";import{f as t,m as n,n as r,p as i,r as a,t as o}from"./index-Bq6VHkyY.js";var s=e(n(),1),c=i(),l=t(),u=r();function d({options:e,onResolve:t}){let n=(0,s.useRef)(null);return(0,s.useEffect)(()=>{n.current?.focus();let e=e=>{e.key===`Escape`&&t(!1)};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[t]),(0,c.createPortal)((0,u.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50`,onClick:e=>{e.target===e.currentTarget&&t(!1)},children:(0,u.jsxs)(`div`,{className:a(`w-full max-w-sm rounded-lg border border-border bg-background p-6 shadow-lg`,`mx-4`),children:[(0,u.jsx)(`h2`,{className:`text-base font-semibold text-foreground`,children:e.title}),(0,u.jsx)(`p`,{className:`mt-2 text-sm text-muted-foreground`,children:e.description}),(0,u.jsxs)(`div`,{className:`mt-5 flex justify-end gap-2`,children:[(0,u.jsx)(o,{ref:n,variant:`outline`,size:`sm`,onClick:()=>t(!1),children:e.cancelLabel??`Cancel`}),(0,u.jsx)(o,{variant:`destructive`,size:`sm`,onClick:()=>t(!0),children:e.confirmLabel??`Delete`})]})]})}),document.body)}function f(e){return new Promise(t=>{let n=document.createElement(`div`);document.body.appendChild(n);let r=(0,l.createRoot)(n);function i(){r.unmount(),setTimeout(()=>n.remove(),0)}function a(e){i(),t(e)}r.render((0,u.jsx)(d,{options:e,onResolve:a}))})}export{f as showConfirm};