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/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 80% context usage</action>
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 ALL results:
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.8;
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 join4 } from "path";
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 ?? join4(homedir2(), ".config", "opencode");
16073
- const configPath = join4(baseDir, "micode.json");
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 parsed = JSON.parse(content);
16077
- if (parsed.agents && typeof parsed.agents === "object") {
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(parsed.agents)) {
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 parsed;
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 });