codemini-cli 0.5.8 → 0.5.10
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 +354 -225
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CCcxtQK_.js → highlighted-body-OFNGDK62-7HL7yft8.js} +1 -1
- package/codemini-web/dist/assets/{index-Cy4HN-FS.js → index-BK75hMb2.js} +95 -93
- package/codemini-web/dist/assets/index-BSdIdn3L.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +1 -0
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +547 -546
- package/codemini-web/server.js +318 -233
- package/package.json +67 -67
- package/skills/brainstorm/SKILL.md +8 -3
- package/skills/codemini.skills.json +40 -0
- package/src/commands/skill.js +16 -5
- package/src/core/ast.js +30 -15
- package/src/core/chat-runtime.js +88 -16
- package/src/core/command-loader.js +120 -25
- package/src/core/command-policy.js +34 -10
- package/src/core/config-store.js +14 -1
- package/src/core/project-index.js +21 -2
- package/src/core/project-instructions.js +98 -0
- package/src/core/shell.js +79 -73
- package/src/core/system-prompt-composer.js +10 -0
- package/src/core/tools.js +114 -65
- package/codemini-web/dist/assets/index-CMISAOFr.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-BWFzYc7A.js +0 -1
package/codemini-web/server.js
CHANGED
|
@@ -9,58 +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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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);
|
|
35
39
|
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (const candidate of candidates) {
|
|
45
|
-
if (!candidate.path) continue;
|
|
46
|
-
const resolved = path.resolve(candidate.path);
|
|
47
|
-
if (seen.has(resolved)) continue;
|
|
48
|
-
try {
|
|
49
|
-
const stat = await fs.stat(resolved);
|
|
50
|
-
if (!stat.isDirectory()) continue;
|
|
51
|
-
seen.add(resolved);
|
|
52
|
-
roots.push({ name: candidate.name, path: resolved, isGit: false, isDrive: false });
|
|
53
|
-
} catch {}
|
|
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: {} };
|
|
54
50
|
}
|
|
55
|
-
return roots;
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
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
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
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));
|
|
64
113
|
const CLIENT_SOURCE_DIR = path.join(__dirname, 'client');
|
|
65
114
|
let CLIENT_DIR = CLIENT_SOURCE_DIR;
|
|
66
115
|
try {
|
|
@@ -69,33 +118,33 @@ try {
|
|
|
69
118
|
if (stat.isDirectory()) CLIENT_DIR = distDir;
|
|
70
119
|
} catch {}
|
|
71
120
|
|
|
72
|
-
const MIME_TYPES = {
|
|
121
|
+
const MIME_TYPES = {
|
|
73
122
|
'.html': 'text/html; charset=utf-8',
|
|
74
123
|
'.css': 'text/css; charset=utf-8',
|
|
75
124
|
'.js': 'text/javascript; charset=utf-8',
|
|
76
125
|
'.json': 'application/json',
|
|
77
126
|
'.svg': 'image/svg+xml',
|
|
78
127
|
'.png': 'image/png',
|
|
79
|
-
'.ico': 'image/x-icon'
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const DEFAULT_GATEWAY_BASE_URL = 'http://127.0.0.1:8000/v1';
|
|
83
|
-
|
|
84
|
-
function normalizeBaseUrl(value) {
|
|
85
|
-
return String(value || '').trim().replace(/\/+$/, '');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function getConfigStatus(config) {
|
|
89
|
-
const baseUrl = normalizeBaseUrl(config?.gateway?.base_url);
|
|
90
|
-
const apiKey = String(config?.gateway?.api_key || '').trim();
|
|
91
|
-
const setupRequired = !baseUrl || (baseUrl === DEFAULT_GATEWAY_BASE_URL && !apiKey);
|
|
92
|
-
return {
|
|
93
|
-
setupRequired,
|
|
94
|
-
baseUrl,
|
|
95
|
-
hasApiKey: !!apiKey,
|
|
96
|
-
reason: setupRequired ? 'gateway_not_configured' : ''
|
|
97
|
-
};
|
|
98
|
-
}
|
|
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
|
+
}
|
|
99
148
|
|
|
100
149
|
function parseArgs(argv) {
|
|
101
150
|
const parsed = { port: 3210, session: undefined, model: undefined, project: undefined, open: true };
|
|
@@ -205,10 +254,14 @@ function collectSessionPathHints(session) {
|
|
|
205
254
|
return hints;
|
|
206
255
|
}
|
|
207
256
|
|
|
208
|
-
async function existingDirectoryForHint(rawHint) {
|
|
209
|
-
let candidate = normalizeProjectPath(rawHint);
|
|
210
|
-
if (!candidate) return '';
|
|
211
|
-
|
|
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, '');
|
|
212
265
|
for (let i = 0; i < 8 && candidate && candidate !== path.dirname(candidate); i += 1) {
|
|
213
266
|
try {
|
|
214
267
|
const stat = await fs.stat(candidate);
|
|
@@ -295,7 +348,7 @@ async function buildRuntimeForSession({ sessionId, model, projectDir }) {
|
|
|
295
348
|
model: model || config.model?.name,
|
|
296
349
|
systemPrompt
|
|
297
350
|
});
|
|
298
|
-
return { runtime, config, session, cwd: process.cwd(), isGeneral: isGeneralProjectDir(process.cwd()) };
|
|
351
|
+
return { runtime, config, session, cwd: process.cwd(), isGeneral: isGeneralProjectDir(process.cwd()) };
|
|
299
352
|
}
|
|
300
353
|
|
|
301
354
|
async function main() {
|
|
@@ -352,25 +405,25 @@ async function main() {
|
|
|
352
405
|
return;
|
|
353
406
|
}
|
|
354
407
|
|
|
355
|
-
// ── Submit / Abort / Approval ──
|
|
356
|
-
if (req.method === 'POST' && url.pathname === '/api/submit') {
|
|
357
|
-
const { line, readOnlyCodeWiki } = await readBody(req);
|
|
358
|
-
if (!line || typeof line !== 'string') { jsonResponse(res, { error: true, message: 'Missing "line" field' }, 400); return; }
|
|
359
|
-
const currentConfig = await loadConfig();
|
|
360
|
-
const configStatus = getConfigStatus(currentConfig);
|
|
361
|
-
if (configStatus.setupRequired) {
|
|
362
|
-
jsonResponse(res, {
|
|
363
|
-
error: true,
|
|
364
|
-
code: 'CONFIG_REQUIRED',
|
|
365
|
-
message: 'Gateway is not configured. Open Settings and set the API Base URL and API Key.',
|
|
366
|
-
configStatus
|
|
367
|
-
}, 409);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
const result = bridge.handleSubmit(line, { readOnlyCodeWiki: readOnlyCodeWiki === true });
|
|
371
|
-
jsonResponse(res, result);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
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
|
+
}
|
|
374
427
|
if (req.method === 'POST' && url.pathname === '/api/abort') {
|
|
375
428
|
bridge.handleAbort();
|
|
376
429
|
jsonResponse(res, { ok: true });
|
|
@@ -402,7 +455,7 @@ async function main() {
|
|
|
402
455
|
try {
|
|
403
456
|
latest = execSync('npm view codemini-cli version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
404
457
|
} catch {}
|
|
405
|
-
jsonResponse(res, { current: VERSION, latest });
|
|
458
|
+
jsonResponse(res, { current: VERSION, latest });
|
|
406
459
|
return;
|
|
407
460
|
}
|
|
408
461
|
if (req.method === 'POST' && url.pathname === '/api/update') {
|
|
@@ -417,7 +470,7 @@ async function main() {
|
|
|
417
470
|
|
|
418
471
|
// ── Runtime state ──
|
|
419
472
|
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
420
|
-
jsonResponse(res, { ...bridge.getState(), cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
473
|
+
jsonResponse(res, { ...bridge.getState(), cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
421
474
|
return;
|
|
422
475
|
}
|
|
423
476
|
if (req.method === 'GET' && url.pathname === '/api/completions') {
|
|
@@ -523,25 +576,25 @@ async function main() {
|
|
|
523
576
|
return;
|
|
524
577
|
}
|
|
525
578
|
|
|
526
|
-
if (req.method === 'POST' && url.pathname === '/api/codewiki/ask') {
|
|
527
|
-
const { question, reportFile } = await readBody(req);
|
|
528
|
-
if (!question || typeof question !== 'string') {
|
|
529
|
-
jsonResponse(res, { error: true, message: 'Missing "question" field' }, 400);
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const currentConfig = await loadConfig();
|
|
533
|
-
const configStatus = getConfigStatus(currentConfig);
|
|
534
|
-
if (configStatus.setupRequired) {
|
|
535
|
-
jsonResponse(res, {
|
|
536
|
-
error: true,
|
|
537
|
-
code: 'CONFIG_REQUIRED',
|
|
538
|
-
message: 'Gateway is not configured. Open Settings and set the API Base URL and API Key.',
|
|
539
|
-
configStatus
|
|
540
|
-
}, 409);
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
const selectedReport = isCodeWikiReportFile(reportFile) ? reportFile : '';
|
|
544
|
-
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()) {
|
|
545
598
|
jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
|
|
546
599
|
return;
|
|
547
600
|
}
|
|
@@ -573,7 +626,7 @@ async function main() {
|
|
|
573
626
|
// ── Session management ──
|
|
574
627
|
if (req.method === 'GET' && url.pathname === '/api/sessions') {
|
|
575
628
|
const sessions = await listSessions(1000);
|
|
576
|
-
const enriched = sessions.map(s => ({ ...s, isGeneral: isGeneralProjectDir(s.projectDir) }));
|
|
629
|
+
const enriched = sessions.map(s => ({ ...s, isGeneral: isGeneralProjectDir(s.projectDir) }));
|
|
577
630
|
jsonResponse(res, enriched);
|
|
578
631
|
return;
|
|
579
632
|
}
|
|
@@ -586,7 +639,7 @@ async function main() {
|
|
|
586
639
|
reused: true,
|
|
587
640
|
sessionId: bridge.getSessionId(),
|
|
588
641
|
cwd: currentProjectDir,
|
|
589
|
-
isGeneral: isGeneralProjectDir(currentProjectDir)
|
|
642
|
+
isGeneral: isGeneralProjectDir(currentProjectDir)
|
|
590
643
|
});
|
|
591
644
|
return;
|
|
592
645
|
}
|
|
@@ -596,7 +649,7 @@ async function main() {
|
|
|
596
649
|
});
|
|
597
650
|
await bridge.switchRuntime(newRuntime);
|
|
598
651
|
currentProjectDir = process.cwd();
|
|
599
|
-
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) });
|
|
600
653
|
} catch (err) {
|
|
601
654
|
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
602
655
|
}
|
|
@@ -612,7 +665,7 @@ async function main() {
|
|
|
612
665
|
});
|
|
613
666
|
await bridge.switchRuntime(newRuntime);
|
|
614
667
|
currentProjectDir = process.cwd();
|
|
615
|
-
jsonResponse(res, { ok: true, sessionId, cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
668
|
+
jsonResponse(res, { ok: true, sessionId, cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
616
669
|
} catch (err) {
|
|
617
670
|
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
618
671
|
}
|
|
@@ -642,7 +695,7 @@ async function main() {
|
|
|
642
695
|
nextSessionId = built.session.id;
|
|
643
696
|
cwd = currentProjectDir;
|
|
644
697
|
}
|
|
645
|
-
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) });
|
|
646
699
|
} catch (err) {
|
|
647
700
|
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
648
701
|
}
|
|
@@ -651,7 +704,7 @@ async function main() {
|
|
|
651
704
|
|
|
652
705
|
// ── Project management ──
|
|
653
706
|
if (req.method === 'GET' && url.pathname === '/api/project') {
|
|
654
|
-
jsonResponse(res, { cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
707
|
+
jsonResponse(res, { cwd: currentProjectDir, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
655
708
|
return;
|
|
656
709
|
}
|
|
657
710
|
if (req.method === 'GET' && url.pathname === '/api/git') {
|
|
@@ -726,53 +779,53 @@ async function main() {
|
|
|
726
779
|
jsonResponse(res, result);
|
|
727
780
|
return;
|
|
728
781
|
}
|
|
729
|
-
if (req.method === 'POST' && url.pathname === '/api/project/open') {
|
|
730
|
-
const { path: projectPath } = await readBody(req);
|
|
731
|
-
if (!projectPath) { jsonResponse(res, { error: true, message: 'Missing path' }, 400); return; }
|
|
732
|
-
try {
|
|
733
|
-
// Client marker for general workspace
|
|
734
|
-
const openingGeneral = projectPath === '__codemini_general__';
|
|
735
|
-
const resolved = openingGeneral ? GENERAL_PROJECT_DIR : path.resolve(projectPath);
|
|
736
|
-
const stat = await fs.stat(resolved);
|
|
737
|
-
if (!stat.isDirectory()) throw new Error('Not a directory');
|
|
738
|
-
let built;
|
|
739
|
-
if (openingGeneral) {
|
|
740
|
-
const all = await listSessions(1000, { includeEmpty: true });
|
|
741
|
-
const reusable = all.find((session) =>
|
|
742
|
-
isGeneralProjectDir(session.projectDir) &&
|
|
743
|
-
Number(session.messageCount || 0) === 0
|
|
744
|
-
);
|
|
745
|
-
built = reusable
|
|
746
|
-
? await buildRuntimeForSession({ sessionId: reusable.id, model: bridge.getState().model })
|
|
747
|
-
: await buildRuntimeForSession({ model: bridge.getState().model, projectDir: GENERAL_PROJECT_DIR });
|
|
748
|
-
} else {
|
|
749
|
-
process.chdir(resolved);
|
|
750
|
-
currentProjectDir = process.cwd();
|
|
751
|
-
built = await buildRuntimeForSession({
|
|
752
|
-
model: bridge.getState().model
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
const { runtime: newRuntime, session } = built;
|
|
756
|
-
await bridge.switchRuntime(newRuntime);
|
|
757
|
-
currentProjectDir = process.cwd();
|
|
758
|
-
jsonResponse(res, { ok: true, cwd: currentProjectDir, sessionId: session.id, isGeneral: isGeneralProjectDir(currentProjectDir) });
|
|
759
|
-
} catch (err) {
|
|
760
|
-
jsonResponse(res, { error: true, message: err.message }, 400);
|
|
761
|
-
}
|
|
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
|
+
}
|
|
762
815
|
return;
|
|
763
|
-
}
|
|
764
|
-
if (req.method === 'POST' && url.pathname === '/api/project/browse') {
|
|
765
|
-
const { dir } = await readBody(req);
|
|
816
|
+
}
|
|
817
|
+
if (req.method === 'POST' && url.pathname === '/api/project/browse') {
|
|
818
|
+
const { dir } = await readBody(req);
|
|
766
819
|
const roots = await listProjectRoots();
|
|
767
820
|
if (!dir && roots.length) {
|
|
768
|
-
jsonResponse(res, { path: '', roots, dirs: [] });
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
const base = dir ? path.resolve(dir) : path.resolve('/');
|
|
772
|
-
try {
|
|
773
|
-
const entries = await fs.readdir(base, { withFileTypes: true });
|
|
774
|
-
const dirs = entries
|
|
775
|
-
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
821
|
+
jsonResponse(res, { path: '', roots, dirs: [] });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const base = dir ? path.resolve(dir) : path.resolve('/');
|
|
825
|
+
try {
|
|
826
|
+
const entries = await fs.readdir(base, { withFileTypes: true });
|
|
827
|
+
const dirs = entries
|
|
828
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
776
829
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
777
830
|
.map(e => ({
|
|
778
831
|
name: e.name,
|
|
@@ -781,40 +834,40 @@ async function main() {
|
|
|
781
834
|
}));
|
|
782
835
|
// Check for .git directories asynchronously
|
|
783
836
|
await Promise.all(dirs.map(async (d) => {
|
|
784
|
-
try { await fs.access(path.join(d.path, '.git')); d.isGit = true; } catch {}
|
|
785
|
-
}));
|
|
786
|
-
jsonResponse(res, { path: base, roots, dirs });
|
|
787
|
-
} catch (err) {
|
|
788
|
-
jsonResponse(res, { path: base, roots, dirs: [], error: err.message });
|
|
789
|
-
}
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
837
|
+
try { await fs.access(path.join(d.path, '.git')); d.isGit = true; } catch {}
|
|
838
|
+
}));
|
|
839
|
+
jsonResponse(res, { path: base, roots, dirs });
|
|
840
|
+
} catch (err) {
|
|
841
|
+
jsonResponse(res, { path: base, roots, dirs: [], error: err.message });
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
792
845
|
|
|
793
|
-
// ── Config management ──
|
|
794
|
-
if (req.method === 'GET' && url.pathname === '/api/config/status') {
|
|
795
|
-
const config = await loadConfig();
|
|
796
|
-
jsonResponse(res, getConfigStatus(config));
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
if (req.method === 'GET' && url.pathname === '/api/config') {
|
|
800
|
-
const config = await loadConfig();
|
|
801
|
-
jsonResponse(res, config);
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
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
|
+
}
|
|
804
857
|
if (req.method === 'POST' && url.pathname === '/api/config/set') {
|
|
805
858
|
const { key, value } = await readBody(req);
|
|
806
859
|
if (!key) { jsonResponse(res, { error: true, message: 'Missing key' }, 400); return; }
|
|
807
|
-
try {
|
|
808
|
-
await setConfigValue(key, value);
|
|
809
|
-
const config = await loadConfig();
|
|
860
|
+
try {
|
|
861
|
+
await setConfigValue(key, value);
|
|
862
|
+
const config = await loadConfig();
|
|
810
863
|
await bridge.reloadConfig(
|
|
811
864
|
key === 'model.name' ? { model: config.model?.name } : {}
|
|
812
865
|
);
|
|
813
866
|
bridge.broadcastRuntimeState();
|
|
814
|
-
jsonResponse(res, { ok: true, config });
|
|
815
|
-
} catch (err) {
|
|
816
|
-
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
817
|
-
}
|
|
867
|
+
jsonResponse(res, { ok: true, config });
|
|
868
|
+
} catch (err) {
|
|
869
|
+
jsonResponse(res, { error: true, message: err.message }, 500);
|
|
870
|
+
}
|
|
818
871
|
return;
|
|
819
872
|
}
|
|
820
873
|
if (req.method === 'GET' && url.pathname.startsWith('/api/config/get/')) {
|
|
@@ -825,53 +878,54 @@ async function main() {
|
|
|
825
878
|
}
|
|
826
879
|
|
|
827
880
|
// ── Skills management ──
|
|
828
|
-
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
829
|
-
try {
|
|
830
|
-
const skills = await listSkillEntries({ scope: 'all' });
|
|
831
|
-
jsonResponse(res, skills);
|
|
832
|
-
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
833
|
-
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;
|
|
834
887
|
}
|
|
835
888
|
if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
|
|
836
|
-
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
|
|
837
|
-
try {
|
|
838
|
-
const entries = await listSkillEntries({ scope: 'all' });
|
|
839
|
-
const skill = entries.find(s => s.name === name);
|
|
840
|
-
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
841
|
-
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');
|
|
842
895
|
jsonResponse(res, { name: skill.name, content, scope: skill.scope });
|
|
843
896
|
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
844
897
|
return;
|
|
845
898
|
}
|
|
846
|
-
if (req.method === 'POST' && url.pathname === '/api/skills/create') {
|
|
847
|
-
const { name, description, content } = await readBody(req);
|
|
848
|
-
if (!name || !content) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
|
|
849
|
-
try {
|
|
850
|
-
const skillDir = path.join(
|
|
851
|
-
await fs.mkdir(skillDir, { recursive: true });
|
|
852
|
-
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
853
|
-
await fs.writeFile(skillFile, content, 'utf8');
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
config
|
|
862
|
-
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 || {};
|
|
863
917
|
config.skills.enabled[name] = true;
|
|
864
918
|
await saveConfig(config);
|
|
865
919
|
jsonResponse(res, { ok: true, name });
|
|
866
920
|
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
867
921
|
return;
|
|
868
922
|
}
|
|
869
|
-
if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
|
|
870
|
-
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
|
|
871
|
-
const { content } = await readBody(req);
|
|
872
|
-
if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
|
|
873
|
-
try {
|
|
874
|
-
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 });
|
|
875
929
|
const skill = entries.find(s => s.name === name);
|
|
876
930
|
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
877
931
|
if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
|
|
@@ -889,25 +943,56 @@ async function main() {
|
|
|
889
943
|
if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
|
|
890
944
|
const dir = path.dirname(skill.path);
|
|
891
945
|
await fs.rm(dir, { recursive: true, force: true });
|
|
892
|
-
const registry = await readSkillRegistry();
|
|
893
|
-
registry.skills = (registry.skills || []).filter(s => s.name !== name);
|
|
894
|
-
await writeSkillRegistry(undefined, registry);
|
|
895
|
-
const
|
|
896
|
-
if (
|
|
897
|
-
|
|
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);
|
|
898
957
|
jsonResponse(res, { ok: true });
|
|
899
958
|
} catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (req.method === '
|
|
903
|
-
const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/
|
|
904
|
-
const
|
|
905
|
-
try {
|
|
906
|
-
const entries = await listSkillEntries({ scope: 'all' });
|
|
907
|
-
const skill = entries.find(s => s.name === name);
|
|
908
|
-
if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
|
|
909
|
-
|
|
910
|
-
|
|
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();
|
|
911
996
|
config.skills = config.skills || {};
|
|
912
997
|
config.skills.enabled = config.skills.enabled || {};
|
|
913
998
|
config.skills.enabled[name] = !!enabled;
|