agent-sh 0.3.1 → 0.5.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 +66 -96
- package/dist/agent/agent-loop.d.ts +85 -0
- package/dist/agent/agent-loop.js +611 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +117 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +98 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +62 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +95 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +55 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +77 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +43 -0
- package/dist/agent/tools/read-file.d.ts +2 -0
- package/dist/agent/tools/read-file.js +55 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +57 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +74 -0
- package/dist/agent/types.d.ts +44 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +24 -14
- package/dist/core.js +260 -36
- package/dist/event-bus.d.ts +84 -14
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.js +12 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +111 -53
- package/dist/index.js +124 -120
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +152 -45
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +45 -2
- package/dist/shell.js +36 -27
- package/dist/types.d.ts +46 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/line-editor.js +4 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.js +2 -2
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.js +15 -5
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +265 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -100
- package/dist/acp-client.js +0 -656
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell exec extension.
|
|
3
|
-
*
|
|
4
|
-
* Runs a Unix domain socket server speaking JSON-RPC 2.0 that external
|
|
5
|
-
* tools (MCP server, pi extensions, etc.) connect to for interacting
|
|
6
|
-
* with the user's live PTY shell.
|
|
7
|
-
*
|
|
8
|
-
* Also registers the MCP server via the `session:configure` pipe so
|
|
9
|
-
* ACP agents discover the `user_shell` tool automatically.
|
|
10
|
-
*
|
|
11
|
-
* This extension has no direct PTY or Shell knowledge — it communicates
|
|
12
|
-
* exclusively through the bus, following the headless-core philosophy.
|
|
13
|
-
*
|
|
14
|
-
* ## Socket protocol (JSON-RPC 2.0, newline-delimited)
|
|
15
|
-
*
|
|
16
|
-
* shell/exec { command: string } → { output, cwd }
|
|
17
|
-
* shell/cwd {} → { cwd }
|
|
18
|
-
* shell/info {} → { busy, shell }
|
|
19
|
-
* shell/recall { operation, ... } → { result }
|
|
20
|
-
*/
|
|
21
|
-
import * as net from "node:net";
|
|
22
|
-
import * as fs from "node:fs";
|
|
23
|
-
import * as path from "node:path";
|
|
24
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { getSettings } from "../settings.js";
|
|
26
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
-
export default function activate({ bus, contextManager }, opts) {
|
|
28
|
-
const { socketPath } = opts;
|
|
29
|
-
// Register MCP server so ACP agents discover the bridge tools.
|
|
30
|
-
// Agents that don't support MCP (e.g. pi-acp) simply ignore it.
|
|
31
|
-
// Can be disabled via settings.json if not needed.
|
|
32
|
-
if (getSettings().enableMcp) {
|
|
33
|
-
bus.onPipe("session:configure", (payload) => {
|
|
34
|
-
return {
|
|
35
|
-
...payload,
|
|
36
|
-
mcpServers: [
|
|
37
|
-
...payload.mcpServers,
|
|
38
|
-
{
|
|
39
|
-
name: "agent-sh",
|
|
40
|
-
command: process.execPath,
|
|
41
|
-
args: [path.join(__dirname, "..", "mcp-server.js")],
|
|
42
|
-
env: [{ name: "AGENT_SH_SOCKET", value: socketPath }],
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
// Set AGENT_SH_SOCKET for agent extensions that connect directly
|
|
49
|
-
process.env.AGENT_SH_SOCKET = socketPath;
|
|
50
|
-
// Serialize shell/exec requests — only one PTY command at a time
|
|
51
|
-
let execPending = Promise.resolve();
|
|
52
|
-
// ── JSON-RPC handler ────────────────────────────────────────────
|
|
53
|
-
async function handleRequest(method, params) {
|
|
54
|
-
switch (method) {
|
|
55
|
-
case "shell/exec": {
|
|
56
|
-
const command = params?.command;
|
|
57
|
-
if (typeof command !== "string" || !command) {
|
|
58
|
-
throw rpcError(-32602, "Missing required parameter: command");
|
|
59
|
-
}
|
|
60
|
-
// Serialize — one PTY command at a time
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
execPending = execPending.then(async () => {
|
|
63
|
-
try {
|
|
64
|
-
bus.emit("shell:agent-exec-start", {});
|
|
65
|
-
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
66
|
-
command,
|
|
67
|
-
output: "",
|
|
68
|
-
cwd: "",
|
|
69
|
-
done: false,
|
|
70
|
-
});
|
|
71
|
-
bus.emit("shell:agent-exec-done", {});
|
|
72
|
-
resolve({ output: result.output, cwd: result.cwd });
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
bus.emit("shell:agent-exec-done", {});
|
|
76
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
-
reject(rpcError(-32000, message));
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
case "shell/cwd":
|
|
83
|
-
return { cwd: contextManager.getCwd() };
|
|
84
|
-
case "shell/info":
|
|
85
|
-
return {
|
|
86
|
-
shell: process.env.SHELL || "unknown",
|
|
87
|
-
agentSh: true,
|
|
88
|
-
};
|
|
89
|
-
case "shell/recall": {
|
|
90
|
-
const operation = params?.operation || "browse";
|
|
91
|
-
switch (operation) {
|
|
92
|
-
case "search": {
|
|
93
|
-
const query = params?.query;
|
|
94
|
-
if (typeof query !== "string" || !query) {
|
|
95
|
-
throw rpcError(-32602, "Missing required parameter: query");
|
|
96
|
-
}
|
|
97
|
-
return { result: contextManager.search(query) };
|
|
98
|
-
}
|
|
99
|
-
case "expand": {
|
|
100
|
-
const ids = params?.ids;
|
|
101
|
-
if (!Array.isArray(ids) || ids.length === 0) {
|
|
102
|
-
throw rpcError(-32602, "Missing required parameter: ids (array of numbers)");
|
|
103
|
-
}
|
|
104
|
-
const start = typeof params?.start === "number" ? params.start : undefined;
|
|
105
|
-
const end = typeof params?.end === "number" ? params.end : undefined;
|
|
106
|
-
return { result: contextManager.expand(ids.map(Number), start, end) };
|
|
107
|
-
}
|
|
108
|
-
case "browse":
|
|
109
|
-
return { result: contextManager.getRecentSummary() };
|
|
110
|
-
default:
|
|
111
|
-
throw rpcError(-32602, `Unknown recall operation: ${operation}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
default:
|
|
115
|
-
throw rpcError(-32601, `Method not found: ${method}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// ── Socket server ───────────────────────────────────────────────
|
|
119
|
-
const server = net.createServer((conn) => {
|
|
120
|
-
let buffer = "";
|
|
121
|
-
conn.on("data", (chunk) => {
|
|
122
|
-
buffer += chunk.toString();
|
|
123
|
-
// Process complete lines (newline-delimited JSON-RPC)
|
|
124
|
-
let newlineIdx;
|
|
125
|
-
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
126
|
-
const line = buffer.slice(0, newlineIdx).trim();
|
|
127
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
128
|
-
if (!line)
|
|
129
|
-
continue;
|
|
130
|
-
processMessage(conn, line);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
function processMessage(conn, line) {
|
|
135
|
-
let id = null;
|
|
136
|
-
try {
|
|
137
|
-
const msg = JSON.parse(line);
|
|
138
|
-
id = msg.id ?? null;
|
|
139
|
-
const method = msg.method;
|
|
140
|
-
if (!method) {
|
|
141
|
-
sendError(conn, id, -32600, "Invalid request: missing method");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
handleRequest(method, msg.params)
|
|
145
|
-
.then((result) => sendResult(conn, id, result))
|
|
146
|
-
.catch((err) => {
|
|
147
|
-
if (err && typeof err === "object" && "rpcCode" in err) {
|
|
148
|
-
sendError(conn, id, err.rpcCode, err.message);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
sendError(conn, id, -32603, String(err));
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
sendError(conn, id, -32700, "Parse error");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// Clean up stale socket file
|
|
160
|
-
try {
|
|
161
|
-
fs.unlinkSync(socketPath);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// Doesn't exist — fine
|
|
165
|
-
}
|
|
166
|
-
server.listen(socketPath);
|
|
167
|
-
// Cleanup on exit
|
|
168
|
-
const cleanup = () => {
|
|
169
|
-
server.close();
|
|
170
|
-
try {
|
|
171
|
-
fs.unlinkSync(socketPath);
|
|
172
|
-
}
|
|
173
|
-
catch { }
|
|
174
|
-
};
|
|
175
|
-
process.on("exit", cleanup);
|
|
176
|
-
}
|
|
177
|
-
// ── JSON-RPC helpers ──────────────────────────────────────────────
|
|
178
|
-
function sendResult(conn, id, result) {
|
|
179
|
-
conn.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
180
|
-
}
|
|
181
|
-
function sendError(conn, id, code, message) {
|
|
182
|
-
conn.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
183
|
-
}
|
|
184
|
-
function rpcError(code, message) {
|
|
185
|
-
const err = new Error(message);
|
|
186
|
-
err.rpcCode = code;
|
|
187
|
-
return err;
|
|
188
|
-
}
|
package/dist/mcp-server.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Minimal MCP server exposing a `user_shell` tool.
|
|
4
|
-
*
|
|
5
|
-
* Spawned by the ACP agent (pi-acp, claude-agent-acp, etc.) as an MCP
|
|
6
|
-
* stdio server. When the LLM calls `user_shell`, this process connects
|
|
7
|
-
* to agent-sh's Unix socket to execute the command in the user's live
|
|
8
|
-
* PTY shell.
|
|
9
|
-
*
|
|
10
|
-
* Protocol: MCP over stdio (newline-delimited JSON-RPC 2.0).
|
|
11
|
-
* No SDK dependency — the protocol surface is tiny.
|
|
12
|
-
*/
|
|
13
|
-
export {};
|
package/dist/mcp-server.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Minimal MCP server exposing a `user_shell` tool.
|
|
4
|
-
*
|
|
5
|
-
* Spawned by the ACP agent (pi-acp, claude-agent-acp, etc.) as an MCP
|
|
6
|
-
* stdio server. When the LLM calls `user_shell`, this process connects
|
|
7
|
-
* to agent-sh's Unix socket to execute the command in the user's live
|
|
8
|
-
* PTY shell.
|
|
9
|
-
*
|
|
10
|
-
* Protocol: MCP over stdio (newline-delimited JSON-RPC 2.0).
|
|
11
|
-
* No SDK dependency — the protocol surface is tiny.
|
|
12
|
-
*/
|
|
13
|
-
import { createConnection } from "node:net";
|
|
14
|
-
import { createInterface } from "node:readline";
|
|
15
|
-
const SOCKET_PATH = process.env.AGENT_SH_SOCKET;
|
|
16
|
-
// ── MCP protocol helpers ────────────────────────────────────────
|
|
17
|
-
function send(msg) {
|
|
18
|
-
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
|
|
19
|
-
}
|
|
20
|
-
function sendResult(id, result) {
|
|
21
|
-
send({ id, result });
|
|
22
|
-
}
|
|
23
|
-
function sendError(id, code, message) {
|
|
24
|
-
send({ id, error: { code, message } });
|
|
25
|
-
}
|
|
26
|
-
// ── Tool definition ─────────────────────────────────────────────
|
|
27
|
-
const SHELL_CWD_TOOL = {
|
|
28
|
-
name: "shell_cwd",
|
|
29
|
-
description: "Get the user's current working directory in their live shell. " +
|
|
30
|
-
"IMPORTANT: Your internal working directory may differ from the user's actual shell cwd — " +
|
|
31
|
-
"the user may have cd'd after your session started. Call this tool to get the real cwd " +
|
|
32
|
-
"before file operations if you're unsure.",
|
|
33
|
-
inputSchema: {
|
|
34
|
-
type: "object",
|
|
35
|
-
properties: {},
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
const USER_SHELL_TOOL = {
|
|
39
|
-
name: "user_shell",
|
|
40
|
-
description: "Execute a command in the user's live terminal session. " +
|
|
41
|
-
"Use this for commands that should affect the user's shell state: " +
|
|
42
|
-
"cd, export, source, pushd/popd, alias, etc. " +
|
|
43
|
-
"The command runs in the user's actual shell with their full environment " +
|
|
44
|
-
"(aliases, functions, PATH), not an isolated subprocess. " +
|
|
45
|
-
"NOTE: Your internal cwd may be stale — the user may have cd'd. " +
|
|
46
|
-
"Check the shell context for [shell cwd:...] labels or call shell_cwd " +
|
|
47
|
-
"to determine the real working directory. Use absolute paths when possible.",
|
|
48
|
-
inputSchema: {
|
|
49
|
-
type: "object",
|
|
50
|
-
properties: {
|
|
51
|
-
command: {
|
|
52
|
-
type: "string",
|
|
53
|
-
description: "Shell command to execute in the user's live terminal",
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
required: ["command"],
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
const SHELL_RECALL_TOOL = {
|
|
60
|
-
name: "shell_recall",
|
|
61
|
-
description: "Retrieve past shell commands, agent responses, and tool executions from the session history. " +
|
|
62
|
-
"Use this to look up truncated output, search for previous commands or errors, " +
|
|
63
|
-
"or browse recent exchanges. Each entry shows [shell cwd:...] so you can see " +
|
|
64
|
-
"which directory commands were run in. Operations: " +
|
|
65
|
-
'"browse" lists recent exchange summaries with line counts, ' +
|
|
66
|
-
'"search" finds exchanges matching a regex query, ' +
|
|
67
|
-
'"expand" retrieves content by exchange ID (use start/end for specific line ranges).',
|
|
68
|
-
inputSchema: {
|
|
69
|
-
type: "object",
|
|
70
|
-
properties: {
|
|
71
|
-
operation: {
|
|
72
|
-
type: "string",
|
|
73
|
-
enum: ["search", "expand", "browse"],
|
|
74
|
-
description: 'Operation to perform (default: "browse")',
|
|
75
|
-
},
|
|
76
|
-
query: {
|
|
77
|
-
type: "string",
|
|
78
|
-
description: 'Search query — supports regex (required for "search" operation)',
|
|
79
|
-
},
|
|
80
|
-
ids: {
|
|
81
|
-
type: "array",
|
|
82
|
-
items: { type: "number" },
|
|
83
|
-
description: 'Exchange IDs to expand (required for "expand" operation)',
|
|
84
|
-
},
|
|
85
|
-
start: {
|
|
86
|
-
type: "number",
|
|
87
|
-
description: "Start line number, 1-indexed (optional, for expand)",
|
|
88
|
-
},
|
|
89
|
-
end: {
|
|
90
|
-
type: "number",
|
|
91
|
-
description: "End line number, inclusive (optional, for expand)",
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
// ── agent-sh socket client (JSON-RPC 2.0) ──────────────────────
|
|
97
|
-
let rpcId = 0;
|
|
98
|
-
function callSocket(method, params) {
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
if (!SOCKET_PATH) {
|
|
101
|
-
reject(new Error("AGENT_SH_SOCKET not set — not running inside agent-sh"));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const conn = createConnection(SOCKET_PATH);
|
|
105
|
-
let buffer = "";
|
|
106
|
-
conn.on("connect", () => {
|
|
107
|
-
const msg = { jsonrpc: "2.0", id: ++rpcId, method, params: params ?? {} };
|
|
108
|
-
conn.write(JSON.stringify(msg) + "\n");
|
|
109
|
-
});
|
|
110
|
-
conn.on("data", (chunk) => {
|
|
111
|
-
buffer += chunk.toString();
|
|
112
|
-
const newlineIdx = buffer.indexOf("\n");
|
|
113
|
-
if (newlineIdx === -1)
|
|
114
|
-
return;
|
|
115
|
-
const line = buffer.slice(0, newlineIdx).trim();
|
|
116
|
-
conn.destroy();
|
|
117
|
-
try {
|
|
118
|
-
const response = JSON.parse(line);
|
|
119
|
-
if (response.error) {
|
|
120
|
-
reject(new Error(response.error.message || "RPC error"));
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
resolve(response.result);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
reject(new Error(`Invalid response from agent-sh: ${line}`));
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
conn.on("error", (err) => {
|
|
131
|
-
reject(new Error(`Failed to connect to agent-sh: ${err.message}`));
|
|
132
|
-
});
|
|
133
|
-
conn.setTimeout(35_000, () => {
|
|
134
|
-
conn.destroy();
|
|
135
|
-
reject(new Error("Connection to agent-sh timed out"));
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
// ── Request handler ─────────────────────────────────────────────
|
|
140
|
-
async function handleRequest(id, method, params) {
|
|
141
|
-
switch (method) {
|
|
142
|
-
case "initialize":
|
|
143
|
-
sendResult(id, {
|
|
144
|
-
protocolVersion: params?.protocolVersion ?? "2024-11-05",
|
|
145
|
-
capabilities: { tools: {} },
|
|
146
|
-
serverInfo: { name: "agent-sh-shell", version: "0.1.0" },
|
|
147
|
-
});
|
|
148
|
-
break;
|
|
149
|
-
case "notifications/initialized":
|
|
150
|
-
// Client acknowledgement — nothing to do
|
|
151
|
-
break;
|
|
152
|
-
case "tools/list":
|
|
153
|
-
sendResult(id, { tools: [SHELL_CWD_TOOL, USER_SHELL_TOOL, SHELL_RECALL_TOOL] });
|
|
154
|
-
break;
|
|
155
|
-
case "tools/call": {
|
|
156
|
-
const toolName = params?.name;
|
|
157
|
-
const args = params?.arguments ?? {};
|
|
158
|
-
try {
|
|
159
|
-
let text;
|
|
160
|
-
if (toolName === "shell_cwd") {
|
|
161
|
-
const result = await callSocket("shell/cwd", {});
|
|
162
|
-
text = `User's current working directory: ${result.cwd}`;
|
|
163
|
-
}
|
|
164
|
-
else if (toolName === "user_shell") {
|
|
165
|
-
const command = args.command;
|
|
166
|
-
if (!command || typeof command !== "string") {
|
|
167
|
-
sendError(id, -32602, "Missing required parameter: command");
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const result = await callSocket("shell/exec", { command });
|
|
171
|
-
text = result.output || "(no output)";
|
|
172
|
-
}
|
|
173
|
-
else if (toolName === "shell_recall") {
|
|
174
|
-
const result = await callSocket("shell/recall", {
|
|
175
|
-
operation: args.operation || "browse",
|
|
176
|
-
query: args.query,
|
|
177
|
-
ids: args.ids,
|
|
178
|
-
start: args.start,
|
|
179
|
-
end: args.end,
|
|
180
|
-
});
|
|
181
|
-
text = result.result || "(no results)";
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
sendError(id, -32602, `Unknown tool: ${toolName}`);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
sendResult(id, {
|
|
188
|
-
content: [{ type: "text", text }],
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
catch (err) {
|
|
192
|
-
sendResult(id, {
|
|
193
|
-
content: [
|
|
194
|
-
{
|
|
195
|
-
type: "text",
|
|
196
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
isError: true,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
default:
|
|
205
|
-
// Unknown methods: return method-not-found for requests (those with id)
|
|
206
|
-
if (id !== undefined && id !== null) {
|
|
207
|
-
sendError(id, -32601, `Method not found: ${method}`);
|
|
208
|
-
}
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// ── Main loop ───────────────────────────────────────────────────
|
|
213
|
-
const rl = createInterface({ input: process.stdin });
|
|
214
|
-
rl.on("line", (line) => {
|
|
215
|
-
if (!line.trim())
|
|
216
|
-
return;
|
|
217
|
-
try {
|
|
218
|
-
const msg = JSON.parse(line);
|
|
219
|
-
const { id, method, params } = msg;
|
|
220
|
-
if (method) {
|
|
221
|
-
handleRequest(id, method, params).catch((err) => {
|
|
222
|
-
if (id !== undefined && id !== null) {
|
|
223
|
-
sendError(id, -32603, String(err));
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
// Malformed JSON — ignore
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
rl.on("close", () => {
|
|
233
|
-
process.exit(0);
|
|
234
|
-
});
|
package/examples/pi-agent-sh.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi extension: agent-sh tools (user_shell + shell_recall).
|
|
3
|
-
*
|
|
4
|
-
* When running inside agent-sh, registers tools that communicate with
|
|
5
|
-
* the user's live terminal via a Unix domain socket (JSON-RPC 2.0).
|
|
6
|
-
*
|
|
7
|
-
* - user_shell: execute commands in the user's live PTY
|
|
8
|
-
* - shell_recall: search/expand/browse session exchange history
|
|
9
|
-
*
|
|
10
|
-
* Socket path comes from the AGENT_SH_SOCKET env var.
|
|
11
|
-
* When not running inside agent-sh, the extension silently does nothing.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
15
|
-
import { Type } from "@sinclair/typebox";
|
|
16
|
-
import { createConnection } from "node:net";
|
|
17
|
-
|
|
18
|
-
const SOCKET_PATH = process.env.AGENT_SH_SOCKET;
|
|
19
|
-
|
|
20
|
-
export default function (pi: ExtensionAPI): void {
|
|
21
|
-
if (!SOCKET_PATH) return; // Not running inside agent-sh
|
|
22
|
-
|
|
23
|
-
pi.registerTool({
|
|
24
|
-
name: "shell_cwd",
|
|
25
|
-
label: "Shell CWD",
|
|
26
|
-
description:
|
|
27
|
-
"Get the user's current working directory in their live shell. " +
|
|
28
|
-
"IMPORTANT: Your internal working directory may differ from the user's actual shell cwd — " +
|
|
29
|
-
"the user may have cd'd after your session started. Call this tool to get the real cwd " +
|
|
30
|
-
"before file operations if you're unsure.",
|
|
31
|
-
promptSnippet:
|
|
32
|
-
"Get the user's real shell cwd (may differ from your internal cwd).",
|
|
33
|
-
parameters: Type.Object({}),
|
|
34
|
-
|
|
35
|
-
async execute() {
|
|
36
|
-
const result = (await callSocket("shell/cwd", {})) as { cwd: string };
|
|
37
|
-
return {
|
|
38
|
-
content: [{ type: "text", text: `User's current working directory: ${result.cwd}` }],
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
pi.registerTool({
|
|
44
|
-
name: "user_shell",
|
|
45
|
-
label: "User Shell",
|
|
46
|
-
description:
|
|
47
|
-
"Execute a command in the user's live terminal session. " +
|
|
48
|
-
"Use this for commands that should affect the user's shell state: " +
|
|
49
|
-
"cd, export, source, pushd/popd, alias, etc. " +
|
|
50
|
-
"The command runs in the user's actual shell with their full environment " +
|
|
51
|
-
"(aliases, functions, PATH), not an isolated subprocess. " +
|
|
52
|
-
"NOTE: Your internal cwd may be stale — the user may have cd'd. " +
|
|
53
|
-
"Check the shell context for [shell cwd:...] labels or call shell_cwd " +
|
|
54
|
-
"to determine the real working directory. Use absolute paths when possible.",
|
|
55
|
-
promptSnippet:
|
|
56
|
-
"Run commands in the user's live shell (cd, export, source — affects their session). " +
|
|
57
|
-
"Your internal cwd may be stale — check shell context or use shell_cwd for the real cwd.",
|
|
58
|
-
parameters: Type.Object({
|
|
59
|
-
command: Type.String({ description: "Shell command to execute in the user's live terminal" }),
|
|
60
|
-
}),
|
|
61
|
-
|
|
62
|
-
async execute(_toolCallId, params) {
|
|
63
|
-
const result = (await callSocket("shell/exec", { command: params.command })) as {
|
|
64
|
-
output: string;
|
|
65
|
-
cwd: string;
|
|
66
|
-
};
|
|
67
|
-
return {
|
|
68
|
-
content: [{ type: "text", text: result.output || "(no output)" }],
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
pi.registerTool({
|
|
74
|
-
name: "shell_recall",
|
|
75
|
-
label: "Shell Recall",
|
|
76
|
-
description:
|
|
77
|
-
"Retrieve past shell commands, agent responses, and tool executions from the session history. " +
|
|
78
|
-
"Use this to look up truncated output, search for previous commands or errors, " +
|
|
79
|
-
"or browse recent exchanges. Each entry shows [shell cwd:...] so you can see " +
|
|
80
|
-
"which directory commands were run in. Operations: " +
|
|
81
|
-
'"browse" lists recent exchange summaries with line counts, ' +
|
|
82
|
-
'"search" finds exchanges matching a regex query, ' +
|
|
83
|
-
'"expand" retrieves content by exchange ID (use start/end for specific line ranges).',
|
|
84
|
-
promptSnippet:
|
|
85
|
-
"Look up session history — search past commands/output, expand truncated exchanges, or browse recent activity.",
|
|
86
|
-
parameters: Type.Object({
|
|
87
|
-
operation: Type.Optional(
|
|
88
|
-
Type.Union([Type.Literal("search"), Type.Literal("expand"), Type.Literal("browse")], {
|
|
89
|
-
description: 'Operation to perform (default: "browse")',
|
|
90
|
-
}),
|
|
91
|
-
),
|
|
92
|
-
query: Type.Optional(
|
|
93
|
-
Type.String({ description: 'Search query — supports regex (required for "search")' }),
|
|
94
|
-
),
|
|
95
|
-
ids: Type.Optional(
|
|
96
|
-
Type.Array(Type.Number(), {
|
|
97
|
-
description: 'Exchange IDs to expand (required for "expand")',
|
|
98
|
-
}),
|
|
99
|
-
),
|
|
100
|
-
start: Type.Optional(
|
|
101
|
-
Type.Number({ description: "Start line number, 1-indexed (optional, for expand)" }),
|
|
102
|
-
),
|
|
103
|
-
end: Type.Optional(
|
|
104
|
-
Type.Number({ description: "End line number, inclusive (optional, for expand)" }),
|
|
105
|
-
),
|
|
106
|
-
}),
|
|
107
|
-
|
|
108
|
-
async execute(_toolCallId, params) {
|
|
109
|
-
const result = (await callSocket("shell/recall", {
|
|
110
|
-
operation: params.operation || "browse",
|
|
111
|
-
query: params.query,
|
|
112
|
-
ids: params.ids,
|
|
113
|
-
start: params.start,
|
|
114
|
-
end: params.end,
|
|
115
|
-
})) as { result: string };
|
|
116
|
-
return {
|
|
117
|
-
content: [{ type: "text", text: result.result || "(no results)" }],
|
|
118
|
-
};
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// -- agent-sh socket client (JSON-RPC 2.0) --
|
|
124
|
-
|
|
125
|
-
let rpcId = 0;
|
|
126
|
-
|
|
127
|
-
function callSocket(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
128
|
-
return new Promise((resolve, reject) => {
|
|
129
|
-
const conn = createConnection(SOCKET_PATH!);
|
|
130
|
-
let buffer = "";
|
|
131
|
-
|
|
132
|
-
conn.on("connect", () => {
|
|
133
|
-
const msg = { jsonrpc: "2.0", id: ++rpcId, method, params: params ?? {} };
|
|
134
|
-
conn.write(JSON.stringify(msg) + "\n");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
conn.on("data", (chunk) => {
|
|
138
|
-
buffer += chunk.toString();
|
|
139
|
-
const newlineIdx = buffer.indexOf("\n");
|
|
140
|
-
if (newlineIdx === -1) return;
|
|
141
|
-
|
|
142
|
-
const line = buffer.slice(0, newlineIdx).trim();
|
|
143
|
-
conn.destroy();
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const response = JSON.parse(line);
|
|
147
|
-
if (response.error) {
|
|
148
|
-
reject(new Error(response.error.message || "RPC error"));
|
|
149
|
-
} else {
|
|
150
|
-
resolve(response.result);
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
reject(new Error(`Invalid response from agent-sh: ${line}`));
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
conn.on("error", (err) => {
|
|
158
|
-
reject(new Error(`Failed to connect to agent-sh: ${err.message}`));
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
conn.setTimeout(35_000, () => {
|
|
162
|
-
conn.destroy();
|
|
163
|
-
reject(new Error("Connection to agent-sh timed out"));
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
}
|