codemini-cli 0.3.5 → 0.3.7
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/README.md +20 -18
- package/package.json +6 -6
- package/souls/anime.md +5 -2
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +159 -47
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +712 -126
- package/src/core/command-policy.js +56 -0
- package/src/core/config-store.js +0 -3
- package/src/core/crypto-utils.js +6 -2
- package/src/core/memory-store.js +3 -3
- package/src/core/project-index.js +4 -18
- package/src/core/provider/anthropic.js +15 -2
- package/src/core/provider/openai-compatible.js +15 -2
- package/src/core/session-store.js +82 -25
- package/src/core/shell-profile.js +17 -1
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +152 -393
- package/src/tui/chat-app.js +461 -147
- package/src/tui/tool-activity/presenters/files.js +2 -2
- package/src/tui/tool-narration.js +0 -3
- package/src/tui/tool-narration/presenters/patch.js +0 -3
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getEffectivePolicy } from './shell-profile.js';
|
|
3
3
|
|
|
4
|
+
const SHELL_KEYWORDS = new Set([
|
|
5
|
+
'if',
|
|
6
|
+
'then',
|
|
7
|
+
'elif',
|
|
8
|
+
'else',
|
|
9
|
+
'fi',
|
|
10
|
+
'for',
|
|
11
|
+
'while',
|
|
12
|
+
'until',
|
|
13
|
+
'do',
|
|
14
|
+
'done',
|
|
15
|
+
'case',
|
|
16
|
+
'esac',
|
|
17
|
+
'in',
|
|
18
|
+
'function',
|
|
19
|
+
'time',
|
|
20
|
+
'{',
|
|
21
|
+
'}'
|
|
22
|
+
]);
|
|
23
|
+
|
|
4
24
|
function firstToken(command) {
|
|
5
25
|
const m = String(command || '').trim().match(/^"([^"]+)"|^'([^']+)'|^(\S+)/);
|
|
6
26
|
const raw = (m && (m[1] || m[2] || m[3])) || '';
|
|
@@ -51,6 +71,11 @@ function splitCommandSegments(command) {
|
|
|
51
71
|
continue;
|
|
52
72
|
}
|
|
53
73
|
|
|
74
|
+
if (ch === '&' && text[i - 1] === '>') {
|
|
75
|
+
current += ch;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
if (ch === '|' || ch === ';' || ch === '&') {
|
|
55
80
|
if (current.trim()) segments.push(current.trim());
|
|
56
81
|
current = '';
|
|
@@ -157,6 +182,30 @@ function suggestionForToken(token, config) {
|
|
|
157
182
|
return 'Prefer structured tools like read, edit, write, grep, glob, and list first. If you need shell fallback, use allowed shell commands for search and local context such as rg, find, grep, sed, cat, or ls.';
|
|
158
183
|
}
|
|
159
184
|
|
|
185
|
+
function validateCdSegment(command, workspaceRoot) {
|
|
186
|
+
const tokens = tokenizeTopLevel(command);
|
|
187
|
+
if (tokens.length === 1) {
|
|
188
|
+
return { allowed: false, reason: 'cd requires a target path in safe mode' };
|
|
189
|
+
}
|
|
190
|
+
if (tokens.length !== 2) {
|
|
191
|
+
return { allowed: false, reason: 'cd only supports a single target path in safe mode' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rawTarget = String(tokens[1] || '').trim();
|
|
195
|
+
if (!rawTarget || rawTarget.startsWith('-')) {
|
|
196
|
+
return { allowed: false, reason: 'cd target is not allowed in safe mode' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
200
|
+
const resolvedTarget = path.resolve(resolvedRoot, rawTarget);
|
|
201
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
202
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
203
|
+
return { allowed: false, reason: `cd escapes workspace: ${rawTarget}` };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { allowed: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
160
209
|
export function evaluateCommandPolicy(command, config, workspaceRoot = process.cwd()) {
|
|
161
210
|
const policy = getEffectivePolicy(config);
|
|
162
211
|
const cmd = String(command || '').trim();
|
|
@@ -181,6 +230,13 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
181
230
|
const inspectedTokens = collectCommandTokens(cmd);
|
|
182
231
|
const allowlist = Array.isArray(policy.command_allowlist) ? policy.command_allowlist : [];
|
|
183
232
|
for (const item of inspectedTokens) {
|
|
233
|
+
if (SHELL_KEYWORDS.has(item.token)) continue;
|
|
234
|
+
if (item.token === 'cd') {
|
|
235
|
+
const cdCheck = validateCdSegment(item.raw, workspaceRoot);
|
|
236
|
+
if (!cdCheck.allowed) {
|
|
237
|
+
return { allowed: false, reason: cdCheck.reason, suggestion: suggestionForToken(item.token, config) };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
184
240
|
if (includesAny(item.token, policy.blocked_commands)) {
|
|
185
241
|
return { allowed: false, reason: `blocked command: ${item.token}`, suggestion: suggestionForToken(item.token, config) };
|
|
186
242
|
}
|
package/src/core/config-store.js
CHANGED
|
@@ -44,8 +44,6 @@ const DEFAULT_CONFIG = {
|
|
|
44
44
|
'edit',
|
|
45
45
|
'write',
|
|
46
46
|
'run',
|
|
47
|
-
'patch',
|
|
48
|
-
'generate_diff',
|
|
49
47
|
'list_background_tasks',
|
|
50
48
|
'get_background_task',
|
|
51
49
|
'stop_background_task'
|
|
@@ -151,7 +149,6 @@ function normalizePolicyLists(config) {
|
|
|
151
149
|
'edit',
|
|
152
150
|
'write',
|
|
153
151
|
'run',
|
|
154
|
-
'generate_diff',
|
|
155
152
|
'list_background_tasks',
|
|
156
153
|
'get_background_task',
|
|
157
154
|
'stop_background_task',
|
package/src/core/crypto-utils.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 共享加密工具函数。
|
|
3
|
-
*
|
|
3
|
+
* 统一使用 sha256 作为默认哈希算法,避免在各模块中重复定义。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 向后兼容别名,内部已迁移到 sha256。
|
|
10
|
+
* @deprecated 请使用 sha256() 替代。
|
|
11
|
+
*/
|
|
8
12
|
export function sha1(input) {
|
|
9
|
-
return
|
|
13
|
+
return sha256(input);
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export function sha256(input) {
|
package/src/core/memory-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { sha256 } from './crypto-utils.js';
|
|
4
4
|
import { getMemoryDir, getProjectMemoryDir } from './paths.js';
|
|
5
5
|
import { assertSafeMemoryContent, normalizeMemoryText, summarizeMemoryContent } from './memory-policy.js';
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ export function getProjectMemoryKey(workspaceRoot = process.cwd(), projectAlias
|
|
|
23
23
|
if (alias) return slugify(alias);
|
|
24
24
|
const root = path.resolve(workspaceRoot || process.cwd());
|
|
25
25
|
const base = path.basename(root);
|
|
26
|
-
return `${slugify(base)}-${
|
|
26
|
+
return `${slugify(base)}-${sha256(root).slice(0, 10)}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function ensureScope(scope) {
|
|
@@ -63,7 +63,7 @@ function normalizeMemoryItem(item, scope, projectKey = '') {
|
|
|
63
63
|
const now = nowIso();
|
|
64
64
|
const content = normalizeMemoryText(item?.content || '');
|
|
65
65
|
return {
|
|
66
|
-
id: String(item?.id || `mem_${
|
|
66
|
+
id: String(item?.id || `mem_${sha256(`${scope}:${projectKey}:${content}:${now}:${Math.random()}`).slice(0, 12)}`),
|
|
67
67
|
scope,
|
|
68
68
|
projectKey: projectKey || undefined,
|
|
69
69
|
kind: String(item?.kind || 'note').trim() || 'note',
|
|
@@ -2,8 +2,9 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getFileIndexPath, getProjectIndexDir, getProjectMapPath, getProjectWorkspaceDir } from './paths.js';
|
|
4
4
|
import { INDEX_SKIP_DIRS as SKIP_DIRS, SOURCE_EXTENSIONS, EXTENSION_LANGUAGE_MAP } from './constants.js';
|
|
5
|
-
import {
|
|
5
|
+
import { sha256 } from './crypto-utils.js';
|
|
6
6
|
import { BoundedCache } from './bounded-cache.js';
|
|
7
|
+
import { trimInline, normalizeRelativePath, escapeRegex } from './string-utils.js';
|
|
7
8
|
|
|
8
9
|
const PROJECT_MARKER_FILES = new Set([
|
|
9
10
|
'package.json',
|
|
@@ -31,11 +32,7 @@ function clipList(values, max = 32) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function rel(cwd, filePath) {
|
|
34
|
-
return path.relative(cwd, filePath)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function normalizeRelativePath(value) {
|
|
38
|
-
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/^\/+/, '');
|
|
35
|
+
return normalizeRelativePath(path.relative(cwd, filePath));
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
async function safeStat(filePath) {
|
|
@@ -58,13 +55,6 @@ function tokenizeQuery(text) {
|
|
|
58
55
|
return [...new Set(String(text || '').toLowerCase().match(/[a-z0-9_./-]+/g) || [])].filter(Boolean);
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
function trimInline(value, max = 240) {
|
|
62
|
-
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
63
|
-
if (!text) return '';
|
|
64
|
-
if (text.length <= max) return text;
|
|
65
|
-
return `${text.slice(0, max - 3)}...`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
function trimMultiline(value, max = 1800) {
|
|
69
59
|
const text = String(value || '').trim();
|
|
70
60
|
if (!text) return '';
|
|
@@ -77,10 +67,6 @@ async function writeJson(filePath, value) {
|
|
|
77
67
|
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
78
68
|
}
|
|
79
69
|
|
|
80
|
-
function escapeRegex(value) {
|
|
81
|
-
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
70
|
function gitignorePatternToRegex(pattern) {
|
|
85
71
|
const normalized = normalizeRelativePath(pattern);
|
|
86
72
|
let regexBody = '';
|
|
@@ -282,7 +268,7 @@ function buildFileEntry(relativePath, content, stat) {
|
|
|
282
268
|
return {
|
|
283
269
|
file: relativePath,
|
|
284
270
|
language: LANGUAGE_BY_EXT[ext] || 'text',
|
|
285
|
-
hash:
|
|
271
|
+
hash: sha256(content),
|
|
286
272
|
size: Number(stat?.size || content.length || 0),
|
|
287
273
|
mtimeMs: Number(stat?.mtimeMs || 0),
|
|
288
274
|
imports,
|
|
@@ -318,14 +318,27 @@ export async function createChatCompletionStream({
|
|
|
318
318
|
onTextDelta,
|
|
319
319
|
onToolCallDelta,
|
|
320
320
|
timeoutMs = 90000,
|
|
321
|
-
maxTokens = 4096
|
|
321
|
+
maxTokens = 4096,
|
|
322
|
+
signal: externalSignal
|
|
322
323
|
}) {
|
|
324
|
+
// 合并超时信号与外部中止信号
|
|
325
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
326
|
+
const controller = new AbortController();
|
|
327
|
+
const onAbort = () => controller.abort();
|
|
328
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
329
|
+
if (externalSignal) {
|
|
330
|
+
if (externalSignal.aborted) {
|
|
331
|
+
controller.abort();
|
|
332
|
+
} else {
|
|
333
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
323
336
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true, maxTokens });
|
|
324
337
|
const response = await fetch(buildMessagesUrl(baseUrl), {
|
|
325
338
|
method: 'POST',
|
|
326
339
|
headers: createHeaders(apiKey),
|
|
327
340
|
body: JSON.stringify(payload),
|
|
328
|
-
signal:
|
|
341
|
+
signal: controller.signal
|
|
329
342
|
});
|
|
330
343
|
|
|
331
344
|
if (!response.ok || !response.body) {
|
|
@@ -370,14 +370,27 @@ export async function createChatCompletionStream({
|
|
|
370
370
|
onTextDelta,
|
|
371
371
|
onToolCallDelta,
|
|
372
372
|
timeoutMs = 90000,
|
|
373
|
-
maxRetries = 2
|
|
373
|
+
maxRetries = 2,
|
|
374
|
+
signal: externalSignal
|
|
374
375
|
}) {
|
|
376
|
+
// 合并超时信号与外部中止信号,任一触发都会中止请求
|
|
377
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
378
|
+
const controller = new AbortController();
|
|
379
|
+
const onAbort = () => controller.abort();
|
|
380
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
381
|
+
if (externalSignal) {
|
|
382
|
+
if (externalSignal.aborted) {
|
|
383
|
+
controller.abort();
|
|
384
|
+
} else {
|
|
385
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
375
388
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
376
389
|
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
377
390
|
method: 'POST',
|
|
378
391
|
headers: createHeaders(apiKey),
|
|
379
392
|
body: JSON.stringify(payload),
|
|
380
|
-
signal:
|
|
393
|
+
signal: controller.signal
|
|
381
394
|
});
|
|
382
395
|
if (!response.ok || !response.body) {
|
|
383
396
|
const text = await response.text().catch(() => '');
|
|
@@ -4,6 +4,8 @@ import { getSessionsDir } from './paths.js';
|
|
|
4
4
|
import { normalizeTodos } from './todo-state.js';
|
|
5
5
|
|
|
6
6
|
const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
7
|
+
const SESSION_LEGACY_EXT = '.json';
|
|
8
|
+
const SESSION_JSONL_EXT = '.jsonl';
|
|
7
9
|
|
|
8
10
|
function createSessionId() {
|
|
9
11
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -101,25 +103,85 @@ function sanitizeSession(session, fallbackId = '') {
|
|
|
101
103
|
return out;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
function sessionPathById(sessionId, ext = SESSION_JSONL_EXT) {
|
|
107
|
+
return path.join(getSessionsDir(), `${sessionId}${ext}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sessionIdFromFileName(fileName) {
|
|
111
|
+
if (fileName.endsWith(SESSION_JSONL_EXT)) return fileName.slice(0, -SESSION_JSONL_EXT.length);
|
|
112
|
+
if (fileName.endsWith(SESSION_LEGACY_EXT)) return fileName.slice(0, -SESSION_LEGACY_EXT.length);
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function summarizeParsedSession(parsed, filePath) {
|
|
117
|
+
const id = parsed.id || sessionIdFromFileName(path.basename(filePath));
|
|
118
|
+
const updatedAt = parsed.updatedAt || parsed.createdAt || '';
|
|
119
|
+
const latestMessage = Array.isArray(parsed.messages) ? parsed.messages.at(-1) : null;
|
|
120
|
+
const preview = latestMessage?.content ? String(latestMessage.content).replace(/\s+/g, ' ').slice(0, 80) : '';
|
|
121
|
+
return {
|
|
122
|
+
id,
|
|
123
|
+
updatedAt,
|
|
124
|
+
messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
|
|
125
|
+
preview
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function tryReadJson(filePath) {
|
|
130
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
131
|
+
return JSON.parse(raw);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function loadLatestJsonlObject(filePath) {
|
|
135
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
136
|
+
const lines = String(raw || '')
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map((line) => line.trim())
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(lines[i]);
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`No valid JSONL record found: ${filePath}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function loadSessionPayload(sessionId) {
|
|
151
|
+
const jsonlPath = sessionPathById(sessionId, SESSION_JSONL_EXT);
|
|
152
|
+
let jsonlError = null;
|
|
153
|
+
try {
|
|
154
|
+
return await loadLatestJsonlObject(jsonlPath);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error?.code !== 'ENOENT') jsonlError = error;
|
|
157
|
+
}
|
|
158
|
+
const legacyPath = sessionPathById(sessionId, SESSION_LEGACY_EXT);
|
|
159
|
+
try {
|
|
160
|
+
return await tryReadJson(legacyPath);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (jsonlError) throw jsonlError;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
104
167
|
export async function createSession() {
|
|
105
168
|
const sessionId = createSessionId();
|
|
106
169
|
const dir = getSessionsDir();
|
|
107
170
|
await fs.mkdir(dir, { recursive: true });
|
|
108
|
-
const filePath =
|
|
171
|
+
const filePath = sessionPathById(sessionId, SESSION_JSONL_EXT);
|
|
109
172
|
const payload = {
|
|
110
173
|
id: sessionId,
|
|
111
174
|
createdAt: new Date().toISOString(),
|
|
112
175
|
updatedAt: new Date().toISOString(),
|
|
113
176
|
messages: []
|
|
114
177
|
};
|
|
115
|
-
await fs.writeFile(filePath, `${JSON.stringify(payload
|
|
178
|
+
await fs.writeFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
116
179
|
return payload;
|
|
117
180
|
}
|
|
118
181
|
|
|
119
182
|
export async function loadSession(sessionId) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
return sanitizeSession(JSON.parse(raw), sessionId);
|
|
183
|
+
const parsed = await loadSessionPayload(sessionId);
|
|
184
|
+
return sanitizeSession(parsed, sessionId);
|
|
123
185
|
}
|
|
124
186
|
|
|
125
187
|
export async function saveSession(session) {
|
|
@@ -127,8 +189,8 @@ export async function saveSession(session) {
|
|
|
127
189
|
await fs.mkdir(dir, { recursive: true });
|
|
128
190
|
const normalized = sanitizeSession(session);
|
|
129
191
|
normalized.updatedAt = new Date().toISOString();
|
|
130
|
-
const filePath =
|
|
131
|
-
await fs.
|
|
192
|
+
const filePath = sessionPathById(normalized.id, SESSION_JSONL_EXT);
|
|
193
|
+
await fs.appendFile(filePath, `${JSON.stringify(normalized)}\n`, 'utf8');
|
|
132
194
|
}
|
|
133
195
|
|
|
134
196
|
export async function resolveSession(sessionId) {
|
|
@@ -143,31 +205,25 @@ export async function listSessions(limit = 30) {
|
|
|
143
205
|
await fs.mkdir(dir, { recursive: true });
|
|
144
206
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
145
207
|
const files = entries
|
|
146
|
-
.filter((e) => e.isFile() && e.name.endsWith(
|
|
208
|
+
.filter((e) => e.isFile() && (e.name.endsWith(SESSION_JSONL_EXT) || e.name.endsWith(SESSION_LEGACY_EXT)))
|
|
147
209
|
.map((e) => path.join(dir, e.name));
|
|
148
210
|
|
|
149
|
-
const
|
|
211
|
+
const sessionsById = new Map();
|
|
150
212
|
for (const file of files) {
|
|
151
213
|
try {
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
: '';
|
|
160
|
-
sessions.push({
|
|
161
|
-
id,
|
|
162
|
-
updatedAt,
|
|
163
|
-
messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
|
|
164
|
-
preview
|
|
165
|
-
});
|
|
214
|
+
const parsed = file.endsWith(SESSION_JSONL_EXT) ? await loadLatestJsonlObject(file) : await tryReadJson(file);
|
|
215
|
+
const summary = summarizeParsedSession(parsed, file);
|
|
216
|
+
if (!summary.id) continue;
|
|
217
|
+
const existing = sessionsById.get(summary.id);
|
|
218
|
+
if (!existing || String(summary.updatedAt) > String(existing.updatedAt)) {
|
|
219
|
+
sessionsById.set(summary.id, summary);
|
|
220
|
+
}
|
|
166
221
|
} catch {
|
|
167
222
|
continue;
|
|
168
223
|
}
|
|
169
224
|
}
|
|
170
225
|
|
|
226
|
+
const sessions = Array.from(sessionsById.values());
|
|
171
227
|
sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
172
228
|
return sessions.filter((s) => Number(s.messageCount || 0) > 0).slice(0, limit);
|
|
173
229
|
}
|
|
@@ -195,8 +251,9 @@ export async function pruneSessions(policy = {}) {
|
|
|
195
251
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
196
252
|
let removed = 0;
|
|
197
253
|
for (const e of entries) {
|
|
198
|
-
if (!e.isFile()
|
|
199
|
-
const id =
|
|
254
|
+
if (!e.isFile()) continue;
|
|
255
|
+
const id = sessionIdFromFileName(e.name);
|
|
256
|
+
if (!id) continue;
|
|
200
257
|
if (keepIds.has(id)) continue;
|
|
201
258
|
try {
|
|
202
259
|
await fs.unlink(path.join(dir, e.name));
|
|
@@ -61,6 +61,7 @@ const SHELL_PROFILES = {
|
|
|
61
61
|
shell: 'bash',
|
|
62
62
|
label: 'bash',
|
|
63
63
|
command_allowlist: [
|
|
64
|
+
'cd',
|
|
64
65
|
'rg',
|
|
65
66
|
'find',
|
|
66
67
|
'grep',
|
|
@@ -73,6 +74,22 @@ const SHELL_PROFILES = {
|
|
|
73
74
|
'ls',
|
|
74
75
|
'cat',
|
|
75
76
|
'sed',
|
|
77
|
+
'head',
|
|
78
|
+
'tail',
|
|
79
|
+
'wc',
|
|
80
|
+
'test',
|
|
81
|
+
'sort',
|
|
82
|
+
'uniq',
|
|
83
|
+
'cut',
|
|
84
|
+
'tr',
|
|
85
|
+
'xargs',
|
|
86
|
+
'basename',
|
|
87
|
+
'dirname',
|
|
88
|
+
'paste',
|
|
89
|
+
'echo',
|
|
90
|
+
'sleep',
|
|
91
|
+
'true',
|
|
92
|
+
'false',
|
|
76
93
|
'cp',
|
|
77
94
|
'mv',
|
|
78
95
|
'mkdir',
|
|
@@ -145,7 +162,6 @@ Use update_todos with these rules:
|
|
|
145
162
|
Some tools are loaded on demand through tool_search. Common examples:
|
|
146
163
|
- glob for pattern-based file lookup
|
|
147
164
|
- ast_query and read_ast_node for advanced AST-scoped reads and edits
|
|
148
|
-
- generate_diff and patch for explicit diff workflows
|
|
149
165
|
- list_background_tasks, get_background_task, and stop_background_task for managing long-running background commands
|
|
150
166
|
- remember_user, remember_global, remember_project, list_memory, search_memory, and forget_memory for persistent memory operations
|
|
151
167
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享字符串/路径工具函数。
|
|
3
|
+
* 统一 trimInline、路径标准化、escapeRegex 等多处重复实现。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 将字符串截断到指定长度,超出时添加省略号。
|
|
8
|
+
* 会先将空白折叠为单个空格再截断。
|
|
9
|
+
*/
|
|
10
|
+
export function trimInline(value, maxLen = 72) {
|
|
11
|
+
const s = String(value || '').replace(/\s+/g, ' ').trim();
|
|
12
|
+
if (!s) return '';
|
|
13
|
+
if (s.length <= maxLen) return s;
|
|
14
|
+
return `${s.slice(0, maxLen - 3)}...`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 将路径中的反斜杠替换为正斜杠,并去掉开头的 "./" 前缀。
|
|
19
|
+
* 用于统一 Windows / Unix 路径格式。
|
|
20
|
+
*/
|
|
21
|
+
export function normalizePath(value) {
|
|
22
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 将路径标准化为相对路径格式:反斜杠→正斜杠,去掉 "./" 和开头的 "/"。
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeRelativePath(value) {
|
|
29
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/^\/+/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 转义正则表达式中的特殊字符。
|
|
34
|
+
*/
|
|
35
|
+
export function escapeRegex(value) {
|
|
36
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
}
|