codemini-cli 0.6.3 → 0.6.5

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 (49) hide show
  1. package/codemini-web/dist/assets/{AboutDialog-jgqGjQgl.js → AboutDialog-BUp8EzDg.js} +2 -2
  2. package/codemini-web/dist/assets/CodeWikiPanel-Fp0VKdzo.js +1 -0
  3. package/codemini-web/dist/assets/ConfigDialog-DIpj779O.js +1 -0
  4. package/codemini-web/dist/assets/GitDiffDialog-ZLEuX8Qm.js +3 -0
  5. package/codemini-web/dist/assets/{MemoryDialog-BhxQgG0I.js → MemoryDialog-D2YbENVd.js} +3 -3
  6. package/codemini-web/dist/assets/MessageBubble-BIgpZsLn.js +12 -0
  7. package/codemini-web/dist/assets/PatchDiff-CvKNaHsw.js +230 -0
  8. package/codemini-web/dist/assets/ProjectSelector-DXIep3lE.js +1 -0
  9. package/codemini-web/dist/assets/{SkillDialog-DxS43NpR.js → SkillDialog-DjPF-XBx.js} +4 -4
  10. package/codemini-web/dist/assets/SoulDialog-BfIoKETs.js +1 -0
  11. package/codemini-web/dist/assets/chevron-right-CfNZHlyU.js +1 -0
  12. package/codemini-web/dist/assets/{chunk-BO2N2NFS-Budy_hfO.js → chunk-BO2N2NFS-DMUdjM9q.js} +6 -6
  13. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CQS1PAvD.js → highlighted-body-OFNGDK62-8ch0jz7Z.js} +1 -1
  14. package/codemini-web/dist/assets/index-BhMtCC8_.js +65 -0
  15. package/codemini-web/dist/assets/index-DRXwJ-n_.css +2 -0
  16. package/codemini-web/dist/assets/input-CYpdNDlR.js +1 -0
  17. package/codemini-web/dist/assets/lib-BXWizt13.js +1 -0
  18. package/codemini-web/dist/assets/mermaid-GHXKKRXX-KBEtMEB9.js +1 -0
  19. package/codemini-web/dist/assets/{pencil-Ce_LFiEh.js → pencil-BdA2cEeE.js} +1 -1
  20. package/codemini-web/dist/assets/{refresh-cw-BKL-AZu5.js → refresh-cw-CJGgUGiS.js} +1 -1
  21. package/codemini-web/dist/assets/select-BLOccU1M.js +1 -0
  22. package/codemini-web/dist/assets/{trash-2-KmAlCwXd.js → trash-2-CQzNOch5.js} +1 -1
  23. package/codemini-web/dist/index.html +2 -2
  24. package/codemini-web/lib/runtime-bridge.js +332 -296
  25. package/codemini-web/server.js +319 -243
  26. package/package.json +1 -1
  27. package/src/core/agent-loop.js +188 -100
  28. package/src/core/chat-runtime.js +676 -571
  29. package/src/core/config-store.js +9 -3
  30. package/src/core/git-oplog-change-tracker.js +468 -0
  31. package/src/core/non-git-backup.js +116 -0
  32. package/src/core/paths.js +123 -123
  33. package/src/core/session-store.js +148 -99
  34. package/src/core/tools.js +555 -434
  35. package/src/tui/chat-app.js +196 -56
  36. package/codemini-web/dist/assets/CodeWikiPanel-EPuoerNv.js +0 -1
  37. package/codemini-web/dist/assets/ConfigDialog-B5IGZCc9.js +0 -1
  38. package/codemini-web/dist/assets/GitDiffDialog-Bb_Tw5ZK.js +0 -222
  39. package/codemini-web/dist/assets/MessageBubble-wUff4GP4.js +0 -6
  40. package/codemini-web/dist/assets/ProjectSelector-C0leTf6f.js +0 -1
  41. package/codemini-web/dist/assets/SoulDialog-XDTEGWvH.js +0 -1
  42. package/codemini-web/dist/assets/chevron-right-Dbzw7YzA.js +0 -1
  43. package/codemini-web/dist/assets/index-D0EGtNPr.js +0 -65
  44. package/codemini-web/dist/assets/index-wOUf3WkN.css +0 -2
  45. package/codemini-web/dist/assets/input-CNQgbKe6.js +0 -1
  46. package/codemini-web/dist/assets/lib-BOngVP_M.js +0 -11
  47. package/codemini-web/dist/assets/lib-DrOTTm_N.js +0 -1
  48. package/codemini-web/dist/assets/mermaid-GHXKKRXX-DrBu5KyC.js +0 -1
  49. package/codemini-web/dist/assets/select-BZXfigic.js +0 -1
@@ -8,11 +8,11 @@ import { loadConfig, saveConfig, setConfigValue, getConfigValue } from '../src/c
8
8
  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
- import { RuntimeBridge } from './lib/runtime-bridge.js';
12
- import { installSkillSource, listSkillEntries } from '../src/commands/skill.js';
13
- import { computeFileSha256, readSkillRegistry, upsertSkillRegistryEntry, writeSkillRegistry } from '../src/core/skill-registry.js';
14
- import { forgetMemory, listMemories, searchMemories } from '../src/core/memory-store.js';
15
- import { getReplyLanguage } from '../src/core/reply-language.js';
11
+ import { RuntimeBridge } from './lib/runtime-bridge.js';
12
+ import { installSkillSource, listSkillEntries } from '../src/commands/skill.js';
13
+ import { computeFileSha256, readSkillRegistry, upsertSkillRegistryEntry, writeSkillRegistry } from '../src/core/skill-registry.js';
14
+ import { forgetMemory, listMemories, searchMemories } from '../src/core/memory-store.js';
15
+ import { getReplyLanguage } from '../src/core/reply-language.js';
16
16
  import { getBaseConfigDir, getFileIndexPath, getProjectSkillsDir, getSkillsDir } from '../src/core/paths.js';
17
17
  import { initializeProjectIndex } from '../src/core/project-index.js';
18
18
  import { INDEX_SKIP_DIRS } from '../src/core/constants.js';
@@ -23,19 +23,19 @@ const GENERAL_PROJECT_DIR = (() => {
23
23
  return path.join(base, 'workspace');
24
24
  })();
25
25
 
26
- const SKILL_CATALOG_FILE = 'codemini.skills.json';
27
- const SKILL_MODES = new Set(['always', 'auto_attach', 'agent_requested', 'manual']);
28
- const SKILL_SCOPES = new Set(['project', 'global']);
29
- const MEMORY_SCOPES = new Set(['user', 'global', 'project']);
30
-
31
- function normalizeSkillScope(scope) {
32
- return SKILL_SCOPES.has(scope) ? scope : 'project';
33
- }
34
-
35
- function normalizeMemoryScope(scope) {
36
- const value = String(scope || '').trim().toLowerCase();
37
- return MEMORY_SCOPES.has(value) ? value : 'user';
38
- }
26
+ const SKILL_CATALOG_FILE = 'codemini.skills.json';
27
+ const SKILL_MODES = new Set(['always', 'auto_attach', 'agent_requested', 'manual']);
28
+ const SKILL_SCOPES = new Set(['project', 'global']);
29
+ const MEMORY_SCOPES = new Set(['user', 'global', 'project']);
30
+
31
+ function normalizeSkillScope(scope) {
32
+ return SKILL_SCOPES.has(scope) ? scope : 'project';
33
+ }
34
+
35
+ function normalizeMemoryScope(scope) {
36
+ const value = String(scope || '').trim().toLowerCase();
37
+ return MEMORY_SCOPES.has(value) ? value : 'user';
38
+ }
39
39
 
40
40
  function isSafeSkillName(name = '') {
41
41
  return /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name);
@@ -272,107 +272,107 @@ async function serveStatic(res, filePath) {
272
272
  }
273
273
  }
274
274
 
275
- function normalizeProjectPath(value) {
276
- const raw = String(value || '').trim();
277
- if (!raw) return '';
278
- const win = raw.match(/^([A-Za-z]):[\\/](.*)$/);
275
+ function normalizeProjectPath(value) {
276
+ const raw = String(value || '').trim();
277
+ if (!raw) return '';
278
+ const win = raw.match(/^([A-Za-z]):[\\/](.*)$/);
279
279
  if (win && process.platform !== 'win32') {
280
280
  return path.join('/mnt', win[1].toLowerCase(), win[2].replace(/[\\/]+/g, '/'));
281
281
  }
282
- return path.resolve(raw);
283
- }
284
-
285
- function projectNameForDir(projectDir) {
286
- if (isGeneralProjectDir(projectDir)) return '__codemini_general__';
287
- return path.basename(path.resolve(projectDir || '')) || projectDir || '';
288
- }
289
-
290
- async function validProjectDir(value) {
291
- const normalized = normalizeProjectPath(value);
292
- if (!normalized) return '';
293
- try {
294
- const stat = await fs.stat(normalized);
295
- return stat.isDirectory() ? normalized : '';
296
- } catch {
297
- return '';
298
- }
299
- }
300
-
301
- async function resolveRequestProjectDir(value, fallbackDir) {
302
- const resolved = await validProjectDir(value);
303
- return resolved || fallbackDir;
304
- }
305
-
306
- async function parseProjectDirsParam(url, fallbackDir) {
307
- const raw = url.searchParams.get('projects');
308
- const parsed = raw ? tryParseJson(raw) : [];
309
- const values = Array.isArray(parsed) ? parsed : [];
310
- const seen = new Set();
311
- const dirs = [];
312
- for (const candidate of [fallbackDir, ...values]) {
313
- const resolved = await validProjectDir(candidate);
314
- if (!resolved || seen.has(resolved)) continue;
315
- seen.add(resolved);
316
- dirs.push(resolved);
317
- }
318
- if (dirs.length === 0 && fallbackDir) {
319
- dirs.push(fallbackDir);
320
- }
321
- return dirs;
322
- }
323
-
324
- async function listSkillsForProjectDirs(projectDirs, fallbackDir) {
325
- const dirs = projectDirs.length > 0 ? projectDirs : [fallbackDir];
326
- const seen = new Set();
327
- const results = [];
328
- for (let index = 0; index < dirs.length; index += 1) {
329
- const projectDir = dirs[index];
330
- const entries = await listSkillEntries({ scope: 'all', cwd: projectDir });
331
- for (const entry of entries) {
332
- if (entry.scope !== 'project') {
333
- const globalKey = `${entry.scope}:${entry.name}:${entry.path || ''}`;
334
- if (seen.has(globalKey)) continue;
335
- seen.add(globalKey);
336
- results.push(entry);
337
- continue;
338
- }
339
- const projectKey = `project:${projectDir}:${entry.name}:${entry.path || ''}`;
340
- if (seen.has(projectKey)) continue;
341
- seen.add(projectKey);
342
- results.push({
343
- ...entry,
344
- projectDir,
345
- projectName: projectNameForDir(projectDir)
346
- });
347
- }
348
- }
349
- return results.sort((a, b) => {
350
- const left = `${a.scope}:${a.projectName || ''}:${a.name}`;
351
- const right = `${b.scope}:${b.projectName || ''}:${b.name}`;
352
- return left.localeCompare(right);
353
- });
354
- }
355
-
356
- async function listMemoriesForProjectDirs({ scope, query, projectDirs, fallbackDir }) {
357
- if (scope !== 'project') {
358
- const items = query
359
- ? await searchMemories({ scope, query, workspaceRoot: fallbackDir })
360
- : await listMemories({ scope, workspaceRoot: fallbackDir });
361
- return items;
362
- }
363
- const dirs = projectDirs.length > 0 ? projectDirs : [fallbackDir];
364
- const chunks = await Promise.all(dirs.map(async (projectDir) => {
365
- const items = query
366
- ? await searchMemories({ scope, query, workspaceRoot: projectDir })
367
- : await listMemories({ scope, workspaceRoot: projectDir });
368
- return (items || []).map((item) => ({
369
- ...item,
370
- projectDir,
371
- projectName: projectNameForDir(projectDir)
372
- }));
373
- }));
374
- return chunks.flat();
375
- }
282
+ return path.resolve(raw);
283
+ }
284
+
285
+ function projectNameForDir(projectDir) {
286
+ if (isGeneralProjectDir(projectDir)) return '__codemini_general__';
287
+ return path.basename(path.resolve(projectDir || '')) || projectDir || '';
288
+ }
289
+
290
+ async function validProjectDir(value) {
291
+ const normalized = normalizeProjectPath(value);
292
+ if (!normalized) return '';
293
+ try {
294
+ const stat = await fs.stat(normalized);
295
+ return stat.isDirectory() ? normalized : '';
296
+ } catch {
297
+ return '';
298
+ }
299
+ }
300
+
301
+ async function resolveRequestProjectDir(value, fallbackDir) {
302
+ const resolved = await validProjectDir(value);
303
+ return resolved || fallbackDir;
304
+ }
305
+
306
+ async function parseProjectDirsParam(url, fallbackDir) {
307
+ const raw = url.searchParams.get('projects');
308
+ const parsed = raw ? tryParseJson(raw) : [];
309
+ const values = Array.isArray(parsed) ? parsed : [];
310
+ const seen = new Set();
311
+ const dirs = [];
312
+ for (const candidate of [fallbackDir, ...values]) {
313
+ const resolved = await validProjectDir(candidate);
314
+ if (!resolved || seen.has(resolved)) continue;
315
+ seen.add(resolved);
316
+ dirs.push(resolved);
317
+ }
318
+ if (dirs.length === 0 && fallbackDir) {
319
+ dirs.push(fallbackDir);
320
+ }
321
+ return dirs;
322
+ }
323
+
324
+ async function listSkillsForProjectDirs(projectDirs, fallbackDir) {
325
+ const dirs = projectDirs.length > 0 ? projectDirs : [fallbackDir];
326
+ const seen = new Set();
327
+ const results = [];
328
+ for (let index = 0; index < dirs.length; index += 1) {
329
+ const projectDir = dirs[index];
330
+ const entries = await listSkillEntries({ scope: 'all', cwd: projectDir });
331
+ for (const entry of entries) {
332
+ if (entry.scope !== 'project') {
333
+ const globalKey = `${entry.scope}:${entry.name}:${entry.path || ''}`;
334
+ if (seen.has(globalKey)) continue;
335
+ seen.add(globalKey);
336
+ results.push(entry);
337
+ continue;
338
+ }
339
+ const projectKey = `project:${projectDir}:${entry.name}:${entry.path || ''}`;
340
+ if (seen.has(projectKey)) continue;
341
+ seen.add(projectKey);
342
+ results.push({
343
+ ...entry,
344
+ projectDir,
345
+ projectName: projectNameForDir(projectDir)
346
+ });
347
+ }
348
+ }
349
+ return results.sort((a, b) => {
350
+ const left = `${a.scope}:${a.projectName || ''}:${a.name}`;
351
+ const right = `${b.scope}:${b.projectName || ''}:${b.name}`;
352
+ return left.localeCompare(right);
353
+ });
354
+ }
355
+
356
+ async function listMemoriesForProjectDirs({ scope, query, projectDirs, fallbackDir }) {
357
+ if (scope !== 'project') {
358
+ const items = query
359
+ ? await searchMemories({ scope, query, workspaceRoot: fallbackDir })
360
+ : await listMemories({ scope, workspaceRoot: fallbackDir });
361
+ return items;
362
+ }
363
+ const dirs = projectDirs.length > 0 ? projectDirs : [fallbackDir];
364
+ const chunks = await Promise.all(dirs.map(async (projectDir) => {
365
+ const items = query
366
+ ? await searchMemories({ scope, query, workspaceRoot: projectDir })
367
+ : await listMemories({ scope, workspaceRoot: projectDir });
368
+ return (items || []).map((item) => ({
369
+ ...item,
370
+ projectDir,
371
+ projectName: projectNameForDir(projectDir)
372
+ }));
373
+ }));
374
+ return chunks.flat();
375
+ }
376
376
 
377
377
  async function resolveCodeWikiProjectDir(url, fallbackDir) {
378
378
  const requested = normalizeProjectPath(url.searchParams.get('project') || '');
@@ -738,20 +738,34 @@ async function main() {
738
738
  jsonResponse(res, { ok: true });
739
739
  return;
740
740
  }
741
- if (req.method === 'POST' && url.pathname === '/api/execution-mode') {
742
- const { mode } = await readBody(req);
743
- if (!mode || !['normal', 'auto', 'plan'].includes(mode)) {
744
- jsonResponse(res, { error: true, message: 'Invalid mode' }, 400);
745
- return;
746
- }
741
+ if (req.method === 'POST' && url.pathname === '/api/execution-mode') {
742
+ const { mode } = await readBody(req);
743
+ if (!mode || !['normal', 'plan'].includes(mode)) {
744
+ jsonResponse(res, { error: true, message: 'Invalid mode' }, 400);
745
+ return;
746
+ }
747
747
  if (bridge.isBusy()) {
748
748
  jsonResponse(res, { error: true, message: 'Cannot switch execution mode while a request is running' }, 409);
749
749
  return;
750
750
  }
751
751
  const ok = await bridge.setExecutionMode(mode);
752
- jsonResponse(res, { ok });
753
- return;
754
- }
752
+ jsonResponse(res, { ok });
753
+ return;
754
+ }
755
+ if (req.method === 'POST' && url.pathname === '/api/approval-mode') {
756
+ const { mode } = await readBody(req);
757
+ if (!mode || !['review', 'auto', 'full_access'].includes(mode)) {
758
+ jsonResponse(res, { error: true, message: 'Invalid approval mode' }, 400);
759
+ return;
760
+ }
761
+ if (bridge.isBusy()) {
762
+ jsonResponse(res, { error: true, message: 'Cannot switch approval mode while a request is running' }, 409);
763
+ return;
764
+ }
765
+ const ok = await bridge.setApprovalMode(mode);
766
+ jsonResponse(res, { ok });
767
+ return;
768
+ }
755
769
  if (req.method === 'POST' && url.pathname === '/api/approval') {
756
770
  const { id, approved } = await readBody(req);
757
771
  jsonResponse(res, { ok: bridge.handleApproval(id, !!approved) });
@@ -968,7 +982,11 @@ async function main() {
968
982
 
969
983
  // ── Session management ──
970
984
  if (req.method === 'GET' && url.pathname === '/api/sessions') {
971
- const sessions = await listSessions(1000);
985
+ const requestedLimit = Number(url.searchParams.get('limit') || 200);
986
+ const limit = Number.isFinite(requestedLimit)
987
+ ? Math.max(1, Math.min(1000, Math.round(requestedLimit)))
988
+ : 200;
989
+ const sessions = await listSessions(limit);
972
990
  const enriched = sessions.map(s => ({ ...s, isGeneral: isGeneralProjectDir(s.projectDir) }));
973
991
  jsonResponse(res, enriched);
974
992
  return;
@@ -1008,7 +1026,17 @@ async function main() {
1008
1026
  });
1009
1027
  await bridge.switchRuntime(newRuntime);
1010
1028
  currentProjectDir = process.cwd();
1011
- jsonResponse(res, { ok: true, sessionId, cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
1029
+ jsonResponse(res, {
1030
+ ok: true,
1031
+ sessionId,
1032
+ cwd: currentProjectDir,
1033
+ isGeneral: isGeneralProjectDir(currentProjectDir),
1034
+ state: { ...bridge.getState(), cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) },
1035
+ sessionData: {
1036
+ messages: bridge.getSessionMessages(),
1037
+ compact: bridge.getSessionCompactMeta()
1038
+ }
1039
+ });
1012
1040
  } catch (err) {
1013
1041
  jsonResponse(res, { error: true, message: err.message }, 500);
1014
1042
  }
@@ -1028,7 +1056,7 @@ async function main() {
1028
1056
  let nextSessionId = bridge.getSessionId();
1029
1057
  let cwd = currentProjectDir;
1030
1058
  if (deletingCurrent) {
1031
- const remaining = await listSessions(1000);
1059
+ const remaining = await listSessions(1);
1032
1060
  const next = remaining.find((session) => session.id !== sessionId);
1033
1061
  const built = next
1034
1062
  ? await buildRuntimeForSession({ sessionId: next.id, model: bridge.getState().model })
@@ -1038,7 +1066,20 @@ async function main() {
1038
1066
  nextSessionId = built.session.id;
1039
1067
  cwd = currentProjectDir;
1040
1068
  }
1041
- jsonResponse(res, { ok: true, removed: result.removed, sessionId: nextSessionId, cwd, isGeneral: isGeneralProjectDir(currentProjectDir) });
1069
+ jsonResponse(res, {
1070
+ ok: true,
1071
+ removed: result.removed,
1072
+ sessionId: nextSessionId,
1073
+ cwd,
1074
+ isGeneral: isGeneralProjectDir(currentProjectDir),
1075
+ ...(deletingCurrent ? {
1076
+ state: { ...bridge.getState(), cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) },
1077
+ sessionData: {
1078
+ messages: bridge.getSessionMessages(),
1079
+ compact: bridge.getSessionCompactMeta()
1080
+ }
1081
+ } : {})
1082
+ });
1042
1083
  } catch (err) {
1043
1084
  jsonResponse(res, { error: true, message: err.message }, 500);
1044
1085
  }
@@ -1068,7 +1109,7 @@ async function main() {
1068
1109
  }
1069
1110
  return;
1070
1111
  }
1071
- if (req.method === 'GET' && url.pathname === '/api/git-diff') {
1112
+ if (req.method === 'GET' && url.pathname === '/api/git-diff') {
1072
1113
  try {
1073
1114
  let patch;
1074
1115
  try {
@@ -1105,9 +1146,44 @@ async function main() {
1105
1146
  } catch {
1106
1147
  jsonResponse(res, { patch: '', files: [] });
1107
1148
  }
1108
- return;
1109
- }
1110
- if (req.method === 'POST' && url.pathname === '/api/git-batch') {
1149
+ return;
1150
+ }
1151
+ if (req.method === 'GET' && url.pathname === '/api/session-changes') {
1152
+ try {
1153
+ jsonResponse(res, { changes: await bridge.getChangeSets() });
1154
+ } catch (err) {
1155
+ jsonResponse(res, { error: true, message: err?.message || 'Failed to read session changes' }, 500);
1156
+ }
1157
+ return;
1158
+ }
1159
+ if (req.method === 'GET' && url.pathname.startsWith('/api/session-changes/') && url.pathname.endsWith('/patch')) {
1160
+ const id = decodeURIComponent(url.pathname.slice('/api/session-changes/'.length, -'/patch'.length));
1161
+ try {
1162
+ jsonResponse(res, { id, patch: await bridge.getChangeSetPatch(id) });
1163
+ } catch (err) {
1164
+ jsonResponse(res, { error: true, message: err?.message || 'Failed to read change patch' }, 404);
1165
+ }
1166
+ return;
1167
+ }
1168
+ if (req.method === 'POST' && url.pathname !== '/api/session-changes/undo' && url.pathname.startsWith('/api/session-changes/') && url.pathname.endsWith('/undo')) {
1169
+ const id = decodeURIComponent(url.pathname.slice('/api/session-changes/'.length, -'/undo'.length));
1170
+ try {
1171
+ jsonResponse(res, await bridge.undoChangeSet(id));
1172
+ } catch (err) {
1173
+ jsonResponse(res, { error: true, message: err?.message || 'Failed to undo change' }, 409);
1174
+ }
1175
+ return;
1176
+ }
1177
+ if (req.method === 'POST' && url.pathname === '/api/session-changes/undo') {
1178
+ const { ids } = await readBody(req);
1179
+ try {
1180
+ jsonResponse(res, await bridge.undoChangeSets(ids));
1181
+ } catch (err) {
1182
+ jsonResponse(res, { error: true, message: err?.message || 'Failed to undo changes' }, 409);
1183
+ }
1184
+ return;
1185
+ }
1186
+ if (req.method === 'POST' && url.pathname === '/api/git-batch') {
1111
1187
  const { dirs } = await readBody(req);
1112
1188
  const result = {};
1113
1189
  for (const dir of (Array.isArray(dirs) ? dirs : [])) {
@@ -1213,64 +1289,64 @@ async function main() {
1213
1289
  }
1214
1290
  return;
1215
1291
  }
1216
- if (req.method === 'GET' && url.pathname.startsWith('/api/config/get/')) {
1217
- const key = url.pathname.slice('/api/config/get/'.length);
1218
- const value = await getConfigValue(key);
1219
- jsonResponse(res, { key, value });
1220
- return;
1221
- }
1222
-
1223
- // ── Memory management ──
1224
- if (req.method === 'GET' && url.pathname === '/api/memory') {
1225
- const scope = normalizeMemoryScope(url.searchParams.get('scope'));
1226
- const query = String(url.searchParams.get('q') || '').trim();
1227
- try {
1228
- const projectDirs = await parseProjectDirsParam(url, currentProjectDir);
1229
- const items = await listMemoriesForProjectDirs({
1230
- scope,
1231
- query,
1232
- projectDirs,
1233
- fallbackDir: currentProjectDir
1234
- });
1235
- jsonResponse(res, { scope, query, items });
1236
- } catch (err) {
1237
- jsonResponse(res, { error: true, message: err.message }, 500);
1238
- }
1239
- return;
1240
- }
1241
- if (req.method === 'DELETE' && url.pathname.startsWith('/api/memory/')) {
1242
- const id = decodeURIComponent(url.pathname.slice('/api/memory/'.length));
1243
- const scope = normalizeMemoryScope(url.searchParams.get('scope'));
1244
- if (!id) { jsonResponse(res, { error: true, message: 'Missing memory id' }, 400); return; }
1245
- try {
1246
- const workspaceRoot = scope === 'project'
1247
- ? await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir)
1248
- : currentProjectDir;
1249
- const result = await forgetMemory({ scope, id, workspaceRoot });
1250
- jsonResponse(res, { ok: true, scope, ...result });
1251
- } catch (err) {
1252
- jsonResponse(res, { error: true, message: err.message }, 500);
1253
- }
1254
- return;
1255
- }
1256
-
1257
- // ── Skills management ──
1258
- if (req.method === 'GET' && url.pathname === '/api/skills') {
1259
- try {
1260
- const projectDirs = await parseProjectDirsParam(url, currentProjectDir);
1261
- const skills = await listSkillsForProjectDirs(projectDirs, currentProjectDir);
1262
- jsonResponse(res, skills);
1263
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1264
- return;
1265
- }
1266
- if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
1267
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1268
- try {
1269
- const targetProjectDir = await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir);
1270
- const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1271
- const skill = entries.find(s => s.name === name);
1272
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1273
- const content = await fs.readFile(skill.path, 'utf8');
1292
+ if (req.method === 'GET' && url.pathname.startsWith('/api/config/get/')) {
1293
+ const key = url.pathname.slice('/api/config/get/'.length);
1294
+ const value = await getConfigValue(key);
1295
+ jsonResponse(res, { key, value });
1296
+ return;
1297
+ }
1298
+
1299
+ // ── Memory management ──
1300
+ if (req.method === 'GET' && url.pathname === '/api/memory') {
1301
+ const scope = normalizeMemoryScope(url.searchParams.get('scope'));
1302
+ const query = String(url.searchParams.get('q') || '').trim();
1303
+ try {
1304
+ const projectDirs = await parseProjectDirsParam(url, currentProjectDir);
1305
+ const items = await listMemoriesForProjectDirs({
1306
+ scope,
1307
+ query,
1308
+ projectDirs,
1309
+ fallbackDir: currentProjectDir
1310
+ });
1311
+ jsonResponse(res, { scope, query, items });
1312
+ } catch (err) {
1313
+ jsonResponse(res, { error: true, message: err.message }, 500);
1314
+ }
1315
+ return;
1316
+ }
1317
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/memory/')) {
1318
+ const id = decodeURIComponent(url.pathname.slice('/api/memory/'.length));
1319
+ const scope = normalizeMemoryScope(url.searchParams.get('scope'));
1320
+ if (!id) { jsonResponse(res, { error: true, message: 'Missing memory id' }, 400); return; }
1321
+ try {
1322
+ const workspaceRoot = scope === 'project'
1323
+ ? await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir)
1324
+ : currentProjectDir;
1325
+ const result = await forgetMemory({ scope, id, workspaceRoot });
1326
+ jsonResponse(res, { ok: true, scope, ...result });
1327
+ } catch (err) {
1328
+ jsonResponse(res, { error: true, message: err.message }, 500);
1329
+ }
1330
+ return;
1331
+ }
1332
+
1333
+ // ── Skills management ──
1334
+ if (req.method === 'GET' && url.pathname === '/api/skills') {
1335
+ try {
1336
+ const projectDirs = await parseProjectDirsParam(url, currentProjectDir);
1337
+ const skills = await listSkillsForProjectDirs(projectDirs, currentProjectDir);
1338
+ jsonResponse(res, skills);
1339
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1340
+ return;
1341
+ }
1342
+ if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
1343
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1344
+ try {
1345
+ const targetProjectDir = await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir);
1346
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1347
+ const skill = entries.find(s => s.name === name);
1348
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1349
+ const content = await fs.readFile(skill.path, 'utf8');
1274
1350
  jsonResponse(res, { name: skill.name, content, scope: skill.scope });
1275
1351
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1276
1352
  return;
@@ -1327,40 +1403,40 @@ async function main() {
1327
1403
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1328
1404
  return;
1329
1405
  }
1330
- if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
1331
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1332
- const { content, projectDir } = await readBody(req);
1333
- if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
1334
- try {
1335
- const targetProjectDir = await resolveRequestProjectDir(projectDir, currentProjectDir);
1336
- const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1337
- const skill = entries.find(s => s.name === name);
1338
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1339
- if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
1406
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
1407
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1408
+ const { content, projectDir } = await readBody(req);
1409
+ if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
1410
+ try {
1411
+ const targetProjectDir = await resolveRequestProjectDir(projectDir, currentProjectDir);
1412
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1413
+ const skill = entries.find(s => s.name === name);
1414
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1415
+ if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
1340
1416
  await fs.writeFile(skill.path, content, 'utf8');
1341
1417
  await bridge.reloadCommandsAndSkills();
1342
1418
  jsonResponse(res, { ok: true });
1343
1419
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1344
1420
  return;
1345
1421
  }
1346
- if (req.method === 'DELETE' && url.pathname.startsWith('/api/skills/')) {
1347
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length));
1348
- try {
1349
- const targetProjectDir = await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir);
1350
- const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1351
- const skill = entries.find(s => s.name === name);
1352
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1353
- if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
1422
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/skills/')) {
1423
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length));
1424
+ try {
1425
+ const targetProjectDir = await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir);
1426
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1427
+ const skill = entries.find(s => s.name === name);
1428
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1429
+ if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
1354
1430
  const dir = path.dirname(skill.path);
1355
1431
  await fs.rm(dir, { recursive: true, force: true });
1356
1432
  const registry = await readSkillRegistry();
1357
1433
  registry.skills = (registry.skills || []).filter(s => s.name !== name);
1358
1434
  await writeSkillRegistry(undefined, registry);
1359
- const catalog = await readProjectSkillCatalog(targetProjectDir);
1360
- if (catalog.skills?.[name]) {
1361
- delete catalog.skills[name];
1362
- await writeProjectSkillCatalog(targetProjectDir, catalog);
1363
- }
1435
+ const catalog = await readProjectSkillCatalog(targetProjectDir);
1436
+ if (catalog.skills?.[name]) {
1437
+ delete catalog.skills[name];
1438
+ await writeProjectSkillCatalog(targetProjectDir, catalog);
1439
+ }
1364
1440
  await deleteSkillCatalogMetadata(getSkillsDir(), name);
1365
1441
  const config = await loadConfig();
1366
1442
  if (config.skills?.enabled) delete config.skills.enabled[name];
@@ -1370,13 +1446,13 @@ async function main() {
1370
1446
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1371
1447
  return;
1372
1448
  }
1373
- if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/metadata')) {
1374
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/metadata'.length));
1375
- const body = await readBody(req);
1376
- try {
1377
- const targetProjectDir = await resolveRequestProjectDir(body?.projectDir, currentProjectDir);
1378
- const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1379
- const skill = entries.find(s => s.name === name);
1449
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/metadata')) {
1450
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/metadata'.length));
1451
+ const body = await readBody(req);
1452
+ try {
1453
+ const targetProjectDir = await resolveRequestProjectDir(body?.projectDir, currentProjectDir);
1454
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1455
+ const skill = entries.find(s => s.name === name);
1380
1456
  if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1381
1457
  if (skill.scope === 'builtin' && body?.scope && body.scope !== 'builtin') {
1382
1458
  jsonResponse(res, { error: true, message: 'Cannot move builtin skill' }, 403);
@@ -1388,15 +1464,15 @@ async function main() {
1388
1464
  let nextScope = skill.scope;
1389
1465
 
1390
1466
  if (skill.scope !== 'builtin' && requestedScope !== skill.scope) {
1391
- const sourceDir = path.dirname(skill.path);
1392
- const targetBaseDir = skillBaseDirForScope(requestedScope, targetProjectDir);
1393
- const targetDir = path.join(targetBaseDir, name);
1467
+ const sourceDir = path.dirname(skill.path);
1468
+ const targetBaseDir = skillBaseDirForScope(requestedScope, targetProjectDir);
1469
+ const targetDir = path.join(targetBaseDir, name);
1394
1470
  await fs.rm(targetDir, { recursive: true, force: true });
1395
1471
  await fs.mkdir(path.dirname(targetDir), { recursive: true });
1396
1472
  await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
1397
1473
  await fs.rm(sourceDir, { recursive: true, force: true });
1398
- if (requestedScope === 'global') {
1399
- await deleteSkillCatalogMetadata(getProjectSkillsDir(targetProjectDir), name);
1474
+ if (requestedScope === 'global') {
1475
+ await deleteSkillCatalogMetadata(getProjectSkillsDir(targetProjectDir), name);
1400
1476
  await upsertSkillRegistryEntry(undefined, {
1401
1477
  name,
1402
1478
  version: skill.version || '0.0.0',
@@ -1423,13 +1499,13 @@ async function main() {
1423
1499
  ...(metadataPatch.enabled !== undefined ? { enabled: metadataPatch.enabled } : {})
1424
1500
  });
1425
1501
  metadata = await upsertSkillCatalogMetadata(getSkillsDir(), name, body || {});
1426
- } else if (nextScope === 'project') {
1427
- metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1428
- } else if (skill.scope !== 'builtin') {
1429
- metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1430
- } else {
1431
- metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1432
- }
1502
+ } else if (nextScope === 'project') {
1503
+ metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1504
+ } else if (skill.scope !== 'builtin') {
1505
+ metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1506
+ } else {
1507
+ metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1508
+ }
1433
1509
  if (skill.scope !== 'builtin' && body?.enabled !== undefined) {
1434
1510
  const config = await loadConfig();
1435
1511
  config.skills = config.skills || {};
@@ -1445,16 +1521,16 @@ async function main() {
1445
1521
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1446
1522
  return;
1447
1523
  }
1448
- if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
1449
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
1450
- const { enabled, projectDir } = await readBody(req);
1451
- try {
1452
- const targetProjectDir = await resolveRequestProjectDir(projectDir, currentProjectDir);
1453
- const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1454
- const skill = entries.find(s => s.name === name);
1455
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1456
- if (skill.scope === 'builtin') {
1457
- const metadata = await upsertProjectSkillMetadata(targetProjectDir, name, { enabled });
1524
+ if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
1525
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
1526
+ const { enabled, projectDir } = await readBody(req);
1527
+ try {
1528
+ const targetProjectDir = await resolveRequestProjectDir(projectDir, currentProjectDir);
1529
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1530
+ const skill = entries.find(s => s.name === name);
1531
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1532
+ if (skill.scope === 'builtin') {
1533
+ const metadata = await upsertProjectSkillMetadata(targetProjectDir, name, { enabled });
1458
1534
  await bridge.reloadCommandsAndSkills();
1459
1535
  jsonResponse(res, { ok: true, name, metadata });
1460
1536
  return;