agent-sh 0.1.0 → 0.3.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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } 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,67 @@ 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 asynchronously.
14
+ * This picks up env vars exported in .zshrc/.bashrc that the
15
+ * Node.js process doesn't have.
16
+ *
17
+ * Uses -l (login shell) instead of -i to avoid TTY blocking issues.
18
+ */
19
+ async function captureShellEnvAsync(shell) {
20
+ return new Promise((resolve) => {
21
+ try {
22
+ const child = spawn(shell, ["-l", "-c", "env -0"], {
23
+ stdio: ["ignore", "pipe", "ignore"],
24
+ timeout: 5000,
25
+ });
26
+ let output = "";
27
+ child.stdout?.on("data", (data) => {
28
+ output += data.toString("utf-8");
29
+ });
30
+ child.on("close", (code) => {
31
+ if (code !== 0 || !output) {
32
+ resolve({}); // Return empty to trigger fallback
33
+ return;
34
+ }
35
+ const env = {};
36
+ for (const entry of output.split("\0")) {
37
+ const eq = entry.indexOf("=");
38
+ if (eq > 0)
39
+ env[entry.slice(0, eq)] = entry.slice(eq + 1);
40
+ }
41
+ resolve(env);
42
+ });
43
+ child.on("error", () => {
44
+ resolve({}); // Return empty to trigger fallback
45
+ });
46
+ // Safety timeout
47
+ setTimeout(() => {
48
+ child.kill("SIGTERM");
49
+ resolve({});
50
+ }, 5000);
51
+ }
52
+ catch {
53
+ resolve({});
54
+ }
55
+ });
56
+ }
57
+ /**
58
+ * Merge captured shell env into base env, only adding keys that don't exist.
59
+ * This preserves any runtime modifications while adding missing shell vars.
60
+ */
61
+ function mergeShellEnv(baseEnv, shellEnv) {
62
+ const merged = { ...baseEnv };
63
+ for (const [key, value] of Object.entries(shellEnv)) {
64
+ // Only add if key doesn't exist or is empty in base env
65
+ if (!(key in merged) || !merged[key]) {
66
+ merged[key] = value;
67
+ }
68
+ }
69
+ return merged;
70
+ }
10
71
  function parseArgs(argv) {
11
72
  // Priority: CLI args > Environment variables > Config file > Defaults
12
73
  const defaultAgent = process.env.AGENT_SH_AGENT || "pi-acp";
@@ -78,7 +139,7 @@ Inside the shell:
78
139
  }
79
140
  return { agentCommand, agentArgs, shell, model, extensions };
80
141
  }
81
- function formatAgentInfo(agentInfo, model) {
142
+ function formatAgentInfo(agentInfo, model, thoughtLevel) {
82
143
  const name = agentInfo.name.replace(/-acp$/, "").replace(/-/g, " ");
83
144
  let infoStr = `${p.dim}${name}${p.reset}`;
84
145
  if (model) {
@@ -88,14 +149,51 @@ function formatAgentInfo(agentInfo, model) {
88
149
  .replace(/^google\//i, "");
89
150
  infoStr += ` ${p.dim}(${cleanModel})${p.reset}`;
90
151
  }
152
+ if (thoughtLevel) {
153
+ // Clean up verbose mode names like "Thinking: medium" → "medium"
154
+ const label = thoughtLevel.replace(/^Thinking:\s*/i, "");
155
+ infoStr += ` ${p.dim}[${label}]${p.reset}`;
156
+ }
91
157
  return `${infoStr} ${p.success}●${p.reset}`;
92
158
  }
93
159
  async function main() {
160
+ // Set up signal handlers before any terminal operations.
161
+ // Ignore SIGTTOU to prevent suspension when modifying terminal settings.
162
+ process.on("SIGTTOU", () => { });
163
+ // Also ignore SIGTTIN which can occur when reading from terminal while backgrounded.
164
+ process.on("SIGTTIN", () => { });
94
165
  const config = parseArgs(process.argv.slice(2));
166
+ // Start with current process environment (fast, non-blocking)
167
+ // We'll enrich it with shell env asynchronously in the background
168
+ const baseEnv = {};
169
+ for (const [k, v] of Object.entries(process.env)) {
170
+ if (v !== undefined)
171
+ baseEnv[k] = v;
172
+ }
173
+ config.shellEnv = baseEnv;
174
+ // Asynchronously capture full shell environment without blocking startup
175
+ const shellPath = config.shell || process.env.SHELL || "/bin/bash";
176
+ captureShellEnvAsync(shellPath).then((shellEnv) => {
177
+ if (Object.keys(shellEnv).length > 0) {
178
+ const merged = mergeShellEnv(config.shellEnv, shellEnv);
179
+ config.shellEnv = merged;
180
+ if (process.env.DEBUG) {
181
+ console.error('[agent-sh] Shell environment enriched asynchronously');
182
+ }
183
+ }
184
+ }).catch(() => {
185
+ // Ignore errors, we already have process.env as fallback
186
+ });
187
+ if (process.env.DEBUG) {
188
+ console.error('[agent-sh] Using current process environment (async enrichment pending)');
189
+ }
95
190
  // ── Core (frontend-agnostic) ──────────────────────────────────
96
191
  const core = createCore(config);
97
192
  const { bus, client } = core;
98
193
  // ── Interactive frontend ──────────────────────────────────────
194
+ if (process.env.DEBUG) {
195
+ console.error('[agent-sh] Setting up interactive frontend...');
196
+ }
99
197
  process.stdout.write(`\x1b]0;agent-sh\x07`);
100
198
  const cols = process.stdout.columns || 80;
101
199
  const rows = process.stdout.rows || 24;
@@ -107,6 +205,12 @@ async function main() {
107
205
  }
108
206
  process.exit(0);
109
207
  };
208
+ if (process.env.DEBUG) {
209
+ console.error('[agent-sh] Creating Shell...');
210
+ }
211
+ // Small delay on macOS to ensure we're fully in the foreground process group
212
+ // before spawning the PTY. This prevents SIGTTOU suspension.
213
+ await new Promise(resolve => setTimeout(resolve, 100));
110
214
  const shell = new Shell({
111
215
  bus,
112
216
  cols,
@@ -118,26 +222,86 @@ async function main() {
118
222
  const agentInfo = client.getAgentInfo();
119
223
  const model = client.getModel();
120
224
  if (agentInfo) {
121
- return { info: formatAgentInfo(agentInfo, model) };
225
+ const mode = client.getCurrentMode();
226
+ return { info: formatAgentInfo(agentInfo, model, mode?.name ?? null) };
122
227
  }
123
228
  }
124
229
  return { info: "" };
125
230
  },
126
231
  });
232
+ if (process.env.DEBUG) {
233
+ console.error('[agent-sh] Shell created');
234
+ }
127
235
  // ── Extensions ────────────────────────────────────────────────
236
+ if (process.env.DEBUG) {
237
+ console.error('[agent-sh] Setting up extensions...');
238
+ }
128
239
  const extCtx = core.extensionContext({ quit: cleanup });
129
240
  tuiRenderer(extCtx);
130
241
  slashCommands(extCtx);
131
242
  fileAutocomplete(extCtx);
132
243
  shellRecall(extCtx);
133
- await loadExtensions(extCtx, config.extensions);
244
+ // Shell-exec: start the Unix domain socket bridge so agent extensions
245
+ // and MCP servers can route tool calls to the PTY via the EventBus.
246
+ const tmpDir = shell.getTmpDir();
247
+ if (tmpDir) {
248
+ if (process.env.DEBUG) {
249
+ console.error('[agent-sh] Starting shell-exec socket server...');
250
+ }
251
+ shellExec(extCtx, { socketPath: `${tmpDir}/shell.sock` });
252
+ }
253
+ // Load extensions with timeout to prevent blocking startup
254
+ if (process.env.DEBUG) {
255
+ console.error('[agent-sh] Loading extensions...');
256
+ }
257
+ const loadExtensionsTimeoutMs = 10000; // 10 seconds
258
+ await Promise.race([
259
+ loadExtensions(extCtx, config.extensions),
260
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
261
+ ]).catch((err) => {
262
+ console.error(`Warning: ${err.message}`);
263
+ });
264
+ if (process.env.DEBUG) {
265
+ console.error('[agent-sh] Extensions loaded');
266
+ }
134
267
  // ── Agent connection (async — don't block shell startup) ──────
135
- core.start().catch((err) => {
268
+ const agentStartTimeoutMs = 35000; // 35 seconds (slightly longer than internal timeouts)
269
+ Promise.race([
270
+ core.start(),
271
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Agent connection timeout`)), agentStartTimeoutMs)),
272
+ ]).catch((err) => {
136
273
  console.error(`Failed to connect to ${config.agentCommand}:`, err);
137
274
  });
138
275
  // ── Terminal lifecycle ────────────────────────────────────────
139
276
  process.on("SIGTERM", cleanup);
140
277
  process.on("SIGHUP", cleanup);
278
+ // Handle terminal stop/resume signals properly
279
+ process.on("SIGTSTP", () => {
280
+ // Handle Ctrl+Z - suspend the entire process group
281
+ // Restore terminal state before suspending
282
+ if (process.stdin.isTTY) {
283
+ try {
284
+ process.stdin.setRawMode(false);
285
+ }
286
+ catch {
287
+ // Ignore
288
+ }
289
+ }
290
+ // Re-send SIGSTOP to actually suspend
291
+ process.kill(process.pid, "SIGSTOP");
292
+ });
293
+ process.on("SIGCONT", () => {
294
+ // Re-acquire terminal when brought back to foreground
295
+ if (process.stdin.isTTY) {
296
+ try {
297
+ // Ensure we reacquire controlling terminal
298
+ process.stdin.setRawMode(true);
299
+ }
300
+ catch {
301
+ // May fail if stdin is not a TTY
302
+ }
303
+ }
304
+ });
141
305
  process.stdout.on("resize", () => {
142
306
  shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
143
307
  });
@@ -148,10 +312,35 @@ async function main() {
148
312
  }
149
313
  process.exit(e.exitCode);
150
314
  });
151
- if (process.stdin.isTTY) {
152
- process.stdin.setRawMode(true);
315
+ // Set up stdin - resume after all event listeners are in place
316
+ if (process.env.DEBUG) {
317
+ console.error('[agent-sh] Resuming stdin...');
153
318
  }
154
319
  process.stdin.resume();
320
+ // Set raw mode after resume to avoid SIGTTOU issues
321
+ if (process.stdin.isTTY) {
322
+ if (process.env.DEBUG) {
323
+ console.error('[agent-sh] Setting raw mode...');
324
+ }
325
+ // Use setImmediate to ensure we're in the next tick
326
+ setImmediate(() => {
327
+ try {
328
+ process.stdin.setRawMode(true);
329
+ if (process.env.DEBUG) {
330
+ console.error('[agent-sh] Raw mode enabled');
331
+ }
332
+ }
333
+ catch (err) {
334
+ if (process.env.DEBUG) {
335
+ console.error(`[agent-sh] Failed to set raw mode: ${err}`);
336
+ }
337
+ // May fail if process is in background; SIGTTOU handler prevents suspension
338
+ }
339
+ });
340
+ }
341
+ if (process.env.DEBUG) {
342
+ console.error('[agent-sh] Startup complete');
343
+ }
155
344
  }
156
345
  main().catch((err) => {
157
346
  console.error("Fatal:", err);
@@ -17,11 +17,16 @@ export declare class InputHandler {
17
17
  private ctx;
18
18
  private lineBuffer;
19
19
  private agentInputMode;
20
- private agentInputBuffer;
20
+ private editor;
21
21
  private autocompleteActive;
22
22
  private autocompleteIndex;
23
23
  private autocompleteItems;
24
24
  private autocompleteLines;
25
+ private history;
26
+ private historyIndex;
27
+ private savedBuffer;
28
+ private promptWrappedLines;
29
+ private escapeTimer;
25
30
  private bus;
26
31
  private onShowAgentInfo;
27
32
  constructor(opts: {
@@ -32,17 +37,22 @@ export declare class InputHandler {
32
37
  model?: string;
33
38
  };
34
39
  });
35
- /** Write the agent prompt line (clear + info prefix + ❯ + buffer text). */
40
+ private loadHistory;
41
+ private saveHistory;
42
+ /** Write the agent prompt line with cursor at the correct position. */
36
43
  private writeAgentPromptLine;
37
44
  handleInput(data: string): void;
38
45
  private enterAgentInputMode;
39
46
  private exitAgentInputMode;
47
+ /** Move to the start of the prompt area and clear everything below. */
48
+ private clearPromptArea;
40
49
  printPrompt(): void;
41
50
  private renderAgentInput;
42
51
  private updateAutocomplete;
43
52
  private renderAutocomplete;
44
- private clearAutocompleteLines;
45
53
  private applyAutocomplete;
46
54
  private dismissAutocomplete;
55
+ private clearAutocompleteLines;
47
56
  private handleAgentInput;
57
+ private processAgentActions;
48
58
  }