cc-viewer 1.6.342 → 1.6.344

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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/App-BjHJ9KJ6.css +1 -0
  3. package/dist/assets/App-D-TSp4Vf.js +2 -0
  4. package/dist/assets/{MdxEditorPanel-CaDMFlsk.js → MdxEditorPanel-BneOGbUW.js} +1 -1
  5. package/dist/assets/{Mobile-C22UBGse.js → Mobile-COacALMA.js} +1 -1
  6. package/dist/assets/index-C1YvPKov.js +2 -0
  7. package/dist/assets/seqResourceLoaders-BPIKMDJV.js +2 -0
  8. package/dist/assets/{seqResourceLoaders-g06U0FFU.css → seqResourceLoaders-CFOTQBIe.css} +1 -1
  9. package/dist/index.html +1 -1
  10. package/package.json +1 -1
  11. package/server/lib/create_system_prompt.js +525 -0
  12. package/server/lib/system-prompt-presets.js +68 -0
  13. package/server/routes/expert.js +21 -0
  14. package/server/system-prompt-templates/presets/GLM-5.2.md +67 -0
  15. package/server/system-prompt-templates/presets/Qwen-3.7-Max.md +67 -0
  16. package/server/system-prompt-templates/presets/deepseek-v4-flash.md +62 -0
  17. package/server/system-prompt-templates/presets/deepseek-v4-pro.md +70 -0
  18. package/server/system-prompt-templates/presets/index.json +47 -0
  19. package/server/system-prompt-templates/presets/kimi-k2.7-code.md +69 -0
  20. package/server/system-prompt-templates/reference/claude-code-cli-startup-options.md +325 -0
  21. package/server/system-prompt-templates/reference/prompt-control-tokens-deepseek-v4.md +178 -0
  22. package/server/system-prompt-templates/reference/prompt-control-tokens-qwen3.md +227 -0
  23. package/server/system-prompt-templates/reference/prompt-xml-tags-fable-5.md +101 -0
  24. package/server/system-prompt-templates/reference/prompt-xml-tags-opus-4-8.md +113 -0
  25. package/server/system-prompt-templates/systemPromptModel.md +168 -0
  26. package/server/system-prompt-templates/systemPromptVariables.ar.md +113 -0
  27. package/server/system-prompt-templates/systemPromptVariables.da.md +113 -0
  28. package/server/system-prompt-templates/systemPromptVariables.de.md +113 -0
  29. package/server/system-prompt-templates/systemPromptVariables.es.md +113 -0
  30. package/server/system-prompt-templates/systemPromptVariables.fr.md +113 -0
  31. package/server/system-prompt-templates/systemPromptVariables.it.md +113 -0
  32. package/server/system-prompt-templates/systemPromptVariables.ja.md +113 -0
  33. package/server/system-prompt-templates/systemPromptVariables.ko.md +113 -0
  34. package/server/system-prompt-templates/systemPromptVariables.md +113 -0
  35. package/server/system-prompt-templates/systemPromptVariables.no.md +113 -0
  36. package/server/system-prompt-templates/systemPromptVariables.pl.md +113 -0
  37. package/server/system-prompt-templates/systemPromptVariables.pt-BR.md +113 -0
  38. package/server/system-prompt-templates/systemPromptVariables.ru.md +113 -0
  39. package/server/system-prompt-templates/systemPromptVariables.th.md +113 -0
  40. package/server/system-prompt-templates/systemPromptVariables.tr.md +113 -0
  41. package/server/system-prompt-templates/systemPromptVariables.uk.md +113 -0
  42. package/server/system-prompt-templates/systemPromptVariables.zh-TW.md +113 -0
  43. package/server/system-prompt-templates/systemPromptVariables.zh.md +113 -0
  44. package/dist/assets/App-BFcpzWEl.js +0 -2
  45. package/dist/assets/App-D5BI6yGO.css +0 -1
  46. package/dist/assets/index-CtSbjm-X.js +0 -2
  47. package/dist/assets/seqResourceLoaders-BqKidNto.js +0 -2
package/dist/index.html CHANGED
@@ -21,7 +21,7 @@
21
21
  // 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
22
22
  // electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
23
23
  </script>
24
- <script type="module" crossorigin src="./assets/index-CtSbjm-X.js"></script>
24
+ <script type="module" crossorigin src="./assets/index-C1YvPKov.js"></script>
25
25
  <link rel="modulepreload" crossorigin href="./assets/vendor-antd-D5HmSDSg.js">
26
26
  <link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-C5yuj7ze.js">
27
27
  <link rel="modulepreload" crossorigin href="./assets/vendor-mdxeditor-BcEooTvb.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.342",
3
+ "version": "1.6.344",
4
4
  "description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -0,0 +1,525 @@
1
+ // System-prompt template builder: resolves runtime variables, loads
2
+ // systemPromptModel.md + the presets, and renders/assembles system-prompt text.
3
+ // Consumed by server/lib/system-prompt-presets.js and runnable directly as a CLI.
4
+ //
5
+ // CLI usage (renders with missing variables blanked out):
6
+ // node server/lib/create_system_prompt.js # base model template
7
+ // node server/lib/create_system_prompt.js deepseek-v4-pro # a named preset
8
+ // node server/lib/create_system_prompt.js --list # list presets
9
+
10
+ import { execFileSync } from 'node:child_process'
11
+ import { readFileSync, statSync } from 'node:fs'
12
+ import os from 'node:os'
13
+ import { delimiter, join, sep } from 'node:path'
14
+ import { pathToFileURL } from 'node:url'
15
+
16
+ const TEMPLATE_VARIABLE_PATTERN = /\$\{([^}]+)\}/g
17
+
18
+ export const DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
19
+
20
+ const MODEL_TEMPLATE_URL = new URL('../system-prompt-templates/systemPromptModel.md', import.meta.url)
21
+ const PRESETS_DIR_URL = new URL('../system-prompt-templates/presets/', import.meta.url)
22
+ const PRESETS_INDEX_URL = new URL('index.json', PRESETS_DIR_URL)
23
+ const PRESET_ID_PATTERN = /^[A-Za-z0-9._-]+$/
24
+
25
+ function stringifyTemplateValue(value) {
26
+ if (typeof value === 'string') return value
27
+ if (typeof value === 'number') {
28
+ return Number.isFinite(value) ? String(value) : ''
29
+ }
30
+ return ''
31
+ }
32
+
33
+ function stringOrEmpty(readValue) {
34
+ try {
35
+ const value = readValue()
36
+ if (value === null || value === undefined) return ''
37
+ return String(value)
38
+ } catch {
39
+ return ''
40
+ }
41
+ }
42
+
43
+ function numberOrEmpty(readValue) {
44
+ try {
45
+ const value = Number(readValue())
46
+ return Number.isFinite(value) ? value : ''
47
+ } catch {
48
+ return ''
49
+ }
50
+ }
51
+
52
+ function envString(name) {
53
+ return stringOrEmpty(() => process.env[name] ?? '')
54
+ }
55
+
56
+ function commandOutput(command, args, cwd) {
57
+ return stringOrEmpty(() =>
58
+ execFileSync(command, args, {
59
+ cwd,
60
+ encoding: 'utf8',
61
+ stdio: ['ignore', 'pipe', 'ignore'],
62
+ }).trim(),
63
+ )
64
+ }
65
+
66
+ function firstNonEmpty(...values) {
67
+ return values.find(value => value.length > 0) ?? ''
68
+ }
69
+
70
+ function currentDate(timeZone, date) {
71
+ if (timeZone.length > 0) {
72
+ const formatted = stringOrEmpty(() =>
73
+ new Intl.DateTimeFormat('en-CA', {
74
+ timeZone,
75
+ year: 'numeric',
76
+ month: '2-digit',
77
+ day: '2-digit',
78
+ }).format(date),
79
+ )
80
+ if (formatted.length > 0) return formatted
81
+ }
82
+ return stringOrEmpty(() => date.toISOString().slice(0, 10))
83
+ }
84
+
85
+ function mergeSystemPromptVariables(base, overrides) {
86
+ return {
87
+ environment: { ...base.environment, ...overrides.environment },
88
+ git: { ...base.git, ...overrides.git },
89
+ os: { ...base.os, ...overrides.os },
90
+ runtime: { ...base.runtime, ...overrides.runtime },
91
+ time: { ...base.time, ...overrides.time },
92
+ permissions: { ...base.permissions, ...overrides.permissions },
93
+ sandbox: { ...base.sandbox, ...overrides.sandbox },
94
+ terminal: { ...base.terminal, ...overrides.terminal },
95
+ filesystem: { ...base.filesystem, ...overrides.filesystem },
96
+ model: { ...base.model, ...overrides.model },
97
+ memory: { ...base.memory, ...overrides.memory },
98
+ scratchpad: { ...base.scratchpad, ...overrides.scratchpad },
99
+ }
100
+ }
101
+
102
+ function slugifyPath(value) {
103
+ return value.replace(/[^A-Za-z0-9]/g, '-')
104
+ }
105
+
106
+ function directoryExists(dir) {
107
+ if (dir.length === 0) return false
108
+ try {
109
+ return statSync(dir).isDirectory()
110
+ } catch {
111
+ return false
112
+ }
113
+ }
114
+
115
+ function resolveMemory(home, cwd) {
116
+ const overrideDir = firstNonEmpty(
117
+ envString('CC_MEMORY_DIR'),
118
+ envString('CLAUDE_MEMORY_DIR'),
119
+ )
120
+ const dir =
121
+ overrideDir.length > 0
122
+ ? overrideDir
123
+ : home.length > 0
124
+ ? join(home, '.claude', 'projects', slugifyPath(cwd), 'memory') + sep
125
+ : ''
126
+ const enabled = directoryExists(dir)
127
+ const index = enabled
128
+ ? stringOrEmpty(() => readFileSync(join(dir, 'MEMORY.md'), 'utf8'))
129
+ : ''
130
+ return { dir, index, enabled: enabled ? 'true' : 'false' }
131
+ }
132
+
133
+ export function createSystemPromptVariables(overrides = {}) {
134
+ const cwd = stringOrEmpty(() => process.cwd())
135
+ const now = new Date()
136
+ const timeZone = stringOrEmpty(
137
+ () => Intl.DateTimeFormat().resolvedOptions().timeZone,
138
+ )
139
+ const isGitRepository =
140
+ commandOutput('git', ['rev-parse', '--is-inside-work-tree'], cwd) === 'true'
141
+ const gitRoot = isGitRepository
142
+ ? commandOutput('git', ['rev-parse', '--show-toplevel'], cwd)
143
+ : ''
144
+ const gitBranch = isGitRepository
145
+ ? firstNonEmpty(
146
+ commandOutput('git', ['branch', '--show-current'], cwd),
147
+ commandOutput('git', ['rev-parse', '--short', 'HEAD'], cwd),
148
+ )
149
+ : ''
150
+ const gitMainBranch = isGitRepository
151
+ ? firstNonEmpty(
152
+ commandOutput('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], cwd).replace(/^origin\//, ''),
153
+ commandOutput('git', ['config', '--get', 'init.defaultBranch'], cwd),
154
+ )
155
+ : ''
156
+
157
+ const variables = {
158
+ environment: {
159
+ cwd,
160
+ originalCwd: envString('PWD'),
161
+ home: envString('HOME'),
162
+ user: firstNonEmpty(envString('USER'), envString('USERNAME')),
163
+ workspaceRoots: firstNonEmpty(
164
+ envString('CODEX_WORKSPACE_ROOTS'),
165
+ envString('CLAUDE_WORKSPACE_ROOTS'),
166
+ ),
167
+ path: envString('PATH'),
168
+ lang: firstNonEmpty(envString('LANG'), envString('LC_ALL')),
169
+ },
170
+ git: {
171
+ isRepository: isGitRepository ? 'true' : 'false',
172
+ root: gitRoot,
173
+ branch: gitBranch,
174
+ mainBranch: gitMainBranch,
175
+ userName: isGitRepository
176
+ ? commandOutput('git', ['config', 'user.name'], cwd)
177
+ : '',
178
+ status: isGitRepository
179
+ ? commandOutput('git', ['status', '--short'], cwd)
180
+ : '',
181
+ recentCommits: isGitRepository
182
+ ? commandOutput('git', ['log', '--oneline', '-5'], cwd)
183
+ : '',
184
+ },
185
+ os: {
186
+ platform: stringOrEmpty(() => process.platform),
187
+ type: stringOrEmpty(() => os.type()),
188
+ arch: stringOrEmpty(() => os.arch()),
189
+ shell: envString('SHELL'),
190
+ version: stringOrEmpty(() => os.version()),
191
+ release: stringOrEmpty(() => os.release()),
192
+ hostname: stringOrEmpty(() => os.hostname()),
193
+ availableParallelism: numberOrEmpty(() => os.availableParallelism()),
194
+ totalMemory: numberOrEmpty(() => os.totalmem()),
195
+ freeMemory: numberOrEmpty(() => os.freemem()),
196
+ uptime: numberOrEmpty(() => os.uptime()),
197
+ },
198
+ runtime: {
199
+ nodeVersion: stringOrEmpty(() => process.version),
200
+ execPath: stringOrEmpty(() => process.execPath),
201
+ pid: numberOrEmpty(() => process.pid),
202
+ ppid: numberOrEmpty(() => process.ppid),
203
+ },
204
+ time: {
205
+ current: stringOrEmpty(() => now.toString()),
206
+ iso: stringOrEmpty(() => now.toISOString()),
207
+ date: currentDate(timeZone, now),
208
+ timezone: timeZone,
209
+ },
210
+ permissions: {
211
+ mode: firstNonEmpty(
212
+ envString('CLAUDE_PERMISSION_MODE'),
213
+ envString('CODEX_PERMISSION_MODE'),
214
+ ),
215
+ approvalsReviewer: envString('CODEX_APPROVALS_REVIEWER'),
216
+ },
217
+ sandbox: {
218
+ mode: firstNonEmpty(envString('SANDBOX_MODE'), envString('CODEX_SANDBOX_MODE')),
219
+ networkAccess: firstNonEmpty(
220
+ envString('NETWORK_ACCESS'),
221
+ envString('CODEX_NETWORK_ACCESS'),
222
+ ),
223
+ writableRoots: firstNonEmpty(
224
+ envString('WRITABLE_ROOTS'),
225
+ envString('CODEX_WRITABLE_ROOTS'),
226
+ ),
227
+ },
228
+ terminal: {
229
+ term: envString('TERM'),
230
+ colorTerm: envString('COLORTERM'),
231
+ columns: numberOrEmpty(() => process.stdout.columns),
232
+ rows: numberOrEmpty(() => process.stdout.rows),
233
+ },
234
+ filesystem: {
235
+ tmpdir: stringOrEmpty(() => os.tmpdir()),
236
+ pathSeparator: sep,
237
+ pathDelimiter: delimiter,
238
+ },
239
+ model: {
240
+ name: firstNonEmpty(envString('CLAUDE_MODEL'), envString('ANTHROPIC_MODEL')),
241
+ knowledgeCutoff: envString('CLAUDE_KNOWLEDGE_CUTOFF'),
242
+ },
243
+ memory: resolveMemory(envString('HOME'), cwd),
244
+ scratchpad: {
245
+ dir: firstNonEmpty(
246
+ envString('CC_SCRATCHPAD_DIR'),
247
+ envString('CLAUDE_SCRATCHPAD_DIR'),
248
+ ),
249
+ },
250
+ }
251
+
252
+ return mergeSystemPromptVariables(variables, overrides)
253
+ }
254
+
255
+ function readDottedPath(path, variables) {
256
+ const normalizedPath = path.trim()
257
+ if (Object.prototype.hasOwnProperty.call(variables, normalizedPath)) {
258
+ return variables[normalizedPath]
259
+ }
260
+
261
+ return normalizedPath.split('.').reduce((current, key) => {
262
+ if (
263
+ current === null ||
264
+ current === undefined ||
265
+ typeof current !== 'object' ||
266
+ !Object.prototype.hasOwnProperty.call(current, key)
267
+ ) {
268
+ return undefined
269
+ }
270
+ return current[key]
271
+ }, variables)
272
+ }
273
+
274
+ function replaceTemplateVariable(rawMatch, rawName, variables, missingVariableMode) {
275
+ const value = readDottedPath(rawName, variables)
276
+ if (value !== undefined) {
277
+ return stringifyTemplateValue(value)
278
+ }
279
+
280
+ if (missingVariableMode === 'keep') return rawMatch
281
+ if (missingVariableMode === 'throw') {
282
+ throw new Error(`Missing system prompt template variable: ${rawName.trim()}`)
283
+ }
284
+ return ''
285
+ }
286
+
287
+ export function listTemplateVariables(markdownTemplate) {
288
+ const names = new Set()
289
+ for (const match of markdownTemplate.matchAll(TEMPLATE_VARIABLE_PATTERN)) {
290
+ names.add(match[1].trim())
291
+ }
292
+ return Array.from(names).sort()
293
+ }
294
+
295
+ export function createSystemPrompt(markdownTemplate, options) {
296
+ const variables = options.variables
297
+ const missingVariableMode = options.missingVariableMode ?? 'throw'
298
+
299
+ return markdownTemplate.replace(TEMPLATE_VARIABLE_PATTERN, (match, name) =>
300
+ replaceTemplateVariable(match, name, variables, missingVariableMode),
301
+ )
302
+ }
303
+
304
+ function isTrue(value) {
305
+ return value === 'true'
306
+ }
307
+
308
+ // Ordered header -> key mapping. Section prose lives only in
309
+ // systemPromptModel.md (parsed at runtime), never duplicated here.
310
+ export const SYSTEM_PROMPT_SECTIONS = [
311
+ { key: 'preamble', header: null },
312
+ { key: 'environment', header: '# Environment' },
313
+ { key: 'operatingSystem', header: '# Operating system' },
314
+ { key: 'runtime', header: '# Runtime' },
315
+ { key: 'time', header: '# Time' },
316
+ { key: 'permissionsSandbox', header: '# Permissions and sandbox' },
317
+ { key: 'terminal', header: '# Terminal' },
318
+ { key: 'filesystem', header: '# Filesystem' },
319
+ { key: 'model', header: '# Model' },
320
+ { key: 'git', header: '# Git' },
321
+ {
322
+ key: 'memory',
323
+ header: '# Memory',
324
+ includeWhen: variables => isTrue(variables.memory.enabled),
325
+ },
326
+ {
327
+ key: 'scratchpad',
328
+ header: '# Scratchpad Directory',
329
+ includeWhen: variables => variables.scratchpad.dir.length > 0,
330
+ },
331
+ { key: 'contextManagement', header: '# Context management' },
332
+ ]
333
+
334
+ // Splits a model template into its preamble and a header -> raw-section-text map.
335
+ // Level-1 headers (`# `) inside fenced code blocks are ignored so fenced
336
+ // examples (e.g. the memory frontmatter) do not start new sections.
337
+ function splitTemplate(markdownTemplate) {
338
+ const parts = markdownTemplate.split(DYNAMIC_BOUNDARY)
339
+ if (parts.length !== 2) {
340
+ throw new Error(
341
+ `System prompt template must contain exactly one ${DYNAMIC_BOUNDARY} marker (found ${parts.length - 1})`,
342
+ )
343
+ }
344
+ const preamble = parts[0].trim()
345
+ const sectionsByHeader = new Map()
346
+ const lines = parts[1].split('\n')
347
+ let currentHeader = null
348
+ let buffer = []
349
+ let inFence = false
350
+
351
+ const flush = () => {
352
+ if (currentHeader !== null) {
353
+ sectionsByHeader.set(currentHeader, buffer.join('\n').trim())
354
+ }
355
+ }
356
+
357
+ for (const line of lines) {
358
+ if (line.trimStart().startsWith('```')) {
359
+ inFence = !inFence
360
+ }
361
+ if (!inFence && line.startsWith('# ')) {
362
+ flush()
363
+ currentHeader = line.trim()
364
+ buffer = [line]
365
+ continue
366
+ }
367
+ if (currentHeader !== null) buffer.push(line)
368
+ }
369
+ flush()
370
+ return { preamble, sectionsByHeader }
371
+ }
372
+
373
+ export function assembleSystemPrompt(markdownTemplate, options) {
374
+ const { variables, missingVariableMode, sections } = options
375
+ const known = new Set(SYSTEM_PROMPT_SECTIONS.map(section => section.key))
376
+ if (sections) {
377
+ for (const key of sections) {
378
+ if (!known.has(key)) {
379
+ throw new Error(`Unknown system prompt section key: ${key}`)
380
+ }
381
+ }
382
+ }
383
+ const selected = sections ? new Set(sections) : known
384
+ const { preamble, sectionsByHeader } = splitTemplate(markdownTemplate)
385
+
386
+ const rendered = []
387
+ for (const section of SYSTEM_PROMPT_SECTIONS) {
388
+ if (!selected.has(section.key)) continue
389
+ if (section.includeWhen && !section.includeWhen(variables)) continue
390
+
391
+ let raw
392
+ if (section.header === null) {
393
+ raw = preamble
394
+ } else {
395
+ const found = sectionsByHeader.get(section.header)
396
+ // A template may legitimately include only a subset of sections (e.g. the
397
+ // presets omit git and most environment blocks); skip any that are absent.
398
+ if (found === undefined) continue
399
+ raw = found
400
+ }
401
+ rendered.push(createSystemPrompt(raw, { variables, missingVariableMode }))
402
+ }
403
+
404
+ // The MEMORY.md contents are runtime data, not authored prose, and they are
405
+ // already self-titled ("# Memory index"). Append them verbatim as a trailing
406
+ // block whenever the memory section is present, without re-templating (the
407
+ // index may legitimately contain literal ${...} text).
408
+ if (
409
+ selected.has('memory') &&
410
+ isTrue(variables.memory.enabled) &&
411
+ variables.memory.index.trim().length > 0
412
+ ) {
413
+ rendered.push(variables.memory.index)
414
+ }
415
+
416
+ return rendered.map(part => part.trim()).filter(Boolean).join('\n\n')
417
+ }
418
+
419
+ export function loadModelTemplate() {
420
+ return readFileSync(MODEL_TEMPLATE_URL, 'utf8')
421
+ }
422
+
423
+ // Locales with a translated systemPromptVariables.<locale>.md sibling. Mirrors the
424
+ // UI's LANG_OPTIONS (src/i18n.js) minus 'en', which is the base file itself.
425
+ export const VARIABLES_DOC_LOCALES = [
426
+ 'zh', 'zh-TW', 'ko', 'ja', 'de', 'es', 'fr', 'it', 'da',
427
+ 'pl', 'ru', 'ar', 'no', 'pt-BR', 'th', 'tr', 'uk',
428
+ ]
429
+
430
+ // The human-readable reference for the ${...} template variables (rendered in the
431
+ // "Edit System Prompt" modal's parameter-docs popup). `lang` is whitelisted against
432
+ // VARIABLES_DOC_LOCALES; 'en', unknown values, and a missing localized file all fall
433
+ // back to the English base document.
434
+ export function loadVariablesDoc(lang) {
435
+ if (typeof lang === 'string' && VARIABLES_DOC_LOCALES.includes(lang)) {
436
+ try {
437
+ return readFileSync(
438
+ new URL(`../system-prompt-templates/systemPromptVariables.${lang}.md`, import.meta.url),
439
+ 'utf8',
440
+ )
441
+ } catch (e) {
442
+ // A whitelisted locale should always have a shipped translation file, so a
443
+ // read failure here is diagnostic — log it, then fall back to the English base.
444
+ console.warn(`[CC Viewer] localized variables doc unreadable for ${lang}:`, e.message)
445
+ }
446
+ }
447
+ return readFileSync(new URL('../system-prompt-templates/systemPromptVariables.md', import.meta.url), 'utf8')
448
+ }
449
+
450
+ function assertSafePresetId(id) {
451
+ if (typeof id !== 'string' || !PRESET_ID_PATTERN.test(id) || id.includes('..')) {
452
+ throw new Error(`Invalid preset id: ${JSON.stringify(id)}`)
453
+ }
454
+ return id
455
+ }
456
+
457
+ export function loadPreset(id) {
458
+ // Accept an optional `.md` suffix so `... deepseek-v4-pro.md` resolves too.
459
+ const normalized = typeof id === 'string' ? id.replace(/\.md$/i, '') : id
460
+ const safeId = assertSafePresetId(normalized)
461
+ return readFileSync(new URL(`${safeId}.md`, PRESETS_DIR_URL), 'utf8')
462
+ }
463
+
464
+ export function listPresets() {
465
+ const manifest = JSON.parse(readFileSync(PRESETS_INDEX_URL, 'utf8'))
466
+ return manifest
467
+ }
468
+
469
+ // Strips HTML editor comments (`<!-- ... -->`) that document the preset file but
470
+ // must not leak into the rendered prompt.
471
+ function stripEditorComments(source) {
472
+ return source.replace(/<!--[\s\S]*?-->/g, '').replace(/^\s+/, '')
473
+ }
474
+
475
+ // Normalizes a preset into a full template. Two shapes are supported:
476
+ // - Self-contained (what the shipped presets use): the preset already has its
477
+ // own DYNAMIC_BOUNDARY + dynamic sections, so it is returned as-is.
478
+ // - Preamble-only (no boundary): composed with systemPromptModel.md's shared
479
+ // dynamic tail, so environment snippets can stay unified. None of the shipped
480
+ // presets currently use this path.
481
+ function toFullTemplate(presetSource) {
482
+ const source = stripEditorComments(presetSource)
483
+ if (source.includes(DYNAMIC_BOUNDARY)) return source
484
+ const modelParts = loadModelTemplate().split(DYNAMIC_BOUNDARY)
485
+ return `${source.trim()}\n\n${DYNAMIC_BOUNDARY}\n${modelParts[1]}`
486
+ }
487
+
488
+ export function renderModel(options) {
489
+ return assembleSystemPrompt(loadModelTemplate(), options)
490
+ }
491
+
492
+ export function renderPreset(id, options) {
493
+ return assembleSystemPrompt(toFullTemplate(loadPreset(id)), options)
494
+ }
495
+
496
+ // Returns a preset composed into a full template with the dynamic-boundary marker
497
+ // removed and `${...}` placeholders left LITERAL (no variable substitution). This
498
+ // is the raw text used to pre-fill the "Edit System Prompt" editor, where the user
499
+ // tweaks it before saving.
500
+ export function renderPresetTemplate(id) {
501
+ return toFullTemplate(loadPreset(id))
502
+ .split(DYNAMIC_BOUNDARY)
503
+ .map(part => part.trim())
504
+ .filter(Boolean)
505
+ .join('\n\n')
506
+ }
507
+
508
+ function runCli() {
509
+ const arg = process.argv[2]
510
+ if (arg === '--list' || arg === '-l') {
511
+ process.stdout.write(JSON.stringify(listPresets(), null, 2) + '\n')
512
+ return
513
+ }
514
+ const variables = createSystemPromptVariables()
515
+ const output = arg
516
+ ? renderPreset(arg, { variables, missingVariableMode: 'empty' })
517
+ : renderModel({ variables, missingVariableMode: 'empty' })
518
+ process.stdout.write(output + '\n')
519
+ }
520
+
521
+ // Guard against `process.argv[1]` being undefined (e.g. under `node --test`,
522
+ // where importing this module must not run the CLI).
523
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
524
+ runCli()
525
+ }
@@ -0,0 +1,68 @@
1
+ // Serves the built-in system-prompt presets (server/system-prompt-templates/presets/*)
2
+ // to the "Edit System Prompt" modal. Reads the manifest and renders each preset into
3
+ // raw editor text (placeholders left literal) via the system-prompt builder.
4
+ //
5
+ // This path is pure file reads + string ops — it does NOT call
6
+ // createSystemPromptVariables, so no git subprocess is spawned when serving.
7
+ import { listPresets, renderPresetTemplate, loadVariablesDoc } from './create_system_prompt.js';
8
+
9
+ // Returns a flat list of presets with their raw editor text:
10
+ // [{ id, title, description, match, category, defaultMode, text }]
11
+ // Best-effort per entry: a preset that fails to load/render is logged and
12
+ // skipped rather than failing the whole list.
13
+ export function listSystemPromptPresets() {
14
+ let manifest;
15
+ try {
16
+ manifest = listPresets();
17
+ } catch (e) {
18
+ console.warn('[CC Viewer] system-prompt presets manifest unreadable:', e.message);
19
+ return [];
20
+ }
21
+ const categories = (manifest && manifest.categories) || {};
22
+ const out = [];
23
+ for (const [category, entries] of Object.entries(categories)) {
24
+ if (!Array.isArray(entries)) continue;
25
+ for (const entry of entries) {
26
+ if (!entry || typeof entry.id !== 'string') continue;
27
+ let text;
28
+ try {
29
+ text = renderPresetTemplate(entry.id);
30
+ } catch (e) {
31
+ console.warn(`[CC Viewer] system-prompt preset "${entry.id}" render failed:`, e.message);
32
+ continue;
33
+ }
34
+ out.push({
35
+ id: entry.id,
36
+ title: entry.title || entry.id,
37
+ description: entry.description || '',
38
+ match: entry.match || '',
39
+ category,
40
+ defaultMode: entry.defaultMode === 'override' ? 'override' : 'append',
41
+ text,
42
+ });
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ // Returns the ${...} template-variable reference (systemPromptVariables.md) as
49
+ // markdown, or '' if it can't be read (best-effort; the modal hides its help
50
+ // affordance when empty). `lang` selects a localized systemPromptVariables.<lang>.md
51
+ // when one exists; anything else falls back to the English base.
52
+ export function getSystemPromptVariablesDoc(lang) {
53
+ try {
54
+ return loadVariablesDoc(lang);
55
+ } catch (e) {
56
+ console.warn('[CC Viewer] system-prompt variables doc unreadable:', e.message);
57
+ return '';
58
+ }
59
+ }
60
+
61
+ // Groups a flat preset list by category, preserving first-seen order.
62
+ export function groupPresetsByCategory(presets) {
63
+ const categories = {};
64
+ for (const p of presets) {
65
+ (categories[p.category] || (categories[p.category] = [])).push(p);
66
+ }
67
+ return categories;
68
+ }
@@ -12,6 +12,7 @@ import {
12
12
  MODEL_PROMPT_DIR, normalizeModelName, listModelPrompts,
13
13
  writeModelPrompt, deleteModelPrompt,
14
14
  } from '../lib/model-system-prompts.js';
15
+ import { listSystemPromptPresets, groupPresetsByCategory, getSystemPromptVariablesDoc } from '../lib/system-prompt-presets.js';
15
16
  import { LOG_DIR } from '../../findcc.js';
16
17
 
17
18
  // 当前工作区目录全部由服务端解析(绝不接收客户端路径 → 无遍历面):
@@ -148,9 +149,29 @@ function postModelPrompts(req, res, parsedUrl, isLocal, deps) {
148
149
  });
149
150
  }
150
151
 
152
+ // 内置系统提示词预设(server/system-prompt-templates/presets/*):只读,返回可直接回填编辑器的原始模板文本
153
+ // (占位符保持字面量,不做变量替换),供「+ 添加模型」时按名称匹配/下拉选择预填。
154
+ function getSystemPromptPresets(req, res, parsedUrl, isLocal, deps) {
155
+ try {
156
+ // Optional UI-language hint for the variables doc. parsedUrl may be null in
157
+ // direct handler invocations (tests); the value is whitelisted downstream.
158
+ const lang = parsedUrl?.searchParams?.get('lang') || undefined;
159
+ const presets = listSystemPromptPresets();
160
+ sendJson(res, 200, {
161
+ presets,
162
+ categories: groupPresetsByCategory(presets),
163
+ variablesDoc: getSystemPromptVariablesDoc(lang),
164
+ });
165
+ } catch (e) {
166
+ console.error('[CC Viewer] expert system-prompt-presets GET failed:', e.message);
167
+ sendJson(res, 500, { error: 'read_failed' });
168
+ }
169
+ }
170
+
151
171
  export const expertRoutes = [
152
172
  { method: 'GET', match: 'exact', path: '/api/expert/system-text', handler: getSystemText },
153
173
  { method: 'POST', match: 'exact', path: '/api/expert/system-text', handler: postSystemText },
154
174
  { method: 'GET', match: 'exact', path: '/api/expert/model-prompts', handler: getModelPrompts },
155
175
  { method: 'POST', match: 'exact', path: '/api/expert/model-prompts', handler: postModelPrompts },
176
+ { method: 'GET', match: 'exact', path: '/api/expert/system-prompt-presets', handler: getSystemPromptPresets },
156
177
  ];