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/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';
|
|
@@ -18,19 +19,60 @@ 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
|
import { forgetMemory, listMemories, rememberMemory, searchMemories } from './memory-store.js';
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
let
|
|
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
|
+
}
|
|
26
42
|
|
|
27
43
|
function resolveInWorkspace(root, targetPath = '.') {
|
|
28
44
|
const absRoot = path.resolve(root);
|
|
45
|
+
const realRoot = fsSync.realpathSync.native(absRoot);
|
|
29
46
|
const absTarget = path.resolve(absRoot, targetPath);
|
|
30
|
-
|
|
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) {
|
|
64
|
+
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const resolvedTarget = path.join(resolvedProbe, path.relative(probe, absTarget));
|
|
68
|
+
if (!isWithinResolvedRoot(realRoot, resolvedTarget)) {
|
|
31
69
|
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
32
70
|
}
|
|
33
|
-
return
|
|
71
|
+
return resolvedTarget;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getBackgroundTasksDir(root) {
|
|
75
|
+
return path.join(resolveInWorkspace(root, '.codemini'), 'tasks');
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
function toWorkspaceRelative(root, absPath) {
|
|
@@ -233,56 +275,68 @@ function normalizeFileTypes(args = {}) {
|
|
|
233
275
|
return [...new Set(merged)];
|
|
234
276
|
}
|
|
235
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
|
+
|
|
236
299
|
async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
237
300
|
const abs = resolveInWorkspace(root, startPath);
|
|
238
|
-
const out = [];
|
|
239
301
|
const allowedExts = new Set((Array.isArray(fileTypes) ? fileTypes : []).map((item) => `.${String(item || '').replace(/^\./, '')}`));
|
|
240
302
|
|
|
241
303
|
async function visit(current) {
|
|
242
304
|
const stat = await fs.stat(current);
|
|
243
305
|
if (stat.isDirectory()) {
|
|
244
306
|
const name = path.basename(current);
|
|
245
|
-
if (SKIP_DIRS.has(name)) return;
|
|
307
|
+
if (SKIP_DIRS.has(name)) return [];
|
|
246
308
|
const entries = await fs.readdir(current);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
return;
|
|
309
|
+
const nested = await mapLimit(entries, WALKER_CONCURRENCY, async (entry) => visit(path.join(current, entry)));
|
|
310
|
+
return nested.flat();
|
|
251
311
|
}
|
|
252
|
-
if (!detectTextFile(current)) return;
|
|
253
|
-
if (allowedExts.size > 0 && !allowedExts.has(path.extname(current).toLowerCase())) return;
|
|
254
|
-
|
|
312
|
+
if (!detectTextFile(current)) return [];
|
|
313
|
+
if (allowedExts.size > 0 && !allowedExts.has(path.extname(current).toLowerCase())) return [];
|
|
314
|
+
return [current];
|
|
255
315
|
}
|
|
256
316
|
|
|
257
|
-
|
|
258
|
-
return out;
|
|
317
|
+
return visit(abs);
|
|
259
318
|
}
|
|
260
319
|
|
|
261
320
|
async function walkWorkspaceEntries(root, startPath = '.', { includeHidden = false } = {}) {
|
|
262
321
|
const abs = resolveInWorkspace(root, startPath);
|
|
263
|
-
const out = [];
|
|
264
322
|
|
|
265
323
|
async function visit(current) {
|
|
266
324
|
const stat = await fs.stat(current);
|
|
267
325
|
const relative = toWorkspaceRelative(root, current) || '.';
|
|
268
326
|
const name = path.basename(current);
|
|
269
327
|
|
|
270
|
-
if (!includeHidden && name.startsWith('.') && relative !== '.') return;
|
|
328
|
+
if (!includeHidden && name.startsWith('.') && relative !== '.') return [];
|
|
271
329
|
if (stat.isDirectory()) {
|
|
272
|
-
if (SKIP_DIRS.has(name) && relative !== '.') return;
|
|
273
|
-
out.push({ path: relative, name, type: 'dir' });
|
|
330
|
+
if (SKIP_DIRS.has(name) && relative !== '.') return [];
|
|
274
331
|
const entries = await fs.readdir(current);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
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()];
|
|
279
334
|
}
|
|
280
335
|
|
|
281
|
-
|
|
336
|
+
return [{ path: relative, name, type: 'file' }];
|
|
282
337
|
}
|
|
283
338
|
|
|
284
|
-
|
|
285
|
-
return out;
|
|
339
|
+
return visit(abs);
|
|
286
340
|
}
|
|
287
341
|
|
|
288
342
|
function globToRegex(pattern) {
|
|
@@ -840,18 +894,6 @@ async function runCommand(root, config, args) {
|
|
|
840
894
|
if (!command.trim()) {
|
|
841
895
|
throw new Error('run requires command');
|
|
842
896
|
}
|
|
843
|
-
if (isLikelyLongRunningCommand(command)) {
|
|
844
|
-
const intent = classifyCommandIntent(command);
|
|
845
|
-
const labelMap = {
|
|
846
|
-
'frontend-service': 'frontend service',
|
|
847
|
-
'backend-service': 'backend service',
|
|
848
|
-
'database-service': 'database service',
|
|
849
|
-
'docker-service': 'Docker service',
|
|
850
|
-
service: 'long-running service'
|
|
851
|
-
};
|
|
852
|
-
const label = labelMap[intent.kind] || 'long-running service';
|
|
853
|
-
throw new Error(`Command looks like a ${label}. Use start_service instead of run.`);
|
|
854
|
-
}
|
|
855
897
|
if (
|
|
856
898
|
!config.policy.allow_dangerous_commands &&
|
|
857
899
|
isDangerousCommand(command, config.policy.blocked_command_patterns)
|
|
@@ -866,6 +908,16 @@ async function runCommand(root, config, args) {
|
|
|
866
908
|
);
|
|
867
909
|
}
|
|
868
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
|
+
|
|
869
921
|
const result = await runShellCommand({
|
|
870
922
|
command,
|
|
871
923
|
cwd: root,
|
|
@@ -875,9 +927,9 @@ async function runCommand(root, config, args) {
|
|
|
875
927
|
return { ...result, command };
|
|
876
928
|
}
|
|
877
929
|
|
|
878
|
-
function
|
|
879
|
-
|
|
880
|
-
return `
|
|
930
|
+
function nextBackgroundTaskId() {
|
|
931
|
+
backgroundTaskCounter += 1;
|
|
932
|
+
return `task_${String(backgroundTaskCounter).padStart(3, '0')}`;
|
|
881
933
|
}
|
|
882
934
|
|
|
883
935
|
function normalizeSuccessMatchers(items = []) {
|
|
@@ -885,39 +937,39 @@ function normalizeSuccessMatchers(items = []) {
|
|
|
885
937
|
return items.map((item) => String(item || '').trim()).filter(Boolean);
|
|
886
938
|
}
|
|
887
939
|
|
|
888
|
-
function
|
|
940
|
+
function shellCommandForBackgroundTask(command, shellSpec) {
|
|
889
941
|
return process.platform !== 'win32' && /(?:^|\/)bash(?:\.exe)?$/i.test(shellSpec.command)
|
|
890
942
|
? `exec ${command}`
|
|
891
943
|
: command;
|
|
892
944
|
}
|
|
893
945
|
|
|
894
|
-
function
|
|
946
|
+
function appendRecentOutput(task, chunk) {
|
|
895
947
|
const lines = String(chunk || '')
|
|
896
948
|
.split(/\r?\n/)
|
|
897
949
|
.map((line) => trimLinePreview(line, 220))
|
|
898
950
|
.filter(Boolean);
|
|
899
951
|
if (lines.length === 0) return;
|
|
900
952
|
for (const line of lines) {
|
|
901
|
-
|
|
902
|
-
|
|
953
|
+
backgroundTaskLogCursorCounter += 1;
|
|
954
|
+
task.recentLogs.push({ cursor: backgroundTaskLogCursorCounter, line });
|
|
903
955
|
}
|
|
904
|
-
if (
|
|
905
|
-
|
|
956
|
+
if (task.recentLogs.length > BACKGROUND_TASK_RECENT_OUTPUT_LIMIT) {
|
|
957
|
+
task.recentLogs.splice(0, task.recentLogs.length - BACKGROUND_TASK_RECENT_OUTPUT_LIMIT);
|
|
906
958
|
}
|
|
907
959
|
}
|
|
908
960
|
|
|
909
|
-
function
|
|
961
|
+
function matchesTaskStartupSuccess(task, text) {
|
|
910
962
|
const value = String(text || '');
|
|
911
963
|
if (!value) return false;
|
|
912
964
|
if (hasReadyOutput(value)) return true;
|
|
913
|
-
return
|
|
965
|
+
return task.successMatchers.some((matcher) => value.toLowerCase().includes(matcher.toLowerCase()));
|
|
914
966
|
}
|
|
915
967
|
|
|
916
|
-
function
|
|
917
|
-
if (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
968
|
+
function markTaskReady(task, source = 'output') {
|
|
969
|
+
if (task.startupConfirmed) return;
|
|
970
|
+
task.startupConfirmed = true;
|
|
971
|
+
task.startupSource = source;
|
|
972
|
+
task.status = 'running';
|
|
921
973
|
}
|
|
922
974
|
|
|
923
975
|
function serviceUrlForPort(port) {
|
|
@@ -936,34 +988,38 @@ function normalizeHttpProbe(value) {
|
|
|
936
988
|
};
|
|
937
989
|
}
|
|
938
990
|
|
|
939
|
-
function
|
|
940
|
-
const
|
|
941
|
-
?
|
|
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)
|
|
942
994
|
: [];
|
|
943
995
|
const latestCursor =
|
|
944
|
-
Array.isArray(
|
|
945
|
-
?
|
|
996
|
+
Array.isArray(task.recentLogs) && task.recentLogs.length > 0
|
|
997
|
+
? task.recentLogs[task.recentLogs.length - 1].cursor
|
|
946
998
|
: 0;
|
|
947
999
|
return {
|
|
948
|
-
task_id:
|
|
949
|
-
pid:
|
|
950
|
-
command:
|
|
951
|
-
cwd:
|
|
952
|
-
status:
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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,
|
|
958
1014
|
log_cursor: latestCursor,
|
|
959
|
-
exit_code:
|
|
960
|
-
signal:
|
|
961
|
-
duration_ms: Date.now() -
|
|
1015
|
+
exit_code: task.exitCode ?? undefined,
|
|
1016
|
+
signal: task.signal ?? undefined,
|
|
1017
|
+
duration_ms: Date.now() - task.startedAt
|
|
962
1018
|
};
|
|
963
1019
|
}
|
|
964
1020
|
|
|
965
|
-
function
|
|
966
|
-
return Array.from(
|
|
1021
|
+
function listBackgroundTaskSnapshots() {
|
|
1022
|
+
return Array.from(backgroundTaskRegistry.values()).map((task) => snapshotBackgroundTask(task, 4));
|
|
967
1023
|
}
|
|
968
1024
|
|
|
969
1025
|
function probePortOnce(port, host = '127.0.0.1', timeoutMs = 250) {
|
|
@@ -1001,9 +1057,16 @@ async function probeHttpOnce(httpProbe, timeoutMs = 400) {
|
|
|
1001
1057
|
}
|
|
1002
1058
|
}
|
|
1003
1059
|
|
|
1004
|
-
|
|
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) {
|
|
1005
1068
|
const command = String(args?.command || args?.cmd || '').trim();
|
|
1006
|
-
if (!command) throw new Error('
|
|
1069
|
+
if (!command) throw new Error('run requires command');
|
|
1007
1070
|
if (
|
|
1008
1071
|
!config.policy.allow_dangerous_commands &&
|
|
1009
1072
|
isDangerousCommand(command, config.policy.blocked_command_patterns)
|
|
@@ -1018,54 +1081,65 @@ async function startService(root, config, args) {
|
|
|
1018
1081
|
}
|
|
1019
1082
|
|
|
1020
1083
|
const shellSpec = resolveShell(config.shell.default);
|
|
1021
|
-
const taskId =
|
|
1084
|
+
const taskId = nextBackgroundTaskId();
|
|
1022
1085
|
const startupTimeoutMs = Math.max(250, Number(args?.startup_timeout_ms || args?.startupTimeoutMs || 20000));
|
|
1023
1086
|
const successMatchers = normalizeSuccessMatchers(args?.success_matchers || args?.successMatchers);
|
|
1024
1087
|
const portProbe = Number(args?.port_probe || args?.portProbe || 0) || 0;
|
|
1025
1088
|
const httpProbe = normalizeHttpProbe(args?.http_probe || args?.httpProbe);
|
|
1026
|
-
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 = {
|
|
1027
1095
|
taskId,
|
|
1028
1096
|
command,
|
|
1029
1097
|
cwd: root,
|
|
1030
|
-
child: spawn(shellSpec.command, [...shellSpec.args,
|
|
1098
|
+
child: spawn(shellSpec.command, [...shellSpec.args, shellCommandForBackgroundTask(command, shellSpec)], {
|
|
1031
1099
|
cwd: root,
|
|
1032
1100
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
1033
1101
|
}),
|
|
1034
1102
|
startedAt: Date.now(),
|
|
1035
1103
|
status: 'starting',
|
|
1104
|
+
intentKind: classifyCommandIntent(command).kind,
|
|
1036
1105
|
startupConfirmed: false,
|
|
1037
1106
|
startupSource: '',
|
|
1038
1107
|
successMatchers,
|
|
1039
1108
|
portProbe,
|
|
1040
1109
|
httpProbe,
|
|
1110
|
+
outputFileAbs,
|
|
1111
|
+
outputFile: toWorkspaceRelative(root, outputFileAbs),
|
|
1041
1112
|
recentLogs: [],
|
|
1042
1113
|
exitCode: null,
|
|
1043
|
-
signal: null
|
|
1114
|
+
signal: null,
|
|
1115
|
+
outputWrite: Promise.resolve()
|
|
1044
1116
|
};
|
|
1045
|
-
|
|
1117
|
+
backgroundTaskRegistry.set(taskId, task);
|
|
1046
1118
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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';
|
|
1052
1124
|
resolve();
|
|
1053
1125
|
});
|
|
1054
1126
|
});
|
|
1055
1127
|
|
|
1056
1128
|
const onOutput = (chunk) => {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1129
|
+
appendRecentOutput(task, chunk);
|
|
1130
|
+
queueBackgroundTaskOutputWrite(task, chunk);
|
|
1131
|
+
if (matchesTaskStartupSuccess(task, chunk)) {
|
|
1132
|
+
markTaskReady(task, 'output');
|
|
1133
|
+
if (task._finishStartup) task._finishStartup();
|
|
1061
1134
|
}
|
|
1062
1135
|
};
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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();
|
|
1069
1143
|
});
|
|
1070
1144
|
|
|
1071
1145
|
await new Promise((resolve) => {
|
|
@@ -1076,20 +1150,20 @@ async function startService(root, config, args) {
|
|
|
1076
1150
|
clearTimeout(timeoutHandle);
|
|
1077
1151
|
clearInterval(portHandle);
|
|
1078
1152
|
clearInterval(httpHandle);
|
|
1079
|
-
|
|
1153
|
+
task._finishStartup = null;
|
|
1080
1154
|
resolve();
|
|
1081
1155
|
};
|
|
1082
|
-
|
|
1083
|
-
if (
|
|
1156
|
+
task._finishStartup = finish;
|
|
1157
|
+
if (task.startupConfirmed || task.status === 'exited') {
|
|
1084
1158
|
finish();
|
|
1085
1159
|
return;
|
|
1086
1160
|
}
|
|
1087
1161
|
const timeoutHandle = setTimeout(() => {
|
|
1088
|
-
if (
|
|
1089
|
-
if (!
|
|
1090
|
-
|
|
1162
|
+
if (task.status === 'starting') {
|
|
1163
|
+
if (!task.startupConfirmed) {
|
|
1164
|
+
markTaskReady(task, 'startup_window');
|
|
1091
1165
|
} else {
|
|
1092
|
-
|
|
1166
|
+
task.status = 'running';
|
|
1093
1167
|
}
|
|
1094
1168
|
}
|
|
1095
1169
|
finish();
|
|
@@ -1099,74 +1173,60 @@ async function startService(root, config, args) {
|
|
|
1099
1173
|
? setInterval(async () => {
|
|
1100
1174
|
const open = await probePortOnce(portProbe);
|
|
1101
1175
|
if (open) {
|
|
1102
|
-
|
|
1176
|
+
markTaskReady(task, 'port_probe');
|
|
1103
1177
|
finish();
|
|
1104
1178
|
}
|
|
1105
|
-
},
|
|
1179
|
+
}, BACKGROUND_TASK_POLL_MS)
|
|
1106
1180
|
: null;
|
|
1107
1181
|
const httpHandle =
|
|
1108
1182
|
httpProbe
|
|
1109
1183
|
? setInterval(async () => {
|
|
1110
1184
|
const ok = await probeHttpOnce(httpProbe);
|
|
1111
1185
|
if (ok) {
|
|
1112
|
-
|
|
1186
|
+
markTaskReady(task, 'http_probe');
|
|
1113
1187
|
finish();
|
|
1114
1188
|
}
|
|
1115
|
-
},
|
|
1189
|
+
}, BACKGROUND_TASK_POLL_MS)
|
|
1116
1190
|
: null;
|
|
1117
|
-
|
|
1191
|
+
task.child.once('close', () => finish());
|
|
1118
1192
|
});
|
|
1119
1193
|
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1194
|
+
if (task.status === 'starting') {
|
|
1195
|
+
task.status = 'running';
|
|
1122
1196
|
}
|
|
1123
|
-
return
|
|
1197
|
+
return snapshotBackgroundTask(task);
|
|
1124
1198
|
}
|
|
1125
1199
|
|
|
1126
|
-
function
|
|
1127
|
-
const
|
|
1128
|
-
if (!
|
|
1129
|
-
return
|
|
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;
|
|
1130
1204
|
}
|
|
1131
1205
|
|
|
1132
|
-
async function
|
|
1133
|
-
const
|
|
1134
|
-
return
|
|
1206
|
+
async function getBackgroundTask(_root, args) {
|
|
1207
|
+
const task = getBackgroundTaskOrThrow(args?.task_id || args?.taskId);
|
|
1208
|
+
return snapshotBackgroundTask(task);
|
|
1135
1209
|
}
|
|
1136
1210
|
|
|
1137
|
-
async function
|
|
1211
|
+
async function listBackgroundTasks() {
|
|
1138
1212
|
return {
|
|
1139
|
-
|
|
1213
|
+
tasks: listBackgroundTaskSnapshots()
|
|
1140
1214
|
};
|
|
1141
1215
|
}
|
|
1142
1216
|
|
|
1143
|
-
async function
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const filtered = afterCursor > 0 ? service.recentLogs.filter((item) => item.cursor > afterCursor) : service.recentLogs;
|
|
1148
|
-
const selected = filtered.slice(-tail);
|
|
1149
|
-
return {
|
|
1150
|
-
task_id: service.taskId,
|
|
1151
|
-
status: service.status,
|
|
1152
|
-
recent_logs: selected.map((item) => item.line),
|
|
1153
|
-
next_cursor: selected.length > 0 ? selected[selected.length - 1].cursor : afterCursor
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
async function stopService(_root, args) {
|
|
1158
|
-
const service = getServiceOrThrow(args?.task_id || args?.taskId);
|
|
1159
|
-
if (service.status === 'stopped' || service.status === 'exited') {
|
|
1160
|
-
return { ...snapshotService(service), stopped: true };
|
|
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 };
|
|
1161
1221
|
}
|
|
1162
|
-
|
|
1163
|
-
terminateChild(
|
|
1164
|
-
setTimeout(() => terminateChild(
|
|
1222
|
+
task.status = 'stopped';
|
|
1223
|
+
terminateChild(task.child, 'SIGTERM');
|
|
1224
|
+
setTimeout(() => terminateChild(task.child, 'SIGKILL'), 200);
|
|
1165
1225
|
await Promise.race([
|
|
1166
|
-
|
|
1226
|
+
task.closePromise,
|
|
1167
1227
|
new Promise((resolve) => setTimeout(resolve, 500))
|
|
1168
1228
|
]);
|
|
1169
|
-
return { ...
|
|
1229
|
+
return { ...snapshotBackgroundTask(task), stopped: true };
|
|
1170
1230
|
}
|
|
1171
1231
|
|
|
1172
1232
|
async function searchCode(root, args) {
|
|
@@ -1697,7 +1757,7 @@ async function editTarget(root, args) {
|
|
|
1697
1757
|
throw new Error(`edit does not support kind: ${kind}`);
|
|
1698
1758
|
}
|
|
1699
1759
|
|
|
1700
|
-
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent }) {
|
|
1760
|
+
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate }) {
|
|
1701
1761
|
const emitSystemTool = (event) => {
|
|
1702
1762
|
if (typeof onSystemEvent === 'function' && event) onSystemEvent(event);
|
|
1703
1763
|
};
|
|
@@ -1825,26 +1885,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1825
1885
|
}
|
|
1826
1886
|
}
|
|
1827
1887
|
},
|
|
1828
|
-
{
|
|
1829
|
-
type: 'function',
|
|
1830
|
-
function: {
|
|
1831
|
-
name: 'glob',
|
|
1832
|
-
description:
|
|
1833
|
-
'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.',
|
|
1834
|
-
parameters: {
|
|
1835
|
-
type: 'object',
|
|
1836
|
-
properties: {
|
|
1837
|
-
pattern: { type: 'string', description: 'Glob pattern' },
|
|
1838
|
-
path: { type: 'string', description: 'Directory to search' },
|
|
1839
|
-
query: { type: 'string', description: 'Alias for pattern' },
|
|
1840
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
1841
|
-
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
1842
|
-
max_results: { type: 'number', description: 'Max results' }
|
|
1843
|
-
},
|
|
1844
|
-
required: ['pattern']
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
},
|
|
1848
1888
|
{
|
|
1849
1889
|
type: 'function',
|
|
1850
1890
|
function: {
|
|
@@ -1931,17 +1971,60 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1931
1971
|
}
|
|
1932
1972
|
}
|
|
1933
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
|
+
},
|
|
1934
2001
|
{
|
|
1935
2002
|
type: 'function',
|
|
1936
2003
|
function: {
|
|
1937
2004
|
name: 'run',
|
|
1938
2005
|
description:
|
|
1939
|
-
'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.',
|
|
1940
2007
|
parameters: {
|
|
1941
2008
|
type: 'object',
|
|
1942
2009
|
properties: {
|
|
1943
2010
|
command: { type: 'string', description: 'Shell command to execute' },
|
|
1944
|
-
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
|
+
}
|
|
1945
2028
|
},
|
|
1946
2029
|
required: ['command']
|
|
1947
2030
|
}
|
|
@@ -1961,215 +2044,208 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1961
2044
|
required: ['query']
|
|
1962
2045
|
}
|
|
1963
2046
|
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
2047
|
+
}
|
|
2048
|
+
];
|
|
2049
|
+
|
|
2050
|
+
const deferredDefinitions = {
|
|
2051
|
+
ast_query: {
|
|
1966
2052
|
type: 'function',
|
|
1967
2053
|
function: {
|
|
1968
|
-
name: '
|
|
1969
|
-
description:
|
|
2054
|
+
name: 'ast_query',
|
|
2055
|
+
description:
|
|
2056
|
+
'Run a Tree-sitter query on a code file and return ast_target objects. Use this when you need node-scoped reads or edits for functions, classes, or methods.',
|
|
1970
2057
|
parameters: {
|
|
1971
2058
|
type: 'object',
|
|
1972
2059
|
properties: {
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2060
|
+
path: { type: 'string' },
|
|
2061
|
+
language: { type: 'string' },
|
|
2062
|
+
query: { type: 'string' },
|
|
2063
|
+
capture_name: { type: 'string' },
|
|
2064
|
+
max_results: { type: 'number' }
|
|
1977
2065
|
},
|
|
1978
|
-
required: ['
|
|
2066
|
+
required: ['path', 'query']
|
|
1979
2067
|
}
|
|
1980
2068
|
}
|
|
1981
2069
|
},
|
|
1982
|
-
{
|
|
2070
|
+
glob: {
|
|
1983
2071
|
type: 'function',
|
|
1984
2072
|
function: {
|
|
1985
|
-
name: '
|
|
1986
|
-
description:
|
|
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.',
|
|
1987
2076
|
parameters: {
|
|
1988
2077
|
type: 'object',
|
|
1989
2078
|
properties: {
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
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' }
|
|
1994
2085
|
},
|
|
1995
|
-
required: ['
|
|
2086
|
+
required: ['pattern']
|
|
1996
2087
|
}
|
|
1997
2088
|
}
|
|
1998
2089
|
},
|
|
1999
|
-
{
|
|
2090
|
+
read_ast_node: {
|
|
2000
2091
|
type: 'function',
|
|
2001
2092
|
function: {
|
|
2002
|
-
name: '
|
|
2003
|
-
description:
|
|
2093
|
+
name: 'read_ast_node',
|
|
2094
|
+
description:
|
|
2095
|
+
'Read a previously selected AST node with compact structural context. Use this after ast_query before a scoped structural edit.',
|
|
2004
2096
|
parameters: {
|
|
2005
2097
|
type: 'object',
|
|
2006
2098
|
properties: {
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
replace_similar: { type: 'boolean' }
|
|
2099
|
+
path: { type: 'string' },
|
|
2100
|
+
language: { type: 'string' },
|
|
2101
|
+
ast_target: { type: 'object' }
|
|
2011
2102
|
},
|
|
2012
|
-
required: ['
|
|
2103
|
+
required: ['path', 'ast_target']
|
|
2013
2104
|
}
|
|
2014
2105
|
}
|
|
2015
2106
|
},
|
|
2016
|
-
{
|
|
2107
|
+
generate_diff: {
|
|
2017
2108
|
type: 'function',
|
|
2018
2109
|
function: {
|
|
2019
|
-
name: '
|
|
2020
|
-
description: '
|
|
2110
|
+
name: 'generate_diff',
|
|
2111
|
+
description: 'Generate a unified diff for proposed content. Use this when you want to preview or prepare a patch before applying it.',
|
|
2021
2112
|
parameters: {
|
|
2022
2113
|
type: 'object',
|
|
2023
2114
|
properties: {
|
|
2024
|
-
|
|
2115
|
+
path: { type: 'string' },
|
|
2116
|
+
new_content: { type: 'string' }
|
|
2025
2117
|
},
|
|
2026
|
-
required: ['
|
|
2118
|
+
required: ['path', 'new_content']
|
|
2027
2119
|
}
|
|
2028
2120
|
}
|
|
2029
2121
|
},
|
|
2030
|
-
{
|
|
2122
|
+
patch: {
|
|
2031
2123
|
type: 'function',
|
|
2032
2124
|
function: {
|
|
2033
|
-
name: '
|
|
2034
|
-
description: '
|
|
2125
|
+
name: 'patch',
|
|
2126
|
+
description: 'Apply one or more unified diff hunks to workspace files. Use this for prepared unified diffs instead of ad-hoc shell patching.',
|
|
2035
2127
|
parameters: {
|
|
2036
2128
|
type: 'object',
|
|
2037
2129
|
properties: {
|
|
2038
|
-
|
|
2039
|
-
|
|
2130
|
+
patch: { type: 'string' },
|
|
2131
|
+
content: { type: 'string' }
|
|
2040
2132
|
},
|
|
2041
|
-
required: ['
|
|
2133
|
+
required: ['patch']
|
|
2042
2134
|
}
|
|
2043
2135
|
}
|
|
2044
2136
|
},
|
|
2045
|
-
{
|
|
2137
|
+
remember_user: {
|
|
2046
2138
|
type: 'function',
|
|
2047
2139
|
function: {
|
|
2048
|
-
name: '
|
|
2049
|
-
description: '
|
|
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.',
|
|
2050
2142
|
parameters: {
|
|
2051
2143
|
type: 'object',
|
|
2052
2144
|
properties: {
|
|
2053
|
-
|
|
2054
|
-
|
|
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' }
|
|
2055
2149
|
},
|
|
2056
|
-
required: ['
|
|
2150
|
+
required: ['content']
|
|
2057
2151
|
}
|
|
2058
2152
|
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
const deferredDefinitions = {
|
|
2063
|
-
ast_query: {
|
|
2153
|
+
},
|
|
2154
|
+
remember_global: {
|
|
2064
2155
|
type: 'function',
|
|
2065
2156
|
function: {
|
|
2066
|
-
name: '
|
|
2067
|
-
description:
|
|
2068
|
-
'Run a Tree-sitter query on a code file and return ast_target objects. Use this when you need node-scoped reads or edits for functions, classes, or methods.',
|
|
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.',
|
|
2069
2159
|
parameters: {
|
|
2070
2160
|
type: 'object',
|
|
2071
2161
|
properties: {
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
max_results: { type: 'number' }
|
|
2162
|
+
content: { type: 'string' },
|
|
2163
|
+
kind: { type: 'string' },
|
|
2164
|
+
summary: { type: 'string' },
|
|
2165
|
+
replace_similar: { type: 'boolean' }
|
|
2077
2166
|
},
|
|
2078
|
-
required: ['
|
|
2167
|
+
required: ['content']
|
|
2079
2168
|
}
|
|
2080
2169
|
}
|
|
2081
2170
|
},
|
|
2082
|
-
|
|
2171
|
+
remember_project: {
|
|
2083
2172
|
type: 'function',
|
|
2084
2173
|
function: {
|
|
2085
|
-
name: '
|
|
2086
|
-
description:
|
|
2087
|
-
'Read a previously selected AST node with compact structural context. Use this after ast_query before a scoped structural edit.',
|
|
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.',
|
|
2088
2176
|
parameters: {
|
|
2089
2177
|
type: 'object',
|
|
2090
2178
|
properties: {
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2179
|
+
content: { type: 'string' },
|
|
2180
|
+
kind: { type: 'string' },
|
|
2181
|
+
summary: { type: 'string' },
|
|
2182
|
+
replace_similar: { type: 'boolean' }
|
|
2094
2183
|
},
|
|
2095
|
-
required: ['
|
|
2184
|
+
required: ['content']
|
|
2096
2185
|
}
|
|
2097
2186
|
}
|
|
2098
2187
|
},
|
|
2099
|
-
|
|
2188
|
+
list_memory: {
|
|
2100
2189
|
type: 'function',
|
|
2101
2190
|
function: {
|
|
2102
|
-
name: '
|
|
2103
|
-
description: '
|
|
2191
|
+
name: 'list_memory',
|
|
2192
|
+
description: 'List stored persistent memories for one scope.',
|
|
2104
2193
|
parameters: {
|
|
2105
2194
|
type: 'object',
|
|
2106
2195
|
properties: {
|
|
2107
|
-
|
|
2108
|
-
new_content: { type: 'string' }
|
|
2196
|
+
scope: { type: 'string', description: 'user, global, or project' }
|
|
2109
2197
|
},
|
|
2110
|
-
required: ['
|
|
2198
|
+
required: ['scope']
|
|
2111
2199
|
}
|
|
2112
2200
|
}
|
|
2113
2201
|
},
|
|
2114
|
-
|
|
2202
|
+
search_memory: {
|
|
2115
2203
|
type: 'function',
|
|
2116
2204
|
function: {
|
|
2117
|
-
name: '
|
|
2118
|
-
description: '
|
|
2205
|
+
name: 'search_memory',
|
|
2206
|
+
description: 'Search stored persistent memories for one scope.',
|
|
2119
2207
|
parameters: {
|
|
2120
2208
|
type: 'object',
|
|
2121
2209
|
properties: {
|
|
2122
|
-
|
|
2123
|
-
|
|
2210
|
+
scope: { type: 'string', description: 'user, global, or project' },
|
|
2211
|
+
query: { type: 'string', description: 'Search phrase' }
|
|
2124
2212
|
},
|
|
2125
|
-
required: ['
|
|
2213
|
+
required: ['scope', 'query']
|
|
2126
2214
|
}
|
|
2127
2215
|
}
|
|
2128
2216
|
},
|
|
2129
|
-
|
|
2217
|
+
forget_memory: {
|
|
2130
2218
|
type: 'function',
|
|
2131
2219
|
function: {
|
|
2132
|
-
name: '
|
|
2133
|
-
description:
|
|
2134
|
-
'Start a long-running local service and return a compact handle. Do not use run for watchers, dev servers, or other persistent processes.',
|
|
2220
|
+
name: 'forget_memory',
|
|
2221
|
+
description: 'Delete a stored persistent memory by id.',
|
|
2135
2222
|
parameters: {
|
|
2136
2223
|
type: 'object',
|
|
2137
2224
|
properties: {
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
success_matchers: {
|
|
2141
|
-
type: 'array',
|
|
2142
|
-
items: { type: 'string' }
|
|
2143
|
-
},
|
|
2144
|
-
port_probe: { type: 'number' },
|
|
2145
|
-
http_probe: {
|
|
2146
|
-
type: 'object',
|
|
2147
|
-
properties: {
|
|
2148
|
-
url: { type: 'string' },
|
|
2149
|
-
expect_status: { type: 'number' }
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2225
|
+
scope: { type: 'string', description: 'user, global, or project' },
|
|
2226
|
+
id: { type: 'string', description: 'Memory id to delete' }
|
|
2152
2227
|
},
|
|
2153
|
-
required: ['
|
|
2228
|
+
required: ['scope', 'id']
|
|
2154
2229
|
}
|
|
2155
2230
|
}
|
|
2156
2231
|
},
|
|
2157
|
-
|
|
2232
|
+
list_background_tasks: {
|
|
2158
2233
|
type: 'function',
|
|
2159
2234
|
function: {
|
|
2160
|
-
name: '
|
|
2161
|
-
description:
|
|
2235
|
+
name: 'list_background_tasks',
|
|
2236
|
+
description:
|
|
2237
|
+
'List background shell tasks started by run(..., run_in_background=true) or auto-backgrounded by run.',
|
|
2162
2238
|
parameters: {
|
|
2163
2239
|
type: 'object',
|
|
2164
2240
|
properties: {}
|
|
2165
2241
|
}
|
|
2166
2242
|
}
|
|
2167
2243
|
},
|
|
2168
|
-
|
|
2244
|
+
get_background_task: {
|
|
2169
2245
|
type: 'function',
|
|
2170
2246
|
function: {
|
|
2171
|
-
name: '
|
|
2172
|
-
description: 'Get the status
|
|
2247
|
+
name: 'get_background_task',
|
|
2248
|
+
description: 'Get the current status for one background shell task.',
|
|
2173
2249
|
parameters: {
|
|
2174
2250
|
type: 'object',
|
|
2175
2251
|
properties: {
|
|
@@ -2179,27 +2255,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2179
2255
|
}
|
|
2180
2256
|
}
|
|
2181
2257
|
},
|
|
2182
|
-
|
|
2258
|
+
stop_background_task: {
|
|
2183
2259
|
type: 'function',
|
|
2184
2260
|
function: {
|
|
2185
|
-
name: '
|
|
2186
|
-
description: '
|
|
2187
|
-
parameters: {
|
|
2188
|
-
type: 'object',
|
|
2189
|
-
properties: {
|
|
2190
|
-
task_id: { type: 'string' },
|
|
2191
|
-
tail: { type: 'number' },
|
|
2192
|
-
after_cursor: { type: 'number' }
|
|
2193
|
-
},
|
|
2194
|
-
required: ['task_id']
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
},
|
|
2198
|
-
stop_service: {
|
|
2199
|
-
type: 'function',
|
|
2200
|
-
function: {
|
|
2201
|
-
name: 'stop_service',
|
|
2202
|
-
description: 'Stop a started service when it is no longer needed or when you need a clean restart.',
|
|
2261
|
+
name: 'stop_background_task',
|
|
2262
|
+
description: 'Stop a running background shell task when it is no longer needed.',
|
|
2203
2263
|
parameters: {
|
|
2204
2264
|
type: 'object',
|
|
2205
2265
|
properties: {
|
|
@@ -2275,6 +2335,18 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2275
2335
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2276
2336
|
return result;
|
|
2277
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
|
+
},
|
|
2278
2350
|
run: (args) => runCommand(workspaceRoot, config, args),
|
|
2279
2351
|
remember_user: async (args = {}) => {
|
|
2280
2352
|
const saved = await rememberMemory({
|
|
@@ -2325,11 +2397,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2325
2397
|
ok: true,
|
|
2326
2398
|
...(await forgetMemory({ scope: args.scope, id: args.id, workspaceRoot }))
|
|
2327
2399
|
}),
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
get_service_logs: (args) => getServiceLogs(workspaceRoot, args),
|
|
2332
|
-
stop_service: (args) => stopService(workspaceRoot, args),
|
|
2400
|
+
list_background_tasks: () => listBackgroundTasks(workspaceRoot),
|
|
2401
|
+
get_background_task: (args) => getBackgroundTask(workspaceRoot, args),
|
|
2402
|
+
stop_background_task: (args) => stopBackgroundTask(workspaceRoot, args),
|
|
2333
2403
|
tool_search: (args) => {
|
|
2334
2404
|
const query = String(args?.query || '').trim().toLowerCase();
|
|
2335
2405
|
if (query === 'all') {
|
|
@@ -2409,6 +2479,17 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2409
2479
|
return `${header}\n${dirs.join('\n')}${dirs.length && files.length ? '\n' : ''}${files.join('\n')}`;
|
|
2410
2480
|
},
|
|
2411
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
|
+
|
|
2412
2493
|
query_project_index(result) {
|
|
2413
2494
|
if (!result || typeof result !== 'object') return String(result);
|
|
2414
2495
|
const lines = [];
|
|
@@ -2466,6 +2547,18 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2466
2547
|
|
|
2467
2548
|
run(result) {
|
|
2468
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
|
+
}
|
|
2469
2562
|
const command = String(result.command || '').slice(0, 200);
|
|
2470
2563
|
const stdout = String(result.stdout || '').slice(0, 500);
|
|
2471
2564
|
const stderr = String(result.stderr || '').slice(0, 500);
|
|
@@ -2547,38 +2640,23 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2547
2640
|
return `${header}\n${content.slice(0, 1200)}\n... [omitted ${content.length - 1600} chars] ...\n${content.slice(-400)}`;
|
|
2548
2641
|
},
|
|
2549
2642
|
|
|
2550
|
-
|
|
2643
|
+
list_background_tasks(result) {
|
|
2551
2644
|
if (!result || typeof result !== 'object') return String(result);
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
const url = result.url || '';
|
|
2556
|
-
return `${tid} ${status} (${confirmed})${url ? ` -> ${url}` : ''}`;
|
|
2557
|
-
},
|
|
2558
|
-
|
|
2559
|
-
list_services(result) {
|
|
2560
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2561
|
-
if (!Array.isArray(result.services)) return JSON.stringify(result);
|
|
2562
|
-
if (result.services.length === 0) return 'No services running.';
|
|
2563
|
-
return result.services.map((s) => `${s.task_id || '?'} ${s.status || 'unknown'}${s.command ? ` (${s.command.slice(0, 60)})` : ''}`).join('\n');
|
|
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');
|
|
2564
2648
|
},
|
|
2565
2649
|
|
|
2566
|
-
|
|
2650
|
+
get_background_task(result) {
|
|
2567
2651
|
if (!result || typeof result !== 'object') return String(result);
|
|
2568
2652
|
const tid = result.task_id || '';
|
|
2569
2653
|
const status = result.status || 'unknown';
|
|
2570
|
-
const
|
|
2571
|
-
const
|
|
2572
|
-
return `${tid} ${status}${
|
|
2573
|
-
},
|
|
2574
|
-
|
|
2575
|
-
get_service_logs(result) {
|
|
2576
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2577
|
-
const logs = Array.isArray(result.recent_logs) ? result.recent_logs.join('\n') : '';
|
|
2578
|
-
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}` : ''}`;
|
|
2579
2657
|
},
|
|
2580
2658
|
|
|
2581
|
-
|
|
2659
|
+
stop_background_task(result) {
|
|
2582
2660
|
if (!result || typeof result !== 'object') return String(result);
|
|
2583
2661
|
return `${result.task_id || '?'} stopped${result.exit_code != null ? ` (exit ${result.exit_code})` : ''}`;
|
|
2584
2662
|
}
|