micode 0.6.0 → 0.7.0
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/README.md +64 -331
- package/dist/hooks/auto-clear-ledger.d.ts +1 -1
- package/dist/index.js +840 -18
- package/dist/tools/pty/buffer.d.ts +11 -0
- package/dist/tools/pty/index.d.ts +74 -0
- package/dist/tools/pty/manager.d.ts +14 -0
- package/dist/tools/pty/tools/kill.d.ts +12 -0
- package/dist/tools/pty/tools/list.d.ts +6 -0
- package/dist/tools/pty/tools/read.d.ts +18 -0
- package/dist/tools/pty/tools/spawn.d.ts +20 -0
- package/dist/tools/pty/tools/write.d.ts +12 -0
- package/dist/tools/pty/types.d.ts +54 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ var __export = (target, all) => {
|
|
|
9
9
|
set: (newValue) => all[name] = () => newValue
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
|
+
var __require = import.meta.require;
|
|
12
13
|
|
|
13
14
|
// src/agents/brainstormer.ts
|
|
14
15
|
var brainstormerAgent = {
|
|
@@ -63,10 +64,10 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
63
64
|
background_task(agent="pattern-finder", prompt="Find patterns for [similar functionality]", description="Find patterns")
|
|
64
65
|
</fire-example>
|
|
65
66
|
<poll>
|
|
66
|
-
background_list() // repeat until all show "completed"
|
|
67
|
+
background_list() // repeat until all show "completed" or "error"
|
|
67
68
|
</poll>
|
|
68
69
|
<collect>
|
|
69
|
-
background_output(task_id=...) for each completed task
|
|
70
|
+
background_output(task_id=...) for each completed task (skip errored tasks)
|
|
70
71
|
</collect>
|
|
71
72
|
<focus>purpose, constraints, success criteria</focus>
|
|
72
73
|
</phase>
|
|
@@ -454,8 +455,8 @@ All research must serve the design - never second-guess design decisions.
|
|
|
454
455
|
- btca_ask for library internals when needed
|
|
455
456
|
</fire-phase>
|
|
456
457
|
<collect-phase description="Poll until all complete, then collect">
|
|
457
|
-
- Poll with background_list until all tasks show completed
|
|
458
|
-
- Call background_output(task_id=...) for each completed task
|
|
458
|
+
- Poll with background_list until all tasks show completed or error
|
|
459
|
+
- Call background_output(task_id=...) for each completed task (skip errored)
|
|
459
460
|
- Combine all results for planning phase
|
|
460
461
|
</collect-phase>
|
|
461
462
|
<rule>Only research what's needed to implement the design</rule>
|
|
@@ -627,6 +628,13 @@ Execute the plan. Write code. Verify.
|
|
|
627
628
|
<step>Report results</step>
|
|
628
629
|
</process>
|
|
629
630
|
|
|
631
|
+
<terminal-tools>
|
|
632
|
+
<bash>Use for synchronous commands that complete (npm install, git, builds)</bash>
|
|
633
|
+
<pty>Use for background processes (dev servers, watch modes, REPLs)</pty>
|
|
634
|
+
<rule>If plan says "start dev server" or "run in background", use pty_spawn</rule>
|
|
635
|
+
<rule>If plan says "run command" or "install", use bash</rule>
|
|
636
|
+
</terminal-tools>
|
|
637
|
+
|
|
630
638
|
<before-each-change>
|
|
631
639
|
<check>Verify file exists where expected</check>
|
|
632
640
|
<check>Verify code structure matches plan assumptions</check>
|
|
@@ -753,6 +761,12 @@ Check correctness and style. Be specific. Run code, don't just read.
|
|
|
753
761
|
<step>Report with precise references</step>
|
|
754
762
|
</process>
|
|
755
763
|
|
|
764
|
+
<terminal-verification>
|
|
765
|
+
<rule>If implementation includes PTY usage, verify sessions are properly cleaned up</rule>
|
|
766
|
+
<rule>If tests require a running server, check that pty_spawn was used appropriately</rule>
|
|
767
|
+
<rule>Check that long-running processes use PTY, not blocking bash</rule>
|
|
768
|
+
</terminal-verification>
|
|
769
|
+
|
|
756
770
|
<output-format>
|
|
757
771
|
<template>
|
|
758
772
|
## Review: [Component]
|
|
@@ -802,6 +816,24 @@ You have access to background task management tools:
|
|
|
802
816
|
- background_list: List all background tasks and their status
|
|
803
817
|
</background-tools>
|
|
804
818
|
|
|
819
|
+
<pty-tools description="For background bash processes">
|
|
820
|
+
PTY tools manage background terminal sessions (different from background_task which runs subagents):
|
|
821
|
+
- pty_spawn: Start a background process (dev server, watch mode, REPL)
|
|
822
|
+
- pty_write: Send input to a PTY (commands, Ctrl+C, etc.)
|
|
823
|
+
- pty_read: Read output from a PTY buffer
|
|
824
|
+
- pty_list: List all PTY sessions
|
|
825
|
+
- pty_kill: Terminate a PTY session
|
|
826
|
+
|
|
827
|
+
Use PTY when:
|
|
828
|
+
- Plan requires starting a dev server before running tests
|
|
829
|
+
- Plan requires a watch mode process running during implementation
|
|
830
|
+
- Plan requires interactive terminal input
|
|
831
|
+
|
|
832
|
+
Do NOT use PTY for:
|
|
833
|
+
- Quick commands (use bash)
|
|
834
|
+
- Subagent tasks (use background_task)
|
|
835
|
+
</pty-tools>
|
|
836
|
+
|
|
805
837
|
<workflow pattern="fire-and-check">
|
|
806
838
|
<step>Parse plan to extract individual tasks</step>
|
|
807
839
|
<step>Analyze task dependencies to build execution graph</step>
|
|
@@ -1052,7 +1084,7 @@ Just do it - including obvious follow-up actions.
|
|
|
1052
1084
|
</phase>
|
|
1053
1085
|
|
|
1054
1086
|
<phase name="ledger" trigger="context getting full or session ending">
|
|
1055
|
-
<action>System auto-updates ledger at
|
|
1087
|
+
<action>System auto-updates ledger at 60% context usage</action>
|
|
1056
1088
|
<output>thoughts/ledgers/CONTINUITY_{session-name}.md</output>
|
|
1057
1089
|
</phase>
|
|
1058
1090
|
</workflow>
|
|
@@ -1080,6 +1112,23 @@ Just do it - including obvious follow-up actions.
|
|
|
1080
1112
|
</when-to-use>
|
|
1081
1113
|
</library-research>
|
|
1082
1114
|
|
|
1115
|
+
<terminal-tools description="Choose the right terminal tool">
|
|
1116
|
+
<tool name="bash">Synchronous commands. Use for: npm install, git, builds, quick commands that complete.</tool>
|
|
1117
|
+
<tool name="pty_spawn">Background PTY sessions. Use for: dev servers, watch modes, REPLs, long-running processes.</tool>
|
|
1118
|
+
<when-to-use>
|
|
1119
|
+
<use tool="bash">Command completes quickly (npm install, git status, mkdir)</use>
|
|
1120
|
+
<use tool="pty_spawn">Process runs indefinitely (npm run dev, pytest --watch, python REPL)</use>
|
|
1121
|
+
<use tool="pty_spawn">Need to send interactive input (Ctrl+C, responding to prompts)</use>
|
|
1122
|
+
<use tool="pty_spawn">Want to check output later without blocking</use>
|
|
1123
|
+
</when-to-use>
|
|
1124
|
+
<pty-workflow>
|
|
1125
|
+
<step>pty_spawn to start the process</step>
|
|
1126
|
+
<step>pty_read to check output (use pattern to filter)</step>
|
|
1127
|
+
<step>pty_write to send input (\\n for Enter, \\x03 for Ctrl+C)</step>
|
|
1128
|
+
<step>pty_kill when done (cleanup=true to remove)</step>
|
|
1129
|
+
</pty-workflow>
|
|
1130
|
+
</terminal-tools>
|
|
1131
|
+
|
|
1083
1132
|
<tracking>
|
|
1084
1133
|
<rule>Use TodoWrite to track what you're doing</rule>
|
|
1085
1134
|
<rule>Never discard tasks without explicit approval</rule>
|
|
@@ -1160,8 +1209,8 @@ var PROMPT2 = `
|
|
|
1160
1209
|
|
|
1161
1210
|
<phase name="2-collect" description="Poll and collect all results">
|
|
1162
1211
|
<description>Poll background_list until all tasks complete, then collect with background_output</description>
|
|
1163
|
-
<action>Poll background_list until all tasks show "completed"</action>
|
|
1164
|
-
<action>Call background_output for each completed task</action>
|
|
1212
|
+
<action>Poll background_list until all tasks show "completed" or "error"</action>
|
|
1213
|
+
<action>Call background_output for each completed task (skip errored)</action>
|
|
1165
1214
|
<action>Process tool results from phase 1</action>
|
|
1166
1215
|
</phase>
|
|
1167
1216
|
|
|
@@ -1329,8 +1378,8 @@ var PROMPT2 = `
|
|
|
1329
1378
|
|
|
1330
1379
|
<step description="COLLECT: Poll and gather all results">
|
|
1331
1380
|
First poll until all tasks complete:
|
|
1332
|
-
- background_list() // repeat until all show "completed"
|
|
1333
|
-
Then collect
|
|
1381
|
+
- background_list() // repeat until all show "completed" or "error"
|
|
1382
|
+
Then collect results (skip errored tasks):
|
|
1334
1383
|
- background_output(task_id=task_id_1)
|
|
1335
1384
|
- background_output(task_id=task_id_2)
|
|
1336
1385
|
- background_output(task_id=task_id_3)
|
|
@@ -15411,7 +15460,7 @@ function createFileOpsTrackerHook(_ctx) {
|
|
|
15411
15460
|
}
|
|
15412
15461
|
|
|
15413
15462
|
// src/hooks/auto-clear-ledger.ts
|
|
15414
|
-
var DEFAULT_THRESHOLD = 0.
|
|
15463
|
+
var DEFAULT_THRESHOLD = 0.6;
|
|
15415
15464
|
var MIN_TOKENS_FOR_CLEAR = 50000;
|
|
15416
15465
|
var CLEAR_COOLDOWN_MS = 60000;
|
|
15417
15466
|
function createAutoClearLedgerHook(ctx) {
|
|
@@ -16063,20 +16112,789 @@ ${result}
|
|
|
16063
16112
|
background_list
|
|
16064
16113
|
};
|
|
16065
16114
|
}
|
|
16115
|
+
// node_modules/bun-pty/src/terminal.ts
|
|
16116
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
16117
|
+
import { Buffer } from "buffer";
|
|
16118
|
+
|
|
16119
|
+
// node_modules/bun-pty/src/interfaces.ts
|
|
16120
|
+
class EventEmitter {
|
|
16121
|
+
listeners = [];
|
|
16122
|
+
event = (listener) => {
|
|
16123
|
+
this.listeners.push(listener);
|
|
16124
|
+
return {
|
|
16125
|
+
dispose: () => {
|
|
16126
|
+
const i = this.listeners.indexOf(listener);
|
|
16127
|
+
if (i !== -1) {
|
|
16128
|
+
this.listeners.splice(i, 1);
|
|
16129
|
+
}
|
|
16130
|
+
}
|
|
16131
|
+
};
|
|
16132
|
+
};
|
|
16133
|
+
fire(data) {
|
|
16134
|
+
for (const listener of this.listeners) {
|
|
16135
|
+
listener(data);
|
|
16136
|
+
}
|
|
16137
|
+
}
|
|
16138
|
+
}
|
|
16139
|
+
|
|
16140
|
+
// node_modules/bun-pty/src/terminal.ts
|
|
16141
|
+
import { join as join4, dirname as dirname3, basename as basename2 } from "path";
|
|
16142
|
+
import { existsSync as existsSync2 } from "fs";
|
|
16143
|
+
var DEFAULT_COLS = 80;
|
|
16144
|
+
var DEFAULT_ROWS = 24;
|
|
16145
|
+
var DEFAULT_FILE = "sh";
|
|
16146
|
+
var DEFAULT_NAME = "xterm";
|
|
16147
|
+
function shQuote(s) {
|
|
16148
|
+
if (s.length === 0)
|
|
16149
|
+
return "''";
|
|
16150
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
16151
|
+
}
|
|
16152
|
+
function resolveLibPath() {
|
|
16153
|
+
const env = process.env.BUN_PTY_LIB;
|
|
16154
|
+
if (env && existsSync2(env))
|
|
16155
|
+
return env;
|
|
16156
|
+
try {
|
|
16157
|
+
const embeddedPath = __require(`../rust-pty/target/release/${process.platform === "win32" ? "rust_pty.dll" : process.platform === "darwin" ? process.arch === "arm64" ? "librust_pty_arm64.dylib" : "librust_pty.dylib" : process.arch === "arm64" ? "librust_pty_arm64.so" : "librust_pty.so"}`);
|
|
16158
|
+
if (embeddedPath)
|
|
16159
|
+
return embeddedPath;
|
|
16160
|
+
} catch {}
|
|
16161
|
+
const platform = process.platform;
|
|
16162
|
+
const arch = process.arch;
|
|
16163
|
+
const filenames = platform === "darwin" ? arch === "arm64" ? ["librust_pty_arm64.dylib", "librust_pty.dylib"] : ["librust_pty.dylib"] : platform === "win32" ? ["rust_pty.dll"] : arch === "arm64" ? ["librust_pty_arm64.so", "librust_pty.so"] : ["librust_pty.so"];
|
|
16164
|
+
const base = Bun.fileURLToPath(import.meta.url);
|
|
16165
|
+
const fileDir = dirname3(base);
|
|
16166
|
+
const dirName = basename2(fileDir);
|
|
16167
|
+
const here = dirName === "src" || dirName === "dist" ? dirname3(fileDir) : fileDir;
|
|
16168
|
+
const basePaths = [
|
|
16169
|
+
join4(here, "rust-pty", "target", "release"),
|
|
16170
|
+
join4(here, "..", "bun-pty", "rust-pty", "target", "release"),
|
|
16171
|
+
join4(process.cwd(), "node_modules", "bun-pty", "rust-pty", "target", "release")
|
|
16172
|
+
];
|
|
16173
|
+
const fallbackPaths = [];
|
|
16174
|
+
for (const basePath of basePaths) {
|
|
16175
|
+
for (const filename of filenames) {
|
|
16176
|
+
fallbackPaths.push(join4(basePath, filename));
|
|
16177
|
+
}
|
|
16178
|
+
}
|
|
16179
|
+
for (const path of fallbackPaths) {
|
|
16180
|
+
if (existsSync2(path))
|
|
16181
|
+
return path;
|
|
16182
|
+
}
|
|
16183
|
+
throw new Error(`librust_pty shared library not found.
|
|
16184
|
+
Checked:
|
|
16185
|
+
- BUN_PTY_LIB=${env ?? "<unset>"}
|
|
16186
|
+
- ${fallbackPaths.join(`
|
|
16187
|
+
- `)}
|
|
16188
|
+
|
|
16189
|
+
Set BUN_PTY_LIB or ensure one of these paths contains the file.`);
|
|
16190
|
+
}
|
|
16191
|
+
var libPath = resolveLibPath();
|
|
16192
|
+
var lib;
|
|
16193
|
+
try {
|
|
16194
|
+
lib = dlopen(libPath, {
|
|
16195
|
+
bun_pty_spawn: {
|
|
16196
|
+
args: [FFIType.cstring, FFIType.cstring, FFIType.cstring, FFIType.i32, FFIType.i32],
|
|
16197
|
+
returns: FFIType.i32
|
|
16198
|
+
},
|
|
16199
|
+
bun_pty_write: {
|
|
16200
|
+
args: [FFIType.i32, FFIType.pointer, FFIType.i32],
|
|
16201
|
+
returns: FFIType.i32
|
|
16202
|
+
},
|
|
16203
|
+
bun_pty_read: {
|
|
16204
|
+
args: [FFIType.i32, FFIType.pointer, FFIType.i32],
|
|
16205
|
+
returns: FFIType.i32
|
|
16206
|
+
},
|
|
16207
|
+
bun_pty_resize: {
|
|
16208
|
+
args: [FFIType.i32, FFIType.i32, FFIType.i32],
|
|
16209
|
+
returns: FFIType.i32
|
|
16210
|
+
},
|
|
16211
|
+
bun_pty_kill: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
16212
|
+
bun_pty_get_pid: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
16213
|
+
bun_pty_get_exit_code: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
16214
|
+
bun_pty_close: { args: [FFIType.i32], returns: FFIType.void }
|
|
16215
|
+
});
|
|
16216
|
+
} catch (error45) {
|
|
16217
|
+
console.error("Failed to load lib", error45);
|
|
16218
|
+
}
|
|
16219
|
+
|
|
16220
|
+
class Terminal {
|
|
16221
|
+
handle = -1;
|
|
16222
|
+
_pid = -1;
|
|
16223
|
+
_cols = DEFAULT_COLS;
|
|
16224
|
+
_rows = DEFAULT_ROWS;
|
|
16225
|
+
_name = DEFAULT_NAME;
|
|
16226
|
+
_readLoop = false;
|
|
16227
|
+
_closing = false;
|
|
16228
|
+
_onData = new EventEmitter;
|
|
16229
|
+
_onExit = new EventEmitter;
|
|
16230
|
+
constructor(file2 = DEFAULT_FILE, args = [], opts = { name: DEFAULT_NAME }) {
|
|
16231
|
+
this._cols = opts.cols ?? DEFAULT_COLS;
|
|
16232
|
+
this._rows = opts.rows ?? DEFAULT_ROWS;
|
|
16233
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
16234
|
+
const cmdline = [shQuote(file2), ...args.map(shQuote)].join(" ");
|
|
16235
|
+
let envStr = "";
|
|
16236
|
+
if (opts.env) {
|
|
16237
|
+
const envPairs = Object.entries(opts.env).map(([k, v]) => `${k}=${v}`);
|
|
16238
|
+
envStr = envPairs.join("\x00") + "\x00";
|
|
16239
|
+
}
|
|
16240
|
+
this.handle = lib.symbols.bun_pty_spawn(Buffer.from(`${cmdline}\x00`, "utf8"), Buffer.from(`${cwd}\x00`, "utf8"), Buffer.from(`${envStr}\x00`, "utf8"), this._cols, this._rows);
|
|
16241
|
+
if (this.handle < 0)
|
|
16242
|
+
throw new Error("PTY spawn failed");
|
|
16243
|
+
this._pid = lib.symbols.bun_pty_get_pid(this.handle);
|
|
16244
|
+
this._startReadLoop();
|
|
16245
|
+
}
|
|
16246
|
+
get pid() {
|
|
16247
|
+
return this._pid;
|
|
16248
|
+
}
|
|
16249
|
+
get cols() {
|
|
16250
|
+
return this._cols;
|
|
16251
|
+
}
|
|
16252
|
+
get rows() {
|
|
16253
|
+
return this._rows;
|
|
16254
|
+
}
|
|
16255
|
+
get process() {
|
|
16256
|
+
return "shell";
|
|
16257
|
+
}
|
|
16258
|
+
get onData() {
|
|
16259
|
+
return this._onData.event;
|
|
16260
|
+
}
|
|
16261
|
+
get onExit() {
|
|
16262
|
+
return this._onExit.event;
|
|
16263
|
+
}
|
|
16264
|
+
write(data) {
|
|
16265
|
+
if (this._closing)
|
|
16266
|
+
return;
|
|
16267
|
+
const buf = Buffer.from(data, "utf8");
|
|
16268
|
+
lib.symbols.bun_pty_write(this.handle, ptr(buf), buf.length);
|
|
16269
|
+
}
|
|
16270
|
+
resize(cols, rows) {
|
|
16271
|
+
if (this._closing)
|
|
16272
|
+
return;
|
|
16273
|
+
this._cols = cols;
|
|
16274
|
+
this._rows = rows;
|
|
16275
|
+
lib.symbols.bun_pty_resize(this.handle, cols, rows);
|
|
16276
|
+
}
|
|
16277
|
+
kill(signal = "SIGTERM") {
|
|
16278
|
+
if (this._closing)
|
|
16279
|
+
return;
|
|
16280
|
+
this._closing = true;
|
|
16281
|
+
lib.symbols.bun_pty_kill(this.handle);
|
|
16282
|
+
lib.symbols.bun_pty_close(this.handle);
|
|
16283
|
+
this._onExit.fire({ exitCode: 0, signal });
|
|
16284
|
+
}
|
|
16285
|
+
async _startReadLoop() {
|
|
16286
|
+
if (this._readLoop)
|
|
16287
|
+
return;
|
|
16288
|
+
this._readLoop = true;
|
|
16289
|
+
const buf = Buffer.allocUnsafe(4096);
|
|
16290
|
+
while (this._readLoop && !this._closing) {
|
|
16291
|
+
const n = lib.symbols.bun_pty_read(this.handle, ptr(buf), buf.length);
|
|
16292
|
+
if (n > 0) {
|
|
16293
|
+
this._onData.fire(buf.subarray(0, n).toString("utf8"));
|
|
16294
|
+
} else if (n === -2) {
|
|
16295
|
+
const exitCode = lib.symbols.bun_pty_get_exit_code(this.handle);
|
|
16296
|
+
this._onExit.fire({ exitCode });
|
|
16297
|
+
break;
|
|
16298
|
+
} else if (n < 0) {
|
|
16299
|
+
break;
|
|
16300
|
+
} else {
|
|
16301
|
+
await new Promise((r) => setTimeout(r, 8));
|
|
16302
|
+
}
|
|
16303
|
+
}
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
16306
|
+
|
|
16307
|
+
// node_modules/bun-pty/src/index.ts
|
|
16308
|
+
function spawn3(file2, args, options) {
|
|
16309
|
+
return new Terminal(file2, args, options);
|
|
16310
|
+
}
|
|
16311
|
+
|
|
16312
|
+
// src/tools/pty/buffer.ts
|
|
16313
|
+
var parsed = parseInt(process.env.PTY_MAX_BUFFER_LINES || "50000", 10);
|
|
16314
|
+
var DEFAULT_MAX_LINES = isNaN(parsed) ? 50000 : parsed;
|
|
16315
|
+
|
|
16316
|
+
class RingBuffer {
|
|
16317
|
+
lines = [];
|
|
16318
|
+
maxLines;
|
|
16319
|
+
constructor(maxLines = DEFAULT_MAX_LINES) {
|
|
16320
|
+
this.maxLines = maxLines;
|
|
16321
|
+
}
|
|
16322
|
+
append(data) {
|
|
16323
|
+
const newLines = data.split(`
|
|
16324
|
+
`);
|
|
16325
|
+
for (const line of newLines) {
|
|
16326
|
+
this.lines.push(line);
|
|
16327
|
+
if (this.lines.length > this.maxLines) {
|
|
16328
|
+
this.lines.shift();
|
|
16329
|
+
}
|
|
16330
|
+
}
|
|
16331
|
+
}
|
|
16332
|
+
read(offset = 0, limit) {
|
|
16333
|
+
const start = Math.max(0, offset);
|
|
16334
|
+
const end = limit !== undefined ? start + limit : this.lines.length;
|
|
16335
|
+
return this.lines.slice(start, end);
|
|
16336
|
+
}
|
|
16337
|
+
search(pattern) {
|
|
16338
|
+
const matches = [];
|
|
16339
|
+
for (let i = 0;i < this.lines.length; i++) {
|
|
16340
|
+
const line = this.lines[i];
|
|
16341
|
+
if (line !== undefined && pattern.test(line)) {
|
|
16342
|
+
matches.push({ lineNumber: i + 1, text: line });
|
|
16343
|
+
}
|
|
16344
|
+
}
|
|
16345
|
+
return matches;
|
|
16346
|
+
}
|
|
16347
|
+
get length() {
|
|
16348
|
+
return this.lines.length;
|
|
16349
|
+
}
|
|
16350
|
+
clear() {
|
|
16351
|
+
this.lines = [];
|
|
16352
|
+
}
|
|
16353
|
+
}
|
|
16354
|
+
|
|
16355
|
+
// src/tools/pty/manager.ts
|
|
16356
|
+
function generateId() {
|
|
16357
|
+
const hex3 = Array.from(crypto.getRandomValues(new Uint8Array(4))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
16358
|
+
return `pty_${hex3}`;
|
|
16359
|
+
}
|
|
16360
|
+
|
|
16361
|
+
class PTYManager {
|
|
16362
|
+
sessions = new Map;
|
|
16363
|
+
spawn(opts) {
|
|
16364
|
+
const id = generateId();
|
|
16365
|
+
const args = opts.args ?? [];
|
|
16366
|
+
const workdir = opts.workdir ?? process.cwd();
|
|
16367
|
+
const env = { ...process.env, ...opts.env };
|
|
16368
|
+
const title = opts.title ?? (`${opts.command} ${args.join(" ")}`.trim() || `Terminal ${id.slice(-4)}`);
|
|
16369
|
+
const ptyProcess = spawn3(opts.command, args, {
|
|
16370
|
+
name: "xterm-256color",
|
|
16371
|
+
cols: 120,
|
|
16372
|
+
rows: 40,
|
|
16373
|
+
cwd: workdir,
|
|
16374
|
+
env
|
|
16375
|
+
});
|
|
16376
|
+
const buffer = new RingBuffer;
|
|
16377
|
+
const session = {
|
|
16378
|
+
id,
|
|
16379
|
+
title,
|
|
16380
|
+
command: opts.command,
|
|
16381
|
+
args,
|
|
16382
|
+
workdir,
|
|
16383
|
+
env: opts.env,
|
|
16384
|
+
status: "running",
|
|
16385
|
+
pid: ptyProcess.pid,
|
|
16386
|
+
createdAt: new Date,
|
|
16387
|
+
parentSessionId: opts.parentSessionId,
|
|
16388
|
+
buffer,
|
|
16389
|
+
process: ptyProcess
|
|
16390
|
+
};
|
|
16391
|
+
this.sessions.set(id, session);
|
|
16392
|
+
ptyProcess.onData((data) => {
|
|
16393
|
+
buffer.append(data);
|
|
16394
|
+
});
|
|
16395
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
16396
|
+
if (session.status === "running") {
|
|
16397
|
+
session.status = "exited";
|
|
16398
|
+
session.exitCode = exitCode;
|
|
16399
|
+
}
|
|
16400
|
+
});
|
|
16401
|
+
return this.toInfo(session);
|
|
16402
|
+
}
|
|
16403
|
+
write(id, data) {
|
|
16404
|
+
const session = this.sessions.get(id);
|
|
16405
|
+
if (!session) {
|
|
16406
|
+
return false;
|
|
16407
|
+
}
|
|
16408
|
+
if (session.status !== "running") {
|
|
16409
|
+
return false;
|
|
16410
|
+
}
|
|
16411
|
+
session.process.write(data);
|
|
16412
|
+
return true;
|
|
16413
|
+
}
|
|
16414
|
+
read(id, offset = 0, limit) {
|
|
16415
|
+
const session = this.sessions.get(id);
|
|
16416
|
+
if (!session) {
|
|
16417
|
+
return null;
|
|
16418
|
+
}
|
|
16419
|
+
const lines = session.buffer.read(offset, limit);
|
|
16420
|
+
const totalLines = session.buffer.length;
|
|
16421
|
+
const hasMore = offset + lines.length < totalLines;
|
|
16422
|
+
return { lines, totalLines, offset, hasMore };
|
|
16423
|
+
}
|
|
16424
|
+
search(id, pattern, offset = 0, limit) {
|
|
16425
|
+
const session = this.sessions.get(id);
|
|
16426
|
+
if (!session) {
|
|
16427
|
+
return null;
|
|
16428
|
+
}
|
|
16429
|
+
const allMatches = session.buffer.search(pattern);
|
|
16430
|
+
const totalMatches = allMatches.length;
|
|
16431
|
+
const totalLines = session.buffer.length;
|
|
16432
|
+
const paginatedMatches = limit !== undefined ? allMatches.slice(offset, offset + limit) : allMatches.slice(offset);
|
|
16433
|
+
const hasMore = offset + paginatedMatches.length < totalMatches;
|
|
16434
|
+
return { matches: paginatedMatches, totalMatches, totalLines, offset, hasMore };
|
|
16435
|
+
}
|
|
16436
|
+
list() {
|
|
16437
|
+
return Array.from(this.sessions.values()).map((s) => this.toInfo(s));
|
|
16438
|
+
}
|
|
16439
|
+
get(id) {
|
|
16440
|
+
const session = this.sessions.get(id);
|
|
16441
|
+
return session ? this.toInfo(session) : null;
|
|
16442
|
+
}
|
|
16443
|
+
kill(id, cleanup = false) {
|
|
16444
|
+
const session = this.sessions.get(id);
|
|
16445
|
+
if (!session) {
|
|
16446
|
+
return false;
|
|
16447
|
+
}
|
|
16448
|
+
if (session.status === "running") {
|
|
16449
|
+
try {
|
|
16450
|
+
session.process.kill();
|
|
16451
|
+
} catch {}
|
|
16452
|
+
session.status = "killed";
|
|
16453
|
+
}
|
|
16454
|
+
if (cleanup) {
|
|
16455
|
+
session.buffer.clear();
|
|
16456
|
+
this.sessions.delete(id);
|
|
16457
|
+
}
|
|
16458
|
+
return true;
|
|
16459
|
+
}
|
|
16460
|
+
cleanupBySession(parentSessionId) {
|
|
16461
|
+
for (const [id, session] of this.sessions) {
|
|
16462
|
+
if (session.parentSessionId === parentSessionId) {
|
|
16463
|
+
this.kill(id, true);
|
|
16464
|
+
}
|
|
16465
|
+
}
|
|
16466
|
+
}
|
|
16467
|
+
cleanupAll() {
|
|
16468
|
+
for (const id of this.sessions.keys()) {
|
|
16469
|
+
this.kill(id, true);
|
|
16470
|
+
}
|
|
16471
|
+
}
|
|
16472
|
+
toInfo(session) {
|
|
16473
|
+
return {
|
|
16474
|
+
id: session.id,
|
|
16475
|
+
title: session.title,
|
|
16476
|
+
command: session.command,
|
|
16477
|
+
args: session.args,
|
|
16478
|
+
workdir: session.workdir,
|
|
16479
|
+
status: session.status,
|
|
16480
|
+
exitCode: session.exitCode,
|
|
16481
|
+
pid: session.pid,
|
|
16482
|
+
createdAt: session.createdAt,
|
|
16483
|
+
lineCount: session.buffer.length
|
|
16484
|
+
};
|
|
16485
|
+
}
|
|
16486
|
+
}
|
|
16487
|
+
// src/tools/pty/tools/spawn.ts
|
|
16488
|
+
var DESCRIPTION = `Spawns a new interactive PTY (pseudo-terminal) session that runs in the background.
|
|
16489
|
+
|
|
16490
|
+
Unlike the built-in bash tool which runs commands synchronously and waits for completion, PTY sessions persist and allow you to:
|
|
16491
|
+
- Run long-running processes (dev servers, watch modes, etc.)
|
|
16492
|
+
- Send interactive input (including Ctrl+C, arrow keys, etc.)
|
|
16493
|
+
- Read output at any time
|
|
16494
|
+
- Manage multiple concurrent terminal sessions
|
|
16495
|
+
|
|
16496
|
+
Usage:
|
|
16497
|
+
- The \`command\` parameter is required (e.g., "npm", "python", "bash")
|
|
16498
|
+
- Use \`args\` to pass arguments to the command (e.g., ["run", "dev"])
|
|
16499
|
+
- Use \`workdir\` to set the working directory (defaults to project root)
|
|
16500
|
+
- Use \`env\` to set additional environment variables
|
|
16501
|
+
- Use \`title\` to give the session a human-readable name
|
|
16502
|
+
- Use \`description\` for a clear, concise 5-10 word description (optional)
|
|
16503
|
+
|
|
16504
|
+
Returns the session info including:
|
|
16505
|
+
- \`id\`: Unique identifier (pty_XXXXXXXX) for use with other pty_* tools
|
|
16506
|
+
- \`pid\`: Process ID
|
|
16507
|
+
- \`status\`: Current status ("running")
|
|
16508
|
+
|
|
16509
|
+
After spawning, use:
|
|
16510
|
+
- \`pty_write\` to send input to the PTY
|
|
16511
|
+
- \`pty_read\` to read output from the PTY
|
|
16512
|
+
- \`pty_list\` to see all active PTY sessions
|
|
16513
|
+
- \`pty_kill\` to terminate the PTY
|
|
16514
|
+
|
|
16515
|
+
Examples:
|
|
16516
|
+
- Start a dev server: command="npm", args=["run", "dev"], title="Dev Server"
|
|
16517
|
+
- Start a Python REPL: command="python3", title="Python REPL"
|
|
16518
|
+
- Run tests in watch mode: command="npm", args=["test", "--", "--watch"]`;
|
|
16519
|
+
function createPtySpawnTool(manager) {
|
|
16520
|
+
return tool({
|
|
16521
|
+
description: DESCRIPTION,
|
|
16522
|
+
args: {
|
|
16523
|
+
command: tool.schema.string().describe("The command/executable to run"),
|
|
16524
|
+
args: tool.schema.array(tool.schema.string()).optional().describe("Arguments to pass to the command"),
|
|
16525
|
+
workdir: tool.schema.string().optional().describe("Working directory for the PTY session"),
|
|
16526
|
+
env: tool.schema.record(tool.schema.string(), tool.schema.string()).optional().describe("Additional environment variables"),
|
|
16527
|
+
title: tool.schema.string().optional().describe("Human-readable title for the session"),
|
|
16528
|
+
description: tool.schema.string().optional().describe("Clear, concise description of what this PTY session is for in 5-10 words")
|
|
16529
|
+
},
|
|
16530
|
+
execute: async (args, ctx) => {
|
|
16531
|
+
const info = manager.spawn({
|
|
16532
|
+
command: args.command,
|
|
16533
|
+
args: args.args,
|
|
16534
|
+
workdir: args.workdir,
|
|
16535
|
+
env: args.env,
|
|
16536
|
+
title: args.title,
|
|
16537
|
+
parentSessionId: ctx.sessionID
|
|
16538
|
+
});
|
|
16539
|
+
const output = [
|
|
16540
|
+
`<pty_spawned>`,
|
|
16541
|
+
`ID: ${info.id}`,
|
|
16542
|
+
`Title: ${info.title}`,
|
|
16543
|
+
`Command: ${info.command} ${info.args.join(" ")}`,
|
|
16544
|
+
`Workdir: ${info.workdir}`,
|
|
16545
|
+
`PID: ${info.pid}`,
|
|
16546
|
+
`Status: ${info.status}`,
|
|
16547
|
+
`</pty_spawned>`
|
|
16548
|
+
].join(`
|
|
16549
|
+
`);
|
|
16550
|
+
return output;
|
|
16551
|
+
}
|
|
16552
|
+
});
|
|
16553
|
+
}
|
|
16554
|
+
// src/tools/pty/tools/write.ts
|
|
16555
|
+
var DESCRIPTION2 = `Sends input data to an active PTY session.
|
|
16556
|
+
|
|
16557
|
+
Use this tool to:
|
|
16558
|
+
- Type commands or text into an interactive terminal
|
|
16559
|
+
- Send special key sequences (Ctrl+C, Enter, arrow keys, etc.)
|
|
16560
|
+
- Respond to prompts in interactive programs
|
|
16561
|
+
|
|
16562
|
+
Usage:
|
|
16563
|
+
- \`id\`: The PTY session ID (from pty_spawn or pty_list)
|
|
16564
|
+
- \`data\`: The input to send (text, commands, or escape sequences)
|
|
16565
|
+
|
|
16566
|
+
Common escape sequences:
|
|
16567
|
+
- Enter/newline: "\\n" or "\\r"
|
|
16568
|
+
- Ctrl+C (interrupt): "\\x03"
|
|
16569
|
+
- Ctrl+D (EOF): "\\x04"
|
|
16570
|
+
- Ctrl+Z (suspend): "\\x1a"
|
|
16571
|
+
- Tab: "\\t"
|
|
16572
|
+
- Arrow Up: "\\x1b[A"
|
|
16573
|
+
- Arrow Down: "\\x1b[B"
|
|
16574
|
+
- Arrow Right: "\\x1b[C"
|
|
16575
|
+
- Arrow Left: "\\x1b[D"
|
|
16576
|
+
|
|
16577
|
+
Returns success or error message.
|
|
16578
|
+
|
|
16579
|
+
Examples:
|
|
16580
|
+
- Send a command: data="ls -la\\n"
|
|
16581
|
+
- Interrupt a process: data="\\x03"
|
|
16582
|
+
- Answer a prompt: data="yes\\n"`;
|
|
16583
|
+
function parseEscapeSequences(input) {
|
|
16584
|
+
return input.replace(/\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|[nrt0\\])/g, (match, seq) => {
|
|
16585
|
+
if (seq.startsWith("x")) {
|
|
16586
|
+
return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
16587
|
+
}
|
|
16588
|
+
if (seq.startsWith("u")) {
|
|
16589
|
+
return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
16590
|
+
}
|
|
16591
|
+
switch (seq) {
|
|
16592
|
+
case "n":
|
|
16593
|
+
return `
|
|
16594
|
+
`;
|
|
16595
|
+
case "r":
|
|
16596
|
+
return "\r";
|
|
16597
|
+
case "t":
|
|
16598
|
+
return "\t";
|
|
16599
|
+
case "0":
|
|
16600
|
+
return "\x00";
|
|
16601
|
+
case "\\":
|
|
16602
|
+
return "\\";
|
|
16603
|
+
default:
|
|
16604
|
+
return match;
|
|
16605
|
+
}
|
|
16606
|
+
});
|
|
16607
|
+
}
|
|
16608
|
+
function createPtyWriteTool(manager) {
|
|
16609
|
+
return tool({
|
|
16610
|
+
description: DESCRIPTION2,
|
|
16611
|
+
args: {
|
|
16612
|
+
id: tool.schema.string().describe("The PTY session ID (e.g., pty_a1b2c3d4)"),
|
|
16613
|
+
data: tool.schema.string().describe("The input data to send to the PTY")
|
|
16614
|
+
},
|
|
16615
|
+
execute: async (args) => {
|
|
16616
|
+
const session = manager.get(args.id);
|
|
16617
|
+
if (!session) {
|
|
16618
|
+
throw new Error(`PTY session '${args.id}' not found. Use pty_list to see active sessions.`);
|
|
16619
|
+
}
|
|
16620
|
+
if (session.status !== "running") {
|
|
16621
|
+
throw new Error(`Cannot write to PTY '${args.id}' - session status is '${session.status}'.`);
|
|
16622
|
+
}
|
|
16623
|
+
const parsedData = parseEscapeSequences(args.data);
|
|
16624
|
+
const success2 = manager.write(args.id, parsedData);
|
|
16625
|
+
if (!success2) {
|
|
16626
|
+
throw new Error(`Failed to write to PTY '${args.id}'.`);
|
|
16627
|
+
}
|
|
16628
|
+
const preview = args.data.length > 50 ? `${args.data.slice(0, 50)}...` : args.data;
|
|
16629
|
+
const displayPreview = preview.replace(/\x03/g, "^C").replace(/\x04/g, "^D").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
16630
|
+
return `Sent ${parsedData.length} bytes to ${args.id}: "${displayPreview}"`;
|
|
16631
|
+
}
|
|
16632
|
+
});
|
|
16633
|
+
}
|
|
16634
|
+
// src/tools/pty/tools/read.ts
|
|
16635
|
+
var DESCRIPTION3 = `Reads output from a PTY session's buffer.
|
|
16636
|
+
|
|
16637
|
+
The PTY maintains a rolling buffer of output lines. Use offset and limit to paginate through the output, similar to reading a file.
|
|
16638
|
+
|
|
16639
|
+
Usage:
|
|
16640
|
+
- \`id\`: The PTY session ID (from pty_spawn or pty_list)
|
|
16641
|
+
- \`offset\`: Line number to start reading from (0-based, defaults to 0)
|
|
16642
|
+
- \`limit\`: Number of lines to read (defaults to 500)
|
|
16643
|
+
- \`pattern\`: Regex pattern to filter lines (optional)
|
|
16644
|
+
- \`ignoreCase\`: Case-insensitive pattern matching (default: false)
|
|
16645
|
+
|
|
16646
|
+
Returns:
|
|
16647
|
+
- Numbered lines of output (similar to cat -n format)
|
|
16648
|
+
- Total line count in the buffer
|
|
16649
|
+
- Indicator if more lines are available
|
|
16650
|
+
|
|
16651
|
+
The buffer stores up to PTY_MAX_BUFFER_LINES (default: 50000) lines. Older lines are discarded when the limit is reached.
|
|
16652
|
+
|
|
16653
|
+
Pattern Filtering:
|
|
16654
|
+
- When \`pattern\` is set, lines are FILTERED FIRST using the regex, then offset/limit apply to the MATCHES
|
|
16655
|
+
- Original line numbers are preserved so you can see where matches occurred in the buffer
|
|
16656
|
+
- Supports full regex syntax (e.g., "error", "ERROR|WARN", "failed.*connection", etc.)
|
|
16657
|
+
- If the pattern is invalid, an error message is returned explaining the issue
|
|
16658
|
+
- If no lines match the pattern, a clear message indicates zero matches
|
|
16659
|
+
|
|
16660
|
+
Tips:
|
|
16661
|
+
- To see the latest output, use a high offset or omit offset to read from the start
|
|
16662
|
+
- To tail recent output, calculate offset as (totalLines - N) where N is how many recent lines you want
|
|
16663
|
+
- Lines longer than 2000 characters are truncated
|
|
16664
|
+
- Empty output may mean the process hasn't produced output yet
|
|
16665
|
+
|
|
16666
|
+
Examples:
|
|
16667
|
+
- Read first 100 lines: offset=0, limit=100
|
|
16668
|
+
- Read lines 500-600: offset=500, limit=100
|
|
16669
|
+
- Read all available: omit both parameters
|
|
16670
|
+
- Find errors: pattern="error", ignoreCase=true
|
|
16671
|
+
- Find specific log levels: pattern="ERROR|WARN|FATAL"
|
|
16672
|
+
- First 10 matches only: pattern="error", limit=10`;
|
|
16673
|
+
var DEFAULT_LIMIT = 500;
|
|
16674
|
+
var MAX_LINE_LENGTH = 2000;
|
|
16675
|
+
function createPtyReadTool(manager) {
|
|
16676
|
+
return tool({
|
|
16677
|
+
description: DESCRIPTION3,
|
|
16678
|
+
args: {
|
|
16679
|
+
id: tool.schema.string().describe("The PTY session ID (e.g., pty_a1b2c3d4)"),
|
|
16680
|
+
offset: tool.schema.number().optional().describe("Line number to start reading from (0-based, defaults to 0)"),
|
|
16681
|
+
limit: tool.schema.number().optional().describe("Number of lines to read (defaults to 500)"),
|
|
16682
|
+
pattern: tool.schema.string().optional().describe("Regex pattern to filter lines"),
|
|
16683
|
+
ignoreCase: tool.schema.boolean().optional().describe("Case-insensitive pattern matching (default: false)")
|
|
16684
|
+
},
|
|
16685
|
+
execute: async (args) => {
|
|
16686
|
+
const session = manager.get(args.id);
|
|
16687
|
+
if (!session) {
|
|
16688
|
+
throw new Error(`PTY session '${args.id}' not found. Use pty_list to see active sessions.`);
|
|
16689
|
+
}
|
|
16690
|
+
const offset = Math.max(0, args.offset ?? 0);
|
|
16691
|
+
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
16692
|
+
if (args.pattern) {
|
|
16693
|
+
let regex;
|
|
16694
|
+
try {
|
|
16695
|
+
regex = new RegExp(args.pattern, args.ignoreCase ? "i" : "");
|
|
16696
|
+
} catch (e) {
|
|
16697
|
+
const error45 = e instanceof Error ? e.message : String(e);
|
|
16698
|
+
throw new Error(`Invalid regex pattern '${args.pattern}': ${error45}`);
|
|
16699
|
+
}
|
|
16700
|
+
const result2 = manager.search(args.id, regex, offset, limit);
|
|
16701
|
+
if (!result2) {
|
|
16702
|
+
throw new Error(`PTY session '${args.id}' not found.`);
|
|
16703
|
+
}
|
|
16704
|
+
if (result2.matches.length === 0) {
|
|
16705
|
+
return [
|
|
16706
|
+
`<pty_output id="${args.id}" status="${session.status}" pattern="${args.pattern}">`,
|
|
16707
|
+
`No lines matched the pattern '${args.pattern}'.`,
|
|
16708
|
+
`Total lines in buffer: ${result2.totalLines}`,
|
|
16709
|
+
`</pty_output>`
|
|
16710
|
+
].join(`
|
|
16711
|
+
`);
|
|
16712
|
+
}
|
|
16713
|
+
const formattedLines2 = result2.matches.map((match) => {
|
|
16714
|
+
const lineNum = match.lineNumber.toString().padStart(5, "0");
|
|
16715
|
+
const truncatedLine = match.text.length > MAX_LINE_LENGTH ? `${match.text.slice(0, MAX_LINE_LENGTH)}...` : match.text;
|
|
16716
|
+
return `${lineNum}| ${truncatedLine}`;
|
|
16717
|
+
});
|
|
16718
|
+
const output2 = [
|
|
16719
|
+
`<pty_output id="${args.id}" status="${session.status}" pattern="${args.pattern}">`,
|
|
16720
|
+
...formattedLines2,
|
|
16721
|
+
""
|
|
16722
|
+
];
|
|
16723
|
+
if (result2.hasMore) {
|
|
16724
|
+
output2.push(`(${result2.matches.length} of ${result2.totalMatches} matches shown. Use offset=${offset + result2.matches.length} to see more.)`);
|
|
16725
|
+
} else {
|
|
16726
|
+
output2.push(`(${result2.totalMatches} match${result2.totalMatches === 1 ? "" : "es"} from ${result2.totalLines} total lines)`);
|
|
16727
|
+
}
|
|
16728
|
+
output2.push(`</pty_output>`);
|
|
16729
|
+
return output2.join(`
|
|
16730
|
+
`);
|
|
16731
|
+
}
|
|
16732
|
+
const result = manager.read(args.id, offset, limit);
|
|
16733
|
+
if (!result) {
|
|
16734
|
+
throw new Error(`PTY session '${args.id}' not found.`);
|
|
16735
|
+
}
|
|
16736
|
+
if (result.lines.length === 0) {
|
|
16737
|
+
return [
|
|
16738
|
+
`<pty_output id="${args.id}" status="${session.status}">`,
|
|
16739
|
+
`(No output available - buffer is empty)`,
|
|
16740
|
+
`Total lines: ${result.totalLines}`,
|
|
16741
|
+
`</pty_output>`
|
|
16742
|
+
].join(`
|
|
16743
|
+
`);
|
|
16744
|
+
}
|
|
16745
|
+
const formattedLines = result.lines.map((line, index) => {
|
|
16746
|
+
const lineNum = (result.offset + index + 1).toString().padStart(5, "0");
|
|
16747
|
+
const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
|
|
16748
|
+
return `${lineNum}| ${truncatedLine}`;
|
|
16749
|
+
});
|
|
16750
|
+
const output = [`<pty_output id="${args.id}" status="${session.status}">`, ...formattedLines];
|
|
16751
|
+
if (result.hasMore) {
|
|
16752
|
+
output.push("");
|
|
16753
|
+
output.push(`(Buffer has more lines. Use offset=${result.offset + result.lines.length} to read beyond line ${result.offset + result.lines.length})`);
|
|
16754
|
+
} else {
|
|
16755
|
+
output.push("");
|
|
16756
|
+
output.push(`(End of buffer - total ${result.totalLines} lines)`);
|
|
16757
|
+
}
|
|
16758
|
+
output.push(`</pty_output>`);
|
|
16759
|
+
return output.join(`
|
|
16760
|
+
`);
|
|
16761
|
+
}
|
|
16762
|
+
});
|
|
16763
|
+
}
|
|
16764
|
+
// src/tools/pty/tools/list.ts
|
|
16765
|
+
var DESCRIPTION4 = `Lists all PTY sessions (active and exited).
|
|
16766
|
+
|
|
16767
|
+
Use this tool to:
|
|
16768
|
+
- See all running and exited PTY sessions
|
|
16769
|
+
- Get session IDs for use with other pty_* tools
|
|
16770
|
+
- Check the status and output line count of each session
|
|
16771
|
+
- Monitor which processes are still running
|
|
16772
|
+
|
|
16773
|
+
Returns for each session:
|
|
16774
|
+
- \`id\`: Unique identifier for use with other tools
|
|
16775
|
+
- \`title\`: Human-readable name
|
|
16776
|
+
- \`command\`: The command that was executed
|
|
16777
|
+
- \`status\`: Current status (running, exited, killed)
|
|
16778
|
+
- \`exitCode\`: Exit code (if exited/killed)
|
|
16779
|
+
- \`pid\`: Process ID
|
|
16780
|
+
- \`lineCount\`: Number of lines in the output buffer
|
|
16781
|
+
- \`createdAt\`: When the session was created
|
|
16782
|
+
|
|
16783
|
+
Tips:
|
|
16784
|
+
- Use the session ID with pty_read, pty_write, or pty_kill
|
|
16785
|
+
- Sessions remain in the list after exit until explicitly cleaned up with pty_kill
|
|
16786
|
+
- This allows you to compare output from multiple sessions`;
|
|
16787
|
+
function createPtyListTool(manager) {
|
|
16788
|
+
return tool({
|
|
16789
|
+
description: DESCRIPTION4,
|
|
16790
|
+
args: {},
|
|
16791
|
+
execute: async () => {
|
|
16792
|
+
const sessions = manager.list();
|
|
16793
|
+
if (sessions.length === 0) {
|
|
16794
|
+
return `<pty_list>
|
|
16795
|
+
No active PTY sessions.
|
|
16796
|
+
</pty_list>`;
|
|
16797
|
+
}
|
|
16798
|
+
const lines = ["<pty_list>"];
|
|
16799
|
+
for (const session of sessions) {
|
|
16800
|
+
const exitInfo = session.exitCode !== undefined ? ` (exit: ${session.exitCode})` : "";
|
|
16801
|
+
lines.push(`[${session.id}] ${session.title}`);
|
|
16802
|
+
lines.push(` Command: ${session.command} ${session.args.join(" ")}`);
|
|
16803
|
+
lines.push(` Status: ${session.status}${exitInfo}`);
|
|
16804
|
+
lines.push(` PID: ${session.pid} | Lines: ${session.lineCount} | Workdir: ${session.workdir}`);
|
|
16805
|
+
lines.push(` Created: ${session.createdAt.toISOString()}`);
|
|
16806
|
+
lines.push("");
|
|
16807
|
+
}
|
|
16808
|
+
lines.push(`Total: ${sessions.length} session(s)`);
|
|
16809
|
+
lines.push("</pty_list>");
|
|
16810
|
+
return lines.join(`
|
|
16811
|
+
`);
|
|
16812
|
+
}
|
|
16813
|
+
});
|
|
16814
|
+
}
|
|
16815
|
+
// src/tools/pty/tools/kill.ts
|
|
16816
|
+
var DESCRIPTION5 = `Terminates a PTY session and optionally cleans up its buffer.
|
|
16817
|
+
|
|
16818
|
+
Use this tool to:
|
|
16819
|
+
- Stop a running process (sends SIGTERM)
|
|
16820
|
+
- Clean up an exited session to free memory
|
|
16821
|
+
- Remove a session from the list
|
|
16822
|
+
|
|
16823
|
+
Usage:
|
|
16824
|
+
- \`id\`: The PTY session ID (from pty_spawn or pty_list)
|
|
16825
|
+
- \`cleanup\`: If true, removes the session and frees the buffer (default: false)
|
|
16826
|
+
|
|
16827
|
+
Behavior:
|
|
16828
|
+
- If the session is running, it will be killed (status becomes "killed")
|
|
16829
|
+
- If cleanup=false (default), the session remains in the list with its output buffer intact
|
|
16830
|
+
- If cleanup=true, the session is removed entirely and the buffer is freed
|
|
16831
|
+
- Keeping sessions without cleanup allows you to compare logs between runs
|
|
16832
|
+
|
|
16833
|
+
Tips:
|
|
16834
|
+
- Use cleanup=false if you might want to read the output later
|
|
16835
|
+
- Use cleanup=true when you're done with the session entirely
|
|
16836
|
+
- To send Ctrl+C instead of killing, use pty_write with data="\\x03"
|
|
16837
|
+
|
|
16838
|
+
Examples:
|
|
16839
|
+
- Kill but keep logs: cleanup=false (or omit)
|
|
16840
|
+
- Kill and remove: cleanup=true`;
|
|
16841
|
+
function createPtyKillTool(manager) {
|
|
16842
|
+
return tool({
|
|
16843
|
+
description: DESCRIPTION5,
|
|
16844
|
+
args: {
|
|
16845
|
+
id: tool.schema.string().describe("The PTY session ID (e.g., pty_a1b2c3d4)"),
|
|
16846
|
+
cleanup: tool.schema.boolean().optional().describe("If true, removes the session and frees the buffer (default: false)")
|
|
16847
|
+
},
|
|
16848
|
+
execute: async (args) => {
|
|
16849
|
+
const session = manager.get(args.id);
|
|
16850
|
+
if (!session) {
|
|
16851
|
+
throw new Error(`PTY session '${args.id}' not found. Use pty_list to see active sessions.`);
|
|
16852
|
+
}
|
|
16853
|
+
const wasRunning = session.status === "running";
|
|
16854
|
+
const cleanup = args.cleanup ?? false;
|
|
16855
|
+
const success2 = manager.kill(args.id, cleanup);
|
|
16856
|
+
if (!success2) {
|
|
16857
|
+
throw new Error(`Failed to kill PTY session '${args.id}'.`);
|
|
16858
|
+
}
|
|
16859
|
+
const action = wasRunning ? "Killed" : "Cleaned up";
|
|
16860
|
+
const cleanupNote = cleanup ? " (session removed)" : " (session retained for log access)";
|
|
16861
|
+
return [
|
|
16862
|
+
`<pty_killed>`,
|
|
16863
|
+
`${action}: ${args.id}${cleanupNote}`,
|
|
16864
|
+
`Title: ${session.title}`,
|
|
16865
|
+
`Command: ${session.command} ${session.args.join(" ")}`,
|
|
16866
|
+
`Final line count: ${session.lineCount}`,
|
|
16867
|
+
`</pty_killed>`
|
|
16868
|
+
].join(`
|
|
16869
|
+
`);
|
|
16870
|
+
}
|
|
16871
|
+
});
|
|
16872
|
+
}
|
|
16873
|
+
// src/tools/pty/index.ts
|
|
16874
|
+
function createPtyTools(manager) {
|
|
16875
|
+
return {
|
|
16876
|
+
pty_spawn: createPtySpawnTool(manager),
|
|
16877
|
+
pty_write: createPtyWriteTool(manager),
|
|
16878
|
+
pty_read: createPtyReadTool(manager),
|
|
16879
|
+
pty_list: createPtyListTool(manager),
|
|
16880
|
+
pty_kill: createPtyKillTool(manager)
|
|
16881
|
+
};
|
|
16882
|
+
}
|
|
16883
|
+
|
|
16066
16884
|
// src/config-loader.ts
|
|
16067
16885
|
import { readFile as readFile3 } from "fs/promises";
|
|
16068
|
-
import { join as
|
|
16886
|
+
import { join as join5 } from "path";
|
|
16069
16887
|
import { homedir as homedir2 } from "os";
|
|
16070
16888
|
var SAFE_AGENT_PROPERTIES = ["model", "temperature", "maxTokens"];
|
|
16071
16889
|
async function loadMicodeConfig(configDir) {
|
|
16072
|
-
const baseDir = configDir ??
|
|
16073
|
-
const configPath =
|
|
16890
|
+
const baseDir = configDir ?? join5(homedir2(), ".config", "opencode");
|
|
16891
|
+
const configPath = join5(baseDir, "micode.json");
|
|
16074
16892
|
try {
|
|
16075
16893
|
const content = await readFile3(configPath, "utf-8");
|
|
16076
|
-
const
|
|
16077
|
-
if (
|
|
16894
|
+
const parsed2 = JSON.parse(content);
|
|
16895
|
+
if (parsed2.agents && typeof parsed2.agents === "object") {
|
|
16078
16896
|
const sanitizedAgents = {};
|
|
16079
|
-
for (const [agentName, agentConfig] of Object.entries(
|
|
16897
|
+
for (const [agentName, agentConfig] of Object.entries(parsed2.agents)) {
|
|
16080
16898
|
if (agentConfig && typeof agentConfig === "object") {
|
|
16081
16899
|
const sanitized = {};
|
|
16082
16900
|
const config2 = agentConfig;
|
|
@@ -16090,7 +16908,7 @@ async function loadMicodeConfig(configDir) {
|
|
|
16090
16908
|
}
|
|
16091
16909
|
return { agents: sanitizedAgents };
|
|
16092
16910
|
}
|
|
16093
|
-
return
|
|
16911
|
+
return parsed2;
|
|
16094
16912
|
} catch {
|
|
16095
16913
|
return null;
|
|
16096
16914
|
}
|
|
@@ -16168,6 +16986,8 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
16168
16986
|
const fileOpsTrackerHook = createFileOpsTrackerHook(ctx);
|
|
16169
16987
|
const backgroundTaskManager = new BackgroundTaskManager(ctx);
|
|
16170
16988
|
const backgroundTaskTools = createBackgroundTaskTools(backgroundTaskManager);
|
|
16989
|
+
const ptyManager = new PTYManager;
|
|
16990
|
+
const ptyTools = createPtyTools(ptyManager);
|
|
16171
16991
|
return {
|
|
16172
16992
|
tool: {
|
|
16173
16993
|
ast_grep_search,
|
|
@@ -16175,7 +16995,8 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
16175
16995
|
btca_ask,
|
|
16176
16996
|
look_at,
|
|
16177
16997
|
artifact_search,
|
|
16178
|
-
...backgroundTaskTools
|
|
16998
|
+
...backgroundTaskTools,
|
|
16999
|
+
...ptyTools
|
|
16179
17000
|
},
|
|
16180
17001
|
config: async (config2) => {
|
|
16181
17002
|
config2.permission = {
|
|
@@ -16249,6 +17070,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
16249
17070
|
const props = event.properties;
|
|
16250
17071
|
if (props?.info?.id) {
|
|
16251
17072
|
thinkModeState.delete(props.info.id);
|
|
17073
|
+
ptyManager.cleanupBySession(props.info.id);
|
|
16252
17074
|
}
|
|
16253
17075
|
}
|
|
16254
17076
|
await autoCompactHook.event({ event });
|