codemini-cli 0.5.7 → 0.5.9
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/README.md +30 -0
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-Dp1CwQdI.js → highlighted-body-OFNGDK62-HgeDi9HJ.js} +1 -1
- package/codemini-web/dist/assets/index-BSdIdn3L.css +2 -0
- package/codemini-web/dist/assets/{index-Bvd2jj3t.js → index-C4tKT3v4.js} +95 -93
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-CDgkkDBg.js +1 -0
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +550 -549
- package/codemini-web/server.js +314 -187
- package/package.json +67 -67
- package/skills/codemini.skills.json +40 -0
- package/src/commands/skill.js +16 -5
- package/src/core/chat-runtime.js +93 -14
- package/src/core/command-loader.js +120 -25
- package/src/core/command-policy.js +34 -10
- package/src/core/config-store.js +11 -0
- package/src/core/provider/anthropic.js +137 -24
- package/src/core/tools.js +114 -65
- package/codemini-web/dist/assets/index-Csjkc1MY.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-DSVp--w4.js +0 -1
package/codemini-web/server.js
CHANGED
|
@@ -9,23 +9,107 @@ 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
|
|
14
|
-
import { getReplyLanguage } from '../src/core/reply-language.js';
|
|
15
|
-
import {
|
|
16
|
-
import { VERSION } from '../src/core/version.js';
|
|
12
|
+
import { listSkillEntries } from '../src/commands/skill.js';
|
|
13
|
+
import { readSkillRegistry, writeSkillRegistry } from '../src/core/skill-registry.js';
|
|
14
|
+
import { getReplyLanguage } from '../src/core/reply-language.js';
|
|
15
|
+
import { getBaseConfigDir, getProjectSkillsDir } from '../src/core/paths.js';
|
|
16
|
+
import { VERSION } from '../src/core/version.js';
|
|
17
17
|
|
|
18
18
|
const GENERAL_PROJECT_DIR = (() => {
|
|
19
19
|
const base = getBaseConfigDir();
|
|
20
20
|
return path.join(base, 'workspace');
|
|
21
21
|
})();
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const SKILL_CATALOG_FILE = 'codemini.skills.json';
|
|
24
|
+
const SKILL_MODES = new Set(['always', 'auto_attach', 'agent_requested', 'manual']);
|
|
25
|
+
|
|
26
|
+
function normalizeSkillMetadataPatch(input = {}) {
|
|
27
|
+
const out = {};
|
|
28
|
+
if (typeof input.description === 'string') out.description = input.description.trim();
|
|
29
|
+
if (typeof input.mode === 'string' && SKILL_MODES.has(input.mode)) out.mode = input.mode;
|
|
30
|
+
if (input.enabled !== undefined) out.enabled = input.enabled !== false;
|
|
31
|
+
if (input.priority !== undefined) {
|
|
32
|
+
const priority = Number(input.priority);
|
|
33
|
+
if (Number.isFinite(priority)) out.priority = Math.max(0, Math.min(100, Math.round(priority)));
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(input.triggers)) {
|
|
36
|
+
out.triggers = input.triggers.map((item) => String(item || '').trim()).filter(Boolean);
|
|
37
|
+
} else if (typeof input.triggers === 'string') {
|
|
38
|
+
out.triggers = input.triggers.split(',').map((item) => item.trim()).filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readProjectSkillCatalog(projectDir) {
|
|
44
|
+
const catalogPath = path.join(getProjectSkillsDir(projectDir), SKILL_CATALOG_FILE);
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(await fs.readFile(catalogPath, 'utf8'));
|
|
47
|
+
return parsed && typeof parsed === 'object' ? parsed : { version: 1, skills: {} };
|
|
48
|
+
} catch {
|
|
49
|
+
return { version: 1, skills: {} };
|
|
50
|
+
}
|
|
26
51
|
}
|
|
27
52
|
|
|
28
|
-
|
|
53
|
+
async function writeProjectSkillCatalog(projectDir, catalog) {
|
|
54
|
+
const catalogPath = path.join(getProjectSkillsDir(projectDir), SKILL_CATALOG_FILE);
|
|
55
|
+
await fs.mkdir(path.dirname(catalogPath), { recursive: true });
|
|
56
|
+
const next = {
|
|
57
|
+
version: 1,
|
|
58
|
+
skills: catalog?.skills && typeof catalog.skills === 'object' ? catalog.skills : {}
|
|
59
|
+
};
|
|
60
|
+
await fs.writeFile(catalogPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function upsertProjectSkillMetadata(projectDir, name, patch) {
|
|
64
|
+
const catalog = await readProjectSkillCatalog(projectDir);
|
|
65
|
+
catalog.skills = catalog.skills || {};
|
|
66
|
+
const prior = catalog.skills[name] && typeof catalog.skills[name] === 'object' ? catalog.skills[name] : {};
|
|
67
|
+
catalog.skills[name] = { ...prior, ...normalizeSkillMetadataPatch(patch) };
|
|
68
|
+
await writeProjectSkillCatalog(projectDir, catalog);
|
|
69
|
+
return catalog.skills[name];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function listProjectRoots() {
|
|
73
|
+
if (process.platform === 'win32') {
|
|
74
|
+
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
|
75
|
+
const roots = [];
|
|
76
|
+
await Promise.all(letters.map(async (letter) => {
|
|
77
|
+
const drivePath = `${letter}:\\`;
|
|
78
|
+
try {
|
|
79
|
+
await fs.access(drivePath);
|
|
80
|
+
roots.push({ name: `${letter}:`, path: drivePath, isGit: false, isDrive: true });
|
|
81
|
+
} catch {}
|
|
82
|
+
}));
|
|
83
|
+
return roots.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const candidates = [
|
|
87
|
+
{ name: '/', path: path.resolve('/') },
|
|
88
|
+
{ name: 'Home', path: process.env.HOME || process.env.USERPROFILE || '' },
|
|
89
|
+
{ name: 'Current', path: process.cwd() },
|
|
90
|
+
];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const roots = [];
|
|
93
|
+
for (const candidate of candidates) {
|
|
94
|
+
if (!candidate.path) continue;
|
|
95
|
+
const resolved = path.resolve(candidate.path);
|
|
96
|
+
if (seen.has(resolved)) continue;
|
|
97
|
+
try {
|
|
98
|
+
const stat = await fs.stat(resolved);
|
|
99
|
+
if (!stat.isDirectory()) continue;
|
|
100
|
+
seen.add(resolved);
|
|
101
|
+
roots.push({ name: candidate.name, path: resolved, isGit: false, isDrive: false });
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
return roots;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isGeneralProjectDir(value) {
|
|
108
|
+
if (!value) return false;
|
|
109
|
+
return path.resolve(value) === path.resolve(GENERAL_PROJECT_DIR);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
113
|
const CLIENT_SOURCE_DIR = path.join(__dirname, 'client');
|
|
30
114
|
let CLIENT_DIR = CLIENT_SOURCE_DIR;
|
|
31
115
|
try {
|
|
@@ -34,33 +118,33 @@ try {
|
|
|
34
118
|
if (stat.isDirectory()) CLIENT_DIR = distDir;
|
|
35
119
|
} catch {}
|
|
36
120
|
|
|
37
|
-
const MIME_TYPES = {
|
|
121
|
+
const MIME_TYPES = {
|
|
38
122
|
'.html': 'text/html; charset=utf-8',
|
|
39
123
|
'.css': 'text/css; charset=utf-8',
|
|
40
124
|
'.js': 'text/javascript; charset=utf-8',
|
|
41
125
|
'.json': 'application/json',
|
|
42
126
|
'.svg': 'image/svg+xml',
|
|
43
127
|
'.png': 'image/png',
|
|
44
|
-
'.ico': 'image/x-icon'
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const DEFAULT_GATEWAY_BASE_URL = 'http://127.0.0.1:8000/v1';
|
|
48
|
-
|
|
49
|
-
function normalizeBaseUrl(value) {
|
|
50
|
-
return String(value || '').trim().replace(/\/+$/, '');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getConfigStatus(config) {
|
|
54
|
-
const baseUrl = normalizeBaseUrl(config?.gateway?.base_url);
|
|
55
|
-
const apiKey = String(config?.gateway?.api_key || '').trim();
|
|
56
|
-
const setupRequired = !baseUrl || (baseUrl === DEFAULT_GATEWAY_BASE_URL && !apiKey);
|
|
57
|
-
return {
|
|
58
|
-
setupRequired,
|
|
59
|
-
baseUrl,
|
|
60
|
-
hasApiKey: !!apiKey,
|
|
61
|
-
reason: setupRequired ? 'gateway_not_configured' : ''
|
|
62
|
-
};
|
|
63
|
-
}
|
|
128
|
+
'.ico': 'image/x-icon'
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const DEFAULT_GATEWAY_BASE_URL = 'http://127.0.0.1:8000/v1';
|
|
132
|
+
|
|
133
|
+
function normalizeBaseUrl(value) {
|
|
134
|
+
return String(value || '').trim().replace(/\/+$/, '');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getConfigStatus(config) {
|
|
138
|
+
const baseUrl = normalizeBaseUrl(config?.gateway?.base_url);
|
|
139
|
+
const apiKey = String(config?.gateway?.api_key || '').trim();
|
|
140
|
+
const setupRequired = !baseUrl || (baseUrl === DEFAULT_GATEWAY_BASE_URL && !apiKey);
|
|
141
|
+
return {
|
|
142
|
+
setupRequired,
|
|
143
|
+
baseUrl,
|
|
144
|
+
hasApiKey: !!apiKey,
|
|
145
|
+
reason: setupRequired ? 'gateway_not_configured' : ''
|
|
146
|
+
};
|
|
147
|
+
}
|
|
64
148
|
|
|
65
149
|
function parseArgs(argv) {
|
|
66
150
|
const parsed = { port: 3210, session: undefined, model: undefined, project: undefined, open: true };
|
|
@@ -170,10 +254,14 @@ function collectSessionPathHints(session) {
|
|
|
170
254
|
return hints;
|
|
171
255
|
}
|
|
172
256
|
|
|
173
|
-
async function existingDirectoryForHint(rawHint) {
|
|
174
|
-
let candidate = normalizeProjectPath(rawHint);
|
|
175
|
-
if (!candidate) return '';
|
|
176
|
-
|
|
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, '');
|
|
177
265
|
for (let i = 0; i < 8 && candidate && candidate !== path.dirname(candidate); i += 1) {
|
|
178
266
|
try {
|
|
179
267
|
const stat = await fs.stat(candidate);
|
|
@@ -260,7 +348,7 @@ async function buildRuntimeForSession({ sessionId, model, projectDir }) {
|
|
|
260
348
|
model: model || config.model?.name,
|
|
261
349
|
systemPrompt
|
|
262
350
|
});
|
|
263
|
-
return { runtime, config, session, cwd: process.cwd(), isGeneral: isGeneralProjectDir(process.cwd()) };
|
|
351
|
+
return { runtime, config, session, cwd: process.cwd(), isGeneral: isGeneralProjectDir(process.cwd()) };
|
|
264
352
|
}
|
|
265
353
|
|
|
266
354
|
async function main() {
|
|
@@ -317,25 +405,25 @@ async function main() {
|
|
|
317
405
|
return;
|
|
318
406
|
}
|
|
319
407
|
|
|
320
|
-
// ── Submit / Abort / Approval ──
|
|
321
|
-
if (req.method === 'POST' && url.pathname === '/api/submit') {
|
|
322
|
-
const { line, readOnlyCodeWiki } = await readBody(req);
|
|
323
|
-
if (!line || typeof line !== 'string') { jsonResponse(res, { error: true, message: 'Missing "line" field' }, 400); return; }
|
|
324
|
-
const currentConfig = await loadConfig();
|
|
325
|
-
const configStatus = getConfigStatus(currentConfig);
|
|
326
|
-
if (configStatus.setupRequired) {
|
|
327
|
-
jsonResponse(res, {
|
|
328
|
-
error: true,
|
|
329
|
-
code: 'CONFIG_REQUIRED',
|
|
330
|
-
message: 'Gateway is not configured. Open Settings and set the API Base URL and API Key.',
|
|
331
|
-
configStatus
|
|
332
|
-
}, 409);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const result = bridge.handleSubmit(line, { readOnlyCodeWiki: readOnlyCodeWiki === true });
|
|
336
|
-
jsonResponse(res, result);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
408
|
+
// ── Submit / Abort / Approval ──
|
|
409
|
+
if (req.method === 'POST' && url.pathname === '/api/submit') {
|
|
410
|
+
const { line, readOnlyCodeWiki } = await readBody(req);
|
|
411
|
+
if (!line || typeof line !== 'string') { jsonResponse(res, { error: true, message: 'Missing "line" field' }, 400); return; }
|
|
412
|
+
const currentConfig = await loadConfig();
|
|
413
|
+
const configStatus = getConfigStatus(currentConfig);
|
|
414
|
+
if (configStatus.setupRequired) {
|
|
415
|
+
jsonResponse(res, {
|
|
416
|
+
error: true,
|
|
417
|
+
code: 'CONFIG_REQUIRED',
|
|
418
|
+
message: 'Gateway is not configured. Open Settings and set the API Base URL and API Key.',
|
|
419
|
+
configStatus
|
|
420
|
+
}, 409);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const result = bridge.handleSubmit(line, { readOnlyCodeWiki: readOnlyCodeWiki === true });
|
|
424
|
+
jsonResponse(res, result);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
339
427
|
if (req.method === 'POST' && url.pathname === '/api/abort') {
|
|
340
428
|
bridge.handleAbort();
|
|
341
429
|
jsonResponse(res, { ok: true });
|
|
@@ -367,7 +455,7 @@ async function main() {
|
|
|
367
455
|
try {
|
|
368
456
|
latest = execSync('npm view codemini-cli version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
369
457
|
} catch {}
|
|
370
|
-
jsonResponse(res, { current: VERSION, latest });
|
|
458
|
+
jsonResponse(res, { current: VERSION, latest });
|
|
371
459
|
return;
|
|
372
460
|
}
|
|
373
461
|
if (req.method === 'POST' && url.pathname === '/api/update') {
|
|
@@ -382,7 +470,7 @@ async function main() {
|
|
|
382
470
|
|
|
383
471
|
// ── Runtime state ──
|
|
384
472
|
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
385
|
-
jsonResponse(res, { ...bridge.getState(), cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
473
|
+
jsonResponse(res, { ...bridge.getState(), cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
386
474
|
return;
|
|
387
475
|
}
|
|
388
476
|
if (req.method === 'GET' && url.pathname === '/api/completions') {
|
|
@@ -488,25 +576,25 @@ async function main() {
|
|
|
488
576
|
return;
|
|
489
577
|
}
|
|
490
578
|
|
|
491
|
-
if (req.method === 'POST' && url.pathname === '/api/codewiki/ask') {
|
|
492
|
-
const { question, reportFile } = await readBody(req);
|
|
493
|
-
if (!question || typeof question !== 'string') {
|
|
494
|
-
jsonResponse(res, { error: true, message: 'Missing "question" field' }, 400);
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
const currentConfig = await loadConfig();
|
|
498
|
-
const configStatus = getConfigStatus(currentConfig);
|
|
499
|
-
if (configStatus.setupRequired) {
|
|
500
|
-
jsonResponse(res, {
|
|
501
|
-
error: true,
|
|
502
|
-
code: 'CONFIG_REQUIRED',
|
|
503
|
-
message: 'Gateway is not configured. Open Settings and set the API Base URL and API Key.',
|
|
504
|
-
configStatus
|
|
505
|
-
}, 409);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const selectedReport = isCodeWikiReportFile(reportFile) ? reportFile : '';
|
|
509
|
-
if (bridge.isBusy()) {
|
|
579
|
+
if (req.method === 'POST' && url.pathname === '/api/codewiki/ask') {
|
|
580
|
+
const { question, reportFile } = await readBody(req);
|
|
581
|
+
if (!question || typeof question !== 'string') {
|
|
582
|
+
jsonResponse(res, { error: true, message: 'Missing "question" field' }, 400);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const currentConfig = await loadConfig();
|
|
586
|
+
const configStatus = getConfigStatus(currentConfig);
|
|
587
|
+
if (configStatus.setupRequired) {
|
|
588
|
+
jsonResponse(res, {
|
|
589
|
+
error: true,
|
|
590
|
+
code: 'CONFIG_REQUIRED',
|
|
591
|
+
message: 'Gateway is not configured. Open Settings and set the API Base URL and API Key.',
|
|
592
|
+
configStatus
|
|
593
|
+
}, 409);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const selectedReport = isCodeWikiReportFile(reportFile) ? reportFile : '';
|
|
597
|
+
if (bridge.isBusy()) {
|
|
510
598
|
jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
|
|
511
599
|
return;
|
|
512
600
|
}
|
|
@@ -538,7 +626,7 @@ async function main() {
|
|
|
538
626
|
// ── Session management ──
|
|
539
627
|
if (req.method === 'GET' && url.pathname === '/api/sessions') {
|
|
540
628
|
const sessions = await listSessions(1000);
|
|
541
|
-
const enriched = sessions.map(s => ({ ...s, isGeneral: isGeneralProjectDir(s.projectDir) }));
|
|
629
|
+
const enriched = sessions.map(s => ({ ...s, isGeneral: isGeneralProjectDir(s.projectDir) }));
|
|
542
630
|
jsonResponse(res, enriched);
|
|
543
631
|
return;
|
|
544
632
|
}
|
|
@@ -551,7 +639,7 @@ async function main() {
|
|
|
551
639
|
reused: true,
|
|
552
640
|
sessionId: bridge.getSessionId(),
|
|
553
641
|
cwd: currentProjectDir,
|
|
554
|
-
isGeneral: isGeneralProjectDir(currentProjectDir)
|
|
642
|
+
isGeneral: isGeneralProjectDir(currentProjectDir)
|
|
555
643
|
});
|
|
556
644
|
return;
|
|
557
645
|
}
|
|
@@ -561,7 +649,7 @@ async function main() {
|
|
|
561
649
|
});
|
|
562
650
|
await bridge.switchRuntime(newRuntime);
|
|
563
651
|
currentProjectDir = process.cwd();
|
|
564
|
-
jsonResponse(res, { ok: true, sessionId: session.id, cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
652
|
+
jsonResponse(res, { ok: true, sessionId: session.id, cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
565
653
|
} catch (err) {
|
|
566
654
|
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
567
655
|
}
|
|
@@ -577,7 +665,7 @@ async function main() {
|
|
|
577
665
|
});
|
|
578
666
|
await bridge.switchRuntime(newRuntime);
|
|
579
667
|
currentProjectDir = process.cwd();
|
|
580
|
-
jsonResponse(res, { ok: true, sessionId, cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
668
|
+
jsonResponse(res, { ok: true, sessionId, cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
581
669
|
} catch (err) {
|
|
582
670
|
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
583
671
|
}
|
|
@@ -607,7 +695,7 @@ async function main() {
|
|
|
607
695
|
nextSessionId = built.session.id;
|
|
608
696
|
cwd = currentProjectDir;
|
|
609
697
|
}
|
|
610
|
-
jsonResponse(res, { ok: true, removed: result.removed, sessionId: nextSessionId, cwd, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
698
|
+
jsonResponse(res, { ok: true, removed: result.removed, sessionId: nextSessionId, cwd, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
611
699
|
} catch (err) {
|
|
612
700
|
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
613
701
|
}
|
|
@@ -616,7 +704,7 @@ async function main() {
|
|
|
616
704
|
|
|
617
705
|
// ── Project management ──
|
|
618
706
|
if (req.method === 'GET' && url.pathname === '/api/project') {
|
|
619
|
-
jsonResponse(res, { cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
707
|
+
jsonResponse(res, { cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
620
708
|
return;
|
|
621
709
|
}
|
|
622
710
|
if (req.method === 'GET' && url.pathname === '/api/git') {
|
|
@@ -691,43 +779,48 @@ async function main() {
|
|
|
691
779
|
jsonResponse(res, result);
|
|
692
780
|
return;
|
|
693
781
|
}
|
|
694
|
-
if (req.method === 'POST' && url.pathname === '/api/project/open') {
|
|
695
|
-
const { path: projectPath } = await readBody(req);
|
|
696
|
-
if (!projectPath) { jsonResponse(res, { error: true, message: 'Missing path' }, 400); return; }
|
|
697
|
-
try {
|
|
698
|
-
// Client marker for general workspace
|
|
699
|
-
const openingGeneral = projectPath === '__codemini_general__';
|
|
700
|
-
const resolved = openingGeneral ? GENERAL_PROJECT_DIR : path.resolve(projectPath);
|
|
701
|
-
const stat = await fs.stat(resolved);
|
|
702
|
-
if (!stat.isDirectory()) throw new Error('Not a directory');
|
|
703
|
-
let built;
|
|
704
|
-
if (openingGeneral) {
|
|
705
|
-
const all = await listSessions(1000, { includeEmpty: true });
|
|
706
|
-
const reusable = all.find((session) =>
|
|
707
|
-
isGeneralProjectDir(session.projectDir) &&
|
|
708
|
-
Number(session.messageCount || 0) === 0
|
|
709
|
-
);
|
|
710
|
-
built = reusable
|
|
711
|
-
? await buildRuntimeForSession({ sessionId: reusable.id, model: bridge.getState().model })
|
|
712
|
-
: await buildRuntimeForSession({ model: bridge.getState().model, projectDir: GENERAL_PROJECT_DIR });
|
|
713
|
-
} else {
|
|
714
|
-
process.chdir(resolved);
|
|
715
|
-
currentProjectDir = process.cwd();
|
|
716
|
-
built = await buildRuntimeForSession({
|
|
717
|
-
model: bridge.getState().model
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
const { runtime: newRuntime, session } = built;
|
|
721
|
-
await bridge.switchRuntime(newRuntime);
|
|
722
|
-
currentProjectDir = process.cwd();
|
|
723
|
-
jsonResponse(res, { ok: true, cwd: currentProjectDir, sessionId: session.id, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
724
|
-
} catch (err) {
|
|
725
|
-
jsonResponse(res, { error: true, message: err.message }, 400);
|
|
726
|
-
}
|
|
782
|
+
if (req.method === 'POST' && url.pathname === '/api/project/open') {
|
|
783
|
+
const { path: projectPath } = await readBody(req);
|
|
784
|
+
if (!projectPath) { jsonResponse(res, { error: true, message: 'Missing path' }, 400); return; }
|
|
785
|
+
try {
|
|
786
|
+
// Client marker for general workspace
|
|
787
|
+
const openingGeneral = projectPath === '__codemini_general__';
|
|
788
|
+
const resolved = openingGeneral ? GENERAL_PROJECT_DIR : path.resolve(projectPath);
|
|
789
|
+
const stat = await fs.stat(resolved);
|
|
790
|
+
if (!stat.isDirectory()) throw new Error('Not a directory');
|
|
791
|
+
let built;
|
|
792
|
+
if (openingGeneral) {
|
|
793
|
+
const all = await listSessions(1000, { includeEmpty: true });
|
|
794
|
+
const reusable = all.find((session) =>
|
|
795
|
+
isGeneralProjectDir(session.projectDir) &&
|
|
796
|
+
Number(session.messageCount || 0) === 0
|
|
797
|
+
);
|
|
798
|
+
built = reusable
|
|
799
|
+
? await buildRuntimeForSession({ sessionId: reusable.id, model: bridge.getState().model })
|
|
800
|
+
: await buildRuntimeForSession({ model: bridge.getState().model, projectDir: GENERAL_PROJECT_DIR });
|
|
801
|
+
} else {
|
|
802
|
+
process.chdir(resolved);
|
|
803
|
+
currentProjectDir = process.cwd();
|
|
804
|
+
built = await buildRuntimeForSession({
|
|
805
|
+
model: bridge.getState().model
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
const { runtime: newRuntime, session } = built;
|
|
809
|
+
await bridge.switchRuntime(newRuntime);
|
|
810
|
+
currentProjectDir = process.cwd();
|
|
811
|
+
jsonResponse(res, { ok: true, cwd: currentProjectDir, sessionId: session.id, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
812
|
+
} catch (err) {
|
|
813
|
+
jsonResponse(res, { error: true, message: err.message }, 400);
|
|
814
|
+
}
|
|
727
815
|
return;
|
|
728
816
|
}
|
|
729
817
|
if (req.method === 'POST' && url.pathname === '/api/project/browse') {
|
|
730
818
|
const { dir } = await readBody(req);
|
|
819
|
+
const roots = await listProjectRoots();
|
|
820
|
+
if (!dir && roots.length) {
|
|
821
|
+
jsonResponse(res, { path: '', roots, dirs: [] });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
731
824
|
const base = dir ? path.resolve(dir) : path.resolve('/');
|
|
732
825
|
try {
|
|
733
826
|
const entries = await fs.readdir(base, { withFileTypes: true });
|
|
@@ -743,36 +836,38 @@ async function main() {
|
|
|
743
836
|
await Promise.all(dirs.map(async (d) => {
|
|
744
837
|
try { await fs.access(path.join(d.path, '.git')); d.isGit = true; } catch {}
|
|
745
838
|
}));
|
|
746
|
-
jsonResponse(res, { path: base, dirs });
|
|
839
|
+
jsonResponse(res, { path: base, roots, dirs });
|
|
747
840
|
} catch (err) {
|
|
748
|
-
jsonResponse(res, { path: base, dirs: [], error: err.message });
|
|
841
|
+
jsonResponse(res, { path: base, roots, dirs: [], error: err.message });
|
|
749
842
|
}
|
|
750
843
|
return;
|
|
751
844
|
}
|
|
752
845
|
|
|
753
|
-
// ── Config management ──
|
|
754
|
-
if (req.method === 'GET' && url.pathname === '/api/config/status') {
|
|
755
|
-
const config = await loadConfig();
|
|
756
|
-
jsonResponse(res, getConfigStatus(config));
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
if (req.method === 'GET' && url.pathname === '/api/config') {
|
|
760
|
-
const config = await loadConfig();
|
|
761
|
-
jsonResponse(res, config);
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
846
|
+
// ── Config management ──
|
|
847
|
+
if (req.method === 'GET' && url.pathname === '/api/config/status') {
|
|
848
|
+
const config = await loadConfig();
|
|
849
|
+
jsonResponse(res, getConfigStatus(config));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (req.method === 'GET' && url.pathname === '/api/config') {
|
|
853
|
+
const config = await loadConfig();
|
|
854
|
+
jsonResponse(res, config);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
764
857
|
if (req.method === 'POST' && url.pathname === '/api/config/set') {
|
|
765
858
|
const { key, value } = await readBody(req);
|
|
766
859
|
if (!key) { jsonResponse(res, { error: true, message: 'Missing key' }, 400); return; }
|
|
767
|
-
try {
|
|
768
|
-
await setConfigValue(key, value);
|
|
769
|
-
const config = await loadConfig();
|
|
770
|
-
await bridge.reloadConfig(
|
|
860
|
+
try {
|
|
861
|
+
await setConfigValue(key, value);
|
|
862
|
+
const config = await loadConfig();
|
|
863
|
+
await bridge.reloadConfig(
|
|
864
|
+
key === 'model.name' ? { model: config.model?.name } : {}
|
|
865
|
+
);
|
|
771
866
|
bridge.broadcastRuntimeState();
|
|
772
|
-
jsonResponse(res, { ok: true, config });
|
|
773
|
-
} catch (err) {
|
|
774
|
-
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
775
|
-
}
|
|
867
|
+
jsonResponse(res, { ok: true, config });
|
|
868
|
+
} catch (err) {
|
|
869
|
+
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
870
|
+
}
|
|
776
871
|
return;
|
|
777
872
|
}
|
|
778
873
|
if (req.method === 'GET' && url.pathname.startsWith('/api/config/get/')) {
|
|
@@ -783,53 +878,54 @@ async function main() {
|
|
|
783
878
|
}
|
|
784
879
|
|
|
785
880
|
// ── Skills management ──
|
|
786
|
-
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
787
|
-
try {
|
|
788
|
-
const skills = await listSkillEntries({ scope: 'all' });
|
|
789
|
-
jsonResponse(res, skills);
|
|
790
|
-
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
791
|
-
return;
|
|
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;
|
|
792
887
|
}
|
|
793
888
|
if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
|
|
794
|
-
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
|
|
795
|
-
try {
|
|
796
|
-
const entries = await listSkillEntries({ scope: 'all' });
|
|
797
|
-
const skill = entries.find(s => s.name === name);
|
|
798
|
-
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
799
|
-
const content = await fs.readFile(skill.path, 'utf8');
|
|
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');
|
|
800
895
|
jsonResponse(res, { name: skill.name, content, scope: skill.scope });
|
|
801
896
|
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
802
897
|
return;
|
|
803
898
|
}
|
|
804
|
-
if (req.method === 'POST' && url.pathname === '/api/skills/create') {
|
|
805
|
-
const { name, description, content } = await readBody(req);
|
|
806
|
-
if (!name || !content) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
|
|
807
|
-
try {
|
|
808
|
-
const skillDir = path.join(
|
|
809
|
-
await fs.mkdir(skillDir, { recursive: true });
|
|
810
|
-
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
811
|
-
await fs.writeFile(skillFile, content, 'utf8');
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
config
|
|
820
|
-
config.skills
|
|
899
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/create') {
|
|
900
|
+
const { name, description, content } = await readBody(req);
|
|
901
|
+
if (!name || !content) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
|
|
902
|
+
try {
|
|
903
|
+
const skillDir = path.join(getProjectSkillsDir(currentProjectDir), name);
|
|
904
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
905
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
906
|
+
await fs.writeFile(skillFile, content, 'utf8');
|
|
907
|
+
await upsertProjectSkillMetadata(currentProjectDir, name, {
|
|
908
|
+
description: description || '',
|
|
909
|
+
mode: 'agent_requested',
|
|
910
|
+
triggers: [],
|
|
911
|
+
enabled: true,
|
|
912
|
+
priority: 50
|
|
913
|
+
});
|
|
914
|
+
const config = await loadConfig();
|
|
915
|
+
config.skills = config.skills || {};
|
|
916
|
+
config.skills.enabled = config.skills.enabled || {};
|
|
821
917
|
config.skills.enabled[name] = true;
|
|
822
918
|
await saveConfig(config);
|
|
823
919
|
jsonResponse(res, { ok: true, name });
|
|
824
920
|
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
825
921
|
return;
|
|
826
922
|
}
|
|
827
|
-
if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
|
|
828
|
-
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
|
|
829
|
-
const { content } = await readBody(req);
|
|
830
|
-
if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
|
|
831
|
-
try {
|
|
832
|
-
const entries = await listSkillEntries({ scope: 'all' });
|
|
923
|
+
if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
|
|
924
|
+
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
|
|
925
|
+
const { content } = await readBody(req);
|
|
926
|
+
if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
|
|
927
|
+
try {
|
|
928
|
+
const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
|
|
833
929
|
const skill = entries.find(s => s.name === name);
|
|
834
930
|
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
835
931
|
if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
|
|
@@ -847,25 +943,56 @@ async function main() {
|
|
|
847
943
|
if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
|
|
848
944
|
const dir = path.dirname(skill.path);
|
|
849
945
|
await fs.rm(dir, { recursive: true, force: true });
|
|
850
|
-
const registry = await readSkillRegistry();
|
|
851
|
-
registry.skills = (registry.skills || []).filter(s => s.name !== name);
|
|
852
|
-
await writeSkillRegistry(undefined, registry);
|
|
853
|
-
const
|
|
854
|
-
if (
|
|
855
|
-
|
|
946
|
+
const registry = await readSkillRegistry();
|
|
947
|
+
registry.skills = (registry.skills || []).filter(s => s.name !== name);
|
|
948
|
+
await writeSkillRegistry(undefined, registry);
|
|
949
|
+
const catalog = await readProjectSkillCatalog(currentProjectDir);
|
|
950
|
+
if (catalog.skills?.[name]) {
|
|
951
|
+
delete catalog.skills[name];
|
|
952
|
+
await writeProjectSkillCatalog(currentProjectDir, catalog);
|
|
953
|
+
}
|
|
954
|
+
const config = await loadConfig();
|
|
955
|
+
if (config.skills?.enabled) delete config.skills.enabled[name];
|
|
956
|
+
await saveConfig(config);
|
|
856
957
|
jsonResponse(res, { ok: true });
|
|
857
958
|
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
if (req.method === '
|
|
861
|
-
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/
|
|
862
|
-
const
|
|
863
|
-
try {
|
|
864
|
-
const entries = await listSkillEntries({ scope: 'all' });
|
|
865
|
-
const skill = entries.find(s => s.name === name);
|
|
866
|
-
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
867
|
-
|
|
868
|
-
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/metadata')) {
|
|
962
|
+
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/metadata'.length));
|
|
963
|
+
const body = await readBody(req);
|
|
964
|
+
try {
|
|
965
|
+
const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
|
|
966
|
+
const skill = entries.find(s => s.name === name);
|
|
967
|
+
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
968
|
+
const metadata = await upsertProjectSkillMetadata(currentProjectDir, name, body || {});
|
|
969
|
+
if (skill.scope !== 'builtin' && body?.enabled !== undefined) {
|
|
970
|
+
const config = await loadConfig();
|
|
971
|
+
config.skills = config.skills || {};
|
|
972
|
+
config.skills.enabled = config.skills.enabled || {};
|
|
973
|
+
config.skills.enabled[name] = body.enabled !== false;
|
|
974
|
+
await saveConfig(config);
|
|
975
|
+
const registry = await readSkillRegistry();
|
|
976
|
+
const idx = registry.skills.findIndex(s => s.name === name);
|
|
977
|
+
if (idx !== -1) { registry.skills[idx].enabled = body.enabled !== false; await writeSkillRegistry(undefined, registry); }
|
|
978
|
+
}
|
|
979
|
+
jsonResponse(res, { ok: true, name, metadata });
|
|
980
|
+
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
|
|
984
|
+
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
|
|
985
|
+
const { enabled } = await readBody(req);
|
|
986
|
+
try {
|
|
987
|
+
const entries = await listSkillEntries({ scope: 'all', cwd: currentProjectDir });
|
|
988
|
+
const skill = entries.find(s => s.name === name);
|
|
989
|
+
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
990
|
+
if (skill.scope === 'builtin') {
|
|
991
|
+
const metadata = await upsertProjectSkillMetadata(currentProjectDir, name, { enabled });
|
|
992
|
+
jsonResponse(res, { ok: true, name, metadata });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const config = await loadConfig();
|
|
869
996
|
config.skills = config.skills || {};
|
|
870
997
|
config.skills.enabled = config.skills.enabled || {};
|
|
871
998
|
config.skills.enabled[name] = !!enabled;
|