agent-sh 0.1.0 → 0.2.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 +50 -581
- package/dist/acp-client.js +37 -10
- package/dist/context-manager.d.ts +1 -1
- package/dist/context-manager.js +14 -14
- package/dist/event-bus.d.ts +25 -0
- package/dist/extensions/shell-exec.d.ts +24 -0
- package/dist/extensions/shell-exec.js +183 -0
- package/dist/extensions/tui-renderer.js +9 -3
- package/dist/index.js +42 -0
- package/dist/input-handler.d.ts +3 -3
- package/dist/input-handler.js +97 -124
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.js +205 -0
- package/dist/output-parser.d.ts +5 -26
- package/dist/output-parser.js +16 -78
- package/dist/shell.d.ts +8 -4
- package/dist/shell.js +46 -8
- package/dist/types.d.ts +2 -0
- package/dist/utils/line-editor.d.ts +39 -0
- package/dist/utils/line-editor.js +287 -0
- package/dist/utils/tool-display.d.ts +9 -0
- package/dist/utils/tool-display.js +63 -8
- package/package.json +1 -1
package/dist/acp-client.js
CHANGED
|
@@ -32,10 +32,12 @@ export class AcpClient {
|
|
|
32
32
|
}
|
|
33
33
|
async start() {
|
|
34
34
|
this.log(`Starting agent: ${this.config.agentCommand} ${this.config.agentArgs.join(" ")}`);
|
|
35
|
-
// Spawn the agent subprocess
|
|
36
|
-
//
|
|
35
|
+
// Spawn the agent subprocess with the user's full shell environment
|
|
36
|
+
// (includes vars from .zshrc/.bashrc that process.env may not have)
|
|
37
|
+
const agentEnv = this.config.shellEnv ?? process.env;
|
|
37
38
|
this.agentProcess = spawn(this.config.agentCommand, this.config.agentArgs, {
|
|
38
39
|
stdio: ["pipe", "pipe", process.env.DEBUG ? "inherit" : "ignore"],
|
|
40
|
+
env: agentEnv,
|
|
39
41
|
});
|
|
40
42
|
// Catch spawn errors (ENOENT, EACCES, etc.) before proceeding
|
|
41
43
|
await new Promise((resolve, reject) => {
|
|
@@ -87,13 +89,17 @@ export class AcpClient {
|
|
|
87
89
|
};
|
|
88
90
|
this.log(`Agent info: ${this.agentInfo.name} v${this.agentInfo.version}`);
|
|
89
91
|
}
|
|
90
|
-
// Create a session
|
|
92
|
+
// Create a session — let extensions add MCP servers via pipe
|
|
91
93
|
const cwd = this.contextManager.getCwd();
|
|
92
94
|
this.log(`Creating new session with cwd: ${cwd}`);
|
|
93
|
-
const
|
|
95
|
+
const sessionConfig = this.bus.emitPipe("session:configure", {
|
|
94
96
|
cwd,
|
|
95
97
|
mcpServers: [],
|
|
96
98
|
});
|
|
99
|
+
const sessionResponse = await this.connection.newSession({
|
|
100
|
+
cwd: sessionConfig.cwd,
|
|
101
|
+
mcpServers: sessionConfig.mcpServers,
|
|
102
|
+
});
|
|
97
103
|
this.sessionId = sessionResponse.sessionId;
|
|
98
104
|
this.log(`Session created: ${this.sessionId}`);
|
|
99
105
|
}
|
|
@@ -185,10 +191,14 @@ export class AcpClient {
|
|
|
185
191
|
async resetSession() {
|
|
186
192
|
if (!this.connection)
|
|
187
193
|
return;
|
|
188
|
-
const
|
|
194
|
+
const sessionConfig = this.bus.emitPipe("session:configure", {
|
|
189
195
|
cwd: this.contextManager.getCwd(),
|
|
190
196
|
mcpServers: [],
|
|
191
197
|
});
|
|
198
|
+
const sessionResponse = await this.connection.newSession({
|
|
199
|
+
cwd: sessionConfig.cwd,
|
|
200
|
+
mcpServers: sessionConfig.mcpServers,
|
|
201
|
+
});
|
|
192
202
|
this.sessionId = sessionResponse.sessionId;
|
|
193
203
|
this.lastResponseText = "";
|
|
194
204
|
this.currentResponseText = "";
|
|
@@ -282,23 +292,39 @@ export class AcpClient {
|
|
|
282
292
|
break;
|
|
283
293
|
}
|
|
284
294
|
case "tool_call": {
|
|
285
|
-
// Use toolCallId if available, otherwise generate a simple ID
|
|
286
295
|
const toolId = update.toolCallId || `tool-${this.pendingToolCounter++}`;
|
|
287
296
|
this.pendingToolCalls.set(toolId, true);
|
|
288
|
-
this.bus.emit("agent:tool-started", {
|
|
297
|
+
this.bus.emit("agent:tool-started", {
|
|
298
|
+
title: update.title,
|
|
299
|
+
toolCallId: toolId,
|
|
300
|
+
kind: update.kind ?? undefined,
|
|
301
|
+
locations: update.locations?.map((l) => ({ path: l.path, line: l.line })),
|
|
302
|
+
rawInput: update.rawInput,
|
|
303
|
+
});
|
|
289
304
|
break;
|
|
290
305
|
}
|
|
291
306
|
case "tool_call_update": {
|
|
292
|
-
//
|
|
307
|
+
// Stream tool output content (text from pi's internal tool results)
|
|
308
|
+
if (update.content && Array.isArray(update.content)) {
|
|
309
|
+
for (const block of update.content) {
|
|
310
|
+
if (block.type === "content" && block.content?.type === "text" && block.content.text) {
|
|
311
|
+
this.bus.emit("agent:tool-output-chunk", { chunk: block.content.text });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
293
315
|
if (update.status === "completed" || update.status === "failed") {
|
|
294
316
|
const toolId = update.toolCallId;
|
|
295
317
|
const exitCode = update.status === "completed" ? 0 : 1;
|
|
296
318
|
if (toolId && this.pendingToolCalls.has(toolId)) {
|
|
297
319
|
this.pendingToolCalls.delete(toolId);
|
|
298
|
-
this.bus.emit("agent:tool-completed", {
|
|
320
|
+
this.bus.emit("agent:tool-completed", {
|
|
321
|
+
toolCallId: toolId,
|
|
322
|
+
exitCode,
|
|
323
|
+
rawOutput: update.rawOutput,
|
|
324
|
+
});
|
|
299
325
|
}
|
|
300
326
|
else if (!toolId) {
|
|
301
|
-
this.bus.emit("agent:tool-completed", { exitCode });
|
|
327
|
+
this.bus.emit("agent:tool-completed", { exitCode, rawOutput: update.rawOutput });
|
|
302
328
|
}
|
|
303
329
|
}
|
|
304
330
|
break;
|
|
@@ -363,6 +389,7 @@ export class AcpClient {
|
|
|
363
389
|
const { session, done } = executeCommand({
|
|
364
390
|
command: fullCommand,
|
|
365
391
|
cwd,
|
|
392
|
+
env: this.config.shellEnv,
|
|
366
393
|
timeout: 60_000,
|
|
367
394
|
maxOutputBytes: 256 * 1024,
|
|
368
395
|
onOutput: (chunk) => {
|
package/dist/context-manager.js
CHANGED
|
@@ -165,21 +165,21 @@ export class ContextManager {
|
|
|
165
165
|
return recent.map((ex) => this.exchangeOneLiner(ex)).join("\n");
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
|
-
* Parse and handle
|
|
168
|
+
* Parse and handle shell_recall commands.
|
|
169
169
|
*/
|
|
170
170
|
handleRecallCommand(command) {
|
|
171
|
-
const args = command.replace(/^
|
|
171
|
+
const args = command.replace(/^_*shell_recall\s*/, "").trim();
|
|
172
172
|
if (!args || args === "--help") {
|
|
173
173
|
return [
|
|
174
174
|
"Usage:",
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
175
|
+
" shell_recall Browse recent exchanges",
|
|
176
|
+
" shell_recall --search <query> Search all exchanges",
|
|
177
|
+
" shell_recall --expand <id,...> Show full content of exchanges",
|
|
178
178
|
"",
|
|
179
179
|
"Examples:",
|
|
180
|
-
'
|
|
181
|
-
"
|
|
182
|
-
"
|
|
180
|
+
' shell_recall --search "test fail"',
|
|
181
|
+
" shell_recall --expand 41",
|
|
182
|
+
" shell_recall --expand 41,42,43",
|
|
183
183
|
].join("\n");
|
|
184
184
|
}
|
|
185
185
|
const searchMatch = args.match(/^--search\s+(?:"([^"]+)"|(\S+))/);
|
|
@@ -232,13 +232,13 @@ export class ContextManager {
|
|
|
232
232
|
const ex = result[i];
|
|
233
233
|
const before = this.exchangeSize(ex);
|
|
234
234
|
if (ex.type === "shell_command") {
|
|
235
|
-
ex.output = `[output omitted, use
|
|
235
|
+
ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
236
236
|
}
|
|
237
237
|
else if (ex.type === "tool_execution") {
|
|
238
|
-
ex.output = `[output omitted, use
|
|
238
|
+
ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
239
239
|
}
|
|
240
240
|
else if (ex.type === "agent_response") {
|
|
241
|
-
ex.response = `[response omitted, use
|
|
241
|
+
ex.response = `[response omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
242
242
|
}
|
|
243
243
|
totalSize -= before - this.exchangeSize(ex);
|
|
244
244
|
}
|
|
@@ -250,7 +250,7 @@ export class ContextManager {
|
|
|
250
250
|
let out = "<shell_context>\n";
|
|
251
251
|
out += `cwd: ${this.currentCwd}\n`;
|
|
252
252
|
out += `session: ${totalCount} exchanges, ${elapsed}m elapsed\n`;
|
|
253
|
-
out += `[hint:
|
|
253
|
+
out += `[hint: use the shell_recall tool to retrieve truncated content — search(query) or expand(ids)]\n`;
|
|
254
254
|
for (const ex of exchanges) {
|
|
255
255
|
out += "\n" + this.formatExchangeTruncated(ex);
|
|
256
256
|
}
|
|
@@ -381,7 +381,7 @@ function truncateOutput(text, threshold, headLines, tailLines, id) {
|
|
|
381
381
|
const omitted = lines.length - headLines - tailLines;
|
|
382
382
|
return [
|
|
383
383
|
...lines.slice(0, headLines),
|
|
384
|
-
`[... ${omitted} lines truncated, use
|
|
384
|
+
`[... ${omitted} lines truncated, use shell_recall tool with expand and id ${id} to see full output ...]`,
|
|
385
385
|
...lines.slice(-tailLines),
|
|
386
386
|
].join("\n");
|
|
387
387
|
}
|
|
@@ -391,7 +391,7 @@ function truncateHead(text, threshold, headLines, id) {
|
|
|
391
391
|
return text;
|
|
392
392
|
return [
|
|
393
393
|
...lines.slice(0, headLines),
|
|
394
|
-
`[... truncated, use
|
|
394
|
+
`[... truncated, use shell_recall tool with expand and id ${id} for full response ...]`,
|
|
395
395
|
].join("\n");
|
|
396
396
|
}
|
|
397
397
|
function indent(text, prefix) {
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -52,10 +52,17 @@ export interface ShellEvents {
|
|
|
52
52
|
"agent:tool-started": {
|
|
53
53
|
title: string;
|
|
54
54
|
toolCallId?: string;
|
|
55
|
+
kind?: string;
|
|
56
|
+
locations?: {
|
|
57
|
+
path: string;
|
|
58
|
+
line?: number | null;
|
|
59
|
+
}[];
|
|
60
|
+
rawInput?: unknown;
|
|
55
61
|
};
|
|
56
62
|
"agent:tool-completed": {
|
|
57
63
|
toolCallId?: string;
|
|
58
64
|
exitCode: number | null;
|
|
65
|
+
rawOutput?: unknown;
|
|
59
66
|
};
|
|
60
67
|
"agent:tool-output-chunk": {
|
|
61
68
|
chunk: string;
|
|
@@ -89,6 +96,24 @@ export interface ShellEvents {
|
|
|
89
96
|
cwd: string;
|
|
90
97
|
handled: boolean;
|
|
91
98
|
};
|
|
99
|
+
"shell:exec-request": {
|
|
100
|
+
command: string;
|
|
101
|
+
output: string;
|
|
102
|
+
cwd: string;
|
|
103
|
+
done: boolean;
|
|
104
|
+
};
|
|
105
|
+
"session:configure": {
|
|
106
|
+
cwd: string;
|
|
107
|
+
mcpServers: {
|
|
108
|
+
name: string;
|
|
109
|
+
command: string;
|
|
110
|
+
args: string[];
|
|
111
|
+
env: {
|
|
112
|
+
name: string;
|
|
113
|
+
value: string;
|
|
114
|
+
}[];
|
|
115
|
+
}[];
|
|
116
|
+
};
|
|
92
117
|
"autocomplete:request": {
|
|
93
118
|
buffer: string;
|
|
94
119
|
items: {
|
|
@@ -0,0 +1,24 @@
|
|
|
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 type { ExtensionContext } from "../types.js";
|
|
22
|
+
export default function activate({ bus, contextManager }: ExtensionContext, opts: {
|
|
23
|
+
socketPath: string;
|
|
24
|
+
}): void;
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
export default function activate({ bus, contextManager }, opts) {
|
|
27
|
+
const { socketPath } = opts;
|
|
28
|
+
// Register MCP server so ACP agents discover the user_shell tool
|
|
29
|
+
bus.onPipe("session:configure", (payload) => {
|
|
30
|
+
return {
|
|
31
|
+
...payload,
|
|
32
|
+
mcpServers: [
|
|
33
|
+
...payload.mcpServers,
|
|
34
|
+
{
|
|
35
|
+
name: "agent-sh",
|
|
36
|
+
command: process.execPath,
|
|
37
|
+
args: [path.join(__dirname, "..", "mcp-server.js")],
|
|
38
|
+
env: [{ name: "AGENT_SH_SOCKET", value: socketPath }],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
// Also set AGENT_SH_SOCKET for pi extensions that connect directly
|
|
44
|
+
process.env.AGENT_SH_SOCKET = socketPath;
|
|
45
|
+
// Serialize shell/exec requests — only one PTY command at a time
|
|
46
|
+
let execPending = Promise.resolve();
|
|
47
|
+
// ── JSON-RPC handler ────────────────────────────────────────────
|
|
48
|
+
async function handleRequest(method, params) {
|
|
49
|
+
switch (method) {
|
|
50
|
+
case "shell/exec": {
|
|
51
|
+
const command = params?.command;
|
|
52
|
+
if (typeof command !== "string" || !command) {
|
|
53
|
+
throw rpcError(-32602, "Missing required parameter: command");
|
|
54
|
+
}
|
|
55
|
+
// Serialize — one PTY command at a time
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
execPending = execPending.then(async () => {
|
|
58
|
+
try {
|
|
59
|
+
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
60
|
+
command,
|
|
61
|
+
output: "",
|
|
62
|
+
cwd: "",
|
|
63
|
+
done: false,
|
|
64
|
+
});
|
|
65
|
+
// Show the command output in the TUI
|
|
66
|
+
if (result.output) {
|
|
67
|
+
bus.emit("agent:tool-output-chunk", { chunk: result.output });
|
|
68
|
+
}
|
|
69
|
+
resolve({ output: result.output, cwd: result.cwd });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
bus.emit("agent:tool-output-chunk", { chunk: `Error: ${message}` });
|
|
74
|
+
reject(rpcError(-32000, message));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
case "shell/cwd":
|
|
80
|
+
return { cwd: contextManager.getCwd() };
|
|
81
|
+
case "shell/info":
|
|
82
|
+
return {
|
|
83
|
+
shell: process.env.SHELL || "unknown",
|
|
84
|
+
agentSh: true,
|
|
85
|
+
};
|
|
86
|
+
case "shell/recall": {
|
|
87
|
+
const operation = params?.operation || "browse";
|
|
88
|
+
switch (operation) {
|
|
89
|
+
case "search": {
|
|
90
|
+
const query = params?.query;
|
|
91
|
+
if (typeof query !== "string" || !query) {
|
|
92
|
+
throw rpcError(-32602, "Missing required parameter: query");
|
|
93
|
+
}
|
|
94
|
+
return { result: contextManager.search(query) };
|
|
95
|
+
}
|
|
96
|
+
case "expand": {
|
|
97
|
+
const ids = params?.ids;
|
|
98
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
99
|
+
throw rpcError(-32602, "Missing required parameter: ids (array of numbers)");
|
|
100
|
+
}
|
|
101
|
+
return { result: contextManager.expand(ids.map(Number)) };
|
|
102
|
+
}
|
|
103
|
+
case "browse":
|
|
104
|
+
return { result: contextManager.getRecentSummary() };
|
|
105
|
+
default:
|
|
106
|
+
throw rpcError(-32602, `Unknown recall operation: ${operation}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
throw rpcError(-32601, `Method not found: ${method}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── Socket server ───────────────────────────────────────────────
|
|
114
|
+
const server = net.createServer((conn) => {
|
|
115
|
+
let buffer = "";
|
|
116
|
+
conn.on("data", (chunk) => {
|
|
117
|
+
buffer += chunk.toString();
|
|
118
|
+
// Process complete lines (newline-delimited JSON-RPC)
|
|
119
|
+
let newlineIdx;
|
|
120
|
+
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
121
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
122
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
123
|
+
if (!line)
|
|
124
|
+
continue;
|
|
125
|
+
processMessage(conn, line);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
function processMessage(conn, line) {
|
|
130
|
+
let id = null;
|
|
131
|
+
try {
|
|
132
|
+
const msg = JSON.parse(line);
|
|
133
|
+
id = msg.id ?? null;
|
|
134
|
+
const method = msg.method;
|
|
135
|
+
if (!method) {
|
|
136
|
+
sendError(conn, id, -32600, "Invalid request: missing method");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
handleRequest(method, msg.params)
|
|
140
|
+
.then((result) => sendResult(conn, id, result))
|
|
141
|
+
.catch((err) => {
|
|
142
|
+
if (err && typeof err === "object" && "rpcCode" in err) {
|
|
143
|
+
sendError(conn, id, err.rpcCode, err.message);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
sendError(conn, id, -32603, String(err));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
sendError(conn, id, -32700, "Parse error");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Clean up stale socket file
|
|
155
|
+
try {
|
|
156
|
+
fs.unlinkSync(socketPath);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Doesn't exist — fine
|
|
160
|
+
}
|
|
161
|
+
server.listen(socketPath);
|
|
162
|
+
// Cleanup on exit
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
server.close();
|
|
165
|
+
try {
|
|
166
|
+
fs.unlinkSync(socketPath);
|
|
167
|
+
}
|
|
168
|
+
catch { }
|
|
169
|
+
};
|
|
170
|
+
process.on("exit", cleanup);
|
|
171
|
+
}
|
|
172
|
+
// ── JSON-RPC helpers ──────────────────────────────────────────────
|
|
173
|
+
function sendResult(conn, id, result) {
|
|
174
|
+
conn.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
175
|
+
}
|
|
176
|
+
function sendError(conn, id, code, message) {
|
|
177
|
+
conn.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
178
|
+
}
|
|
179
|
+
function rpcError(code, message) {
|
|
180
|
+
const err = new Error(message);
|
|
181
|
+
err.rpcCode = code;
|
|
182
|
+
return err;
|
|
183
|
+
}
|
|
@@ -62,7 +62,7 @@ export default function activate({ bus }) {
|
|
|
62
62
|
});
|
|
63
63
|
bus.on("agent:tool-started", (e) => {
|
|
64
64
|
stopCurrentSpinner();
|
|
65
|
-
showToolCall(e.title, lastCommand);
|
|
65
|
+
showToolCall(e.title, lastCommand, e);
|
|
66
66
|
lastCommand = "";
|
|
67
67
|
});
|
|
68
68
|
bus.on("agent:tool-completed", (e) => showToolComplete(e.exitCode));
|
|
@@ -169,13 +169,19 @@ export default function activate({ bus }) {
|
|
|
169
169
|
renderer.push(text);
|
|
170
170
|
flushOutput();
|
|
171
171
|
}
|
|
172
|
-
function showToolCall(title, command) {
|
|
172
|
+
function showToolCall(title, command, extra) {
|
|
173
173
|
stopCurrentSpinner();
|
|
174
174
|
if (!renderer)
|
|
175
175
|
startAgentResponse();
|
|
176
176
|
renderer.flush();
|
|
177
177
|
const termW = process.stdout.columns || 80;
|
|
178
|
-
const lines = renderToolCall({
|
|
178
|
+
const lines = renderToolCall({
|
|
179
|
+
title,
|
|
180
|
+
command: command || undefined,
|
|
181
|
+
kind: extra?.kind,
|
|
182
|
+
locations: extra?.locations,
|
|
183
|
+
rawInput: extra?.rawInput,
|
|
184
|
+
}, termW);
|
|
179
185
|
for (const line of lines) {
|
|
180
186
|
renderer.writeLine(line);
|
|
181
187
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { Shell } from "./shell.js";
|
|
3
4
|
import { createCore } from "./core.js";
|
|
4
5
|
import { palette as p } from "./utils/palette.js";
|
|
@@ -6,7 +7,39 @@ import tuiRenderer from "./extensions/tui-renderer.js";
|
|
|
6
7
|
import slashCommands from "./extensions/slash-commands.js";
|
|
7
8
|
import fileAutocomplete from "./extensions/file-autocomplete.js";
|
|
8
9
|
import shellRecall from "./extensions/shell-recall.js";
|
|
10
|
+
import shellExec from "./extensions/shell-exec.js";
|
|
9
11
|
import { loadExtensions } from "./extension-loader.js";
|
|
12
|
+
/**
|
|
13
|
+
* Capture the user's full shell environment by running a quick interactive
|
|
14
|
+
* subshell. This picks up env vars exported in .zshrc/.bashrc that the
|
|
15
|
+
* Node.js process (which was spawned before the PTY sources rc files)
|
|
16
|
+
* doesn't have.
|
|
17
|
+
*/
|
|
18
|
+
function captureShellEnv(shell) {
|
|
19
|
+
try {
|
|
20
|
+
const output = execFileSync(shell, ["-i", "-c", "env -0"], {
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
timeout: 5000,
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"], // suppress interactive noise on stderr
|
|
24
|
+
});
|
|
25
|
+
const env = {};
|
|
26
|
+
for (const entry of output.split("\0")) {
|
|
27
|
+
const eq = entry.indexOf("=");
|
|
28
|
+
if (eq > 0)
|
|
29
|
+
env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
30
|
+
}
|
|
31
|
+
return env;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Fallback: use Node's own environment
|
|
35
|
+
const env = {};
|
|
36
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
37
|
+
if (v !== undefined)
|
|
38
|
+
env[k] = v;
|
|
39
|
+
}
|
|
40
|
+
return env;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
10
43
|
function parseArgs(argv) {
|
|
11
44
|
// Priority: CLI args > Environment variables > Config file > Defaults
|
|
12
45
|
const defaultAgent = process.env.AGENT_SH_AGENT || "pi-acp";
|
|
@@ -92,6 +125,9 @@ function formatAgentInfo(agentInfo, model) {
|
|
|
92
125
|
}
|
|
93
126
|
async function main() {
|
|
94
127
|
const config = parseArgs(process.argv.slice(2));
|
|
128
|
+
// Capture the user's shell environment (picks up vars from .zshrc/.bashrc
|
|
129
|
+
// that the Node process doesn't have)
|
|
130
|
+
config.shellEnv = captureShellEnv(config.shell || process.env.SHELL || "/bin/bash");
|
|
95
131
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
96
132
|
const core = createCore(config);
|
|
97
133
|
const { bus, client } = core;
|
|
@@ -130,6 +166,12 @@ async function main() {
|
|
|
130
166
|
slashCommands(extCtx);
|
|
131
167
|
fileAutocomplete(extCtx);
|
|
132
168
|
shellRecall(extCtx);
|
|
169
|
+
// Shell-exec: start the Unix socket bridge so the MCP server can
|
|
170
|
+
// route user_shell tool calls to the PTY via the EventBus.
|
|
171
|
+
const tmpDir = shell.getTmpDir();
|
|
172
|
+
if (tmpDir) {
|
|
173
|
+
shellExec(extCtx, { socketPath: `${tmpDir}/shell.sock` });
|
|
174
|
+
}
|
|
133
175
|
await loadExtensions(extCtx, config.extensions);
|
|
134
176
|
// ── Agent connection (async — don't block shell startup) ──────
|
|
135
177
|
core.start().catch((err) => {
|
package/dist/input-handler.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare class InputHandler {
|
|
|
17
17
|
private ctx;
|
|
18
18
|
private lineBuffer;
|
|
19
19
|
private agentInputMode;
|
|
20
|
-
private
|
|
20
|
+
private editor;
|
|
21
21
|
private autocompleteActive;
|
|
22
22
|
private autocompleteIndex;
|
|
23
23
|
private autocompleteItems;
|
|
@@ -32,7 +32,7 @@ export declare class InputHandler {
|
|
|
32
32
|
model?: string;
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
|
-
/** Write the agent prompt line
|
|
35
|
+
/** Write the agent prompt line with cursor at the correct position. */
|
|
36
36
|
private writeAgentPromptLine;
|
|
37
37
|
handleInput(data: string): void;
|
|
38
38
|
private enterAgentInputMode;
|
|
@@ -41,8 +41,8 @@ export declare class InputHandler {
|
|
|
41
41
|
private renderAgentInput;
|
|
42
42
|
private updateAutocomplete;
|
|
43
43
|
private renderAutocomplete;
|
|
44
|
-
private clearAutocompleteLines;
|
|
45
44
|
private applyAutocomplete;
|
|
46
45
|
private dismissAutocomplete;
|
|
46
|
+
private clearAutocompleteLines;
|
|
47
47
|
private handleAgentInput;
|
|
48
48
|
}
|