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,66 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ function checkpointsDir(cwd = process.cwd()) {
5
+ return path.join(cwd, '.coder', 'checkpoints');
6
+ }
7
+
8
+ function makeId(name = '') {
9
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
10
+ const slug = String(name || '')
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
13
+ .replace(/^-+|-+$/g, '')
14
+ .slice(0, 48);
15
+ return `${stamp}-${slug || 'checkpoint'}`;
16
+ }
17
+
18
+ export async function createCheckpoint({ name, session, config, tasks }, cwd = process.cwd()) {
19
+ const dir = checkpointsDir(cwd);
20
+ await fs.mkdir(dir, { recursive: true });
21
+ const id = makeId(name);
22
+ const filePath = path.join(dir, `${id}.json`);
23
+ const payload = {
24
+ id,
25
+ name: String(name || ''),
26
+ createdAt: new Date().toISOString(),
27
+ session,
28
+ config,
29
+ tasks: Array.isArray(tasks) ? tasks : []
30
+ };
31
+ await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
32
+ return payload;
33
+ }
34
+
35
+ export async function listCheckpoints(cwd = process.cwd()) {
36
+ const dir = checkpointsDir(cwd);
37
+ await fs.mkdir(dir, { recursive: true });
38
+ const entries = await fs.readdir(dir, { withFileTypes: true });
39
+ const out = [];
40
+ for (const e of entries) {
41
+ if (!e.isFile() || !e.name.endsWith('.json')) continue;
42
+ const filePath = path.join(dir, e.name);
43
+ try {
44
+ const raw = await fs.readFile(filePath, 'utf8');
45
+ const parsed = JSON.parse(raw);
46
+ out.push({
47
+ id: parsed.id || path.basename(e.name, '.json'),
48
+ name: parsed.name || '',
49
+ createdAt: parsed.createdAt || '',
50
+ sessionId: parsed?.session?.id || '',
51
+ filePath
52
+ });
53
+ } catch {
54
+ continue;
55
+ }
56
+ }
57
+ out.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
58
+ return out;
59
+ }
60
+
61
+ export async function loadCheckpoint(id, cwd = process.cwd()) {
62
+ const dir = checkpointsDir(cwd);
63
+ const filePath = path.join(dir, `${id}.json`);
64
+ const raw = await fs.readFile(filePath, 'utf8');
65
+ return JSON.parse(raw);
66
+ }
@@ -0,0 +1,201 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import {
5
+ getCommandsDir,
6
+ getLegacyGlobalSkillsDir,
7
+ getLegacyProjectSkillsDir,
8
+ getProjectCommandsDir,
9
+ getSkillsDir
10
+ } from './paths.js';
11
+ import { readSkillRegistry } from './skill-registry.js';
12
+
13
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ const BUNDLED_SKILLS_DIR = path.resolve(MODULE_DIR, '..', '..', 'skills');
15
+
16
+ function parseArrayText(value) {
17
+ const inner = value.slice(1, -1).trim();
18
+ if (!inner) return [];
19
+ return inner.split(',').map((item) => item.trim().replace(/^["']|["']$/g, ''));
20
+ }
21
+
22
+ function parseFrontmatter(raw) {
23
+ if (!raw.startsWith('---\n')) {
24
+ return { metadata: {}, content: raw };
25
+ }
26
+ const end = raw.indexOf('\n---\n', 4);
27
+ if (end === -1) {
28
+ return { metadata: {}, content: raw };
29
+ }
30
+
31
+ const metaRaw = raw.slice(4, end).trim();
32
+ const content = raw.slice(end + 5).trim();
33
+ const metadata = {};
34
+
35
+ for (const line of metaRaw.split('\n')) {
36
+ const idx = line.indexOf(':');
37
+ if (idx <= 0) continue;
38
+ const key = line.slice(0, idx).trim();
39
+ const value = line.slice(idx + 1).trim();
40
+ if (value.startsWith('[') && value.endsWith(']')) {
41
+ metadata[key] = parseArrayText(value);
42
+ } else {
43
+ metadata[key] = value.replace(/^["']|["']$/g, '');
44
+ }
45
+ }
46
+
47
+ return { metadata, content };
48
+ }
49
+
50
+ function safeEntries(dir) {
51
+ try {
52
+ return fs.readdirSync(dir);
53
+ } catch {
54
+ return [];
55
+ }
56
+ }
57
+
58
+ function isSafeEntry(entry) {
59
+ return entry !== '.' && entry !== '..' && !entry.includes('/') && !entry.includes('\\');
60
+ }
61
+
62
+ function loadMarkdownCommandsFromDir(baseDir, source, out) {
63
+ if (!fs.existsSync(baseDir)) return;
64
+ for (const entry of safeEntries(baseDir)) {
65
+ if (!isSafeEntry(entry)) continue;
66
+ const full = path.join(baseDir, entry);
67
+ const stat = fs.statSync(full);
68
+
69
+ if (stat.isDirectory()) {
70
+ const commandFile = path.join(full, `${entry}.md`);
71
+ if (fs.existsSync(commandFile)) {
72
+ const raw = fs.readFileSync(commandFile, 'utf8');
73
+ const parsed = parseFrontmatter(raw);
74
+ out.set(entry, {
75
+ name: entry,
76
+ source,
77
+ path: commandFile,
78
+ metadata: parsed.metadata,
79
+ content: parsed.content
80
+ });
81
+ }
82
+ continue;
83
+ }
84
+
85
+ if (entry.endsWith('.md')) {
86
+ const name = entry.replace(/\.md$/, '');
87
+ const raw = fs.readFileSync(full, 'utf8');
88
+ const parsed = parseFrontmatter(raw);
89
+ out.set(name, {
90
+ name,
91
+ source,
92
+ path: full,
93
+ metadata: parsed.metadata,
94
+ content: parsed.content
95
+ });
96
+ }
97
+ }
98
+ }
99
+
100
+ function loadLegacySkillsFromDir(baseDir, source, out) {
101
+ if (!fs.existsSync(baseDir)) return;
102
+ for (const entry of safeEntries(baseDir)) {
103
+ if (!isSafeEntry(entry)) continue;
104
+ const full = path.join(baseDir, entry);
105
+ const stat = fs.statSync(full);
106
+ if (!stat.isDirectory()) continue;
107
+ const skillFile = path.join(full, 'SKILL.md');
108
+ if (!fs.existsSync(skillFile)) continue;
109
+ const raw = fs.readFileSync(skillFile, 'utf8');
110
+ const parsed = parseFrontmatter(raw);
111
+ out.set(entry, {
112
+ name: entry,
113
+ source: `${source}-skill`,
114
+ path: skillFile,
115
+ metadata: {
116
+ description: parsed.metadata.description || 'Legacy skill',
117
+ type: 'skill'
118
+ },
119
+ content: parsed.content
120
+ });
121
+ }
122
+ }
123
+
124
+ function loadBundledSkillsFromDir(baseDir, out) {
125
+ if (!fs.existsSync(baseDir)) return;
126
+ for (const entry of safeEntries(baseDir)) {
127
+ if (!isSafeEntry(entry)) continue;
128
+ const full = path.join(baseDir, entry);
129
+ const stat = fs.statSync(full);
130
+ if (!stat.isDirectory()) continue;
131
+ const skillFile = path.join(full, 'SKILL.md');
132
+ if (!fs.existsSync(skillFile)) continue;
133
+ const raw = fs.readFileSync(skillFile, 'utf8');
134
+ const parsed = parseFrontmatter(raw);
135
+ out.set(entry, {
136
+ name: entry,
137
+ source: 'bundled-skill',
138
+ path: skillFile,
139
+ metadata: {
140
+ ...parsed.metadata,
141
+ type: 'skill',
142
+ version: parsed.metadata.version || '0.1.0',
143
+ description: parsed.metadata.description || 'Bundled skill'
144
+ },
145
+ content: parsed.content
146
+ });
147
+ }
148
+ }
149
+
150
+ function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
151
+ if (!registry || !Array.isArray(registry.skills)) return;
152
+ for (const skill of registry.skills) {
153
+ if (skill.enabled === false) continue;
154
+ const name = skill.name;
155
+ const entry = skill.entryFile || 'SKILL.md';
156
+ const full = path.join(baseDir, name, entry);
157
+ if (!fs.existsSync(full)) continue;
158
+ const raw = fs.readFileSync(full, 'utf8');
159
+ const parsed = parseFrontmatter(raw);
160
+ out.set(name, {
161
+ name,
162
+ source: 'registry-skill',
163
+ path: full,
164
+ metadata: {
165
+ ...parsed.metadata,
166
+ type: 'skill',
167
+ version: skill.version || parsed.metadata.version || '0.0.0',
168
+ description: skill.description || parsed.metadata.description || 'Installed skill'
169
+ },
170
+ content: parsed.content
171
+ });
172
+ }
173
+ }
174
+
175
+ function substituteVariables(text, args = []) {
176
+ let out = text;
177
+ args.forEach((arg, index) => {
178
+ out = out.replaceAll(`{{${index + 1}}}`, arg);
179
+ });
180
+ out = out.replaceAll('{{args}}', args.join(' '));
181
+ out = out.replaceAll('{{cwd}}', process.cwd());
182
+ return out;
183
+ }
184
+
185
+ export async function loadCommandsAndSkills(cwd = process.cwd()) {
186
+ const commands = new Map();
187
+
188
+ loadBundledSkillsFromDir(BUNDLED_SKILLS_DIR, commands);
189
+ loadMarkdownCommandsFromDir(getCommandsDir(), 'global', commands);
190
+ loadMarkdownCommandsFromDir(getProjectCommandsDir(cwd), 'project', commands);
191
+ loadLegacySkillsFromDir(getLegacyGlobalSkillsDir(), 'global', commands);
192
+ loadLegacySkillsFromDir(getLegacyProjectSkillsDir(cwd), 'project', commands);
193
+ const registry = await readSkillRegistry();
194
+ loadInstalledSkillsFromRegistry(getSkillsDir(), registry, commands);
195
+
196
+ return commands;
197
+ }
198
+
199
+ export function renderCommandPrompt(command, args) {
200
+ return `[Executing ${command.metadata.type === 'skill' ? 'skill' : 'command'}: /${command.name}]\n\n${substituteVariables(command.content, args)}`;
201
+ }
@@ -0,0 +1,71 @@
1
+ import path from 'node:path';
2
+ import { getEffectivePolicy } from './shell-profile.js';
3
+
4
+ function firstToken(command) {
5
+ const m = String(command || '').trim().match(/^"([^"]+)"|^'([^']+)'|^(\S+)/);
6
+ const raw = (m && (m[1] || m[2] || m[3])) || '';
7
+ const base = path.basename(raw).toLowerCase();
8
+ return base.replace(/\.exe$/i, '');
9
+ }
10
+
11
+ function includesAny(haystackLower, patterns = []) {
12
+ return patterns.some((p) => haystackLower.includes(String(p).toLowerCase()));
13
+ }
14
+
15
+ function suggestionForToken(token, config) {
16
+ const shell = String(config?.shell?.default || '').toLowerCase();
17
+ if (token === 'find' || token === 'grep') {
18
+ return shell === 'powershell'
19
+ ? 'Use allowed search and context commands such as Get-ChildItem, Select-String, Get-Content, or rg when available.'
20
+ : 'Use allowed search and context commands such as rg, find, grep, sed, cat, or ls.';
21
+ }
22
+ if (shell === 'powershell') {
23
+ return 'Use allowed shell commands for search and local context such as Get-ChildItem, Get-Content, Select-String, or rg when available.';
24
+ }
25
+ return 'Use allowed shell commands for search and local context such as rg, find, grep, sed, cat, or ls.';
26
+ }
27
+
28
+ export function evaluateCommandPolicy(command, config, workspaceRoot = process.cwd()) {
29
+ const policy = getEffectivePolicy(config);
30
+ const cmd = String(command || '').trim();
31
+ const lower = cmd.toLowerCase();
32
+ if (!cmd) {
33
+ return { allowed: false, reason: 'empty command' };
34
+ }
35
+
36
+ if (!policy.allow_dangerous_commands && includesAny(lower, policy.blocked_command_patterns)) {
37
+ return { allowed: false, reason: 'blocked by dangerous command pattern' };
38
+ }
39
+
40
+ if (!policy.safe_mode) {
41
+ return { allowed: true };
42
+ }
43
+
44
+ if (includesAny(lower, policy.blocked_path_patterns)) {
45
+ return { allowed: false, reason: 'blocked protected system path' };
46
+ }
47
+
48
+ const token = firstToken(cmd);
49
+ if (includesAny(token, policy.blocked_commands)) {
50
+ return { allowed: false, reason: `blocked command: ${token}`, suggestion: suggestionForToken(token, config) };
51
+ }
52
+
53
+ const allowlist = Array.isArray(policy.command_allowlist) ? policy.command_allowlist : [];
54
+ if (allowlist.length > 0 && !allowlist.includes(token)) {
55
+ return {
56
+ allowed: false,
57
+ reason: `command not in allowlist: ${token}`,
58
+ suggestion: suggestionForToken(token, config)
59
+ };
60
+ }
61
+
62
+ const workspaceLower = String(workspaceRoot).toLowerCase().replace(/\//g, '\\');
63
+ const windowsAbsPath = lower.match(/[a-z]:\\[^\s'"]+/g) || [];
64
+ for (const p of windowsAbsPath) {
65
+ if (!p.startsWith(workspaceLower)) {
66
+ return { allowed: false, reason: `absolute path outside workspace: ${p}`, suggestion: suggestionForToken(token, config) };
67
+ }
68
+ }
69
+
70
+ return { allowed: true };
71
+ }
@@ -0,0 +1,196 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getConfigFilePath, getLegacyConfigDir } from './paths.js';
4
+ import { normalizeShellName } from './shell-profile.js';
5
+
6
+ function normalizeUiLanguage(value) {
7
+ const raw = String(value || '').trim().toLowerCase();
8
+ if (!raw) return 'zh';
9
+ if (['en', 'en-us', 'en_us', 'english'].includes(raw)) return 'en';
10
+ if (['zh', 'zh-cn', 'zh_cn', 'cn', 'chinese'].includes(raw)) return 'zh';
11
+ return 'zh';
12
+ }
13
+
14
+ const DEFAULT_CONFIG = {
15
+ gateway: {
16
+ base_url: 'http://127.0.0.1:8000/v1',
17
+ api_key: '',
18
+ timeout_ms: 90000,
19
+ max_retries: 2
20
+ },
21
+ model: {
22
+ name: 'gpt-4.1-mini',
23
+ max_context_tokens: 202752
24
+ },
25
+ context: {
26
+ max_tokens: 32000,
27
+ preflight_trigger_pct: 92,
28
+ hard_limit_pct: 98,
29
+ tool_result_max_chars: 12000,
30
+ read_file_default_lines: 220,
31
+ read_file_max_chars: 24000
32
+ },
33
+ execution: {
34
+ mode: 'auto',
35
+ always_allow_tools: ['run_command', 'read_file', 'write_file'],
36
+ max_steps: 16
37
+ },
38
+ sessions: {
39
+ max_sessions: 100,
40
+ retention_days: 30
41
+ },
42
+ shell: {
43
+ default: normalizeShellName(process.platform === 'win32' ? 'powershell' : 'bash'),
44
+ timeout_ms: 120000
45
+ },
46
+ ui: {
47
+ language: 'zh'
48
+ },
49
+ soul: {
50
+ preset: 'default',
51
+ custom_path: ''
52
+ },
53
+ policy: {
54
+ safe_mode: true,
55
+ allow_dangerous_commands: false,
56
+ command_allowlist: [],
57
+ blocked_commands: [],
58
+ blocked_path_patterns: [],
59
+ blocked_command_patterns: ['rm -rf /', 'format c:', 'del /f /s /q C:\\\\']
60
+ },
61
+ skills: {
62
+ enabled: {}
63
+ }
64
+ };
65
+
66
+ async function ensureDir(filePath) {
67
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
68
+ }
69
+
70
+ function isObject(v) {
71
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
72
+ }
73
+
74
+ function deepMerge(base, extra) {
75
+ if (!isObject(base) || !isObject(extra)) {
76
+ return extra;
77
+ }
78
+ const out = { ...base };
79
+ for (const [k, v] of Object.entries(extra)) {
80
+ if (k in out) {
81
+ out[k] = deepMerge(out[k], v);
82
+ } else {
83
+ out[k] = v;
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ function uniqueStrings(items = []) {
90
+ const out = [];
91
+ const seen = new Set();
92
+ for (const it of items) {
93
+ const v = String(it || '').trim();
94
+ if (!v || seen.has(v)) continue;
95
+ seen.add(v);
96
+ out.push(v);
97
+ }
98
+ return out;
99
+ }
100
+
101
+ function normalizePolicyLists(config) {
102
+ const next = structuredClone(config);
103
+ next.shell = next.shell || {};
104
+ next.shell.default = normalizeShellName(next.shell.default);
105
+ next.execution = next.execution || {};
106
+ next.execution.mode = ['auto', 'normal', 'plan'].includes(String(next.execution.mode || '').toLowerCase())
107
+ ? String(next.execution.mode).toLowerCase()
108
+ : 'auto';
109
+ const rawTools = Array.isArray(next.execution.always_allow_tools)
110
+ ? next.execution.always_allow_tools
111
+ : [];
112
+ next.execution.always_allow_tools = uniqueStrings(
113
+ ['run_command', 'read_file', 'write_file', ...rawTools].filter((name) => String(name) !== 'list_files')
114
+ );
115
+ next.ui = next.ui || {};
116
+ next.ui.language = normalizeUiLanguage(next.ui.language);
117
+ next.policy = next.policy || {};
118
+ next.policy.command_allowlist = uniqueStrings(
119
+ Array.isArray(next.policy.command_allowlist) ? next.policy.command_allowlist : []
120
+ );
121
+ next.policy.blocked_commands = uniqueStrings(
122
+ Array.isArray(next.policy.blocked_commands) ? next.policy.blocked_commands : []
123
+ );
124
+ next.policy.blocked_path_patterns = uniqueStrings(
125
+ Array.isArray(next.policy.blocked_path_patterns) ? next.policy.blocked_path_patterns : []
126
+ );
127
+ return next;
128
+ }
129
+
130
+ function getNested(obj, keyPath) {
131
+ return keyPath.split('.').reduce((acc, k) => (acc && k in acc ? acc[k] : undefined), obj);
132
+ }
133
+
134
+ function parseValue(input) {
135
+ if (input === 'true') return true;
136
+ if (input === 'false') return false;
137
+ if (input === 'null') return null;
138
+ if (!Number.isNaN(Number(input)) && input.trim() !== '') return Number(input);
139
+ return input;
140
+ }
141
+
142
+ function setNested(obj, keyPath, rawValue) {
143
+ const value = parseValue(rawValue);
144
+ const parts = keyPath.split('.');
145
+ let cursor = obj;
146
+ for (let i = 0; i < parts.length - 1; i += 1) {
147
+ const p = parts[i];
148
+ if (!isObject(cursor[p])) {
149
+ cursor[p] = {};
150
+ }
151
+ cursor = cursor[p];
152
+ }
153
+ cursor[parts[parts.length - 1]] = value;
154
+ }
155
+
156
+ export async function loadConfig() {
157
+ const configPath = getConfigFilePath();
158
+ try {
159
+ const raw = await fs.readFile(configPath, 'utf8');
160
+ const parsed = JSON.parse(raw);
161
+ return normalizePolicyLists(deepMerge(DEFAULT_CONFIG, parsed));
162
+ } catch {
163
+ if (process.env.CODEMINI_CONFIG_DIR || process.env.COMPANY_CODER_CONFIG_DIR) {
164
+ return normalizePolicyLists(structuredClone(DEFAULT_CONFIG));
165
+ }
166
+ try {
167
+ const legacyPath = path.join(getLegacyConfigDir(), 'config.json');
168
+ const raw = await fs.readFile(legacyPath, 'utf8');
169
+ const parsed = JSON.parse(raw);
170
+ return normalizePolicyLists(deepMerge(DEFAULT_CONFIG, parsed));
171
+ } catch {
172
+ return normalizePolicyLists(structuredClone(DEFAULT_CONFIG));
173
+ }
174
+ }
175
+ }
176
+
177
+ export async function saveConfig(config) {
178
+ const configPath = getConfigFilePath();
179
+ await ensureDir(configPath);
180
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
181
+ }
182
+
183
+ export async function setConfigValue(keyPath, value) {
184
+ const config = await loadConfig();
185
+ setNested(config, keyPath, value);
186
+ await saveConfig(config);
187
+ }
188
+
189
+ export async function getConfigValue(keyPath) {
190
+ const config = await loadConfig();
191
+ return getNested(config, keyPath);
192
+ }
193
+
194
+ export async function resetConfig() {
195
+ await saveConfig(structuredClone(DEFAULT_CONFIG));
196
+ }
@@ -0,0 +1,90 @@
1
+ function textFromContent(content) {
2
+ if (typeof content === 'string') return content;
3
+ if (Array.isArray(content)) {
4
+ return content
5
+ .map((part) => {
6
+ if (typeof part === 'string') return part;
7
+ if (part?.type === 'text') return part.text || '';
8
+ return '';
9
+ })
10
+ .join('');
11
+ }
12
+ return '';
13
+ }
14
+
15
+ export function estimateMessagesTokens(messages) {
16
+ let total = 0;
17
+ for (const message of messages || []) {
18
+ const roleOverhead = 6;
19
+ const text = textFromContent(message.content);
20
+ total += roleOverhead + Math.ceil(text.length / 4);
21
+ }
22
+ return total;
23
+ }
24
+
25
+ function modeToKeepRecent(mode) {
26
+ if (mode === 'aggressive') return 4;
27
+ if (mode === 'conservative') return 10;
28
+ return 6;
29
+ }
30
+
31
+ function buildLocalSummary(messages) {
32
+ const lines = [];
33
+ const limit = 12;
34
+ for (const msg of messages.slice(-limit)) {
35
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
36
+ if (!text) continue;
37
+ const clipped = text.length > 160 ? `${text.slice(0, 160)}...` : text;
38
+ lines.push(`- ${msg.role}: ${clipped}`);
39
+ }
40
+ return `Context Summary\n${lines.join('\n')}`.trim();
41
+ }
42
+
43
+ export function compactMessagesLocally(messages, { mode = 'default' } = {}) {
44
+ const keepRecent = modeToKeepRecent(mode);
45
+ if (!Array.isArray(messages) || messages.length <= keepRecent + 1) {
46
+ return {
47
+ compacted: [...(messages || [])],
48
+ changed: false
49
+ };
50
+ }
51
+
52
+ const older = messages.slice(0, Math.max(0, messages.length - keepRecent));
53
+ const recent = messages.slice(Math.max(0, messages.length - keepRecent));
54
+ const summary = buildLocalSummary(older);
55
+ const compacted = [{ role: 'assistant', content: summary }, ...recent];
56
+
57
+ return {
58
+ compacted,
59
+ changed: true,
60
+ summary
61
+ };
62
+ }
63
+
64
+ export function parseCompactArgs(args = []) {
65
+ const parsed = {
66
+ mode: 'default',
67
+ preview: false,
68
+ restore: false,
69
+ auto: undefined,
70
+ threshold: undefined
71
+ };
72
+
73
+ for (let i = 0; i < args.length; i += 1) {
74
+ const arg = args[i];
75
+ if (arg === '--preview') parsed.preview = true;
76
+ if (arg === '--restore') parsed.restore = true;
77
+ if (arg === '--aggressive') parsed.mode = 'aggressive';
78
+ if (arg === '--conservative') parsed.mode = 'conservative';
79
+ if (arg === '--default') parsed.mode = 'default';
80
+ if (arg === '--auto-on') parsed.auto = 'on';
81
+ if (arg === '--auto-off') parsed.auto = 'off';
82
+ if (arg === '--threshold') {
83
+ const n = Number(args[i + 1]);
84
+ if (!Number.isNaN(n)) parsed.threshold = n;
85
+ i += 1;
86
+ }
87
+ }
88
+
89
+ return parsed;
90
+ }
@@ -0,0 +1,5 @@
1
+ import { getShellSystemPrompt } from './shell-profile.js';
2
+
3
+ export function buildDefaultSystemPrompt(config = {}) {
4
+ return `${getShellSystemPrompt(config?.shell?.default)} If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools. Do not claim filesystem access is impossible unless the allowed search/read tools also fail.`;
5
+ }
@@ -0,0 +1,16 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export async function copyRecursive(src, dest) {
5
+ const stat = await fs.stat(src);
6
+ if (stat.isDirectory()) {
7
+ await fs.mkdir(dest, { recursive: true });
8
+ const entries = await fs.readdir(src);
9
+ for (const entry of entries) {
10
+ await copyRecursive(path.join(src, entry), path.join(dest, entry));
11
+ }
12
+ return;
13
+ }
14
+ await fs.mkdir(path.dirname(dest), { recursive: true });
15
+ await fs.copyFile(src, dest);
16
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getInputHistoryFilePath } from './paths.js';
4
+
5
+ const MAX_HISTORY = 300;
6
+
7
+ async function ensureDir(filePath) {
8
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
9
+ }
10
+
11
+ function normalizeList(value) {
12
+ if (!Array.isArray(value)) return [];
13
+ const lines = value
14
+ .map((v) => String(v || '').trim())
15
+ .filter(Boolean);
16
+ if (lines.length <= MAX_HISTORY) return lines;
17
+ return lines.slice(lines.length - MAX_HISTORY);
18
+ }
19
+
20
+ export async function loadInputHistory() {
21
+ const filePath = getInputHistoryFilePath();
22
+ try {
23
+ const raw = await fs.readFile(filePath, 'utf8');
24
+ const parsed = JSON.parse(raw);
25
+ return normalizeList(parsed?.items);
26
+ } catch {
27
+ return [];
28
+ }
29
+ }
30
+
31
+ export async function appendInputHistory(line) {
32
+ const normalized = String(line || '').trim();
33
+ if (!normalized) return [];
34
+
35
+ const filePath = getInputHistoryFilePath();
36
+ const existing = await loadInputHistory();
37
+ const next = existing.filter((v) => v !== normalized);
38
+ next.push(normalized);
39
+ const finalList = normalizeList(next);
40
+
41
+ await ensureDir(filePath);
42
+ await fs.writeFile(
43
+ filePath,
44
+ `${JSON.stringify({ updatedAt: new Date().toISOString(), items: finalList }, null, 2)}\n`,
45
+ 'utf8'
46
+ );
47
+ return finalList;
48
+ }