@semalt-ai/code 1.8.4 → 1.8.5
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/.claude/settings.local.json +3 -1
- package/CLAUDE.md +4 -1
- package/TECHNICAL_DEBT.md +66 -0
- package/index.js +9 -2
- package/lib/agent.js +234 -87
- package/lib/api.js +95 -6
- package/lib/args.js +22 -0
- package/lib/commands.js +168 -18
- package/lib/config.js +13 -0
- package/lib/debug.js +106 -0
- package/lib/proc.js +96 -0
- package/lib/prompts.js +4 -3
- package/lib/tool_specs.js +14 -7
- package/lib/tools.js +287 -113
- package/lib/ui/chat-history.js +19 -1
- package/lib/ui/format.js +79 -5
- package/lib/ui/terminal.js +10 -4
- package/lib/ui/writer.js +7 -9
- package/package.json +1 -1
package/lib/proc.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const dbg = require('./debug');
|
|
4
|
+
|
|
5
|
+
// Platform-aware subprocess spawn + tree-kill helpers.
|
|
6
|
+
//
|
|
7
|
+
// Why this module exists: when a child is started with `shell: true`, the
|
|
8
|
+
// PID Node hands back is the shell wrapper (`sh -c "..."` on POSIX, `cmd.exe
|
|
9
|
+
// /c "..."` on Windows). Calling `child.kill()` kills the wrapper, but its
|
|
10
|
+
// descendants (the actual `find`, `grep`, `bash` pipeline) become orphans
|
|
11
|
+
// and keep running. To abort cleanly we have to kill the whole process tree.
|
|
12
|
+
//
|
|
13
|
+
// Constraint from the project: no other file imports `process.kill` or
|
|
14
|
+
// `child.kill` directly — those calls live here. `tools.js` (and any future
|
|
15
|
+
// caller) only knows about `spawnWithGroup` and `killTreeEscalating`.
|
|
16
|
+
|
|
17
|
+
const isWindows = process.platform === 'win32';
|
|
18
|
+
|
|
19
|
+
// Wrap `child_process.spawn` so the resulting child is addressable as a
|
|
20
|
+
// process group. POSIX: `detached: true` makes the child a process-group
|
|
21
|
+
// leader, so `process.kill(-pid, sig)` reaches all descendants. Windows:
|
|
22
|
+
// taskkill /T walks the PID hierarchy itself, so `detached` is unnecessary
|
|
23
|
+
// and actively harmful — it would spawn the child in a new console window.
|
|
24
|
+
function spawnWithGroup(spawn, command, args, opts = {}) {
|
|
25
|
+
const finalOpts = { ...opts };
|
|
26
|
+
if (!isWindows) finalOpts.detached = true;
|
|
27
|
+
return spawn(command, args, finalOpts);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function killTree(child, signal) {
|
|
31
|
+
if (!child || child.killed || child.exitCode !== null || child.pid == null) return;
|
|
32
|
+
if (isWindows) {
|
|
33
|
+
// taskkill /T = traverse children, /F = force. windowsHide prevents the
|
|
34
|
+
// brief CMD window flash. Fire and forget — taskkill exits on its own
|
|
35
|
+
// and we don't care about its result code (the child's `exit` event is
|
|
36
|
+
// the authoritative signal).
|
|
37
|
+
const { spawn } = require('child_process');
|
|
38
|
+
try {
|
|
39
|
+
const args = ['/PID', String(child.pid), '/T'];
|
|
40
|
+
if (signal === 'SIGKILL') args.push('/F');
|
|
41
|
+
const tk = spawn('taskkill', args, { windowsHide: true, stdio: 'ignore' });
|
|
42
|
+
tk.on('error', () => {});
|
|
43
|
+
tk.unref();
|
|
44
|
+
} catch {
|
|
45
|
+
// taskkill failed to launch (PID already gone, or taskkill missing on
|
|
46
|
+
// a stripped-down Windows image). The child's exit event will still
|
|
47
|
+
// fire if the process is gone; nothing else to do here.
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
try {
|
|
51
|
+
// Negative PID = whole process group. Requires detached:true at spawn.
|
|
52
|
+
process.kill(-child.pid, signal || 'SIGTERM');
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// ESRCH = process group already gone. Anything else is unexpected but
|
|
55
|
+
// not fatal — surface only when debug is active for triage.
|
|
56
|
+
if (err.code !== 'ESRCH') {
|
|
57
|
+
dbg.log(`[killTree] kill failed: ${err.code} ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Send SIGTERM (or taskkill graceful), wait 2s, escalate to SIGKILL (or
|
|
64
|
+
// taskkill /F) if the tree didn't exit. Hard-coded 2s grace per the abort
|
|
65
|
+
// requirements — long enough for well-behaved children to clean up, short
|
|
66
|
+
// enough that a stuck `trap "" TERM` process doesn't tie up the agent.
|
|
67
|
+
function killTreeEscalating(child) {
|
|
68
|
+
killTree(child, 'SIGTERM');
|
|
69
|
+
const escalation = setTimeout(() => {
|
|
70
|
+
if (child.exitCode === null && !child.killed) killTree(child, 'SIGKILL');
|
|
71
|
+
}, 2000);
|
|
72
|
+
// Don't keep the event loop alive solely for the escalation timer; if the
|
|
73
|
+
// process exits naturally first, the `once('exit')` listener clears it.
|
|
74
|
+
escalation.unref();
|
|
75
|
+
child.once('exit', () => clearTimeout(escalation));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Future Windows-enablement notes:
|
|
79
|
+
// - Job objects (CreateJobObject API via a native binding) give stronger
|
|
80
|
+
// tree-kill guarantees than taskkill, especially for grandchild
|
|
81
|
+
// processes that detach themselves. Consider migrating if taskkill
|
|
82
|
+
// proves unreliable for nested children.
|
|
83
|
+
// - Windows has no SIGTERM/SIGKILL distinction at the OS level for
|
|
84
|
+
// spawned processes. taskkill (without /F) attempts WM_CLOSE-style
|
|
85
|
+
// graceful close; /F is a hard terminate. The 2s escalation here maps
|
|
86
|
+
// to "graceful taskkill, then forceful taskkill" — same shape as POSIX.
|
|
87
|
+
// - shell: true on Windows uses cmd.exe by default. Cross-platform
|
|
88
|
+
// command translation (find, grep, etc.) is the tool layer's problem,
|
|
89
|
+
// not this module's.
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
spawnWithGroup,
|
|
93
|
+
killTree,
|
|
94
|
+
killTreeEscalating,
|
|
95
|
+
isWindows,
|
|
96
|
+
};
|
package/lib/prompts.js
CHANGED
|
@@ -9,6 +9,7 @@ const WRAPPER_NAMES = new Set([
|
|
|
9
9
|
'parameter',
|
|
10
10
|
'tool_call',
|
|
11
11
|
'function_call',
|
|
12
|
+
'function',
|
|
12
13
|
]);
|
|
13
14
|
|
|
14
15
|
// For each tool tag: required attributes and a one-line purpose.
|
|
@@ -32,7 +33,7 @@ const TOOL_TAG_SPECS = {
|
|
|
32
33
|
edit_file: { attrs: ['path', 'line'], purpose: 'Replace a single line in a file (inline content = new line).' },
|
|
33
34
|
search_files: { attrs: ['pattern?', 'dir?'], purpose: 'Find files by glob pattern.' },
|
|
34
35
|
search_in_file: { attrs: ['path'], purpose: 'Regex search inside a file (inline content = pattern).' },
|
|
35
|
-
replace_in_file: { attrs: ['path', 'search', 'replace'], purpose: 'Regex replace inside a file.' },
|
|
36
|
+
replace_in_file: { attrs: ['path', 'search', 'replace'], purpose: 'Regex replace inside a file; inline content is interpreted as regex flags (e.g. g, i, gi).' },
|
|
36
37
|
get_env: { attrs: [], purpose: 'Read an env var (inline content = name).' },
|
|
37
38
|
set_env: { attrs: ['name', 'value'], purpose: 'Set an env var for this process.' },
|
|
38
39
|
download: { attrs: [], purpose: 'HTTP download to the CWD (inline content = URL).' },
|
|
@@ -80,8 +81,8 @@ ${TAG_INVENTORY}
|
|
|
80
81
|
## Reasoning vs planning — IMPORTANT:
|
|
81
82
|
|
|
82
83
|
- Your internal chain-of-thought reasoning uses your native \`<think>...</think>\` block. Use it normally for deliberation. Do NOT treat \`<think>\` as a user-facing tool and do NOT try to emit \`<think>\` as an action — it is reserved for your own reasoning and is handled by the runtime.
|
|
83
|
-
- When you need to explicitly record a short plan that the agent framework can see (for logging or hand-off between steps), use \`<plan>...</plan>\` instead. \`<plan>\`
|
|
84
|
-
-
|
|
84
|
+
- When you need to explicitly record a short plan that the agent framework can see (for logging or hand-off between steps), use \`<plan>...</plan>\` instead. Both \`<think>\` and \`<plan>\` are display-only tags handled by the runtime — never emit either as an action.
|
|
85
|
+
- The valid action tags are the ones listed above.
|
|
85
86
|
|
|
86
87
|
## STRICT RULES — follow exactly:
|
|
87
88
|
|
package/lib/tool_specs.js
CHANGED
|
@@ -75,9 +75,10 @@ const TOOL_SPECS = {
|
|
|
75
75
|
content: {
|
|
76
76
|
type: 'string',
|
|
77
77
|
description: 'Full UTF-8 text to write as the new file contents',
|
|
78
|
+
default: '',
|
|
78
79
|
},
|
|
79
80
|
},
|
|
80
|
-
required: ['path'
|
|
81
|
+
required: ['path'],
|
|
81
82
|
},
|
|
82
83
|
},
|
|
83
84
|
|
|
@@ -93,9 +94,10 @@ const TOOL_SPECS = {
|
|
|
93
94
|
content: {
|
|
94
95
|
type: 'string',
|
|
95
96
|
description: 'Full UTF-8 text to write as the initial file contents',
|
|
97
|
+
default: '',
|
|
96
98
|
},
|
|
97
99
|
},
|
|
98
|
-
required: ['path'
|
|
100
|
+
required: ['path'],
|
|
99
101
|
},
|
|
100
102
|
},
|
|
101
103
|
|
|
@@ -111,9 +113,10 @@ const TOOL_SPECS = {
|
|
|
111
113
|
content: {
|
|
112
114
|
type: 'string',
|
|
113
115
|
description: 'UTF-8 text to append to the end of the file',
|
|
116
|
+
default: '',
|
|
114
117
|
},
|
|
115
118
|
},
|
|
116
|
-
required: ['path'
|
|
119
|
+
required: ['path'],
|
|
117
120
|
},
|
|
118
121
|
},
|
|
119
122
|
|
|
@@ -241,9 +244,10 @@ const TOOL_SPECS = {
|
|
|
241
244
|
content: {
|
|
242
245
|
type: 'string',
|
|
243
246
|
description: 'New text for the target line; trailing newline is added automatically when the file is rejoined',
|
|
247
|
+
default: '',
|
|
244
248
|
},
|
|
245
249
|
},
|
|
246
|
-
required: ['path', 'line'
|
|
250
|
+
required: ['path', 'line'],
|
|
247
251
|
},
|
|
248
252
|
},
|
|
249
253
|
|
|
@@ -255,6 +259,7 @@ const TOOL_SPECS = {
|
|
|
255
259
|
pattern: {
|
|
256
260
|
type: 'string',
|
|
257
261
|
description: 'Glob pattern such as "*.ts" or "src/**/*.js"; matches against the basename when the pattern contains no slash, otherwise against the relative path',
|
|
262
|
+
default: '*',
|
|
258
263
|
},
|
|
259
264
|
dir: {
|
|
260
265
|
type: 'string',
|
|
@@ -262,7 +267,7 @@ const TOOL_SPECS = {
|
|
|
262
267
|
default: '.',
|
|
263
268
|
},
|
|
264
269
|
},
|
|
265
|
-
required: [
|
|
270
|
+
required: [],
|
|
266
271
|
},
|
|
267
272
|
},
|
|
268
273
|
|
|
@@ -300,6 +305,7 @@ const TOOL_SPECS = {
|
|
|
300
305
|
replace: {
|
|
301
306
|
type: 'string',
|
|
302
307
|
description: 'Replacement string; supports the standard $1, $2, $& back-references',
|
|
308
|
+
default: '',
|
|
303
309
|
},
|
|
304
310
|
flags: {
|
|
305
311
|
type: 'string',
|
|
@@ -307,7 +313,7 @@ const TOOL_SPECS = {
|
|
|
307
313
|
default: '',
|
|
308
314
|
},
|
|
309
315
|
},
|
|
310
|
-
required: ['path', 'search'
|
|
316
|
+
required: ['path', 'search'],
|
|
311
317
|
},
|
|
312
318
|
},
|
|
313
319
|
|
|
@@ -370,9 +376,10 @@ const TOOL_SPECS = {
|
|
|
370
376
|
content: {
|
|
371
377
|
type: 'string',
|
|
372
378
|
description: 'Base64-encoded payload to decode and write as the file contents',
|
|
379
|
+
default: '',
|
|
373
380
|
},
|
|
374
381
|
},
|
|
375
|
-
required: ['path'
|
|
382
|
+
required: ['path'],
|
|
376
383
|
},
|
|
377
384
|
},
|
|
378
385
|
|