@shawnstack/quickforge 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +12 -12
  2. package/bin/quickforge.mjs +9 -0
  3. package/dist/assets/AgentProfilesPage-DUmXUxjA.js +1 -0
  4. package/dist/assets/ChatPanelHost-Syx0SSLe.js +242 -0
  5. package/dist/assets/PluginsPage-kiBq0gOT.js +1 -0
  6. package/dist/assets/ScheduledTasksPage-Dw4-tgp9.js +2 -0
  7. package/dist/assets/SharedConversationPage-CaE9bNb9.js +1 -0
  8. package/dist/assets/TerminalDock-BYJcp8Ts.js +2 -0
  9. package/dist/assets/WorkspaceInspector-Bzmv8Cvi.js +3 -0
  10. package/dist/assets/WorkspaceReaderDialog-BJo_KEWi.js +1 -0
  11. package/dist/assets/diff-line-counts-BZoYp5ai.js +10 -0
  12. package/dist/assets/icons-47L5YLKz.js +1 -0
  13. package/dist/assets/index-CqfScETb.js +1200 -0
  14. package/dist/assets/index-DzkBgHZf.css +3 -0
  15. package/dist/assets/{monaco-evITXh-m.js → monaco-CGq6uVF1.js} +1 -1
  16. package/dist/assets/{react-vendor-Mthyt1p4.js → react-vendor-DunfCFfp.js} +1 -1
  17. package/dist/favicon.svg +16 -1
  18. package/dist/index.html +5 -5
  19. package/dist/manifest.webmanifest +30 -30
  20. package/package.json +3 -2
  21. package/server/acp/server.mjs +921 -0
  22. package/server/agent-manager.mjs +198 -32
  23. package/server/agent-profile-files.mjs +179 -0
  24. package/server/agent-profiles.mjs +59 -5
  25. package/server/auto-compaction.mjs +82 -39
  26. package/server/channels/process-channel.mjs +278 -0
  27. package/server/channels/providers/wechat.mjs +271 -0
  28. package/server/channels/registry.mjs +58 -0
  29. package/server/custom-commands.mjs +13 -1
  30. package/server/frontmatter.mjs +167 -0
  31. package/server/index.mjs +52 -3
  32. package/server/project-config.mjs +43 -6
  33. package/server/routes/agent-profiles.mjs +6 -2
  34. package/server/routes/agent.mjs +12 -1
  35. package/server/routes/channels.mjs +145 -0
  36. package/server/routes/models.mjs +68 -0
  37. package/server/routes/project.mjs +2 -2
  38. package/server/routes/scheduled-tasks.mjs +6 -5
  39. package/server/routes/storage.mjs +4 -2
  40. package/server/routes/system.mjs +27 -0
  41. package/server/routes/tools.mjs +17 -6
  42. package/server/routes/workspace.mjs +138 -0
  43. package/server/session-utils.mjs +10 -2
  44. package/server/storage.mjs +29 -2
  45. package/server/system-prompt.mjs +1 -0
  46. package/server/tools/definitions.mjs +18 -0
  47. package/server/tools/index.mjs +83 -0
  48. package/server/utils/package-update.mjs +156 -0
  49. package/dist/assets/AgentProfilesPage-CNK5PxA3.js +0 -1
  50. package/dist/assets/ChatPanelHost-FqPQwwMO.js +0 -217
  51. package/dist/assets/PluginsPage-BCu1Ept0.js +0 -1
  52. package/dist/assets/ScheduledTasksPage-Bx04rjui.js +0 -2
  53. package/dist/assets/SharedConversationPage-55vX9sqe.js +0 -1
  54. package/dist/assets/TerminalDock-DLN_pLkJ.js +0 -2
  55. package/dist/assets/WorkspaceInspector-DoemHHnY.js +0 -3
  56. package/dist/assets/WorkspaceReaderDialog-C6xUHBCw.js +0 -6
  57. package/dist/assets/icons-BWtivFsx.js +0 -1
  58. package/dist/assets/index-CxOHP41X.css +0 -3
  59. package/dist/assets/index-Dcf73EL8.js +0 -895
@@ -12,6 +12,8 @@ import {
12
12
  } from '../utils/workspace.mjs'
13
13
 
14
14
  const MAX_PREVIEW_BYTES = 1024 * 1024
15
+ const MAX_STATIC_PREVIEW_BYTES = 10 * 1024 * 1024
16
+ const PREVIEW_ALLOWED_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif', '.ico', '.txt', '.md'])
15
17
  const MAX_TREE_NODES = 5000
16
18
  const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', 'dist-ssr', 'package-dist', 'package-offline', '.vite', 'coverage'])
17
19
 
@@ -34,6 +36,28 @@ function languageFromPath(filePath) {
34
36
  return extensionLanguageMap.get(extension) || 'plaintext'
35
37
  }
36
38
 
39
+ function previewContentType(filePath) {
40
+ const ext = path.extname(filePath).toLowerCase()
41
+ const map = {
42
+ '.html': 'text/html; charset=utf-8',
43
+ '.htm': 'text/html; charset=utf-8',
44
+ '.js': 'application/javascript; charset=utf-8',
45
+ '.mjs': 'application/javascript; charset=utf-8',
46
+ '.css': 'text/css; charset=utf-8',
47
+ '.json': 'application/json; charset=utf-8',
48
+ '.svg': 'image/svg+xml',
49
+ '.png': 'image/png',
50
+ '.jpg': 'image/jpeg',
51
+ '.jpeg': 'image/jpeg',
52
+ '.webp': 'image/webp',
53
+ '.gif': 'image/gif',
54
+ '.ico': 'image/x-icon',
55
+ '.txt': 'text/plain; charset=utf-8',
56
+ '.md': 'text/markdown; charset=utf-8',
57
+ }
58
+ return map[ext] || 'application/octet-stream'
59
+ }
60
+
37
61
  function isBinaryBuffer(buffer) {
38
62
  const length = Math.min(buffer.length, 8000)
39
63
  for (let index = 0; index < length; index += 1) {
@@ -144,10 +168,71 @@ function countGitStatus(files) {
144
168
  }, { staged: 0, unstaged: 0, untracked: 0, conflicts: 0, total: 0 })
145
169
  }
146
170
 
171
+ function countTextLines(text) {
172
+ if (text.length === 0) return 0
173
+ return text.split('\n').length - (text.endsWith('\n') ? 1 : 0)
174
+ }
175
+
176
+ // numstat 的路径列对 rename 用 "prefix/{old => new}" 或 "old => new" 形式,取新路径
177
+ function numstatNewPath(rawPath) {
178
+ const arrow = rawPath.indexOf(' => ')
179
+ if (arrow < 0) return rawPath
180
+ const head = rawPath.slice(0, arrow)
181
+ const tail = rawPath.slice(arrow + 4)
182
+ const brace = head.lastIndexOf('{')
183
+ if (brace < 0) return tail
184
+ return `${head.slice(0, brace)}${tail.replace(/\}$/, '')}`
185
+ }
186
+
187
+ // 工作区 vs HEAD 的每个文件增删行数(口径与 git diff --numstat 一致)
188
+ async function collectNumstat(context) {
189
+ const map = new Map()
190
+ const result = await git(['diff', 'HEAD', '--numstat', '-z'], context.workspaceRoot, { allowFailure: true })
191
+ if (result.code !== 0) return map
192
+ const records = result.stdout.toString('utf8').split('\0').filter(Boolean)
193
+ for (const record of records) {
194
+ const fields = record.split('\t')
195
+ if (fields.length < 3) continue
196
+ const added = fields[0]
197
+ const removed = fields[1]
198
+ const rawPath = fields.slice(2).join('\t')
199
+ if (added === '-' || removed === '-') continue // 二进制文件
200
+ const additions = Number(added)
201
+ const deletions = Number(removed)
202
+ if (!Number.isFinite(additions) || !Number.isFinite(deletions)) continue
203
+ map.set(numstatNewPath(rawPath), { additions, deletions })
204
+ }
205
+ return map
206
+ }
207
+
208
+ // 未跟踪文件不在 numstat 中,按工作区文件行数估算新增行
209
+ async function countWorkspaceLines(context, relativePath) {
210
+ try {
211
+ const { content } = await readWorkspaceTextFile(context, relativePath)
212
+ return countTextLines(content)
213
+ } catch {
214
+ return undefined
215
+ }
216
+ }
217
+
147
218
  async function listGitStatus(context) {
148
219
  if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, files: [] }
149
220
  const result = await git(['status', '--porcelain=v1', '-z'], context.workspaceRoot)
150
221
  const files = parseGitStatus(result.stdout)
222
+ const numstat = await collectNumstat(context)
223
+ for (const file of files) {
224
+ const entry = numstat.get(file.path)
225
+ if (entry) {
226
+ file.additions = entry.additions
227
+ file.deletions = entry.deletions
228
+ } else if (file.status === 'untracked' || file.status === 'added') {
229
+ const count = await countWorkspaceLines(context, file.path)
230
+ if (typeof count === 'number') {
231
+ file.additions = count
232
+ file.deletions = 0
233
+ }
234
+ }
235
+ }
151
236
  return {
152
237
  isGitRepository: true,
153
238
  branch: await currentGitBranch(context.workspaceRoot),
@@ -246,6 +331,55 @@ async function handleWorkspaceFile(req, res, url) {
246
331
  })
247
332
  }
248
333
 
334
+ async function handleWorkspacePreview(req, res, url) {
335
+ const prefix = '/api/workspace/preview/'
336
+ const tail = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : ''
337
+ const slashIndex = tail.indexOf('/')
338
+ if (slashIndex <= 0) {
339
+ const error = new Error('projectId and path are required')
340
+ error.statusCode = 400
341
+ throw error
342
+ }
343
+
344
+ const projectId = decodeURIComponent(tail.slice(0, slashIndex))
345
+ const relativePath = decodeURIComponent(tail.slice(slashIndex + 1))
346
+ if (!projectId || !relativePath) {
347
+ const error = new Error('projectId and path are required')
348
+ error.statusCode = 400
349
+ throw error
350
+ }
351
+
352
+ const context = await projectContextFromId(projectId)
353
+ const file = resolveWorkspacePath(relativePath, context)
354
+ await assertSafeWorkspacePath(file, context)
355
+ const extension = path.extname(file).toLowerCase()
356
+ if (!PREVIEW_ALLOWED_EXTENSIONS.has(extension)) {
357
+ const error = new Error('Unsupported preview file type')
358
+ error.statusCode = 415
359
+ throw error
360
+ }
361
+ const stat = await fs.stat(file)
362
+ if (!stat.isFile()) {
363
+ const error = new Error('Path is not a file')
364
+ error.statusCode = 400
365
+ throw error
366
+ }
367
+ if (stat.size > MAX_STATIC_PREVIEW_BYTES) {
368
+ const error = new Error('File is too large to preview')
369
+ error.statusCode = 413
370
+ throw error
371
+ }
372
+
373
+ const contentType = previewContentType(file)
374
+ res.writeHead(200, {
375
+ 'content-type': contentType,
376
+ 'cache-control': 'no-store',
377
+ 'x-content-type-options': 'nosniff',
378
+ })
379
+ const buffer = await fs.readFile(file)
380
+ res.end(buffer)
381
+ }
382
+
249
383
  async function handleWorkspaceResolvePath(req, res) {
250
384
  const body = await readJsonBody(req, 16 * 1024)
251
385
  const projectId = typeof body?.projectId === 'string' ? body.projectId : ''
@@ -344,6 +478,10 @@ export async function handleWorkspaceApi(req, res, url) {
344
478
  await handleWorkspaceFile(req, res, url)
345
479
  return
346
480
  }
481
+ if (req.method === 'GET' && url.pathname.startsWith('/api/workspace/preview/')) {
482
+ await handleWorkspacePreview(req, res, url)
483
+ return
484
+ }
347
485
  if (req.method === 'POST' && url.pathname === '/api/workspace/resolve-path') {
348
486
  await handleWorkspaceResolvePath(req, res)
349
487
  return
@@ -1,5 +1,5 @@
1
1
  import { streamSimple } from '@earendil-works/pi-ai'
2
- import { buildInstructionsPayload } from './project-config.mjs'
2
+ import { buildInstructionsPayload, projectContextFromId } from './project-config.mjs'
3
3
  import { composeSystemPrompt } from './system-prompt.mjs'
4
4
  import { listSubagentProfiles } from './agent-profiles.mjs'
5
5
 
@@ -9,9 +9,17 @@ import { listSubagentProfiles } from './agent-profiles.mjs'
9
9
 
10
10
  export async function buildSystemPrompt(projectId) {
11
11
  const instructions = await buildInstructionsPayload(projectId)
12
+ let workspaceRoot = instructions.workspace?.root || null
13
+ if (projectId && !workspaceRoot) {
14
+ try {
15
+ workspaceRoot = (await projectContextFromId(projectId))?.workspaceRoot || null
16
+ } catch {
17
+ // project may have been removed; fall back to global agent profile discovery
18
+ }
19
+ }
12
20
  return composeSystemPrompt({
13
21
  ...instructions,
14
- subagents: await listSubagentProfiles(),
22
+ subagents: await listSubagentProfiles({ projectId, workspaceRoot }),
15
23
  })
16
24
  }
17
25
 
@@ -468,6 +468,26 @@ async function rebuildBucketIndex() {
468
468
  bucketIndexBuilt = true
469
469
  }
470
470
 
471
+ async function findSessionBucketByDataFile(sessionId) {
472
+ assertSafePathSegment(sessionId)
473
+ const buckets = [
474
+ { scope: 'global' },
475
+ ...(await listProjectIds()).map((projectId) => ({ scope: 'project', projectId })),
476
+ ]
477
+
478
+ for (const bucket of buckets) {
479
+ const file = sessionDataFile(sessionId, bucket)
480
+ try {
481
+ const stat = await fs.stat(file)
482
+ if (stat.isFile()) return bucket
483
+ } catch (error) {
484
+ if (error?.code !== 'ENOENT') throw error
485
+ }
486
+ }
487
+
488
+ return null
489
+ }
490
+
471
491
  export async function findSessionBucket(sessionId) {
472
492
  if (!bucketIndexBuilt) {
473
493
  await ensureStorage()
@@ -478,8 +498,12 @@ export async function findSessionBucket(sessionId) {
478
498
 
479
499
  export async function readSessionValue(sessionId) {
480
500
  const bucket = await findSessionBucket(sessionId)
481
- if (!bucket) return null
482
- return readJsonFile(sessionDataFile(sessionId, bucket), null)
501
+ if (bucket) return readJsonFile(sessionDataFile(sessionId, bucket), null)
502
+
503
+ const recoveredBucket = await findSessionBucketByDataFile(sessionId)
504
+ if (!recoveredBucket) return null
505
+ sessionBucketIndex.set(sessionId, recoveredBucket)
506
+ return readJsonFile(sessionDataFile(sessionId, recoveredBucket), null)
483
507
  }
484
508
 
485
509
  export async function writeSessionValue(sessionId, value) {
@@ -725,6 +749,9 @@ export function ensureStorage() {
725
749
  fs.mkdir(path.join(cacheDir, 'projects'), { recursive: true }),
726
750
  fs.mkdir(path.join(storageDir, 'conversations', 'global', 'sessions'), { recursive: true }),
727
751
  fs.mkdir(path.join(storageDir, 'conversations', 'projects'), { recursive: true }),
752
+ // Default workspace directory for global (non-project) conversations, so
753
+ // they share the same file-tool capabilities as project conversations.
754
+ fs.mkdir(path.join(dataDir, 'workspace'), { recursive: true }),
728
755
  cleanOldLogs(),
729
756
  ])
730
757
 
@@ -5,6 +5,7 @@ For project tasks:
5
5
  - Prefer the simplest solution that satisfies the request.
6
6
  - Make surgical changes only. Do not refactor unrelated code.
7
7
  - Match existing style.
8
+ - When content has room for visual explanation, first consider whether an SVG diagram can improve understanding.
8
9
  - For multi-step work, use a brief plan.
9
10
  - Before changing files, gather sufficient context: relevant files, entry points or call chains, existing patterns, tests or validation commands, and docs/wiki impact.
10
11
  - Before taking action, confirm with the user.
@@ -86,6 +86,24 @@ export const workspaceTools = [
86
86
  }),
87
87
  executionMode: 'sequential',
88
88
  },
89
+ {
90
+ name: 'present_files',
91
+ label: 'Present files',
92
+ description: 'Show one or more AI-produced artifact files to the user. Use this after creating or editing user-facing files such as HTML pages, SVG/images, Markdown documents, or other deliverables. HTML files will be previewed directly in the artifact preview panel.',
93
+ parameters: Type.Object({
94
+ files: Type.Array(Type.Union([
95
+ Type.String({ description: 'File path relative to the workspace root.' }),
96
+ Type.Object({
97
+ path: Type.String({ description: 'File path relative to the workspace root.' }),
98
+ title: Type.Optional(Type.String({ description: 'Optional display title.' })),
99
+ description: Type.Optional(Type.String({ description: 'Optional short description shown in artifact lists.' })),
100
+ kind: Type.Optional(Type.String({ description: 'Optional file kind hint, such as html, image, markdown, or code.' })),
101
+ preview: Type.Optional(Type.Boolean({ description: 'Whether this file should be opened as the default preview.' })),
102
+ }),
103
+ ]), { description: 'Artifact files to present.' }),
104
+ defaultPreview: Type.Optional(Type.String({ description: 'File path to open as the default preview when multiple files are presented.' })),
105
+ }),
106
+ },
89
107
  ]
90
108
 
91
109
  function activeSkillSchema(skills) {
@@ -619,6 +619,88 @@ export async function toolReadSkillResource(params, context) {
619
619
  }
620
620
  }
621
621
 
622
+ // --- present_files ---
623
+ function inferPresentedFileKind(relativePath) {
624
+ const lower = String(relativePath || '').toLowerCase()
625
+ if (lower.endsWith('.html') || lower.endsWith('.htm')) return 'html'
626
+ if (/\.(svg|png|jpe?g|webp|gif|bmp|ico)$/.test(lower)) return 'image'
627
+ if (lower.endsWith('.md') || lower.endsWith('.mdx')) return 'markdown'
628
+ if (/\.(ts|tsx|js|jsx|mjs|cjs|css|scss|less|json|jsonc|txt|xml|yml|yaml|toml|ini|py|rb|go|rs|java|c|h|cpp|hpp|cs|php|sh|bash|zsh|ps1)$/.test(lower)) return 'code'
629
+ return 'unknown'
630
+ }
631
+
632
+ function normalizePresentFileEntry(entry) {
633
+ if (typeof entry === 'string') return { path: entry }
634
+ if (entry && typeof entry === 'object' && typeof entry.path === 'string') {
635
+ return {
636
+ path: entry.path,
637
+ title: typeof entry.title === 'string' ? entry.title : undefined,
638
+ description: typeof entry.description === 'string' ? entry.description : undefined,
639
+ kind: typeof entry.kind === 'string' ? entry.kind : undefined,
640
+ preview: typeof entry.preview === 'boolean' ? entry.preview : undefined,
641
+ }
642
+ }
643
+ return undefined
644
+ }
645
+
646
+ export async function toolPresentFiles(params, context) {
647
+ const rawFiles = Array.isArray(params?.files) ? params.files : []
648
+ const defaultPreviewInput = typeof params?.defaultPreview === 'string' ? params.defaultPreview : undefined
649
+ if (rawFiles.length === 0) {
650
+ const error = new Error('files must contain at least one path')
651
+ error.statusCode = 400
652
+ throw error
653
+ }
654
+
655
+ const files = []
656
+ const seen = new Set()
657
+ for (const raw of rawFiles) {
658
+ const entry = normalizePresentFileEntry(raw)
659
+ if (!entry?.path?.trim()) continue
660
+ const file = resolveWorkspacePath(entry.path, context)
661
+ await assertSafeWorkspacePath(file, context)
662
+ const stat = await fs.stat(file)
663
+ if (!stat.isFile()) {
664
+ const error = new Error(`Path is not a file: ${entry.path}`)
665
+ error.statusCode = 400
666
+ throw error
667
+ }
668
+ const relativePath = toWorkspaceRelative(file, context)
669
+ const key = relativePath.toLowerCase()
670
+ if (seen.has(key)) continue
671
+ seen.add(key)
672
+ const kind = entry.kind || inferPresentedFileKind(relativePath)
673
+ const isDefaultPreview = defaultPreviewInput ? relativePath === defaultPreviewInput.replace(/\\/g, '/') : false
674
+ files.push({
675
+ path: relativePath,
676
+ title: entry.title,
677
+ description: entry.description,
678
+ kind,
679
+ preview: entry.preview ?? (isDefaultPreview || kind === 'html'),
680
+ defaultPreview: isDefaultPreview,
681
+ bytes: stat.size,
682
+ })
683
+ }
684
+
685
+ if (files.length === 0) {
686
+ const error = new Error('No valid files to present')
687
+ error.statusCode = 400
688
+ throw error
689
+ }
690
+
691
+ const previewed = files.filter((file) => file.preview || file.kind === 'html').map((file) => file.path)
692
+ return {
693
+ content: `Presented ${files.length} file(s)${previewed.length ? ` and opened ${previewed.length} preview(s)` : ''}: ${files.map((file) => file.path).join(', ')}`,
694
+ details: {
695
+ type: 'present_files_result',
696
+ files,
697
+ defaultPreview: files.find((file) => file.defaultPreview)?.path,
698
+ previewed,
699
+ project: context?.project,
700
+ },
701
+ }
702
+ }
703
+
622
704
  // --- run_command ---
623
705
  const DEFAULT_RUN_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
624
706
  const MIN_RUN_COMMAND_TIMEOUT_MS = 1000
@@ -1009,6 +1091,7 @@ export const toolHandlers = {
1009
1091
  write_file: toolWriteFile,
1010
1092
  edit_file: toolEditFile,
1011
1093
  run_command: toolRunCommand,
1094
+ present_files: toolPresentFiles,
1012
1095
  activate_skill: toolActivateSkill,
1013
1096
  read_skill_resource: toolReadSkillResource,
1014
1097
  }
@@ -0,0 +1,156 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { promises as fs } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ function normalizeRepositoryUrl(value) {
6
+ if (!value || typeof value !== 'string') return ''
7
+ return value
8
+ .replace(/^git\+/i, '')
9
+ .replace(/^git@github\.com:/i, 'https://github.com/')
10
+ .replace(/\.git$/i, '')
11
+ }
12
+
13
+ export async function getPackageInfo(projectRoot) {
14
+ const packageJsonPath = path.join(projectRoot, 'package.json')
15
+ try {
16
+ const text = await fs.readFile(packageJsonPath, 'utf8')
17
+ const pkg = JSON.parse(text)
18
+ const repositoryUrl = normalizeRepositoryUrl(
19
+ typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url,
20
+ )
21
+
22
+ return {
23
+ name: pkg.name || 'quickforge',
24
+ version: pkg.version || '0.0.0',
25
+ repositoryUrl,
26
+ homepage: pkg.homepage || repositoryUrl,
27
+ bugsUrl: typeof pkg.bugs === 'string' ? pkg.bugs : pkg.bugs?.url || '',
28
+ }
29
+ } catch (error) {
30
+ throw new Error(`Unable to read package metadata: ${error.message}`)
31
+ }
32
+ }
33
+
34
+ function normalizeVersion(version) {
35
+ return String(version || '').trim().replace(/^v/i, '')
36
+ }
37
+
38
+ function parseVersion(version) {
39
+ const [main, prerelease = ''] = normalizeVersion(version).split('-', 2)
40
+ const numbers = main.split('.').slice(0, 3).map((part) => {
41
+ const value = Number(part)
42
+ return Number.isFinite(value) ? value : 0
43
+ })
44
+
45
+ while (numbers.length < 3) numbers.push(0)
46
+ return { numbers, prerelease }
47
+ }
48
+
49
+ function comparePrerelease(left, right) {
50
+ if (left === right) return 0
51
+ if (!left) return 1
52
+ if (!right) return -1
53
+
54
+ const leftParts = left.split('.')
55
+ const rightParts = right.split('.')
56
+ const maxLength = Math.max(leftParts.length, rightParts.length)
57
+
58
+ for (let i = 0; i < maxLength; i += 1) {
59
+ const leftPart = leftParts[i]
60
+ const rightPart = rightParts[i]
61
+ if (leftPart === rightPart) continue
62
+ if (leftPart === undefined) return -1
63
+ if (rightPart === undefined) return 1
64
+
65
+ const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null
66
+ const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null
67
+
68
+ if (leftNumber !== null && rightNumber !== null) return leftNumber > rightNumber ? 1 : -1
69
+ if (leftNumber !== null) return -1
70
+ if (rightNumber !== null) return 1
71
+
72
+ return leftPart > rightPart ? 1 : -1
73
+ }
74
+
75
+ return 0
76
+ }
77
+
78
+ export function compareVersions(left, right) {
79
+ const parsedLeft = parseVersion(left)
80
+ const parsedRight = parseVersion(right)
81
+
82
+ for (let i = 0; i < 3; i += 1) {
83
+ if (parsedLeft.numbers[i] > parsedRight.numbers[i]) return 1
84
+ if (parsedLeft.numbers[i] < parsedRight.numbers[i]) return -1
85
+ }
86
+
87
+ return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease)
88
+ }
89
+
90
+ function getRegistryPackageUrl(packageName) {
91
+ const registry = (process.env.npm_config_registry || 'https://registry.npmjs.org/').replace(/\/+$/, '')
92
+ return `${registry}/${encodeURIComponent(packageName)}`
93
+ }
94
+
95
+ export async function fetchLatestVersion(packageName) {
96
+ const controller = new AbortController()
97
+ const timeout = setTimeout(() => controller.abort(), 5000)
98
+
99
+ try {
100
+ const response = await fetch(getRegistryPackageUrl(packageName), {
101
+ headers: { accept: 'application/json' },
102
+ signal: controller.signal,
103
+ })
104
+
105
+ if (!response.ok) throw new Error(`registry returned HTTP ${response.status}`)
106
+
107
+ const metadata = await response.json()
108
+ const latest = metadata?.['dist-tags']?.latest
109
+ if (!latest || typeof latest !== 'string') throw new Error('latest version not found in registry response')
110
+ return latest
111
+ } catch (error) {
112
+ if (error.name === 'AbortError') throw new Error('request timeout')
113
+ throw error
114
+ } finally {
115
+ clearTimeout(timeout)
116
+ }
117
+ }
118
+
119
+ export async function checkForUpdates(projectRoot) {
120
+ const pkg = await getPackageInfo(projectRoot)
121
+ const latestVersion = await fetchLatestVersion(pkg.name)
122
+ const comparison = compareVersions(pkg.version, latestVersion)
123
+ return {
124
+ ...pkg,
125
+ currentVersion: pkg.version,
126
+ latestVersion,
127
+ updateAvailable: comparison < 0,
128
+ localVersionIsNewer: comparison > 0,
129
+ installCommand: `npm install -g ${pkg.name}@latest`,
130
+ }
131
+ }
132
+
133
+ function getNpmCommand() {
134
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm'
135
+ }
136
+
137
+ export async function installLatestVersion(packageName, options = {}) {
138
+ const target = `${packageName}@latest`
139
+ const child = spawn(getNpmCommand(), ['install', '-g', target], {
140
+ cwd: options.cwd,
141
+ stdio: 'ignore',
142
+ shell: process.platform === 'win32',
143
+ windowsHide: true,
144
+ })
145
+
146
+ return new Promise((resolve, reject) => {
147
+ child.on('error', reject)
148
+ child.on('exit', (code) => {
149
+ if (code === 0) {
150
+ resolve()
151
+ return
152
+ }
153
+ reject(new Error(`npm install exited with code ${code}`))
154
+ })
155
+ })
156
+ }
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{ct as t,s as n,ut as r}from"./icons-BWtivFsx.js";import{n as i}from"./react-vendor-Mthyt1p4.js";import{E as a,O as o,S as s,T as c,b as l,v as u,x as d,y as f}from"./index-Dcf73EL8.js";var p=e(r(),1),m=i();function h(){return{name:``,label:``,description:``,systemPrompt:``,allowedTools:[`read_file`,`grep_files`],maxRuntimeMs:`1800000`,maxToolCalls:`300`,enabledAsSubagent:!0}}function g(e){return{name:e.name,label:e.label,description:e.description??``,systemPrompt:e.systemPrompt??``,allowedTools:e.allowedTools??[],maxRuntimeMs:String(e.maxRuntimeMs??18e5),maxToolCalls:String(e.maxToolCalls??300),enabledAsSubagent:e.enabledAsSubagent}}function _(e){return{name:e.name.trim().toLowerCase(),label:e.label.trim(),description:e.description.trim(),systemPrompt:e.systemPrompt.trim(),allowedTools:e.allowedTools,maxRuntimeMs:Number(e.maxRuntimeMs||18e5),maxToolCalls:Number(e.maxToolCalls||300),enabledAsSubagent:e.enabledAsSubagent}}function v(e){return!!(e.name.trim()&&e.label.trim()&&e.allowedTools.length>0)}async function y(e,t){let n=await fetch(e,{...t,headers:{"content-type":`application/json`,...t?.headers}}),r=await n.json().catch(()=>null);if(!n.ok)throw Error(r?.error||`请求失败`);return r}function b(){let[e,r]=(0,p.useState)([]),[i,b]=(0,p.useState)([]),[x,S]=(0,p.useState)(!1),[C,w]=(0,p.useState)(null),[T,E]=(0,p.useState)(()=>h()),[D,O]=(0,p.useState)(!1),[k,A]=(0,p.useState)(``),[j,M]=(0,p.useState)(!1),[N,P]=(0,p.useState)(),[F,I]=(0,p.useState)(`off`),[L,R]=(0,p.useState)(``);async function z(){let[e,t]=await Promise.all([y(`/api/agent-profiles`),y(`/api/agent-profiles/available-tools`)]);r(e.agents),b(t.tools)}(0,p.useEffect)(()=>{let e=!1;async function t(){try{let[t,n]=await Promise.all([y(`/api/agent-profiles`),y(`/api/agent-profiles/available-tools`)]);if(e)return;r(t.agents),b(n.tools)}catch(t){e||R(t instanceof Error?t.message:o(`requestFailed`))}}return t(),()=>{e=!0}},[]),(0,p.useEffect)(()=>{let e=!1;async function t(){try{let t=await l(),n=await f(t),r=await d(t),i=r.model??await s(t)??n[0];if(e)return;P(i),I(r.thinkingLevel??u(i))}catch{}}return t(),()=>{e=!0}},[]);let B=(0,p.useMemo)(()=>e.find(e=>e.id===C)??null,[e,C]);function V(e,t){E(n=>({...n,[e]:t}))}function H(e){E(t=>({...t,allowedTools:t.allowedTools.includes(e)?t.allowedTools.filter(t=>t!==e):[...t.allowedTools,e]}))}function U(){w(null),E(h()),A(``),R(``),S(!0)}function W(e){w(e.id),E(g(e)),A(``),R(``),S(!0)}function G(){D||j||(S(!1),w(null),E(h()),A(``))}async function K(){let e=k.trim();if(!e){R(o(`aiFillAgentInputRequired`));return}if(!N){R(o(`aiFillAgentNoModel`));return}M(!0),R(``);try{let t=await y(`/api/agent-profiles/ai-fill`,{method:`POST`,body:JSON.stringify({instruction:e,model:N,thinkingLevel:F})});E(e=>({...e,name:t.agent.name,label:t.agent.label,description:t.agent.description,systemPrompt:t.agent.systemPrompt}))}catch(e){R(e instanceof Error?e.message:o(`aiFillAgentFailed`))}finally{M(!1)}}async function q(){if(v(T)){O(!0),R(``);try{let e=_(T);C?await y(`/api/agent-profiles/${encodeURIComponent(C)}`,{method:`PATCH`,body:JSON.stringify(e)}):await y(`/api/agent-profiles`,{method:`POST`,body:JSON.stringify(e)}),G(),await z()}catch(e){R(e instanceof Error?e.message:o(`requestFailed`))}finally{O(!1)}}}async function J(e){if(!e.builtin&&await c({description:o(`confirmDeleteAgent`),confirmLabel:o(`confirmDelete`),cancelLabel:o(`cancel`),variant:`destructive`})){R(``);try{await y(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await z()}catch(e){R(e instanceof Error?e.message:o(`requestFailed`))}}}return(0,m.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col overflow-hidden bg-background`,children:[(0,m.jsx)(`div`,{className:`border-b border-border px-6 py-5`,children:(0,m.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-3`,children:[(0,m.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,m.jsx)(`div`,{className:`flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary`,children:(0,m.jsx)(t,{className:`size-5`})}),(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h1`,{className:`text-lg font-semibold text-foreground`,children:o(`agentsTab`)}),(0,m.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:o(`agentsDescription`)})]})]}),(0,m.jsx)(a,{onClick:U,children:o(`createAgent`)})]})}),(0,m.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto p-6`,children:(0,m.jsxs)(`div`,{className:`mx-auto max-w-5xl space-y-5`,children:[L&&!x?(0,m.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:L}):null,(0,m.jsx)(`div`,{className:`grid gap-4 md:grid-cols-2`,children:e.map(e=>(0,m.jsxs)(`div`,{className:`rounded-2xl border border-border bg-card p-4 shadow-sm`,children:[(0,m.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,m.jsxs)(`div`,{className:`min-w-0`,children:[(0,m.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,m.jsx)(`h3`,{className:`truncate text-base font-semibold text-foreground`,children:e.label}),e.builtin?(0,m.jsx)(`span`,{className:`rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary`,children:o(`builtinAgent`)}):null,e.enabledAsSubagent?(0,m.jsx)(`span`,{className:`rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs text-emerald-700`,children:o(`enabledAsSubagent`)}):null]}),(0,m.jsx)(`p`,{className:`mt-1 font-mono text-xs text-muted-foreground`,children:e.name}),(0,m.jsx)(`p`,{className:`mt-2 text-sm text-muted-foreground`,children:e.description||o(`noDescription`)})]}),(0,m.jsxs)(`div`,{className:`flex shrink-0 gap-1`,children:[(0,m.jsx)(a,{variant:`outline`,size:`sm`,disabled:e.builtin,onClick:()=>W(e),children:o(`editTask`)}),(0,m.jsx)(a,{variant:`destructive`,size:`sm`,disabled:e.builtin,onClick:()=>void J(e),children:o(`delete`)})]})]}),(0,m.jsx)(`div`,{className:`mt-3 flex flex-wrap gap-1`,children:e.allowedTools.map(e=>(0,m.jsx)(`span`,{className:`rounded-full bg-muted px-2 py-0.5 font-mono text-xs text-muted-foreground`,children:e},e))}),(0,m.jsxs)(`div`,{className:`mt-3 grid gap-2 border-t border-border pt-3 text-xs text-muted-foreground sm:grid-cols-2`,children:[(0,m.jsxs)(`span`,{children:[o(`maxRuntimeMs`),e.maxRuntimeMs??`-`]}),(0,m.jsxs)(`span`,{children:[o(`maxToolCalls`),e.maxToolCalls??`-`]})]})]},e.id))})]})}),x?(0,m.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,onMouseDown:e=>{e.target===e.currentTarget&&G()},children:(0,m.jsxs)(`div`,{className:`flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-2xl`,onMouseDown:e=>e.stopPropagation(),children:[(0,m.jsxs)(`div`,{className:`shrink-0 border-b border-border px-5 py-4`,children:[(0,m.jsx)(`h2`,{className:`text-base font-semibold text-foreground`,children:o(B?`editAgent`:`createAgent`)}),B?.builtin?(0,m.jsx)(`p`,{className:`mt-1 text-sm text-muted-foreground`,children:o(`builtinAgentReadonly`)}):null]}),(0,m.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-5 py-4`,children:(0,m.jsxs)(`div`,{className:`space-y-4`,children:[(0,m.jsxs)(`div`,{className:`rounded-2xl border border-border bg-muted/20 p-3`,children:[(0,m.jsxs)(`div`,{className:`mb-2 flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,m.jsx)(n,{className:`size-4 text-primary`}),o(`aiFillAgent`)]}),(0,m.jsx)(`p`,{className:`mb-2 text-xs text-muted-foreground`,children:o(`aiFillAgentDescription`)}),(0,m.jsx)(`textarea`,{className:`min-h-20 w-full resize-y rounded-xl border border-input bg-background px-3 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground/65 focus:border-ring disabled:opacity-60`,value:k,disabled:!!B?.builtin||j,onChange:e=>A(e.target.value),placeholder:o(`aiFillAgentPlaceholder`)}),(0,m.jsx)(`div`,{className:`mt-2 flex justify-end`,children:(0,m.jsxs)(a,{variant:`outline`,size:`sm`,onClick:()=>void K(),disabled:!!B?.builtin||j||!k.trim(),children:[(0,m.jsx)(n,{className:`mr-1 size-3.5`}),o(j?`aiFillAgentLoading`:`aiFillAgent`)]})})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`agentName`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:T.name,disabled:!!B?.builtin,onChange:e=>V(`name`,e.target.value),placeholder:`reviewer`})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`agentLabel`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:T.label,disabled:!!B?.builtin,onChange:e=>V(`label`,e.target.value),placeholder:o(`agentLabelPlaceholder`)})]})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`agentDescription`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:T.description,disabled:!!B?.builtin,onChange:e=>V(`description`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`agentSystemPrompt`),(0,m.jsx)(`textarea`,{className:`mt-1 min-h-36 w-full resize-y rounded-xl border border-input bg-background px-3 py-2 text-sm outline-none focus:border-ring disabled:opacity-60`,value:T.systemPrompt,disabled:!!B?.builtin,onChange:e=>V(`systemPrompt`,e.target.value)})]}),(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`div`,{className:`mb-2 text-sm font-medium text-foreground`,children:o(`allowedTools`)}),(0,m.jsx)(`div`,{className:`grid gap-2 sm:grid-cols-2`,children:i.map(e=>(0,m.jsxs)(`label`,{className:`flex items-start gap-2 rounded-xl border border-border bg-muted/20 p-3 text-sm disabled:opacity-60`,children:[(0,m.jsx)(`input`,{type:`checkbox`,className:`mt-1`,disabled:!!B?.builtin,checked:T.allowedTools.includes(e.name),onChange:()=>H(e.name)}),(0,m.jsxs)(`span`,{children:[(0,m.jsx)(`span`,{className:`font-medium text-foreground`,children:e.label}),(0,m.jsx)(`span`,{className:`ml-2 font-mono text-xs text-muted-foreground`,children:e.name}),e.riskLevel===`dangerous`?(0,m.jsx)(`span`,{className:`ml-2 rounded-full bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700`,children:o(`highRiskTool`)}):null,(0,m.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:e.description})]})]},e.name))})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`maxRuntimeMs`),(0,m.jsx)(`input`,{type:`number`,className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:T.maxRuntimeMs,disabled:!!B?.builtin,onChange:e=>V(`maxRuntimeMs`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`maxToolCalls`),(0,m.jsx)(`input`,{type:`number`,className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:T.maxToolCalls,disabled:!!B?.builtin,onChange:e=>V(`maxToolCalls`,e.target.value)})]})]}),(0,m.jsxs)(`label`,{className:`flex items-center gap-2 text-sm text-foreground`,children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:T.enabledAsSubagent,disabled:!!B?.builtin,onChange:e=>V(`enabledAsSubagent`,e.target.checked)}),o(`enabledAsSubagent`)]}),L?(0,m.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:L}):null]})}),(0,m.jsx)(`div`,{className:`shrink-0 border-t border-border px-5 py-4`,children:(0,m.jsxs)(`div`,{className:`flex justify-end gap-2`,children:[(0,m.jsx)(a,{variant:`outline`,onClick:G,disabled:D||j,children:o(`cancel`)}),(0,m.jsx)(a,{onClick:q,disabled:D||j||!!B?.builtin||!v(T),children:o(`save`)})]})})]})}):null]})}export{b as AgentProfilesPage};