agent-sh 0.2.0 → 0.3.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 (42) hide show
  1. package/README.md +21 -0
  2. package/dist/acp-client.d.ts +24 -0
  3. package/dist/acp-client.js +155 -33
  4. package/dist/context-manager.d.ts +5 -3
  5. package/dist/context-manager.js +62 -31
  6. package/dist/core.js +10 -0
  7. package/dist/event-bus.d.ts +26 -0
  8. package/dist/event-bus.js +10 -0
  9. package/dist/extension-loader.js +3 -14
  10. package/dist/extensions/shell-exec.js +27 -22
  11. package/dist/extensions/tui-renderer.d.ts +1 -1
  12. package/dist/extensions/tui-renderer.js +369 -126
  13. package/dist/index.js +184 -37
  14. package/dist/input-handler.d.ts +10 -0
  15. package/dist/input-handler.js +169 -10
  16. package/dist/mcp-server.js +37 -8
  17. package/dist/settings.d.ts +44 -0
  18. package/dist/settings.js +61 -0
  19. package/dist/shell.d.ts +1 -0
  20. package/dist/shell.js +44 -4
  21. package/dist/types.d.ts +17 -0
  22. package/dist/utils/ansi.d.ts +4 -1
  23. package/dist/utils/ansi.js +60 -2
  24. package/dist/utils/box-frame.js +2 -1
  25. package/dist/utils/diff-renderer.js +1 -1
  26. package/dist/utils/frame-renderer.d.ts +26 -0
  27. package/dist/utils/frame-renderer.js +76 -0
  28. package/dist/utils/handler-registry.d.ts +41 -0
  29. package/dist/utils/handler-registry.js +52 -0
  30. package/dist/utils/line-editor.d.ts +21 -1
  31. package/dist/utils/line-editor.js +193 -99
  32. package/dist/utils/markdown.d.ts +15 -6
  33. package/dist/utils/markdown.js +106 -67
  34. package/dist/utils/output-writer.d.ts +22 -0
  35. package/dist/utils/output-writer.js +29 -0
  36. package/dist/utils/stream-transform.d.ts +70 -0
  37. package/dist/utils/stream-transform.js +229 -0
  38. package/dist/utils/tool-display.d.ts +11 -8
  39. package/dist/utils/tool-display.js +69 -46
  40. package/examples/extensions/latex-images.ts +142 -0
  41. package/examples/pi-agent-sh.ts +166 -0
  42. package/package.json +10 -2
@@ -18,6 +18,8 @@ export interface ShellEvents {
18
18
  "shell:foreground-busy": {
19
19
  busy: boolean;
20
20
  };
21
+ "shell:agent-exec-start": Record<string, never>;
22
+ "shell:agent-exec-done": Record<string, never>;
21
23
  "agent:submit": {
22
24
  query: string;
23
25
  };
@@ -30,6 +32,7 @@ export interface ShellEvents {
30
32
  };
31
33
  "agent:response-chunk": {
32
34
  text: string;
35
+ blocks?: ContentBlock[];
33
36
  };
34
37
  "agent:response-done": {
35
38
  response: string;
@@ -114,6 +117,8 @@ export interface ShellEvents {
114
117
  }[];
115
118
  }[];
116
119
  };
120
+ "config:changed": Record<string, never>;
121
+ "config:cycle": Record<string, never>;
117
122
  "autocomplete:request": {
118
123
  buffer: string;
119
124
  items: {
@@ -122,6 +127,20 @@ export interface ShellEvents {
122
127
  }[];
123
128
  };
124
129
  }
130
+ export type ContentBlock = {
131
+ type: "text";
132
+ text: string;
133
+ } | {
134
+ type: "code-block";
135
+ language: string;
136
+ code: string;
137
+ } | {
138
+ type: "image";
139
+ data: Buffer;
140
+ } | {
141
+ type: "raw";
142
+ escape: string;
143
+ };
125
144
  type Listener<T> = (payload: T) => void;
126
145
  type PipeListener<T> = (payload: T) => T;
127
146
  type AsyncPipeListener<T> = (payload: T) => T | Promise<T>;
@@ -141,6 +160,13 @@ export declare class EventBus {
141
160
  off<K extends keyof ShellEvents>(event: K, fn: Listener<ShellEvents[K]>): void;
142
161
  /** Emit a fire-and-forget event. */
143
162
  emit<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
163
+ /**
164
+ * Transform-then-notify: run the payload through any registered pipe
165
+ * listeners (transforms), then emit the final result to regular `on`
166
+ * listeners (renderers). This enables content pipelines where extensions
167
+ * modify data (e.g. render LaTeX → terminal image) before renderers see it.
168
+ */
169
+ emitTransform<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
144
170
  /** Register a transform listener for a pipeline event. */
145
171
  onPipe<K extends keyof ShellEvents>(event: K, fn: PipeListener<ShellEvents[K]>): void;
146
172
  /**
package/dist/event-bus.js CHANGED
@@ -21,6 +21,16 @@ export class EventBus {
21
21
  emit(event, payload) {
22
22
  this.emitter.emit(event, payload);
23
23
  }
24
+ /**
25
+ * Transform-then-notify: run the payload through any registered pipe
26
+ * listeners (transforms), then emit the final result to regular `on`
27
+ * listeners (renderers). This enables content pipelines where extensions
28
+ * modify data (e.g. render LaTeX → terminal image) before renderers see it.
29
+ */
30
+ emitTransform(event, payload) {
31
+ const transformed = this.emitPipe(event, payload);
32
+ this.emitter.emit(event, transformed);
33
+ }
24
34
  /** Register a transform listener for a pipeline event. */
25
35
  onPipe(event, fn) {
26
36
  let listeners = this.pipeListeners.get(event);
@@ -1,9 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import * as os from "node:os";
4
- const CONFIG_DIR = path.join(os.homedir(), ".agent-sh");
3
+ import { CONFIG_DIR, getSettings } from "./settings.js";
5
4
  const EXT_DIR = path.join(CONFIG_DIR, "extensions");
6
- const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
7
5
  const TS_EXTS = [".ts", ".tsx", ".mts"];
8
6
  const SCRIPT_EXTS = [".js", ".mjs", ".ts", ".tsx", ".mts"];
9
7
  let tsRegistered = false;
@@ -19,15 +17,6 @@ async function ensureTsSupport() {
19
17
  // tsx not available — TS extensions will fail with a clear error
20
18
  }
21
19
  }
22
- async function loadSettings() {
23
- try {
24
- const raw = await fs.readFile(SETTINGS_PATH, "utf-8");
25
- return JSON.parse(raw);
26
- }
27
- catch {
28
- return {};
29
- }
30
- }
31
20
  /**
32
21
  * Load extensions from three sources (merged, deduplicated):
33
22
  *
@@ -49,8 +38,8 @@ export async function loadExtensions(ctx, cliExtensions) {
49
38
  specifiers.push(...cliExtensions);
50
39
  }
51
40
  // 2. settings.json
52
- const settings = await loadSettings();
53
- if (settings.extensions) {
41
+ const settings = getSettings();
42
+ if (settings.extensions.length > 0) {
54
43
  specifiers.push(...settings.extensions);
55
44
  }
56
45
  // 3. ~/.agent-sh/extensions/ directory
@@ -22,25 +22,30 @@ import * as net from "node:net";
22
22
  import * as fs from "node:fs";
23
23
  import * as path from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
+ import { getSettings } from "../settings.js";
25
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
27
  export default function activate({ bus, contextManager }, opts) {
27
28
  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
29
+ // Register MCP server so ACP agents discover the bridge tools.
30
+ // Agents that don't support MCP (e.g. pi-acp) simply ignore it.
31
+ // Can be disabled via settings.json if not needed.
32
+ if (getSettings().enableMcp) {
33
+ bus.onPipe("session:configure", (payload) => {
34
+ return {
35
+ ...payload,
36
+ mcpServers: [
37
+ ...payload.mcpServers,
38
+ {
39
+ name: "agent-sh",
40
+ command: process.execPath,
41
+ args: [path.join(__dirname, "..", "mcp-server.js")],
42
+ env: [{ name: "AGENT_SH_SOCKET", value: socketPath }],
43
+ },
44
+ ],
45
+ };
46
+ });
47
+ }
48
+ // Set AGENT_SH_SOCKET for agent extensions that connect directly
44
49
  process.env.AGENT_SH_SOCKET = socketPath;
45
50
  // Serialize shell/exec requests — only one PTY command at a time
46
51
  let execPending = Promise.resolve();
@@ -56,21 +61,19 @@ export default function activate({ bus, contextManager }, opts) {
56
61
  return new Promise((resolve, reject) => {
57
62
  execPending = execPending.then(async () => {
58
63
  try {
64
+ bus.emit("shell:agent-exec-start", {});
59
65
  const result = await bus.emitPipeAsync("shell:exec-request", {
60
66
  command,
61
67
  output: "",
62
68
  cwd: "",
63
69
  done: false,
64
70
  });
65
- // Show the command output in the TUI
66
- if (result.output) {
67
- bus.emit("agent:tool-output-chunk", { chunk: result.output });
68
- }
71
+ bus.emit("shell:agent-exec-done", {});
69
72
  resolve({ output: result.output, cwd: result.cwd });
70
73
  }
71
74
  catch (err) {
75
+ bus.emit("shell:agent-exec-done", {});
72
76
  const message = err instanceof Error ? err.message : String(err);
73
- bus.emit("agent:tool-output-chunk", { chunk: `Error: ${message}` });
74
77
  reject(rpcError(-32000, message));
75
78
  }
76
79
  });
@@ -98,7 +101,9 @@ export default function activate({ bus, contextManager }, opts) {
98
101
  if (!Array.isArray(ids) || ids.length === 0) {
99
102
  throw rpcError(-32602, "Missing required parameter: ids (array of numbers)");
100
103
  }
101
- return { result: contextManager.expand(ids.map(Number)) };
104
+ const start = typeof params?.start === "number" ? params.start : undefined;
105
+ const end = typeof params?.end === "number" ? params.end : undefined;
106
+ return { result: contextManager.expand(ids.map(Number), start, end) };
102
107
  }
103
108
  case "browse":
104
109
  return { result: contextManager.getRecentSummary() };
@@ -1,2 +1,2 @@
1
1
  import type { ExtensionContext } from "../types.js";
2
- export default function activate({ bus }: ExtensionContext): void;
2
+ export default function activate(ctx: ExtensionContext): void;