agent-sh 0.15.0 → 0.15.2

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 (124) hide show
  1. package/dist/agent/agent-loop.js +11 -8
  2. package/dist/agent/events.d.ts +4 -0
  3. package/docs/README.md +14 -0
  4. package/docs/agent.md +398 -0
  5. package/docs/architecture.md +196 -0
  6. package/docs/context-management.md +200 -0
  7. package/docs/extensions.md +951 -0
  8. package/docs/library.md +84 -0
  9. package/docs/troubleshooting.md +65 -0
  10. package/docs/tui-composition.md +294 -0
  11. package/docs/usage.md +306 -0
  12. package/examples/extensions/ash-scheme/package.json +1 -1
  13. package/examples/extensions/ashi/EXTENDING.md +2 -2
  14. package/examples/extensions/ashi/README.md +2 -2
  15. package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
  16. package/examples/extensions/ashi/package.json +5 -3
  17. package/examples/extensions/ashi/src/chat/tool-group.ts +3 -2
  18. package/examples/extensions/ashi/src/cli.ts +9 -8
  19. package/examples/extensions/ashi/src/dialogs.ts +16 -1
  20. package/examples/extensions/ashi/src/events.ts +1 -0
  21. package/examples/extensions/ashi/src/frontend.ts +26 -6
  22. package/examples/extensions/ashi/src/renderer.ts +24 -4
  23. package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +4 -3
  24. package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
  25. package/examples/extensions/ashi/src/ui.ts +11 -0
  26. package/examples/extensions/ashi-ink/package.json +2 -2
  27. package/examples/extensions/claude-code-bridge/package.json +1 -1
  28. package/examples/extensions/opencode-bridge/package.json +1 -1
  29. package/package.json +3 -1
  30. package/src/agent/agent-loop.ts +1566 -0
  31. package/src/agent/entry-format.ts +19 -0
  32. package/src/agent/events.ts +153 -0
  33. package/src/agent/extensions/rolling-history/constants.ts +1 -0
  34. package/src/agent/extensions/rolling-history/index.ts +202 -0
  35. package/src/agent/extensions/rolling-history/recall.ts +131 -0
  36. package/src/agent/extensions/rolling-history/strategy.ts +404 -0
  37. package/src/agent/host-types.ts +192 -0
  38. package/src/agent/index.ts +591 -0
  39. package/src/agent/live-view.ts +279 -0
  40. package/src/agent/llm-client.ts +111 -0
  41. package/src/agent/llm-facade.ts +43 -0
  42. package/src/agent/normalize-args.ts +61 -0
  43. package/src/agent/nuclear-form.ts +382 -0
  44. package/src/agent/providers/deepseek.ts +39 -0
  45. package/src/agent/providers/ollama.ts +92 -0
  46. package/src/agent/providers/openai-compatible.ts +36 -0
  47. package/src/agent/providers/openai.ts +52 -0
  48. package/src/agent/providers/opencode.ts +142 -0
  49. package/src/agent/providers/openrouter.ts +105 -0
  50. package/src/agent/providers/zai-coding-plan.ts +33 -0
  51. package/src/agent/session-store.ts +336 -0
  52. package/src/agent/skills.ts +228 -0
  53. package/src/agent/store.ts +310 -0
  54. package/src/agent/subagent.ts +305 -0
  55. package/src/agent/system-prompt.ts +151 -0
  56. package/src/agent/token-budget.ts +12 -0
  57. package/src/agent/tool-protocol.ts +722 -0
  58. package/src/agent/tool-registry.ts +66 -0
  59. package/src/agent/tools/bash.ts +95 -0
  60. package/src/agent/tools/edit-file.ts +154 -0
  61. package/src/agent/tools/expand-home.ts +7 -0
  62. package/src/agent/tools/glob.ts +108 -0
  63. package/src/agent/tools/grep.ts +228 -0
  64. package/src/agent/tools/list-skills.ts +37 -0
  65. package/src/agent/tools/ls.ts +81 -0
  66. package/src/agent/tools/pwsh.ts +140 -0
  67. package/src/agent/tools/read-file.ts +164 -0
  68. package/src/agent/tools/write-file.ts +72 -0
  69. package/src/agent/types.ts +149 -0
  70. package/src/cli/args.ts +91 -0
  71. package/src/cli/auth/cli.ts +244 -0
  72. package/src/cli/auth/discover.ts +52 -0
  73. package/src/cli/auth/keys.ts +143 -0
  74. package/src/cli/index.ts +295 -0
  75. package/src/cli/init.ts +74 -0
  76. package/src/cli/install.ts +439 -0
  77. package/src/cli/shell-env.ts +68 -0
  78. package/src/cli/subcommands.ts +24 -0
  79. package/src/core/event-bus.ts +252 -0
  80. package/src/core/extension-loader.ts +347 -0
  81. package/src/core/index.ts +152 -0
  82. package/src/core/settings.ts +398 -0
  83. package/src/core/types.ts +61 -0
  84. package/src/extensions/file-autocomplete.ts +71 -0
  85. package/src/extensions/index.ts +38 -0
  86. package/src/extensions/slash-commands/events.ts +14 -0
  87. package/src/extensions/slash-commands/index.ts +269 -0
  88. package/src/shell/events.ts +73 -0
  89. package/src/shell/host-types.ts +150 -0
  90. package/src/shell/index.ts +159 -0
  91. package/src/shell/input-handler.ts +505 -0
  92. package/src/shell/output-parser.ts +156 -0
  93. package/src/shell/shell-context.ts +193 -0
  94. package/src/shell/shell.ts +414 -0
  95. package/src/shell/strategies/bash.ts +83 -0
  96. package/src/shell/strategies/fish.ts +77 -0
  97. package/src/shell/strategies/index.ts +24 -0
  98. package/src/shell/strategies/types.ts +64 -0
  99. package/src/shell/strategies/zsh.ts +92 -0
  100. package/src/shell/terminal.ts +124 -0
  101. package/src/shell/tui-input-view.ts +222 -0
  102. package/src/shell/tui-renderer.ts +1126 -0
  103. package/src/utils/ansi.ts +140 -0
  104. package/src/utils/box-frame.ts +138 -0
  105. package/src/utils/compositor.ts +157 -0
  106. package/src/utils/diff-renderer.ts +829 -0
  107. package/src/utils/diff.ts +244 -0
  108. package/src/utils/executor.ts +305 -0
  109. package/src/utils/file-watcher.ts +110 -0
  110. package/src/utils/floating-panel.ts +1160 -0
  111. package/src/utils/handler-registry.ts +110 -0
  112. package/src/utils/line-editor.ts +636 -0
  113. package/src/utils/markdown.ts +437 -0
  114. package/src/utils/message-utils.ts +113 -0
  115. package/src/utils/package-version.ts +12 -0
  116. package/src/utils/palette.ts +64 -0
  117. package/src/utils/ref-counter.ts +9 -0
  118. package/src/utils/ripgrep-path.ts +17 -0
  119. package/src/utils/shell-output-spill.ts +76 -0
  120. package/src/utils/stream-transform.ts +292 -0
  121. package/src/utils/terminal-buffer.ts +213 -0
  122. package/src/utils/tool-display.ts +315 -0
  123. package/src/utils/tool-interactive.ts +71 -0
  124. package/src/utils/tty.ts +14 -0
@@ -0,0 +1,156 @@
1
+ import type { EventBus } from "../core/event-bus.js";
2
+ import { stripAnsi } from "../utils/ansi.js";
3
+
4
+ // Self-emitted form: \e]<num>;id=<own>;<body>\a — only this is honored.
5
+ // Anything else (mismatched tag, untagged) is ignored as opaque foreground output.
6
+ const PROMPT_RE = /\x1b\]9999;(?:id=([a-f0-9]+);)?PROMPT\x07/;
7
+ const PREEXEC_RE = /\x1b\]9997;(?:id=([a-f0-9]+);)?([^\x07]*)\x07/;
8
+ const READY_RE = /\x1b\]9998;(?:id=([a-f0-9]+);)?READY\x07/;
9
+
10
+ export interface OutputParserOpts {
11
+ /** Optional shell-specific cleanup applied to raw output before stripAnsi. */
12
+ cleanOutput?(raw: string): string;
13
+ }
14
+
15
+ export class OutputParser {
16
+ private bus: EventBus;
17
+ private cwd: string;
18
+ private ownTag: string;
19
+ private cleanOutput: (raw: string) => string;
20
+ private currentOutputCapture = "";
21
+ private lastCommand = "";
22
+ private foregroundBusy = false;
23
+ private promptReady = false;
24
+
25
+ constructor(bus: EventBus, initialCwd: string, ownTag: string, opts: OutputParserOpts = {}) {
26
+ this.bus = bus;
27
+ this.cwd = initialCwd;
28
+ this.ownTag = ownTag.startsWith("id=") ? ownTag.slice(3) : ownTag;
29
+ this.cleanOutput = opts.cleanOutput ?? ((raw) => raw);
30
+ }
31
+
32
+ processData(data: string): void {
33
+ this.parseOSC7(data);
34
+ data = this.handlePreexec(data);
35
+ this.parsePromptMarker(data);
36
+ this.parsePromptEnd(data);
37
+ }
38
+
39
+ onCommandEntered(command: string, cwd: string): void {
40
+ this.lastCommand = command;
41
+ this.currentOutputCapture = "";
42
+ this.bus.emit("shell:command-start", { command, cwd });
43
+ if (!this.foregroundBusy) {
44
+ this.foregroundBusy = true;
45
+ this.bus.emit("shell:foreground-busy", { busy: true });
46
+ }
47
+ }
48
+
49
+ isPromptReady(): boolean {
50
+ return this.promptReady;
51
+ }
52
+
53
+ isForegroundBusy(): boolean {
54
+ return this.foregroundBusy;
55
+ }
56
+
57
+ getCwd(): string {
58
+ return this.cwd;
59
+ }
60
+
61
+ /** Pulls the actual command from the shell's OSC 9997 preexec marker —
62
+ * more reliable than the InputHandler's lineBuffer, which can't track
63
+ * history recall or tab completion. Returns data with the OSC stripped. */
64
+ private handlePreexec(data: string): string {
65
+ const match = PREEXEC_RE.exec(data);
66
+ if (!match) return data;
67
+
68
+ if (match[1] !== this.ownTag) {
69
+ // Nested instance or untagged foreign emission — strip and ignore.
70
+ return data.slice(0, match.index) + data.slice(match.index + match[0].length);
71
+ }
72
+
73
+ const command = match[2]!;
74
+ this.lastCommand = command;
75
+ // Discard echo accumulated before preexec.
76
+ this.currentOutputCapture = "";
77
+
78
+ if (!this.foregroundBusy) {
79
+ this.foregroundBusy = true;
80
+ this.bus.emit("shell:foreground-busy", { busy: true });
81
+ }
82
+ this.bus.emit("shell:command-start", { command, cwd: this.cwd });
83
+
84
+ return data.slice(match.index + match[0].length);
85
+ }
86
+
87
+ private parseOSC7(data: string): void {
88
+ const match = data.match(/\x1b\]7;file:\/\/[^/]*(\/[^\x07\x1b]*)/);
89
+ if (match?.[1]) {
90
+ const newCwd = decodeURIComponent(match[1]);
91
+ if (newCwd !== this.cwd) {
92
+ this.cwd = newCwd;
93
+ this.bus.emit("shell:cwd-change", { cwd: this.cwd });
94
+ }
95
+ }
96
+ }
97
+
98
+ /** OSC 9999 marker — each occurrence finalizes the previous command's output. */
99
+ private parsePromptMarker(data: string): void {
100
+ const match = PROMPT_RE.exec(data);
101
+ if (match) {
102
+ if (match[1] !== this.ownTag) {
103
+ // Nested or untagged emission: keep as opaque foreground output.
104
+ this.currentOutputCapture += data;
105
+ return;
106
+ }
107
+ const markerIdx = match.index;
108
+ if (markerIdx > 0) {
109
+ this.currentOutputCapture += data.slice(0, markerIdx);
110
+ }
111
+ this.promptReady = false;
112
+ if (this.foregroundBusy) {
113
+ this.foregroundBusy = false;
114
+ this.bus.emit("shell:foreground-busy", { busy: false });
115
+ }
116
+ if (this.lastCommand) {
117
+ const raw = this.cleanOutput(this.currentOutputCapture);
118
+ const output = this.removeEchoedCommand(stripAnsi(raw).trim(), this.lastCommand);
119
+ const outputRaw = this.removeEchoedCommand(raw.trim(), this.lastCommand);
120
+ this.bus.emit("shell:command-done", {
121
+ command: this.lastCommand,
122
+ output,
123
+ outputRaw,
124
+ cwd: this.cwd,
125
+ exitCode: null,
126
+ });
127
+ }
128
+ this.lastCommand = "";
129
+ this.currentOutputCapture = "";
130
+ } else {
131
+ // Cap to the tail so a long-running foreground program (tmux, vim)
132
+ // emitting output without prompt markers can't grow this unboundedly.
133
+ const MAX_CAPTURE = 128 * 1024;
134
+ this.currentOutputCapture += data;
135
+ if (this.currentOutputCapture.length > MAX_CAPTURE) {
136
+ this.currentOutputCapture = this.currentOutputCapture.slice(-MAX_CAPTURE);
137
+ }
138
+ }
139
+ }
140
+
141
+ /** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
142
+ private parsePromptEnd(data: string): void {
143
+ const match = READY_RE.exec(data);
144
+ if (!match) return;
145
+ if (match[1] !== this.ownTag) return;
146
+ this.promptReady = true;
147
+ }
148
+
149
+ private removeEchoedCommand(output: string, command: string): string {
150
+ const lines = output.split("\n");
151
+ if (lines.length > 0 && stripAnsi(lines[0]!).includes(command.slice(0, 20))) {
152
+ return lines.slice(1).join("\n").trim();
153
+ }
154
+ return output;
155
+ }
156
+ }
@@ -0,0 +1,193 @@
1
+ /** Tracks PTY commands and cwd, spills long outputs, contributes per-query
2
+ * `<shell_events>` (fresh user exchanges) and — under the shell frontend —
3
+ * `<cwd>`. Frontends without a PTY skip this. */
4
+ import type { ExtensionContext } from "./host-types.js";
5
+ import { getSettings } from "../core/settings.js";
6
+ import { spillOutput } from "../utils/shell-output-spill.js";
7
+
8
+ // The cwd-drift note applies only under the shell frontend (where the agent shares
9
+ // the user's cwd); other frontends own a fixed cwd.
10
+ const SHELL_EVENTS_NOTE = `When the user runs shell commands, they appear as \`<shell_events>\` inside \`<query_context>\` on your next turn — use them to ground "fix this" / "what just happened" requests.`;
11
+
12
+ const CWD_DRIFT_NOTE = `\`<cwd>\` is the working directory your own tool calls run in: relative paths resolve against it, and it follows the user's shell \`cd\`, so it can change from one turn to the next. Always act on the latest \`<cwd>\`, not one from earlier in the conversation.`;
13
+
14
+ const PREFERENCES_NOTE = `Treat the user's commands as standing preferences: check them for recurring patterns and apply them proactively, without waiting to be asked.`;
15
+
16
+ interface ShellExchange {
17
+ id: number;
18
+ timestamp: number;
19
+ cwd: string;
20
+ command: string;
21
+ /** In-context representation: full text if short, head+tail+path stub if spilled. */
22
+ output: string;
23
+ exitCode: number | null;
24
+ outputLines: number;
25
+ outputBytes: number;
26
+ source: "user" | "agent" | "user-excluded";
27
+ spillPath?: string;
28
+ }
29
+
30
+ export default function activate(ctx: ExtensionContext): void {
31
+ const { bus } = ctx;
32
+ // The agent shares the user's cwd only under the shell frontend (which installs
33
+ // ctx.shell); other frontends — e.g. ashi — keep their own fixed cwd.
34
+ const ownsAgentCwd = !!(ctx as { shell?: unknown }).shell;
35
+
36
+ const exchanges: ShellExchange[] = [];
37
+ let nextId = 1;
38
+ let currentCwd = process.cwd();
39
+ let agentShellActive = false;
40
+ let nextUserExcluded = false;
41
+ let lastSeq = 0;
42
+
43
+ bus.on("shell:command-done", (e) => {
44
+ const lines = e.output.split("\n");
45
+ const s = getSettings();
46
+
47
+ // Long outputs spill to a tempfile the agent can `read_file` instead of
48
+ // carrying the full text in LLM context.
49
+ let output = e.output;
50
+ let spillPath: string | undefined;
51
+ if (lines.length > s.shellTruncateThreshold) {
52
+ const id = nextId;
53
+ try {
54
+ spillPath = spillOutput(id, e.output);
55
+ output = buildSpillStub(lines, s.shellHeadLines, s.shellTailLines, spillPath);
56
+ } catch {
57
+ output = e.output;
58
+ spillPath = undefined;
59
+ }
60
+ }
61
+
62
+ const source: ShellExchange["source"] = agentShellActive
63
+ ? "agent"
64
+ : nextUserExcluded
65
+ ? "user-excluded"
66
+ : "user";
67
+ if (nextUserExcluded) nextUserExcluded = false;
68
+
69
+ exchanges.push({
70
+ id: nextId++,
71
+ timestamp: Date.now(),
72
+ cwd: e.cwd,
73
+ command: e.command,
74
+ output,
75
+ exitCode: e.exitCode,
76
+ outputLines: lines.length,
77
+ outputBytes: e.output.length,
78
+ source,
79
+ spillPath,
80
+ });
81
+ });
82
+
83
+ bus.on("shell:cwd-change", (e) => { currentCwd = e.cwd; });
84
+ bus.on("shell:agent-exec-start", () => { agentShellActive = true; });
85
+ bus.on("shell:agent-exec-done", () => { agentShellActive = false; });
86
+ bus.on("shell:user-exec-exclude-next", () => { nextUserExcluded = true; });
87
+
88
+ if (ownsAgentCwd) ctx.advise("cwd", () => currentCwd);
89
+
90
+ // Advise the core handler directly: this loads before the agent host
91
+ // attaches `ctx.agent`, so the sugar isn't available yet.
92
+ ctx.advise("query-context:build", (next) => {
93
+ const base = next() as string;
94
+ const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source === "user");
95
+ let shellEvents = "";
96
+ if (fresh.length > 0) {
97
+ lastSeq = exchanges[exchanges.length - 1]!.id;
98
+ const text = fresh.map(formatExchangeTruncated).filter(Boolean).join("\n");
99
+ if (text) shellEvents = `<shell_events>\n${text}\n</shell_events>`;
100
+ }
101
+ const part = ownsAgentCwd
102
+ ? [`<cwd>${currentCwd}</cwd>`, shellEvents].filter(Boolean).join("\n")
103
+ : shellEvents;
104
+ return [base, part].filter(Boolean).join("\n\n");
105
+ });
106
+
107
+ bus.onPipe("agent:instructions", (acc) => {
108
+ const text = [SHELL_EVENTS_NOTE, ownsAgentCwd ? CWD_DRIFT_NOTE : "", PREFERENCES_NOTE]
109
+ .filter(Boolean).join("\n\n");
110
+ acc.instructions.push({ name: "shell-events", text });
111
+ return acc;
112
+ });
113
+
114
+ ctx.define("shell:context-recent", (n: number = 25) => {
115
+ const recent = exchanges.slice(-n);
116
+ if (recent.length === 0) return "No exchanges yet.";
117
+ return recent.map(exchangeOneLiner).join("\n");
118
+ });
119
+
120
+ ctx.define("shell:context-search", (query: string) => {
121
+ if (!query.trim()) return "No query provided.";
122
+
123
+ let regex: RegExp;
124
+ try {
125
+ regex = new RegExp(query, "i");
126
+ } catch {
127
+ const words = query.split(/\s+/).filter((w) => w.length > 0);
128
+ regex = new RegExp(words.map(escapeRegex).join("|"), "i");
129
+ }
130
+
131
+ const matches: { exchange: ShellExchange; excerpts: string[] }[] = [];
132
+ for (const ex of exchanges) {
133
+ const text = `${ex.command}\n${ex.output}`;
134
+ const lines = text.split("\n");
135
+ const matchingIndices: number[] = [];
136
+ for (let i = 0; i < lines.length; i++) {
137
+ if (regex.test(lines[i]!)) matchingIndices.push(i);
138
+ }
139
+ if (matchingIndices.length > 0) {
140
+ const excerpts = matchingIndices.slice(0, 5).map((idx) => {
141
+ const start = Math.max(0, idx - 2);
142
+ const end = Math.min(lines.length, idx + 3);
143
+ return lines.slice(start, end).join("\n");
144
+ });
145
+ matches.push({ exchange: ex, excerpts });
146
+ }
147
+ }
148
+
149
+ if (matches.length === 0) return `No results found for "${query}".`;
150
+ const parts: string[] = [`Search results for "${query}" (${matches.length} exchanges):\n`];
151
+ for (const m of matches.slice(0, 20)) {
152
+ parts.push(`#${m.exchange.id} [shell_command]`);
153
+ for (const excerpt of m.excerpts) parts.push(indent(excerpt, " "));
154
+ parts.push("");
155
+ }
156
+ return parts.join("\n");
157
+ });
158
+ }
159
+
160
+ function formatExchangeTruncated(ex: ShellExchange): string {
161
+ const label = ex.source === "agent" ? "agent → shell" : "shell";
162
+ let s = `#${ex.id} [${label} cwd:${ex.cwd}] $ ${ex.command}\n`;
163
+ if (ex.output) s += indent(ex.output, " ") + "\n";
164
+ if (ex.exitCode !== null) s += ` exit ${ex.exitCode}\n`;
165
+ return s;
166
+ }
167
+
168
+ function exchangeOneLiner(ex: ShellExchange): string {
169
+ const label = ex.source === "agent" ? "agent → shell" : "shell";
170
+ return `#${ex.id} ${label} [cwd:${ex.cwd}]: ${ex.command} (${ex.outputLines} total lines, exit ${ex.exitCode ?? "?"})`;
171
+ }
172
+
173
+ function buildSpillStub(
174
+ lines: string[],
175
+ headLines: number,
176
+ tailLines: number,
177
+ spillPath: string,
178
+ ): string {
179
+ const omitted = lines.length - headLines - tailLines;
180
+ return [
181
+ ...lines.slice(0, headLines),
182
+ `[... ${omitted} lines truncated — full output at ${spillPath}; use read_file to expand ...]`,
183
+ ...lines.slice(-tailLines),
184
+ ].join("\n");
185
+ }
186
+
187
+ function indent(text: string, prefix: string): string {
188
+ return text.split("\n").map((line) => prefix + line).join("\n");
189
+ }
190
+
191
+ function escapeRegex(str: string): string {
192
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
193
+ }