codemini-cli 0.3.2 → 0.3.3
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/OPERATIONS.md +4 -0
- package/README.md +199 -133
- package/package.json +1 -1
- package/src/core/agent-loop.js +19 -18
- package/src/core/chat-runtime.js +30 -106
- package/src/core/checkpoint-store.js +2 -3
- package/src/core/command-policy.js +144 -10
- package/src/core/config-store.js +6 -10
- package/src/core/context-compact.js +7 -1
- package/src/core/default-system-prompt.js +12 -1
- package/src/core/memory-policy.js +6 -0
- package/src/core/session-store.js +4 -0
- package/src/core/shell-profile.js +29 -17
- package/src/core/todo-state.js +19 -0
- package/src/core/tools.js +396 -318
- package/src/tui/chat-app.js +54 -33
- package/src/tui/tool-activity/presenters/command.js +8 -15
- package/src/tui/tool-activity/presenters/misc.js +2 -5
- package/src/core/task-store.js +0 -117
package/src/core/chat-runtime.js
CHANGED
|
@@ -13,13 +13,6 @@ import { listSessions, loadSession, pruneSessions, saveSession } from './session
|
|
|
13
13
|
import { getConfigValue, loadConfig, resetConfig, setConfigValue } from './config-store.js';
|
|
14
14
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
15
15
|
import { appendInputHistory, loadInputHistory } from './input-history-store.js';
|
|
16
|
-
import {
|
|
17
|
-
clearTasks,
|
|
18
|
-
createTasks,
|
|
19
|
-
deleteTasks,
|
|
20
|
-
loadTasks,
|
|
21
|
-
updateTask
|
|
22
|
-
} from './task-store.js';
|
|
23
16
|
import { createCheckpoint, listCheckpoints, loadCheckpoint } from './checkpoint-store.js';
|
|
24
17
|
import {
|
|
25
18
|
compactMessagesLocally,
|
|
@@ -32,6 +25,7 @@ import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSess
|
|
|
32
25
|
import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
|
|
33
26
|
import { buildMemorySnapshot } from './memory-prompt.js';
|
|
34
27
|
import { forgetMemory, listMemories, searchMemories } from './memory-store.js';
|
|
28
|
+
import { countActiveTodos, normalizeTodos } from './todo-state.js';
|
|
35
29
|
|
|
36
30
|
function toOpenAIMessages(sessionMessages) {
|
|
37
31
|
const mapped = [];
|
|
@@ -143,7 +137,6 @@ function getCompletionCopy(language = 'zh') {
|
|
|
143
137
|
status: '查看运行状态(mode/model/session)',
|
|
144
138
|
mode: '设置执行模式:normal|auto|plan',
|
|
145
139
|
compact: '压缩消息上下文',
|
|
146
|
-
tasks: '任务面板管理',
|
|
147
140
|
checkpoint: '创建/查看/加载检查点',
|
|
148
141
|
spec: '在 .codemini/specs 中创建 spec',
|
|
149
142
|
plan: '在 .codemini/plans 中创建实施计划',
|
|
@@ -158,7 +151,6 @@ function getCompletionCopy(language = 'zh') {
|
|
|
158
151
|
configCommand: '配置命令',
|
|
159
152
|
historyCommand: '历史会话命令',
|
|
160
153
|
modeCommand: '切换执行模式',
|
|
161
|
-
taskCommand: '任务面板命令',
|
|
162
154
|
checkpointCommand: '检查点命令',
|
|
163
155
|
specCommand: '创建 spec 文件',
|
|
164
156
|
planCommand: '规划命令',
|
|
@@ -232,7 +224,6 @@ function getCompletionCopy(language = 'zh') {
|
|
|
232
224
|
status: 'show runtime status (mode/model/session)',
|
|
233
225
|
mode: 'set execution mode: normal|auto|plan',
|
|
234
226
|
compact: 'compress message context',
|
|
235
|
-
tasks: 'task board management',
|
|
236
227
|
checkpoint: 'create/list/load conversation checkpoints',
|
|
237
228
|
spec: 'create a spec markdown file in .codemini/specs',
|
|
238
229
|
plan: 'create an implementation plan markdown file in .codemini/plans',
|
|
@@ -247,7 +238,6 @@ function getCompletionCopy(language = 'zh') {
|
|
|
247
238
|
configCommand: 'config command',
|
|
248
239
|
historyCommand: 'history command',
|
|
249
240
|
modeCommand: 'switch execution mode',
|
|
250
|
-
taskCommand: 'task board command',
|
|
251
241
|
checkpointCommand: 'checkpoint command',
|
|
252
242
|
specCommand: 'create a spec file',
|
|
253
243
|
planCommand: 'planning command',
|
|
@@ -276,7 +266,7 @@ const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
|
|
|
276
266
|
const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
|
|
277
267
|
const SUB_AGENT_EVIDENCE_MAX_ITEMS = 3;
|
|
278
268
|
const SUB_AGENT_HANDOFF_MAX_ITEMS = 6;
|
|
279
|
-
function getSubAgentRolePrompt(role) {
|
|
269
|
+
export function getSubAgentRolePrompt(role) {
|
|
280
270
|
if (role === 'planner') {
|
|
281
271
|
return 'You are a planning sub-agent. Produce a concrete implementation plan with risks and verification.';
|
|
282
272
|
}
|
|
@@ -315,7 +305,11 @@ function getSubAgentRolePrompt(role) {
|
|
|
315
305
|
'- <single best next step>'
|
|
316
306
|
].join('\n');
|
|
317
307
|
}
|
|
318
|
-
return
|
|
308
|
+
return [
|
|
309
|
+
'You are an execution sub-agent. Produce practical implementation guidance with code-level detail.',
|
|
310
|
+
'Stop when: you have produced the code change and verified it compiles/passes basic checks.',
|
|
311
|
+
'If blocked: report what blocked you and what you tried, then stop.'
|
|
312
|
+
].join('\n');
|
|
319
313
|
}
|
|
320
314
|
|
|
321
315
|
function trimInlineText(value, maxLen = 220) {
|
|
@@ -1753,14 +1747,19 @@ async function askModel({
|
|
|
1753
1747
|
|
|
1754
1748
|
const projectContextSnippet = await buildProjectContextSnippet(process.cwd(), text).catch(() => '');
|
|
1755
1749
|
const effectiveSystemPrompt = projectContextSnippet
|
|
1756
|
-
? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance
|
|
1750
|
+
? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance and verify important details with fresh reads when needed.`
|
|
1757
1751
|
: systemPrompt;
|
|
1758
1752
|
|
|
1759
1753
|
const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
|
|
1760
1754
|
workspaceRoot: process.cwd(),
|
|
1761
1755
|
config,
|
|
1762
1756
|
sessionId: session.id,
|
|
1763
|
-
onSystemEvent: onAgentEvent
|
|
1757
|
+
onSystemEvent: onAgentEvent,
|
|
1758
|
+
getTodos: () => normalizeTodos(session.todos),
|
|
1759
|
+
onTodosUpdate: (todos) => {
|
|
1760
|
+
session.todos = normalizeTodos(todos);
|
|
1761
|
+
scheduleSessionSave();
|
|
1762
|
+
}
|
|
1764
1763
|
});
|
|
1765
1764
|
|
|
1766
1765
|
let activeAssistantIndex = -1;
|
|
@@ -2122,6 +2121,17 @@ export async function createChatRuntime({
|
|
|
2122
2121
|
summary: initialIndex.summary
|
|
2123
2122
|
});
|
|
2124
2123
|
}
|
|
2124
|
+
const initialTodos = normalizeTodos(session?.todos);
|
|
2125
|
+
if (initialTodos.length > 0) {
|
|
2126
|
+
startupEvents.push({
|
|
2127
|
+
type: 'tool',
|
|
2128
|
+
id: `startup-todos-${String(session?.id || 'session')}`,
|
|
2129
|
+
name: 'update_todos',
|
|
2130
|
+
status: 'done',
|
|
2131
|
+
arguments: { todos: initialTodos },
|
|
2132
|
+
summary: `${initialTodos.length} todo item(s)`
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2125
2135
|
let currentSession = session;
|
|
2126
2136
|
let config = initialConfig;
|
|
2127
2137
|
const baseSystemPrompt = systemPrompt;
|
|
@@ -2211,7 +2221,6 @@ export async function createChatRuntime({
|
|
|
2211
2221
|
'/memory',
|
|
2212
2222
|
'/mode',
|
|
2213
2223
|
'/plan',
|
|
2214
|
-
'/tasks',
|
|
2215
2224
|
'/history',
|
|
2216
2225
|
'/checkpoint',
|
|
2217
2226
|
'/agents',
|
|
@@ -2230,7 +2239,6 @@ export async function createChatRuntime({
|
|
|
2230
2239
|
{ name: 'status', description: completionCopy.commands.status },
|
|
2231
2240
|
{ name: 'mode', description: completionCopy.commands.mode },
|
|
2232
2241
|
{ name: 'compact', description: completionCopy.commands.compact },
|
|
2233
|
-
{ name: 'tasks', description: completionCopy.commands.tasks },
|
|
2234
2242
|
{ name: 'checkpoint', description: completionCopy.commands.checkpoint },
|
|
2235
2243
|
{ name: 'spec', description: completionCopy.commands.spec },
|
|
2236
2244
|
{ name: 'plan', description: completionCopy.commands.plan },
|
|
@@ -2275,7 +2283,6 @@ export async function createChatRuntime({
|
|
|
2275
2283
|
const historyTemplates = ['/history list', '/history current', '/history resume <session_id>'];
|
|
2276
2284
|
const memoryTemplates = ['/memory list <scope>', '/memory search <scope> <query>', '/memory forget <scope> <id>'];
|
|
2277
2285
|
const modeTemplates = ['/mode normal', '/mode auto', '/mode plan'];
|
|
2278
|
-
const taskTemplates = ['/tasks', '/tasks add <title>', '/tasks start <id>', '/tasks done <id>', '/tasks remove <id>', '/tasks clear'];
|
|
2279
2286
|
const checkpointTemplates = [
|
|
2280
2287
|
'/checkpoint create <name>',
|
|
2281
2288
|
'/checkpoint list',
|
|
@@ -2292,7 +2299,6 @@ export async function createChatRuntime({
|
|
|
2292
2299
|
...memoryTemplates,
|
|
2293
2300
|
...historyTemplates,
|
|
2294
2301
|
...modeTemplates,
|
|
2295
|
-
...taskTemplates,
|
|
2296
2302
|
...checkpointTemplates,
|
|
2297
2303
|
...specTemplates,
|
|
2298
2304
|
...planTemplates,
|
|
@@ -2338,7 +2344,6 @@ export async function createChatRuntime({
|
|
|
2338
2344
|
'memory',
|
|
2339
2345
|
'compact',
|
|
2340
2346
|
'mode',
|
|
2341
|
-
'tasks',
|
|
2342
2347
|
'checkpoint',
|
|
2343
2348
|
'plan',
|
|
2344
2349
|
'agents',
|
|
@@ -2358,7 +2363,6 @@ export async function createChatRuntime({
|
|
|
2358
2363
|
for (const template of memoryTemplates) registerSuggestion(template, completionCopy.generic.memoryCommand);
|
|
2359
2364
|
for (const template of historyTemplates) registerSuggestion(template, completionCopy.generic.historyCommand);
|
|
2360
2365
|
for (const template of modeTemplates) registerSuggestion(template, completionCopy.generic.modeCommand);
|
|
2361
|
-
for (const template of taskTemplates) registerSuggestion(template, completionCopy.generic.taskCommand);
|
|
2362
2366
|
for (const template of checkpointTemplates) registerSuggestion(template, completionCopy.generic.checkpointCommand);
|
|
2363
2367
|
for (const template of specTemplates) registerSuggestion(template, completionCopy.generic.specCommand);
|
|
2364
2368
|
for (const template of planTemplates) {
|
|
@@ -2460,15 +2464,6 @@ export async function createChatRuntime({
|
|
|
2460
2464
|
}
|
|
2461
2465
|
return materializeSuggestions(modeTemplates);
|
|
2462
2466
|
}
|
|
2463
|
-
if (commandPart === 'tasks') {
|
|
2464
|
-
if (tokens.length <= 2 && !hasTrailingSpace) {
|
|
2465
|
-
const sub = tokens[1] || '';
|
|
2466
|
-
return ['add', 'start', 'done', 'remove', 'rm', 'clear']
|
|
2467
|
-
.filter((s) => s.startsWith(sub))
|
|
2468
|
-
.map((s) => registerSuggestion(`/tasks ${s}`, completionCopy.generic.taskCommand));
|
|
2469
|
-
}
|
|
2470
|
-
return materializeSuggestions(taskTemplates);
|
|
2471
|
-
}
|
|
2472
2467
|
if (commandPart === 'checkpoint') {
|
|
2473
2468
|
if (tokens.length <= 2 && !hasTrailingSpace) {
|
|
2474
2469
|
const sub = tokens[1] || '';
|
|
@@ -2614,7 +2609,7 @@ export async function createChatRuntime({
|
|
|
2614
2609
|
workspaceRoot: process.cwd()
|
|
2615
2610
|
}).catch(() => '');
|
|
2616
2611
|
const memoryGuide =
|
|
2617
|
-
'Persistent memory
|
|
2612
|
+
'Persistent memory stores durable preferences and stable workflow knowledge. Verify changeable details from files, and only write memory for future-useful, non-sensitive facts.';
|
|
2618
2613
|
return [soulPrompt, memorySnapshot, memoryGuide].filter(Boolean).join('\n\n');
|
|
2619
2614
|
};
|
|
2620
2615
|
|
|
@@ -2633,7 +2628,6 @@ export async function createChatRuntime({
|
|
|
2633
2628
|
'commands',
|
|
2634
2629
|
'status',
|
|
2635
2630
|
'mode',
|
|
2636
|
-
'tasks',
|
|
2637
2631
|
'checkpoint',
|
|
2638
2632
|
'history',
|
|
2639
2633
|
'memory',
|
|
@@ -2664,14 +2658,14 @@ export async function createChatRuntime({
|
|
|
2664
2658
|
if (parsedInput.command === 'help') {
|
|
2665
2659
|
return {
|
|
2666
2660
|
type: 'system',
|
|
2667
|
-
text: 'Commands: /help /exit /commands /status /mode /compact /
|
|
2661
|
+
text: 'Commands: /help /exit /commands /status /mode /compact /checkpoint /spec /plan /agents /config /memory /history /debug /retry /<custom> !<shell>'
|
|
2668
2662
|
};
|
|
2669
2663
|
}
|
|
2670
2664
|
if (parsedInput.command === 'status') {
|
|
2671
|
-
const
|
|
2665
|
+
const todoCount = countActiveTodos(currentSession.todos);
|
|
2672
2666
|
return {
|
|
2673
2667
|
type: 'system',
|
|
2674
|
-
text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} |
|
|
2668
|
+
text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
|
|
2675
2669
|
};
|
|
2676
2670
|
}
|
|
2677
2671
|
if (parsedInput.command === 'mode') {
|
|
@@ -2689,74 +2683,15 @@ export async function createChatRuntime({
|
|
|
2689
2683
|
await persistLocalExchange(line, text);
|
|
2690
2684
|
return { type: 'system', text };
|
|
2691
2685
|
}
|
|
2692
|
-
if (parsedInput.command === 'tasks') {
|
|
2693
|
-
const sub = (parsedInput.args[0] || '').trim().toLowerCase();
|
|
2694
|
-
if (!sub) {
|
|
2695
|
-
const tasks = await loadTasks(process.cwd(), currentSession.id);
|
|
2696
|
-
if (tasks.length === 0) return { type: 'system', text: 'No tasks' };
|
|
2697
|
-
const rows = tasks.map((t, idx) => `${idx + 1}. ${t.id} | ${t.status} | ${t.title}`);
|
|
2698
|
-
return { type: 'system', text: rows.join('\n') };
|
|
2699
|
-
}
|
|
2700
|
-
if (sub === 'add') {
|
|
2701
|
-
const title = parsedInput.args.slice(1).join(' ').trim();
|
|
2702
|
-
if (!title) return { type: 'system', text: 'Usage: /tasks add <title>' };
|
|
2703
|
-
const created = await createTasks([{ title }], process.cwd(), currentSession.id);
|
|
2704
|
-
const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
|
|
2705
|
-
await persistLocalExchange(line, text);
|
|
2706
|
-
return { type: 'system', text };
|
|
2707
|
-
}
|
|
2708
|
-
if (sub === 'start') {
|
|
2709
|
-
const id = parsedInput.args[1];
|
|
2710
|
-
if (!id) return { type: 'system', text: 'Usage: /tasks start <id>' };
|
|
2711
|
-
const updated = await updateTask(id, { status: 'in_progress' }, process.cwd(), currentSession.id);
|
|
2712
|
-
if (!updated) return { type: 'system', text: `Task not found: ${id}` };
|
|
2713
|
-
const text = `Task in progress: ${id}`;
|
|
2714
|
-
await persistLocalExchange(line, text);
|
|
2715
|
-
return { type: 'system', text };
|
|
2716
|
-
}
|
|
2717
|
-
if (sub === 'done') {
|
|
2718
|
-
const id = parsedInput.args[1];
|
|
2719
|
-
if (!id) return { type: 'system', text: 'Usage: /tasks done <id>' };
|
|
2720
|
-
const updated = await updateTask(id, { status: 'completed' }, process.cwd(), currentSession.id);
|
|
2721
|
-
if (!updated) return { type: 'system', text: `Task not found: ${id}` };
|
|
2722
|
-
const text = `Task completed: ${id}`;
|
|
2723
|
-
await persistLocalExchange(line, text);
|
|
2724
|
-
return { type: 'system', text };
|
|
2725
|
-
}
|
|
2726
|
-
if (sub === 'remove' || sub === 'rm') {
|
|
2727
|
-
const id = parsedInput.args[1];
|
|
2728
|
-
if (!id) return { type: 'system', text: 'Usage: /tasks remove <id>' };
|
|
2729
|
-
const result = await deleteTasks([id], process.cwd(), currentSession.id);
|
|
2730
|
-
const text = `Removed=${result.removed}, Remaining=${result.remaining}`;
|
|
2731
|
-
await persistLocalExchange(line, text);
|
|
2732
|
-
return { type: 'system', text };
|
|
2733
|
-
}
|
|
2734
|
-
if (sub === 'clear') {
|
|
2735
|
-
await clearTasks(process.cwd(), currentSession.id);
|
|
2736
|
-
const text = 'All tasks cleared';
|
|
2737
|
-
await persistLocalExchange(line, text);
|
|
2738
|
-
return { type: 'system', text };
|
|
2739
|
-
}
|
|
2740
|
-
// shorthand: /tasks implement x
|
|
2741
|
-
const title = parsedInput.args.join(' ').trim();
|
|
2742
|
-
if (title) {
|
|
2743
|
-
const created = await createTasks([{ title }], process.cwd(), currentSession.id);
|
|
2744
|
-
const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
|
|
2745
|
-
await persistLocalExchange(line, text);
|
|
2746
|
-
return { type: 'system', text };
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
2686
|
if (parsedInput.command === 'checkpoint') {
|
|
2750
2687
|
const sub = (parsedInput.args[0] || 'list').trim().toLowerCase();
|
|
2751
2688
|
if (sub === 'create') {
|
|
2752
2689
|
const name = parsedInput.args.slice(1).join(' ').trim();
|
|
2753
|
-
const tasks = await loadTasks(process.cwd(), currentSession.id);
|
|
2754
2690
|
const cp = await createCheckpoint(
|
|
2755
2691
|
{
|
|
2756
2692
|
name,
|
|
2757
2693
|
session: currentSession,
|
|
2758
|
-
config
|
|
2759
|
-
tasks
|
|
2694
|
+
config
|
|
2760
2695
|
},
|
|
2761
2696
|
process.cwd()
|
|
2762
2697
|
);
|
|
@@ -2791,17 +2726,6 @@ export async function createChatRuntime({
|
|
|
2791
2726
|
config = cp.config;
|
|
2792
2727
|
executionMode = config.execution?.mode || executionMode;
|
|
2793
2728
|
}
|
|
2794
|
-
if (Array.isArray(cp?.tasks)) {
|
|
2795
|
-
await clearTasks(process.cwd(), currentSession.id);
|
|
2796
|
-
if (cp.tasks.length > 0) {
|
|
2797
|
-
// restore with new ids to avoid stale references
|
|
2798
|
-
await createTasks(
|
|
2799
|
-
cp.tasks.map((t) => ({ title: t.title, description: t.description })),
|
|
2800
|
-
process.cwd(),
|
|
2801
|
-
currentSession.id
|
|
2802
|
-
);
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
2729
|
const text = `Checkpoint loaded: ${id}`;
|
|
2806
2730
|
await persistLocalExchange(line, text, { includeUser: false });
|
|
2807
2731
|
return { type: 'system', text };
|
|
@@ -16,7 +16,7 @@ function makeId(name = '') {
|
|
|
16
16
|
return `${stamp}-${slug || 'checkpoint'}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export async function createCheckpoint({ name, session, config
|
|
19
|
+
export async function createCheckpoint({ name, session, config }, cwd = process.cwd()) {
|
|
20
20
|
const dir = checkpointsDir(cwd);
|
|
21
21
|
await fs.mkdir(dir, { recursive: true });
|
|
22
22
|
const id = makeId(name);
|
|
@@ -26,8 +26,7 @@ export async function createCheckpoint({ name, session, config, tasks }, cwd = p
|
|
|
26
26
|
name: String(name || ''),
|
|
27
27
|
createdAt: new Date().toISOString(),
|
|
28
28
|
session,
|
|
29
|
-
config
|
|
30
|
-
tasks: Array.isArray(tasks) ? tasks : []
|
|
29
|
+
config
|
|
31
30
|
};
|
|
32
31
|
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
33
32
|
return payload;
|
|
@@ -8,6 +8,138 @@ function firstToken(command) {
|
|
|
8
8
|
return base.replace(/\.exe$/i, '');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function splitCommandSegments(command) {
|
|
12
|
+
const text = String(command || '').trim();
|
|
13
|
+
if (!text) return [];
|
|
14
|
+
const segments = [];
|
|
15
|
+
let current = '';
|
|
16
|
+
let quote = '';
|
|
17
|
+
let escapeNext = false;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
20
|
+
const ch = text[i];
|
|
21
|
+
const next = text[i + 1];
|
|
22
|
+
|
|
23
|
+
if (escapeNext) {
|
|
24
|
+
current += ch;
|
|
25
|
+
escapeNext = false;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (ch === '\\' && quote !== '\'') {
|
|
30
|
+
current += ch;
|
|
31
|
+
escapeNext = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (quote) {
|
|
36
|
+
current += ch;
|
|
37
|
+
if (ch === quote) quote = '';
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (ch === '"' || ch === '\'') {
|
|
42
|
+
quote = ch;
|
|
43
|
+
current += ch;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if ((ch === '&' && next === '&') || (ch === '|' && next === '|')) {
|
|
48
|
+
if (current.trim()) segments.push(current.trim());
|
|
49
|
+
current = '';
|
|
50
|
+
i += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (ch === '|' || ch === ';' || ch === '&') {
|
|
55
|
+
if (current.trim()) segments.push(current.trim());
|
|
56
|
+
current = '';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
current += ch;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (current.trim()) segments.push(current.trim());
|
|
64
|
+
return segments;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tokenizeTopLevel(command) {
|
|
68
|
+
const text = String(command || '').trim();
|
|
69
|
+
if (!text) return [];
|
|
70
|
+
const tokens = [];
|
|
71
|
+
let current = '';
|
|
72
|
+
let quote = '';
|
|
73
|
+
let escapeNext = false;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
76
|
+
const ch = text[i];
|
|
77
|
+
if (escapeNext) {
|
|
78
|
+
current += ch;
|
|
79
|
+
escapeNext = false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (ch === '\\' && quote !== '\'') {
|
|
83
|
+
escapeNext = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (quote) {
|
|
87
|
+
if (ch === quote) {
|
|
88
|
+
quote = '';
|
|
89
|
+
} else {
|
|
90
|
+
current += ch;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch === '"' || ch === '\'') {
|
|
95
|
+
quote = ch;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (/\s/.test(ch)) {
|
|
99
|
+
if (current) {
|
|
100
|
+
tokens.push(current);
|
|
101
|
+
current = '';
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
current += ch;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (current) tokens.push(current);
|
|
109
|
+
return tokens;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unwrapShellPayload(command) {
|
|
113
|
+
const tokens = tokenizeTopLevel(command);
|
|
114
|
+
const token = firstToken(command);
|
|
115
|
+
if (!['bash', 'sh', 'zsh', 'powershell', 'pwsh', 'cmd'].includes(token)) return '';
|
|
116
|
+
|
|
117
|
+
const index = tokens.findIndex((item, itemIndex) => {
|
|
118
|
+
if (token === 'cmd') return itemIndex > 0 && /^\/c$/i.test(item);
|
|
119
|
+
return /^-(?:c|lc|command)$/i.test(item);
|
|
120
|
+
});
|
|
121
|
+
if (index < 0 || index + 1 >= tokens.length) return '';
|
|
122
|
+
return tokens.slice(index + 1).join(' ').trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectCommandTokens(command) {
|
|
126
|
+
const cmd = String(command || '').trim();
|
|
127
|
+
if (!cmd) return [];
|
|
128
|
+
|
|
129
|
+
const chained = splitCommandSegments(cmd);
|
|
130
|
+
if (chained.length > 1) {
|
|
131
|
+
return chained.flatMap((segment) => collectCommandTokens(segment));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const token = firstToken(cmd);
|
|
135
|
+
const out = token ? [{ token, raw: cmd }] : [];
|
|
136
|
+
const wrapped = unwrapShellPayload(cmd);
|
|
137
|
+
if (wrapped && wrapped !== cmd) {
|
|
138
|
+
out.push(...collectCommandTokens(wrapped));
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
11
143
|
function includesAny(haystackLower, patterns = []) {
|
|
12
144
|
return patterns.some((p) => haystackLower.includes(String(p).toLowerCase()));
|
|
13
145
|
}
|
|
@@ -46,17 +178,19 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
46
178
|
}
|
|
47
179
|
|
|
48
180
|
const token = firstToken(cmd);
|
|
49
|
-
|
|
50
|
-
return { allowed: false, reason: `blocked command: ${token}`, suggestion: suggestionForToken(token, config) };
|
|
51
|
-
}
|
|
52
|
-
|
|
181
|
+
const inspectedTokens = collectCommandTokens(cmd);
|
|
53
182
|
const allowlist = Array.isArray(policy.command_allowlist) ? policy.command_allowlist : [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
allowed: false,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
183
|
+
for (const item of inspectedTokens) {
|
|
184
|
+
if (includesAny(item.token, policy.blocked_commands)) {
|
|
185
|
+
return { allowed: false, reason: `blocked command: ${item.token}`, suggestion: suggestionForToken(item.token, config) };
|
|
186
|
+
}
|
|
187
|
+
if (allowlist.length > 0 && !allowlist.includes(item.token)) {
|
|
188
|
+
return {
|
|
189
|
+
allowed: false,
|
|
190
|
+
reason: `command not in allowlist: ${item.token}`,
|
|
191
|
+
suggestion: suggestionForToken(item.token, config)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
60
194
|
}
|
|
61
195
|
|
|
62
196
|
const workspaceLower = String(workspaceRoot).toLowerCase().replace(/\//g, '\\');
|
package/src/core/config-store.js
CHANGED
|
@@ -46,11 +46,9 @@ const DEFAULT_CONFIG = {
|
|
|
46
46
|
'run',
|
|
47
47
|
'patch',
|
|
48
48
|
'generate_diff',
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'get_service_logs',
|
|
53
|
-
'stop_service'
|
|
49
|
+
'list_background_tasks',
|
|
50
|
+
'get_background_task',
|
|
51
|
+
'stop_background_task'
|
|
54
52
|
],
|
|
55
53
|
max_steps: 16
|
|
56
54
|
},
|
|
@@ -154,11 +152,9 @@ function normalizePolicyLists(config) {
|
|
|
154
152
|
'write',
|
|
155
153
|
'run',
|
|
156
154
|
'generate_diff',
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'get_service_logs',
|
|
161
|
-
'stop_service',
|
|
155
|
+
'list_background_tasks',
|
|
156
|
+
'get_background_task',
|
|
157
|
+
'stop_background_task',
|
|
162
158
|
...rawTools
|
|
163
159
|
].filter((name) => String(name) !== 'list_files')
|
|
164
160
|
);
|
|
@@ -19,7 +19,13 @@ export function estimateMessagesTokens(messages) {
|
|
|
19
19
|
for (const message of messages || []) {
|
|
20
20
|
const roleOverhead = 6;
|
|
21
21
|
const text = textFromContent(message.content);
|
|
22
|
-
|
|
22
|
+
let asciiChars = 0;
|
|
23
|
+
let nonAsciiChars = 0;
|
|
24
|
+
for (const char of text) {
|
|
25
|
+
if (char.charCodeAt(0) <= 0x7f) asciiChars += 1;
|
|
26
|
+
else nonAsciiChars += 1;
|
|
27
|
+
}
|
|
28
|
+
total += roleOverhead + Math.ceil(asciiChars / 4) + Math.ceil(nonAsciiChars / 2);
|
|
23
29
|
}
|
|
24
30
|
return total;
|
|
25
31
|
}
|
|
@@ -18,6 +18,11 @@ Assistant: first narrow the search with the project index
|
|
|
18
18
|
Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
|
|
19
19
|
Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
|
|
20
20
|
|
|
21
|
+
If the visible tool list does not include a needed capability, load it with tool_search instead of assuming it does not exist.
|
|
22
|
+
Example:
|
|
23
|
+
Tool: tool_search({"query":"glob"})
|
|
24
|
+
Tool: glob({"pattern":"src/**/*.ts"})
|
|
25
|
+
|
|
21
26
|
2. Targeted search then exact text edit
|
|
22
27
|
User: rename loginUser to signInUser
|
|
23
28
|
Assistant: first find the exact occurrences
|
|
@@ -29,7 +34,13 @@ User: inspect the reducer around line 120
|
|
|
29
34
|
Assistant: read only the needed range
|
|
30
35
|
Tool: read({"path":"${cwd}/src/store/reducer.ts:110-150"})
|
|
31
36
|
|
|
32
|
-
4.
|
|
37
|
+
4. Track a complex task with todos
|
|
38
|
+
User: update the login flow and verify it
|
|
39
|
+
Assistant: create a focused todo checklist before starting
|
|
40
|
+
Tool: update_todos({"todos":[{"content":"Inspect the current login flow","activeForm":"Inspecting the current login flow","status":"in_progress"},{"content":"Implement the requested login changes","activeForm":"Implementing the requested login changes","status":"pending"},{"content":"Run focused verification for the login flow","activeForm":"Running focused verification for the login flow","status":"pending"}]})
|
|
41
|
+
Assistant: keep the checklist updated as each phase finishes, and do not give a completion-style wrap-up until the checklist is complete or a blocker is recorded
|
|
42
|
+
|
|
43
|
+
5. Create a new file
|
|
33
44
|
User: add a notes file
|
|
34
45
|
Assistant: create the file directly
|
|
35
46
|
Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const SECRET_PATTERNS = [
|
|
2
2
|
/\b(api[_-]?key|token|secret|password|passwd|bearer)\b/i,
|
|
3
|
+
/\b(database_url|aws_secret_access_key|aws_access_key_id|openai_api_key|github_token|github_pat|slack_bot_token)\b\s*[:=]\s*\S+/i,
|
|
4
|
+
/\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^/\s:@]+:[^@\s]+@/i,
|
|
5
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
6
|
+
/\bghp_[a-z0-9]{20,}\b/i,
|
|
7
|
+
/\bgithub_pat_[a-z0-9_]{20,}\b/i,
|
|
8
|
+
/\bglpat-[a-z0-9_-]{20,}\b/i,
|
|
3
9
|
/\bsk-[a-z0-9]{8,}\b/i,
|
|
4
10
|
/-----BEGIN [A-Z ]+PRIVATE KEY-----/i
|
|
5
11
|
];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getSessionsDir } from './paths.js';
|
|
4
|
+
import { normalizeTodos } from './todo-state.js';
|
|
4
5
|
|
|
5
6
|
const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
6
7
|
|
|
@@ -86,6 +87,9 @@ function sanitizeSession(session, fallbackId = '') {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
const todos = normalizeTodos(session?.todos);
|
|
91
|
+
if (todos.length > 0) out.todos = todos;
|
|
92
|
+
|
|
89
93
|
return out;
|
|
90
94
|
}
|
|
91
95
|
|