codemini-cli 0.5.10 → 0.5.12

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/OPERATIONS.md +242 -242
  2. package/README.md +588 -588
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-B-G99D0A.js} +1 -1
  4. package/codemini-web/dist/assets/{index-BK75hMb2.js → index-DIGUEzan.js} +108 -108
  5. package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
  6. package/codemini-web/dist/assets/mermaid-GHXKKRXX-va2Kl89u.js +1 -0
  7. package/codemini-web/dist/index.html +35 -23
  8. package/codemini-web/lib/approval-manager.js +32 -32
  9. package/codemini-web/lib/runtime-bridge.js +17 -11
  10. package/codemini-web/server.js +534 -205
  11. package/deployment.md +212 -212
  12. package/package.json +2 -2
  13. package/skills/brainstorm/SKILL.md +77 -77
  14. package/skills/codemini.skills.json +40 -40
  15. package/skills/grill-me/SKILL.md +30 -30
  16. package/skills/superpowers-lite/SKILL.md +82 -82
  17. package/src/cli.js +74 -74
  18. package/src/commands/chat.js +210 -210
  19. package/src/commands/run.js +313 -313
  20. package/src/commands/skill.js +438 -304
  21. package/src/commands/web.js +57 -57
  22. package/src/core/agent-loop.js +980 -980
  23. package/src/core/ast.js +309 -307
  24. package/src/core/chat-runtime.js +6261 -6253
  25. package/src/core/command-evaluator.js +72 -72
  26. package/src/core/command-loader.js +311 -311
  27. package/src/core/command-policy.js +301 -301
  28. package/src/core/command-risk.js +156 -156
  29. package/src/core/config-store.js +286 -285
  30. package/src/core/constants.js +18 -1
  31. package/src/core/context-compact.js +365 -365
  32. package/src/core/default-system-prompt.js +114 -107
  33. package/src/core/dream-audit.js +105 -105
  34. package/src/core/dream-consolidate.js +229 -229
  35. package/src/core/dream-evaluator.js +185 -185
  36. package/src/core/fff-adapter.js +383 -383
  37. package/src/core/memory-store.js +543 -543
  38. package/src/core/project-index.js +737 -548
  39. package/src/core/project-instructions.js +98 -98
  40. package/src/core/provider/anthropic.js +514 -514
  41. package/src/core/provider/openai-compatible.js +501 -501
  42. package/src/core/reflect-skill.js +178 -178
  43. package/src/core/reply-language.js +40 -40
  44. package/src/core/session-store.js +474 -474
  45. package/src/core/shell-profile.js +237 -237
  46. package/src/core/shell.js +323 -323
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -52
  49. package/src/core/tool-args.js +199 -154
  50. package/src/core/tool-output.js +184 -184
  51. package/src/core/tool-result-store.js +206 -206
  52. package/src/core/tools.js +3024 -2893
  53. package/src/core/version.js +11 -11
  54. package/src/tui/chat-app.js +5173 -5171
  55. package/src/tui/tool-activity/presenters/misc.js +30 -30
  56. package/src/tui/tool-activity/presenters/system.js +20 -20
  57. package/templates/project-requirements/report-shell.html +582 -582
  58. package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
  59. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
@@ -9,69 +9,103 @@ import { createChatRuntime } from '../src/core/chat-runtime.js';
9
9
  import { createSession, loadSession, listSessions, resolveSession, deleteSession } from '../src/core/session-store.js';
10
10
  import { buildDefaultSystemPrompt } from '../src/core/default-system-prompt.js';
11
11
  import { RuntimeBridge } from './lib/runtime-bridge.js';
12
- import { listSkillEntries } from '../src/commands/skill.js';
13
- import { readSkillRegistry, writeSkillRegistry } from '../src/core/skill-registry.js';
14
- import { getReplyLanguage } from '../src/core/reply-language.js';
15
- import { getBaseConfigDir, getProjectSkillsDir } from '../src/core/paths.js';
16
- import { VERSION } from '../src/core/version.js';
17
-
18
- const GENERAL_PROJECT_DIR = (() => {
19
- const base = getBaseConfigDir();
20
- return path.join(base, 'workspace');
21
- })();
22
-
23
- const SKILL_CATALOG_FILE = 'codemini.skills.json';
24
- const SKILL_MODES = new Set(['always', 'auto_attach', 'agent_requested', 'manual']);
25
-
26
- function normalizeSkillMetadataPatch(input = {}) {
27
- const out = {};
28
- if (typeof input.description === 'string') out.description = input.description.trim();
29
- if (typeof input.mode === 'string' && SKILL_MODES.has(input.mode)) out.mode = input.mode;
30
- if (input.enabled !== undefined) out.enabled = input.enabled !== false;
31
- if (input.priority !== undefined) {
32
- const priority = Number(input.priority);
33
- if (Number.isFinite(priority)) out.priority = Math.max(0, Math.min(100, Math.round(priority)));
34
- }
35
- if (Array.isArray(input.triggers)) {
36
- out.triggers = input.triggers.map((item) => String(item || '').trim()).filter(Boolean);
37
- } else if (typeof input.triggers === 'string') {
38
- out.triggers = input.triggers.split(',').map((item) => item.trim()).filter(Boolean);
39
- }
40
- return out;
41
- }
42
-
43
- async function readProjectSkillCatalog(projectDir) {
44
- const catalogPath = path.join(getProjectSkillsDir(projectDir), SKILL_CATALOG_FILE);
45
- try {
46
- const parsed = JSON.parse(await fs.readFile(catalogPath, 'utf8'));
47
- return parsed && typeof parsed === 'object' ? parsed : { version: 1, skills: {} };
48
- } catch {
49
- return { version: 1, skills: {} };
50
- }
51
- }
52
-
53
- async function writeProjectSkillCatalog(projectDir, catalog) {
54
- const catalogPath = path.join(getProjectSkillsDir(projectDir), SKILL_CATALOG_FILE);
55
- await fs.mkdir(path.dirname(catalogPath), { recursive: true });
56
- const next = {
57
- version: 1,
58
- skills: catalog?.skills && typeof catalog.skills === 'object' ? catalog.skills : {}
59
- };
60
- await fs.writeFile(catalogPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
61
- }
62
-
63
- async function upsertProjectSkillMetadata(projectDir, name, patch) {
64
- const catalog = await readProjectSkillCatalog(projectDir);
65
- catalog.skills = catalog.skills || {};
66
- const prior = catalog.skills[name] && typeof catalog.skills[name] === 'object' ? catalog.skills[name] : {};
67
- catalog.skills[name] = { ...prior, ...normalizeSkillMetadataPatch(patch) };
68
- await writeProjectSkillCatalog(projectDir, catalog);
69
- return catalog.skills[name];
70
- }
12
+ import { installSkillSource, listSkillEntries } from '../src/commands/skill.js';
13
+ import { computeFileSha256, readSkillRegistry, upsertSkillRegistryEntry, writeSkillRegistry } from '../src/core/skill-registry.js';
14
+ import { getReplyLanguage } from '../src/core/reply-language.js';
15
+ import { getBaseConfigDir, getFileIndexPath, getProjectSkillsDir, getSkillsDir } from '../src/core/paths.js';
16
+ import { initializeProjectIndex } from '../src/core/project-index.js';
17
+ import { INDEX_SKIP_DIRS } from '../src/core/constants.js';
18
+ import { VERSION } from '../src/core/version.js';
19
+
20
+ const GENERAL_PROJECT_DIR = (() => {
21
+ const base = getBaseConfigDir();
22
+ return path.join(base, 'workspace');
23
+ })();
24
+
25
+ const SKILL_CATALOG_FILE = 'codemini.skills.json';
26
+ const SKILL_MODES = new Set(['always', 'auto_attach', 'agent_requested', 'manual']);
27
+ const SKILL_SCOPES = new Set(['project', 'global']);
28
+
29
+ function normalizeSkillScope(scope) {
30
+ return SKILL_SCOPES.has(scope) ? scope : 'project';
31
+ }
32
+
33
+ function isSafeSkillName(name = '') {
34
+ return /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name);
35
+ }
36
+
37
+ function skillBaseDirForScope(scope, projectDir) {
38
+ return scope === 'global' ? getSkillsDir() : getProjectSkillsDir(projectDir);
39
+ }
40
+
41
+ function normalizeSkillMetadataPatch(input = {}) {
42
+ const out = {};
43
+ if (typeof input.description === 'string') out.description = input.description.trim();
44
+ if (typeof input.mode === 'string' && SKILL_MODES.has(input.mode)) out.mode = input.mode;
45
+ if (input.enabled !== undefined) out.enabled = input.enabled !== false;
46
+ if (input.priority !== undefined) {
47
+ const priority = Number(input.priority);
48
+ if (Number.isFinite(priority)) out.priority = Math.max(0, Math.min(100, Math.round(priority)));
49
+ }
50
+ if (Array.isArray(input.triggers)) {
51
+ out.triggers = input.triggers.map((item) => String(item || '').trim()).filter(Boolean);
52
+ } else if (typeof input.triggers === 'string') {
53
+ out.triggers = input.triggers.split(',').map((item) => item.trim()).filter(Boolean);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ async function readProjectSkillCatalog(projectDir) {
59
+ return readSkillCatalogFromDir(getProjectSkillsDir(projectDir));
60
+ }
61
+
62
+ async function readSkillCatalogFromDir(skillBaseDir) {
63
+ const catalogPath = path.join(skillBaseDir, SKILL_CATALOG_FILE);
64
+ try {
65
+ const parsed = JSON.parse(await fs.readFile(catalogPath, 'utf8'));
66
+ return parsed && typeof parsed === 'object' ? parsed : { version: 1, skills: {} };
67
+ } catch {
68
+ return { version: 1, skills: {} };
69
+ }
70
+ }
71
+
72
+ async function writeProjectSkillCatalog(projectDir, catalog) {
73
+ return writeSkillCatalogToDir(getProjectSkillsDir(projectDir), catalog);
74
+ }
75
+
76
+ async function writeSkillCatalogToDir(skillBaseDir, catalog) {
77
+ const catalogPath = path.join(skillBaseDir, SKILL_CATALOG_FILE);
78
+ await fs.mkdir(path.dirname(catalogPath), { recursive: true });
79
+ const next = {
80
+ version: 1,
81
+ skills: catalog?.skills && typeof catalog.skills === 'object' ? catalog.skills : {}
82
+ };
83
+ await fs.writeFile(catalogPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
84
+ }
85
+
86
+ async function upsertProjectSkillMetadata(projectDir, name, patch) {
87
+ return upsertSkillCatalogMetadata(getProjectSkillsDir(projectDir), name, patch);
88
+ }
71
89
 
72
- async function listProjectRoots() {
73
- if (process.platform === 'win32') {
74
- const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
90
+ async function upsertSkillCatalogMetadata(skillBaseDir, name, patch) {
91
+ const catalog = await readSkillCatalogFromDir(skillBaseDir);
92
+ catalog.skills = catalog.skills || {};
93
+ const prior = catalog.skills[name] && typeof catalog.skills[name] === 'object' ? catalog.skills[name] : {};
94
+ catalog.skills[name] = { ...prior, ...normalizeSkillMetadataPatch(patch) };
95
+ await writeSkillCatalogToDir(skillBaseDir, catalog);
96
+ return catalog.skills[name];
97
+ }
98
+
99
+ async function deleteSkillCatalogMetadata(skillBaseDir, name) {
100
+ const catalog = await readSkillCatalogFromDir(skillBaseDir);
101
+ if (!catalog.skills?.[name]) return;
102
+ delete catalog.skills[name];
103
+ await writeSkillCatalogToDir(skillBaseDir, catalog);
104
+ }
105
+
106
+ async function listProjectRoots() {
107
+ if (process.platform === 'win32') {
108
+ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
75
109
  const roots = [];
76
110
  await Promise.all(letters.map(async (letter) => {
77
111
  const drivePath = `${letter}:\\`;
@@ -100,15 +134,25 @@ async function listProjectRoots() {
100
134
  seen.add(resolved);
101
135
  roots.push({ name: candidate.name, path: resolved, isGit: false, isDrive: false });
102
136
  } catch {}
103
- }
104
- return roots;
105
- }
137
+ }
138
+ return roots;
139
+ }
106
140
 
107
141
  function isGeneralProjectDir(value) {
108
142
  if (!value) return false;
109
143
  return path.resolve(value) === path.resolve(GENERAL_PROJECT_DIR);
110
144
  }
111
145
 
146
+ function getGeneralChatSystemPromptBlock() {
147
+ return `# General Chat Mode
148
+
149
+ This is a general conversation, not an opened project workspace.
150
+ - The working directory is Codemini's internal general workspace. Do not treat it as a user project.
151
+ - Use filesystem read, write, and edit tools only as auxiliary scratch or artifact tools when the user explicitly needs local files.
152
+ - When the user asks to rewrite or transform remote content, fetch or read the content and answer with the rewritten text unless they explicitly ask you to create or modify a local file.
153
+ - Before making persistent filesystem changes in this mode, make sure the user requested a local artifact and use an obvious user-facing path or file name.`;
154
+ }
155
+
112
156
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
113
157
  const CLIENT_SOURCE_DIR = path.join(__dirname, 'client');
114
158
  let CLIENT_DIR = CLIENT_SOURCE_DIR;
@@ -221,19 +265,29 @@ async function serveStatic(res, filePath) {
221
265
  }
222
266
  }
223
267
 
224
- function normalizeProjectPath(value) {
225
- const raw = String(value || '').trim();
226
- if (!raw) return '';
227
- const win = raw.match(/^([A-Za-z]):[\\/](.*)$/);
268
+ function normalizeProjectPath(value) {
269
+ const raw = String(value || '').trim();
270
+ if (!raw) return '';
271
+ const win = raw.match(/^([A-Za-z]):[\\/](.*)$/);
228
272
  if (win && process.platform !== 'win32') {
229
273
  return path.join('/mnt', win[1].toLowerCase(), win[2].replace(/[\\/]+/g, '/'));
230
274
  }
231
- return path.resolve(raw);
232
- }
233
-
234
- function tryParseJson(value) {
235
- try { return JSON.parse(String(value || '')); } catch { return null; }
236
- }
275
+ return path.resolve(raw);
276
+ }
277
+
278
+ async function resolveCodeWikiProjectDir(url, fallbackDir) {
279
+ const requested = normalizeProjectPath(url.searchParams.get('project') || '');
280
+ if (!requested) return fallbackDir;
281
+ try {
282
+ const stat = await fs.stat(requested);
283
+ if (stat.isDirectory()) return requested;
284
+ } catch {}
285
+ return fallbackDir;
286
+ }
287
+
288
+ function tryParseJson(value) {
289
+ try { return JSON.parse(String(value || '')); } catch { return null; }
290
+ }
237
291
 
238
292
  function collectSessionPathHints(session) {
239
293
  const hints = [];
@@ -254,14 +308,14 @@ function collectSessionPathHints(session) {
254
308
  return hints;
255
309
  }
256
310
 
257
- async function existingDirectoryForHint(rawHint) {
258
- let candidate = normalizeProjectPath(rawHint);
259
- if (!candidate) return '';
260
- const configRoot = path.resolve(getBaseConfigDir());
261
- const candidateLower = path.resolve(candidate).toLowerCase();
262
- const configRootLower = configRoot.toLowerCase();
263
- if (candidateLower === configRootLower || candidateLower.startsWith(`${configRootLower}${path.sep}`)) return '';
264
- candidate = candidate.replace(/[),\].。;;:]+$/g, '');
311
+ async function existingDirectoryForHint(rawHint) {
312
+ let candidate = normalizeProjectPath(rawHint);
313
+ if (!candidate) return '';
314
+ const configRoot = path.resolve(getBaseConfigDir());
315
+ const candidateLower = path.resolve(candidate).toLowerCase();
316
+ const configRootLower = configRoot.toLowerCase();
317
+ if (candidateLower === configRootLower || candidateLower.startsWith(`${configRootLower}${path.sep}`)) return '';
318
+ candidate = candidate.replace(/[),\].。;;:]+$/g, '');
265
319
  for (let i = 0; i < 8 && candidate && candidate !== path.dirname(candidate); i += 1) {
266
320
  try {
267
321
  const stat = await fs.stat(candidate);
@@ -283,11 +337,164 @@ function isCodeWikiReportFile(fileName) {
283
337
  return CODEWIKI_REPORT_RE.test(String(fileName || ''));
284
338
  }
285
339
 
286
- function codeWikiReportTitle(fileName) {
287
- return String(fileName || '')
288
- .replace(/-project-requirements\.html$/, '')
289
- .replace(/-/g, ' ');
290
- }
340
+ function codeWikiReportTitle(fileName) {
341
+ return String(fileName || '')
342
+ .replace(/-project-requirements\.html$/, '')
343
+ .replace(/-/g, ' ');
344
+ }
345
+
346
+ function clipGraphList(values, max = 12) {
347
+ return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))].slice(0, max);
348
+ }
349
+
350
+ const CODEWIKI_GRAPH_NOISY_NAMES = new Set([
351
+ '__init__',
352
+ '__enter__',
353
+ '__exit__',
354
+ '__getitem__',
355
+ '__setitem__',
356
+ '__delitem__',
357
+ '__contains__',
358
+ '__len__',
359
+ '__iter__',
360
+ '__next__',
361
+ '__call__',
362
+ 'get',
363
+ 'set',
364
+ 'add',
365
+ 'run',
366
+ 'close',
367
+ 'open',
368
+ 'read',
369
+ 'write',
370
+ 'send',
371
+ 'recv',
372
+ 'poll',
373
+ 'update',
374
+ 'copy',
375
+ 'size',
376
+ 'apply'
377
+ ]);
378
+
379
+ function normalizeGraphPath(value = '') {
380
+ return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
381
+ }
382
+
383
+ function isDependencyLikeGraphPath(file = '') {
384
+ const normalized = normalizeGraphPath(file);
385
+ const segments = normalized.split('/').filter(Boolean);
386
+ return segments.some(
387
+ (segment) =>
388
+ INDEX_SKIP_DIRS.has(segment) ||
389
+ /^venv[-_]/i.test(segment) ||
390
+ /\.egg-info$/i.test(segment) ||
391
+ /^python\d+(?:\.\d+)?$/i.test(segment)
392
+ );
393
+ }
394
+
395
+ function isNoisyGraphSymbol(symbol = {}) {
396
+ const name = String(symbol.name || symbol.symbol_id || '').split('.').pop();
397
+ if (!name) return true;
398
+ if (CODEWIKI_GRAPH_NOISY_NAMES.has(name)) return true;
399
+ return /^__.*__$/.test(name);
400
+ }
401
+
402
+ function sourceRootScore(file = '') {
403
+ const normalized = normalizeGraphPath(file);
404
+ if (normalized.startsWith('src/')) return 8;
405
+ if (normalized.startsWith('codemini-web/client/src/')) return 8;
406
+ if (normalized.startsWith('codemini-web/server.js')) return 7;
407
+ if (normalized.startsWith('codemini-web/')) return 5;
408
+ if (normalized.startsWith('tests/')) return 1;
409
+ return 3;
410
+ }
411
+
412
+ function buildCodeWikiSymbolGraph(fileIndex, { maxNodes = 42 } = {}) {
413
+ const files = Array.isArray(fileIndex?.files) ? fileIndex.files : [];
414
+ const sourceFiles = files.filter((entry) => !isDependencyLikeGraphPath(entry.file));
415
+ const symbols = sourceFiles
416
+ .flatMap((entry) =>
417
+ (Array.isArray(entry.symbols) ? entry.symbols : []).map((symbol) => ({
418
+ ...symbol,
419
+ file: symbol.file || entry.file
420
+ }))
421
+ )
422
+ .filter((symbol) => !isDependencyLikeGraphPath(symbol.file) && !isNoisyGraphSymbol(symbol));
423
+ const ranked = symbols
424
+ .map((symbol) => {
425
+ const calls = Array.isArray(symbol.calls) ? symbol.calls.length : 0;
426
+ const calledBy = Array.isArray(symbol.called_by) ? symbol.called_by.length : 0;
427
+ const writes = Array.isArray(symbol.writes) ? symbol.writes.length : 0;
428
+ const emits = Array.isArray(symbol.emits) ? symbol.emits.length : 0;
429
+ const typeBoost = symbol.type === 'class' ? 8 : symbol.type === 'method' ? 4 : 2;
430
+ return {
431
+ symbol,
432
+ score: sourceRootScore(symbol.file) + typeBoost + calledBy * 4 + calls * 2 + writes * 2 + emits * 2
433
+ };
434
+ })
435
+ .sort((a, b) => b.score - a.score || String(a.symbol.symbol_id).localeCompare(String(b.symbol.symbol_id)))
436
+ .slice(0, maxNodes)
437
+ .map((item) => item.symbol);
438
+
439
+ const byId = new Map(ranked.map((symbol) => [String(symbol.symbol_id || ''), symbol]));
440
+ const byShortName = new Map();
441
+ for (const symbol of ranked) {
442
+ const shortName = String(symbol.name || '').split('.').pop();
443
+ if (!shortName) continue;
444
+ if (!byShortName.has(shortName)) byShortName.set(shortName, []);
445
+ byShortName.get(shortName).push(symbol);
446
+ }
447
+
448
+ const nodes = ranked.map((symbol) => ({
449
+ id: symbol.symbol_id,
450
+ label: symbol.name || symbol.symbol_id,
451
+ type: symbol.type || 'symbol',
452
+ file: symbol.file || '',
453
+ range: symbol.range || null,
454
+ signature: symbol.signature || '',
455
+ calls: clipGraphList(symbol.calls || [], 8),
456
+ called_by: clipGraphList(symbol.called_by || [], 8),
457
+ imports: clipGraphList(symbol.imports || [], 6),
458
+ writes: clipGraphList(symbol.writes || [], 6),
459
+ emits: clipGraphList(symbol.emits || [], 6)
460
+ }));
461
+
462
+ const edgeMap = new Map();
463
+ const addEdge = (source, target, kind, label = '') => {
464
+ if (!source || !target || source === target) return;
465
+ if (!byId.has(source) || !byId.has(target)) return;
466
+ const key = `${source}->${target}:${kind}`;
467
+ if (!edgeMap.has(key)) edgeMap.set(key, { source, target, kind, label });
468
+ };
469
+
470
+ for (const symbol of ranked) {
471
+ const source = String(symbol.symbol_id || '');
472
+ for (const call of symbol.calls || []) {
473
+ const shortName = String(call || '').split('.').pop();
474
+ for (const target of byShortName.get(shortName) || []) {
475
+ addEdge(source, target.symbol_id, 'calls', call);
476
+ }
477
+ }
478
+ for (const caller of symbol.called_by || []) {
479
+ addEdge(caller, source, 'called_by');
480
+ }
481
+ }
482
+
483
+ const edges = [...edgeMap.values()].slice(0, 80);
484
+
485
+ return {
486
+ updatedAt: fileIndex?.updatedAt || '',
487
+ stats: {
488
+ files: files.length,
489
+ source_files: sourceFiles.length,
490
+ symbols: symbols.length,
491
+ displayed_nodes: nodes.length,
492
+ displayed_edges: edges.length
493
+ },
494
+ nodes,
495
+ edges
496
+ };
497
+ }
291
498
 
292
499
  function commonPathPrefix(paths) {
293
500
  const normalized = paths.map((p) => path.resolve(p).split(path.sep).filter(Boolean));
@@ -341,14 +548,17 @@ async function buildRuntimeForSession({ sessionId, model, projectDir }) {
341
548
  } catch {}
342
549
  }
343
550
  session.projectDir = process.cwd();
344
- const systemPrompt = buildDefaultSystemPrompt(config);
551
+ const isGeneral = isGeneralProjectDir(process.cwd());
552
+ const systemPrompt = buildDefaultSystemPrompt(config, {
553
+ extraPrompts: isGeneral ? [getGeneralChatSystemPromptBlock()] : []
554
+ });
345
555
  const runtime = await createChatRuntime({
346
556
  session,
347
557
  config,
348
558
  model: model || config.model?.name,
349
559
  systemPrompt
350
560
  });
351
- return { runtime, config, session, cwd: process.cwd(), isGeneral: isGeneralProjectDir(process.cwd()) };
561
+ return { runtime, config, session, cwd: process.cwd(), isGeneral };
352
562
  }
353
563
 
354
564
  async function main() {
@@ -498,9 +708,10 @@ async function main() {
498
708
  return;
499
709
  }
500
710
 
501
- // ── CodeWiki / project requirements reports ──
502
- if (req.method === 'GET' && url.pathname === '/api/codewiki/reports') {
503
- const requirementsDir = getRequirementsDir(currentProjectDir);
711
+ // ── CodeWiki / project requirements reports ──
712
+ if (req.method === 'GET' && url.pathname === '/api/codewiki/reports') {
713
+ const codeWikiProjectDir = await resolveCodeWikiProjectDir(url, currentProjectDir);
714
+ const requirementsDir = getRequirementsDir(codeWikiProjectDir);
504
715
  try {
505
716
  const entries = await fs.readdir(requirementsDir, { withFileTypes: true });
506
717
  const reports = [];
@@ -521,16 +732,38 @@ async function main() {
521
732
  if (err?.code === 'ENOENT') jsonResponse(res, { reports: [] });
522
733
  else jsonResponse(res, { error: true, message: err.message }, 500);
523
734
  }
524
- return;
525
- }
526
-
527
- if (req.method === 'GET' && url.pathname.startsWith('/api/codewiki/report/')) {
735
+ return;
736
+ }
737
+
738
+ if (req.method === 'GET' && url.pathname === '/api/codewiki/symbol-graph') {
739
+ try {
740
+ const codeWikiProjectDir = await resolveCodeWikiProjectDir(url, currentProjectDir);
741
+ const initialized = await initializeProjectIndex(codeWikiProjectDir);
742
+ const projectRoot = initialized?.projectRoot || codeWikiProjectDir;
743
+ const fileIndexPath = getFileIndexPath(projectRoot);
744
+ const fileIndex = JSON.parse(await fs.readFile(fileIndexPath, 'utf8'));
745
+ const maxNodes = Math.max(12, Math.min(80, Number(url.searchParams.get('max_nodes') || 42)));
746
+ jsonResponse(res, buildCodeWikiSymbolGraph(fileIndex, { maxNodes }));
747
+ } catch (err) {
748
+ jsonResponse(res, {
749
+ updatedAt: '',
750
+ stats: { files: 0, symbols: 0, displayed_nodes: 0, displayed_edges: 0 },
751
+ nodes: [],
752
+ edges: [],
753
+ error: err?.message || String(err)
754
+ });
755
+ }
756
+ return;
757
+ }
758
+
759
+ if (req.method === 'GET' && url.pathname.startsWith('/api/codewiki/report/')) {
528
760
  const fileName = decodeURIComponent(url.pathname.slice('/api/codewiki/report/'.length));
529
761
  if (!isCodeWikiReportFile(fileName)) {
530
762
  jsonResponse(res, { error: true, message: 'Invalid report file' }, 400);
531
763
  return;
532
764
  }
533
- const requirementsDir = path.resolve(getRequirementsDir(currentProjectDir));
765
+ const codeWikiProjectDir = await resolveCodeWikiProjectDir(url, currentProjectDir);
766
+ const requirementsDir = path.resolve(getRequirementsDir(codeWikiProjectDir));
534
767
  const reportPath = path.resolve(requirementsDir, fileName);
535
768
  if (!reportPath.startsWith(`${requirementsDir}${path.sep}`)) {
536
769
  jsonResponse(res, { error: true, message: 'Invalid report path' }, 403);
@@ -546,7 +779,8 @@ async function main() {
546
779
  jsonResponse(res, { error: true, message: 'Invalid report file' }, 400);
547
780
  return;
548
781
  }
549
- const requirementsDir = path.resolve(getRequirementsDir(currentProjectDir));
782
+ const codeWikiProjectDir = await resolveCodeWikiProjectDir(url, currentProjectDir);
783
+ const requirementsDir = path.resolve(getRequirementsDir(codeWikiProjectDir));
550
784
  const reportPath = path.resolve(requirementsDir, fileName);
551
785
  if (!reportPath.startsWith(`${requirementsDir}${path.sep}`)) {
552
786
  jsonResponse(res, { error: true, message: 'Invalid report path' }, 403);
@@ -567,11 +801,20 @@ async function main() {
567
801
  jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
568
802
  return;
569
803
  }
570
- const { depth } = await readBody(req);
571
- const normalizedDepth = ['fast', 'standard', 'deep'].includes(String(depth || '').toLowerCase())
572
- ? String(depth).toLowerCase()
573
- : 'standard';
574
- const result = bridge.handleSubmit(`/project-requirements --${normalizedDepth}`);
804
+ const { depth } = await readBody(req);
805
+ const normalizedDepth = ['fast', 'standard', 'deep'].includes(String(depth || '').toLowerCase())
806
+ ? String(depth).toLowerCase()
807
+ : 'standard';
808
+ const codeWikiProjectDir = await resolveCodeWikiProjectDir(url, currentProjectDir);
809
+ if (codeWikiProjectDir !== currentProjectDir) {
810
+ const { runtime } = await buildRuntimeForSession({
811
+ model: bridge.getState().model,
812
+ projectDir: codeWikiProjectDir
813
+ });
814
+ await bridge.switchRuntime(runtime);
815
+ currentProjectDir = process.cwd();
816
+ }
817
+ const result = bridge.handleSubmit(`/project-requirements --${normalizedDepth}`);
575
818
  jsonResponse(res, result);
576
819
  return;
577
820
  }
@@ -598,15 +841,16 @@ async function main() {
598
841
  jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
599
842
  return;
600
843
  }
601
- const reportPath = selectedReport
602
- ? path.join(getRequirementsDir(currentProjectDir), selectedReport)
603
- : getRequirementsDir(currentProjectDir);
604
- const prompt = buildCodeWikiAskPrompt({
605
- question,
606
- reportPath,
607
- projectDir: currentProjectDir,
608
- replyLanguage: bridge.getState()?.replyLanguage
609
- });
844
+ const codeWikiProjectDir = await resolveCodeWikiProjectDir(url, currentProjectDir);
845
+ const reportPath = selectedReport
846
+ ? path.join(getRequirementsDir(codeWikiProjectDir), selectedReport)
847
+ : getRequirementsDir(codeWikiProjectDir);
848
+ const prompt = buildCodeWikiAskPrompt({
849
+ question,
850
+ reportPath,
851
+ projectDir: codeWikiProjectDir,
852
+ replyLanguage: bridge.getState()?.replyLanguage
853
+ });
610
854
 
611
855
  res.writeHead(200, {
612
856
  'Content-Type': 'application/x-ndjson; charset=utf-8',
@@ -816,8 +1060,8 @@ async function main() {
816
1060
  }
817
1061
  if (req.method === 'POST' && url.pathname === '/api/project/browse') {
818
1062
  const { dir } = await readBody(req);
819
- const roots = await listProjectRoots();
820
- if (!dir && roots.length) {
1063
+ const roots = await listProjectRoots();
1064
+ if (!dir && roots.length) {
821
1065
  jsonResponse(res, { path: '', roots, dirs: [] });
822
1066
  return;
823
1067
  }
@@ -860,10 +1104,10 @@ async function main() {
860
1104
  try {
861
1105
  await setConfigValue(key, value);
862
1106
  const config = await loadConfig();
863
- await bridge.reloadConfig(
864
- key === 'model.name' ? { model: config.model?.name } : {}
865
- );
866
- bridge.broadcastRuntimeState();
1107
+ await bridge.reloadConfig(
1108
+ key === 'model.name' ? { model: config.model?.name } : {}
1109
+ );
1110
+ bridge.broadcastRuntimeState();
867
1111
  jsonResponse(res, { ok: true, config });
868
1112
  } catch (err) {
869
1113
  jsonResponse(res, { error: true, message: err.message }, 500);
@@ -878,58 +1122,87 @@ async function main() {
878
1122
  }
879
1123
 
880
1124
  // ── Skills management ──
881
- if (req.method === 'GET' && url.pathname === '/api/skills') {
882
- try {
883
- const skills = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
884
- jsonResponse(res, skills);
885
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
886
- return;
1125
+ if (req.method === 'GET' && url.pathname === '/api/skills') {
1126
+ try {
1127
+ const skills = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
1128
+ jsonResponse(res, skills);
1129
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1130
+ return;
887
1131
  }
888
1132
  if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
889
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
890
- try {
891
- const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
892
- const skill = entries.find(s => s.name === name);
893
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
894
- const content = await fs.readFile(skill.path, 'utf8');
1133
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1134
+ try {
1135
+ const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
1136
+ const skill = entries.find(s => s.name === name);
1137
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1138
+ const content = await fs.readFile(skill.path, 'utf8');
895
1139
  jsonResponse(res, { name: skill.name, content, scope: skill.scope });
896
1140
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
897
1141
  return;
898
1142
  }
899
- if (req.method === 'POST' && url.pathname === '/api/skills/create') {
900
- const { name, description, content } = await readBody(req);
901
- if (!name || !content) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
902
- try {
903
- const skillDir = path.join(getProjectSkillsDir(currentProjectDir), name);
904
- await fs.mkdir(skillDir, { recursive: true });
905
- const skillFile = path.join(skillDir, 'SKILL.md');
906
- await fs.writeFile(skillFile, content, 'utf8');
907
- await upsertProjectSkillMetadata(currentProjectDir, name, {
908
- description: description || '',
909
- mode: 'agent_requested',
910
- triggers: [],
911
- enabled: true,
912
- priority: 50
913
- });
914
- const config = await loadConfig();
915
- config.skills = config.skills || {};
916
- config.skills.enabled = config.skills.enabled || {};
1143
+ if (req.method === 'POST' && url.pathname === '/api/skills/create') {
1144
+ const { name, description, content, scope: rawScope } = await readBody(req);
1145
+ if (!name || !content) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
1146
+ if (!isSafeSkillName(name)) { jsonResponse(res, { error: true, message: 'Invalid skill name' }, 400); return; }
1147
+ try {
1148
+ const scope = normalizeSkillScope(rawScope);
1149
+ const skillBaseDir = skillBaseDirForScope(scope, currentProjectDir);
1150
+ const skillDir = path.join(skillBaseDir, name);
1151
+ await fs.mkdir(skillDir, { recursive: true });
1152
+ const skillFile = path.join(skillDir, 'SKILL.md');
1153
+ await fs.writeFile(skillFile, content, 'utf8');
1154
+ if (scope === 'global') {
1155
+ await upsertSkillRegistryEntry(undefined, {
1156
+ name,
1157
+ version: '0.0.0',
1158
+ description: description || '',
1159
+ enabled: true,
1160
+ source: 'web-create',
1161
+ entryFile: 'SKILL.md',
1162
+ sha256: await computeFileSha256(skillFile),
1163
+ installedAt: new Date().toISOString()
1164
+ });
1165
+ } else {
1166
+ await upsertProjectSkillMetadata(currentProjectDir, name, {
1167
+ description: description || '',
1168
+ mode: 'agent_requested',
1169
+ triggers: [],
1170
+ enabled: true,
1171
+ priority: 50
1172
+ });
1173
+ }
1174
+ const config = await loadConfig();
1175
+ config.skills = config.skills || {};
1176
+ config.skills.enabled = config.skills.enabled || {};
917
1177
  config.skills.enabled[name] = true;
918
1178
  await saveConfig(config);
919
- jsonResponse(res, { ok: true, name });
1179
+ await bridge.reloadCommandsAndSkills();
1180
+ jsonResponse(res, { ok: true, name, scope });
920
1181
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
921
1182
  return;
922
1183
  }
923
- if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
924
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
925
- const { content } = await readBody(req);
926
- if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
927
- try {
928
- const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
1184
+ if (req.method === 'POST' && url.pathname === '/api/skills/install') {
1185
+ const { source, scope: rawScope } = await readBody(req);
1186
+ if (!source) { jsonResponse(res, { error: true, message: 'Missing source' }, 400); return; }
1187
+ try {
1188
+ const scope = normalizeSkillScope(rawScope);
1189
+ const installed = await installSkillSource(source, { scope, cwd: currentProjectDir });
1190
+ await bridge.reloadCommandsAndSkills();
1191
+ jsonResponse(res, { ok: true, installed, scope });
1192
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1193
+ return;
1194
+ }
1195
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
1196
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1197
+ const { content } = await readBody(req);
1198
+ if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
1199
+ try {
1200
+ const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
929
1201
  const skill = entries.find(s => s.name === name);
930
1202
  if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
931
1203
  if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
932
1204
  await fs.writeFile(skill.path, content, 'utf8');
1205
+ await bridge.reloadCommandsAndSkills();
933
1206
  jsonResponse(res, { ok: true });
934
1207
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
935
1208
  return;
@@ -937,62 +1210,117 @@ async function main() {
937
1210
  if (req.method === 'DELETE' && url.pathname.startsWith('/api/skills/')) {
938
1211
  const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length));
939
1212
  try {
940
- const entries = await listSkillEntries({ scope: 'all' });
1213
+ const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
941
1214
  const skill = entries.find(s => s.name === name);
942
1215
  if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
943
1216
  if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
944
1217
  const dir = path.dirname(skill.path);
945
1218
  await fs.rm(dir, { recursive: true, force: true });
946
- const registry = await readSkillRegistry();
947
- registry.skills = (registry.skills || []).filter(s => s.name !== name);
948
- await writeSkillRegistry(undefined, registry);
949
- const catalog = await readProjectSkillCatalog(currentProjectDir);
950
- if (catalog.skills?.[name]) {
951
- delete catalog.skills[name];
952
- await writeProjectSkillCatalog(currentProjectDir, catalog);
953
- }
954
- const config = await loadConfig();
955
- if (config.skills?.enabled) delete config.skills.enabled[name];
956
- await saveConfig(config);
1219
+ const registry = await readSkillRegistry();
1220
+ registry.skills = (registry.skills || []).filter(s => s.name !== name);
1221
+ await writeSkillRegistry(undefined, registry);
1222
+ const catalog = await readProjectSkillCatalog(currentProjectDir);
1223
+ if (catalog.skills?.[name]) {
1224
+ delete catalog.skills[name];
1225
+ await writeProjectSkillCatalog(currentProjectDir, catalog);
1226
+ }
1227
+ await deleteSkillCatalogMetadata(getSkillsDir(), name);
1228
+ const config = await loadConfig();
1229
+ if (config.skills?.enabled) delete config.skills.enabled[name];
1230
+ await saveConfig(config);
1231
+ await bridge.reloadCommandsAndSkills();
957
1232
  jsonResponse(res, { ok: true });
958
1233
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
959
- return;
960
- }
961
- if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/metadata')) {
962
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/metadata'.length));
963
- const body = await readBody(req);
964
- try {
965
- const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
966
- const skill = entries.find(s => s.name === name);
967
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
968
- const metadata = await upsertProjectSkillMetadata(currentProjectDir, name, body || {});
969
- if (skill.scope !== 'builtin' && body?.enabled !== undefined) {
970
- const config = await loadConfig();
971
- config.skills = config.skills || {};
972
- config.skills.enabled = config.skills.enabled || {};
973
- config.skills.enabled[name] = body.enabled !== false;
974
- await saveConfig(config);
975
- const registry = await readSkillRegistry();
976
- const idx = registry.skills.findIndex(s => s.name === name);
977
- if (idx !== -1) { registry.skills[idx].enabled = body.enabled !== false; await writeSkillRegistry(undefined, registry); }
978
- }
979
- jsonResponse(res, { ok: true, name, metadata });
980
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
981
- return;
982
- }
983
- if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
984
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
985
- const { enabled } = await readBody(req);
986
- try {
987
- const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
988
- const skill = entries.find(s => s.name === name);
989
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
990
- if (skill.scope === 'builtin') {
991
- const metadata = await upsertProjectSkillMetadata(currentProjectDir, name, { enabled });
992
- jsonResponse(res, { ok: true, name, metadata });
993
- return;
994
- }
995
- const config = await loadConfig();
1234
+ return;
1235
+ }
1236
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/metadata')) {
1237
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/metadata'.length));
1238
+ const body = await readBody(req);
1239
+ try {
1240
+ const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
1241
+ const skill = entries.find(s => s.name === name);
1242
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1243
+ if (skill.scope === 'builtin' && body?.scope && body.scope !== 'builtin') {
1244
+ jsonResponse(res, { error: true, message: 'Cannot move builtin skill' }, 403);
1245
+ return;
1246
+ }
1247
+ const metadataPatch = normalizeSkillMetadataPatch(body || {});
1248
+ let metadata = metadataPatch;
1249
+ const requestedScope = body?.scope ? normalizeSkillScope(body.scope) : skill.scope;
1250
+ let nextScope = skill.scope;
1251
+
1252
+ if (skill.scope !== 'builtin' && requestedScope !== skill.scope) {
1253
+ const sourceDir = path.dirname(skill.path);
1254
+ const targetBaseDir = skillBaseDirForScope(requestedScope, currentProjectDir);
1255
+ const targetDir = path.join(targetBaseDir, name);
1256
+ await fs.rm(targetDir, { recursive: true, force: true });
1257
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
1258
+ await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
1259
+ await fs.rm(sourceDir, { recursive: true, force: true });
1260
+ if (requestedScope === 'global') {
1261
+ await deleteSkillCatalogMetadata(getProjectSkillsDir(currentProjectDir), name);
1262
+ await upsertSkillRegistryEntry(undefined, {
1263
+ name,
1264
+ version: skill.version || '0.0.0',
1265
+ description: metadataPatch.description ?? skill.description ?? '',
1266
+ enabled: metadataPatch.enabled !== undefined ? metadataPatch.enabled : skill.enabled !== false,
1267
+ source: 'web-move',
1268
+ entryFile: 'SKILL.md',
1269
+ sha256: await computeFileSha256(path.join(targetDir, 'SKILL.md')),
1270
+ installedAt: new Date().toISOString()
1271
+ });
1272
+ } else {
1273
+ const registry = await readSkillRegistry();
1274
+ registry.skills = (registry.skills || []).filter(s => s.name !== name);
1275
+ await writeSkillRegistry(undefined, registry);
1276
+ await deleteSkillCatalogMetadata(getSkillsDir(), name);
1277
+ }
1278
+ nextScope = requestedScope;
1279
+ }
1280
+
1281
+ if (nextScope === 'global') {
1282
+ await upsertSkillRegistryEntry(undefined, {
1283
+ name,
1284
+ ...(metadataPatch.description !== undefined ? { description: metadataPatch.description } : {}),
1285
+ ...(metadataPatch.enabled !== undefined ? { enabled: metadataPatch.enabled } : {})
1286
+ });
1287
+ metadata = await upsertSkillCatalogMetadata(getSkillsDir(), name, body || {});
1288
+ } else if (nextScope === 'project') {
1289
+ metadata = await upsertProjectSkillMetadata(currentProjectDir, name, body || {});
1290
+ } else if (skill.scope !== 'builtin') {
1291
+ metadata = await upsertProjectSkillMetadata(currentProjectDir, name, body || {});
1292
+ } else {
1293
+ metadata = await upsertProjectSkillMetadata(currentProjectDir, name, body || {});
1294
+ }
1295
+ if (skill.scope !== 'builtin' && body?.enabled !== undefined) {
1296
+ const config = await loadConfig();
1297
+ config.skills = config.skills || {};
1298
+ config.skills.enabled = config.skills.enabled || {};
1299
+ config.skills.enabled[name] = body.enabled !== false;
1300
+ await saveConfig(config);
1301
+ const registry = await readSkillRegistry();
1302
+ const idx = registry.skills.findIndex(s => s.name === name);
1303
+ if (idx !== -1) { registry.skills[idx].enabled = body.enabled !== false; await writeSkillRegistry(undefined, registry); }
1304
+ }
1305
+ await bridge.reloadCommandsAndSkills();
1306
+ jsonResponse(res, { ok: true, name, metadata });
1307
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1308
+ return;
1309
+ }
1310
+ if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
1311
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
1312
+ const { enabled } = await readBody(req);
1313
+ try {
1314
+ const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
1315
+ const skill = entries.find(s => s.name === name);
1316
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1317
+ if (skill.scope === 'builtin') {
1318
+ const metadata = await upsertProjectSkillMetadata(currentProjectDir, name, { enabled });
1319
+ await bridge.reloadCommandsAndSkills();
1320
+ jsonResponse(res, { ok: true, name, metadata });
1321
+ return;
1322
+ }
1323
+ const config = await loadConfig();
996
1324
  config.skills = config.skills || {};
997
1325
  config.skills.enabled = config.skills.enabled || {};
998
1326
  config.skills.enabled[name] = !!enabled;
@@ -1000,6 +1328,7 @@ async function main() {
1000
1328
  const registry = await readSkillRegistry();
1001
1329
  const idx = registry.skills.findIndex(s => s.name === name);
1002
1330
  if (idx !== -1) { registry.skills[idx].enabled = !!enabled; await writeSkillRegistry(undefined, registry); }
1331
+ await bridge.reloadCommandsAndSkills();
1003
1332
  jsonResponse(res, { ok: true });
1004
1333
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1005
1334
  return;