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,15 @@
|
|
|
1
|
+
export function parseInput(input) {
|
|
2
|
+
const trimmed = input.trim();
|
|
3
|
+
if (!trimmed) {
|
|
4
|
+
return { type: 'empty' };
|
|
5
|
+
}
|
|
6
|
+
if (trimmed.startsWith('!')) {
|
|
7
|
+
return { type: 'shell', command: trimmed.slice(1).trim() };
|
|
8
|
+
}
|
|
9
|
+
if (trimmed.startsWith('/')) {
|
|
10
|
+
const body = trimmed.slice(1).trim();
|
|
11
|
+
const [command = '', ...args] = body.split(/\s+/);
|
|
12
|
+
return { type: 'slash', command, args, full: body };
|
|
13
|
+
}
|
|
14
|
+
return { type: 'chat', text: trimmed };
|
|
15
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const APP_DIR = 'codemini-cli';
|
|
6
|
+
const LEGACY_APP_DIR = 'company-coder';
|
|
7
|
+
|
|
8
|
+
function getPreferredBaseConfigDir() {
|
|
9
|
+
if (process.env.CODEMINI_CONFIG_DIR) {
|
|
10
|
+
return process.env.CODEMINI_CONFIG_DIR;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (process.env.COMPANY_CODER_CONFIG_DIR) {
|
|
14
|
+
return process.env.COMPANY_CODER_CONFIG_DIR;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
19
|
+
return path.join(appData, APP_DIR);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (process.platform === 'darwin') {
|
|
23
|
+
return path.join(os.homedir(), 'Library', 'Preferences', APP_DIR);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
27
|
+
return path.join(process.env.XDG_CONFIG_HOME, APP_DIR);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback for restricted/sandboxed non-Windows environments.
|
|
31
|
+
return path.join(process.cwd(), '.codemini-cli');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getLegacyBaseConfigDir() {
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
37
|
+
return path.join(appData, LEGACY_APP_DIR);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (process.platform === 'darwin') {
|
|
41
|
+
return path.join(os.homedir(), 'Library', 'Preferences', LEGACY_APP_DIR);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
45
|
+
return path.join(process.env.XDG_CONFIG_HOME, LEGACY_APP_DIR);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return path.join(process.cwd(), '.company-coder');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tryMigrateLegacyDir(preferred, legacy) {
|
|
52
|
+
if (!preferred || !legacy || preferred === legacy) return preferred;
|
|
53
|
+
if (fs.existsSync(preferred) || !fs.existsSync(legacy)) return preferred;
|
|
54
|
+
try {
|
|
55
|
+
fs.renameSync(legacy, preferred);
|
|
56
|
+
return preferred;
|
|
57
|
+
} catch {
|
|
58
|
+
return preferred;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getBaseConfigDir() {
|
|
63
|
+
const preferred = getPreferredBaseConfigDir();
|
|
64
|
+
if (process.env.CODEMINI_CONFIG_DIR || process.env.COMPANY_CODER_CONFIG_DIR) {
|
|
65
|
+
return preferred;
|
|
66
|
+
}
|
|
67
|
+
const legacy = getLegacyBaseConfigDir();
|
|
68
|
+
return tryMigrateLegacyDir(preferred, legacy);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getLegacyConfigDir() {
|
|
72
|
+
return getLegacyBaseConfigDir();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getConfigFilePath() {
|
|
76
|
+
return path.join(getBaseConfigDir(), 'config.json');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getSessionsDir() {
|
|
80
|
+
return path.join(getBaseConfigDir(), 'sessions');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getSkillsDir() {
|
|
84
|
+
return path.join(getBaseConfigDir(), 'skills');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getSkillRegistryPath() {
|
|
88
|
+
return path.join(getBaseConfigDir(), 'skill-registry.json');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getCommandsDir() {
|
|
92
|
+
return path.join(getBaseConfigDir(), 'commands');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getInputHistoryFilePath() {
|
|
96
|
+
return path.join(getBaseConfigDir(), 'input-history.json');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getProjectCommandsDir(cwd = process.cwd()) {
|
|
100
|
+
return path.join(cwd, '.coder', 'commands');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getLegacyProjectSkillsDir(cwd = process.cwd()) {
|
|
104
|
+
return path.join(cwd, '.coder', 'skills');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getLegacyGlobalSkillsDir() {
|
|
108
|
+
return path.join(getBaseConfigDir(), 'skills');
|
|
109
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
|
|
3
|
+
function extractTextContent(content) {
|
|
4
|
+
if (typeof content === 'string') return content;
|
|
5
|
+
if (Array.isArray(content)) {
|
|
6
|
+
return content
|
|
7
|
+
.map((part) => {
|
|
8
|
+
if (typeof part === 'string') return part;
|
|
9
|
+
if (part?.type === 'text') return part.text || '';
|
|
10
|
+
return '';
|
|
11
|
+
})
|
|
12
|
+
.join('');
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function emptyToolCall(index) {
|
|
18
|
+
return {
|
|
19
|
+
index,
|
|
20
|
+
id: '',
|
|
21
|
+
name: '',
|
|
22
|
+
arguments: ''
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createClient({ baseUrl, apiKey, timeoutMs, maxRetries }) {
|
|
27
|
+
return new OpenAI({
|
|
28
|
+
apiKey,
|
|
29
|
+
baseURL: baseUrl,
|
|
30
|
+
timeout: timeoutMs,
|
|
31
|
+
maxRetries
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isMiniMaxModel(model) {
|
|
36
|
+
return String(model || '').toLowerCase().includes('minimax');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildPayload({ model, temperature, messages, tools, stream = false }) {
|
|
40
|
+
const payload = {
|
|
41
|
+
model,
|
|
42
|
+
temperature,
|
|
43
|
+
messages
|
|
44
|
+
};
|
|
45
|
+
if (stream) {
|
|
46
|
+
payload.stream = true;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
49
|
+
payload.tools = tools;
|
|
50
|
+
payload.tool_choice = 'auto';
|
|
51
|
+
}
|
|
52
|
+
if (isMiniMaxModel(model)) {
|
|
53
|
+
payload.extra_body = { reasoning_split: true };
|
|
54
|
+
}
|
|
55
|
+
return payload;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage) {
|
|
59
|
+
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
60
|
+
.sort((a, b) => a[0] - b[0])
|
|
61
|
+
.map(([, tc], i) => ({
|
|
62
|
+
id: tc.id || `tc-${i + 1}`,
|
|
63
|
+
name: tc.name,
|
|
64
|
+
arguments: tc.arguments || '{}'
|
|
65
|
+
}))
|
|
66
|
+
.filter((tc) => tc.name);
|
|
67
|
+
|
|
68
|
+
if (!text && toolCalls.length === 0) {
|
|
69
|
+
throw new Error('Gateway stream returned empty assistant response');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
text,
|
|
74
|
+
toolCalls,
|
|
75
|
+
usage
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stripMiniMaxThinkContent(text) {
|
|
80
|
+
const input = String(text || '');
|
|
81
|
+
if (!input) return '';
|
|
82
|
+
|
|
83
|
+
let cursor = 0;
|
|
84
|
+
let out = '';
|
|
85
|
+
let removedThink = false;
|
|
86
|
+
|
|
87
|
+
while (cursor < input.length) {
|
|
88
|
+
const openIdx = input.indexOf('<think>', cursor);
|
|
89
|
+
const closeIdx = input.indexOf('</think>', cursor);
|
|
90
|
+
|
|
91
|
+
if (openIdx === -1 && closeIdx === -1) {
|
|
92
|
+
out += input.slice(cursor);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (closeIdx !== -1 && (openIdx === -1 || closeIdx < openIdx)) {
|
|
97
|
+
removedThink = true;
|
|
98
|
+
cursor = closeIdx + '</think>'.length;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
out += input.slice(cursor, openIdx);
|
|
103
|
+
const closingTagIdx = input.indexOf('</think>', openIdx + '<think>'.length);
|
|
104
|
+
removedThink = true;
|
|
105
|
+
if (closingTagIdx === -1) {
|
|
106
|
+
cursor = input.length;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
cursor = closingTagIdx + '</think>'.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return removedThink ? out.trimStart() : out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sanitizeMiniMaxText(model, text) {
|
|
116
|
+
return isMiniMaxModel(model) ? stripMiniMaxThinkContent(text) : text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function nextMiniMaxVisibleChunk(state, content) {
|
|
120
|
+
const rawChunk = extractTextContent(content);
|
|
121
|
+
if (!rawChunk) {
|
|
122
|
+
return { textDelta: '', nextState: state };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nextRawContent = rawChunk.startsWith(state.rawContent) ? rawChunk : `${state.rawContent}${rawChunk}`;
|
|
126
|
+
const nextVisibleText = stripMiniMaxThinkContent(nextRawContent);
|
|
127
|
+
const textDelta = nextVisibleText.startsWith(state.visibleText)
|
|
128
|
+
? nextVisibleText.slice(state.visibleText.length)
|
|
129
|
+
: nextVisibleText;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
textDelta,
|
|
133
|
+
nextState: {
|
|
134
|
+
rawContent: nextRawContent,
|
|
135
|
+
visibleText: nextVisibleText
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function createChatCompletion({
|
|
141
|
+
baseUrl,
|
|
142
|
+
apiKey,
|
|
143
|
+
model,
|
|
144
|
+
messages,
|
|
145
|
+
temperature = 0.2,
|
|
146
|
+
tools,
|
|
147
|
+
timeoutMs = 90000,
|
|
148
|
+
maxRetries = 2
|
|
149
|
+
}) {
|
|
150
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
151
|
+
const payload = buildPayload({ model, temperature, messages, tools });
|
|
152
|
+
|
|
153
|
+
const data = await client.chat.completions.create(payload);
|
|
154
|
+
const message = data?.choices?.[0]?.message || {};
|
|
155
|
+
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
156
|
+
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
157
|
+
id: tc.id,
|
|
158
|
+
name: tc.function?.name,
|
|
159
|
+
arguments: tc.function?.arguments || '{}'
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
if (!text && toolCalls.length === 0) {
|
|
163
|
+
throw new Error('Gateway returned empty assistant response');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
text,
|
|
168
|
+
toolCalls,
|
|
169
|
+
usage: data?.usage || null
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function createChatCompletionStream({
|
|
174
|
+
baseUrl,
|
|
175
|
+
apiKey,
|
|
176
|
+
model,
|
|
177
|
+
messages,
|
|
178
|
+
temperature = 0.2,
|
|
179
|
+
tools,
|
|
180
|
+
onTextDelta,
|
|
181
|
+
timeoutMs = 90000,
|
|
182
|
+
maxRetries = 2
|
|
183
|
+
}) {
|
|
184
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
185
|
+
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
186
|
+
|
|
187
|
+
const stream = await client.chat.completions.create(payload);
|
|
188
|
+
let text = '';
|
|
189
|
+
const toolCallsByIndex = new Map();
|
|
190
|
+
let usage = null;
|
|
191
|
+
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
192
|
+
|
|
193
|
+
for await (const chunk of stream) {
|
|
194
|
+
usage = chunk?.usage || usage;
|
|
195
|
+
const choice0 = chunk?.choices?.[0] || {};
|
|
196
|
+
const delta = choice0?.delta || {};
|
|
197
|
+
const content = delta.content;
|
|
198
|
+
if (isMiniMaxModel(model)) {
|
|
199
|
+
const next = nextMiniMaxVisibleChunk(miniMaxStreamState, content);
|
|
200
|
+
miniMaxStreamState = next.nextState;
|
|
201
|
+
if (next.textDelta) {
|
|
202
|
+
text += next.textDelta;
|
|
203
|
+
if (onTextDelta) onTextDelta(next.textDelta);
|
|
204
|
+
}
|
|
205
|
+
} else if (typeof content === 'string' && content.length > 0) {
|
|
206
|
+
text += content;
|
|
207
|
+
if (onTextDelta) onTextDelta(content);
|
|
208
|
+
} else if (Array.isArray(content) && content.length > 0) {
|
|
209
|
+
const chunkText = extractTextContent(content);
|
|
210
|
+
if (chunkText) {
|
|
211
|
+
text += chunkText;
|
|
212
|
+
if (onTextDelta) onTextDelta(chunkText);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const toolDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
217
|
+
for (const td of toolDeltas) {
|
|
218
|
+
const idx = typeof td.index === 'number' ? td.index : 0;
|
|
219
|
+
const current = toolCallsByIndex.get(idx) || emptyToolCall(idx);
|
|
220
|
+
if (td.id) current.id = td.id;
|
|
221
|
+
if (td.function?.name) current.name = `${current.name}${td.function.name}`;
|
|
222
|
+
if (td.function?.arguments) current.arguments = `${current.arguments}${td.function.arguments}`;
|
|
223
|
+
toolCallsByIndex.set(idx, current);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return buildFinalStreamResult(text, toolCallsByIndex, usage);
|
|
228
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getSessionsDir } from './paths.js';
|
|
4
|
+
|
|
5
|
+
const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
6
|
+
|
|
7
|
+
function createSessionId() {
|
|
8
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
9
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
10
|
+
return `${ts}-${rand}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sanitizeToolCall(tc, index) {
|
|
14
|
+
const id = String(tc?.id || `tc-${index + 1}`);
|
|
15
|
+
const fnName = String(tc?.function?.name || tc?.name || '').trim();
|
|
16
|
+
const fnArgsRaw = tc?.function?.arguments ?? tc?.arguments ?? '{}';
|
|
17
|
+
const fnArgs = typeof fnArgsRaw === 'string' ? fnArgsRaw : JSON.stringify(fnArgsRaw);
|
|
18
|
+
if (!fnName) return null;
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
type: 'function',
|
|
22
|
+
function: {
|
|
23
|
+
name: fnName,
|
|
24
|
+
arguments: fnArgs
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sanitizeMessage(msg) {
|
|
30
|
+
const role = String(msg?.role || '').trim();
|
|
31
|
+
if (!ALLOWED_ROLES.has(role)) return null;
|
|
32
|
+
const content =
|
|
33
|
+
typeof msg?.content === 'string' || Array.isArray(msg?.content) ? msg.content : String(msg?.content || '');
|
|
34
|
+
|
|
35
|
+
const out = {
|
|
36
|
+
role,
|
|
37
|
+
content
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (msg?.tool_call_id) out.tool_call_id = String(msg.tool_call_id);
|
|
41
|
+
if (typeof msg?.name === 'string' && msg.name.trim()) out.name = msg.name.trim();
|
|
42
|
+
if (typeof msg?.at === 'string' && msg.at.trim()) out.at = msg.at;
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(msg?.tool_calls)) {
|
|
45
|
+
const toolCalls = msg.tool_calls.map(sanitizeToolCall).filter(Boolean);
|
|
46
|
+
if (toolCalls.length > 0) out.tool_calls = toolCalls;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sanitizeSession(session, fallbackId = '') {
|
|
53
|
+
const id = String(session?.id || fallbackId || '').trim();
|
|
54
|
+
if (!id) throw new Error('Session id is required');
|
|
55
|
+
const now = new Date().toISOString();
|
|
56
|
+
const createdAt = String(session?.createdAt || now);
|
|
57
|
+
const updatedAt = String(session?.updatedAt || now);
|
|
58
|
+
const messages = Array.isArray(session?.messages) ? session.messages.map(sanitizeMessage).filter(Boolean) : [];
|
|
59
|
+
|
|
60
|
+
const out = {
|
|
61
|
+
id,
|
|
62
|
+
createdAt,
|
|
63
|
+
updatedAt,
|
|
64
|
+
messages
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (session?.model) out.model = String(session.model);
|
|
68
|
+
if (session?.mode) out.mode = String(session.mode);
|
|
69
|
+
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function createSession() {
|
|
74
|
+
const sessionId = createSessionId();
|
|
75
|
+
const dir = getSessionsDir();
|
|
76
|
+
await fs.mkdir(dir, { recursive: true });
|
|
77
|
+
const filePath = path.join(dir, `${sessionId}.json`);
|
|
78
|
+
const payload = {
|
|
79
|
+
id: sessionId,
|
|
80
|
+
createdAt: new Date().toISOString(),
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
messages: []
|
|
83
|
+
};
|
|
84
|
+
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
85
|
+
return payload;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function loadSession(sessionId) {
|
|
89
|
+
const filePath = path.join(getSessionsDir(), `${sessionId}.json`);
|
|
90
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
91
|
+
return sanitizeSession(JSON.parse(raw), sessionId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function saveSession(session) {
|
|
95
|
+
const dir = getSessionsDir();
|
|
96
|
+
await fs.mkdir(dir, { recursive: true });
|
|
97
|
+
const normalized = sanitizeSession(session);
|
|
98
|
+
normalized.updatedAt = new Date().toISOString();
|
|
99
|
+
const filePath = path.join(dir, `${normalized.id}.json`);
|
|
100
|
+
await fs.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function resolveSession(sessionId) {
|
|
104
|
+
if (sessionId) {
|
|
105
|
+
return loadSession(sessionId);
|
|
106
|
+
}
|
|
107
|
+
return createSession();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function listSessions(limit = 30) {
|
|
111
|
+
const dir = getSessionsDir();
|
|
112
|
+
await fs.mkdir(dir, { recursive: true });
|
|
113
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
114
|
+
const files = entries
|
|
115
|
+
.filter((e) => e.isFile() && e.name.endsWith('.json'))
|
|
116
|
+
.map((e) => path.join(dir, e.name));
|
|
117
|
+
|
|
118
|
+
const sessions = [];
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
122
|
+
const parsed = JSON.parse(raw);
|
|
123
|
+
const id = parsed.id || path.basename(file, '.json');
|
|
124
|
+
const updatedAt = parsed.updatedAt || parsed.createdAt || '';
|
|
125
|
+
const latestMessage = Array.isArray(parsed.messages) ? parsed.messages.at(-1) : null;
|
|
126
|
+
const preview = latestMessage?.content
|
|
127
|
+
? String(latestMessage.content).replace(/\s+/g, ' ').slice(0, 80)
|
|
128
|
+
: '';
|
|
129
|
+
sessions.push({
|
|
130
|
+
id,
|
|
131
|
+
updatedAt,
|
|
132
|
+
messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
|
|
133
|
+
preview
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
141
|
+
return sessions.filter((s) => Number(s.messageCount || 0) > 0).slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function pruneSessions(policy = {}) {
|
|
145
|
+
const maxSessions = Number(policy.max_sessions || 100);
|
|
146
|
+
const retentionDays = Number(policy.retention_days || 30);
|
|
147
|
+
const all = await listSessions(10000);
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const expireMs = retentionDays > 0 ? retentionDays * 24 * 60 * 60 * 1000 : 0;
|
|
150
|
+
const keepIds = new Set();
|
|
151
|
+
|
|
152
|
+
const sorted = [...all].sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
153
|
+
for (let i = 0; i < sorted.length; i += 1) {
|
|
154
|
+
const s = sorted[i];
|
|
155
|
+
if (i >= maxSessions) continue;
|
|
156
|
+
if (expireMs > 0 && s.updatedAt) {
|
|
157
|
+
const t = Date.parse(s.updatedAt);
|
|
158
|
+
if (!Number.isNaN(t) && now - t > expireMs) continue;
|
|
159
|
+
}
|
|
160
|
+
keepIds.add(s.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const dir = getSessionsDir();
|
|
164
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
165
|
+
let removed = 0;
|
|
166
|
+
for (const e of entries) {
|
|
167
|
+
if (!e.isFile() || !e.name.endsWith('.json')) continue;
|
|
168
|
+
const id = path.basename(e.name, '.json');
|
|
169
|
+
if (keepIds.has(id)) continue;
|
|
170
|
+
try {
|
|
171
|
+
await fs.unlink(path.join(dir, e.name));
|
|
172
|
+
removed += 1;
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { removed, kept: keepIds.size };
|
|
178
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const DEFAULT_SHELL = process.platform === 'win32' ? 'powershell' : 'bash';
|
|
2
|
+
|
|
3
|
+
function uniqueStrings(items = []) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
const value = String(item || '').trim();
|
|
8
|
+
if (!value || seen.has(value)) continue;
|
|
9
|
+
seen.add(value);
|
|
10
|
+
out.push(value);
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SHELL_PROFILES = {
|
|
16
|
+
powershell: {
|
|
17
|
+
shell: 'powershell',
|
|
18
|
+
label: 'PowerShell',
|
|
19
|
+
command_allowlist: [
|
|
20
|
+
'rg',
|
|
21
|
+
'git',
|
|
22
|
+
'node',
|
|
23
|
+
'npm',
|
|
24
|
+
'npx',
|
|
25
|
+
'python',
|
|
26
|
+
'py',
|
|
27
|
+
'pip',
|
|
28
|
+
'get-childitem',
|
|
29
|
+
'get-content',
|
|
30
|
+
'select-string',
|
|
31
|
+
'set-content',
|
|
32
|
+
'new-item',
|
|
33
|
+
'copy-item',
|
|
34
|
+
'move-item',
|
|
35
|
+
'pwd'
|
|
36
|
+
],
|
|
37
|
+
blocked_commands: [
|
|
38
|
+
'del',
|
|
39
|
+
'erase',
|
|
40
|
+
'rmdir',
|
|
41
|
+
'rd',
|
|
42
|
+
'format',
|
|
43
|
+
'diskpart',
|
|
44
|
+
'cipher',
|
|
45
|
+
'bcdedit',
|
|
46
|
+
'reg',
|
|
47
|
+
'takeown',
|
|
48
|
+
'icacls',
|
|
49
|
+
'remove-item'
|
|
50
|
+
],
|
|
51
|
+
blocked_path_patterns: [
|
|
52
|
+
'c:\\windows',
|
|
53
|
+
'c:\\program files',
|
|
54
|
+
'c:\\program files (x86)',
|
|
55
|
+
'c:\\users\\default',
|
|
56
|
+
'%systemroot%',
|
|
57
|
+
'$env:systemroot'
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
bash: {
|
|
61
|
+
shell: 'bash',
|
|
62
|
+
label: 'bash',
|
|
63
|
+
command_allowlist: [
|
|
64
|
+
'rg',
|
|
65
|
+
'find',
|
|
66
|
+
'grep',
|
|
67
|
+
'git',
|
|
68
|
+
'node',
|
|
69
|
+
'npm',
|
|
70
|
+
'npx',
|
|
71
|
+
'python',
|
|
72
|
+
'pip',
|
|
73
|
+
'ls',
|
|
74
|
+
'cat',
|
|
75
|
+
'sed',
|
|
76
|
+
'cp',
|
|
77
|
+
'mv',
|
|
78
|
+
'mkdir',
|
|
79
|
+
'pwd'
|
|
80
|
+
],
|
|
81
|
+
blocked_commands: ['rm', 'sudo', 'su', 'dd', 'mkfs', 'mount', 'umount', 'chmod', 'chown'],
|
|
82
|
+
blocked_path_patterns: ['/etc/', '/bin/', '/usr/', '/var/', '/sys/', '/proc/', '/system/', '/library/']
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function normalizeShellName(value) {
|
|
87
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
88
|
+
if (raw === 'pwsh') return 'powershell';
|
|
89
|
+
if (raw === 'sh' || raw === 'zsh') return 'bash';
|
|
90
|
+
if (raw === 'cmd') return 'powershell';
|
|
91
|
+
if (raw === 'powershell' || raw === 'bash') return raw;
|
|
92
|
+
return DEFAULT_SHELL;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getShellProfile(value) {
|
|
96
|
+
return SHELL_PROFILES[normalizeShellName(value)];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getEffectivePolicy(config) {
|
|
100
|
+
const profile = getShellProfile(config?.shell?.default);
|
|
101
|
+
const policy = config?.policy || {};
|
|
102
|
+
return {
|
|
103
|
+
...policy,
|
|
104
|
+
command_allowlist: uniqueStrings([
|
|
105
|
+
...(Array.isArray(profile.command_allowlist) ? profile.command_allowlist : []),
|
|
106
|
+
...(Array.isArray(policy.command_allowlist) ? policy.command_allowlist : [])
|
|
107
|
+
]),
|
|
108
|
+
blocked_commands: uniqueStrings([
|
|
109
|
+
...(Array.isArray(profile.blocked_commands) ? profile.blocked_commands : []),
|
|
110
|
+
...(Array.isArray(policy.blocked_commands) ? policy.blocked_commands : [])
|
|
111
|
+
]),
|
|
112
|
+
blocked_path_patterns: uniqueStrings([
|
|
113
|
+
...(Array.isArray(profile.blocked_path_patterns) ? profile.blocked_path_patterns : []),
|
|
114
|
+
...(Array.isArray(policy.blocked_path_patterns) ? policy.blocked_path_patterns : [])
|
|
115
|
+
])
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getShellSystemPrompt(value) {
|
|
120
|
+
const profile = getShellProfile(value);
|
|
121
|
+
return `You are CodeMini CLI working in a ${profile.label} shell environment. For codebase exploration, use the allowed shell search and context commands that best fit the task, then use read_file only when command output is not enough. Use write_file for edits and always provide a concrete file path, not a directory. Avoid unnecessary tool calls.`;
|
|
122
|
+
}
|