micode 0.6.0 → 0.7.1
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/package.json +9 -14
- package/src/agents/artifact-searcher.ts +46 -0
- package/src/agents/brainstormer.ts +145 -0
- package/src/agents/codebase-analyzer.ts +75 -0
- package/src/agents/codebase-locator.ts +71 -0
- package/src/agents/commander.ts +138 -0
- package/src/agents/executor.ts +215 -0
- package/src/agents/implementer.ts +99 -0
- package/src/agents/index.ts +44 -0
- package/src/agents/ledger-creator.ts +113 -0
- package/src/agents/pattern-finder.ts +70 -0
- package/src/agents/planner.ts +230 -0
- package/src/agents/project-initializer.ts +264 -0
- package/src/agents/reviewer.ts +102 -0
- package/src/config-loader.ts +89 -0
- package/src/hooks/artifact-auto-index.ts +111 -0
- package/src/hooks/auto-clear-ledger.ts +230 -0
- package/src/hooks/auto-compact.ts +241 -0
- package/src/hooks/comment-checker.ts +120 -0
- package/src/hooks/context-injector.ts +163 -0
- package/src/hooks/context-window-monitor.ts +106 -0
- package/src/hooks/file-ops-tracker.ts +96 -0
- package/src/hooks/ledger-loader.ts +78 -0
- package/src/hooks/preemptive-compaction.ts +183 -0
- package/src/hooks/session-recovery.ts +258 -0
- package/src/hooks/token-aware-truncation.ts +189 -0
- package/src/index.ts +258 -0
- package/src/tools/artifact-index/index.ts +269 -0
- package/src/tools/artifact-index/schema.sql +44 -0
- package/src/tools/artifact-search.ts +49 -0
- package/src/tools/ast-grep/index.ts +189 -0
- package/src/tools/background-task/manager.ts +374 -0
- package/src/tools/background-task/tools.ts +145 -0
- package/src/tools/background-task/types.ts +68 -0
- package/src/tools/btca/index.ts +82 -0
- package/src/tools/look-at.ts +210 -0
- package/src/tools/pty/buffer.ts +49 -0
- package/src/tools/pty/index.ts +34 -0
- package/src/tools/pty/manager.ts +159 -0
- package/src/tools/pty/tools/kill.ts +68 -0
- package/src/tools/pty/tools/list.ts +55 -0
- package/src/tools/pty/tools/read.ts +152 -0
- package/src/tools/pty/tools/spawn.ts +78 -0
- package/src/tools/pty/tools/write.ts +97 -0
- package/src/tools/pty/types.ts +62 -0
- package/src/utils/model-limits.ts +36 -0
- package/dist/agents/artifact-searcher.d.ts +0 -2
- package/dist/agents/brainstormer.d.ts +0 -2
- package/dist/agents/codebase-analyzer.d.ts +0 -2
- package/dist/agents/codebase-locator.d.ts +0 -2
- package/dist/agents/commander.d.ts +0 -3
- package/dist/agents/executor.d.ts +0 -2
- package/dist/agents/implementer.d.ts +0 -2
- package/dist/agents/index.d.ts +0 -15
- package/dist/agents/ledger-creator.d.ts +0 -2
- package/dist/agents/pattern-finder.d.ts +0 -2
- package/dist/agents/planner.d.ts +0 -2
- package/dist/agents/project-initializer.d.ts +0 -2
- package/dist/agents/reviewer.d.ts +0 -2
- package/dist/config-loader.d.ts +0 -20
- package/dist/hooks/artifact-auto-index.d.ts +0 -19
- package/dist/hooks/auto-clear-ledger.d.ts +0 -11
- package/dist/hooks/auto-compact.d.ts +0 -9
- package/dist/hooks/comment-checker.d.ts +0 -9
- package/dist/hooks/context-injector.d.ts +0 -15
- package/dist/hooks/context-window-monitor.d.ts +0 -15
- package/dist/hooks/file-ops-tracker.d.ts +0 -26
- package/dist/hooks/ledger-loader.d.ts +0 -16
- package/dist/hooks/preemptive-compaction.d.ts +0 -9
- package/dist/hooks/session-recovery.d.ts +0 -9
- package/dist/hooks/token-aware-truncation.d.ts +0 -15
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -16267
- package/dist/tools/artifact-index/index.d.ts +0 -38
- package/dist/tools/artifact-search.d.ts +0 -17
- package/dist/tools/ast-grep/index.d.ts +0 -88
- package/dist/tools/background-task/manager.d.ts +0 -27
- package/dist/tools/background-task/tools.d.ts +0 -41
- package/dist/tools/background-task/types.d.ts +0 -53
- package/dist/tools/btca/index.d.ts +0 -19
- package/dist/tools/look-at.d.ts +0 -11
- package/dist/utils/model-limits.d.ts +0 -7
- /package/{dist/tools/background-task/index.d.ts → src/tools/background-task/index.ts} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { extname, basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
// File size threshold for triggering extraction (100KB)
|
|
6
|
+
const LARGE_FILE_THRESHOLD = 100 * 1024;
|
|
7
|
+
|
|
8
|
+
// Max lines to return without extraction
|
|
9
|
+
const MAX_LINES_WITHOUT_EXTRACT = 200;
|
|
10
|
+
|
|
11
|
+
// Supported file types for smart extraction
|
|
12
|
+
const EXTRACTABLE_EXTENSIONS = [
|
|
13
|
+
".ts",
|
|
14
|
+
".tsx",
|
|
15
|
+
".js",
|
|
16
|
+
".jsx",
|
|
17
|
+
".py",
|
|
18
|
+
".go",
|
|
19
|
+
".rs",
|
|
20
|
+
".java",
|
|
21
|
+
".md",
|
|
22
|
+
".json",
|
|
23
|
+
".yaml",
|
|
24
|
+
".yml",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function extractStructure(content: string, ext: string): string {
|
|
28
|
+
const lines = content.split("\n");
|
|
29
|
+
|
|
30
|
+
switch (ext) {
|
|
31
|
+
case ".ts":
|
|
32
|
+
case ".tsx":
|
|
33
|
+
case ".js":
|
|
34
|
+
case ".jsx":
|
|
35
|
+
return extractTypeScriptStructure(lines);
|
|
36
|
+
case ".py":
|
|
37
|
+
return extractPythonStructure(lines);
|
|
38
|
+
case ".go":
|
|
39
|
+
return extractGoStructure(lines);
|
|
40
|
+
case ".md":
|
|
41
|
+
return extractMarkdownStructure(lines);
|
|
42
|
+
case ".json":
|
|
43
|
+
return extractJsonStructure(content);
|
|
44
|
+
case ".yaml":
|
|
45
|
+
case ".yml":
|
|
46
|
+
return extractYamlStructure(lines);
|
|
47
|
+
default:
|
|
48
|
+
return extractGenericStructure(lines);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractTypeScriptStructure(lines: string[]): string {
|
|
53
|
+
const output: string[] = ["## Structure\n"];
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
const line = lines[i];
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
|
|
59
|
+
// Capture exports, classes, interfaces, types, functions
|
|
60
|
+
if (
|
|
61
|
+
trimmed.startsWith("export ") ||
|
|
62
|
+
trimmed.startsWith("class ") ||
|
|
63
|
+
trimmed.startsWith("interface ") ||
|
|
64
|
+
trimmed.startsWith("type ") ||
|
|
65
|
+
trimmed.startsWith("function ") ||
|
|
66
|
+
trimmed.startsWith("const ") ||
|
|
67
|
+
trimmed.startsWith("async function ")
|
|
68
|
+
) {
|
|
69
|
+
// Get the signature (first line only for multi-line)
|
|
70
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
71
|
+
output.push(`Line ${i + 1}: ${signature}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return output.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractPythonStructure(lines: string[]): string {
|
|
79
|
+
const output: string[] = ["## Structure\n"];
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const line = lines[i];
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
|
|
85
|
+
// Capture classes, functions, decorators
|
|
86
|
+
if (
|
|
87
|
+
trimmed.startsWith("class ") ||
|
|
88
|
+
trimmed.startsWith("def ") ||
|
|
89
|
+
trimmed.startsWith("async def ") ||
|
|
90
|
+
trimmed.startsWith("@")
|
|
91
|
+
) {
|
|
92
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
93
|
+
output.push(`Line ${i + 1}: ${signature}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return output.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractGoStructure(lines: string[]): string {
|
|
101
|
+
const output: string[] = ["## Structure\n"];
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < lines.length; i++) {
|
|
104
|
+
const line = lines[i];
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
|
|
107
|
+
// Capture types, functions, methods
|
|
108
|
+
if (trimmed.startsWith("type ") || trimmed.startsWith("func ") || trimmed.startsWith("package ")) {
|
|
109
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
110
|
+
output.push(`Line ${i + 1}: ${signature}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return output.join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractMarkdownStructure(lines: string[]): string {
|
|
118
|
+
const output: string[] = ["## Outline\n"];
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
const line = lines[i];
|
|
122
|
+
|
|
123
|
+
// Capture headings
|
|
124
|
+
if (line.startsWith("#")) {
|
|
125
|
+
output.push(`Line ${i + 1}: ${line}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return output.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractJsonStructure(content: string): string {
|
|
133
|
+
try {
|
|
134
|
+
const obj = JSON.parse(content);
|
|
135
|
+
const keys = Object.keys(obj);
|
|
136
|
+
return `## Top-level keys (${keys.length})\n\n${keys.slice(0, 50).join(", ")}${keys.length > 50 ? "..." : ""}`;
|
|
137
|
+
} catch {
|
|
138
|
+
return "## Invalid JSON";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractYamlStructure(lines: string[]): string {
|
|
143
|
+
const output: string[] = ["## Top-level keys\n"];
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < lines.length; i++) {
|
|
146
|
+
const line = lines[i];
|
|
147
|
+
|
|
148
|
+
// Top-level keys (no indentation)
|
|
149
|
+
if (line.match(/^[a-zA-Z_][a-zA-Z0-9_]*:/)) {
|
|
150
|
+
output.push(`Line ${i + 1}: ${line}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return output.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function extractGenericStructure(lines: string[]): string {
|
|
158
|
+
// For unknown files, just show first/last lines and line count
|
|
159
|
+
const total = lines.length;
|
|
160
|
+
const preview = lines.slice(0, 10).join("\n");
|
|
161
|
+
const tail = lines.slice(-5).join("\n");
|
|
162
|
+
|
|
163
|
+
return `## File Preview (${total} lines)\n\n### First 10 lines:\n${preview}\n\n### Last 5 lines:\n${tail}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const look_at = tool({
|
|
167
|
+
description: `Extract key information from a file to save context tokens.
|
|
168
|
+
For large files, returns structure/outline instead of full content.
|
|
169
|
+
Use when you need to understand a file without loading all content.
|
|
170
|
+
Ideal for: large files, getting file structure, quick overview.`,
|
|
171
|
+
args: {
|
|
172
|
+
filePath: tool.schema.string().describe("Path to the file"),
|
|
173
|
+
extract: tool.schema
|
|
174
|
+
.string()
|
|
175
|
+
.optional()
|
|
176
|
+
.describe("What to extract: 'structure', 'imports', 'exports', 'all' (default: auto)"),
|
|
177
|
+
},
|
|
178
|
+
execute: async (args) => {
|
|
179
|
+
try {
|
|
180
|
+
const stats = statSync(args.filePath);
|
|
181
|
+
const ext = extname(args.filePath).toLowerCase();
|
|
182
|
+
const name = basename(args.filePath);
|
|
183
|
+
|
|
184
|
+
// Read file
|
|
185
|
+
const content = readFileSync(args.filePath, "utf-8");
|
|
186
|
+
const lines = content.split("\n");
|
|
187
|
+
|
|
188
|
+
// For small files, return full content
|
|
189
|
+
if (stats.size < LARGE_FILE_THRESHOLD && lines.length <= MAX_LINES_WITHOUT_EXTRACT) {
|
|
190
|
+
return `## ${name} (${lines.length} lines)\n\n${content}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// For large files, extract structure
|
|
194
|
+
let output = `## ${name}\n`;
|
|
195
|
+
output += `**Size**: ${Math.round(stats.size / 1024)}KB | **Lines**: ${lines.length}\n\n`;
|
|
196
|
+
|
|
197
|
+
if (EXTRACTABLE_EXTENSIONS.includes(ext)) {
|
|
198
|
+
output += extractStructure(content, ext);
|
|
199
|
+
} else {
|
|
200
|
+
output += extractGenericStructure(lines);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
output += `\n\n---\n*Use Read tool with line offset/limit for specific sections*`;
|
|
204
|
+
|
|
205
|
+
return output;
|
|
206
|
+
} catch (e) {
|
|
207
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/tools/pty/buffer.ts
|
|
2
|
+
import type { SearchMatch } from "./types";
|
|
3
|
+
|
|
4
|
+
const parsed = parseInt(process.env.PTY_MAX_BUFFER_LINES || "50000", 10);
|
|
5
|
+
const DEFAULT_MAX_LINES = isNaN(parsed) ? 50000 : parsed;
|
|
6
|
+
|
|
7
|
+
export class RingBuffer {
|
|
8
|
+
private lines: string[] = [];
|
|
9
|
+
private maxLines: number;
|
|
10
|
+
|
|
11
|
+
constructor(maxLines: number = DEFAULT_MAX_LINES) {
|
|
12
|
+
this.maxLines = maxLines;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
append(data: string): void {
|
|
16
|
+
const newLines = data.split("\n");
|
|
17
|
+
for (const line of newLines) {
|
|
18
|
+
this.lines.push(line);
|
|
19
|
+
if (this.lines.length > this.maxLines) {
|
|
20
|
+
this.lines.shift();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
read(offset: number = 0, limit?: number): string[] {
|
|
26
|
+
const start = Math.max(0, offset);
|
|
27
|
+
const end = limit !== undefined ? start + limit : this.lines.length;
|
|
28
|
+
return this.lines.slice(start, end);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
search(pattern: RegExp): SearchMatch[] {
|
|
32
|
+
const matches: SearchMatch[] = [];
|
|
33
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
34
|
+
const line = this.lines[i];
|
|
35
|
+
if (line !== undefined && pattern.test(line)) {
|
|
36
|
+
matches.push({ lineNumber: i + 1, text: line });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return matches;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get length(): number {
|
|
43
|
+
return this.lines.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clear(): void {
|
|
47
|
+
this.lines = [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/tools/pty/index.ts
|
|
2
|
+
export { PTYManager } from "./manager";
|
|
3
|
+
export { RingBuffer } from "./buffer";
|
|
4
|
+
export { createPtySpawnTool } from "./tools/spawn";
|
|
5
|
+
export { createPtyWriteTool } from "./tools/write";
|
|
6
|
+
export { createPtyReadTool } from "./tools/read";
|
|
7
|
+
export { createPtyListTool } from "./tools/list";
|
|
8
|
+
export { createPtyKillTool } from "./tools/kill";
|
|
9
|
+
export type {
|
|
10
|
+
PTYSession,
|
|
11
|
+
PTYSessionInfo,
|
|
12
|
+
PTYStatus,
|
|
13
|
+
SpawnOptions,
|
|
14
|
+
ReadResult,
|
|
15
|
+
SearchMatch,
|
|
16
|
+
SearchResult,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
import type { PTYManager } from "./manager";
|
|
20
|
+
import { createPtySpawnTool } from "./tools/spawn";
|
|
21
|
+
import { createPtyWriteTool } from "./tools/write";
|
|
22
|
+
import { createPtyReadTool } from "./tools/read";
|
|
23
|
+
import { createPtyListTool } from "./tools/list";
|
|
24
|
+
import { createPtyKillTool } from "./tools/kill";
|
|
25
|
+
|
|
26
|
+
export function createPtyTools(manager: PTYManager) {
|
|
27
|
+
return {
|
|
28
|
+
pty_spawn: createPtySpawnTool(manager),
|
|
29
|
+
pty_write: createPtyWriteTool(manager),
|
|
30
|
+
pty_read: createPtyReadTool(manager),
|
|
31
|
+
pty_list: createPtyListTool(manager),
|
|
32
|
+
pty_kill: createPtyKillTool(manager),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -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
|
+
}
|