@mjasnikovs/pi-task 0.18.25 → 0.18.26
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/dist/config/config.d.ts +29 -0
- package/dist/config/config.js +25 -1
- package/dist/config/register.js +23 -2
- package/dist/index.js +2 -0
- package/dist/task/command-watchdog.d.ts +67 -0
- package/dist/task/command-watchdog.js +114 -0
- package/dist/task/final-gate.js +6 -0
- package/package.json +1 -1
package/dist/config/config.d.ts
CHANGED
|
@@ -42,7 +42,36 @@ export interface PiTaskConfig {
|
|
|
42
42
|
* /task-config, which enumerates the currently installed extensions.
|
|
43
43
|
*/
|
|
44
44
|
extensionWhitelist: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Wall-clock ceiling (ms) on a SINGLE tool execution in the MAIN session
|
|
47
|
+
* before the command watchdog cancels it and reminds the model to set its
|
|
48
|
+
* own timeout. Local models routinely run a command that never returns
|
|
49
|
+
* (e.g. `godot --headless` with no timeout, a dev server, a hung test) and
|
|
50
|
+
* the run wedges until the user manually aborts. pi's bash tool has an
|
|
51
|
+
* OPTIONAL timeout with NO default (bash.js), so a command the model didn't
|
|
52
|
+
* bound runs forever; this is the missing default, enforced from the host
|
|
53
|
+
* side via ctx.abort() (which kills the tool's whole process tree) plus an
|
|
54
|
+
* auto-reminder turn. Tool-agnostic: it arms on any tool execution, though
|
|
55
|
+
* in practice only bash runs long enough to trip it. 0 = off.
|
|
56
|
+
* DEFAULT 15 min: long enough for a real build/test suite, short enough that
|
|
57
|
+
* a true hang doesn't cost half an hour of dead time.
|
|
58
|
+
*/
|
|
59
|
+
requestTimeoutMs: number;
|
|
45
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The command-watchdog timeout choices offered by /task-config, newest-first in
|
|
63
|
+
* the cycle order the picker shows. The stored config value is the ms number;
|
|
64
|
+
* the label is display-only (mirrors the searchProvider label/value split).
|
|
65
|
+
*/
|
|
66
|
+
export declare const COMMAND_TIMEOUT_OPTIONS: ReadonlyArray<{
|
|
67
|
+
label: string;
|
|
68
|
+
ms: number;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* A hand-edited or stale config could carry any number (or a string); pin it to
|
|
72
|
+
* one of the offered choices so the watchdog never arms on a nonsense value.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sanitizeRequestTimeoutMs(value: unknown): number;
|
|
46
75
|
/**
|
|
47
76
|
* A hand-edited config can hold anything; keep only string entries so a stray
|
|
48
77
|
* object/number can't reach the child argv as `-e [object Object]`.
|
package/dist/config/config.js
CHANGED
|
@@ -3,6 +3,28 @@ import * as fsp from 'node:fs/promises';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import { isSearchProvider } from '../workers/search-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* The command-watchdog timeout choices offered by /task-config, newest-first in
|
|
8
|
+
* the cycle order the picker shows. The stored config value is the ms number;
|
|
9
|
+
* the label is display-only (mirrors the searchProvider label/value split).
|
|
10
|
+
*/
|
|
11
|
+
export const COMMAND_TIMEOUT_OPTIONS = [
|
|
12
|
+
{ label: '5 min', ms: 5 * 60_000 },
|
|
13
|
+
{ label: '10 min', ms: 10 * 60_000 },
|
|
14
|
+
{ label: '15 min', ms: 15 * 60_000 },
|
|
15
|
+
{ label: '30 min', ms: 30 * 60_000 },
|
|
16
|
+
{ label: 'off', ms: 0 }
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15 * 60_000;
|
|
19
|
+
/**
|
|
20
|
+
* A hand-edited or stale config could carry any number (or a string); pin it to
|
|
21
|
+
* one of the offered choices so the watchdog never arms on a nonsense value.
|
|
22
|
+
*/
|
|
23
|
+
export function sanitizeRequestTimeoutMs(value) {
|
|
24
|
+
return COMMAND_TIMEOUT_OPTIONS.some(o => o.ms === value) ?
|
|
25
|
+
value
|
|
26
|
+
: DEFAULT_REQUEST_TIMEOUT_MS;
|
|
27
|
+
}
|
|
6
28
|
const DEFAULTS = {
|
|
7
29
|
remote: true,
|
|
8
30
|
compressReasoning: true,
|
|
@@ -15,7 +37,8 @@ const DEFAULTS = {
|
|
|
15
37
|
// 3/3, 0 collisions; ~14.5s of repeated docs lookups collapse to 0ms on a hit).
|
|
16
38
|
researchCache: true,
|
|
17
39
|
searchProvider: 'exa',
|
|
18
|
-
extensionWhitelist: []
|
|
40
|
+
extensionWhitelist: [],
|
|
41
|
+
requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS
|
|
19
42
|
};
|
|
20
43
|
/**
|
|
21
44
|
* A hand-edited config can hold anything; keep only string entries so a stray
|
|
@@ -43,6 +66,7 @@ if (!G.loaded) {
|
|
|
43
66
|
if (!isSearchProvider(parsed.searchProvider))
|
|
44
67
|
delete parsed.searchProvider;
|
|
45
68
|
parsed.extensionWhitelist = sanitizeExtensionWhitelist(parsed.extensionWhitelist);
|
|
69
|
+
parsed.requestTimeoutMs = sanitizeRequestTimeoutMs(parsed.requestTimeoutMs);
|
|
46
70
|
G.config = { ...DEFAULTS, ...parsed };
|
|
47
71
|
}
|
|
48
72
|
catch {
|
package/dist/config/register.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SettingsList, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import { registerBridgeCommand } from '../remote/bridge.js';
|
|
3
3
|
import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
|
|
4
|
-
import { getConfig, saveConfig } from './config.js';
|
|
4
|
+
import { COMMAND_TIMEOUT_OPTIONS, getConfig, saveConfig } from './config.js';
|
|
5
5
|
import { listInstalledExtensions } from './extension-list.js';
|
|
6
6
|
const CONFIG_TITLE = 'pi-task settings';
|
|
7
7
|
/**
|
|
@@ -95,12 +95,28 @@ const ITEMS = [
|
|
|
95
95
|
description: 'Engine behind web search (pi-worker-search + freshness checks). Exa and DuckDuckGo need no API key; Brave needs BRAVE_SEARCH_API_KEY',
|
|
96
96
|
// Display full engine names; the stored config value stays the short id.
|
|
97
97
|
values: SEARCH_PROVIDERS.map(p => SEARCH_PROVIDER_LABELS[p])
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'requestTimeoutMs',
|
|
101
|
+
label: 'command timeout',
|
|
102
|
+
description: 'Cancel a single command that runs longer than this and remind the model to set '
|
|
103
|
+
+ 'its own timeout. Catches a local model that runs a command which never returns '
|
|
104
|
+
+ '(hung build, dev server, no-timeout check) so the run stops itself instead of '
|
|
105
|
+
+ 'waiting for a manual abort. off disables it',
|
|
106
|
+
// Display human labels; the stored config value stays the ms number.
|
|
107
|
+
values: COMMAND_TIMEOUT_OPTIONS.map(o => o.label)
|
|
98
108
|
}
|
|
99
109
|
];
|
|
110
|
+
/** Human label for the stored command-timeout ms (falls back to the raw ms). */
|
|
111
|
+
function timeoutLabel(ms) {
|
|
112
|
+
return COMMAND_TIMEOUT_OPTIONS.find(o => o.ms === ms)?.label ?? `${ms}ms`;
|
|
113
|
+
}
|
|
100
114
|
/** What /task-config shows for a setting's current value. */
|
|
101
115
|
function displayValue(cfg, id, isEnum) {
|
|
102
116
|
if (id === 'searchProvider')
|
|
103
117
|
return SEARCH_PROVIDER_LABELS[cfg.searchProvider];
|
|
118
|
+
if (id === 'requestTimeoutMs')
|
|
119
|
+
return timeoutLabel(cfg.requestTimeoutMs);
|
|
104
120
|
if (isEnum)
|
|
105
121
|
return String(cfg[id]);
|
|
106
122
|
return cfg[id] ? 'on' : 'off';
|
|
@@ -173,6 +189,11 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
173
189
|
if (provider)
|
|
174
190
|
cfg.searchProvider = provider;
|
|
175
191
|
}
|
|
192
|
+
else if (id === 'requestTimeoutMs') {
|
|
193
|
+
const opt = COMMAND_TIMEOUT_OPTIONS.find(o => o.label === newValue);
|
|
194
|
+
if (opt)
|
|
195
|
+
cfg.requestTimeoutMs = opt.ms;
|
|
196
|
+
}
|
|
176
197
|
else {
|
|
177
198
|
;
|
|
178
199
|
cfg[id] = newValue === 'on';
|
|
@@ -185,7 +206,7 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
185
206
|
export function registerConfig(pi) {
|
|
186
207
|
registerBridgeCommand(pi, 'task-config', {
|
|
187
208
|
description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation, '
|
|
188
|
-
+ 'enforce guidelines, extension whitelist for child sessions).',
|
|
209
|
+
+ 'enforce guidelines, command timeout, extension whitelist for child sessions).',
|
|
189
210
|
handler: handleTaskConfig
|
|
190
211
|
});
|
|
191
212
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { registerTaskAuto } from './task/auto-orchestrator.js';
|
|
|
4
4
|
import { registerWorkers } from './workers/index.js';
|
|
5
5
|
import { registerRemote } from './remote/register.js';
|
|
6
6
|
import { registerThinkingCompression } from './thinking/compress.js';
|
|
7
|
+
import { registerCommandWatchdog } from './task/command-watchdog.js';
|
|
7
8
|
export default function (pi) {
|
|
8
9
|
registerConfig(pi);
|
|
9
10
|
registerTask(pi);
|
|
@@ -11,4 +12,5 @@ export default function (pi) {
|
|
|
11
12
|
registerWorkers(pi);
|
|
12
13
|
registerRemote(pi);
|
|
13
14
|
registerThinkingCompression(pi);
|
|
15
|
+
registerCommandWatchdog(pi);
|
|
14
16
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
/**
|
|
3
|
+
* Command watchdog — cancels a single tool execution that overruns the
|
|
4
|
+
* configured ceiling and reminds the model to bound its own commands.
|
|
5
|
+
*
|
|
6
|
+
* WHY: a local model in the MAIN session routinely runs a command that never
|
|
7
|
+
* returns — `godot --headless --check-only` with no timeout, a dev server, a
|
|
8
|
+
* hung test — and the run wedges until the user manually aborts and tells the
|
|
9
|
+
* model to add a timeout. pi's bash tool takes an OPTIONAL `timeout` with NO
|
|
10
|
+
* default (see pi-coding-agent tools/bash.js), so any command the model didn't
|
|
11
|
+
* bound runs forever. This supplies the missing default from the host side.
|
|
12
|
+
*
|
|
13
|
+
* HOW: arm a wall-clock timer on `tool_execution_start`, disarm it on
|
|
14
|
+
* `tool_execution_end`. If it elapses, `ctx.abort()` cancels the in-flight
|
|
15
|
+
* operation — which fires the tool's AbortSignal, and pi's bash executor kills
|
|
16
|
+
* the whole process tree on abort — then a follow-up user turn tells the model
|
|
17
|
+
* what happened so it retries with a timeout instead of hanging again.
|
|
18
|
+
*
|
|
19
|
+
* Tool-agnostic: it arms on ANY tool, honouring "any command can run forever",
|
|
20
|
+
* though in practice only bash runs long enough to trip it. The pure timer
|
|
21
|
+
* state lives in {@link CommandWatchdog}; all side effects (abort, reminder,
|
|
22
|
+
* per-call ctx lookup) live in the registration's `onFire`, so the machine is
|
|
23
|
+
* unit-testable without a real pi session.
|
|
24
|
+
*/
|
|
25
|
+
/** Opaque timer handle — a real `setTimeout` return in production, anything the
|
|
26
|
+
* test's fake scheduler hands back under test. */
|
|
27
|
+
export type TimerHandle = unknown;
|
|
28
|
+
export interface WatchdogDeps {
|
|
29
|
+
/**
|
|
30
|
+
* The ceiling in ms, read PER command-start so a /task-config change takes
|
|
31
|
+
* effect on the next command with no reload. 0 (or any non-positive value)
|
|
32
|
+
* means the watchdog is off and never arms.
|
|
33
|
+
*/
|
|
34
|
+
getTimeoutMs: () => number;
|
|
35
|
+
schedule: (fn: () => void, ms: number) => TimerHandle;
|
|
36
|
+
cancel: (handle: TimerHandle) => void;
|
|
37
|
+
/** Invoked when a command overruns: the registration aborts + reminds here. */
|
|
38
|
+
onFire: (toolCallId: string, toolName: string, timeoutMs: number) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The reminder delivered to the model after its command is cancelled. Kept pure
|
|
42
|
+
* and exported so a test can assert its shape without driving the whole session.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reminderMessage(toolName: string, timeoutMs: number): string;
|
|
45
|
+
export declare class CommandWatchdog {
|
|
46
|
+
private readonly deps;
|
|
47
|
+
/** Armed timers, keyed by the tool call they guard. Tool executions are
|
|
48
|
+
* sequential, so this holds at most one entry in normal operation, but the
|
|
49
|
+
* map keeps it correct even if pi ever overlaps two calls. */
|
|
50
|
+
private readonly active;
|
|
51
|
+
constructor(deps: WatchdogDeps);
|
|
52
|
+
/** Arm a timer for a starting tool. No-op when the watchdog is off. */
|
|
53
|
+
onStart(toolCallId: string, toolName: string): void;
|
|
54
|
+
/** Disarm the timer for a finished tool. */
|
|
55
|
+
onEnd(toolCallId: string): void;
|
|
56
|
+
/** Cancel every armed timer — a turn-end / session-shutdown safety net so no
|
|
57
|
+
* stray timer can fire into a later, unrelated command. */
|
|
58
|
+
clearAll(): void;
|
|
59
|
+
private disarm;
|
|
60
|
+
private fire;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Wire the watchdog into the main session. Only ever active in the host session
|
|
64
|
+
* (children run `--no-extensions`), which is exactly where the observed hangs
|
|
65
|
+
* happen.
|
|
66
|
+
*/
|
|
67
|
+
export declare function registerCommandWatchdog(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getConfig } from '../config/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The reminder delivered to the model after its command is cancelled. Kept pure
|
|
4
|
+
* and exported so a test can assert its shape without driving the whole session.
|
|
5
|
+
*/
|
|
6
|
+
export function reminderMessage(toolName, timeoutMs) {
|
|
7
|
+
const mins = Math.max(1, Math.round(timeoutMs / 60_000));
|
|
8
|
+
return (`[SYSTEM] Your \`${toolName}\` call ran longer than ${mins} minute`
|
|
9
|
+
+ `${mins === 1 ? '' : 's'} and was automatically cancelled — it looked stuck. `
|
|
10
|
+
// Anti-fabrication: a live run showed the model react to the cancel by
|
|
11
|
+
// reporting the killed command as succeeded ("the server is now running").
|
|
12
|
+
// State plainly that it produced nothing so the model can't claim success.
|
|
13
|
+
+ `The command was killed before it finished and produced NO result, so do not `
|
|
14
|
+
+ `report it as completed or successful, and do not claim that anything it would `
|
|
15
|
+
+ `have started (a server, build, or process) is now running. `
|
|
16
|
+
+ `If it was a genuinely long-running command, you MUST re-run it with an explicit `
|
|
17
|
+
+ `timeout — set the bash tool's \`timeout\` parameter (in seconds) so it cannot hang `
|
|
18
|
+
+ `again — or break it into smaller steps. Do NOT simply retry the same unbounded command.`);
|
|
19
|
+
}
|
|
20
|
+
export class CommandWatchdog {
|
|
21
|
+
deps;
|
|
22
|
+
/** Armed timers, keyed by the tool call they guard. Tool executions are
|
|
23
|
+
* sequential, so this holds at most one entry in normal operation, but the
|
|
24
|
+
* map keeps it correct even if pi ever overlaps two calls. */
|
|
25
|
+
active = new Map();
|
|
26
|
+
constructor(deps) {
|
|
27
|
+
this.deps = deps;
|
|
28
|
+
}
|
|
29
|
+
/** Arm a timer for a starting tool. No-op when the watchdog is off. */
|
|
30
|
+
onStart(toolCallId, toolName) {
|
|
31
|
+
const ms = this.deps.getTimeoutMs();
|
|
32
|
+
if (!(ms > 0))
|
|
33
|
+
return;
|
|
34
|
+
// A duplicate start for the same id must not leak the previous timer.
|
|
35
|
+
this.disarm(toolCallId);
|
|
36
|
+
const handle = this.deps.schedule(() => this.fire(toolCallId, toolName, ms), ms);
|
|
37
|
+
this.active.set(toolCallId, handle);
|
|
38
|
+
}
|
|
39
|
+
/** Disarm the timer for a finished tool. */
|
|
40
|
+
onEnd(toolCallId) {
|
|
41
|
+
this.disarm(toolCallId);
|
|
42
|
+
}
|
|
43
|
+
/** Cancel every armed timer — a turn-end / session-shutdown safety net so no
|
|
44
|
+
* stray timer can fire into a later, unrelated command. */
|
|
45
|
+
clearAll() {
|
|
46
|
+
for (const handle of this.active.values())
|
|
47
|
+
this.deps.cancel(handle);
|
|
48
|
+
this.active.clear();
|
|
49
|
+
}
|
|
50
|
+
disarm(toolCallId) {
|
|
51
|
+
const handle = this.active.get(toolCallId);
|
|
52
|
+
if (handle !== undefined) {
|
|
53
|
+
this.deps.cancel(handle);
|
|
54
|
+
this.active.delete(toolCallId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
fire(toolCallId, toolName, ms) {
|
|
58
|
+
// If the tool ended in the same tick the timer fired, its entry is gone
|
|
59
|
+
// already — never abort a command that has just finished cleanly.
|
|
60
|
+
if (!this.active.has(toolCallId))
|
|
61
|
+
return;
|
|
62
|
+
this.active.delete(toolCallId);
|
|
63
|
+
this.deps.onFire(toolCallId, toolName, ms);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wire the watchdog into the main session. Only ever active in the host session
|
|
68
|
+
* (children run `--no-extensions`), which is exactly where the observed hangs
|
|
69
|
+
* happen.
|
|
70
|
+
*/
|
|
71
|
+
export function registerCommandWatchdog(pi) {
|
|
72
|
+
// The ctx that owns each in-flight tool's AbortSignal, captured per start so
|
|
73
|
+
// the timer callback (which fires outside the event handler) aborts the
|
|
74
|
+
// right operation.
|
|
75
|
+
const ctxByCall = new Map();
|
|
76
|
+
const watchdog = new CommandWatchdog({
|
|
77
|
+
getTimeoutMs: () => getConfig().requestTimeoutMs,
|
|
78
|
+
schedule: (fn, ms) => {
|
|
79
|
+
const handle = setTimeout(fn, ms);
|
|
80
|
+
// Don't let a pending watchdog timer keep the process alive on exit.
|
|
81
|
+
if (typeof handle.unref === 'function') {
|
|
82
|
+
;
|
|
83
|
+
handle.unref();
|
|
84
|
+
}
|
|
85
|
+
return handle;
|
|
86
|
+
},
|
|
87
|
+
cancel: handle => clearTimeout(handle),
|
|
88
|
+
onFire: (toolCallId, toolName, timeoutMs) => {
|
|
89
|
+
const ctx = ctxByCall.get(toolCallId);
|
|
90
|
+
ctxByCall.delete(toolCallId);
|
|
91
|
+
// Cancel the stuck command (kills the tool's whole process tree via
|
|
92
|
+
// the turn's AbortSignal), then start a fresh turn telling the model
|
|
93
|
+
// to bound its next attempt.
|
|
94
|
+
ctx?.abort();
|
|
95
|
+
pi.sendUserMessage(reminderMessage(toolName, timeoutMs), { deliverAs: 'followUp' });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
pi.on('tool_execution_start', (event, ctx) => {
|
|
99
|
+
ctxByCall.set(event.toolCallId, ctx);
|
|
100
|
+
watchdog.onStart(event.toolCallId, event.toolName);
|
|
101
|
+
});
|
|
102
|
+
pi.on('tool_execution_end', event => {
|
|
103
|
+
ctxByCall.delete(event.toolCallId);
|
|
104
|
+
watchdog.onEnd(event.toolCallId);
|
|
105
|
+
});
|
|
106
|
+
// Safety net: nothing should outlive its turn, but if a start ever lacks a
|
|
107
|
+
// matching end, clear on turn/session teardown so no timer fires stale.
|
|
108
|
+
const reset = () => {
|
|
109
|
+
watchdog.clearAll();
|
|
110
|
+
ctxByCall.clear();
|
|
111
|
+
};
|
|
112
|
+
pi.on('turn_end', reset);
|
|
113
|
+
pi.on('session_shutdown', reset);
|
|
114
|
+
}
|
package/dist/task/final-gate.js
CHANGED
|
@@ -383,6 +383,12 @@ export function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}) {
|
|
|
383
383
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
384
384
|
env: { ...process.env }
|
|
385
385
|
});
|
|
386
|
+
// Best-effort cleanup only: killGroup below can silently fail to reap the
|
|
387
|
+
// process (platform/sandbox-specific — observed on a GH Actions Linux
|
|
388
|
+
// runner where the group-kill did not take, hanging the whole `bun test
|
|
389
|
+
// --isolate` run on the leaked child's piped stdio). unref() so a child
|
|
390
|
+
// we already tried to kill can never itself keep this process alive.
|
|
391
|
+
child.unref();
|
|
386
392
|
let out = '';
|
|
387
393
|
let err = '';
|
|
388
394
|
let listenerSeen = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.26",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|