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,71 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ function resolveShell(defaultShell) {
4
+ if (process.platform === 'win32') {
5
+ if (defaultShell === 'cmd') {
6
+ return { command: 'cmd.exe', args: ['/d', '/s', '/c'] };
7
+ }
8
+ if (defaultShell === 'bash') {
9
+ return { command: 'bash.exe', args: ['-lc'] };
10
+ }
11
+ return { command: 'powershell.exe', args: ['-NoLogo', '-NoProfile', '-Command'] };
12
+ }
13
+
14
+ if (defaultShell === 'powershell') {
15
+ return { command: 'pwsh', args: ['-NoLogo', '-NoProfile', '-Command'] };
16
+ }
17
+
18
+ return { command: '/bin/bash', args: ['-lc'] };
19
+ }
20
+
21
+ export function isDangerousCommand(command, blockedPatterns = []) {
22
+ const lowered = command.toLowerCase();
23
+ return blockedPatterns.some((pattern) => lowered.includes(String(pattern).toLowerCase()));
24
+ }
25
+
26
+ export function runShellCommand({
27
+ command,
28
+ cwd = process.cwd(),
29
+ shell = 'powershell',
30
+ timeoutMs = 120000
31
+ }) {
32
+ const shellSpec = resolveShell(shell);
33
+
34
+ return new Promise((resolve, reject) => {
35
+ const child = spawn(shellSpec.command, [...shellSpec.args, command], {
36
+ cwd,
37
+ stdio: ['ignore', 'pipe', 'pipe']
38
+ });
39
+
40
+ let stdout = '';
41
+ let stderr = '';
42
+ let timedOut = false;
43
+
44
+ const timer = setTimeout(() => {
45
+ timedOut = true;
46
+ child.kill('SIGTERM');
47
+ }, timeoutMs);
48
+
49
+ child.stdout.on('data', (chunk) => {
50
+ stdout += chunk.toString();
51
+ });
52
+
53
+ child.stderr.on('data', (chunk) => {
54
+ stderr += chunk.toString();
55
+ });
56
+
57
+ child.on('error', (err) => {
58
+ clearTimeout(timer);
59
+ reject(err);
60
+ });
61
+
62
+ child.on('close', (code) => {
63
+ clearTimeout(timer);
64
+ if (timedOut) {
65
+ reject(new Error(`Command timed out after ${timeoutMs}ms`));
66
+ return;
67
+ }
68
+ resolve({ code, stdout, stderr });
69
+ });
70
+ });
71
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { getSkillRegistryPath as getSkillRegistryPathFromPaths } from './paths.js';
5
+
6
+ function defaultRegistry() {
7
+ return {
8
+ version: 1,
9
+ updatedAt: new Date().toISOString(),
10
+ skills: []
11
+ };
12
+ }
13
+
14
+ export function getSkillRegistryPath() {
15
+ return getSkillRegistryPathFromPaths();
16
+ }
17
+
18
+ export async function readSkillRegistry(registryPath = getSkillRegistryPath()) {
19
+ try {
20
+ const raw = await fs.readFile(registryPath, 'utf8');
21
+ const parsed = JSON.parse(raw);
22
+ if (!Array.isArray(parsed.skills)) {
23
+ return defaultRegistry();
24
+ }
25
+ return parsed;
26
+ } catch {
27
+ return defaultRegistry();
28
+ }
29
+ }
30
+
31
+ export async function writeSkillRegistry(registryPath = getSkillRegistryPath(), registry) {
32
+ await fs.mkdir(path.dirname(registryPath), { recursive: true });
33
+ registry.updatedAt = new Date().toISOString();
34
+ await fs.writeFile(registryPath, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
35
+ }
36
+
37
+ export async function upsertSkillRegistryEntry(registryPath = getSkillRegistryPath(), entry) {
38
+ const registry = await readSkillRegistry(registryPath);
39
+ const index = registry.skills.findIndex((s) => s.name === entry.name);
40
+ if (index === -1) {
41
+ registry.skills.push(entry);
42
+ } else {
43
+ registry.skills[index] = { ...registry.skills[index], ...entry };
44
+ }
45
+ await writeSkillRegistry(registryPath, registry);
46
+ }
47
+
48
+ export function getEnabledSkills(registry) {
49
+ return (registry.skills || []).filter((s) => s.enabled !== false);
50
+ }
51
+
52
+ export async function computeFileSha256(filePath) {
53
+ const content = await fs.readFile(filePath);
54
+ return createHash('sha256').update(content).digest('hex');
55
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { getBaseConfigDir } from './paths.js';
5
+
6
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7
+ const BUNDLED_SOULS_DIR = path.resolve(MODULE_DIR, '..', '..', 'souls');
8
+
9
+ function normalizeSoulName(value) {
10
+ const name = String(value || '').trim().toLowerCase();
11
+ return name || 'default';
12
+ }
13
+
14
+ function resolveCustomSoulPath(customPath = '') {
15
+ const raw = String(customPath || '').trim();
16
+ if (!raw) return '';
17
+ if (path.isAbsolute(raw)) return raw;
18
+ return path.join(getBaseConfigDir(), raw);
19
+ }
20
+
21
+ export async function loadSoulPrompt(config = {}) {
22
+ const customPath = resolveCustomSoulPath(config?.soul?.custom_path);
23
+ if (customPath) {
24
+ try {
25
+ const content = await fs.readFile(customPath, 'utf8');
26
+ const text = String(content || '').trim();
27
+ if (text) return `[Soul custom]\n${text}`;
28
+ } catch {
29
+ // fall through to bundled preset
30
+ }
31
+ }
32
+
33
+ const preset = normalizeSoulName(config?.soul?.preset);
34
+ const presetPath = path.join(BUNDLED_SOULS_DIR, `${preset}.md`);
35
+ try {
36
+ const content = await fs.readFile(presetPath, 'utf8');
37
+ const text = String(content || '').trim();
38
+ if (text) return `[Soul preset: ${preset}]\n${text}`;
39
+ } catch {
40
+ // fall through to default preset
41
+ }
42
+
43
+ const defaultContent = await fs.readFile(path.join(BUNDLED_SOULS_DIR, 'default.md'), 'utf8');
44
+ return `[Soul preset: default]\n${String(defaultContent || '').trim()}`;
45
+ }
46
+
47
+ export async function buildSystemPromptWithSoul(baseSystemPrompt, config = {}) {
48
+ const soulPrompt = await loadSoulPrompt(config);
49
+ const guard = [
50
+ '[Soul guard]',
51
+ 'Apply this soul to response tone only.',
52
+ 'Response tone only: do not change plans, code, tests, file formats, or technical decisions.'
53
+ ].join('\n');
54
+ return `${String(baseSystemPrompt || '').trim()}\n\n${guard}\n\n${soulPrompt}`.trim();
55
+ }
@@ -0,0 +1,116 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ function legacyTasksFilePath(cwd = process.cwd()) {
5
+ return path.join(cwd, '.coder', 'tasks.json');
6
+ }
7
+
8
+ function tasksFilePath(cwd = process.cwd(), sessionId = '') {
9
+ const sid = String(sessionId || '').trim();
10
+ if (!sid) return legacyTasksFilePath(cwd);
11
+ return path.join(cwd, '.coder', 'tasks', `${sid}.json`);
12
+ }
13
+
14
+ async function ensureDir(filePath) {
15
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
16
+ }
17
+
18
+ function normalizeTasks(value) {
19
+ if (!Array.isArray(value)) return [];
20
+ return value
21
+ .map((t) => ({
22
+ id: String(t?.id || '').trim(),
23
+ title: String(t?.title || '').trim(),
24
+ status: String(t?.status || 'pending').trim() || 'pending',
25
+ description: String(t?.description || '').trim(),
26
+ createdAt: String(t?.createdAt || ''),
27
+ updatedAt: String(t?.updatedAt || '')
28
+ }))
29
+ .filter((t) => t.id && t.title);
30
+ }
31
+
32
+ function createTaskId() {
33
+ const ts = Date.now().toString(36);
34
+ const rnd = Math.random().toString(36).slice(2, 7);
35
+ return `task-${ts}-${rnd}`;
36
+ }
37
+
38
+ export async function loadTasks(cwd = process.cwd(), sessionId = '') {
39
+ const filePath = tasksFilePath(cwd, sessionId);
40
+ try {
41
+ const raw = await fs.readFile(filePath, 'utf8');
42
+ const parsed = JSON.parse(raw);
43
+ return normalizeTasks(parsed?.tasks);
44
+ } catch {
45
+ if (sessionId) {
46
+ try {
47
+ const raw = await fs.readFile(legacyTasksFilePath(cwd), 'utf8');
48
+ const parsed = JSON.parse(raw);
49
+ return normalizeTasks(parsed?.tasks);
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+ return [];
55
+ }
56
+ }
57
+
58
+ export async function saveTasks(tasks, cwd = process.cwd(), sessionId = '') {
59
+ const filePath = tasksFilePath(cwd, sessionId);
60
+ await ensureDir(filePath);
61
+ const normalized = normalizeTasks(tasks);
62
+ await fs.writeFile(
63
+ filePath,
64
+ `${JSON.stringify({ updatedAt: new Date().toISOString(), tasks: normalized }, null, 2)}\n`,
65
+ 'utf8'
66
+ );
67
+ return normalized;
68
+ }
69
+
70
+ export async function createTasks(items, cwd = process.cwd(), sessionId = '') {
71
+ const existing = await loadTasks(cwd, sessionId);
72
+ const now = new Date().toISOString();
73
+ const input = Array.isArray(items) ? items : [];
74
+ const add = input
75
+ .map((t) => ({
76
+ id: createTaskId(),
77
+ title: String(t?.title || '').trim(),
78
+ description: String(t?.description || '').trim(),
79
+ status: 'pending',
80
+ createdAt: now,
81
+ updatedAt: now
82
+ }))
83
+ .filter((t) => t.title);
84
+ const next = [...existing, ...add];
85
+ await saveTasks(next, cwd, sessionId);
86
+ return add;
87
+ }
88
+
89
+ export async function updateTask(taskId, patch, cwd = process.cwd(), sessionId = '') {
90
+ const tasks = await loadTasks(cwd, sessionId);
91
+ const idx = tasks.findIndex((t) => t.id === taskId);
92
+ if (idx === -1) return null;
93
+ const next = [...tasks];
94
+ const status = String(patch?.status || next[idx].status).trim();
95
+ next[idx] = {
96
+ ...next[idx],
97
+ ...(patch?.title ? { title: String(patch.title) } : {}),
98
+ ...(patch?.description !== undefined ? { description: String(patch.description || '') } : {}),
99
+ status: ['pending', 'in_progress', 'completed'].includes(status) ? status : next[idx].status,
100
+ updatedAt: new Date().toISOString()
101
+ };
102
+ await saveTasks(next, cwd, sessionId);
103
+ return next[idx];
104
+ }
105
+
106
+ export async function deleteTasks(ids, cwd = process.cwd(), sessionId = '') {
107
+ const remove = new Set((Array.isArray(ids) ? ids : []).map((v) => String(v)));
108
+ const before = await loadTasks(cwd, sessionId);
109
+ const kept = before.filter((t) => !remove.has(t.id));
110
+ await saveTasks(kept, cwd, sessionId);
111
+ return { removed: before.length - kept.length, remaining: kept.length };
112
+ }
113
+
114
+ export async function clearTasks(cwd = process.cwd(), sessionId = '') {
115
+ await saveTasks([], cwd, sessionId);
116
+ }
@@ -0,0 +1,237 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { isDangerousCommand, runShellCommand } from './shell.js';
5
+ import { evaluateCommandPolicy } from './command-policy.js';
6
+
7
+ function resolveInWorkspace(root, targetPath = '.') {
8
+ const absRoot = path.resolve(root);
9
+ const absTarget = path.resolve(absRoot, targetPath);
10
+ if (!absTarget.startsWith(absRoot)) {
11
+ throw new Error(`Path escapes workspace: ${targetPath}`);
12
+ }
13
+ return absTarget;
14
+ }
15
+
16
+ async function readFile(root, args) {
17
+ const target = resolveInWorkspace(root, args?.path);
18
+ const stat = await fs.stat(target);
19
+ const text = await fs.readFile(target, 'utf8');
20
+ const lines = text.split('\n');
21
+ const totalLines = lines.length;
22
+ const startLineRaw = Number(args?.start_line);
23
+ const endLineRaw = Number(args?.end_line);
24
+ const defaultLines = Number(args?.default_lines || 220);
25
+ const maxChars = Number(args?.max_chars || 24000);
26
+ const includeContent = Boolean(args?.include_content);
27
+
28
+ let startLine = Number.isFinite(startLineRaw) && startLineRaw > 0 ? startLineRaw : 1;
29
+ let endLine =
30
+ Number.isFinite(endLineRaw) && endLineRaw >= startLine
31
+ ? endLineRaw
32
+ : Math.min(totalLines, startLine + Math.max(1, defaultLines) - 1);
33
+ startLine = Math.max(1, Math.min(startLine, totalLines));
34
+ endLine = Math.max(startLine, Math.min(endLine, totalLines));
35
+
36
+ const tokenSeed = `${args?.path}|${stat.size}|${stat.mtimeMs}|${startLine}|${endLine}`;
37
+ const readToken = crypto.createHash('sha1').update(tokenSeed).digest('hex').slice(0, 16);
38
+
39
+ if (!includeContent) {
40
+ return {
41
+ path: args?.path,
42
+ phase: 'metadata',
43
+ size_bytes: stat.size,
44
+ modified_at: new Date(stat.mtimeMs).toISOString(),
45
+ total_lines: totalLines,
46
+ suggested_start_line: startLine,
47
+ suggested_end_line: endLine,
48
+ read_token: readToken,
49
+ next: 'Call read_file again with include_content=true and this read_token'
50
+ };
51
+ }
52
+
53
+ if (String(args?.read_token || '') !== readToken) {
54
+ return {
55
+ path: args?.path,
56
+ phase: 'metadata',
57
+ error: 'read_token mismatch or missing',
58
+ size_bytes: stat.size,
59
+ modified_at: new Date(stat.mtimeMs).toISOString(),
60
+ total_lines: totalLines,
61
+ suggested_start_line: startLine,
62
+ suggested_end_line: endLine,
63
+ read_token: readToken,
64
+ next: 'Retry with include_content=true and read_token from latest metadata'
65
+ };
66
+ }
67
+
68
+ let content = lines.slice(startLine - 1, endLine).join('\n');
69
+ let truncated = false;
70
+ if (maxChars > 0 && content.length > maxChars) {
71
+ content = `${content.slice(0, maxChars)}\n... [truncated by max_chars]`;
72
+ truncated = true;
73
+ }
74
+
75
+ return {
76
+ path: args?.path,
77
+ phase: 'content',
78
+ start_line: startLine,
79
+ end_line: endLine,
80
+ total_lines: totalLines,
81
+ truncated,
82
+ content
83
+ };
84
+ }
85
+
86
+ async function writeFile(root, args) {
87
+ const rawPath = String(args?.path || '').trim();
88
+ if (!rawPath) {
89
+ throw new Error('write_file requires a file path like weather/WeatherForecast.js');
90
+ }
91
+ if (rawPath === '.' || rawPath === './') {
92
+ throw new Error('write_file requires a file path, not the workspace root');
93
+ }
94
+ const target = resolveInWorkspace(root, rawPath);
95
+ try {
96
+ const stat = await fs.stat(target);
97
+ if (stat.isDirectory()) {
98
+ throw new Error(`write_file target is a directory: ${rawPath}`);
99
+ }
100
+ } catch (error) {
101
+ if (error?.code && error.code !== 'ENOENT') {
102
+ throw error;
103
+ }
104
+ }
105
+ let before = '';
106
+ let existed = true;
107
+ try {
108
+ before = await fs.readFile(target, 'utf8');
109
+ } catch {
110
+ existed = false;
111
+ }
112
+ await fs.mkdir(path.dirname(target), { recursive: true });
113
+ if (args?.append) {
114
+ await fs.appendFile(target, args?.content || '', 'utf8');
115
+ } else {
116
+ await fs.writeFile(target, args?.content || '', 'utf8');
117
+ }
118
+ const after = args?.append ? `${before}${args?.content || ''}` : args?.content || '';
119
+ const beforeLines = before.split('\n');
120
+ const afterLines = after.split('\n');
121
+ let changeLine = 0;
122
+ const scanMax = Math.max(beforeLines.length, afterLines.length);
123
+ for (let i = 0; i < scanMax; i += 1) {
124
+ if ((beforeLines[i] || '') !== (afterLines[i] || '')) {
125
+ changeLine = i + 1;
126
+ break;
127
+ }
128
+ }
129
+ const previewStart = Math.max(0, (changeLine || 1) - 1);
130
+ const previewLines = afterLines.slice(previewStart, previewStart + 6);
131
+ return {
132
+ ok: true,
133
+ path: rawPath,
134
+ action: args?.append ? 'append' : existed ? 'overwrite' : 'create',
135
+ changed_line: changeLine || Math.max(1, afterLines.length),
136
+ diff_preview: previewLines.map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n')
137
+ };
138
+ }
139
+
140
+ async function runCommand(root, config, args) {
141
+ const command = args?.command || '';
142
+ if (!command.trim()) {
143
+ throw new Error('run_command requires command');
144
+ }
145
+ if (
146
+ !config.policy.allow_dangerous_commands &&
147
+ isDangerousCommand(command, config.policy.blocked_command_patterns)
148
+ ) {
149
+ throw new Error('Command blocked by policy');
150
+ }
151
+
152
+ const check = evaluateCommandPolicy(command, config, root);
153
+ if (!check.allowed) {
154
+ throw new Error(
155
+ `Command blocked by safe mode: ${check.reason}${check.suggestion ? ` | ${check.suggestion}` : ''}`
156
+ );
157
+ }
158
+
159
+ const result = await runShellCommand({
160
+ command,
161
+ cwd: root,
162
+ shell: config.shell.default,
163
+ timeoutMs: config.shell.timeout_ms
164
+ });
165
+ return { ...result, command };
166
+ }
167
+
168
+ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, sessionId = '' }) {
169
+ const definitions = [
170
+ {
171
+ type: 'function',
172
+ function: {
173
+ name: 'read_file',
174
+ description:
175
+ 'Two-phase read: first call returns metadata+read_token; second call with include_content=true and matching read_token returns content',
176
+ parameters: {
177
+ type: 'object',
178
+ properties: {
179
+ path: { type: 'string' },
180
+ start_line: { type: 'number' },
181
+ end_line: { type: 'number' },
182
+ max_chars: { type: 'number' },
183
+ include_content: { type: 'boolean' },
184
+ read_token: { type: 'string' }
185
+ },
186
+ required: ['path']
187
+ }
188
+ }
189
+ },
190
+ {
191
+ type: 'function',
192
+ function: {
193
+ name: 'write_file',
194
+ description: 'Write a UTF-8 text file in workspace. Always provide a full file path, not a directory.',
195
+ parameters: {
196
+ type: 'object',
197
+ properties: {
198
+ path: { type: 'string' },
199
+ content: { type: 'string' },
200
+ append: { type: 'boolean' }
201
+ },
202
+ required: ['path', 'content']
203
+ }
204
+ }
205
+ },
206
+ {
207
+ type: 'function',
208
+ function: {
209
+ name: 'run_command',
210
+ description: 'Execute a shell command in workspace',
211
+ parameters: {
212
+ type: 'object',
213
+ properties: {
214
+ command: { type: 'string' }
215
+ },
216
+ required: ['command']
217
+ }
218
+ }
219
+ }
220
+ ];
221
+
222
+ const handlers = {
223
+ read_file: (args) =>
224
+ readFile(workspaceRoot, {
225
+ ...args,
226
+ default_lines: config.context?.read_file_default_lines ?? 220,
227
+ max_chars:
228
+ typeof args?.max_chars === 'number'
229
+ ? args.max_chars
230
+ : config.context?.read_file_max_chars ?? 24000
231
+ }),
232
+ write_file: (args) => writeFile(workspaceRoot, args),
233
+ run_command: (args) => runCommand(workspaceRoot, config, args)
234
+ };
235
+
236
+ return { definitions, handlers };
237
+ }