micode 0.7.0 → 0.7.2

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.
Files changed (92) hide show
  1. package/package.json +7 -13
  2. package/src/agents/artifact-searcher.ts +46 -0
  3. package/src/agents/brainstormer.ts +145 -0
  4. package/src/agents/codebase-analyzer.ts +75 -0
  5. package/src/agents/codebase-locator.ts +71 -0
  6. package/src/agents/commander.ts +138 -0
  7. package/src/agents/executor.ts +215 -0
  8. package/src/agents/implementer.ts +99 -0
  9. package/src/agents/index.ts +44 -0
  10. package/src/agents/ledger-creator.ts +113 -0
  11. package/src/agents/pattern-finder.ts +70 -0
  12. package/src/agents/planner.ts +230 -0
  13. package/src/agents/project-initializer.ts +264 -0
  14. package/src/agents/reviewer.ts +102 -0
  15. package/src/config-loader.ts +89 -0
  16. package/src/hooks/artifact-auto-index.ts +111 -0
  17. package/src/hooks/auto-clear-ledger.ts +230 -0
  18. package/src/hooks/auto-compact.ts +241 -0
  19. package/src/hooks/comment-checker.ts +120 -0
  20. package/src/hooks/context-injector.ts +163 -0
  21. package/src/hooks/context-window-monitor.ts +106 -0
  22. package/src/hooks/file-ops-tracker.ts +96 -0
  23. package/src/hooks/ledger-loader.ts +78 -0
  24. package/src/hooks/preemptive-compaction.ts +183 -0
  25. package/src/hooks/session-recovery.ts +258 -0
  26. package/src/hooks/token-aware-truncation.ts +189 -0
  27. package/src/index.ts +258 -0
  28. package/src/tools/artifact-index/index.ts +269 -0
  29. package/src/tools/artifact-index/schema.sql +44 -0
  30. package/src/tools/artifact-search.ts +49 -0
  31. package/src/tools/ast-grep/index.ts +189 -0
  32. package/src/tools/background-task/manager.ts +397 -0
  33. package/src/tools/background-task/tools.ts +145 -0
  34. package/src/tools/background-task/types.ts +68 -0
  35. package/src/tools/btca/index.ts +82 -0
  36. package/src/tools/look-at.ts +210 -0
  37. package/src/tools/pty/buffer.ts +49 -0
  38. package/src/tools/pty/index.ts +34 -0
  39. package/src/tools/pty/manager.ts +159 -0
  40. package/src/tools/pty/tools/kill.ts +68 -0
  41. package/src/tools/pty/tools/list.ts +55 -0
  42. package/src/tools/pty/tools/read.ts +152 -0
  43. package/src/tools/pty/tools/spawn.ts +78 -0
  44. package/src/tools/pty/tools/write.ts +97 -0
  45. package/src/tools/pty/types.ts +62 -0
  46. package/src/utils/model-limits.ts +36 -0
  47. package/dist/agents/artifact-searcher.d.ts +0 -2
  48. package/dist/agents/brainstormer.d.ts +0 -2
  49. package/dist/agents/codebase-analyzer.d.ts +0 -2
  50. package/dist/agents/codebase-locator.d.ts +0 -2
  51. package/dist/agents/commander.d.ts +0 -3
  52. package/dist/agents/executor.d.ts +0 -2
  53. package/dist/agents/implementer.d.ts +0 -2
  54. package/dist/agents/index.d.ts +0 -15
  55. package/dist/agents/ledger-creator.d.ts +0 -2
  56. package/dist/agents/pattern-finder.d.ts +0 -2
  57. package/dist/agents/planner.d.ts +0 -2
  58. package/dist/agents/project-initializer.d.ts +0 -2
  59. package/dist/agents/reviewer.d.ts +0 -2
  60. package/dist/config-loader.d.ts +0 -20
  61. package/dist/hooks/artifact-auto-index.d.ts +0 -19
  62. package/dist/hooks/auto-clear-ledger.d.ts +0 -11
  63. package/dist/hooks/auto-compact.d.ts +0 -9
  64. package/dist/hooks/comment-checker.d.ts +0 -9
  65. package/dist/hooks/context-injector.d.ts +0 -15
  66. package/dist/hooks/context-window-monitor.d.ts +0 -15
  67. package/dist/hooks/file-ops-tracker.d.ts +0 -26
  68. package/dist/hooks/ledger-loader.d.ts +0 -16
  69. package/dist/hooks/preemptive-compaction.d.ts +0 -9
  70. package/dist/hooks/session-recovery.d.ts +0 -9
  71. package/dist/hooks/token-aware-truncation.d.ts +0 -15
  72. package/dist/index.d.ts +0 -3
  73. package/dist/index.js +0 -17089
  74. package/dist/tools/artifact-index/index.d.ts +0 -38
  75. package/dist/tools/artifact-search.d.ts +0 -17
  76. package/dist/tools/ast-grep/index.d.ts +0 -88
  77. package/dist/tools/background-task/manager.d.ts +0 -27
  78. package/dist/tools/background-task/tools.d.ts +0 -41
  79. package/dist/tools/background-task/types.d.ts +0 -53
  80. package/dist/tools/btca/index.d.ts +0 -19
  81. package/dist/tools/look-at.d.ts +0 -11
  82. package/dist/tools/pty/buffer.d.ts +0 -11
  83. package/dist/tools/pty/index.d.ts +0 -74
  84. package/dist/tools/pty/manager.d.ts +0 -14
  85. package/dist/tools/pty/tools/kill.d.ts +0 -12
  86. package/dist/tools/pty/tools/list.d.ts +0 -6
  87. package/dist/tools/pty/tools/read.d.ts +0 -18
  88. package/dist/tools/pty/tools/spawn.d.ts +0 -20
  89. package/dist/tools/pty/tools/write.d.ts +0 -12
  90. package/dist/tools/pty/types.d.ts +0 -54
  91. package/dist/utils/model-limits.d.ts +0 -7
  92. /package/{dist/tools/background-task/index.d.ts → src/tools/background-task/index.ts} +0 -0
@@ -0,0 +1,159 @@
1
+ // src/tools/pty/manager.ts
2
+ import { spawn, type IPty } from "bun-pty";
3
+ import { RingBuffer } from "./buffer";
4
+ import type { PTYSession, PTYSessionInfo, SpawnOptions, ReadResult, SearchResult } from "./types";
5
+
6
+ function generateId(): string {
7
+ const hex = Array.from(crypto.getRandomValues(new Uint8Array(4)))
8
+ .map((b) => b.toString(16).padStart(2, "0"))
9
+ .join("");
10
+ return `pty_${hex}`;
11
+ }
12
+
13
+ export class PTYManager {
14
+ private sessions: Map<string, PTYSession> = new Map();
15
+
16
+ spawn(opts: SpawnOptions): PTYSessionInfo {
17
+ const id = generateId();
18
+ const args = opts.args ?? [];
19
+ const workdir = opts.workdir ?? process.cwd();
20
+ const env = { ...process.env, ...opts.env } as Record<string, string>;
21
+ const title = opts.title ?? (`${opts.command} ${args.join(" ")}`.trim() || `Terminal ${id.slice(-4)}`);
22
+
23
+ const ptyProcess: IPty = spawn(opts.command, args, {
24
+ name: "xterm-256color",
25
+ cols: 120,
26
+ rows: 40,
27
+ cwd: workdir,
28
+ env,
29
+ });
30
+
31
+ const buffer = new RingBuffer();
32
+ const session: PTYSession = {
33
+ id,
34
+ title,
35
+ command: opts.command,
36
+ args,
37
+ workdir,
38
+ env: opts.env,
39
+ status: "running",
40
+ pid: ptyProcess.pid,
41
+ createdAt: new Date(),
42
+ parentSessionId: opts.parentSessionId,
43
+ buffer,
44
+ process: ptyProcess,
45
+ };
46
+
47
+ this.sessions.set(id, session);
48
+
49
+ ptyProcess.onData((data: string) => {
50
+ buffer.append(data);
51
+ });
52
+
53
+ ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
54
+ if (session.status === "running") {
55
+ session.status = "exited";
56
+ session.exitCode = exitCode;
57
+ }
58
+ });
59
+
60
+ return this.toInfo(session);
61
+ }
62
+
63
+ write(id: string, data: string): boolean {
64
+ const session = this.sessions.get(id);
65
+ if (!session) {
66
+ return false;
67
+ }
68
+ if (session.status !== "running") {
69
+ return false;
70
+ }
71
+ session.process.write(data);
72
+ return true;
73
+ }
74
+
75
+ read(id: string, offset: number = 0, limit?: number): ReadResult | null {
76
+ const session = this.sessions.get(id);
77
+ if (!session) {
78
+ return null;
79
+ }
80
+ const lines = session.buffer.read(offset, limit);
81
+ const totalLines = session.buffer.length;
82
+ const hasMore = offset + lines.length < totalLines;
83
+ return { lines, totalLines, offset, hasMore };
84
+ }
85
+
86
+ search(id: string, pattern: RegExp, offset: number = 0, limit?: number): SearchResult | null {
87
+ const session = this.sessions.get(id);
88
+ if (!session) {
89
+ return null;
90
+ }
91
+ const allMatches = session.buffer.search(pattern);
92
+ const totalMatches = allMatches.length;
93
+ const totalLines = session.buffer.length;
94
+ const paginatedMatches = limit !== undefined ? allMatches.slice(offset, offset + limit) : allMatches.slice(offset);
95
+ const hasMore = offset + paginatedMatches.length < totalMatches;
96
+ return { matches: paginatedMatches, totalMatches, totalLines, offset, hasMore };
97
+ }
98
+
99
+ list(): PTYSessionInfo[] {
100
+ return Array.from(this.sessions.values()).map((s) => this.toInfo(s));
101
+ }
102
+
103
+ get(id: string): PTYSessionInfo | null {
104
+ const session = this.sessions.get(id);
105
+ return session ? this.toInfo(session) : null;
106
+ }
107
+
108
+ kill(id: string, cleanup: boolean = false): boolean {
109
+ const session = this.sessions.get(id);
110
+ if (!session) {
111
+ return false;
112
+ }
113
+
114
+ if (session.status === "running") {
115
+ try {
116
+ session.process.kill();
117
+ } catch {
118
+ // Process may already be dead
119
+ }
120
+ session.status = "killed";
121
+ }
122
+
123
+ if (cleanup) {
124
+ session.buffer.clear();
125
+ this.sessions.delete(id);
126
+ }
127
+
128
+ return true;
129
+ }
130
+
131
+ cleanupBySession(parentSessionId: string): void {
132
+ for (const [id, session] of this.sessions) {
133
+ if (session.parentSessionId === parentSessionId) {
134
+ this.kill(id, true);
135
+ }
136
+ }
137
+ }
138
+
139
+ cleanupAll(): void {
140
+ for (const id of this.sessions.keys()) {
141
+ this.kill(id, true);
142
+ }
143
+ }
144
+
145
+ private toInfo(session: PTYSession): PTYSessionInfo {
146
+ return {
147
+ id: session.id,
148
+ title: session.title,
149
+ command: session.command,
150
+ args: session.args,
151
+ workdir: session.workdir,
152
+ status: session.status,
153
+ exitCode: session.exitCode,
154
+ pid: session.pid,
155
+ createdAt: session.createdAt,
156
+ lineCount: session.buffer.length,
157
+ };
158
+ }
159
+ }
@@ -0,0 +1,68 @@
1
+ // src/tools/pty/tools/kill.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import type { PTYManager } from "../manager";
4
+
5
+ const DESCRIPTION = `Terminates a PTY session and optionally cleans up its buffer.
6
+
7
+ Use this tool to:
8
+ - Stop a running process (sends SIGTERM)
9
+ - Clean up an exited session to free memory
10
+ - Remove a session from the list
11
+
12
+ Usage:
13
+ - \`id\`: The PTY session ID (from pty_spawn or pty_list)
14
+ - \`cleanup\`: If true, removes the session and frees the buffer (default: false)
15
+
16
+ Behavior:
17
+ - If the session is running, it will be killed (status becomes "killed")
18
+ - If cleanup=false (default), the session remains in the list with its output buffer intact
19
+ - If cleanup=true, the session is removed entirely and the buffer is freed
20
+ - Keeping sessions without cleanup allows you to compare logs between runs
21
+
22
+ Tips:
23
+ - Use cleanup=false if you might want to read the output later
24
+ - Use cleanup=true when you're done with the session entirely
25
+ - To send Ctrl+C instead of killing, use pty_write with data="\\x03"
26
+
27
+ Examples:
28
+ - Kill but keep logs: cleanup=false (or omit)
29
+ - Kill and remove: cleanup=true`;
30
+
31
+ export function createPtyKillTool(manager: PTYManager) {
32
+ return tool({
33
+ description: DESCRIPTION,
34
+ args: {
35
+ id: tool.schema.string().describe("The PTY session ID (e.g., pty_a1b2c3d4)"),
36
+ cleanup: tool.schema
37
+ .boolean()
38
+ .optional()
39
+ .describe("If true, removes the session and frees the buffer (default: false)"),
40
+ },
41
+ execute: async (args) => {
42
+ const session = manager.get(args.id);
43
+ if (!session) {
44
+ throw new Error(`PTY session '${args.id}' not found. Use pty_list to see active sessions.`);
45
+ }
46
+
47
+ const wasRunning = session.status === "running";
48
+ const cleanup = args.cleanup ?? false;
49
+ const success = manager.kill(args.id, cleanup);
50
+
51
+ if (!success) {
52
+ throw new Error(`Failed to kill PTY session '${args.id}'.`);
53
+ }
54
+
55
+ const action = wasRunning ? "Killed" : "Cleaned up";
56
+ const cleanupNote = cleanup ? " (session removed)" : " (session retained for log access)";
57
+
58
+ return [
59
+ `<pty_killed>`,
60
+ `${action}: ${args.id}${cleanupNote}`,
61
+ `Title: ${session.title}`,
62
+ `Command: ${session.command} ${session.args.join(" ")}`,
63
+ `Final line count: ${session.lineCount}`,
64
+ `</pty_killed>`,
65
+ ].join("\n");
66
+ },
67
+ });
68
+ }
@@ -0,0 +1,55 @@
1
+ // src/tools/pty/tools/list.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import type { PTYManager } from "../manager";
4
+
5
+ const DESCRIPTION = `Lists all PTY sessions (active and exited).
6
+
7
+ Use this tool to:
8
+ - See all running and exited PTY sessions
9
+ - Get session IDs for use with other pty_* tools
10
+ - Check the status and output line count of each session
11
+ - Monitor which processes are still running
12
+
13
+ Returns for each session:
14
+ - \`id\`: Unique identifier for use with other tools
15
+ - \`title\`: Human-readable name
16
+ - \`command\`: The command that was executed
17
+ - \`status\`: Current status (running, exited, killed)
18
+ - \`exitCode\`: Exit code (if exited/killed)
19
+ - \`pid\`: Process ID
20
+ - \`lineCount\`: Number of lines in the output buffer
21
+ - \`createdAt\`: When the session was created
22
+
23
+ Tips:
24
+ - Use the session ID with pty_read, pty_write, or pty_kill
25
+ - Sessions remain in the list after exit until explicitly cleaned up with pty_kill
26
+ - This allows you to compare output from multiple sessions`;
27
+
28
+ export function createPtyListTool(manager: PTYManager) {
29
+ return tool({
30
+ description: DESCRIPTION,
31
+ args: {},
32
+ execute: async () => {
33
+ const sessions = manager.list();
34
+
35
+ if (sessions.length === 0) {
36
+ return "<pty_list>\nNo active PTY sessions.\n</pty_list>";
37
+ }
38
+
39
+ const lines = ["<pty_list>"];
40
+ for (const session of sessions) {
41
+ const exitInfo = session.exitCode !== undefined ? ` (exit: ${session.exitCode})` : "";
42
+ lines.push(`[${session.id}] ${session.title}`);
43
+ lines.push(` Command: ${session.command} ${session.args.join(" ")}`);
44
+ lines.push(` Status: ${session.status}${exitInfo}`);
45
+ lines.push(` PID: ${session.pid} | Lines: ${session.lineCount} | Workdir: ${session.workdir}`);
46
+ lines.push(` Created: ${session.createdAt.toISOString()}`);
47
+ lines.push("");
48
+ }
49
+ lines.push(`Total: ${sessions.length} session(s)`);
50
+ lines.push("</pty_list>");
51
+
52
+ return lines.join("\n");
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,152 @@
1
+ // src/tools/pty/tools/read.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import type { PTYManager } from "../manager";
4
+
5
+ const DESCRIPTION = `Reads output from a PTY session's buffer.
6
+
7
+ The PTY maintains a rolling buffer of output lines. Use offset and limit to paginate through the output, similar to reading a file.
8
+
9
+ Usage:
10
+ - \`id\`: The PTY session ID (from pty_spawn or pty_list)
11
+ - \`offset\`: Line number to start reading from (0-based, defaults to 0)
12
+ - \`limit\`: Number of lines to read (defaults to 500)
13
+ - \`pattern\`: Regex pattern to filter lines (optional)
14
+ - \`ignoreCase\`: Case-insensitive pattern matching (default: false)
15
+
16
+ Returns:
17
+ - Numbered lines of output (similar to cat -n format)
18
+ - Total line count in the buffer
19
+ - Indicator if more lines are available
20
+
21
+ The buffer stores up to PTY_MAX_BUFFER_LINES (default: 50000) lines. Older lines are discarded when the limit is reached.
22
+
23
+ Pattern Filtering:
24
+ - When \`pattern\` is set, lines are FILTERED FIRST using the regex, then offset/limit apply to the MATCHES
25
+ - Original line numbers are preserved so you can see where matches occurred in the buffer
26
+ - Supports full regex syntax (e.g., "error", "ERROR|WARN", "failed.*connection", etc.)
27
+ - If the pattern is invalid, an error message is returned explaining the issue
28
+ - If no lines match the pattern, a clear message indicates zero matches
29
+
30
+ Tips:
31
+ - To see the latest output, use a high offset or omit offset to read from the start
32
+ - To tail recent output, calculate offset as (totalLines - N) where N is how many recent lines you want
33
+ - Lines longer than 2000 characters are truncated
34
+ - Empty output may mean the process hasn't produced output yet
35
+
36
+ Examples:
37
+ - Read first 100 lines: offset=0, limit=100
38
+ - Read lines 500-600: offset=500, limit=100
39
+ - Read all available: omit both parameters
40
+ - Find errors: pattern="error", ignoreCase=true
41
+ - Find specific log levels: pattern="ERROR|WARN|FATAL"
42
+ - First 10 matches only: pattern="error", limit=10`;
43
+
44
+ const DEFAULT_LIMIT = 500;
45
+ const MAX_LINE_LENGTH = 2000;
46
+
47
+ export function createPtyReadTool(manager: PTYManager) {
48
+ return tool({
49
+ description: DESCRIPTION,
50
+ args: {
51
+ id: tool.schema.string().describe("The PTY session ID (e.g., pty_a1b2c3d4)"),
52
+ offset: tool.schema.number().optional().describe("Line number to start reading from (0-based, defaults to 0)"),
53
+ limit: tool.schema.number().optional().describe("Number of lines to read (defaults to 500)"),
54
+ pattern: tool.schema.string().optional().describe("Regex pattern to filter lines"),
55
+ ignoreCase: tool.schema.boolean().optional().describe("Case-insensitive pattern matching (default: false)"),
56
+ },
57
+ execute: async (args) => {
58
+ const session = manager.get(args.id);
59
+ if (!session) {
60
+ throw new Error(`PTY session '${args.id}' not found. Use pty_list to see active sessions.`);
61
+ }
62
+
63
+ const offset = Math.max(0, args.offset ?? 0);
64
+ const limit = args.limit ?? DEFAULT_LIMIT;
65
+
66
+ if (args.pattern) {
67
+ let regex: RegExp;
68
+ try {
69
+ regex = new RegExp(args.pattern, args.ignoreCase ? "i" : "");
70
+ } catch (e) {
71
+ const error = e instanceof Error ? e.message : String(e);
72
+ throw new Error(`Invalid regex pattern '${args.pattern}': ${error}`);
73
+ }
74
+
75
+ const result = manager.search(args.id, regex, offset, limit);
76
+ if (!result) {
77
+ throw new Error(`PTY session '${args.id}' not found.`);
78
+ }
79
+
80
+ if (result.matches.length === 0) {
81
+ return [
82
+ `<pty_output id="${args.id}" status="${session.status}" pattern="${args.pattern}">`,
83
+ `No lines matched the pattern '${args.pattern}'.`,
84
+ `Total lines in buffer: ${result.totalLines}`,
85
+ `</pty_output>`,
86
+ ].join("\n");
87
+ }
88
+
89
+ const formattedLines = result.matches.map((match) => {
90
+ const lineNum = match.lineNumber.toString().padStart(5, "0");
91
+ const truncatedLine =
92
+ match.text.length > MAX_LINE_LENGTH ? `${match.text.slice(0, MAX_LINE_LENGTH)}...` : match.text;
93
+ return `${lineNum}| ${truncatedLine}`;
94
+ });
95
+
96
+ const output = [
97
+ `<pty_output id="${args.id}" status="${session.status}" pattern="${args.pattern}">`,
98
+ ...formattedLines,
99
+ "",
100
+ ];
101
+
102
+ if (result.hasMore) {
103
+ output.push(
104
+ `(${result.matches.length} of ${result.totalMatches} matches shown. Use offset=${offset + result.matches.length} to see more.)`,
105
+ );
106
+ } else {
107
+ output.push(
108
+ `(${result.totalMatches} match${result.totalMatches === 1 ? "" : "es"} from ${result.totalLines} total lines)`,
109
+ );
110
+ }
111
+ output.push(`</pty_output>`);
112
+
113
+ return output.join("\n");
114
+ }
115
+
116
+ const result = manager.read(args.id, offset, limit);
117
+ if (!result) {
118
+ throw new Error(`PTY session '${args.id}' not found.`);
119
+ }
120
+
121
+ if (result.lines.length === 0) {
122
+ return [
123
+ `<pty_output id="${args.id}" status="${session.status}">`,
124
+ `(No output available - buffer is empty)`,
125
+ `Total lines: ${result.totalLines}`,
126
+ `</pty_output>`,
127
+ ].join("\n");
128
+ }
129
+
130
+ const formattedLines = result.lines.map((line, index) => {
131
+ const lineNum = (result.offset + index + 1).toString().padStart(5, "0");
132
+ const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
133
+ return `${lineNum}| ${truncatedLine}`;
134
+ });
135
+
136
+ const output = [`<pty_output id="${args.id}" status="${session.status}">`, ...formattedLines];
137
+
138
+ if (result.hasMore) {
139
+ output.push("");
140
+ output.push(
141
+ `(Buffer has more lines. Use offset=${result.offset + result.lines.length} to read beyond line ${result.offset + result.lines.length})`,
142
+ );
143
+ } else {
144
+ output.push("");
145
+ output.push(`(End of buffer - total ${result.totalLines} lines)`);
146
+ }
147
+ output.push(`</pty_output>`);
148
+
149
+ return output.join("\n");
150
+ },
151
+ });
152
+ }
@@ -0,0 +1,78 @@
1
+ // src/tools/pty/tools/spawn.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import type { PTYManager } from "../manager";
4
+
5
+ const DESCRIPTION = `Spawns a new interactive PTY (pseudo-terminal) session that runs in the background.
6
+
7
+ Unlike the built-in bash tool which runs commands synchronously and waits for completion, PTY sessions persist and allow you to:
8
+ - Run long-running processes (dev servers, watch modes, etc.)
9
+ - Send interactive input (including Ctrl+C, arrow keys, etc.)
10
+ - Read output at any time
11
+ - Manage multiple concurrent terminal sessions
12
+
13
+ Usage:
14
+ - The \`command\` parameter is required (e.g., "npm", "python", "bash")
15
+ - Use \`args\` to pass arguments to the command (e.g., ["run", "dev"])
16
+ - Use \`workdir\` to set the working directory (defaults to project root)
17
+ - Use \`env\` to set additional environment variables
18
+ - Use \`title\` to give the session a human-readable name
19
+ - Use \`description\` for a clear, concise 5-10 word description (optional)
20
+
21
+ Returns the session info including:
22
+ - \`id\`: Unique identifier (pty_XXXXXXXX) for use with other pty_* tools
23
+ - \`pid\`: Process ID
24
+ - \`status\`: Current status ("running")
25
+
26
+ After spawning, use:
27
+ - \`pty_write\` to send input to the PTY
28
+ - \`pty_read\` to read output from the PTY
29
+ - \`pty_list\` to see all active PTY sessions
30
+ - \`pty_kill\` to terminate the PTY
31
+
32
+ Examples:
33
+ - Start a dev server: command="npm", args=["run", "dev"], title="Dev Server"
34
+ - Start a Python REPL: command="python3", title="Python REPL"
35
+ - Run tests in watch mode: command="npm", args=["test", "--", "--watch"]`;
36
+
37
+ export function createPtySpawnTool(manager: PTYManager) {
38
+ return tool({
39
+ description: DESCRIPTION,
40
+ args: {
41
+ command: tool.schema.string().describe("The command/executable to run"),
42
+ args: tool.schema.array(tool.schema.string()).optional().describe("Arguments to pass to the command"),
43
+ workdir: tool.schema.string().optional().describe("Working directory for the PTY session"),
44
+ env: tool.schema
45
+ .record(tool.schema.string(), tool.schema.string())
46
+ .optional()
47
+ .describe("Additional environment variables"),
48
+ title: tool.schema.string().optional().describe("Human-readable title for the session"),
49
+ description: tool.schema
50
+ .string()
51
+ .optional()
52
+ .describe("Clear, concise description of what this PTY session is for in 5-10 words"),
53
+ },
54
+ execute: async (args, ctx) => {
55
+ const info = manager.spawn({
56
+ command: args.command,
57
+ args: args.args,
58
+ workdir: args.workdir,
59
+ env: args.env,
60
+ title: args.title,
61
+ parentSessionId: ctx.sessionID,
62
+ });
63
+
64
+ const output = [
65
+ `<pty_spawned>`,
66
+ `ID: ${info.id}`,
67
+ `Title: ${info.title}`,
68
+ `Command: ${info.command} ${info.args.join(" ")}`,
69
+ `Workdir: ${info.workdir}`,
70
+ `PID: ${info.pid}`,
71
+ `Status: ${info.status}`,
72
+ `</pty_spawned>`,
73
+ ].join("\n");
74
+
75
+ return output;
76
+ },
77
+ });
78
+ }
@@ -0,0 +1,97 @@
1
+ // src/tools/pty/tools/write.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import type { PTYManager } from "../manager";
4
+
5
+ const DESCRIPTION = `Sends input data to an active PTY session.
6
+
7
+ Use this tool to:
8
+ - Type commands or text into an interactive terminal
9
+ - Send special key sequences (Ctrl+C, Enter, arrow keys, etc.)
10
+ - Respond to prompts in interactive programs
11
+
12
+ Usage:
13
+ - \`id\`: The PTY session ID (from pty_spawn or pty_list)
14
+ - \`data\`: The input to send (text, commands, or escape sequences)
15
+
16
+ Common escape sequences:
17
+ - Enter/newline: "\\n" or "\\r"
18
+ - Ctrl+C (interrupt): "\\x03"
19
+ - Ctrl+D (EOF): "\\x04"
20
+ - Ctrl+Z (suspend): "\\x1a"
21
+ - Tab: "\\t"
22
+ - Arrow Up: "\\x1b[A"
23
+ - Arrow Down: "\\x1b[B"
24
+ - Arrow Right: "\\x1b[C"
25
+ - Arrow Left: "\\x1b[D"
26
+
27
+ Returns success or error message.
28
+
29
+ Examples:
30
+ - Send a command: data="ls -la\\n"
31
+ - Interrupt a process: data="\\x03"
32
+ - Answer a prompt: data="yes\\n"`;
33
+
34
+ /**
35
+ * Parse escape sequences in a string to their actual byte values.
36
+ * Handles: \n, \r, \t, \xNN (hex), \uNNNN (unicode), \\
37
+ */
38
+ function parseEscapeSequences(input: string): string {
39
+ return input.replace(/\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|[nrt0\\])/g, (match, seq: string) => {
40
+ if (seq.startsWith("x")) {
41
+ return String.fromCharCode(parseInt(seq.slice(1), 16));
42
+ }
43
+ if (seq.startsWith("u")) {
44
+ return String.fromCharCode(parseInt(seq.slice(1), 16));
45
+ }
46
+ switch (seq) {
47
+ case "n":
48
+ return "\n";
49
+ case "r":
50
+ return "\r";
51
+ case "t":
52
+ return "\t";
53
+ case "0":
54
+ return "\0";
55
+ case "\\":
56
+ return "\\";
57
+ default:
58
+ return match;
59
+ }
60
+ });
61
+ }
62
+
63
+ export function createPtyWriteTool(manager: PTYManager) {
64
+ return tool({
65
+ description: DESCRIPTION,
66
+ args: {
67
+ id: tool.schema.string().describe("The PTY session ID (e.g., pty_a1b2c3d4)"),
68
+ data: tool.schema.string().describe("The input data to send to the PTY"),
69
+ },
70
+ execute: async (args) => {
71
+ const session = manager.get(args.id);
72
+ if (!session) {
73
+ throw new Error(`PTY session '${args.id}' not found. Use pty_list to see active sessions.`);
74
+ }
75
+
76
+ if (session.status !== "running") {
77
+ throw new Error(`Cannot write to PTY '${args.id}' - session status is '${session.status}'.`);
78
+ }
79
+
80
+ // Parse escape sequences to actual bytes
81
+ const parsedData = parseEscapeSequences(args.data);
82
+
83
+ const success = manager.write(args.id, parsedData);
84
+ if (!success) {
85
+ throw new Error(`Failed to write to PTY '${args.id}'.`);
86
+ }
87
+
88
+ const preview = args.data.length > 50 ? `${args.data.slice(0, 50)}...` : args.data;
89
+ const displayPreview = preview
90
+ .replace(/\x03/g, "^C")
91
+ .replace(/\x04/g, "^D")
92
+ .replace(/\n/g, "\\n")
93
+ .replace(/\r/g, "\\r");
94
+ return `Sent ${parsedData.length} bytes to ${args.id}: "${displayPreview}"`;
95
+ },
96
+ });
97
+ }
@@ -0,0 +1,62 @@
1
+ // src/tools/pty/types.ts
2
+ import type { RingBuffer } from "./buffer";
3
+
4
+ export type PTYStatus = "running" | "exited" | "killed";
5
+
6
+ export interface PTYSession {
7
+ id: string;
8
+ title: string;
9
+ command: string;
10
+ args: string[];
11
+ workdir: string;
12
+ env?: Record<string, string>;
13
+ status: PTYStatus;
14
+ exitCode?: number;
15
+ pid: number;
16
+ createdAt: Date;
17
+ parentSessionId: string;
18
+ buffer: RingBuffer;
19
+ process: import("bun-pty").IPty;
20
+ }
21
+
22
+ export interface PTYSessionInfo {
23
+ id: string;
24
+ title: string;
25
+ command: string;
26
+ args: string[];
27
+ workdir: string;
28
+ status: PTYStatus;
29
+ exitCode?: number;
30
+ pid: number;
31
+ createdAt: Date;
32
+ lineCount: number;
33
+ }
34
+
35
+ export interface SpawnOptions {
36
+ command: string;
37
+ args?: string[];
38
+ workdir?: string;
39
+ env?: Record<string, string>;
40
+ title?: string;
41
+ parentSessionId: string;
42
+ }
43
+
44
+ export interface ReadResult {
45
+ lines: string[];
46
+ totalLines: number;
47
+ offset: number;
48
+ hasMore: boolean;
49
+ }
50
+
51
+ export interface SearchMatch {
52
+ lineNumber: number;
53
+ text: string;
54
+ }
55
+
56
+ export interface SearchResult {
57
+ matches: SearchMatch[];
58
+ totalMatches: number;
59
+ totalLines: number;
60
+ offset: number;
61
+ hasMore: boolean;
62
+ }