agent-sh 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -43
- package/dist/agent/agent-loop.d.ts +69 -6
- package/dist/agent/agent-loop.js +954 -153
- package/dist/agent/conversation-state.d.ts +74 -21
- package/dist/agent/conversation-state.js +361 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +88 -6
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +37 -5
- package/dist/agent/system-prompt.js +100 -67
- package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
- package/dist/{token-budget.js → agent/token-budget.js} +15 -20
- package/dist/agent/tool-protocol.d.ts +105 -0
- package/dist/agent/tool-protocol.js +551 -0
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +22 -2
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.d.ts +7 -7
- package/dist/core.js +99 -196
- package/dist/event-bus.d.ts +85 -2
- package/dist/event-bus.js +20 -1
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +143 -19
- package/dist/extensions/agent-backend.d.ts +14 -0
- package/dist/extensions/agent-backend.js +188 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +24 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +30 -10
- package/dist/extensions/tui-renderer.js +117 -113
- package/dist/index.js +39 -26
- package/dist/settings.d.ts +40 -3
- package/dist/settings.js +57 -10
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
- package/dist/{input-handler.js → shell/input-handler.js} +111 -85
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +39 -8
- package/dist/types.d.ts +61 -10
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +67 -0
- package/dist/utils/compositor.js +116 -0
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +312 -146
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +31 -10
- package/dist/utils/handler-registry.js +58 -16
- package/dist/utils/line-editor.d.ts +33 -3
- package/dist/utils/line-editor.js +221 -44
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +98 -112
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +565 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +260 -0
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +32 -53
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +335 -0
- package/package.json +44 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -125
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peer mesh — cross-instance communication for agent-sh.
|
|
3
|
+
*
|
|
4
|
+
* Lets all running ash instances discover each other and communicate.
|
|
5
|
+
* Inspired by Ray's @ray.remote: opt-in exposure of named handlers,
|
|
6
|
+
* transparent network calls, automatic discovery.
|
|
7
|
+
*
|
|
8
|
+
* What this extension provides:
|
|
9
|
+
* 1. PeerServer — Unix socket server + peer file registry + client
|
|
10
|
+
* 2. Standard exposed handlers — terminal read, context, search
|
|
11
|
+
* 3. Agent tools — peers, peer_terminal, peer_history, peer_search
|
|
12
|
+
* 4. Handler registry API — peer:call, peer:discover, peer:expose
|
|
13
|
+
* for other extensions to use
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ash -e ./examples/extensions/peer-mesh.ts
|
|
17
|
+
*
|
|
18
|
+
* # Or install permanently
|
|
19
|
+
* cp examples/extensions/peer-mesh.ts ~/.agent-sh/extensions/
|
|
20
|
+
*
|
|
21
|
+
* Other extensions can depend on this via the handler registry:
|
|
22
|
+
* ctx.define("my:data", () => computeData());
|
|
23
|
+
* ctx.call("peer:expose", "my:data");
|
|
24
|
+
* const result = await ctx.call("peer:call", peerId, "my:data");
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as net from "node:net";
|
|
28
|
+
import * as os from "node:os";
|
|
29
|
+
import * as path from "node:path";
|
|
30
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
31
|
+
|
|
32
|
+
// ── Types ──────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
interface PeerInfo {
|
|
35
|
+
id: string;
|
|
36
|
+
pid: number;
|
|
37
|
+
cwd: string;
|
|
38
|
+
socketPath: string;
|
|
39
|
+
startTime: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RpcRequest {
|
|
43
|
+
method: string;
|
|
44
|
+
args: unknown[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface RpcResponse {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
result?: unknown;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Paths ──────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const PEERS_DIR = path.join(os.homedir(), ".agent-sh", "peers");
|
|
56
|
+
|
|
57
|
+
function peerFilePath(id: string): string {
|
|
58
|
+
return path.join(PEERS_DIR, `${id}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function socketPath(pid: number): string {
|
|
62
|
+
return path.join(os.tmpdir(), `agent-sh-peer-${pid}.sock`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── PeerServer ─────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
class PeerServer {
|
|
68
|
+
private server: net.Server | null = null;
|
|
69
|
+
private exposed = new Set<string>();
|
|
70
|
+
private readonly info: PeerInfo;
|
|
71
|
+
private readonly callHandler: (name: string, ...args: unknown[]) => unknown;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
instanceId: string,
|
|
75
|
+
cwd: string,
|
|
76
|
+
callHandler: (name: string, ...args: unknown[]) => unknown,
|
|
77
|
+
) {
|
|
78
|
+
this.callHandler = callHandler;
|
|
79
|
+
this.info = {
|
|
80
|
+
id: instanceId,
|
|
81
|
+
pid: process.pid,
|
|
82
|
+
cwd,
|
|
83
|
+
socketPath: socketPath(process.pid),
|
|
84
|
+
startTime: Date.now(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Lifecycle ──────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
start(): void {
|
|
91
|
+
// Ensure peers directory exists
|
|
92
|
+
fs.mkdirSync(PEERS_DIR, { recursive: true });
|
|
93
|
+
|
|
94
|
+
// Clean up stale socket
|
|
95
|
+
try { fs.unlinkSync(this.info.socketPath); } catch {}
|
|
96
|
+
|
|
97
|
+
// Start Unix socket server
|
|
98
|
+
this.server = net.createServer((conn) => this.handleConnection(conn));
|
|
99
|
+
this.server.on("error", () => {}); // swallow server errors
|
|
100
|
+
this.server.listen(this.info.socketPath);
|
|
101
|
+
|
|
102
|
+
// Register peer file
|
|
103
|
+
fs.writeFileSync(peerFilePath(this.info.id), JSON.stringify(this.info));
|
|
104
|
+
|
|
105
|
+
// Cleanup on exit
|
|
106
|
+
const cleanup = () => this.stop();
|
|
107
|
+
process.on("exit", cleanup);
|
|
108
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
109
|
+
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stop(): void {
|
|
113
|
+
if (this.server) {
|
|
114
|
+
try { this.server.close(); } catch {}
|
|
115
|
+
this.server = null;
|
|
116
|
+
}
|
|
117
|
+
try { fs.unlinkSync(this.info.socketPath); } catch {}
|
|
118
|
+
try { fs.unlinkSync(peerFilePath(this.info.id)); } catch {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Expose / discover / call ───────────────────────────────
|
|
122
|
+
|
|
123
|
+
expose(name: string): void {
|
|
124
|
+
this.exposed.add(name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
discover(): PeerInfo[] {
|
|
128
|
+
const peers: PeerInfo[] = [];
|
|
129
|
+
let entries: string[];
|
|
130
|
+
try { entries = fs.readdirSync(PEERS_DIR); } catch { return []; }
|
|
131
|
+
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
if (!entry.endsWith(".json")) continue;
|
|
134
|
+
try {
|
|
135
|
+
const raw = fs.readFileSync(path.join(PEERS_DIR, entry), "utf-8");
|
|
136
|
+
const info: PeerInfo = JSON.parse(raw);
|
|
137
|
+
// Skip self
|
|
138
|
+
if (info.id === this.info.id) continue;
|
|
139
|
+
// Check if process is alive
|
|
140
|
+
try { process.kill(info.pid, 0); } catch {
|
|
141
|
+
// Stale — prune
|
|
142
|
+
try { fs.unlinkSync(path.join(PEERS_DIR, entry)); } catch {}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
peers.push(info);
|
|
146
|
+
} catch {
|
|
147
|
+
// Malformed file — skip
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return peers;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async call(peerId: string, method: string, ...args: unknown[]): Promise<unknown> {
|
|
154
|
+
// Find peer socket path
|
|
155
|
+
const peers = this.discover();
|
|
156
|
+
const peer = peers.find((p) => p.id === peerId);
|
|
157
|
+
if (!peer) throw new Error(`Peer "${peerId}" not found`);
|
|
158
|
+
|
|
159
|
+
return this.callSocket(peer.socketPath, method, args);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Private ────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
private handleConnection(conn: net.Socket): void {
|
|
165
|
+
let buffer = "";
|
|
166
|
+
conn.on("data", (data) => {
|
|
167
|
+
buffer += data.toString();
|
|
168
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
169
|
+
if (newlineIdx === -1) return;
|
|
170
|
+
|
|
171
|
+
const line = buffer.slice(0, newlineIdx);
|
|
172
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
173
|
+
|
|
174
|
+
let response: RpcResponse;
|
|
175
|
+
try {
|
|
176
|
+
const req: RpcRequest = JSON.parse(line);
|
|
177
|
+
if (!this.exposed.has(req.method)) {
|
|
178
|
+
response = { ok: false, error: `Handler "${req.method}" is not exposed` };
|
|
179
|
+
} else {
|
|
180
|
+
const result = this.callHandler(req.method, ...(req.args ?? []));
|
|
181
|
+
response = { ok: true, result };
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
response = { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
conn.write(JSON.stringify(response) + "\n");
|
|
189
|
+
} catch {}
|
|
190
|
+
conn.end();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
conn.on("error", () => {});
|
|
194
|
+
// Timeout in case client hangs
|
|
195
|
+
conn.setTimeout(5000, () => conn.destroy());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private callSocket(sockPath: string, method: string, args: unknown[]): Promise<unknown> {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const conn = net.createConnection(sockPath);
|
|
201
|
+
let buffer = "";
|
|
202
|
+
let settled = false;
|
|
203
|
+
|
|
204
|
+
const settle = (fn: () => void) => {
|
|
205
|
+
if (settled) return;
|
|
206
|
+
settled = true;
|
|
207
|
+
fn();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
conn.on("connect", () => {
|
|
211
|
+
const req: RpcRequest = { method, args };
|
|
212
|
+
conn.write(JSON.stringify(req) + "\n");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
conn.on("data", (data) => {
|
|
216
|
+
buffer += data.toString();
|
|
217
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
218
|
+
if (newlineIdx === -1) return;
|
|
219
|
+
|
|
220
|
+
const line = buffer.slice(0, newlineIdx);
|
|
221
|
+
try {
|
|
222
|
+
const resp: RpcResponse = JSON.parse(line);
|
|
223
|
+
settle(() => resp.ok ? resolve(resp.result) : reject(new Error(resp.error)));
|
|
224
|
+
} catch (e) {
|
|
225
|
+
settle(() => reject(e));
|
|
226
|
+
}
|
|
227
|
+
conn.end();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
conn.on("error", (e) => settle(() => reject(e)));
|
|
231
|
+
conn.setTimeout(5000, () => settle(() => {
|
|
232
|
+
reject(new Error("Peer call timed out"));
|
|
233
|
+
conn.destroy();
|
|
234
|
+
}));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Extension ──────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
242
|
+
const { bus, contextManager, registerCommand, registerTool, registerInstruction, define } = ctx;
|
|
243
|
+
const startTime = Date.now();
|
|
244
|
+
|
|
245
|
+
const server = new PeerServer(ctx.instanceId, contextManager.getCwd(), (...args) => ctx.call(...args));
|
|
246
|
+
server.start();
|
|
247
|
+
|
|
248
|
+
// ── Standard handlers (define + expose) ────────────────────
|
|
249
|
+
|
|
250
|
+
define("peer:info", () => ({
|
|
251
|
+
id: ctx.instanceId,
|
|
252
|
+
pid: process.pid,
|
|
253
|
+
cwd: contextManager.getCwd(),
|
|
254
|
+
uptime: Math.round((Date.now() - startTime) / 1000),
|
|
255
|
+
}));
|
|
256
|
+
server.expose("peer:info");
|
|
257
|
+
|
|
258
|
+
define("peer:terminal-read", () => {
|
|
259
|
+
const tb = ctx.terminalBuffer;
|
|
260
|
+
if (!tb) return { text: "(terminal buffer not available)", altScreen: false };
|
|
261
|
+
return tb.readScreen({ includeScrollback: true });
|
|
262
|
+
});
|
|
263
|
+
server.expose("peer:terminal-read");
|
|
264
|
+
|
|
265
|
+
define("peer:context-recent", (n: number = 15) => contextManager.getRecentSummary(n));
|
|
266
|
+
server.expose("peer:context-recent");
|
|
267
|
+
|
|
268
|
+
define("peer:context-search", (query: string) => contextManager.search(query));
|
|
269
|
+
server.expose("peer:context-search");
|
|
270
|
+
|
|
271
|
+
// ── Inter-peer messaging ───────────────────────────────────
|
|
272
|
+
|
|
273
|
+
interface InboxEntry { from: string; text: string; at: number; }
|
|
274
|
+
const inbox: InboxEntry[] = [];
|
|
275
|
+
const INBOX_MAX = 100;
|
|
276
|
+
|
|
277
|
+
const pending: InboxEntry[] = [];
|
|
278
|
+
|
|
279
|
+
define("peer:message", (from: string, text: string) => {
|
|
280
|
+
if (typeof from !== "string" || typeof text !== "string") {
|
|
281
|
+
throw new Error("peer:message requires (from: string, text: string)");
|
|
282
|
+
}
|
|
283
|
+
const entry: InboxEntry = { from, text, at: Date.now() };
|
|
284
|
+
inbox.push(entry);
|
|
285
|
+
if (inbox.length > INBOX_MAX) inbox.splice(0, inbox.length - INBOX_MAX);
|
|
286
|
+
pending.push(entry);
|
|
287
|
+
bus.emit("peer:message-received", entry);
|
|
288
|
+
bus.emit("ui:info", { message: `[peer ${from}] ${text}` });
|
|
289
|
+
setTimeout(drainPending, 100);
|
|
290
|
+
return { ok: true };
|
|
291
|
+
});
|
|
292
|
+
server.expose("peer:message");
|
|
293
|
+
|
|
294
|
+
// Drain pending peer messages by injecting a synthetic user turn.
|
|
295
|
+
// Only one submission per processing cycle — wait for agent:processing-done
|
|
296
|
+
// before releasing the next batch.
|
|
297
|
+
let busy = false;
|
|
298
|
+
|
|
299
|
+
function drainPending(): void {
|
|
300
|
+
if (busy || pending.length === 0) return;
|
|
301
|
+
const batch = pending.splice(0, pending.length);
|
|
302
|
+
const lines = batch.map((e) => `[from peer ${e.from}] ${e.text}`);
|
|
303
|
+
const query = [
|
|
304
|
+
"You received message(s) from other peer(s) in the mesh:",
|
|
305
|
+
"",
|
|
306
|
+
...lines,
|
|
307
|
+
"",
|
|
308
|
+
"Decide whether to reply (via `peer_send`), act on the request, or note and continue.",
|
|
309
|
+
].join("\n");
|
|
310
|
+
busy = true;
|
|
311
|
+
bus.emit("agent:submit", { query });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
bus.on("agent:processing-done", () => {
|
|
315
|
+
busy = false;
|
|
316
|
+
setTimeout(drainPending, 100);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ── Handler registry API (for other extensions) ────────────
|
|
320
|
+
|
|
321
|
+
define("peer:discover", () => server.discover());
|
|
322
|
+
define("peer:call", (peerId: string, method: string, ...args: unknown[]) =>
|
|
323
|
+
server.call(peerId, method, ...args));
|
|
324
|
+
define("peer:expose", (name: string) => server.expose(name));
|
|
325
|
+
|
|
326
|
+
// ── Agent tools ────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
registerTool({
|
|
329
|
+
name: "peers",
|
|
330
|
+
description: "List all running agent-sh instances that can be communicated with.",
|
|
331
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
332
|
+
showOutput: false,
|
|
333
|
+
getDisplayInfo: () => ({ kind: "search" as const }),
|
|
334
|
+
formatCall: () => "discovering peers",
|
|
335
|
+
|
|
336
|
+
async execute() {
|
|
337
|
+
const peers = server.discover();
|
|
338
|
+
if (peers.length === 0) {
|
|
339
|
+
return { content: "No other agent-sh instances found.", exitCode: 0, isError: false };
|
|
340
|
+
}
|
|
341
|
+
const lines = peers.map((p) =>
|
|
342
|
+
`- id: ${p.id}, pid: ${p.pid}, cwd: ${p.cwd}, uptime: ${Math.round((Date.now() - p.startTime) / 1000)}s`
|
|
343
|
+
);
|
|
344
|
+
return {
|
|
345
|
+
content: `Found ${peers.length} peer(s):\n${lines.join("\n")}`,
|
|
346
|
+
exitCode: 0,
|
|
347
|
+
isError: false,
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
formatResult: (_args, result) => ({
|
|
352
|
+
summary: result.content.startsWith("No") ? "none found" : result.content.split("\n")[0],
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
registerTool({
|
|
357
|
+
name: "peer_terminal",
|
|
358
|
+
description: "Read the terminal screen content of another running agent-sh instance. Shows what is currently visible on their terminal.",
|
|
359
|
+
input_schema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
peer_id: { type: "string", description: "The instance ID of the peer (from the peers tool)." },
|
|
363
|
+
},
|
|
364
|
+
required: ["peer_id"],
|
|
365
|
+
},
|
|
366
|
+
showOutput: false,
|
|
367
|
+
getDisplayInfo: () => ({ kind: "read" as const }),
|
|
368
|
+
formatCall: (args) => `peer ${args.peer_id}`,
|
|
369
|
+
|
|
370
|
+
async execute(args) {
|
|
371
|
+
try {
|
|
372
|
+
const screen = await server.call(args.peer_id as string, "peer:terminal-read") as any;
|
|
373
|
+
const text = screen?.text?.trim() || "(empty screen)";
|
|
374
|
+
const alt = screen?.altScreen ? " [alternate screen active]" : "";
|
|
375
|
+
return {
|
|
376
|
+
content: `Terminal content from peer ${args.peer_id}${alt}:\n\n${text}`,
|
|
377
|
+
exitCode: 0,
|
|
378
|
+
isError: false,
|
|
379
|
+
};
|
|
380
|
+
} catch (e) {
|
|
381
|
+
return {
|
|
382
|
+
content: `Failed to read peer terminal: ${e instanceof Error ? e.message : String(e)}`,
|
|
383
|
+
exitCode: 1,
|
|
384
|
+
isError: true,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
formatResult: (_args, result) => ({
|
|
390
|
+
summary: result.isError ? "failed" : `${result.content.split("\n").length - 2} lines`,
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
registerTool({
|
|
395
|
+
name: "peer_history",
|
|
396
|
+
description: "Get the recent shell command history from another running agent-sh instance.",
|
|
397
|
+
input_schema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
peer_id: { type: "string", description: "The instance ID of the peer." },
|
|
401
|
+
count: { type: "number", description: "Number of recent exchanges to return (default: 15)." },
|
|
402
|
+
},
|
|
403
|
+
required: ["peer_id"],
|
|
404
|
+
},
|
|
405
|
+
showOutput: false,
|
|
406
|
+
getDisplayInfo: () => ({ kind: "read" as const }),
|
|
407
|
+
formatCall: (args) => `peer ${args.peer_id}`,
|
|
408
|
+
|
|
409
|
+
async execute(args) {
|
|
410
|
+
try {
|
|
411
|
+
const n = (args.count as number) || 15;
|
|
412
|
+
const summary = await server.call(args.peer_id as string, "peer:context-recent", n) as string;
|
|
413
|
+
return { content: summary || "(no history)", exitCode: 0, isError: false };
|
|
414
|
+
} catch (e) {
|
|
415
|
+
return {
|
|
416
|
+
content: `Failed to read peer history: ${e instanceof Error ? e.message : String(e)}`,
|
|
417
|
+
exitCode: 1,
|
|
418
|
+
isError: true,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
formatResult: (_args, result) => ({
|
|
424
|
+
summary: result.isError ? "failed" : `${result.content.split("\n").length} lines`,
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
registerTool({
|
|
429
|
+
name: "peer_search",
|
|
430
|
+
description: "Search another agent-sh instance's shell context by keyword or regex.",
|
|
431
|
+
input_schema: {
|
|
432
|
+
type: "object",
|
|
433
|
+
properties: {
|
|
434
|
+
peer_id: { type: "string", description: "The instance ID of the peer." },
|
|
435
|
+
query: { type: "string", description: "Search query (keyword or regex)." },
|
|
436
|
+
},
|
|
437
|
+
required: ["peer_id", "query"],
|
|
438
|
+
},
|
|
439
|
+
showOutput: false,
|
|
440
|
+
getDisplayInfo: () => ({ kind: "search" as const }),
|
|
441
|
+
formatCall: (args) => `peer ${args.peer_id}: "${args.query}"`,
|
|
442
|
+
|
|
443
|
+
async execute(args) {
|
|
444
|
+
try {
|
|
445
|
+
const results = await server.call(
|
|
446
|
+
args.peer_id as string, "peer:context-search", args.query as string,
|
|
447
|
+
) as string;
|
|
448
|
+
return { content: results || "(no matches)", exitCode: 0, isError: false };
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return {
|
|
451
|
+
content: `Failed to search peer context: ${e instanceof Error ? e.message : String(e)}`,
|
|
452
|
+
exitCode: 1,
|
|
453
|
+
isError: true,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
formatResult: (_args, result) => ({
|
|
459
|
+
summary: result.isError ? "failed" : `${result.content.split("\n").length} lines`,
|
|
460
|
+
}),
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
registerTool({
|
|
464
|
+
name: "peer_send",
|
|
465
|
+
description: "Send a text message to another running agent-sh peer. The peer will see it in their UI and on their next turn.",
|
|
466
|
+
input_schema: {
|
|
467
|
+
type: "object",
|
|
468
|
+
properties: {
|
|
469
|
+
peer_id: { type: "string", description: "The instance ID of the peer (from the peers tool)." },
|
|
470
|
+
text: { type: "string", description: "Message body." },
|
|
471
|
+
},
|
|
472
|
+
required: ["peer_id", "text"],
|
|
473
|
+
},
|
|
474
|
+
showOutput: false,
|
|
475
|
+
getDisplayInfo: () => ({ kind: "write" as const }),
|
|
476
|
+
formatCall: (args) => `peer ${args.peer_id}: "${String(args.text).slice(0, 40)}"`,
|
|
477
|
+
|
|
478
|
+
async execute(args) {
|
|
479
|
+
try {
|
|
480
|
+
await server.call(args.peer_id as string, "peer:message", ctx.instanceId, args.text as string);
|
|
481
|
+
return { content: `Sent to peer ${args.peer_id}.`, exitCode: 0, isError: false };
|
|
482
|
+
} catch (e) {
|
|
483
|
+
return {
|
|
484
|
+
content: `Failed to send: ${e instanceof Error ? e.message : String(e)}`,
|
|
485
|
+
exitCode: 1,
|
|
486
|
+
isError: true,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
formatResult: (_args, result) => ({
|
|
492
|
+
summary: result.isError ? "failed" : "sent",
|
|
493
|
+
}),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
registerTool({
|
|
497
|
+
name: "peer_inbox",
|
|
498
|
+
description: "Read recent messages received from other peers via peer_send.",
|
|
499
|
+
input_schema: {
|
|
500
|
+
type: "object",
|
|
501
|
+
properties: {
|
|
502
|
+
count: { type: "number", description: "Number of recent messages to return (default: 20)." },
|
|
503
|
+
},
|
|
504
|
+
required: [],
|
|
505
|
+
},
|
|
506
|
+
showOutput: false,
|
|
507
|
+
getDisplayInfo: () => ({ kind: "read" as const }),
|
|
508
|
+
formatCall: () => "reading inbox",
|
|
509
|
+
|
|
510
|
+
async execute(args) {
|
|
511
|
+
const n = (args.count as number) || 20;
|
|
512
|
+
const recent = inbox.slice(-n);
|
|
513
|
+
if (recent.length === 0) {
|
|
514
|
+
return { content: "(inbox empty)", exitCode: 0, isError: false };
|
|
515
|
+
}
|
|
516
|
+
const lines = recent.map((e) => {
|
|
517
|
+
const ago = Math.round((Date.now() - e.at) / 1000);
|
|
518
|
+
return `[${ago}s ago] ${e.from}: ${e.text}`;
|
|
519
|
+
});
|
|
520
|
+
return { content: lines.join("\n"), exitCode: 0, isError: false };
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
formatResult: (_args, result) => ({
|
|
524
|
+
summary: result.content === "(inbox empty)" ? "empty" : `${result.content.split("\n").length} msg`,
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// ── Slash command ──────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
registerCommand("peers", "List running agent-sh peer instances", () => {
|
|
531
|
+
const peers = server.discover();
|
|
532
|
+
if (peers.length === 0) {
|
|
533
|
+
bus.emit("ui:info", { message: "No peers found." });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const lines = peers.map((p) => {
|
|
537
|
+
const uptime = Math.round((Date.now() - p.startTime) / 1000);
|
|
538
|
+
return ` ${p.id} pid=${p.pid} cwd=${p.cwd} ${uptime}s`;
|
|
539
|
+
});
|
|
540
|
+
bus.emit("ui:info", { message: `Peers:\n${lines.join("\n")}` });
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// ── System prompt instruction ──────────────────────────────
|
|
544
|
+
|
|
545
|
+
registerInstruction("Peer Mesh", [
|
|
546
|
+
"You have access to a peer mesh — other running agent-sh instances on this machine.",
|
|
547
|
+
"Use the `peers` tool to discover them, then:",
|
|
548
|
+
"- `peer_terminal` to see what's on another terminal's screen",
|
|
549
|
+
"- `peer_history` to see what commands they ran recently",
|
|
550
|
+
"- `peer_search` to search their shell context by keyword",
|
|
551
|
+
"- `peer_send` to deliver a text message to another peer (appears in their UI)",
|
|
552
|
+
"- `peer_inbox` to read messages other peers have sent you",
|
|
553
|
+
"When the user references 'the other terminal' or 'my other shell', use these tools.",
|
|
554
|
+
].join("\n"));
|
|
555
|
+
|
|
556
|
+
// ── Update CWD in peer file on directory change ────────────
|
|
557
|
+
|
|
558
|
+
bus.on("shell:cwd-change", ({ cwd }) => {
|
|
559
|
+
try {
|
|
560
|
+
const info: PeerInfo = JSON.parse(fs.readFileSync(peerFilePath(ctx.instanceId), "utf-8"));
|
|
561
|
+
info.cwd = cwd;
|
|
562
|
+
fs.writeFileSync(peerFilePath(ctx.instanceId), JSON.stringify(info));
|
|
563
|
+
} catch {}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
SessionManager,
|
|
24
24
|
} from "@mariozechner/pi-coding-agent";
|
|
25
25
|
import { Type } from "@sinclair/typebox";
|
|
26
|
-
import type { ExtensionContext } from "
|
|
27
|
-
import type { EventBus } from "
|
|
26
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
27
|
+
import type { EventBus } from "agent-sh/event-bus";
|
|
28
28
|
|
|
29
29
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
30
30
|
function interpretEscapes(str: string): string {
|