agent-sh 0.4.0 → 0.5.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.
Files changed (76) hide show
  1. package/README.md +66 -113
  2. package/dist/agent/agent-loop.d.ts +85 -0
  3. package/dist/agent/agent-loop.js +611 -0
  4. package/dist/agent/conversation-state.d.ts +27 -0
  5. package/dist/agent/conversation-state.js +59 -0
  6. package/dist/agent/index.d.ts +11 -0
  7. package/dist/agent/index.js +9 -0
  8. package/dist/agent/skills.d.ts +25 -0
  9. package/dist/agent/skills.js +186 -0
  10. package/dist/agent/subagent.d.ts +37 -0
  11. package/dist/agent/subagent.js +117 -0
  12. package/dist/agent/system-prompt.d.ts +14 -0
  13. package/dist/agent/system-prompt.js +98 -0
  14. package/dist/agent/tool-registry.d.ts +15 -0
  15. package/dist/agent/tool-registry.js +30 -0
  16. package/dist/agent/tools/bash.d.ts +7 -0
  17. package/dist/agent/tools/bash.js +62 -0
  18. package/dist/agent/tools/edit-file.d.ts +2 -0
  19. package/dist/agent/tools/edit-file.js +95 -0
  20. package/dist/agent/tools/glob.d.ts +2 -0
  21. package/dist/agent/tools/glob.js +55 -0
  22. package/dist/agent/tools/grep.d.ts +2 -0
  23. package/dist/agent/tools/grep.js +77 -0
  24. package/dist/agent/tools/list-skills.d.ts +2 -0
  25. package/dist/agent/tools/list-skills.js +28 -0
  26. package/dist/agent/tools/ls.d.ts +2 -0
  27. package/dist/agent/tools/ls.js +43 -0
  28. package/dist/agent/tools/read-file.d.ts +2 -0
  29. package/dist/agent/tools/read-file.js +55 -0
  30. package/dist/agent/tools/user-shell.d.ts +13 -0
  31. package/dist/agent/tools/user-shell.js +57 -0
  32. package/dist/agent/tools/write-file.d.ts +2 -0
  33. package/dist/agent/tools/write-file.js +74 -0
  34. package/dist/agent/types.d.ts +44 -0
  35. package/dist/agent/types.js +1 -0
  36. package/dist/core.d.ts +24 -14
  37. package/dist/core.js +260 -36
  38. package/dist/event-bus.d.ts +80 -14
  39. package/dist/event-bus.js +10 -1
  40. package/dist/extension-loader.js +12 -1
  41. package/dist/extensions/command-suggest.d.ts +10 -0
  42. package/dist/extensions/command-suggest.js +41 -0
  43. package/dist/extensions/slash-commands.d.ts +1 -1
  44. package/dist/extensions/slash-commands.js +161 -64
  45. package/dist/extensions/tui-renderer.js +90 -48
  46. package/dist/index.js +98 -122
  47. package/dist/input-handler.js +74 -7
  48. package/dist/output-parser.d.ts +7 -0
  49. package/dist/output-parser.js +27 -0
  50. package/dist/settings.d.ts +53 -2
  51. package/dist/settings.js +45 -2
  52. package/dist/shell.js +33 -26
  53. package/dist/types.d.ts +33 -6
  54. package/dist/utils/box-frame.d.ts +3 -1
  55. package/dist/utils/box-frame.js +12 -5
  56. package/dist/utils/llm-client.d.ts +45 -0
  57. package/dist/utils/llm-client.js +60 -0
  58. package/dist/utils/markdown.js +2 -2
  59. package/dist/utils/stream-transform.js +20 -47
  60. package/dist/utils/tool-display.js +15 -5
  61. package/examples/extensions/claude-code-bridge/README.md +35 -0
  62. package/examples/extensions/claude-code-bridge/index.ts +198 -0
  63. package/examples/extensions/claude-code-bridge/package.json +11 -0
  64. package/examples/extensions/openrouter.ts +87 -0
  65. package/examples/extensions/pi-bridge/README.md +35 -0
  66. package/examples/extensions/pi-bridge/index.ts +265 -0
  67. package/examples/extensions/pi-bridge/package.json +13 -0
  68. package/examples/extensions/subagents.ts +87 -0
  69. package/package.json +3 -5
  70. package/dist/acp-client.d.ts +0 -105
  71. package/dist/acp-client.js +0 -684
  72. package/dist/extensions/shell-exec.d.ts +0 -24
  73. package/dist/extensions/shell-exec.js +0 -188
  74. package/dist/mcp-server.d.ts +0 -13
  75. package/dist/mcp-server.js +0 -234
  76. package/examples/pi-agent-sh.ts +0 -166
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "../types.js";
2
+ export declare function createEditFileTool(getCwd: () => string): ToolDefinition;
@@ -0,0 +1,95 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { computeDiff } from "../../utils/diff.js";
4
+ export function createEditFileTool(getCwd) {
5
+ return {
6
+ name: "edit_file",
7
+ description: "Edit a file by replacing an exact text match with new text. The old_text must appear exactly once in the file. Include enough context to make the match unique.",
8
+ input_schema: {
9
+ type: "object",
10
+ properties: {
11
+ path: {
12
+ type: "string",
13
+ description: "Absolute or relative file path",
14
+ },
15
+ old_text: {
16
+ type: "string",
17
+ description: "Exact text to find (must appear exactly once)",
18
+ },
19
+ new_text: {
20
+ type: "string",
21
+ description: "Replacement text",
22
+ },
23
+ },
24
+ required: ["path", "old_text", "new_text"],
25
+ },
26
+ showOutput: true,
27
+ modifiesFiles: true,
28
+ requiresPermission: true,
29
+ getDisplayInfo: (args) => ({
30
+ kind: "write",
31
+ locations: [{ path: args.path }],
32
+ }),
33
+ async execute(args, onChunk) {
34
+ const filePath = args.path;
35
+ const oldText = args.old_text;
36
+ const newText = args.new_text;
37
+ const absPath = path.resolve(getCwd(), filePath);
38
+ try {
39
+ const content = await fs.readFile(absPath, "utf-8");
40
+ // Normalize line endings for matching
41
+ const normalized = content.replace(/\r\n/g, "\n");
42
+ const normalizedOld = oldText.replace(/\r\n/g, "\n");
43
+ const occurrences = normalized.split(normalizedOld).length - 1;
44
+ if (occurrences === 0) {
45
+ return {
46
+ content: `Error: old_text not found in ${filePath}`,
47
+ exitCode: 1,
48
+ isError: true,
49
+ };
50
+ }
51
+ if (occurrences > 1) {
52
+ return {
53
+ content: `Error: old_text found ${occurrences} times, must be unique. Add more surrounding context.`,
54
+ exitCode: 1,
55
+ isError: true,
56
+ };
57
+ }
58
+ const normalizedNew = newText.replace(/\r\n/g, "\n");
59
+ const newContent = normalized.replace(normalizedOld, normalizedNew);
60
+ // Restore original line endings
61
+ const useCRLF = content.includes("\r\n");
62
+ const finalContent = useCRLF
63
+ ? newContent.replace(/\n/g, "\r\n")
64
+ : newContent;
65
+ await fs.writeFile(absPath, finalContent);
66
+ // Compute and stream diff for display
67
+ const diff = computeDiff(normalized, newContent);
68
+ if (onChunk && diff.hunks.length > 0) {
69
+ for (const hunk of diff.hunks) {
70
+ for (const line of hunk.lines) {
71
+ if (line.type === "added")
72
+ onChunk(`+${line.text}\n`);
73
+ else if (line.type === "removed")
74
+ onChunk(`-${line.text}\n`);
75
+ else
76
+ onChunk(` ${line.text}\n`);
77
+ }
78
+ }
79
+ }
80
+ const stats = diff.isNewFile
81
+ ? `+${diff.added}`
82
+ : `+${diff.added} -${diff.removed}`;
83
+ return {
84
+ content: `Edited ${absPath} (${stats})`,
85
+ exitCode: 0,
86
+ isError: false,
87
+ };
88
+ }
89
+ catch (err) {
90
+ const msg = err instanceof Error ? err.message : String(err);
91
+ return { content: `Error: ${msg}`, exitCode: 1, isError: true };
92
+ }
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "../types.js";
2
+ export declare function createGlobTool(getCwd: () => string): ToolDefinition;
@@ -0,0 +1,55 @@
1
+ import { executeCommand } from "../../executor.js";
2
+ export function createGlobTool(getCwd) {
3
+ return {
4
+ name: "glob",
5
+ description: "Find files matching a glob pattern. Returns file paths sorted by modification time.",
6
+ input_schema: {
7
+ type: "object",
8
+ properties: {
9
+ pattern: {
10
+ type: "string",
11
+ description: "Glob pattern (e.g., 'src/**/*.ts', '*.json')",
12
+ },
13
+ path: {
14
+ type: "string",
15
+ description: "Base directory to search (default: cwd)",
16
+ },
17
+ },
18
+ required: ["pattern"],
19
+ },
20
+ showOutput: false,
21
+ getDisplayInfo: (args) => ({
22
+ kind: "search",
23
+ locations: args.path
24
+ ? [{ path: args.path }]
25
+ : [],
26
+ }),
27
+ async execute(args) {
28
+ const pattern = args.pattern;
29
+ const searchPath = args.path ?? ".";
30
+ // Use find + shell glob via bash, or rg --files --glob
31
+ const { session, done } = executeCommand({
32
+ command: `find ${JSON.stringify(searchPath)} -path ${JSON.stringify(pattern)} -type f 2>/dev/null | head -200`,
33
+ cwd: getCwd(),
34
+ timeout: 10_000,
35
+ });
36
+ await done;
37
+ if (!session.output.trim()) {
38
+ return {
39
+ content: "No files matched.",
40
+ exitCode: 0,
41
+ isError: false,
42
+ };
43
+ }
44
+ const lines = session.output.trim().split("\n");
45
+ const suffix = lines.length >= 200
46
+ ? `\n[Results capped at 200 files]`
47
+ : "";
48
+ return {
49
+ content: session.output.trim() + suffix,
50
+ exitCode: 0,
51
+ isError: false,
52
+ };
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "../types.js";
2
+ export declare function createGrepTool(getCwd: () => string): ToolDefinition;
@@ -0,0 +1,77 @@
1
+ import { executeCommand } from "../../executor.js";
2
+ export function createGrepTool(getCwd) {
3
+ return {
4
+ name: "grep",
5
+ description: "Search file contents using ripgrep (rg). Returns matching lines with file paths and line numbers.",
6
+ input_schema: {
7
+ type: "object",
8
+ properties: {
9
+ pattern: {
10
+ type: "string",
11
+ description: "Regex pattern to search for",
12
+ },
13
+ path: {
14
+ type: "string",
15
+ description: "Directory or file to search (default: cwd)",
16
+ },
17
+ include: {
18
+ type: "string",
19
+ description: "Glob pattern for files to include (e.g., '*.ts')",
20
+ },
21
+ },
22
+ required: ["pattern"],
23
+ },
24
+ showOutput: false,
25
+ getDisplayInfo: (args) => ({
26
+ kind: "search",
27
+ locations: args.path
28
+ ? [{ path: args.path }]
29
+ : [],
30
+ }),
31
+ async execute(args) {
32
+ const pattern = args.pattern;
33
+ const searchPath = args.path ?? ".";
34
+ const include = args.include;
35
+ const parts = [
36
+ "rg",
37
+ "--line-number",
38
+ "--no-heading",
39
+ "--color=never",
40
+ "--max-count=200",
41
+ ];
42
+ if (include) {
43
+ parts.push("--glob", JSON.stringify(include));
44
+ }
45
+ parts.push(JSON.stringify(pattern), JSON.stringify(searchPath));
46
+ const { session, done } = executeCommand({
47
+ command: parts.join(" "),
48
+ cwd: getCwd(),
49
+ timeout: 10_000,
50
+ maxOutputBytes: 64 * 1024,
51
+ });
52
+ await done;
53
+ if (session.exitCode === 1 && !session.output.trim()) {
54
+ return {
55
+ content: "No matches found.",
56
+ exitCode: 0,
57
+ isError: false,
58
+ };
59
+ }
60
+ // Truncate to ~100 lines
61
+ const lines = session.output.split("\n");
62
+ if (lines.length > 100) {
63
+ return {
64
+ content: lines.slice(0, 100).join("\n") +
65
+ `\n[${lines.length - 100} more lines truncated]`,
66
+ exitCode: 0,
67
+ isError: false,
68
+ };
69
+ }
70
+ return {
71
+ content: session.output || "No matches found.",
72
+ exitCode: 0,
73
+ isError: false,
74
+ };
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "../types.js";
2
+ export declare function createListSkillsTool(getCwd: () => string): ToolDefinition;
@@ -0,0 +1,28 @@
1
+ import { discoverSkills } from "../skills.js";
2
+ export function createListSkillsTool(getCwd) {
3
+ return {
4
+ name: "list_skills",
5
+ description: "List available skills. Use read_file on a skill's path to load its full instructions.",
6
+ input_schema: {
7
+ type: "object",
8
+ properties: {},
9
+ },
10
+ showOutput: false,
11
+ async execute() {
12
+ const skills = discoverSkills(getCwd());
13
+ if (skills.length === 0) {
14
+ return {
15
+ content: "No skills found.",
16
+ exitCode: 0,
17
+ isError: false,
18
+ };
19
+ }
20
+ const lines = skills.map((s) => `${s.name} ${s.filePath}\n ${s.description}`);
21
+ return {
22
+ content: lines.join("\n\n"),
23
+ exitCode: 0,
24
+ isError: false,
25
+ };
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "../types.js";
2
+ export declare function createLsTool(getCwd: () => string): ToolDefinition;
@@ -0,0 +1,43 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ export function createLsTool(getCwd) {
4
+ return {
5
+ name: "ls",
6
+ description: "List files and directories in a given path.",
7
+ input_schema: {
8
+ type: "object",
9
+ properties: {
10
+ path: {
11
+ type: "string",
12
+ description: "Directory to list (default: cwd)",
13
+ },
14
+ },
15
+ },
16
+ showOutput: false,
17
+ getDisplayInfo: (args) => ({
18
+ kind: "read",
19
+ locations: args.path
20
+ ? [{ path: args.path }]
21
+ : [],
22
+ }),
23
+ async execute(args) {
24
+ const dirPath = args.path ?? ".";
25
+ const absPath = path.resolve(getCwd(), dirPath);
26
+ try {
27
+ const entries = await fs.readdir(absPath, {
28
+ withFileTypes: true,
29
+ });
30
+ const lines = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name);
31
+ return {
32
+ content: lines.join("\n") || "(empty directory)",
33
+ exitCode: 0,
34
+ isError: false,
35
+ };
36
+ }
37
+ catch (err) {
38
+ const msg = err instanceof Error ? err.message : String(err);
39
+ return { content: `Error: ${msg}`, exitCode: 1, isError: true };
40
+ }
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "../types.js";
2
+ export declare function createReadFileTool(getCwd: () => string): ToolDefinition;
@@ -0,0 +1,55 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ export function createReadFileTool(getCwd) {
4
+ return {
5
+ name: "read_file",
6
+ description: "Read a file's contents with line numbers. Optionally specify offset and limit for large files.",
7
+ input_schema: {
8
+ type: "object",
9
+ properties: {
10
+ path: {
11
+ type: "string",
12
+ description: "Absolute or relative file path",
13
+ },
14
+ offset: {
15
+ type: "number",
16
+ description: "Starting line number (1-indexed)",
17
+ },
18
+ limit: {
19
+ type: "number",
20
+ description: "Max lines to read",
21
+ },
22
+ },
23
+ required: ["path"],
24
+ },
25
+ showOutput: false,
26
+ getDisplayInfo: (args) => ({
27
+ kind: "read",
28
+ locations: [{ path: args.path }],
29
+ }),
30
+ async execute(args) {
31
+ const filePath = args.path;
32
+ const absPath = path.resolve(getCwd(), filePath);
33
+ try {
34
+ const content = await fs.readFile(absPath, "utf-8");
35
+ const lines = content.split("\n");
36
+ const start = (args.offset ?? 1) - 1; // 1-indexed → 0-indexed
37
+ const end = args.limit ? start + args.limit : lines.length;
38
+ const slice = lines.slice(start, end);
39
+ // Add line numbers (1-indexed)
40
+ const numbered = slice
41
+ .map((line, i) => `${start + i + 1}\t${line}`)
42
+ .join("\n");
43
+ const truncated = end < lines.length;
44
+ const suffix = truncated
45
+ ? `\n[${lines.length - end} more lines, use offset=${end + 1} to continue]`
46
+ : "";
47
+ return { content: numbered + suffix, exitCode: 0, isError: false };
48
+ }
49
+ catch (err) {
50
+ const msg = err instanceof Error ? err.message : String(err);
51
+ return { content: `Error: ${msg}`, exitCode: 1, isError: true };
52
+ }
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,13 @@
1
+ import type { EventBus } from "../../event-bus.js";
2
+ import type { ToolDefinition } from "../types.js";
3
+ /**
4
+ * user_shell — runs commands in the user's live PTY shell.
5
+ *
6
+ * Unlike bash, this affects the user's shell state (cd, export, source).
7
+ * Output is shown directly in the terminal. By default, the agent doesn't
8
+ * see the output (return_output=false) to save tokens.
9
+ */
10
+ export declare function createUserShellTool(opts: {
11
+ getCwd: () => string;
12
+ bus: EventBus;
13
+ }): ToolDefinition;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * user_shell — runs commands in the user's live PTY shell.
3
+ *
4
+ * Unlike bash, this affects the user's shell state (cd, export, source).
5
+ * Output is shown directly in the terminal. By default, the agent doesn't
6
+ * see the output (return_output=false) to save tokens.
7
+ */
8
+ export function createUserShellTool(opts) {
9
+ return {
10
+ name: "user_shell",
11
+ description: "Run a command in the user's live shell (visible in terminal). Output is returned to you by default. Use for cd, export, source, or commands the user wants to see. Set return_output=false for long-running or interactive commands.",
12
+ input_schema: {
13
+ type: "object",
14
+ properties: {
15
+ command: {
16
+ type: "string",
17
+ description: "Command to execute in user's shell",
18
+ },
19
+ return_output: {
20
+ type: "boolean",
21
+ default: false,
22
+ description: "Whether to return the command output to you. Default false — output is shown directly to the user. Set true only if you need to inspect the result to answer a question.",
23
+ },
24
+ },
25
+ required: ["command"],
26
+ },
27
+ showOutput: false,
28
+ modifiesFiles: true,
29
+ getDisplayInfo: () => ({
30
+ kind: "execute",
31
+ locations: [],
32
+ }),
33
+ async execute(args) {
34
+ const command = args.command;
35
+ const returnOutput = args.return_output ?? false;
36
+ // Execute via the shell-exec extension's async pipe
37
+ const result = await opts.bus.emitPipeAsync("shell:exec-request", {
38
+ command,
39
+ output: "",
40
+ cwd: opts.getCwd(),
41
+ done: false,
42
+ });
43
+ if (returnOutput) {
44
+ return {
45
+ content: result.output || "(no output)",
46
+ exitCode: 0,
47
+ isError: false,
48
+ };
49
+ }
50
+ return {
51
+ content: "Command executed.",
52
+ exitCode: 0,
53
+ isError: false,
54
+ };
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "../types.js";
2
+ export declare function createWriteFileTool(getCwd: () => string): ToolDefinition;
@@ -0,0 +1,74 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { computeDiff } from "../../utils/diff.js";
4
+ export function createWriteFileTool(getCwd) {
5
+ return {
6
+ name: "write_file",
7
+ description: "Create or overwrite a file with the given content. Creates parent directories if needed. Prefer edit_file for modifying existing files.",
8
+ input_schema: {
9
+ type: "object",
10
+ properties: {
11
+ path: {
12
+ type: "string",
13
+ description: "Absolute or relative file path",
14
+ },
15
+ content: {
16
+ type: "string",
17
+ description: "File content to write",
18
+ },
19
+ },
20
+ required: ["path", "content"],
21
+ },
22
+ showOutput: true,
23
+ modifiesFiles: true,
24
+ requiresPermission: true,
25
+ getDisplayInfo: (args) => ({
26
+ kind: "write",
27
+ locations: [{ path: args.path }],
28
+ }),
29
+ async execute(args, onChunk) {
30
+ const filePath = args.path;
31
+ const content = args.content;
32
+ const absPath = path.resolve(getCwd(), filePath);
33
+ try {
34
+ let oldContent = null;
35
+ try {
36
+ oldContent = await fs.readFile(absPath, "utf-8");
37
+ }
38
+ catch {
39
+ // New file
40
+ }
41
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
42
+ await fs.writeFile(absPath, content);
43
+ // Compute and stream diff for display
44
+ const diff = computeDiff(oldContent, content);
45
+ if (onChunk && diff.hunks.length > 0) {
46
+ for (const hunk of diff.hunks) {
47
+ for (const line of hunk.lines) {
48
+ if (line.type === "added")
49
+ onChunk(`+${line.text}\n`);
50
+ else if (line.type === "removed")
51
+ onChunk(`-${line.text}\n`);
52
+ else
53
+ onChunk(` ${line.text}\n`);
54
+ }
55
+ }
56
+ }
57
+ const stats = diff.isNewFile
58
+ ? `+${diff.added}`
59
+ : `+${diff.added} -${diff.removed}`;
60
+ return {
61
+ content: oldContent === null
62
+ ? `Created ${absPath} (${stats})`
63
+ : `Wrote ${absPath} (${stats})`,
64
+ exitCode: 0,
65
+ isError: false,
66
+ };
67
+ }
68
+ catch (err) {
69
+ const msg = err instanceof Error ? err.message : String(err);
70
+ return { content: `Error: ${msg}`, exitCode: 1, isError: true };
71
+ }
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Minimal agent backend interface — bus-driven.
3
+ *
4
+ * Backends self-wire to bus events in their constructor:
5
+ * - agent:submit → handle queries
6
+ * - agent:cancel-request → handle cancellation
7
+ * - config:cycle → handle mode switching
8
+ *
9
+ * They emit bus events for results:
10
+ * - agent:response-chunk, agent:tool-started, agent:tool-completed, etc.
11
+ *
12
+ * The only imperative method is kill() for lifecycle cleanup.
13
+ */
14
+ export interface AgentBackend {
15
+ /** Async startup (e.g. spawn subprocess). No-op if not needed. */
16
+ start?(): Promise<void>;
17
+ kill(): void;
18
+ }
19
+ export interface ToolResult {
20
+ content: string;
21
+ exitCode: number | null;
22
+ isError: boolean;
23
+ }
24
+ export interface ToolDisplayInfo {
25
+ kind: "read" | "write" | "execute" | "search";
26
+ locations?: {
27
+ path: string;
28
+ line?: number | null;
29
+ }[];
30
+ }
31
+ export interface ToolDefinition {
32
+ name: string;
33
+ description: string;
34
+ input_schema: Record<string, unknown>;
35
+ execute(args: Record<string, unknown>, onChunk?: (chunk: string) => void): Promise<ToolResult>;
36
+ /** Whether to stream tool output to the TUI (default: true). */
37
+ showOutput?: boolean;
38
+ /** Whether this tool may modify files — triggers file watcher (default: false). */
39
+ modifiesFiles?: boolean;
40
+ /** Whether to gate execution via permission:request (default: false). */
41
+ requiresPermission?: boolean;
42
+ /** Derive display metadata (icon kind, file paths) for the TUI. */
43
+ getDisplayInfo?: (args: Record<string, unknown>) => ToolDisplayInfo;
44
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/core.d.ts CHANGED
@@ -1,41 +1,51 @@
1
1
  /**
2
2
  * Core kernel — the minimum viable agent-sh.
3
3
  *
4
- * Wires up EventBus + ContextManager + AcpClient without any frontend.
4
+ * Wires up EventBus + ContextManager + AgentBackend without any frontend.
5
5
  * Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
6
- * subscribing to bus events and calling client methods.
6
+ * subscribing to bus events.
7
7
  *
8
- * The core listens for `agent:submit` and `agent:cancel-request` events
9
- * from any frontend, routing them to the AcpClient. This means frontends
10
- * never need a direct reference to AcpClient — they just emit events.
8
+ * The default backend (AgentLoop) is created eagerly but wired lazily —
9
+ * extensions can register alternative backends via agent:register-backend
10
+ * before activateBackend() is called.
11
11
  *
12
12
  * Usage:
13
13
  * import { createCore } from "agent-sh";
14
- * const core = createCore({ agentCommand: "pi-acp" });
15
- * core.bus.on("agent:response-chunk", ({ text }) => ws.send(text));
16
- * await core.start();
17
- * core.bus.emit("agent:submit", { query: "hello" });
14
+ * const core = createCore({ apiKey: "...", model: "gpt-4o" });
15
+ * core.bus.on("agent:response-chunk", ({ blocks }) => { ... });
16
+ * core.activateBackend();
17
+ * const response = await core.query("hello");
18
18
  */
19
19
  import { EventBus } from "./event-bus.js";
20
20
  import { ContextManager } from "./context-manager.js";
21
- import { AcpClient } from "./acp-client.js";
21
+ import { LlmClient } from "./utils/llm-client.js";
22
22
  import type { AgentShellConfig, ExtensionContext } from "./types.js";
23
23
  export { EventBus } from "./event-bus.js";
24
24
  export type { ShellEvents } from "./event-bus.js";
25
25
  export type { AgentShellConfig, ExtensionContext } from "./types.js";
26
26
  export { palette, setPalette, resetPalette } from "./utils/palette.js";
27
27
  export type { ColorPalette } from "./utils/palette.js";
28
+ export type { AgentBackend, ToolDefinition } from "./agent/types.js";
29
+ export { runSubagent, type SubagentOptions } from "./agent/subagent.js";
30
+ export { LlmClient } from "./utils/llm-client.js";
28
31
  export interface AgentShellCore {
29
32
  bus: EventBus;
30
33
  contextManager: ContextManager;
31
- client: AcpClient;
32
- /** Connect to the agent subprocess. Call after wiring up bus listeners. */
33
- start(): Promise<void>;
34
+ /** LLM client for fast-path features (null when no provider configured). */
35
+ llmClient: LlmClient | null;
36
+ /** Activate the agent backend (call after extensions load). */
37
+ activateBackend(): void;
38
+ /** Convenience: emit agent:submit and await the response. */
39
+ query(text: string, opts?: {
40
+ mode?: string;
41
+ }): Promise<string>;
42
+ /** Convenience: emit agent:cancel-request. */
43
+ cancel(): void;
34
44
  /** Build an ExtensionContext for loading extensions against this core. */
35
45
  extensionContext(opts: {
36
46
  quit: () => void;
37
47
  }): ExtensionContext;
38
- /** Tear down the agent process and clean up. */
48
+ /** Tear down the agent and clean up. */
39
49
  kill(): void;
40
50
  }
41
51
  export declare function createCore(config: AgentShellConfig): AgentShellCore;