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.
- package/LICENSE +21 -0
- package/OPERATIONS.md +202 -0
- package/README.md +138 -0
- package/bin/coder.js +7 -0
- package/deployment.md +205 -0
- package/package.json +54 -0
- package/skills/brainstorming-lite/SKILL.md +37 -0
- package/skills/executing-plan-lite/SKILL.md +41 -0
- package/skills/superpowers-lite/SKILL.md +44 -0
- package/souls/anime.md +3 -0
- package/souls/default.md +3 -0
- package/souls/playful.md +3 -0
- package/souls/professional.md +3 -0
- package/src/cli.js +62 -0
- package/src/commands/chat.js +106 -0
- package/src/commands/config.js +61 -0
- package/src/commands/doctor.js +87 -0
- package/src/commands/run.js +64 -0
- package/src/commands/skill.js +264 -0
- package/src/core/agent-loop.js +281 -0
- package/src/core/chat-runtime.js +2075 -0
- package/src/core/checkpoint-store.js +66 -0
- package/src/core/command-loader.js +201 -0
- package/src/core/command-policy.js +71 -0
- package/src/core/config-store.js +196 -0
- package/src/core/context-compact.js +90 -0
- package/src/core/default-system-prompt.js +5 -0
- package/src/core/fs-utils.js +16 -0
- package/src/core/input-history-store.js +48 -0
- package/src/core/input-parser.js +15 -0
- package/src/core/paths.js +109 -0
- package/src/core/provider/openai-compatible.js +228 -0
- package/src/core/session-store.js +178 -0
- package/src/core/shell-profile.js +122 -0
- package/src/core/shell.js +71 -0
- package/src/core/skill-registry.js +55 -0
- package/src/core/soul.js +55 -0
- package/src/core/task-store.js +116 -0
- package/src/core/tools.js +237 -0
- package/src/tui/chat-app.js +2007 -0
- 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
|
+
}
|