codemini-cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/OPERATIONS.md +202 -0
  3. package/README.md +138 -0
  4. package/bin/coder.js +7 -0
  5. package/deployment.md +205 -0
  6. package/package.json +54 -0
  7. package/skills/brainstorming-lite/SKILL.md +37 -0
  8. package/skills/executing-plan-lite/SKILL.md +41 -0
  9. package/skills/superpowers-lite/SKILL.md +44 -0
  10. package/souls/anime.md +3 -0
  11. package/souls/default.md +3 -0
  12. package/souls/playful.md +3 -0
  13. package/souls/professional.md +3 -0
  14. package/src/cli.js +62 -0
  15. package/src/commands/chat.js +106 -0
  16. package/src/commands/config.js +61 -0
  17. package/src/commands/doctor.js +87 -0
  18. package/src/commands/run.js +64 -0
  19. package/src/commands/skill.js +264 -0
  20. package/src/core/agent-loop.js +281 -0
  21. package/src/core/chat-runtime.js +2075 -0
  22. package/src/core/checkpoint-store.js +66 -0
  23. package/src/core/command-loader.js +201 -0
  24. package/src/core/command-policy.js +71 -0
  25. package/src/core/config-store.js +196 -0
  26. package/src/core/context-compact.js +90 -0
  27. package/src/core/default-system-prompt.js +5 -0
  28. package/src/core/fs-utils.js +16 -0
  29. package/src/core/input-history-store.js +48 -0
  30. package/src/core/input-parser.js +15 -0
  31. package/src/core/paths.js +109 -0
  32. package/src/core/provider/openai-compatible.js +228 -0
  33. package/src/core/session-store.js +178 -0
  34. package/src/core/shell-profile.js +122 -0
  35. package/src/core/shell.js +71 -0
  36. package/src/core/skill-registry.js +55 -0
  37. package/src/core/soul.js +55 -0
  38. package/src/core/task-store.js +116 -0
  39. package/src/core/tools.js +237 -0
  40. package/src/tui/chat-app.js +2007 -0
  41. package/src/tui/input-escape.js +21 -0
@@ -0,0 +1,264 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawn } from 'node:child_process';
5
+ import { copyRecursive } from '../core/fs-utils.js';
6
+ import { getSkillsDir } from '../core/paths.js';
7
+ import {
8
+ computeFileSha256,
9
+ readSkillRegistry,
10
+ upsertSkillRegistryEntry,
11
+ writeSkillRegistry
12
+ } from '../core/skill-registry.js';
13
+
14
+ async function listSkillEntries() {
15
+ const registry = await readSkillRegistry();
16
+ const byName = new Map((registry.skills || []).map((s) => [s.name, s]));
17
+ await fs.mkdir(getSkillsDir(), { recursive: true });
18
+ const entries = await fs.readdir(getSkillsDir(), { withFileTypes: true });
19
+ const names = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
20
+ return names.map((name) => byName.get(name) || { name, version: 'unknown', enabled: true });
21
+ }
22
+
23
+ async function readSkillMeta(name) {
24
+ const dir = path.join(getSkillsDir(), name);
25
+ const manifestPath = path.join(dir, 'manifest.json');
26
+ let manifest = null;
27
+ try {
28
+ manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
29
+ } catch {
30
+ manifest = null;
31
+ }
32
+ const entryFile = manifest?.entry || 'SKILL.md';
33
+ const skillPath = path.join(dir, entryFile);
34
+ try {
35
+ const content = await fs.readFile(skillPath, 'utf8');
36
+ const firstLines = content.split('\n').slice(0, 20).join('\n');
37
+ return { exists: true, path: skillPath, preview: firstLines, manifest };
38
+ } catch {
39
+ return { exists: false, path: skillPath, preview: '', manifest };
40
+ }
41
+ }
42
+
43
+ async function runTarExtract(tgzPath, destDir) {
44
+ await new Promise((resolve, reject) => {
45
+ const child = spawn('tar', ['-xzf', tgzPath, '-C', destDir], {
46
+ stdio: ['ignore', 'pipe', 'pipe']
47
+ });
48
+ let stderr = '';
49
+ child.stderr.on('data', (chunk) => {
50
+ stderr += chunk.toString();
51
+ });
52
+ child.on('error', reject);
53
+ child.on('close', (code) => {
54
+ if (code !== 0) {
55
+ reject(new Error(`tar extract failed: ${stderr || `exit ${code}`}`));
56
+ return;
57
+ }
58
+ resolve();
59
+ });
60
+ });
61
+ }
62
+
63
+ async function readManifestSafe(skillRoot) {
64
+ const p = path.join(skillRoot, 'manifest.json');
65
+ try {
66
+ const raw = await fs.readFile(p, 'utf8');
67
+ return JSON.parse(raw);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ async function resolveSkillSourceDir(sourcePath) {
74
+ const absSrc = path.resolve(sourcePath);
75
+ const srcStat = await fs.stat(absSrc);
76
+
77
+ if (srcStat.isFile() && absSrc.endsWith('.tgz')) {
78
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'codemini-skill-'));
79
+ await runTarExtract(absSrc, tmp);
80
+ const candidates = ['package', ...((await fs.readdir(tmp, { withFileTypes: true }))
81
+ .filter((d) => d.isDirectory())
82
+ .map((d) => d.name))];
83
+ for (const c of candidates) {
84
+ const dir = path.join(tmp, c);
85
+ try {
86
+ await fs.access(path.join(dir, 'SKILL.md'));
87
+ return { dir, cleanupDir: tmp };
88
+ } catch {
89
+ continue;
90
+ }
91
+ }
92
+ throw new Error('No SKILL.md found in tgz package');
93
+ }
94
+
95
+ if (srcStat.isFile() && path.basename(absSrc) === 'SKILL.md') {
96
+ return { dir: path.dirname(absSrc), cleanupDir: null };
97
+ }
98
+
99
+ if (srcStat.isDirectory()) {
100
+ await fs.access(path.join(absSrc, 'SKILL.md'));
101
+ return { dir: absSrc, cleanupDir: null };
102
+ }
103
+
104
+ throw new Error('skill install supports <skill-dir>, <SKILL.md>, or <skill.tgz>');
105
+ }
106
+
107
+ async function installSkill(sourcePath) {
108
+ const resolved = await resolveSkillSourceDir(sourcePath);
109
+ const manifest = await readManifestSafe(resolved.dir);
110
+ const folderName = manifest?.name || path.basename(resolved.dir);
111
+ const targetDir = path.join(getSkillsDir(), folderName);
112
+ await fs.rm(targetDir, { recursive: true, force: true });
113
+ await copyRecursive(resolved.dir, targetDir);
114
+
115
+ const entryFile = manifest?.entry || 'SKILL.md';
116
+ const entryPath = path.join(targetDir, entryFile);
117
+ await fs.access(entryPath);
118
+
119
+ const hash = await computeFileSha256(entryPath);
120
+ await upsertSkillRegistryEntry(undefined, {
121
+ name: folderName,
122
+ version: manifest?.version || '0.0.0',
123
+ description: manifest?.description || '',
124
+ enabled: true,
125
+ source: sourcePath,
126
+ entryFile,
127
+ sha256: hash,
128
+ installedAt: new Date().toISOString()
129
+ });
130
+
131
+ if (resolved.cleanupDir) {
132
+ await fs.rm(resolved.cleanupDir, { recursive: true, force: true });
133
+ }
134
+
135
+ return folderName;
136
+ }
137
+
138
+ async function setEnabled(name, enabled) {
139
+ const registry = await readSkillRegistry();
140
+ const idx = registry.skills.findIndex((s) => s.name === name);
141
+ if (idx === -1) {
142
+ throw new Error(`skill not found: ${name}`);
143
+ }
144
+ registry.skills[idx].enabled = enabled;
145
+ await writeSkillRegistry(undefined, registry);
146
+ }
147
+
148
+ async function reindexSkills() {
149
+ await fs.mkdir(getSkillsDir(), { recursive: true });
150
+ const entries = await fs.readdir(getSkillsDir(), { withFileTypes: true });
151
+ const registry = await readSkillRegistry();
152
+ const byName = new Map((registry.skills || []).map((s) => [s.name, s]));
153
+ const rebuilt = [];
154
+
155
+ for (const entry of entries) {
156
+ if (!entry.isDirectory()) continue;
157
+ const name = entry.name;
158
+ const dir = path.join(getSkillsDir(), name);
159
+ const manifest = await readManifestSafe(dir);
160
+ const entryFile = manifest?.entry || 'SKILL.md';
161
+ const entryPath = path.join(dir, entryFile);
162
+ try {
163
+ await fs.access(entryPath);
164
+ } catch {
165
+ continue;
166
+ }
167
+ const hash = await computeFileSha256(entryPath);
168
+ const prior = byName.get(name);
169
+ rebuilt.push({
170
+ name: manifest?.name || name,
171
+ version: manifest?.version || prior?.version || '0.0.0',
172
+ description: manifest?.description || prior?.description || '',
173
+ enabled: prior?.enabled !== false,
174
+ source: prior?.source || 'reindex',
175
+ entryFile,
176
+ sha256: hash,
177
+ installedAt: prior?.installedAt || new Date().toISOString()
178
+ });
179
+ }
180
+
181
+ await writeSkillRegistry(undefined, {
182
+ version: 1,
183
+ skills: rebuilt
184
+ });
185
+
186
+ return rebuilt.length;
187
+ }
188
+
189
+ function usage() {
190
+ console.log(`Usage:
191
+ codemini skill list
192
+ codemini skill install <path>
193
+ codemini skill enable <name>
194
+ codemini skill disable <name>
195
+ codemini skill inspect <name>
196
+ codemini skill reindex`);
197
+ }
198
+
199
+ export async function handleSkill(args) {
200
+ const [sub, ...rest] = args;
201
+ if (!sub) {
202
+ usage();
203
+ return;
204
+ }
205
+
206
+ if (sub === 'list') {
207
+ const entries = await listSkillEntries();
208
+ if (entries.length === 0) {
209
+ console.log('No installed skills');
210
+ return;
211
+ }
212
+ for (const item of entries) {
213
+ const state = item.enabled !== false ? 'enabled' : 'disabled';
214
+ console.log(`${item.name}@${item.version || '0.0.0'} (${state})`);
215
+ }
216
+ return;
217
+ }
218
+
219
+ if (sub === 'install') {
220
+ const sourcePath = rest[0];
221
+ if (!sourcePath) {
222
+ throw new Error('skill install requires <path>');
223
+ }
224
+ const installedName = await installSkill(sourcePath);
225
+ await setEnabled(installedName, true);
226
+ console.log(`Installed skill: ${installedName}`);
227
+ return;
228
+ }
229
+
230
+ if (sub === 'enable' || sub === 'disable') {
231
+ const name = rest[0];
232
+ if (!name) {
233
+ throw new Error(`skill ${sub} requires <name>`);
234
+ }
235
+ await setEnabled(name, sub === 'enable');
236
+ console.log(`${sub}d skill: ${name}`);
237
+ return;
238
+ }
239
+
240
+ if (sub === 'inspect') {
241
+ const name = rest[0];
242
+ if (!name) {
243
+ throw new Error('skill inspect requires <name>');
244
+ }
245
+ const meta = await readSkillMeta(name);
246
+ if (!meta.exists) {
247
+ throw new Error(`skill not found: ${name}`);
248
+ }
249
+ if (meta.manifest) {
250
+ console.log(`Manifest: ${JSON.stringify(meta.manifest, null, 2)}\n`);
251
+ }
252
+ console.log(`Path: ${meta.path}\n`);
253
+ console.log(meta.preview);
254
+ return;
255
+ }
256
+
257
+ if (sub === 'reindex') {
258
+ const count = await reindexSkills();
259
+ console.log(`Reindexed skills: ${count}`);
260
+ return;
261
+ }
262
+
263
+ usage();
264
+ }
@@ -0,0 +1,281 @@
1
+ function safeJsonParse(raw) {
2
+ if (!raw || typeof raw !== 'string') return {};
3
+ try {
4
+ return JSON.parse(raw);
5
+ } catch {
6
+ return {};
7
+ }
8
+ }
9
+
10
+ function clipToolResult(result, maxChars = 12000) {
11
+ const raw = typeof result === 'string' ? result : JSON.stringify(result);
12
+ if (!maxChars || raw.length <= maxChars) return raw;
13
+ return `${raw.slice(0, maxChars)}\n... [tool result truncated ${raw.length - maxChars} chars]`;
14
+ }
15
+
16
+ function summarizeToolResult(result) {
17
+ if (result === null || result === undefined) return 'no output';
18
+ if (typeof result === 'string') {
19
+ const oneLine = result.replace(/\s+/g, ' ').trim();
20
+ return oneLine.length > 90 ? `${oneLine.slice(0, 87)}...` : oneLine || 'empty string';
21
+ }
22
+ if (typeof result === 'object') {
23
+ const obj = result;
24
+ if (Array.isArray(obj)) return `array(${obj.length})`;
25
+ if ('path' in obj && 'action' in obj) {
26
+ const p = String(obj.path || '');
27
+ const action = String(obj.action || 'write');
28
+ const line = Number(obj.changed_line || 1);
29
+ const preview = String(obj.diff_preview || '')
30
+ .split('\n')
31
+ .slice(0, 3)
32
+ .join('\n');
33
+ return `${action} ${p} @L${line}${preview ? `\n${preview}` : ''}`;
34
+ }
35
+ if ('path' in obj && 'phase' in obj) {
36
+ const phase = String(obj.phase || '');
37
+ const p = String(obj.path || '');
38
+ const total = Number(obj.total_lines);
39
+ const start =
40
+ Number(obj.suggested_start_line || obj.start_line) > 0
41
+ ? Number(obj.suggested_start_line || obj.start_line)
42
+ : 1;
43
+ const end =
44
+ Number(obj.suggested_end_line || obj.end_line) >= start
45
+ ? Number(obj.suggested_end_line || obj.end_line)
46
+ : start;
47
+ const rangeText = start > 0 && end >= start ? ` lines ${start}-${end}` : '';
48
+ const totalText = total > 0 ? ` of ${total}` : '';
49
+ const errorText = obj.error ? ` (${trimInline(obj.error, 64)})` : '';
50
+ const truncatedText = obj.truncated ? ' [truncated]' : '';
51
+ return phase === 'metadata'
52
+ ? `metadata for ${p}${rangeText}${totalText}${errorText}`
53
+ : `content from ${p}${rangeText}${totalText}${truncatedText}`;
54
+ }
55
+ if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
56
+ const stdout = trimInline(obj.stdout || '', 96);
57
+ const stderr = trimInline(obj.stderr || '', 96);
58
+ const command = trimInline(obj.command || '', 72);
59
+ const lead = command ? `${command} -> ` : '';
60
+ if (stdout) return `${lead}exit ${obj.code ?? 0}\nstdout: ${stdout}`;
61
+ if (stderr) return `${lead}exit ${obj.code ?? 0}\nstderr: ${stderr}`;
62
+ return `${lead}exit ${obj.code ?? 0}`;
63
+ }
64
+ if ('created' in obj && Array.isArray(obj.created)) {
65
+ return `created ${obj.created.length} task(s)`;
66
+ }
67
+ if ('tasks' in obj && Array.isArray(obj.tasks)) {
68
+ return `${obj.tasks.length} task(s)`;
69
+ }
70
+ const keys = Object.keys(obj);
71
+ return keys.length > 0 ? `keys: ${keys.slice(0, 5).join(',')}` : 'object';
72
+ }
73
+ return String(result);
74
+ }
75
+
76
+ function trimInline(value, maxLen = 72) {
77
+ const s = String(value || '').replace(/\s+/g, ' ').trim();
78
+ if (!s) return '';
79
+ if (s.length <= maxLen) return s;
80
+ return `${s.slice(0, maxLen - 3)}...`;
81
+ }
82
+
83
+ function formatToolDisplayName(name, args) {
84
+ if (name === 'read_file' || name === 'write_file') {
85
+ const target = trimInline(args?.path || '.', 96) || '.';
86
+ if (name === 'read_file') {
87
+ const start = Number(args?.start_line);
88
+ const end = Number(args?.end_line);
89
+ const hasRange = Number.isFinite(start) && start > 0;
90
+ const suffix = hasRange ? `:${start}-${Number.isFinite(end) && end >= start ? end : start}` : '';
91
+ return `${name}(${target}${suffix})`;
92
+ }
93
+ return `${name}(${target})`;
94
+ }
95
+ if (name === 'run_command') {
96
+ const command = trimInline(args?.command || '', 96);
97
+ return command ? `${name}(${command})` : name;
98
+ }
99
+ return name;
100
+ }
101
+
102
+ export async function runAgentLoop({
103
+ systemPrompt,
104
+ userPrompt,
105
+ model,
106
+ requestCompletion,
107
+ toolHandlers = {},
108
+ toolDefinitions = [],
109
+ maxSteps = 8,
110
+ initialMessages = [],
111
+ onEvent,
112
+ executionMode = 'auto',
113
+ alwaysAllowTools = [],
114
+ requestToolApproval,
115
+ toolResultMaxChars = 12000
116
+ }) {
117
+ const messages = [];
118
+ if (systemPrompt) {
119
+ messages.push({ role: 'system', content: systemPrompt });
120
+ }
121
+ if (Array.isArray(initialMessages) && initialMessages.length > 0) {
122
+ messages.push(...initialMessages);
123
+ }
124
+ if (userPrompt) {
125
+ messages.push({ role: 'user', content: userPrompt });
126
+ }
127
+
128
+ let finalText = '';
129
+ let lastAssistantText = '';
130
+ const alwaysAllowSet = new Set((Array.isArray(alwaysAllowTools) ? alwaysAllowTools : []).map((t) => String(t)));
131
+
132
+ for (let step = 0; step < maxSteps; step += 1) {
133
+ if (onEvent) onEvent({ type: 'step:start', step: step + 1 });
134
+ const completion = await requestCompletion({
135
+ model,
136
+ messages,
137
+ tools: toolDefinitions
138
+ });
139
+
140
+ const toolCalls = Array.isArray(completion.toolCalls) ? completion.toolCalls : [];
141
+ const assistantText = completion.text || '';
142
+ lastAssistantText = assistantText || lastAssistantText;
143
+
144
+ const assistantMessage = { role: 'assistant', content: assistantText };
145
+ if (toolCalls.length > 0) {
146
+ assistantMessage.tool_calls = toolCalls.map((tc) => ({
147
+ id: tc.id,
148
+ type: 'function',
149
+ function: { name: tc.name, arguments: tc.arguments || '{}' }
150
+ }));
151
+ }
152
+ messages.push(assistantMessage);
153
+ if (onEvent) {
154
+ onEvent({
155
+ type: 'assistant:response',
156
+ step: step + 1,
157
+ text: assistantText,
158
+ toolCalls: toolCalls.map((tc) => tc.name),
159
+ assistantMessage
160
+ });
161
+ }
162
+
163
+ if (toolCalls.length === 0) {
164
+ finalText = assistantText;
165
+ return { text: finalText, messages, steps: step + 1 };
166
+ }
167
+
168
+ if (executionMode === 'plan') {
169
+ finalText = `${assistantText || ''}\n\n[plan mode] ${toolCalls.length} tool call(s) were planned but not executed.`;
170
+ return { text: finalText.trim(), messages, steps: step + 1 };
171
+ }
172
+
173
+ for (const call of toolCalls) {
174
+ const args = safeJsonParse(call.arguments);
175
+ const displayName = formatToolDisplayName(call.name, args);
176
+ const startedAt = Date.now();
177
+ let approved = true;
178
+ if (executionMode === 'normal' && !alwaysAllowSet.has(call.name)) {
179
+ approved = false;
180
+ if (typeof requestToolApproval === 'function') {
181
+ const decision = await requestToolApproval({
182
+ id: call.id,
183
+ name: call.name,
184
+ displayName,
185
+ arguments: args
186
+ });
187
+ approved = Boolean(decision?.approved);
188
+ }
189
+ }
190
+
191
+ if (!approved) {
192
+ if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id });
193
+ const blockedMessage = {
194
+ role: 'tool',
195
+ tool_call_id: call.id,
196
+ content: JSON.stringify({ blocked: true, reason: 'Tool call requires approval in normal mode' })
197
+ };
198
+ messages.push(blockedMessage);
199
+ if (onEvent) {
200
+ onEvent({
201
+ type: 'tool:result',
202
+ name: displayName,
203
+ id: call.id,
204
+ content: blockedMessage.content,
205
+ blocked: true
206
+ });
207
+ }
208
+ continue;
209
+ }
210
+
211
+ if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id });
212
+ const handler = toolHandlers[call.name];
213
+ if (!handler) {
214
+ throw new Error(`Unknown tool: ${call.name}`);
215
+ }
216
+ let toolResult;
217
+ try {
218
+ toolResult = await handler(args);
219
+ } catch (error) {
220
+ const durationMs = Date.now() - startedAt;
221
+ const message = error instanceof Error ? error.message : String(error);
222
+ if (onEvent) {
223
+ onEvent({
224
+ type: 'tool:error',
225
+ name: displayName,
226
+ id: call.id,
227
+ durationMs,
228
+ summary: trimInline(message, 120)
229
+ });
230
+ }
231
+ const toolMessage = {
232
+ role: 'tool',
233
+ tool_call_id: call.id,
234
+ content: clipToolResult({ error: message }, toolResultMaxChars)
235
+ };
236
+ messages.push(toolMessage);
237
+ if (onEvent) {
238
+ onEvent({
239
+ type: 'tool:result',
240
+ name: displayName,
241
+ id: call.id,
242
+ content: toolMessage.content,
243
+ error: true
244
+ });
245
+ }
246
+ continue;
247
+ }
248
+ const durationMs = Date.now() - startedAt;
249
+ if (onEvent) {
250
+ onEvent({
251
+ type: 'tool:end',
252
+ name: displayName,
253
+ id: call.id,
254
+ durationMs,
255
+ summary: summarizeToolResult(toolResult)
256
+ });
257
+ }
258
+ const toolMessage = {
259
+ role: 'tool',
260
+ tool_call_id: call.id,
261
+ content: clipToolResult(toolResult, toolResultMaxChars)
262
+ };
263
+ messages.push(toolMessage);
264
+ if (onEvent) {
265
+ onEvent({
266
+ type: 'tool:result',
267
+ name: displayName,
268
+ id: call.id,
269
+ content: toolMessage.content
270
+ });
271
+ }
272
+ }
273
+ }
274
+
275
+ const fallback = lastAssistantText || 'Stopped before final response.';
276
+ return {
277
+ text: `${fallback}\n\n[stopped] Reached max tool steps (${maxSteps}). Try a narrower prompt or increase execution.max_steps.`,
278
+ messages,
279
+ steps: maxSteps
280
+ };
281
+ }