agent-sh 0.12.27 → 0.13.1

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.
Files changed (146) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +42 -98
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +16 -0
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +358 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +31 -2
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +1 -1
  89. package/dist/utils/floating-panel.js +9 -4
  90. package/dist/utils/llm-client.d.ts +16 -26
  91. package/dist/utils/llm-client.js +15 -26
  92. package/dist/utils/llm-facade.d.ts +7 -3
  93. package/dist/utils/stream-transform.d.ts +1 -1
  94. package/dist/utils/terminal-buffer.d.ts +1 -1
  95. package/dist/utils/tool-display.js +4 -0
  96. package/dist/utils/tool-interactive.d.ts +1 -1
  97. package/dist/utils/tty.d.ts +7 -0
  98. package/dist/utils/tty.js +15 -0
  99. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  100. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  101. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  102. package/examples/extensions/ashi/README.md +250 -0
  103. package/examples/extensions/ashi/package.json +60 -0
  104. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  105. package/examples/extensions/ashi/src/capture.ts +34 -0
  106. package/examples/extensions/ashi/src/cli.ts +176 -0
  107. package/examples/extensions/ashi/src/commands.ts +82 -0
  108. package/examples/extensions/ashi/src/compaction.ts +157 -0
  109. package/examples/extensions/ashi/src/components.ts +327 -0
  110. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  111. package/examples/extensions/ashi/src/display-config.ts +62 -0
  112. package/examples/extensions/ashi/src/frontend.ts +735 -0
  113. package/examples/extensions/ashi/src/hooks.ts +136 -0
  114. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  115. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  116. package/examples/extensions/ashi/src/session-store.ts +264 -0
  117. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  118. package/examples/extensions/ashi/src/theme.ts +151 -0
  119. package/examples/extensions/ashi/tsconfig.json +14 -0
  120. package/examples/extensions/emacs-buffer.ts +1 -1
  121. package/examples/extensions/interactive-prompts.ts +114 -69
  122. package/examples/extensions/latex-images.ts +3 -3
  123. package/examples/extensions/opencode-bridge/index.ts +1 -1
  124. package/examples/extensions/overlay-agent.ts +7 -5
  125. package/examples/extensions/peer-mesh.ts +1 -1
  126. package/examples/extensions/pi-bridge/index.ts +0 -1
  127. package/examples/extensions/questionnaire.ts +2 -1
  128. package/examples/extensions/rtk-proxy.ts +3 -3
  129. package/examples/extensions/solarized-theme.ts +3 -3
  130. package/examples/extensions/subagents.ts +6 -6
  131. package/examples/extensions/terminal-buffer.ts +1 -1
  132. package/examples/extensions/tmux-pane.ts +6 -4
  133. package/examples/extensions/tunnel-vision.ts +5 -5
  134. package/examples/extensions/user-shell.ts +1 -1
  135. package/examples/extensions/web-access.ts +5 -5
  136. package/package.json +38 -22
  137. package/dist/extensions/agent-backend.d.ts +0 -14
  138. package/dist/extensions/agent-backend.js +0 -307
  139. package/dist/types.d.ts +0 -227
  140. /package/dist/{types.js → agent/host-types.js} +0 -0
  141. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  142. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  143. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  144. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  145. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  146. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { LineEditor } from "../utils/line-editor.js";
4
- import { CONFIG_DIR, getSettings } from "../settings.js";
4
+ import { CONFIG_DIR, getSettings } from "../core/settings.js";
5
5
  import { TuiInputView } from "./tui-input-view.js";
6
6
  const HISTORY_FILE = path.join(CONFIG_DIR, "input-history");
7
7
  /** Line editor + shell-passthrough buffer. Delegates rendering to TuiInputView. */
@@ -21,11 +21,13 @@ export class InputHandler {
21
21
  savedBuffer = "";
22
22
  escapeTimer = null;
23
23
  bus;
24
+ handlers;
24
25
  onShowAgentInfo;
25
26
  view;
26
27
  constructor(opts) {
27
28
  this.ctx = opts.ctx;
28
29
  this.bus = opts.bus;
30
+ this.handlers = opts.handlers;
29
31
  this.onShowAgentInfo = opts.onShowAgentInfo;
30
32
  this.view = opts.view ?? new TuiInputView();
31
33
  this.loadHistory();
@@ -46,6 +48,7 @@ export class InputHandler {
46
48
  }
47
49
  this.modes.set(config.trigger, config);
48
50
  this.modesById.set(config.id, config);
51
+ this.handlers.define(`input-mode:${config.id}:submit`, config.onSubmit.bind(config));
49
52
  }
50
53
  loadHistory() {
51
54
  try {
@@ -380,7 +383,7 @@ export class InputHandler {
380
383
  }
381
384
  else if (query) {
382
385
  this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
383
- currentMode.onSubmit(query, this.bus);
386
+ this.handlers.call(`input-mode:${currentMode.id}:submit`, query, this.bus);
384
387
  }
385
388
  else {
386
389
  this.exitMode();
@@ -1,4 +1,4 @@
1
- import type { EventBus } from "../event-bus.js";
1
+ import type { EventBus } from "../core/event-bus.js";
2
2
  /**
3
3
  * Parses PTY output to detect command boundaries, track cwd,
4
4
  * and emit shell events. Owns the command lifecycle state.
@@ -4,5 +4,5 @@
4
4
  * user-shell exchanges) signals. Frontends without a PTY skip this
5
5
  * built-in and the agent runs cwd-aware via core's process.cwd() default.
6
6
  */
7
- import type { ExtensionContext } from "../types.js";
7
+ import type { ExtensionContext } from "./host-types.js";
8
8
  export default function activate(ctx: ExtensionContext): void;
@@ -1,4 +1,4 @@
1
- import { getSettings } from "../settings.js";
1
+ import { getSettings } from "../core/settings.js";
2
2
  import { spillOutput } from "../utils/shell-output-spill.js";
3
3
  export default function activate(ctx) {
4
4
  const { bus } = ctx;
@@ -43,17 +43,23 @@ export default function activate(ctx) {
43
43
  bus.on("shell:agent-exec-done", () => { agentShellActive = false; });
44
44
  // Override core's process.cwd() default with the PTY-tracked value.
45
45
  ctx.advise("cwd", () => currentCwd);
46
- ctx.registerContextProducer("shell-context", () => {
47
- const cwdTag = `<cwd>${currentCwd}</cwd>`;
48
- const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source !== "agent");
49
- if (fresh.length === 0)
50
- return cwdTag;
51
- lastSeq = exchanges[exchanges.length - 1].id;
52
- const text = fresh.map(formatExchangeTruncated).filter(Boolean).join("\n");
53
- if (!text)
54
- return cwdTag;
55
- return `${cwdTag}\n<shell_events>\n${text}\n</shell_events>`;
56
- }, { mode: "per-query" });
46
+ // Advises the core handler directly: shell-context loads before the
47
+ // agent host attaches `ctx.agent`, so the sugar isn't available yet.
48
+ ctx.advise("query-context:build", (next) => {
49
+ const base = next();
50
+ const part = (() => {
51
+ const cwdTag = `<cwd>${currentCwd}</cwd>`;
52
+ const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source !== "agent");
53
+ if (fresh.length === 0)
54
+ return cwdTag;
55
+ lastSeq = exchanges[exchanges.length - 1].id;
56
+ const text = fresh.map(formatExchangeTruncated).filter(Boolean).join("\n");
57
+ if (!text)
58
+ return cwdTag;
59
+ return `${cwdTag}\n<shell_events>\n${text}\n</shell_events>`;
60
+ })();
61
+ return base ? `${base}\n\n${part}` : part;
62
+ });
57
63
  ctx.define("shell:context-recent", (n = 25) => {
58
64
  const recent = exchanges.slice(-n);
59
65
  if (recent.length === 0)
@@ -1,4 +1,4 @@
1
- import type { EventBus } from "../event-bus.js";
1
+ import type { EventBus } from "../core/event-bus.js";
2
2
  import { type InputContext } from "./input-handler.js";
3
3
  export interface ShellHandlers {
4
4
  define: (name: string, fn: (...args: any[]) => any) => void;
@@ -24,7 +24,7 @@ export declare class Shell implements InputContext {
24
24
  private unmuteScopes;
25
25
  private pendingEchoSkips;
26
26
  private agentActive;
27
- private isZsh;
27
+ private strategy;
28
28
  private tmpDir?;
29
29
  constructor(opts: {
30
30
  bus: EventBus;
@@ -53,8 +53,10 @@ export declare class Shell implements InputContext {
53
53
  isAgentActive(): boolean;
54
54
  writeToPty(data: string): void;
55
55
  /**
56
- * Ask the shell to redraw its own prompt in place via \e[9999~, which both
57
- * zsh (ZLE widget) and bash (readline redraw-current-line) bind to repaint.
56
+ * Ask the shell to redraw its own prompt in place. The escape sequence is
57
+ * defined per-strategy and bound in the generated rc file (zsh: ZLE widget,
58
+ * bash: readline redraw-current-line). When the strategy returns null we
59
+ * skip the in-place redraw and let freshPrompt do a heavy redraw instead.
58
60
  */
59
61
  redrawPrompt(): void;
60
62
  /**
@@ -1,10 +1,11 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
- import * as path from "path";
4
3
  import * as pty from "node-pty";
5
4
  import { InputHandler } from "./input-handler.js";
6
5
  import { OutputParser } from "./output-parser.js";
7
- import { getSettings } from "../settings.js";
6
+ import { getSettings } from "../core/settings.js";
7
+ import { clearOpost } from "../utils/tty.js";
8
+ import { pickStrategy, FALLBACK_STRATEGY, SUPPORTED_SHELL_NAMES, } from "./strategies/index.js";
8
9
  export class Shell {
9
10
  ptyProcess;
10
11
  bus;
@@ -19,7 +20,7 @@ export class Shell {
19
20
  unmuteScopes = new Set();
20
21
  pendingEchoSkips = 0;
21
22
  agentActive = false;
22
- isZsh = false;
23
+ strategy;
23
24
  tmpDir;
24
25
  constructor(opts) {
25
26
  // Build environment — filter out undefined values (node-pty's native
@@ -35,98 +36,28 @@ export class Shell {
35
36
  // - OSC 7: cwd tracking (required by OutputParser)
36
37
  // - OSC 9999: prompt start marker (command boundary detection)
37
38
  // - OSC 9998: prompt end marker (bracketed prompt capture)
38
- // Prompt theming is left entirely to the user's shell config.
39
- const shellName = path.basename(opts.shell);
40
- const isZsh = shellName.includes("zsh");
41
- const isBash = shellName.includes("bash");
42
- if (!isZsh && !isBash) {
43
- console.warn(`Warning: agent-sh only supports zsh and bash. ` +
39
+ // Prompt theming is left entirely to the user's shell config. Per-shell
40
+ // rc-file generation lives in src/shell/strategies/.
41
+ const matched = pickStrategy(opts.shell);
42
+ if (!matched) {
43
+ console.warn(`Warning: agent-sh only supports ${SUPPORTED_SHELL_NAMES.join(", ")}. ` +
44
44
  `"${opts.shell}" may not work correctly — falling back to /bin/bash.`);
45
45
  }
46
- const shellBin = (isZsh || isBash) ? opts.shell : "/bin/bash";
47
- let shellArgs;
46
+ this.strategy = matched ?? FALLBACK_STRATEGY;
47
+ const shellBin = matched ? opts.shell : "/bin/bash";
48
48
  // Per-instance tag so nested agent-sh hooks don't cross-trigger.
49
49
  const instanceTag = `id=${opts.instanceId}`;
50
- const osc7Cmd = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
51
- const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
52
- const titleCmd = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
53
- this.isZsh = isZsh;
54
50
  const settings = getSettings();
55
- const showIndicator = settings.promptIndicator !== false;
56
- if (isZsh) {
57
- // For zsh: use ZDOTDIR to source user's real config, then append
58
- // our hooks via precmd_functions (additive — doesn't clobber p10k/omz).
59
- this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-sh-"));
60
- const userZdotdir = env.ZDOTDIR || env.HOME || os.homedir();
61
- const zshrcLines = [
62
- `ZDOTDIR="${userZdotdir}"`,
63
- `[ -f "${userZdotdir}/.zshrc" ] && source "${userZdotdir}/.zshrc"`,
64
- "",
65
- "# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
66
- "__agent_sh_precmd() {",
67
- ` ${osc7Cmd}`,
68
- ` ${promptMarker}`,
69
- ...(showIndicator ? [` ${titleCmd}`] : []),
70
- "}",
71
- "precmd_functions+=(__agent_sh_precmd)",
72
- "",
73
- "# Preexec hook: emit actual command text so agent-sh can track",
74
- "# history-recalled and tab-completed commands accurately",
75
- "__agent_sh_preexec() {",
76
- ` printf "\\e]9997;${instanceTag};%s\\a" "$1"`,
77
- "}",
78
- "preexec_functions+=(__agent_sh_preexec)",
79
- ];
80
- zshrcLines.push("", "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)", "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering", 'if (( ${+widgets[zle-line-init]} )); then', " zle -A zle-line-init __agent_sh_orig_line_init", " __agent_sh_line_init() {", " zle __agent_sh_orig_line_init", ` printf "\\e]9998;${instanceTag};READY\\a"`, " }", "else", " __agent_sh_line_init() {", ` printf "\\e]9998;${instanceTag};READY\\a"`, " }", "fi", "zle -N zle-line-init __agent_sh_line_init", "", "# Hidden widget to trigger prompt redraw from Node.js side", "# Bound to an unused escape sequence that no real key produces", "__agent_sh_redraw() {", " zle reset-prompt", "}", "zle -N __agent_sh_redraw", "bindkey '\\e[9999~' __agent_sh_redraw");
81
- fs.writeFileSync(path.join(this.tmpDir, ".zshrc"), zshrcLines.join("\n") + "\n");
82
- env.ZDOTDIR = this.tmpDir;
83
- shellArgs = ["--no-globalrcs"];
84
- }
85
- else {
86
- // For bash: use --rcfile to source our wrapper, which sources the user's
87
- // real bashrc then appends our hooks. No HOME override needed.
88
- this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-sh-"));
89
- const userHome = env.HOME || os.homedir();
90
- const bashrcLines = [
91
- `[ -f "${userHome}/.bashrc" ] && source "${userHome}/.bashrc"`,
92
- "",
93
- "# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
94
- "# Wrapped in a function because inlining printf \"...\" into",
95
- "# PROMPT_COMMAND=\"...\" breaks the outer quoting.",
96
- "__agent_sh_precmd() {",
97
- ` ${osc7Cmd}`,
98
- ` ${promptMarker}`,
99
- ...(showIndicator ? [` ${titleCmd}`] : []),
100
- " __agent_sh_preexec_ran=0",
101
- "}",
102
- `PROMPT_COMMAND="\${PROMPT_COMMAND%;}"`,
103
- `PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}__agent_sh_precmd"`,
104
- "",
105
- "# Preexec hook via DEBUG trap: emit actual command text so agent-sh",
106
- "# can track history-recalled and tab-completed commands accurately",
107
- "__agent_sh_preexec_ran=0",
108
- "__agent_sh_emit_preexec() {",
109
- ' [[ $__agent_sh_preexec_ran == 1 ]] && return',
110
- ' [[ -n $COMP_LINE ]] && return',
111
- " __agent_sh_preexec_ran=1",
112
- " local this_cmd",
113
- ` this_cmd=$(HISTTIMEFORMAT='' builtin history 1 | command sed 's/^ *[0-9]* *//')`,
114
- ` printf '\\e]9997;${instanceTag};%s\\a' "$this_cmd"`,
115
- "}",
116
- "trap '__agent_sh_emit_preexec' DEBUG",
117
- "",
118
- "# End-of-prompt marker: append to PS1 (\\[...\\] marks it zero-width)",
119
- `case "$PS1" in *9998*) ;; *) PS1="\${PS1}\\[\\e]9998;${instanceTag};READY\\a\\]";; esac`,
120
- "",
121
- "# Mirrors the zsh \\e[9999~ reset-prompt widget — used by agent-sh",
122
- "# to repaint the prompt in place. All keymaps so `set -o vi` works.",
123
- `bind -m emacs '"\\e[9999~":redraw-current-line' 2>/dev/null`,
124
- `bind -m vi-insert '"\\e[9999~":redraw-current-line' 2>/dev/null`,
125
- `bind -m vi-command '"\\e[9999~":redraw-current-line' 2>/dev/null`,
126
- ];
127
- fs.writeFileSync(path.join(this.tmpDir, ".bashrc"), bashrcLines.join("\n") + "\n");
128
- shellArgs = ["--rcfile", path.join(this.tmpDir, ".bashrc")];
129
- }
51
+ const spawnConfig = this.strategy.prepareSpawn({
52
+ tmpDirRoot: os.tmpdir(),
53
+ instanceTag,
54
+ showIndicator: settings.promptIndicator !== false,
55
+ userHome: env.HOME || os.homedir(),
56
+ env,
57
+ });
58
+ this.tmpDir = spawnConfig.tmpDir;
59
+ Object.assign(env, spawnConfig.envOverrides);
60
+ const shellArgs = spawnConfig.args;
130
61
  // Pause stdin before spawning PTY to avoid TTY contention on macOS.
131
62
  // The PTY will become the controlling terminal for the child shell.
132
63
  const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
@@ -158,6 +89,7 @@ export class Shell {
158
89
  // Ignore - will be set up later in index.ts
159
90
  }
160
91
  }
92
+ clearOpost();
161
93
  this.bus = opts.bus;
162
94
  this.handlers = opts.handlers;
163
95
  this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag);
@@ -175,6 +107,7 @@ export class Shell {
175
107
  this.inputHandler = new InputHandler({
176
108
  ctx: this,
177
109
  bus: opts.bus,
110
+ handlers: opts.handlers,
178
111
  onShowAgentInfo: opts.onShowAgentInfo ?? (() => ({ info: "" })),
179
112
  });
180
113
  this.setupOutput();
@@ -272,8 +205,10 @@ export class Shell {
272
205
  this.ptyProcess.write(data);
273
206
  }
274
207
  /**
275
- * Ask the shell to redraw its own prompt in place via \e[9999~, which both
276
- * zsh (ZLE widget) and bash (readline redraw-current-line) bind to repaint.
208
+ * Ask the shell to redraw its own prompt in place. The escape sequence is
209
+ * defined per-strategy and bound in the generated rc file (zsh: ZLE widget,
210
+ * bash: readline redraw-current-line). When the strategy returns null we
211
+ * skip the in-place redraw and let freshPrompt do a heavy redraw instead.
277
212
  */
278
213
  redrawPrompt() {
279
214
  const result = this.bus.emitPipe("shell:redraw-prompt", {
@@ -281,9 +216,11 @@ export class Shell {
281
216
  kind: "redraw",
282
217
  handled: false,
283
218
  });
284
- if (!result.handled) {
285
- this.ptyProcess.write("\x1b[9999~");
286
- }
219
+ if (result.handled)
220
+ return;
221
+ const escape = this.strategy.redrawEscape();
222
+ if (escape)
223
+ this.ptyProcess.write(escape);
287
224
  }
288
225
  /**
289
226
  * Heavy redraw: send \n to PTY to trigger a full precmd → prompt cycle.
@@ -364,23 +301,10 @@ export class Shell {
364
301
  this.bus.on("agent:processing-done", () => {
365
302
  this.handlers.call("shell:on-processing-done");
366
303
  });
367
- // Permission UI is briefly visible during the prompt; an unmute scope
368
- // overrides whatever mute is currently held, then releases cleanly.
369
- // Doesn't touch agent-turn state, so suppressed handlers can't leak.
370
- let permissionVisible = null;
371
- this.bus.on("permission:request", () => {
372
- permissionVisible?.release();
373
- permissionVisible = this.acquireUnmute("permission-ui");
374
- });
375
- this.bus.onPipeAsync("permission:request", async (payload) => {
376
- permissionVisible?.release();
377
- permissionVisible = null;
378
- return payload;
379
- });
380
304
  this.bus.onPipeAsync("shell:exec-request", async (payload) => {
381
305
  const visible = this.acquireUnmute("exec-request");
382
306
  this.skipNextLine();
383
- process.stdout.write("\n");
307
+ process.stdout.write("\r\n");
384
308
  this.bus.emit("shell:agent-exec-start", {});
385
309
  try {
386
310
  const output = await new Promise((resolve, reject) => {
@@ -0,0 +1,2 @@
1
+ import type { ShellStrategy } from "./types.js";
2
+ export declare const bashStrategy: ShellStrategy;
@@ -0,0 +1,68 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const OSC7_CMD = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
4
+ const TITLE_CMD = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
5
+ export const bashStrategy = {
6
+ name: "bash",
7
+ matches(shellPath) {
8
+ return path.basename(shellPath).includes("bash");
9
+ },
10
+ prepareSpawn(opts) {
11
+ const { tmpDirRoot, instanceTag, showIndicator, env, userHome } = opts;
12
+ // Use --rcfile to source our wrapper, which sources the user's real
13
+ // bashrc then appends our hooks. No HOME override needed.
14
+ const tmpDir = fs.mkdtempSync(path.join(tmpDirRoot, "agent-sh-"));
15
+ const home = env.HOME || userHome;
16
+ const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
17
+ const lines = [
18
+ `[ -f "${home}/.bashrc" ] && source "${home}/.bashrc"`,
19
+ "",
20
+ "# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
21
+ "# Wrapped in a function because inlining printf \"...\" into",
22
+ "# PROMPT_COMMAND=\"...\" breaks the outer quoting.",
23
+ "__agent_sh_precmd() {",
24
+ ` ${OSC7_CMD}`,
25
+ ` ${promptMarker}`,
26
+ ...(showIndicator ? [` ${TITLE_CMD}`] : []),
27
+ " __agent_sh_preexec_ran=0",
28
+ "}",
29
+ `PROMPT_COMMAND="\${PROMPT_COMMAND%;}"`,
30
+ `PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}__agent_sh_precmd"`,
31
+ "",
32
+ "# Preexec hook via DEBUG trap: emit actual command text so agent-sh",
33
+ "# can track history-recalled and tab-completed commands accurately",
34
+ "__agent_sh_preexec_ran=0",
35
+ "__agent_sh_emit_preexec() {",
36
+ ' [[ $__agent_sh_preexec_ran == 1 ]] && return',
37
+ ' [[ -n $COMP_LINE ]] && return',
38
+ " __agent_sh_preexec_ran=1",
39
+ " local this_cmd",
40
+ ` this_cmd=$(HISTTIMEFORMAT='' builtin history 1 | command sed 's/^ *[0-9]* *//')`,
41
+ ` printf '\\e]9997;${instanceTag};%s\\a' "$this_cmd"`,
42
+ "}",
43
+ "trap '__agent_sh_emit_preexec' DEBUG",
44
+ "",
45
+ "# End-of-prompt marker: append to PS1 (\\[...\\] marks it zero-width)",
46
+ `case "$PS1" in *9998*) ;; *) PS1="\${PS1}\\[\\e]9998;${instanceTag};READY\\a\\]";; esac`,
47
+ "",
48
+ "# Mirrors the zsh \\e[9999~ reset-prompt widget — used by agent-sh",
49
+ "# to repaint the prompt in place. All keymaps so `set -o vi` works.",
50
+ `bind -m emacs '"\\e[9999~":redraw-current-line' 2>/dev/null`,
51
+ `bind -m vi-insert '"\\e[9999~":redraw-current-line' 2>/dev/null`,
52
+ `bind -m vi-command '"\\e[9999~":redraw-current-line' 2>/dev/null`,
53
+ ];
54
+ const rcPath = path.join(tmpDir, ".bashrc");
55
+ fs.writeFileSync(rcPath, lines.join("\n") + "\n");
56
+ return {
57
+ args: ["--rcfile", rcPath],
58
+ envOverrides: {},
59
+ tmpDir,
60
+ };
61
+ },
62
+ envCaptureCommand() {
63
+ return "[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null; env -0";
64
+ },
65
+ redrawEscape() {
66
+ return "\x1b[9999~";
67
+ },
68
+ };
@@ -0,0 +1,2 @@
1
+ import type { ShellStrategy } from "./types.js";
2
+ export declare const fishStrategy: ShellStrategy;
@@ -0,0 +1,65 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const OSC7_CMD = 'printf "\\e]7;file://%s%s\\a" (hostname) "$PWD"';
4
+ const TITLE_CMD = 'printf "\\e]0;⚡ agent-sh: %s\\a" (string replace -- "$HOME" "~" "$PWD")';
5
+ export const fishStrategy = {
6
+ name: "fish",
7
+ matches(shellPath) {
8
+ return path.basename(shellPath).includes("fish");
9
+ },
10
+ prepareSpawn(opts) {
11
+ const { tmpDirRoot, instanceTag, showIndicator } = opts;
12
+ // Layer hooks via `-C` so they run after the user's config — our wrapper
13
+ // around fish_prompt needs to see the user's final definition.
14
+ const tmpDir = fs.mkdtempSync(path.join(tmpDirRoot, "agent-sh-"));
15
+ const initPath = path.join(tmpDir, "init.fish");
16
+ const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
17
+ const lines = [
18
+ "# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
19
+ "function __agent_sh_precmd --on-event fish_prompt",
20
+ ` ${OSC7_CMD}`,
21
+ ` ${promptMarker}`,
22
+ ...(showIndicator ? [` ${TITLE_CMD}`] : []),
23
+ "end",
24
+ "",
25
+ "# Preexec hook: emit actual command text so agent-sh can track",
26
+ "# history-recalled and tab-completed commands accurately",
27
+ "function __agent_sh_preexec --on-event fish_preexec",
28
+ ` printf "\\e]9997;${instanceTag};%s\\a" "$argv"`,
29
+ "end",
30
+ "",
31
+ "# End-of-prompt marker: wrap fish_prompt so READY fires after render",
32
+ "if functions -q fish_prompt",
33
+ " functions --copy fish_prompt __agent_sh_orig_fish_prompt",
34
+ " function fish_prompt",
35
+ " __agent_sh_orig_fish_prompt",
36
+ ` printf "\\e]9998;${instanceTag};READY\\a"`,
37
+ " end",
38
+ "else",
39
+ " function fish_prompt",
40
+ " printf '%s> ' (prompt_pwd)",
41
+ ` printf "\\e]9998;${instanceTag};READY\\a"`,
42
+ " end",
43
+ "end",
44
+ "",
45
+ "# Redraw binding. fish 4 silently drops \\e[N~ outside the F-key table,",
46
+ "# so we use CSI-u with a private-use codepoint (U+E028) instead.",
47
+ "bind \\e\\[57400u 'commandline -f repaint' 2>/dev/null",
48
+ "bind -M insert \\e\\[57400u 'commandline -f repaint' 2>/dev/null",
49
+ "bind -M default \\e\\[57400u 'commandline -f repaint' 2>/dev/null",
50
+ ];
51
+ fs.writeFileSync(initPath, lines.join("\n") + "\n");
52
+ return {
53
+ args: ["-l", "-i", "-C", `source ${initPath}`],
54
+ envOverrides: {},
55
+ tmpDir,
56
+ };
57
+ },
58
+ envCaptureCommand() {
59
+ // `fish -l` already sources config.fish + conf.d, so no explicit source.
60
+ return "env -0";
61
+ },
62
+ redrawEscape() {
63
+ return "\x1b[57400u";
64
+ },
65
+ };
@@ -0,0 +1,13 @@
1
+ import type { ShellStrategy } from "./types.js";
2
+ export type { ShellStrategy, PrepareSpawnOpts, ShellSpawnConfig } from "./types.js";
3
+ /** Strategy used when the requested shell isn't recognized. */
4
+ export declare const FALLBACK_STRATEGY: ShellStrategy;
5
+ /** Names of supported shells, used for warning messages. */
6
+ export declare const SUPPORTED_SHELL_NAMES: readonly string[];
7
+ /**
8
+ * Pick the strategy that matches the given shell binary path. Returns null
9
+ * when no strategy claims the path — caller decides whether to warn and how
10
+ * to fall back (e.g. shell.ts swaps the binary to /bin/bash; env capture
11
+ * just runs the fallback strategy's syntax against the original binary).
12
+ */
13
+ export declare function pickStrategy(shellPath: string): ShellStrategy | null;
@@ -0,0 +1,17 @@
1
+ import { zshStrategy } from "./zsh.js";
2
+ import { bashStrategy } from "./bash.js";
3
+ import { fishStrategy } from "./fish.js";
4
+ const STRATEGIES = [zshStrategy, bashStrategy, fishStrategy];
5
+ /** Strategy used when the requested shell isn't recognized. */
6
+ export const FALLBACK_STRATEGY = bashStrategy;
7
+ /** Names of supported shells, used for warning messages. */
8
+ export const SUPPORTED_SHELL_NAMES = STRATEGIES.map((s) => s.name);
9
+ /**
10
+ * Pick the strategy that matches the given shell binary path. Returns null
11
+ * when no strategy claims the path — caller decides whether to warn and how
12
+ * to fall back (e.g. shell.ts swaps the binary to /bin/bash; env capture
13
+ * just runs the fallback strategy's syntax against the original binary).
14
+ */
15
+ export function pickStrategy(shellPath) {
16
+ return STRATEGIES.find((s) => s.matches(shellPath)) ?? null;
17
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Per-shell adapter for the bits of agent-sh that are inherently shell-syntax
3
+ * specific: rc-file generation, spawn args/env, env-capture command, and the
4
+ * escape sequence used to repaint the prompt in place.
5
+ *
6
+ * Everything else (PTY I/O, OSC parsing, mute scopes, prompt boundary
7
+ * detection) is shell-agnostic and lives in shell.ts / output-parser.ts.
8
+ */
9
+ export interface PrepareSpawnOpts {
10
+ /** Root for mkdtemp — typically os.tmpdir(). */
11
+ tmpDirRoot: string;
12
+ /** Per-instance tag (e.g. "id=abc123") so nested agent-sh hooks don't cross-trigger. */
13
+ instanceTag: string;
14
+ /** Whether to emit the terminal title indicator from the prompt hook. */
15
+ showIndicator: boolean;
16
+ /** Resolved user home (env.HOME ?? os.homedir()). */
17
+ userHome: string;
18
+ /** Inherited env at spawn time — strategies may read ZDOTDIR etc. */
19
+ env: Record<string, string>;
20
+ }
21
+ export interface ShellSpawnConfig {
22
+ /** Args to pass to pty.spawn after the shell binary. */
23
+ args: string[];
24
+ /** Env vars the strategy needs to set on the child (e.g. ZDOTDIR, XDG_CONFIG_HOME). */
25
+ envOverrides: Record<string, string>;
26
+ /** Temp directory the strategy created, if any — caller cleans up on exit. */
27
+ tmpDir?: string;
28
+ }
29
+ export interface ShellStrategy {
30
+ /** Short name used for fallback warnings ("zsh", "bash", "fish"). */
31
+ readonly name: string;
32
+ /** Does this strategy claim the binary at `shellPath`? */
33
+ matches(shellPath: string): boolean;
34
+ /**
35
+ * Generate any rc files and return spawn args + env overrides. May create
36
+ * a tmp directory; caller is responsible for cleanup via the returned path.
37
+ */
38
+ prepareSpawn(opts: PrepareSpawnOpts): ShellSpawnConfig;
39
+ /**
40
+ * Shell-syntax command run via `<shell> -l -c "<cmd>"` to source the user's
41
+ * config and dump env. Used at startup to inherit shell-only env vars.
42
+ */
43
+ envCaptureCommand(): string;
44
+ /**
45
+ * Escape sequence to write to the PTY to ask the shell to repaint its
46
+ * prompt in place. The corresponding binding is set up in prepareSpawn.
47
+ * Returns null if the shell can't redraw — caller falls back to freshPrompt.
48
+ */
49
+ redrawEscape(): string | null;
50
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Per-shell adapter for the bits of agent-sh that are inherently shell-syntax
3
+ * specific: rc-file generation, spawn args/env, env-capture command, and the
4
+ * escape sequence used to repaint the prompt in place.
5
+ *
6
+ * Everything else (PTY I/O, OSC parsing, mute scopes, prompt boundary
7
+ * detection) is shell-agnostic and lives in shell.ts / output-parser.ts.
8
+ */
9
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ShellStrategy } from "./types.js";
2
+ export declare const zshStrategy: ShellStrategy;
@@ -0,0 +1,72 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const OSC7_CMD = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
4
+ const TITLE_CMD = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
5
+ export const zshStrategy = {
6
+ name: "zsh",
7
+ matches(shellPath) {
8
+ return path.basename(shellPath).includes("zsh");
9
+ },
10
+ prepareSpawn(opts) {
11
+ const { tmpDirRoot, instanceTag, showIndicator, env, userHome } = opts;
12
+ // Use ZDOTDIR to source user's real config, then append our hooks via
13
+ // precmd_functions (additive — doesn't clobber p10k/omz).
14
+ const tmpDir = fs.mkdtempSync(path.join(tmpDirRoot, "agent-sh-"));
15
+ const userZdotdir = env.ZDOTDIR || env.HOME || userHome;
16
+ const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
17
+ const lines = [
18
+ `ZDOTDIR="${userZdotdir}"`,
19
+ `[ -f "${userZdotdir}/.zshrc" ] && source "${userZdotdir}/.zshrc"`,
20
+ "",
21
+ "# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
22
+ "__agent_sh_precmd() {",
23
+ ` ${OSC7_CMD}`,
24
+ ` ${promptMarker}`,
25
+ ...(showIndicator ? [` ${TITLE_CMD}`] : []),
26
+ "}",
27
+ "precmd_functions+=(__agent_sh_precmd)",
28
+ "",
29
+ "# Preexec hook: emit actual command text so agent-sh can track",
30
+ "# history-recalled and tab-completed commands accurately",
31
+ "__agent_sh_preexec() {",
32
+ ` printf "\\e]9997;${instanceTag};%s\\a" "$1"`,
33
+ "}",
34
+ "preexec_functions+=(__agent_sh_preexec)",
35
+ "",
36
+ "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)",
37
+ "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering",
38
+ 'if (( ${+widgets[zle-line-init]} )); then',
39
+ " zle -A zle-line-init __agent_sh_orig_line_init",
40
+ " __agent_sh_line_init() {",
41
+ " zle __agent_sh_orig_line_init",
42
+ ` printf "\\e]9998;${instanceTag};READY\\a"`,
43
+ " }",
44
+ "else",
45
+ " __agent_sh_line_init() {",
46
+ ` printf "\\e]9998;${instanceTag};READY\\a"`,
47
+ " }",
48
+ "fi",
49
+ "zle -N zle-line-init __agent_sh_line_init",
50
+ "",
51
+ "# Hidden widget to trigger prompt redraw from Node.js side",
52
+ "# Bound to an unused escape sequence that no real key produces",
53
+ "__agent_sh_redraw() {",
54
+ " zle reset-prompt",
55
+ "}",
56
+ "zle -N __agent_sh_redraw",
57
+ "bindkey '\\e[9999~' __agent_sh_redraw",
58
+ ];
59
+ fs.writeFileSync(path.join(tmpDir, ".zshrc"), lines.join("\n") + "\n");
60
+ return {
61
+ args: ["--no-globalrcs"],
62
+ envOverrides: { ZDOTDIR: tmpDir },
63
+ tmpDir,
64
+ };
65
+ },
66
+ envCaptureCommand() {
67
+ return "source ~/.zshrc 2>/dev/null; env -0";
68
+ },
69
+ redrawEscape() {
70
+ return "\x1b[9999~";
71
+ },
72
+ };