@agi-cli/sdk 0.1.89 → 0.1.91
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/package.json +14 -9
- package/src/core/src/index.ts +14 -0
- package/src/core/src/terminals/bun-pty.ts +13 -0
- package/src/core/src/terminals/circular-buffer.ts +30 -0
- package/src/core/src/terminals/ensure-bun-pty.ts +94 -0
- package/src/core/src/terminals/index.ts +8 -0
- package/src/core/src/terminals/manager.ts +154 -0
- package/src/core/src/terminals/terminal.ts +124 -0
- package/src/core/src/tools/builtin/terminal.ts +299 -0
- package/src/core/src/tools/builtin/terminal.txt +93 -0
- package/src/core/src/tools/loader.ts +17 -0
- package/src/core/src/utils/ansi.ts +27 -0
- package/src/index.ts +11 -0
- package/src/prompts/src/agents/build.txt +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.91",
|
|
4
4
|
"description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
|
|
5
5
|
"author": "ntishxyz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
"import": "./src/core/src/tools/builtin/websearch.ts",
|
|
66
66
|
"types": "./src/core/src/tools/builtin/websearch.ts"
|
|
67
67
|
},
|
|
68
|
+
"./tools/builtin/terminal": {
|
|
69
|
+
"import": "./src/core/src/tools/builtin/terminal.ts",
|
|
70
|
+
"types": "./src/core/src/tools/builtin/terminal.ts"
|
|
71
|
+
},
|
|
68
72
|
"./tools/error": {
|
|
69
73
|
"import": "./src/core/src/tools/error.ts",
|
|
70
74
|
"types": "./src/core/src/tools/error.ts"
|
|
@@ -82,18 +86,19 @@
|
|
|
82
86
|
"typecheck": "tsc --noEmit"
|
|
83
87
|
},
|
|
84
88
|
"dependencies": {
|
|
85
|
-
"@openauthjs/openauth": "^0.4.3",
|
|
86
|
-
"opencode-anthropic-auth": "^0.0.2",
|
|
87
|
-
"ai": "^5.0.43",
|
|
88
89
|
"@ai-sdk/anthropic": "^2.0.16",
|
|
89
|
-
"@ai-sdk/openai": "^2.0.30",
|
|
90
90
|
"@ai-sdk/google": "^2.0.14",
|
|
91
|
-
"@
|
|
91
|
+
"@ai-sdk/openai": "^2.0.30",
|
|
92
92
|
"@ai-sdk/openai-compatible": "^1.0.18",
|
|
93
|
-
"
|
|
94
|
-
"
|
|
93
|
+
"@openauthjs/openauth": "^0.4.3",
|
|
94
|
+
"@openrouter/ai-sdk-provider": "^1.2.0",
|
|
95
|
+
"ai": "^5.0.43",
|
|
96
|
+
"bun-pty": "^0.3.2",
|
|
97
|
+
"diff": "^8.0.2",
|
|
95
98
|
"fast-glob": "^3.3.2",
|
|
96
|
-
"
|
|
99
|
+
"hono": "^4.9.7",
|
|
100
|
+
"opencode-anthropic-auth": "^0.0.2",
|
|
101
|
+
"zod": "^4.1.8"
|
|
97
102
|
},
|
|
98
103
|
"devDependencies": {
|
|
99
104
|
"@types/bun": "latest",
|
package/src/core/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type { ProviderId, ModelInfo } from '../../types/src/index.ts';
|
|
|
31
31
|
// =======================
|
|
32
32
|
export { discoverProjectTools } from './tools/loader';
|
|
33
33
|
export type { DiscoveredTool } from './tools/loader';
|
|
34
|
+
export { setTerminalManager, getTerminalManager } from './tools/loader';
|
|
34
35
|
|
|
35
36
|
// Tool error handling utilities
|
|
36
37
|
export {
|
|
@@ -48,6 +49,19 @@ export type {
|
|
|
48
49
|
// Re-export builtin tools for direct access
|
|
49
50
|
export { buildFsTools } from './tools/builtin/fs/index';
|
|
50
51
|
export { buildGitTools } from './tools/builtin/git';
|
|
52
|
+
export { buildTerminalTool } from './tools/builtin/terminal';
|
|
53
|
+
|
|
54
|
+
// =======================
|
|
55
|
+
// Terminals
|
|
56
|
+
// =======================
|
|
57
|
+
export { TerminalManager } from './terminals/index';
|
|
58
|
+
export type {
|
|
59
|
+
Terminal,
|
|
60
|
+
TerminalOptions,
|
|
61
|
+
TerminalStatus,
|
|
62
|
+
TerminalCreator,
|
|
63
|
+
CreateTerminalOptions,
|
|
64
|
+
} from './terminals/index';
|
|
51
65
|
|
|
52
66
|
// =======================
|
|
53
67
|
// Streaming & Artifacts
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ensureBunPtyLibrary } from './ensure-bun-pty.ts';
|
|
2
|
+
|
|
3
|
+
await ensureBunPtyLibrary();
|
|
4
|
+
|
|
5
|
+
const bunPty = await import('bun-pty');
|
|
6
|
+
|
|
7
|
+
export const spawn = bunPty.spawn;
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
IPty,
|
|
11
|
+
IPtyForkOptions as PtyOptions,
|
|
12
|
+
IExitEvent,
|
|
13
|
+
} from 'bun-pty';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class CircularBuffer {
|
|
2
|
+
private buffer: string[] = [];
|
|
3
|
+
private maxSize: number;
|
|
4
|
+
|
|
5
|
+
constructor(maxSize = 500) {
|
|
6
|
+
this.maxSize = maxSize;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
push(line: string): void {
|
|
10
|
+
this.buffer.push(line);
|
|
11
|
+
if (this.buffer.length > this.maxSize) {
|
|
12
|
+
this.buffer.shift();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
read(lines?: number): string[] {
|
|
17
|
+
if (lines === undefined) {
|
|
18
|
+
return [...this.buffer];
|
|
19
|
+
}
|
|
20
|
+
return this.buffer.slice(-lines);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clear(): void {
|
|
24
|
+
this.buffer = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get length(): number {
|
|
28
|
+
return this.buffer.length;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
function resolveLibraryFilename(): string {
|
|
7
|
+
const platform = process.platform;
|
|
8
|
+
const arch = process.arch;
|
|
9
|
+
|
|
10
|
+
if (platform === 'darwin') {
|
|
11
|
+
return arch === 'arm64' ? 'librust_pty_arm64.dylib' : 'librust_pty.dylib';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
return 'rust_pty.dll';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return arch === 'arm64' ? 'librust_pty_arm64.so' : 'librust_pty.so';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tryUseExistingPath(path?: string | null): string | null {
|
|
22
|
+
if (!path) return null;
|
|
23
|
+
if (existsSync(path)) {
|
|
24
|
+
process.env.BUN_PTY_LIB = path;
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readFromEmbedded(
|
|
31
|
+
url: URL,
|
|
32
|
+
targetPath: string,
|
|
33
|
+
): Promise<string | null> {
|
|
34
|
+
const file = Bun.file(url);
|
|
35
|
+
if (!(await file.exists())) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const dir = dirname(targetPath);
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
await Bun.write(targetPath, file);
|
|
42
|
+
process.env.BUN_PTY_LIB = targetPath;
|
|
43
|
+
return targetPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function ensureBunPtyLibrary(): Promise<string | null> {
|
|
47
|
+
const already = tryUseExistingPath(process.env.BUN_PTY_LIB);
|
|
48
|
+
if (already) return already;
|
|
49
|
+
|
|
50
|
+
const filename = resolveLibraryFilename();
|
|
51
|
+
const candidates: string[] = [];
|
|
52
|
+
|
|
53
|
+
let pkgUrl: string | null = null;
|
|
54
|
+
try {
|
|
55
|
+
pkgUrl = await import.meta.resolve('bun-pty/package.json');
|
|
56
|
+
const pkgPath = fileURLToPath(pkgUrl);
|
|
57
|
+
const pkgDir = dirname(pkgPath);
|
|
58
|
+
candidates.push(
|
|
59
|
+
join(pkgDir, 'rust-pty', 'target', 'release', filename),
|
|
60
|
+
join(pkgDir, '..', 'bun-pty', 'rust-pty', 'target', 'release', filename),
|
|
61
|
+
);
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore resolution failures
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
candidates.push(
|
|
67
|
+
join(
|
|
68
|
+
process.cwd(),
|
|
69
|
+
'node_modules',
|
|
70
|
+
'bun-pty',
|
|
71
|
+
'rust-pty',
|
|
72
|
+
'target',
|
|
73
|
+
'release',
|
|
74
|
+
filename,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
const path = tryUseExistingPath(candidate);
|
|
80
|
+
if (path) return path;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (pkgUrl) {
|
|
84
|
+
const embeddedUrl = new URL(
|
|
85
|
+
`./rust-pty/target/release/${filename}`,
|
|
86
|
+
pkgUrl,
|
|
87
|
+
);
|
|
88
|
+
const tmpPath = join(tmpdir(), 'agi-cli', 'bun-pty', filename);
|
|
89
|
+
const fromEmbedded = await readFromEmbedded(embeddedUrl, tmpPath);
|
|
90
|
+
if (fromEmbedded) return fromEmbedded;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { spawn as spawnPty } from './bun-pty.ts';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { Terminal } from './terminal.ts';
|
|
4
|
+
import type { PtyOptions } from './bun-pty.ts';
|
|
5
|
+
|
|
6
|
+
const MAX_TERMINALS = 10;
|
|
7
|
+
const CLEANUP_DELAY_MS = 5 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export interface CreateTerminalOptions {
|
|
10
|
+
command: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
cwd: string;
|
|
13
|
+
purpose: string;
|
|
14
|
+
createdBy: 'user' | 'llm';
|
|
15
|
+
title?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class TerminalManager {
|
|
19
|
+
private terminals = new Map<string, Terminal>();
|
|
20
|
+
private cleanupTimers = new Map<string, NodeJS.Timeout>();
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
process.on('SIGTERM', () => this.killAll());
|
|
24
|
+
process.on('SIGINT', () => this.killAll());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
create(options: CreateTerminalOptions): Terminal {
|
|
28
|
+
if (this.terminals.size >= MAX_TERMINALS) {
|
|
29
|
+
throw new Error(`Maximum ${MAX_TERMINALS} terminals reached`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const id = this.generateId();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
console.log('[TerminalManager] Creating terminal:', {
|
|
36
|
+
id,
|
|
37
|
+
command: options.command,
|
|
38
|
+
args: options.args,
|
|
39
|
+
cwd: options.cwd,
|
|
40
|
+
purpose: options.purpose,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const ptyOptions: PtyOptions = {
|
|
44
|
+
name: 'xterm-256color',
|
|
45
|
+
cols: 80,
|
|
46
|
+
rows: 30,
|
|
47
|
+
cwd: options.cwd,
|
|
48
|
+
env: process.env as Record<string, string>,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const pty = spawnPty(options.command, options.args || [], ptyOptions);
|
|
52
|
+
|
|
53
|
+
console.log('[TerminalManager] PTY created successfully:', pty.pid);
|
|
54
|
+
|
|
55
|
+
const terminal = new Terminal(id, pty, options);
|
|
56
|
+
|
|
57
|
+
terminal.onExit((_exitCode) => {
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
this.delete(id);
|
|
60
|
+
}, CLEANUP_DELAY_MS);
|
|
61
|
+
|
|
62
|
+
this.cleanupTimers.set(id, timer);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
this.terminals.set(id, terminal);
|
|
66
|
+
|
|
67
|
+
console.log('[TerminalManager] Terminal added to map');
|
|
68
|
+
|
|
69
|
+
return terminal;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('[TerminalManager] Failed to create terminal:', error);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get(id: string): Terminal | undefined {
|
|
77
|
+
return this.terminals.get(id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
list(): Terminal[] {
|
|
81
|
+
return Array.from(this.terminals.values());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async kill(id: string): Promise<void> {
|
|
85
|
+
const terminal = this.terminals.get(id);
|
|
86
|
+
if (!terminal) {
|
|
87
|
+
throw new Error(`Terminal ${id} not found`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
terminal.kill();
|
|
91
|
+
|
|
92
|
+
await new Promise<void>((resolve) => {
|
|
93
|
+
if (terminal.status === 'exited') {
|
|
94
|
+
resolve();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const exitHandler = () => {
|
|
99
|
+
terminal.removeExitListener(exitHandler);
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
terminal.onExit(exitHandler);
|
|
104
|
+
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
terminal.removeExitListener(exitHandler);
|
|
107
|
+
resolve();
|
|
108
|
+
}, 5000);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.delete(id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async killAll(): Promise<void> {
|
|
115
|
+
const killPromises = Array.from(this.terminals.keys()).map((id) =>
|
|
116
|
+
this.kill(id).catch((err) =>
|
|
117
|
+
console.error(`Failed to kill terminal ${id}:`, err),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await Promise.all(killPromises);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
delete(id: string): boolean {
|
|
125
|
+
const timer = this.cleanupTimers.get(id);
|
|
126
|
+
if (timer) {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
this.cleanupTimers.delete(id);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this.terminals.delete(id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private generateId(): string {
|
|
135
|
+
return `term-${randomBytes(8).toString('hex')}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getContext(): string {
|
|
139
|
+
const terminals = this.list();
|
|
140
|
+
|
|
141
|
+
if (terminals.length === 0) {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const summary = terminals
|
|
146
|
+
.map(
|
|
147
|
+
(t) =>
|
|
148
|
+
`- [${t.id}] ${t.purpose} (${t.status}, ${t.createdBy}, pid: ${t.pid})`,
|
|
149
|
+
)
|
|
150
|
+
.join('\n');
|
|
151
|
+
|
|
152
|
+
return `\n\n## Active Terminals (${terminals.length}):\n${summary}\n\nYou can read from any terminal using the 'terminal' tool with operation: 'read'.`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { IPty } from './bun-pty.ts';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { CircularBuffer } from './circular-buffer.ts';
|
|
4
|
+
|
|
5
|
+
export type TerminalStatus = 'running' | 'exited';
|
|
6
|
+
export type TerminalCreator = 'user' | 'llm';
|
|
7
|
+
|
|
8
|
+
export interface TerminalOptions {
|
|
9
|
+
command: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
cwd: string;
|
|
12
|
+
purpose: string;
|
|
13
|
+
createdBy: TerminalCreator;
|
|
14
|
+
title?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Terminal {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly pty: IPty;
|
|
20
|
+
readonly command: string;
|
|
21
|
+
readonly args: string[];
|
|
22
|
+
readonly cwd: string;
|
|
23
|
+
readonly purpose: string;
|
|
24
|
+
readonly createdBy: TerminalCreator;
|
|
25
|
+
readonly createdAt: Date;
|
|
26
|
+
|
|
27
|
+
private buffer: CircularBuffer;
|
|
28
|
+
private _status: TerminalStatus = 'running';
|
|
29
|
+
private _exitCode?: number;
|
|
30
|
+
private _title?: string;
|
|
31
|
+
private dataEmitter = new EventEmitter();
|
|
32
|
+
private exitEmitter = new EventEmitter();
|
|
33
|
+
|
|
34
|
+
constructor(id: string, pty: IPty, options: TerminalOptions) {
|
|
35
|
+
this.id = id;
|
|
36
|
+
this.pty = pty;
|
|
37
|
+
this.command = options.command;
|
|
38
|
+
this.args = options.args || [];
|
|
39
|
+
this.cwd = options.cwd;
|
|
40
|
+
this.purpose = options.purpose;
|
|
41
|
+
this.createdBy = options.createdBy;
|
|
42
|
+
this._title = options.title;
|
|
43
|
+
this.createdAt = new Date();
|
|
44
|
+
this.buffer = new CircularBuffer(500);
|
|
45
|
+
|
|
46
|
+
this.pty.onData((data) => {
|
|
47
|
+
// Store in buffer for history
|
|
48
|
+
this.buffer.push(data);
|
|
49
|
+
// Emit raw data - terminals need control chars, ANSI codes, etc.
|
|
50
|
+
this.dataEmitter.emit('data', data);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.pty.onExit(({ exitCode }) => {
|
|
54
|
+
this._status = 'exited';
|
|
55
|
+
this._exitCode = exitCode;
|
|
56
|
+
this.exitEmitter.emit('exit', exitCode);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get pid(): number {
|
|
61
|
+
return this.pty.pid;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get status(): TerminalStatus {
|
|
65
|
+
return this._status;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get exitCode(): number | undefined {
|
|
69
|
+
return this._exitCode;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get title(): string {
|
|
73
|
+
return this._title || this.purpose;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
set title(value: string) {
|
|
77
|
+
this._title = value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
read(lines?: number): string[] {
|
|
81
|
+
return this.buffer.read(lines);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
write(input: string): void {
|
|
85
|
+
this.pty.write(input);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
kill(signal?: string): void {
|
|
89
|
+
this.pty.kill(signal);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onData(callback: (line: string) => void): void {
|
|
93
|
+
this.dataEmitter.on('data', callback);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onExit(callback: (exitCode: number) => void): void {
|
|
97
|
+
this.exitEmitter.on('exit', callback);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
removeDataListener(callback: (line: string) => void): void {
|
|
101
|
+
this.dataEmitter.off('data', callback);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
removeExitListener(callback: (exitCode: number) => void): void {
|
|
105
|
+
this.exitEmitter.off('exit', callback);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
toJSON() {
|
|
109
|
+
return {
|
|
110
|
+
id: this.id,
|
|
111
|
+
pid: this.pid,
|
|
112
|
+
command: this.command,
|
|
113
|
+
args: this.args,
|
|
114
|
+
cwd: this.cwd,
|
|
115
|
+
purpose: this.purpose,
|
|
116
|
+
createdBy: this.createdBy,
|
|
117
|
+
title: this.title,
|
|
118
|
+
status: this.status,
|
|
119
|
+
exitCode: this.exitCode,
|
|
120
|
+
createdAt: this.createdAt,
|
|
121
|
+
uptime: Date.now() - this.createdAt.getTime(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import DESCRIPTION from './terminal.txt' with { type: 'text' };
|
|
4
|
+
import { createToolError } from '../error.ts';
|
|
5
|
+
import type { TerminalManager } from '../../terminals/index.ts';
|
|
6
|
+
import type { TerminalStatus } from '../../terminals/terminal.ts';
|
|
7
|
+
import { normalizeTerminalLine } from '../../utils/ansi.ts';
|
|
8
|
+
|
|
9
|
+
function shellQuote(segment: string): string {
|
|
10
|
+
if (/^[a-zA-Z0-9._-]+$/.test(segment)) {
|
|
11
|
+
return segment;
|
|
12
|
+
}
|
|
13
|
+
return `'${segment.replace(/'/g, `'\\''`)}'`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatShellCommand(parts: string[]): string {
|
|
17
|
+
return parts.map(shellQuote).join(' ');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizePath(p: string) {
|
|
21
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
22
|
+
const stack: string[] = [];
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (!part || part === '.') continue;
|
|
25
|
+
if (part === '..') stack.pop();
|
|
26
|
+
else stack.push(part);
|
|
27
|
+
}
|
|
28
|
+
return `/${stack.join('/')}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveSafePath(projectRoot: string, p: string) {
|
|
32
|
+
const root = normalizePath(projectRoot);
|
|
33
|
+
const abs = normalizePath(`${root}/${p || '.'}`);
|
|
34
|
+
if (!(abs === root || abs.startsWith(`${root}/`))) {
|
|
35
|
+
throw new Error(`cwd escapes project root: ${p}`);
|
|
36
|
+
}
|
|
37
|
+
return abs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildTerminalTool(
|
|
41
|
+
projectRoot: string,
|
|
42
|
+
terminalManager: TerminalManager,
|
|
43
|
+
): {
|
|
44
|
+
name: string;
|
|
45
|
+
tool: Tool;
|
|
46
|
+
} {
|
|
47
|
+
const terminal = tool({
|
|
48
|
+
description: DESCRIPTION,
|
|
49
|
+
inputSchema: z.object({
|
|
50
|
+
operation: z
|
|
51
|
+
.enum(['start', 'read', 'write', 'interrupt', 'list', 'kill'])
|
|
52
|
+
.describe('Operation to perform'),
|
|
53
|
+
|
|
54
|
+
command: z.string().optional().describe('For start: Command to run'),
|
|
55
|
+
args: z
|
|
56
|
+
.array(z.string())
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('For start: Command arguments'),
|
|
59
|
+
shell: z
|
|
60
|
+
.boolean()
|
|
61
|
+
.default(true)
|
|
62
|
+
.describe(
|
|
63
|
+
'For start: Launch inside interactive shell and optionally run command',
|
|
64
|
+
),
|
|
65
|
+
purpose: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('For start: Description of what this terminal is for'),
|
|
69
|
+
title: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe(
|
|
73
|
+
'For start: Short name shown in the UI (defaults to purpose)',
|
|
74
|
+
),
|
|
75
|
+
cwd: z
|
|
76
|
+
.string()
|
|
77
|
+
.default('.')
|
|
78
|
+
.describe('For start: Working directory relative to project root'),
|
|
79
|
+
|
|
80
|
+
terminalId: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('For read/write/kill: Terminal ID'),
|
|
84
|
+
|
|
85
|
+
lines: z
|
|
86
|
+
.number()
|
|
87
|
+
.default(100)
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('For read: Number of lines to read from end'),
|
|
90
|
+
raw: z
|
|
91
|
+
.boolean()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe(
|
|
94
|
+
'For read: Include raw output with ANSI escape sequences (default false)',
|
|
95
|
+
),
|
|
96
|
+
|
|
97
|
+
input: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('For write: String to write to stdin'),
|
|
101
|
+
}),
|
|
102
|
+
execute: async (params) => {
|
|
103
|
+
try {
|
|
104
|
+
const { operation } = params;
|
|
105
|
+
|
|
106
|
+
switch (operation) {
|
|
107
|
+
case 'start': {
|
|
108
|
+
const runInShell = params.shell;
|
|
109
|
+
|
|
110
|
+
if (!params.command && !runInShell) {
|
|
111
|
+
return createToolError('command is required for start operation');
|
|
112
|
+
}
|
|
113
|
+
if (!params.purpose) {
|
|
114
|
+
return createToolError('purpose is required for start operation');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cwd = resolveSafePath(projectRoot, params.cwd);
|
|
118
|
+
|
|
119
|
+
const shellPath = process.env.SHELL || '/bin/sh';
|
|
120
|
+
|
|
121
|
+
let command = params.command ?? shellPath;
|
|
122
|
+
let args = params.args ?? [];
|
|
123
|
+
let initialCommand: string | null = null;
|
|
124
|
+
|
|
125
|
+
if (runInShell) {
|
|
126
|
+
command = shellPath;
|
|
127
|
+
args = ['-i'];
|
|
128
|
+
const providedCommand = params.command;
|
|
129
|
+
const providedArgs = params.args ?? [];
|
|
130
|
+
|
|
131
|
+
if (providedCommand || providedArgs.length > 0) {
|
|
132
|
+
if (providedArgs.length === 0 && providedCommand) {
|
|
133
|
+
// Command already contains spaces; treat as full shell snippet
|
|
134
|
+
initialCommand = providedCommand;
|
|
135
|
+
} else {
|
|
136
|
+
const commandParts = [
|
|
137
|
+
providedCommand,
|
|
138
|
+
...providedArgs,
|
|
139
|
+
].filter((part): part is string => Boolean(part));
|
|
140
|
+
if (commandParts.length > 0) {
|
|
141
|
+
initialCommand = formatShellCommand(commandParts);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const term = terminalManager.create({
|
|
148
|
+
command,
|
|
149
|
+
args,
|
|
150
|
+
cwd,
|
|
151
|
+
purpose: params.purpose,
|
|
152
|
+
title: params.title,
|
|
153
|
+
createdBy: 'llm',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (initialCommand) {
|
|
157
|
+
queueMicrotask(() => {
|
|
158
|
+
term.write(`${initialCommand}\n`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
terminalId: term.id,
|
|
165
|
+
pid: term.pid,
|
|
166
|
+
purpose: term.purpose,
|
|
167
|
+
command: params.command ?? command,
|
|
168
|
+
args: params.args || [],
|
|
169
|
+
shell: runInShell,
|
|
170
|
+
title: term.title,
|
|
171
|
+
message: `Started: ${params.command ?? command}${params.args ? ` ${params.args.join(' ')}` : ''}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case 'read': {
|
|
176
|
+
if (!params.terminalId) {
|
|
177
|
+
return createToolError(
|
|
178
|
+
'terminalId is required for read operation',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const term = terminalManager.get(params.terminalId);
|
|
183
|
+
if (!term) {
|
|
184
|
+
return createToolError(`Terminal ${params.terminalId} not found`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const output = term.read(params.lines);
|
|
188
|
+
const normalized = output.map(normalizeTerminalLine);
|
|
189
|
+
const text = normalized.join('\n').replace(/\u0000/g, '');
|
|
190
|
+
|
|
191
|
+
const response: {
|
|
192
|
+
ok: true;
|
|
193
|
+
terminalId: string;
|
|
194
|
+
output: string[];
|
|
195
|
+
status: TerminalStatus;
|
|
196
|
+
exitCode: number | undefined;
|
|
197
|
+
lines: number;
|
|
198
|
+
text: string;
|
|
199
|
+
rawOutput?: string[];
|
|
200
|
+
} = {
|
|
201
|
+
ok: true,
|
|
202
|
+
terminalId: term.id,
|
|
203
|
+
output: normalized,
|
|
204
|
+
status: term.status,
|
|
205
|
+
exitCode: term.exitCode,
|
|
206
|
+
lines: normalized.length,
|
|
207
|
+
text,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (params.raw) {
|
|
211
|
+
response.rawOutput = output;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return response;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'write': {
|
|
218
|
+
if (!params.terminalId) {
|
|
219
|
+
return createToolError(
|
|
220
|
+
'terminalId is required for write operation',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (!params.input) {
|
|
224
|
+
return createToolError('input is required for write operation');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const term = terminalManager.get(params.terminalId);
|
|
228
|
+
if (!term) {
|
|
229
|
+
return createToolError(`Terminal ${params.terminalId} not found`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
term.write(params.input);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
terminalId: term.id,
|
|
237
|
+
message: `Wrote ${params.input.length} characters to terminal`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'interrupt': {
|
|
242
|
+
if (!params.terminalId) {
|
|
243
|
+
return createToolError(
|
|
244
|
+
'terminalId is required for interrupt operation',
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const term = terminalManager.get(params.terminalId);
|
|
249
|
+
if (!term) {
|
|
250
|
+
return createToolError(`Terminal ${params.terminalId} not found`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
term.write('\u0003');
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
terminalId: term.id,
|
|
258
|
+
message: 'Sent SIGINT (Ctrl+C) to terminal',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case 'list': {
|
|
263
|
+
const terminals = terminalManager.list();
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
terminals: terminals.map((t) => t.toJSON()),
|
|
268
|
+
count: terminals.length,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case 'kill': {
|
|
273
|
+
if (!params.terminalId) {
|
|
274
|
+
return createToolError(
|
|
275
|
+
'terminalId is required for kill operation',
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await terminalManager.kill(params.terminalId);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
ok: true,
|
|
283
|
+
terminalId: params.terminalId,
|
|
284
|
+
message: `Killed terminal ${params.terminalId}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
default:
|
|
289
|
+
return createToolError(`Unknown operation: ${operation}`);
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
return createToolError(`Terminal operation failed: ${message}`);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return { name: 'terminal', tool: terminal };
|
|
299
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
- Manage persistent terminals for long-running processes (dev servers, watchers, build processes)
|
|
2
|
+
- Returns terminal information and output
|
|
3
|
+
- Supports creating, reading, writing, listing, and killing terminals
|
|
4
|
+
|
|
5
|
+
## Operations
|
|
6
|
+
|
|
7
|
+
### start
|
|
8
|
+
- Spawns a new persistent terminal (interactive shell by default)
|
|
9
|
+
- Returns terminal ID for future operations
|
|
10
|
+
- Use for processes that need to stay alive (dev servers, watchers, logs)
|
|
11
|
+
- Before starting, call `terminal(operation: "list")` to see if a matching service is already running
|
|
12
|
+
- Provide a clear `purpose` or `title` (e.g. "web dev server port 9100") so humans can recognize it
|
|
13
|
+
- Parameters:
|
|
14
|
+
- command (optional when `shell` is true): Command to run
|
|
15
|
+
- purpose (required): Description of what this terminal is for
|
|
16
|
+
- title (optional): Short UI label shown beside the terminal
|
|
17
|
+
- cwd (optional): Working directory relative to project root (default: '.')
|
|
18
|
+
- args (optional): Array of command arguments
|
|
19
|
+
- shell (optional, default: true): Launch an interactive shell and run the command via stdin. Set to `false` to spawn the process directly.
|
|
20
|
+
|
|
21
|
+
### read
|
|
22
|
+
- Read output from a terminal's buffer (last N lines)
|
|
23
|
+
- Strips ANSI escape codes by default so responses are easy to read
|
|
24
|
+
- Parameters:
|
|
25
|
+
- terminalId (required): Terminal ID from start operation
|
|
26
|
+
- lines (optional): Number of lines to read from end (default: 100)
|
|
27
|
+
- raw (optional): Include `rawOutput` array with ANSI escape sequences (default: false)
|
|
28
|
+
- Returns sanitized output lines, combined `text`, status, and exit code
|
|
29
|
+
|
|
30
|
+
### write
|
|
31
|
+
- Write input to a terminal's stdin
|
|
32
|
+
- Useful for interactive commands or sending signals
|
|
33
|
+
- Parameters:
|
|
34
|
+
- terminalId (required): Terminal ID
|
|
35
|
+
- input (required): String to write to stdin
|
|
36
|
+
|
|
37
|
+
### interrupt
|
|
38
|
+
- Sends SIGINT (Ctrl+C) to the terminal without closing the PTY
|
|
39
|
+
- Useful for stopping dev servers or watchers while keeping the shell alive
|
|
40
|
+
- Parameters:
|
|
41
|
+
- terminalId (required): Terminal ID
|
|
42
|
+
|
|
43
|
+
### list
|
|
44
|
+
- List all active terminals
|
|
45
|
+
- Returns array of terminal metadata (id, purpose, status, pid, uptime)
|
|
46
|
+
- No parameters required
|
|
47
|
+
|
|
48
|
+
### kill
|
|
49
|
+
- Kill a running terminal
|
|
50
|
+
- Sends SIGTERM by default
|
|
51
|
+
- Parameters:
|
|
52
|
+
- terminalId (required): Terminal ID to kill
|
|
53
|
+
|
|
54
|
+
## When to Use Terminal vs Bash
|
|
55
|
+
|
|
56
|
+
### Use terminal for:
|
|
57
|
+
- Dev servers: npm run dev, bun dev
|
|
58
|
+
- File watchers: bun test --watch, nodemon
|
|
59
|
+
- Build watchers: bun build --watch
|
|
60
|
+
- Log tailing: tail -f logs/app.log
|
|
61
|
+
- Background services: docker compose up
|
|
62
|
+
- Any process that needs to stay alive and produce continuous output
|
|
63
|
+
|
|
64
|
+
### Use bash for:
|
|
65
|
+
- Status checks: git status, ls, ps
|
|
66
|
+
- One-off commands: mkdir, rm, curl
|
|
67
|
+
- Quick scripts: bun run build, git commit
|
|
68
|
+
- File operations: cat, grep, sed
|
|
69
|
+
- Short-lived commands with immediate output
|
|
70
|
+
|
|
71
|
+
## Example Workflow
|
|
72
|
+
|
|
73
|
+
1. Start dev server:
|
|
74
|
+
terminal(operation: "start", command: "npm", args: ["run", "dev"], purpose: "dev server")
|
|
75
|
+
→ Returns { terminalId: "term-abc123", pid: 12345 }
|
|
76
|
+
|
|
77
|
+
2. Later, check for errors:
|
|
78
|
+
terminal(operation: "read", terminalId: "term-abc123", lines: 50)
|
|
79
|
+
→ Returns last 50 lines of output
|
|
80
|
+
|
|
81
|
+
3. Kill when done:
|
|
82
|
+
terminal(operation: "kill", terminalId: "term-abc123")
|
|
83
|
+
|
|
84
|
+
## Notes
|
|
85
|
+
|
|
86
|
+
- Terminals persist across multiple LLM turns (unlike bash commands)
|
|
87
|
+
- Maximum 10 terminals per session
|
|
88
|
+
- Exited terminals auto-cleanup after 5 minutes
|
|
89
|
+
- Output is buffered (last 500 lines kept)
|
|
90
|
+
- Both user-created and LLM-created terminals are visible
|
|
91
|
+
- You can read from user-created terminals to understand context
|
|
92
|
+
- Prefer `read` over `start` when you only need status — avoid duplicating services that already exist
|
|
93
|
+
- Mention running terminals (purpose, command, port) in your responses so humans know what is active
|
|
@@ -12,6 +12,8 @@ import { buildApplyPatchTool } from './builtin/patch.ts';
|
|
|
12
12
|
import { updatePlanTool } from './builtin/plan.ts';
|
|
13
13
|
import { editTool } from './builtin/edit.ts';
|
|
14
14
|
import { buildWebSearchTool } from './builtin/websearch.ts';
|
|
15
|
+
import { buildTerminalTool } from './builtin/terminal.ts';
|
|
16
|
+
import type { TerminalManager } from '../terminals/index.ts';
|
|
15
17
|
import fg from 'fast-glob';
|
|
16
18
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
17
19
|
import { pathToFileURL } from 'node:url';
|
|
@@ -90,6 +92,16 @@ type FsHelpers = {
|
|
|
90
92
|
|
|
91
93
|
const pluginPatterns = ['tools/*/tool.js', 'tools/*/tool.mjs'];
|
|
92
94
|
|
|
95
|
+
let globalTerminalManager: TerminalManager | null = null;
|
|
96
|
+
|
|
97
|
+
export function setTerminalManager(manager: TerminalManager): void {
|
|
98
|
+
globalTerminalManager = manager;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getTerminalManager(): TerminalManager | null {
|
|
102
|
+
return globalTerminalManager;
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
export async function discoverProjectTools(
|
|
94
106
|
projectRoot: string,
|
|
95
107
|
globalConfigDir?: string,
|
|
@@ -120,6 +132,11 @@ export async function discoverProjectTools(
|
|
|
120
132
|
// Web search
|
|
121
133
|
const ws = buildWebSearchTool();
|
|
122
134
|
tools.set(ws.name, ws.tool);
|
|
135
|
+
// Terminal (if manager is available)
|
|
136
|
+
if (globalTerminalManager) {
|
|
137
|
+
const term = buildTerminalTool(projectRoot, globalTerminalManager);
|
|
138
|
+
tools.set(term.name, term.tool);
|
|
139
|
+
}
|
|
123
140
|
|
|
124
141
|
async function loadFromBase(base: string | null | undefined) {
|
|
125
142
|
if (!base) return;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function stripAnsi(input: string): string {
|
|
2
|
+
let result = '';
|
|
3
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
4
|
+
const ch = input[i];
|
|
5
|
+
if (ch === '\u001B' || ch === '\u009B') {
|
|
6
|
+
// Skip CSI sequences until we hit a terminating byte (A-Z or a-z)
|
|
7
|
+
i += 1;
|
|
8
|
+
while (i < input.length) {
|
|
9
|
+
const code = input[i];
|
|
10
|
+
if (
|
|
11
|
+
code &&
|
|
12
|
+
((code >= '@' && code <= 'Z') || (code >= 'a' && code <= 'z'))
|
|
13
|
+
) {
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
i += 1;
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
result += ch;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeTerminalLine(line: string): string {
|
|
26
|
+
return stripAnsi(line).replace(/\r/g, '');
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -120,9 +120,20 @@ export type { ProviderName, ModelConfig } from './core/src/index.ts';
|
|
|
120
120
|
// Tools
|
|
121
121
|
export { discoverProjectTools } from './core/src/index.ts';
|
|
122
122
|
export type { DiscoveredTool } from './core/src/index.ts';
|
|
123
|
+
export { setTerminalManager, getTerminalManager } from './core/src/index.ts';
|
|
123
124
|
export { buildFsTools } from './core/src/index.ts';
|
|
124
125
|
export { buildGitTools } from './core/src/index.ts';
|
|
125
126
|
|
|
127
|
+
// Terminals
|
|
128
|
+
export { TerminalManager } from './core/src/index.ts';
|
|
129
|
+
export type {
|
|
130
|
+
Terminal,
|
|
131
|
+
TerminalOptions,
|
|
132
|
+
TerminalStatus,
|
|
133
|
+
TerminalCreator,
|
|
134
|
+
CreateTerminalOptions,
|
|
135
|
+
} from './core/src/index.ts';
|
|
136
|
+
|
|
126
137
|
// Streaming & Artifacts
|
|
127
138
|
export {
|
|
128
139
|
createFileDiffArtifact,
|
|
@@ -4,6 +4,14 @@ You help with coding and build tasks.
|
|
|
4
4
|
- Keep tool inputs short; avoid long prose inside tool parameters.
|
|
5
5
|
- Stream your answer, then call finish.
|
|
6
6
|
|
|
7
|
+
## Terminal Tool Workflow
|
|
8
|
+
|
|
9
|
+
- List existing terminals before starting new ones to avoid duplicate dev servers or watchers.
|
|
10
|
+
- Reuse running services when possible; read their output instead of spawning another copy.
|
|
11
|
+
- When starting a terminal, give it a descriptive purpose/title (e.g. "web dev server 9100" or "bun test --watch") and prefer `terminal(...)` over `bash(...)` for long-lived tasks.
|
|
12
|
+
- Use `terminal(operation: "write", input: "\u0003")` or `terminal(operation: "interrupt")` to stop a process before resorting to `kill`.
|
|
13
|
+
- Summarize active terminals (purpose, key command, port) in your updates so collaborators know what's running.
|
|
14
|
+
|
|
7
15
|
## File Editing Best Practices
|
|
8
16
|
|
|
9
17
|
**Using the `apply_patch` Tool** (Recommended):
|