codemini-cli 0.6.3 → 0.6.4

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 (48) hide show
  1. package/codemini-web/dist/assets/{AboutDialog-jgqGjQgl.js → AboutDialog-MRopwNIL.js} +2 -2
  2. package/codemini-web/dist/assets/CodeWikiPanel-UpK5xGE3.js +1 -0
  3. package/codemini-web/dist/assets/ConfigDialog-CNl28wsj.js +1 -0
  4. package/codemini-web/dist/assets/GitDiffDialog-gSysUg2J.js +3 -0
  5. package/codemini-web/dist/assets/{MemoryDialog-BhxQgG0I.js → MemoryDialog-DFUmo3Kl.js} +3 -3
  6. package/codemini-web/dist/assets/MessageBubble-CGnnViv0.js +12 -0
  7. package/codemini-web/dist/assets/PatchDiff-B8rwvEg5.js +230 -0
  8. package/codemini-web/dist/assets/ProjectSelector-BF59M1zb.js +1 -0
  9. package/codemini-web/dist/assets/{SkillDialog-DxS43NpR.js → SkillDialog-CQTjbSiw.js} +4 -4
  10. package/codemini-web/dist/assets/SoulDialog-BLjUGqqB.js +1 -0
  11. package/codemini-web/dist/assets/chevron-right--85xg7qk.js +1 -0
  12. package/codemini-web/dist/assets/{chunk-BO2N2NFS-Budy_hfO.js → chunk-BO2N2NFS-6uELoidu.js} +6 -6
  13. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CQS1PAvD.js → highlighted-body-OFNGDK62-gb1UMBZ5.js} +1 -1
  14. package/codemini-web/dist/assets/index-1xqD0R5t.css +2 -0
  15. package/codemini-web/dist/assets/index-CDXQGwPs.js +65 -0
  16. package/codemini-web/dist/assets/{input-CNQgbKe6.js → input-Ca8O_061.js} +1 -1
  17. package/codemini-web/dist/assets/lib-BXWizt13.js +1 -0
  18. package/codemini-web/dist/assets/mermaid-GHXKKRXX-ROliF8Yd.js +1 -0
  19. package/codemini-web/dist/assets/{pencil-Ce_LFiEh.js → pencil-BhT11Ztp.js} +1 -1
  20. package/codemini-web/dist/assets/{refresh-cw-BKL-AZu5.js → refresh-cw-D7R5Lth6.js} +1 -1
  21. package/codemini-web/dist/assets/select-DBvcHBzs.js +1 -0
  22. package/codemini-web/dist/assets/{trash-2-KmAlCwXd.js → trash-2-BfNZcWfX.js} +1 -1
  23. package/codemini-web/dist/index.html +2 -2
  24. package/codemini-web/lib/runtime-bridge.js +325 -296
  25. package/codemini-web/server.js +310 -243
  26. package/package.json +1 -1
  27. package/src/core/agent-loop.js +188 -97
  28. package/src/core/chat-runtime.js +674 -571
  29. package/src/core/config-store.js +11 -3
  30. package/src/core/git-oplog-change-tracker.js +387 -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 +499 -456
  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/lib-BOngVP_M.js +0 -11
  46. package/codemini-web/dist/assets/lib-DrOTTm_N.js +0 -1
  47. package/codemini-web/dist/assets/mermaid-GHXKKRXX-DrBu5KyC.js +0 -1
  48. 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,35 @@ 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.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/git-batch') {
1111
1178
  const { dirs } = await readBody(req);
1112
1179
  const result = {};
1113
1180
  for (const dir of (Array.isArray(dirs) ? dirs : [])) {
@@ -1213,64 +1280,64 @@ async function main() {
1213
1280
  }
1214
1281
  return;
1215
1282
  }
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');
1283
+ if (req.method === 'GET' && url.pathname.startsWith('/api/config/get/')) {
1284
+ const key = url.pathname.slice('/api/config/get/'.length);
1285
+ const value = await getConfigValue(key);
1286
+ jsonResponse(res, { key, value });
1287
+ return;
1288
+ }
1289
+
1290
+ // ── Memory management ──
1291
+ if (req.method === 'GET' && url.pathname === '/api/memory') {
1292
+ const scope = normalizeMemoryScope(url.searchParams.get('scope'));
1293
+ const query = String(url.searchParams.get('q') || '').trim();
1294
+ try {
1295
+ const projectDirs = await parseProjectDirsParam(url, currentProjectDir);
1296
+ const items = await listMemoriesForProjectDirs({
1297
+ scope,
1298
+ query,
1299
+ projectDirs,
1300
+ fallbackDir: currentProjectDir
1301
+ });
1302
+ jsonResponse(res, { scope, query, items });
1303
+ } catch (err) {
1304
+ jsonResponse(res, { error: true, message: err.message }, 500);
1305
+ }
1306
+ return;
1307
+ }
1308
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/memory/')) {
1309
+ const id = decodeURIComponent(url.pathname.slice('/api/memory/'.length));
1310
+ const scope = normalizeMemoryScope(url.searchParams.get('scope'));
1311
+ if (!id) { jsonResponse(res, { error: true, message: 'Missing memory id' }, 400); return; }
1312
+ try {
1313
+ const workspaceRoot = scope === 'project'
1314
+ ? await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir)
1315
+ : currentProjectDir;
1316
+ const result = await forgetMemory({ scope, id, workspaceRoot });
1317
+ jsonResponse(res, { ok: true, scope, ...result });
1318
+ } catch (err) {
1319
+ jsonResponse(res, { error: true, message: err.message }, 500);
1320
+ }
1321
+ return;
1322
+ }
1323
+
1324
+ // ── Skills management ──
1325
+ if (req.method === 'GET' && url.pathname === '/api/skills') {
1326
+ try {
1327
+ const projectDirs = await parseProjectDirsParam(url, currentProjectDir);
1328
+ const skills = await listSkillsForProjectDirs(projectDirs, currentProjectDir);
1329
+ jsonResponse(res, skills);
1330
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1331
+ return;
1332
+ }
1333
+ if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
1334
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1335
+ try {
1336
+ const targetProjectDir = await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir);
1337
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1338
+ const skill = entries.find(s => s.name === name);
1339
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1340
+ const content = await fs.readFile(skill.path, 'utf8');
1274
1341
  jsonResponse(res, { name: skill.name, content, scope: skill.scope });
1275
1342
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1276
1343
  return;
@@ -1327,40 +1394,40 @@ async function main() {
1327
1394
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1328
1395
  return;
1329
1396
  }
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; }
1397
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
1398
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
1399
+ const { content, projectDir } = await readBody(req);
1400
+ if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
1401
+ try {
1402
+ const targetProjectDir = await resolveRequestProjectDir(projectDir, currentProjectDir);
1403
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1404
+ const skill = entries.find(s => s.name === name);
1405
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1406
+ if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
1340
1407
  await fs.writeFile(skill.path, content, 'utf8');
1341
1408
  await bridge.reloadCommandsAndSkills();
1342
1409
  jsonResponse(res, { ok: true });
1343
1410
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1344
1411
  return;
1345
1412
  }
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; }
1413
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/skills/')) {
1414
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length));
1415
+ try {
1416
+ const targetProjectDir = await resolveRequestProjectDir(url.searchParams.get('projectDir'), currentProjectDir);
1417
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1418
+ const skill = entries.find(s => s.name === name);
1419
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1420
+ if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
1354
1421
  const dir = path.dirname(skill.path);
1355
1422
  await fs.rm(dir, { recursive: true, force: true });
1356
1423
  const registry = await readSkillRegistry();
1357
1424
  registry.skills = (registry.skills || []).filter(s => s.name !== name);
1358
1425
  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
- }
1426
+ const catalog = await readProjectSkillCatalog(targetProjectDir);
1427
+ if (catalog.skills?.[name]) {
1428
+ delete catalog.skills[name];
1429
+ await writeProjectSkillCatalog(targetProjectDir, catalog);
1430
+ }
1364
1431
  await deleteSkillCatalogMetadata(getSkillsDir(), name);
1365
1432
  const config = await loadConfig();
1366
1433
  if (config.skills?.enabled) delete config.skills.enabled[name];
@@ -1370,13 +1437,13 @@ async function main() {
1370
1437
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1371
1438
  return;
1372
1439
  }
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);
1440
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/metadata')) {
1441
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/metadata'.length));
1442
+ const body = await readBody(req);
1443
+ try {
1444
+ const targetProjectDir = await resolveRequestProjectDir(body?.projectDir, currentProjectDir);
1445
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1446
+ const skill = entries.find(s => s.name === name);
1380
1447
  if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1381
1448
  if (skill.scope === 'builtin' && body?.scope && body.scope !== 'builtin') {
1382
1449
  jsonResponse(res, { error: true, message: 'Cannot move builtin skill' }, 403);
@@ -1388,15 +1455,15 @@ async function main() {
1388
1455
  let nextScope = skill.scope;
1389
1456
 
1390
1457
  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);
1458
+ const sourceDir = path.dirname(skill.path);
1459
+ const targetBaseDir = skillBaseDirForScope(requestedScope, targetProjectDir);
1460
+ const targetDir = path.join(targetBaseDir, name);
1394
1461
  await fs.rm(targetDir, { recursive: true, force: true });
1395
1462
  await fs.mkdir(path.dirname(targetDir), { recursive: true });
1396
1463
  await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
1397
1464
  await fs.rm(sourceDir, { recursive: true, force: true });
1398
- if (requestedScope === 'global') {
1399
- await deleteSkillCatalogMetadata(getProjectSkillsDir(targetProjectDir), name);
1465
+ if (requestedScope === 'global') {
1466
+ await deleteSkillCatalogMetadata(getProjectSkillsDir(targetProjectDir), name);
1400
1467
  await upsertSkillRegistryEntry(undefined, {
1401
1468
  name,
1402
1469
  version: skill.version || '0.0.0',
@@ -1423,13 +1490,13 @@ async function main() {
1423
1490
  ...(metadataPatch.enabled !== undefined ? { enabled: metadataPatch.enabled } : {})
1424
1491
  });
1425
1492
  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
- }
1493
+ } else if (nextScope === 'project') {
1494
+ metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1495
+ } else if (skill.scope !== 'builtin') {
1496
+ metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1497
+ } else {
1498
+ metadata = await upsertProjectSkillMetadata(targetProjectDir, name, body || {});
1499
+ }
1433
1500
  if (skill.scope !== 'builtin' && body?.enabled !== undefined) {
1434
1501
  const config = await loadConfig();
1435
1502
  config.skills = config.skills || {};
@@ -1445,16 +1512,16 @@ async function main() {
1445
1512
  } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
1446
1513
  return;
1447
1514
  }
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 });
1515
+ if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
1516
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
1517
+ const { enabled, projectDir } = await readBody(req);
1518
+ try {
1519
+ const targetProjectDir = await resolveRequestProjectDir(projectDir, currentProjectDir);
1520
+ const entries = await listSkillEntries({ scope: 'all', cwd: targetProjectDir });
1521
+ const skill = entries.find(s => s.name === name);
1522
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
1523
+ if (skill.scope === 'builtin') {
1524
+ const metadata = await upsertProjectSkillMetadata(targetProjectDir, name, { enabled });
1458
1525
  await bridge.reloadCommandsAndSkills();
1459
1526
  jsonResponse(res, { ok: true, name, metadata });
1460
1527
  return;