codemini-cli 0.3.1 → 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 +2 -1
- package/src/commands/chat.js +1 -0
- package/src/commands/run.js +6 -2
- package/src/core/agent-loop.js +20 -19
- package/src/core/chat-runtime.js +567 -233
- package/src/core/checkpoint-store.js +2 -3
- package/src/core/command-policy.js +144 -10
- package/src/core/config-store.js +36 -10
- package/src/core/context-compact.js +7 -1
- package/src/core/default-system-prompt.js +12 -1
- package/src/core/memory-policy.js +33 -0
- package/src/core/memory-prompt.js +45 -0
- package/src/core/memory-store.js +181 -0
- package/src/core/paths.js +8 -0
- package/src/core/provider/anthropic.js +388 -0
- package/src/core/provider/index.js +37 -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 +486 -235
- package/src/tui/chat-app.js +278 -57
- 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/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawn } from 'node:child_process';
|
|
4
5
|
import net from 'node:net';
|
|
@@ -17,19 +18,61 @@ import { initializeProjectIndex, queryProjectIndex, refreshIndexedFile } from '.
|
|
|
17
18
|
import { checkReadDedup } from './agent-loop.js';
|
|
18
19
|
import { TOOL_SKIP_DIRS as SKIP_DIRS, TEXT_EXTENSIONS, CODE_WRITE_GUARD_EXTENSIONS, LANGUAGE_FILE_TYPES } from './constants.js';
|
|
19
20
|
import { sha256Prefixed as sha256, sha1 } from './crypto-utils.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
import { forgetMemory, listMemories, rememberMemory, searchMemories } from './memory-store.js';
|
|
22
|
+
import { normalizeTodos } from './todo-state.js';
|
|
23
|
+
const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
|
|
24
|
+
const BACKGROUND_TASK_POLL_MS = 150;
|
|
25
|
+
const backgroundTaskRegistry = new Map();
|
|
26
|
+
let backgroundTaskCounter = 0;
|
|
27
|
+
let backgroundTaskLogCursorCounter = 0;
|
|
28
|
+
|
|
29
|
+
function realpathIfExists(targetPath) {
|
|
30
|
+
try {
|
|
31
|
+
return fsSync.realpathSync.native(targetPath);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code === 'ENOENT') return null;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isWithinResolvedRoot(resolvedRoot, candidatePath) {
|
|
39
|
+
const relative = path.relative(resolvedRoot, candidatePath);
|
|
40
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
41
|
+
}
|
|
25
42
|
|
|
26
43
|
function resolveInWorkspace(root, targetPath = '.') {
|
|
27
44
|
const absRoot = path.resolve(root);
|
|
45
|
+
const realRoot = fsSync.realpathSync.native(absRoot);
|
|
28
46
|
const absTarget = path.resolve(absRoot, targetPath);
|
|
29
|
-
|
|
47
|
+
const realTarget = realpathIfExists(absTarget);
|
|
48
|
+
if (realTarget) {
|
|
49
|
+
if (!isWithinResolvedRoot(realRoot, realTarget)) {
|
|
50
|
+
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
51
|
+
}
|
|
52
|
+
return realTarget;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let probe = path.dirname(absTarget);
|
|
56
|
+
while (!realpathIfExists(probe)) {
|
|
57
|
+
const parent = path.dirname(probe);
|
|
58
|
+
if (parent === probe) break;
|
|
59
|
+
probe = parent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const resolvedProbe = realpathIfExists(probe);
|
|
63
|
+
if (!resolvedProbe) {
|
|
30
64
|
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
31
65
|
}
|
|
32
|
-
|
|
66
|
+
|
|
67
|
+
const resolvedTarget = path.join(resolvedProbe, path.relative(probe, absTarget));
|
|
68
|
+
if (!isWithinResolvedRoot(realRoot, resolvedTarget)) {
|
|
69
|
+
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
70
|
+
}
|
|
71
|
+
return resolvedTarget;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getBackgroundTasksDir(root) {
|
|
75
|
+
return path.join(resolveInWorkspace(root, '.codemini'), 'tasks');
|
|
33
76
|
}
|
|
34
77
|
|
|
35
78
|
function toWorkspaceRelative(root, absPath) {
|
|
@@ -232,56 +275,68 @@ function normalizeFileTypes(args = {}) {
|
|
|
232
275
|
return [...new Set(merged)];
|
|
233
276
|
}
|
|
234
277
|
|
|
278
|
+
async function mapLimit(items, limit, worker) {
|
|
279
|
+
const list = Array.isArray(items) ? items : [];
|
|
280
|
+
if (list.length === 0) return [];
|
|
281
|
+
const maxConcurrent = Math.max(1, Math.min(Number(limit) || 1, list.length));
|
|
282
|
+
const results = new Array(list.length);
|
|
283
|
+
let nextIndex = 0;
|
|
284
|
+
|
|
285
|
+
async function runNext() {
|
|
286
|
+
while (nextIndex < list.length) {
|
|
287
|
+
const currentIndex = nextIndex;
|
|
288
|
+
nextIndex += 1;
|
|
289
|
+
results[currentIndex] = await worker(list[currentIndex], currentIndex);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await Promise.all(Array.from({ length: maxConcurrent }, () => runNext()));
|
|
294
|
+
return results;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const WALKER_CONCURRENCY = 8;
|
|
298
|
+
|
|
235
299
|
async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
236
300
|
const abs = resolveInWorkspace(root, startPath);
|
|
237
|
-
const out = [];
|
|
238
301
|
const allowedExts = new Set((Array.isArray(fileTypes) ? fileTypes : []).map((item) => `.${String(item || '').replace(/^\./, '')}`));
|
|
239
302
|
|
|
240
303
|
async function visit(current) {
|
|
241
304
|
const stat = await fs.stat(current);
|
|
242
305
|
if (stat.isDirectory()) {
|
|
243
306
|
const name = path.basename(current);
|
|
244
|
-
if (SKIP_DIRS.has(name)) return;
|
|
307
|
+
if (SKIP_DIRS.has(name)) return [];
|
|
245
308
|
const entries = await fs.readdir(current);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
return;
|
|
309
|
+
const nested = await mapLimit(entries, WALKER_CONCURRENCY, async (entry) => visit(path.join(current, entry)));
|
|
310
|
+
return nested.flat();
|
|
250
311
|
}
|
|
251
|
-
if (!detectTextFile(current)) return;
|
|
252
|
-
if (allowedExts.size > 0 && !allowedExts.has(path.extname(current).toLowerCase())) return;
|
|
253
|
-
|
|
312
|
+
if (!detectTextFile(current)) return [];
|
|
313
|
+
if (allowedExts.size > 0 && !allowedExts.has(path.extname(current).toLowerCase())) return [];
|
|
314
|
+
return [current];
|
|
254
315
|
}
|
|
255
316
|
|
|
256
|
-
|
|
257
|
-
return out;
|
|
317
|
+
return visit(abs);
|
|
258
318
|
}
|
|
259
319
|
|
|
260
320
|
async function walkWorkspaceEntries(root, startPath = '.', { includeHidden = false } = {}) {
|
|
261
321
|
const abs = resolveInWorkspace(root, startPath);
|
|
262
|
-
const out = [];
|
|
263
322
|
|
|
264
323
|
async function visit(current) {
|
|
265
324
|
const stat = await fs.stat(current);
|
|
266
325
|
const relative = toWorkspaceRelative(root, current) || '.';
|
|
267
326
|
const name = path.basename(current);
|
|
268
327
|
|
|
269
|
-
if (!includeHidden && name.startsWith('.') && relative !== '.') return;
|
|
328
|
+
if (!includeHidden && name.startsWith('.') && relative !== '.') return [];
|
|
270
329
|
if (stat.isDirectory()) {
|
|
271
|
-
if (SKIP_DIRS.has(name) && relative !== '.') return;
|
|
272
|
-
out.push({ path: relative, name, type: 'dir' });
|
|
330
|
+
if (SKIP_DIRS.has(name) && relative !== '.') return [];
|
|
273
331
|
const entries = await fs.readdir(current);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
332
|
+
const nested = await mapLimit(entries, WALKER_CONCURRENCY, async (entry) => visit(path.join(current, entry)));
|
|
333
|
+
return [{ path: relative, name, type: 'dir' }, ...nested.flat()];
|
|
278
334
|
}
|
|
279
335
|
|
|
280
|
-
|
|
336
|
+
return [{ path: relative, name, type: 'file' }];
|
|
281
337
|
}
|
|
282
338
|
|
|
283
|
-
|
|
284
|
-
return out;
|
|
339
|
+
return visit(abs);
|
|
285
340
|
}
|
|
286
341
|
|
|
287
342
|
function globToRegex(pattern) {
|
|
@@ -839,18 +894,6 @@ async function runCommand(root, config, args) {
|
|
|
839
894
|
if (!command.trim()) {
|
|
840
895
|
throw new Error('run requires command');
|
|
841
896
|
}
|
|
842
|
-
if (isLikelyLongRunningCommand(command)) {
|
|
843
|
-
const intent = classifyCommandIntent(command);
|
|
844
|
-
const labelMap = {
|
|
845
|
-
'frontend-service': 'frontend service',
|
|
846
|
-
'backend-service': 'backend service',
|
|
847
|
-
'database-service': 'database service',
|
|
848
|
-
'docker-service': 'Docker service',
|
|
849
|
-
service: 'long-running service'
|
|
850
|
-
};
|
|
851
|
-
const label = labelMap[intent.kind] || 'long-running service';
|
|
852
|
-
throw new Error(`Command looks like a ${label}. Use start_service instead of run.`);
|
|
853
|
-
}
|
|
854
897
|
if (
|
|
855
898
|
!config.policy.allow_dangerous_commands &&
|
|
856
899
|
isDangerousCommand(command, config.policy.blocked_command_patterns)
|
|
@@ -865,6 +908,16 @@ async function runCommand(root, config, args) {
|
|
|
865
908
|
);
|
|
866
909
|
}
|
|
867
910
|
|
|
911
|
+
const shouldBackground =
|
|
912
|
+
args?.run_in_background === true ||
|
|
913
|
+
args?.runInBackground === true ||
|
|
914
|
+
args?.background === true ||
|
|
915
|
+
isLikelyLongRunningCommand(command);
|
|
916
|
+
|
|
917
|
+
if (shouldBackground) {
|
|
918
|
+
return startBackgroundTask(root, config, args);
|
|
919
|
+
}
|
|
920
|
+
|
|
868
921
|
const result = await runShellCommand({
|
|
869
922
|
command,
|
|
870
923
|
cwd: root,
|
|
@@ -874,9 +927,9 @@ async function runCommand(root, config, args) {
|
|
|
874
927
|
return { ...result, command };
|
|
875
928
|
}
|
|
876
929
|
|
|
877
|
-
function
|
|
878
|
-
|
|
879
|
-
return `
|
|
930
|
+
function nextBackgroundTaskId() {
|
|
931
|
+
backgroundTaskCounter += 1;
|
|
932
|
+
return `task_${String(backgroundTaskCounter).padStart(3, '0')}`;
|
|
880
933
|
}
|
|
881
934
|
|
|
882
935
|
function normalizeSuccessMatchers(items = []) {
|
|
@@ -884,39 +937,39 @@ function normalizeSuccessMatchers(items = []) {
|
|
|
884
937
|
return items.map((item) => String(item || '').trim()).filter(Boolean);
|
|
885
938
|
}
|
|
886
939
|
|
|
887
|
-
function
|
|
940
|
+
function shellCommandForBackgroundTask(command, shellSpec) {
|
|
888
941
|
return process.platform !== 'win32' && /(?:^|\/)bash(?:\.exe)?$/i.test(shellSpec.command)
|
|
889
942
|
? `exec ${command}`
|
|
890
943
|
: command;
|
|
891
944
|
}
|
|
892
945
|
|
|
893
|
-
function
|
|
946
|
+
function appendRecentOutput(task, chunk) {
|
|
894
947
|
const lines = String(chunk || '')
|
|
895
948
|
.split(/\r?\n/)
|
|
896
949
|
.map((line) => trimLinePreview(line, 220))
|
|
897
950
|
.filter(Boolean);
|
|
898
951
|
if (lines.length === 0) return;
|
|
899
952
|
for (const line of lines) {
|
|
900
|
-
|
|
901
|
-
|
|
953
|
+
backgroundTaskLogCursorCounter += 1;
|
|
954
|
+
task.recentLogs.push({ cursor: backgroundTaskLogCursorCounter, line });
|
|
902
955
|
}
|
|
903
|
-
if (
|
|
904
|
-
|
|
956
|
+
if (task.recentLogs.length > BACKGROUND_TASK_RECENT_OUTPUT_LIMIT) {
|
|
957
|
+
task.recentLogs.splice(0, task.recentLogs.length - BACKGROUND_TASK_RECENT_OUTPUT_LIMIT);
|
|
905
958
|
}
|
|
906
959
|
}
|
|
907
960
|
|
|
908
|
-
function
|
|
961
|
+
function matchesTaskStartupSuccess(task, text) {
|
|
909
962
|
const value = String(text || '');
|
|
910
963
|
if (!value) return false;
|
|
911
964
|
if (hasReadyOutput(value)) return true;
|
|
912
|
-
return
|
|
965
|
+
return task.successMatchers.some((matcher) => value.toLowerCase().includes(matcher.toLowerCase()));
|
|
913
966
|
}
|
|
914
967
|
|
|
915
|
-
function
|
|
916
|
-
if (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
968
|
+
function markTaskReady(task, source = 'output') {
|
|
969
|
+
if (task.startupConfirmed) return;
|
|
970
|
+
task.startupConfirmed = true;
|
|
971
|
+
task.startupSource = source;
|
|
972
|
+
task.status = 'running';
|
|
920
973
|
}
|
|
921
974
|
|
|
922
975
|
function serviceUrlForPort(port) {
|
|
@@ -935,34 +988,38 @@ function normalizeHttpProbe(value) {
|
|
|
935
988
|
};
|
|
936
989
|
}
|
|
937
990
|
|
|
938
|
-
function
|
|
939
|
-
const
|
|
940
|
-
?
|
|
991
|
+
function snapshotBackgroundTask(task, tail = 12) {
|
|
992
|
+
const recentOutput = Array.isArray(task.recentLogs)
|
|
993
|
+
? task.recentLogs.slice(-Math.max(1, tail)).map((item) => item.line)
|
|
941
994
|
: [];
|
|
942
995
|
const latestCursor =
|
|
943
|
-
Array.isArray(
|
|
944
|
-
?
|
|
996
|
+
Array.isArray(task.recentLogs) && task.recentLogs.length > 0
|
|
997
|
+
? task.recentLogs[task.recentLogs.length - 1].cursor
|
|
945
998
|
: 0;
|
|
946
999
|
return {
|
|
947
|
-
task_id:
|
|
948
|
-
pid:
|
|
949
|
-
command:
|
|
950
|
-
cwd:
|
|
951
|
-
status:
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1000
|
+
task_id: task.taskId,
|
|
1001
|
+
pid: task.child?.pid || null,
|
|
1002
|
+
command: task.command,
|
|
1003
|
+
cwd: task.cwd,
|
|
1004
|
+
status: task.status,
|
|
1005
|
+
background: true,
|
|
1006
|
+
kind: task.intentKind,
|
|
1007
|
+
startup_confirmed: task.startupConfirmed,
|
|
1008
|
+
startup_source: task.startupSource || '',
|
|
1009
|
+
http_probe: task.httpProbe || undefined,
|
|
1010
|
+
url: serviceUrlForPort(task.portProbe) || undefined,
|
|
1011
|
+
output_file: task.outputFile,
|
|
1012
|
+
recent_output: recentOutput,
|
|
1013
|
+
recent_logs: recentOutput,
|
|
957
1014
|
log_cursor: latestCursor,
|
|
958
|
-
exit_code:
|
|
959
|
-
signal:
|
|
960
|
-
duration_ms: Date.now() -
|
|
1015
|
+
exit_code: task.exitCode ?? undefined,
|
|
1016
|
+
signal: task.signal ?? undefined,
|
|
1017
|
+
duration_ms: Date.now() - task.startedAt
|
|
961
1018
|
};
|
|
962
1019
|
}
|
|
963
1020
|
|
|
964
|
-
function
|
|
965
|
-
return Array.from(
|
|
1021
|
+
function listBackgroundTaskSnapshots() {
|
|
1022
|
+
return Array.from(backgroundTaskRegistry.values()).map((task) => snapshotBackgroundTask(task, 4));
|
|
966
1023
|
}
|
|
967
1024
|
|
|
968
1025
|
function probePortOnce(port, host = '127.0.0.1', timeoutMs = 250) {
|
|
@@ -1000,9 +1057,16 @@ async function probeHttpOnce(httpProbe, timeoutMs = 400) {
|
|
|
1000
1057
|
}
|
|
1001
1058
|
}
|
|
1002
1059
|
|
|
1003
|
-
|
|
1060
|
+
function queueBackgroundTaskOutputWrite(task, chunk) {
|
|
1061
|
+
if (!task?.outputFileAbs) return;
|
|
1062
|
+
task.outputWrite = (task.outputWrite || Promise.resolve())
|
|
1063
|
+
.then(() => fs.appendFile(task.outputFileAbs, String(chunk || ''), 'utf8'))
|
|
1064
|
+
.catch(() => {});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function startBackgroundTask(root, config, args) {
|
|
1004
1068
|
const command = String(args?.command || args?.cmd || '').trim();
|
|
1005
|
-
if (!command) throw new Error('
|
|
1069
|
+
if (!command) throw new Error('run requires command');
|
|
1006
1070
|
if (
|
|
1007
1071
|
!config.policy.allow_dangerous_commands &&
|
|
1008
1072
|
isDangerousCommand(command, config.policy.blocked_command_patterns)
|
|
@@ -1017,54 +1081,65 @@ async function startService(root, config, args) {
|
|
|
1017
1081
|
}
|
|
1018
1082
|
|
|
1019
1083
|
const shellSpec = resolveShell(config.shell.default);
|
|
1020
|
-
const taskId =
|
|
1084
|
+
const taskId = nextBackgroundTaskId();
|
|
1021
1085
|
const startupTimeoutMs = Math.max(250, Number(args?.startup_timeout_ms || args?.startupTimeoutMs || 20000));
|
|
1022
1086
|
const successMatchers = normalizeSuccessMatchers(args?.success_matchers || args?.successMatchers);
|
|
1023
1087
|
const portProbe = Number(args?.port_probe || args?.portProbe || 0) || 0;
|
|
1024
1088
|
const httpProbe = normalizeHttpProbe(args?.http_probe || args?.httpProbe);
|
|
1025
|
-
const
|
|
1089
|
+
const outputDir = getBackgroundTasksDir(root);
|
|
1090
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
1091
|
+
const outputFileAbs = path.join(outputDir, `${taskId}.log`);
|
|
1092
|
+
await fs.writeFile(outputFileAbs, '', 'utf8');
|
|
1093
|
+
|
|
1094
|
+
const task = {
|
|
1026
1095
|
taskId,
|
|
1027
1096
|
command,
|
|
1028
1097
|
cwd: root,
|
|
1029
|
-
child: spawn(shellSpec.command, [...shellSpec.args,
|
|
1098
|
+
child: spawn(shellSpec.command, [...shellSpec.args, shellCommandForBackgroundTask(command, shellSpec)], {
|
|
1030
1099
|
cwd: root,
|
|
1031
1100
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
1032
1101
|
}),
|
|
1033
1102
|
startedAt: Date.now(),
|
|
1034
1103
|
status: 'starting',
|
|
1104
|
+
intentKind: classifyCommandIntent(command).kind,
|
|
1035
1105
|
startupConfirmed: false,
|
|
1036
1106
|
startupSource: '',
|
|
1037
1107
|
successMatchers,
|
|
1038
1108
|
portProbe,
|
|
1039
1109
|
httpProbe,
|
|
1110
|
+
outputFileAbs,
|
|
1111
|
+
outputFile: toWorkspaceRelative(root, outputFileAbs),
|
|
1040
1112
|
recentLogs: [],
|
|
1041
1113
|
exitCode: null,
|
|
1042
|
-
signal: null
|
|
1114
|
+
signal: null,
|
|
1115
|
+
outputWrite: Promise.resolve()
|
|
1043
1116
|
};
|
|
1044
|
-
|
|
1117
|
+
backgroundTaskRegistry.set(taskId, task);
|
|
1045
1118
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1119
|
+
task.closePromise = new Promise((resolve) => {
|
|
1120
|
+
task.child.on('close', (code, signal) => {
|
|
1121
|
+
task.exitCode = code;
|
|
1122
|
+
task.signal = signal;
|
|
1123
|
+
task.status = task.status === 'stopped' ? 'stopped' : 'exited';
|
|
1051
1124
|
resolve();
|
|
1052
1125
|
});
|
|
1053
1126
|
});
|
|
1054
1127
|
|
|
1055
1128
|
const onOutput = (chunk) => {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1129
|
+
appendRecentOutput(task, chunk);
|
|
1130
|
+
queueBackgroundTaskOutputWrite(task, chunk);
|
|
1131
|
+
if (matchesTaskStartupSuccess(task, chunk)) {
|
|
1132
|
+
markTaskReady(task, 'output');
|
|
1133
|
+
if (task._finishStartup) task._finishStartup();
|
|
1060
1134
|
}
|
|
1061
1135
|
};
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1136
|
+
task.child.stdout.on('data', onOutput);
|
|
1137
|
+
task.child.stderr.on('data', onOutput);
|
|
1138
|
+
task.child.on('error', (error) => {
|
|
1139
|
+
appendRecentOutput(task, error?.message || String(error));
|
|
1140
|
+
queueBackgroundTaskOutputWrite(task, error?.message || String(error));
|
|
1141
|
+
task.status = 'exited';
|
|
1142
|
+
if (task._finishStartup) task._finishStartup();
|
|
1068
1143
|
});
|
|
1069
1144
|
|
|
1070
1145
|
await new Promise((resolve) => {
|
|
@@ -1075,20 +1150,20 @@ async function startService(root, config, args) {
|
|
|
1075
1150
|
clearTimeout(timeoutHandle);
|
|
1076
1151
|
clearInterval(portHandle);
|
|
1077
1152
|
clearInterval(httpHandle);
|
|
1078
|
-
|
|
1153
|
+
task._finishStartup = null;
|
|
1079
1154
|
resolve();
|
|
1080
1155
|
};
|
|
1081
|
-
|
|
1082
|
-
if (
|
|
1156
|
+
task._finishStartup = finish;
|
|
1157
|
+
if (task.startupConfirmed || task.status === 'exited') {
|
|
1083
1158
|
finish();
|
|
1084
1159
|
return;
|
|
1085
1160
|
}
|
|
1086
1161
|
const timeoutHandle = setTimeout(() => {
|
|
1087
|
-
if (
|
|
1088
|
-
if (!
|
|
1089
|
-
|
|
1162
|
+
if (task.status === 'starting') {
|
|
1163
|
+
if (!task.startupConfirmed) {
|
|
1164
|
+
markTaskReady(task, 'startup_window');
|
|
1090
1165
|
} else {
|
|
1091
|
-
|
|
1166
|
+
task.status = 'running';
|
|
1092
1167
|
}
|
|
1093
1168
|
}
|
|
1094
1169
|
finish();
|
|
@@ -1098,74 +1173,60 @@ async function startService(root, config, args) {
|
|
|
1098
1173
|
? setInterval(async () => {
|
|
1099
1174
|
const open = await probePortOnce(portProbe);
|
|
1100
1175
|
if (open) {
|
|
1101
|
-
|
|
1176
|
+
markTaskReady(task, 'port_probe');
|
|
1102
1177
|
finish();
|
|
1103
1178
|
}
|
|
1104
|
-
},
|
|
1179
|
+
}, BACKGROUND_TASK_POLL_MS)
|
|
1105
1180
|
: null;
|
|
1106
1181
|
const httpHandle =
|
|
1107
1182
|
httpProbe
|
|
1108
1183
|
? setInterval(async () => {
|
|
1109
1184
|
const ok = await probeHttpOnce(httpProbe);
|
|
1110
1185
|
if (ok) {
|
|
1111
|
-
|
|
1186
|
+
markTaskReady(task, 'http_probe');
|
|
1112
1187
|
finish();
|
|
1113
1188
|
}
|
|
1114
|
-
},
|
|
1189
|
+
}, BACKGROUND_TASK_POLL_MS)
|
|
1115
1190
|
: null;
|
|
1116
|
-
|
|
1191
|
+
task.child.once('close', () => finish());
|
|
1117
1192
|
});
|
|
1118
1193
|
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1194
|
+
if (task.status === 'starting') {
|
|
1195
|
+
task.status = 'running';
|
|
1121
1196
|
}
|
|
1122
|
-
return
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function getServiceOrThrow(taskId) {
|
|
1126
|
-
const service = serviceRegistry.get(String(taskId || '').trim());
|
|
1127
|
-
if (!service) throw new Error(`Unknown service task: ${taskId}`);
|
|
1128
|
-
return service;
|
|
1197
|
+
return snapshotBackgroundTask(task);
|
|
1129
1198
|
}
|
|
1130
1199
|
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1200
|
+
function getBackgroundTaskOrThrow(taskId) {
|
|
1201
|
+
const task = backgroundTaskRegistry.get(String(taskId || '').trim());
|
|
1202
|
+
if (!task) throw new Error(`Unknown background task: ${taskId}`);
|
|
1203
|
+
return task;
|
|
1134
1204
|
}
|
|
1135
1205
|
|
|
1136
|
-
async function
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
};
|
|
1206
|
+
async function getBackgroundTask(_root, args) {
|
|
1207
|
+
const task = getBackgroundTaskOrThrow(args?.task_id || args?.taskId);
|
|
1208
|
+
return snapshotBackgroundTask(task);
|
|
1140
1209
|
}
|
|
1141
1210
|
|
|
1142
|
-
async function
|
|
1143
|
-
const service = getServiceOrThrow(args?.task_id || args?.taskId);
|
|
1144
|
-
const tail = Math.max(1, Math.min(200, Number(args?.tail || 40)));
|
|
1145
|
-
const afterCursor = Math.max(0, Number(args?.after_cursor || args?.afterCursor || 0));
|
|
1146
|
-
const filtered = afterCursor > 0 ? service.recentLogs.filter((item) => item.cursor > afterCursor) : service.recentLogs;
|
|
1147
|
-
const selected = filtered.slice(-tail);
|
|
1211
|
+
async function listBackgroundTasks() {
|
|
1148
1212
|
return {
|
|
1149
|
-
|
|
1150
|
-
status: service.status,
|
|
1151
|
-
recent_logs: selected.map((item) => item.line),
|
|
1152
|
-
next_cursor: selected.length > 0 ? selected[selected.length - 1].cursor : afterCursor
|
|
1213
|
+
tasks: listBackgroundTaskSnapshots()
|
|
1153
1214
|
};
|
|
1154
1215
|
}
|
|
1155
1216
|
|
|
1156
|
-
async function
|
|
1157
|
-
const
|
|
1158
|
-
if (
|
|
1159
|
-
return { ...
|
|
1217
|
+
async function stopBackgroundTask(_root, args) {
|
|
1218
|
+
const task = getBackgroundTaskOrThrow(args?.task_id || args?.taskId);
|
|
1219
|
+
if (task.status === 'stopped' || task.status === 'exited') {
|
|
1220
|
+
return { ...snapshotBackgroundTask(task), stopped: true };
|
|
1160
1221
|
}
|
|
1161
|
-
|
|
1162
|
-
terminateChild(
|
|
1163
|
-
setTimeout(() => terminateChild(
|
|
1222
|
+
task.status = 'stopped';
|
|
1223
|
+
terminateChild(task.child, 'SIGTERM');
|
|
1224
|
+
setTimeout(() => terminateChild(task.child, 'SIGKILL'), 200);
|
|
1164
1225
|
await Promise.race([
|
|
1165
|
-
|
|
1226
|
+
task.closePromise,
|
|
1166
1227
|
new Promise((resolve) => setTimeout(resolve, 500))
|
|
1167
1228
|
]);
|
|
1168
|
-
return { ...
|
|
1229
|
+
return { ...snapshotBackgroundTask(task), stopped: true };
|
|
1169
1230
|
}
|
|
1170
1231
|
|
|
1171
1232
|
async function searchCode(root, args) {
|
|
@@ -1696,7 +1757,7 @@ async function editTarget(root, args) {
|
|
|
1696
1757
|
throw new Error(`edit does not support kind: ${kind}`);
|
|
1697
1758
|
}
|
|
1698
1759
|
|
|
1699
|
-
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent }) {
|
|
1760
|
+
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate }) {
|
|
1700
1761
|
const emitSystemTool = (event) => {
|
|
1701
1762
|
if (typeof onSystemEvent === 'function' && event) onSystemEvent(event);
|
|
1702
1763
|
};
|
|
@@ -1824,26 +1885,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1824
1885
|
}
|
|
1825
1886
|
}
|
|
1826
1887
|
},
|
|
1827
|
-
{
|
|
1828
|
-
type: 'function',
|
|
1829
|
-
function: {
|
|
1830
|
-
name: 'glob',
|
|
1831
|
-
description:
|
|
1832
|
-
'Find files by glob pattern. Use this for file discovery before read. Aliases like query and directory are accepted. Do not use run with find for normal file lookup.',
|
|
1833
|
-
parameters: {
|
|
1834
|
-
type: 'object',
|
|
1835
|
-
properties: {
|
|
1836
|
-
pattern: { type: 'string', description: 'Glob pattern' },
|
|
1837
|
-
path: { type: 'string', description: 'Directory to search' },
|
|
1838
|
-
query: { type: 'string', description: 'Alias for pattern' },
|
|
1839
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
1840
|
-
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
1841
|
-
max_results: { type: 'number', description: 'Max results' }
|
|
1842
|
-
},
|
|
1843
|
-
required: ['pattern']
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
},
|
|
1847
1888
|
{
|
|
1848
1889
|
type: 'function',
|
|
1849
1890
|
function: {
|
|
@@ -1930,17 +1971,60 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1930
1971
|
}
|
|
1931
1972
|
}
|
|
1932
1973
|
},
|
|
1974
|
+
{
|
|
1975
|
+
type: 'function',
|
|
1976
|
+
function: {
|
|
1977
|
+
name: 'update_todos',
|
|
1978
|
+
description:
|
|
1979
|
+
'Create or replace the structured todo checklist for the current session. Use this proactively for complex single-task work to track progress. Provide the full current list each time, and keep exactly one item in_progress when work is actively underway.',
|
|
1980
|
+
parameters: {
|
|
1981
|
+
type: 'object',
|
|
1982
|
+
properties: {
|
|
1983
|
+
todos: {
|
|
1984
|
+
type: 'array',
|
|
1985
|
+
items: {
|
|
1986
|
+
type: 'object',
|
|
1987
|
+
properties: {
|
|
1988
|
+
content: { type: 'string', description: 'Imperative task text such as "Run tests"' },
|
|
1989
|
+
activeForm: { type: 'string', description: 'Present continuous form such as "Running tests"' },
|
|
1990
|
+
status: { type: 'string', description: 'pending, in_progress, or completed' }
|
|
1991
|
+
},
|
|
1992
|
+
required: ['content', 'activeForm', 'status']
|
|
1993
|
+
},
|
|
1994
|
+
description: 'The full current todo checklist for this session'
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
required: ['todos']
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
},
|
|
1933
2001
|
{
|
|
1934
2002
|
type: 'function',
|
|
1935
2003
|
function: {
|
|
1936
2004
|
name: 'run',
|
|
1937
2005
|
description:
|
|
1938
|
-
'Run a
|
|
2006
|
+
'Run a shell command. Use this for one-shot commands like install/build/test, and also for long-running commands by setting run_in_background=true. Long-running commands may also be backgrounded automatically.',
|
|
1939
2007
|
parameters: {
|
|
1940
2008
|
type: 'object',
|
|
1941
2009
|
properties: {
|
|
1942
2010
|
command: { type: 'string', description: 'Shell command to execute' },
|
|
1943
|
-
timeout: { type: 'number', description: 'Timeout in milliseconds' }
|
|
2011
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
|
2012
|
+
run_in_background: { type: 'boolean', description: 'Run in the background and return a task handle immediately' },
|
|
2013
|
+
startup_timeout_ms: { type: 'number', description: 'Background startup wait window in milliseconds' },
|
|
2014
|
+
success_matchers: {
|
|
2015
|
+
type: 'array',
|
|
2016
|
+
items: { type: 'string' },
|
|
2017
|
+
description: 'Optional startup success phrases to look for in command output'
|
|
2018
|
+
},
|
|
2019
|
+
port_probe: { type: 'number', description: 'Optional localhost port to probe for readiness' },
|
|
2020
|
+
http_probe: {
|
|
2021
|
+
type: 'object',
|
|
2022
|
+
properties: {
|
|
2023
|
+
url: { type: 'string' },
|
|
2024
|
+
expect_status: { type: 'number' }
|
|
2025
|
+
},
|
|
2026
|
+
description: 'Optional HTTP readiness probe for a background task'
|
|
2027
|
+
}
|
|
1944
2028
|
},
|
|
1945
2029
|
required: ['command']
|
|
1946
2030
|
}
|
|
@@ -1983,6 +2067,26 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1983
2067
|
}
|
|
1984
2068
|
}
|
|
1985
2069
|
},
|
|
2070
|
+
glob: {
|
|
2071
|
+
type: 'function',
|
|
2072
|
+
function: {
|
|
2073
|
+
name: 'glob',
|
|
2074
|
+
description:
|
|
2075
|
+
'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts. Aliases like query and directory are accepted.',
|
|
2076
|
+
parameters: {
|
|
2077
|
+
type: 'object',
|
|
2078
|
+
properties: {
|
|
2079
|
+
pattern: { type: 'string', description: 'Glob pattern' },
|
|
2080
|
+
path: { type: 'string', description: 'Directory to search' },
|
|
2081
|
+
query: { type: 'string', description: 'Alias for pattern' },
|
|
2082
|
+
directory: { type: 'string', description: 'Alias for path' },
|
|
2083
|
+
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
2084
|
+
max_results: { type: 'number', description: 'Max results' }
|
|
2085
|
+
},
|
|
2086
|
+
required: ['pattern']
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
},
|
|
1986
2090
|
read_ast_node: {
|
|
1987
2091
|
type: 'function',
|
|
1988
2092
|
function: {
|
|
@@ -2030,80 +2134,132 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2030
2134
|
}
|
|
2031
2135
|
}
|
|
2032
2136
|
},
|
|
2033
|
-
|
|
2137
|
+
remember_user: {
|
|
2034
2138
|
type: 'function',
|
|
2035
2139
|
function: {
|
|
2036
|
-
name: '
|
|
2037
|
-
description:
|
|
2038
|
-
'Start a long-running local service and return a compact handle. Do not use run for watchers, dev servers, or other persistent processes.',
|
|
2140
|
+
name: 'remember_user',
|
|
2141
|
+
description: 'Store a durable user preference, communication habit, or long-term instruction for future sessions. Use this for things like reply style, language, explanation depth, or stable guardrails. Never store secrets, tokens, passwords, or one-off task details.',
|
|
2039
2142
|
parameters: {
|
|
2040
2143
|
type: 'object',
|
|
2041
2144
|
properties: {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
items: { type: 'string' }
|
|
2047
|
-
},
|
|
2048
|
-
port_probe: { type: 'number' },
|
|
2049
|
-
http_probe: {
|
|
2050
|
-
type: 'object',
|
|
2051
|
-
properties: {
|
|
2052
|
-
url: { type: 'string' },
|
|
2053
|
-
expect_status: { type: 'number' }
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2145
|
+
content: { type: 'string', description: 'Stable preference or instruction to remember' },
|
|
2146
|
+
kind: { type: 'string', description: 'preference, workflow, constraint, or warning' },
|
|
2147
|
+
summary: { type: 'string', description: 'Short summary for the memory index' },
|
|
2148
|
+
replace_similar: { type: 'boolean', description: 'Replace an existing similar memory when true' }
|
|
2056
2149
|
},
|
|
2057
|
-
required: ['
|
|
2150
|
+
required: ['content']
|
|
2058
2151
|
}
|
|
2059
2152
|
}
|
|
2060
2153
|
},
|
|
2061
|
-
|
|
2154
|
+
remember_global: {
|
|
2062
2155
|
type: 'function',
|
|
2063
2156
|
function: {
|
|
2064
|
-
name: '
|
|
2065
|
-
description: '
|
|
2157
|
+
name: 'remember_global',
|
|
2158
|
+
description: 'Store a durable cross-project workflow, environment fact, or generally reusable lesson that can help across many repositories. Use this for stable habits like preferred search tools or repeatable debugging workflow. Never store secrets.',
|
|
2066
2159
|
parameters: {
|
|
2067
2160
|
type: 'object',
|
|
2068
|
-
properties: {
|
|
2161
|
+
properties: {
|
|
2162
|
+
content: { type: 'string' },
|
|
2163
|
+
kind: { type: 'string' },
|
|
2164
|
+
summary: { type: 'string' },
|
|
2165
|
+
replace_similar: { type: 'boolean' }
|
|
2166
|
+
},
|
|
2167
|
+
required: ['content']
|
|
2069
2168
|
}
|
|
2070
2169
|
}
|
|
2071
2170
|
},
|
|
2072
|
-
|
|
2171
|
+
remember_project: {
|
|
2073
2172
|
type: 'function',
|
|
2074
2173
|
function: {
|
|
2075
|
-
name: '
|
|
2076
|
-
description: '
|
|
2174
|
+
name: 'remember_project',
|
|
2175
|
+
description: 'Store a durable project-specific convention, architecture note, key module warning, or local workflow expectation. Use this for repository-specific rules, important files, testing conventions, or architectural boundaries. Never store secrets or transient task state.',
|
|
2077
2176
|
parameters: {
|
|
2078
2177
|
type: 'object',
|
|
2079
2178
|
properties: {
|
|
2080
|
-
|
|
2179
|
+
content: { type: 'string' },
|
|
2180
|
+
kind: { type: 'string' },
|
|
2181
|
+
summary: { type: 'string' },
|
|
2182
|
+
replace_similar: { type: 'boolean' }
|
|
2081
2183
|
},
|
|
2082
|
-
required: ['
|
|
2184
|
+
required: ['content']
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
},
|
|
2188
|
+
list_memory: {
|
|
2189
|
+
type: 'function',
|
|
2190
|
+
function: {
|
|
2191
|
+
name: 'list_memory',
|
|
2192
|
+
description: 'List stored persistent memories for one scope.',
|
|
2193
|
+
parameters: {
|
|
2194
|
+
type: 'object',
|
|
2195
|
+
properties: {
|
|
2196
|
+
scope: { type: 'string', description: 'user, global, or project' }
|
|
2197
|
+
},
|
|
2198
|
+
required: ['scope']
|
|
2083
2199
|
}
|
|
2084
2200
|
}
|
|
2085
2201
|
},
|
|
2086
|
-
|
|
2202
|
+
search_memory: {
|
|
2087
2203
|
type: 'function',
|
|
2088
2204
|
function: {
|
|
2089
|
-
name: '
|
|
2090
|
-
description: '
|
|
2205
|
+
name: 'search_memory',
|
|
2206
|
+
description: 'Search stored persistent memories for one scope.',
|
|
2091
2207
|
parameters: {
|
|
2092
2208
|
type: 'object',
|
|
2093
2209
|
properties: {
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2210
|
+
scope: { type: 'string', description: 'user, global, or project' },
|
|
2211
|
+
query: { type: 'string', description: 'Search phrase' }
|
|
2212
|
+
},
|
|
2213
|
+
required: ['scope', 'query']
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
},
|
|
2217
|
+
forget_memory: {
|
|
2218
|
+
type: 'function',
|
|
2219
|
+
function: {
|
|
2220
|
+
name: 'forget_memory',
|
|
2221
|
+
description: 'Delete a stored persistent memory by id.',
|
|
2222
|
+
parameters: {
|
|
2223
|
+
type: 'object',
|
|
2224
|
+
properties: {
|
|
2225
|
+
scope: { type: 'string', description: 'user, global, or project' },
|
|
2226
|
+
id: { type: 'string', description: 'Memory id to delete' }
|
|
2227
|
+
},
|
|
2228
|
+
required: ['scope', 'id']
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
},
|
|
2232
|
+
list_background_tasks: {
|
|
2233
|
+
type: 'function',
|
|
2234
|
+
function: {
|
|
2235
|
+
name: 'list_background_tasks',
|
|
2236
|
+
description:
|
|
2237
|
+
'List background shell tasks started by run(..., run_in_background=true) or auto-backgrounded by run.',
|
|
2238
|
+
parameters: {
|
|
2239
|
+
type: 'object',
|
|
2240
|
+
properties: {}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
},
|
|
2244
|
+
get_background_task: {
|
|
2245
|
+
type: 'function',
|
|
2246
|
+
function: {
|
|
2247
|
+
name: 'get_background_task',
|
|
2248
|
+
description: 'Get the current status for one background shell task.',
|
|
2249
|
+
parameters: {
|
|
2250
|
+
type: 'object',
|
|
2251
|
+
properties: {
|
|
2252
|
+
task_id: { type: 'string' }
|
|
2097
2253
|
},
|
|
2098
2254
|
required: ['task_id']
|
|
2099
2255
|
}
|
|
2100
2256
|
}
|
|
2101
2257
|
},
|
|
2102
|
-
|
|
2258
|
+
stop_background_task: {
|
|
2103
2259
|
type: 'function',
|
|
2104
2260
|
function: {
|
|
2105
|
-
name: '
|
|
2106
|
-
description: 'Stop a
|
|
2261
|
+
name: 'stop_background_task',
|
|
2262
|
+
description: 'Stop a running background shell task when it is no longer needed.',
|
|
2107
2263
|
parameters: {
|
|
2108
2264
|
type: 'object',
|
|
2109
2265
|
properties: {
|
|
@@ -2179,12 +2335,71 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2179
2335
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2180
2336
|
return result;
|
|
2181
2337
|
},
|
|
2338
|
+
update_todos: async (args = {}) => {
|
|
2339
|
+
const oldTodos = normalizeTodos(typeof getTodos === 'function' ? getTodos() : []);
|
|
2340
|
+
const nextTodos = normalizeTodos(args?.todos);
|
|
2341
|
+
if (typeof onTodosUpdate === 'function') {
|
|
2342
|
+
onTodosUpdate(nextTodos);
|
|
2343
|
+
}
|
|
2344
|
+
return {
|
|
2345
|
+
ok: true,
|
|
2346
|
+
oldTodos,
|
|
2347
|
+
newTodos: nextTodos
|
|
2348
|
+
};
|
|
2349
|
+
},
|
|
2182
2350
|
run: (args) => runCommand(workspaceRoot, config, args),
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2351
|
+
remember_user: async (args = {}) => {
|
|
2352
|
+
const saved = await rememberMemory({
|
|
2353
|
+
scope: 'user',
|
|
2354
|
+
content: args.content,
|
|
2355
|
+
kind: args.kind,
|
|
2356
|
+
summary: args.summary,
|
|
2357
|
+
replaceSimilar: args.replace_similar !== false,
|
|
2358
|
+
workspaceRoot,
|
|
2359
|
+
config
|
|
2360
|
+
});
|
|
2361
|
+
return { ok: true, scope: 'user', memory: saved };
|
|
2362
|
+
},
|
|
2363
|
+
remember_global: async (args = {}) => {
|
|
2364
|
+
const saved = await rememberMemory({
|
|
2365
|
+
scope: 'global',
|
|
2366
|
+
content: args.content,
|
|
2367
|
+
kind: args.kind,
|
|
2368
|
+
summary: args.summary,
|
|
2369
|
+
replaceSimilar: args.replace_similar !== false,
|
|
2370
|
+
workspaceRoot,
|
|
2371
|
+
config
|
|
2372
|
+
});
|
|
2373
|
+
return { ok: true, scope: 'global', memory: saved };
|
|
2374
|
+
},
|
|
2375
|
+
remember_project: async (args = {}) => {
|
|
2376
|
+
const saved = await rememberMemory({
|
|
2377
|
+
scope: 'project',
|
|
2378
|
+
content: args.content,
|
|
2379
|
+
kind: args.kind,
|
|
2380
|
+
summary: args.summary,
|
|
2381
|
+
replaceSimilar: args.replace_similar !== false,
|
|
2382
|
+
workspaceRoot,
|
|
2383
|
+
config
|
|
2384
|
+
});
|
|
2385
|
+
return { ok: true, scope: 'project', memory: saved };
|
|
2386
|
+
},
|
|
2387
|
+
list_memory: async (args = {}) => ({
|
|
2388
|
+
scope: String(args.scope || ''),
|
|
2389
|
+
items: await listMemories({ scope: args.scope, workspaceRoot })
|
|
2390
|
+
}),
|
|
2391
|
+
search_memory: async (args = {}) => ({
|
|
2392
|
+
scope: String(args.scope || ''),
|
|
2393
|
+
query: String(args.query || ''),
|
|
2394
|
+
items: await searchMemories({ scope: args.scope, query: args.query, workspaceRoot })
|
|
2395
|
+
}),
|
|
2396
|
+
forget_memory: async (args = {}) => ({
|
|
2397
|
+
ok: true,
|
|
2398
|
+
...(await forgetMemory({ scope: args.scope, id: args.id, workspaceRoot }))
|
|
2399
|
+
}),
|
|
2400
|
+
list_background_tasks: () => listBackgroundTasks(workspaceRoot),
|
|
2401
|
+
get_background_task: (args) => getBackgroundTask(workspaceRoot, args),
|
|
2402
|
+
stop_background_task: (args) => stopBackgroundTask(workspaceRoot, args),
|
|
2188
2403
|
tool_search: (args) => {
|
|
2189
2404
|
const query = String(args?.query || '').trim().toLowerCase();
|
|
2190
2405
|
if (query === 'all') {
|
|
@@ -2264,6 +2479,17 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2264
2479
|
return `${header}\n${dirs.join('\n')}${dirs.length && files.length ? '\n' : ''}${files.join('\n')}`;
|
|
2265
2480
|
},
|
|
2266
2481
|
|
|
2482
|
+
update_todos(result) {
|
|
2483
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2484
|
+
const nextTodos = normalizeTodos(result.newTodos);
|
|
2485
|
+
if (nextTodos.length === 0) return 'Todo list cleared.';
|
|
2486
|
+
const lines = nextTodos.map((item) => {
|
|
2487
|
+
const box = item.status === 'completed' ? '[x]' : item.status === 'in_progress' ? '[~]' : '[ ]';
|
|
2488
|
+
return `${box} ${item.content}`;
|
|
2489
|
+
});
|
|
2490
|
+
return ['Updated todo list:', ...lines].join('\n');
|
|
2491
|
+
},
|
|
2492
|
+
|
|
2267
2493
|
query_project_index(result) {
|
|
2268
2494
|
if (!result || typeof result !== 'object') return String(result);
|
|
2269
2495
|
const lines = [];
|
|
@@ -2321,6 +2547,18 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2321
2547
|
|
|
2322
2548
|
run(result) {
|
|
2323
2549
|
if (!result || typeof result !== 'object') return String(result);
|
|
2550
|
+
if (result.background) {
|
|
2551
|
+
const parts = [
|
|
2552
|
+
`[background task: ${result.task_id || '?'}]`,
|
|
2553
|
+
`status: ${result.status || 'running'}`
|
|
2554
|
+
];
|
|
2555
|
+
if (result.command) parts.push(`command: ${String(result.command).slice(0, 200)}`);
|
|
2556
|
+
if (result.output_file) parts.push(`output_file: ${result.output_file}`);
|
|
2557
|
+
if (Array.isArray(result.recent_output) && result.recent_output.length > 0) {
|
|
2558
|
+
parts.push(`recent_output:\n${result.recent_output.slice(0, 6).join('\n')}`);
|
|
2559
|
+
}
|
|
2560
|
+
return parts.join('\n');
|
|
2561
|
+
}
|
|
2324
2562
|
const command = String(result.command || '').slice(0, 200);
|
|
2325
2563
|
const stdout = String(result.stdout || '').slice(0, 500);
|
|
2326
2564
|
const stderr = String(result.stderr || '').slice(0, 500);
|
|
@@ -2332,6 +2570,34 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2332
2570
|
return parts.join('\n');
|
|
2333
2571
|
},
|
|
2334
2572
|
|
|
2573
|
+
remember_user(result) {
|
|
2574
|
+
return result?.memory?.content ? `stored user memory: ${result.memory.content}` : JSON.stringify(result);
|
|
2575
|
+
},
|
|
2576
|
+
|
|
2577
|
+
remember_global(result) {
|
|
2578
|
+
return result?.memory?.content ? `stored global memory: ${result.memory.content}` : JSON.stringify(result);
|
|
2579
|
+
},
|
|
2580
|
+
|
|
2581
|
+
remember_project(result) {
|
|
2582
|
+
return result?.memory?.content ? `stored project memory: ${result.memory.content}` : JSON.stringify(result);
|
|
2583
|
+
},
|
|
2584
|
+
|
|
2585
|
+
list_memory(result) {
|
|
2586
|
+
if (!result || typeof result !== 'object' || !Array.isArray(result.items)) return JSON.stringify(result);
|
|
2587
|
+
if (result.items.length === 0) return `No ${result.scope || ''} memories found.`;
|
|
2588
|
+
return result.items.map((item) => `${item.id} [${item.kind}] ${item.content}`).join('\n');
|
|
2589
|
+
},
|
|
2590
|
+
|
|
2591
|
+
search_memory(result) {
|
|
2592
|
+
if (!result || typeof result !== 'object' || !Array.isArray(result.items)) return JSON.stringify(result);
|
|
2593
|
+
if (result.items.length === 0) return `No ${result.scope || ''} memories matched "${result.query || ''}".`;
|
|
2594
|
+
return result.items.map((item) => `${item.id} [${item.kind}] ${item.content}`).join('\n');
|
|
2595
|
+
},
|
|
2596
|
+
|
|
2597
|
+
forget_memory(result) {
|
|
2598
|
+
return `removed ${Number(result?.removed || 0)} memory item(s)`;
|
|
2599
|
+
},
|
|
2600
|
+
|
|
2335
2601
|
generate_diff(result) {
|
|
2336
2602
|
if (!result || typeof result !== 'object') return String(result);
|
|
2337
2603
|
const p = result.path || '';
|
|
@@ -2374,38 +2640,23 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2374
2640
|
return `${header}\n${content.slice(0, 1200)}\n... [omitted ${content.length - 1600} chars] ...\n${content.slice(-400)}`;
|
|
2375
2641
|
},
|
|
2376
2642
|
|
|
2377
|
-
|
|
2378
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2379
|
-
const tid = result.task_id || '';
|
|
2380
|
-
const status = result.status || 'unknown';
|
|
2381
|
-
const confirmed = result.startup_confirmed ? 'ready' : 'starting';
|
|
2382
|
-
const url = result.url || '';
|
|
2383
|
-
return `${tid} ${status} (${confirmed})${url ? ` -> ${url}` : ''}`;
|
|
2384
|
-
},
|
|
2385
|
-
|
|
2386
|
-
list_services(result) {
|
|
2643
|
+
list_background_tasks(result) {
|
|
2387
2644
|
if (!result || typeof result !== 'object') return String(result);
|
|
2388
|
-
if (!Array.isArray(result.
|
|
2389
|
-
if (result.
|
|
2390
|
-
return result.
|
|
2645
|
+
if (!Array.isArray(result.tasks)) return JSON.stringify(result);
|
|
2646
|
+
if (result.tasks.length === 0) return 'No background tasks running.';
|
|
2647
|
+
return result.tasks.map((task) => `${task.task_id || '?'} ${task.status || 'unknown'}${task.command ? ` (${task.command.slice(0, 60)})` : ''}`).join('\n');
|
|
2391
2648
|
},
|
|
2392
2649
|
|
|
2393
|
-
|
|
2650
|
+
get_background_task(result) {
|
|
2394
2651
|
if (!result || typeof result !== 'object') return String(result);
|
|
2395
2652
|
const tid = result.task_id || '';
|
|
2396
2653
|
const status = result.status || 'unknown';
|
|
2397
|
-
const
|
|
2398
|
-
const
|
|
2399
|
-
return `${tid} ${status}${
|
|
2400
|
-
},
|
|
2401
|
-
|
|
2402
|
-
get_service_logs(result) {
|
|
2403
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2404
|
-
const logs = Array.isArray(result.recent_logs) ? result.recent_logs.join('\n') : '';
|
|
2405
|
-
return logs || 'No recent logs.';
|
|
2654
|
+
const outputFile = result.output_file || '';
|
|
2655
|
+
const output = Array.isArray(result.recent_output) ? result.recent_output.slice(-3).join('\n') : '';
|
|
2656
|
+
return `${tid} ${status}${outputFile ? ` -> ${outputFile}` : ''}${output ? `\n${output}` : ''}`;
|
|
2406
2657
|
},
|
|
2407
2658
|
|
|
2408
|
-
|
|
2659
|
+
stop_background_task(result) {
|
|
2409
2660
|
if (!result || typeof result !== 'object') return String(result);
|
|
2410
2661
|
return `${result.task_id || '?'} stopped${result.exit_code != null ? ` (exit ${result.exit_code})` : ''}`;
|
|
2411
2662
|
}
|