codemini-cli 0.5.10 → 0.5.11
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.
- package/OPERATIONS.md +242 -242
- package/README.md +588 -588
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
- package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
- package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
- package/codemini-web/dist/index.html +23 -23
- package/codemini-web/lib/approval-manager.js +32 -32
- package/codemini-web/lib/runtime-bridge.js +17 -11
- package/codemini-web/server.js +534 -205
- package/deployment.md +212 -212
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +77 -77
- package/skills/codemini.skills.json +40 -40
- package/skills/grill-me/SKILL.md +30 -30
- package/skills/superpowers-lite/SKILL.md +82 -82
- package/src/cli.js +74 -74
- package/src/commands/chat.js +210 -210
- package/src/commands/run.js +313 -313
- package/src/commands/skill.js +438 -304
- package/src/commands/web.js +57 -57
- package/src/core/agent-loop.js +980 -980
- package/src/core/ast.js +309 -307
- package/src/core/chat-runtime.js +6261 -6253
- package/src/core/command-evaluator.js +72 -72
- package/src/core/command-loader.js +311 -311
- package/src/core/command-policy.js +301 -301
- package/src/core/command-risk.js +156 -156
- package/src/core/config-store.js +289 -289
- package/src/core/constants.js +18 -1
- package/src/core/context-compact.js +365 -365
- package/src/core/default-system-prompt.js +114 -107
- package/src/core/dream-audit.js +105 -105
- package/src/core/dream-consolidate.js +229 -229
- package/src/core/dream-evaluator.js +185 -185
- package/src/core/fff-adapter.js +383 -383
- package/src/core/memory-store.js +543 -543
- package/src/core/project-index.js +737 -548
- package/src/core/project-instructions.js +98 -98
- package/src/core/provider/anthropic.js +514 -514
- package/src/core/provider/openai-compatible.js +501 -501
- package/src/core/reflect-skill.js +178 -178
- package/src/core/reply-language.js +40 -40
- package/src/core/session-store.js +474 -474
- package/src/core/shell-profile.js +237 -237
- package/src/core/shell.js +323 -323
- package/src/core/soul.js +69 -69
- package/src/core/system-prompt-composer.js +52 -52
- package/src/core/tool-args.js +199 -154
- package/src/core/tool-output.js +184 -184
- package/src/core/tool-result-store.js +206 -206
- package/src/core/tools.js +3024 -2893
- package/src/core/version.js +11 -11
- package/src/tui/chat-app.js +5171 -5171
- package/src/tui/tool-activity/presenters/misc.js +30 -30
- package/src/tui/tool-activity/presenters/system.js +20 -20
- package/templates/project-requirements/report-shell.html +582 -582
- package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
package/codemini-web/server.js
CHANGED
|
@@ -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 {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
235
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
await
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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 === '
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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;
|