agent-sh 0.4.0 → 0.6.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/README.md +37 -115
- package/dist/agent/agent-loop.d.ts +86 -0
- package/dist/agent/agent-loop.js +704 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +119 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +103 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +71 -0
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +148 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +87 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +168 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +72 -0
- package/dist/agent/tools/read-file.d.ts +10 -0
- package/dist/agent/tools/read-file.js +101 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +84 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +82 -0
- package/dist/agent/types.d.ts +78 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +22 -14
- package/dist/core.js +256 -36
- package/dist/event-bus.d.ts +98 -17
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +10 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +426 -126
- package/dist/index.js +110 -129
- package/dist/input-handler.js +78 -9
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +46 -3
- package/dist/shell.js +35 -28
- package/dist/types.d.ts +33 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/diff.js +10 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +25 -3
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +35 -8
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +194 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +263 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/secret-guard.ts +100 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -105
- package/dist/acp-client.js +0 -684
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
function formatSize(bytes) {
|
|
4
|
+
if (bytes < 1024)
|
|
5
|
+
return `${bytes}B`;
|
|
6
|
+
if (bytes < 1024 * 1024)
|
|
7
|
+
return `${(bytes / 1024).toFixed(1)}K`;
|
|
8
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
9
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
10
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
|
11
|
+
}
|
|
12
|
+
export function createLsTool(getCwd) {
|
|
13
|
+
return {
|
|
14
|
+
name: "ls",
|
|
15
|
+
description: "List files and directories with timestamps and sizes. " +
|
|
16
|
+
"Use for exploring a single directory. Use glob for recursive file search by pattern.",
|
|
17
|
+
input_schema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
path: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Directory to list (default: cwd)",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
showOutput: false,
|
|
27
|
+
getDisplayInfo: (args) => ({
|
|
28
|
+
kind: "read",
|
|
29
|
+
icon: "◆",
|
|
30
|
+
locations: args.path
|
|
31
|
+
? [{ path: args.path }]
|
|
32
|
+
: [],
|
|
33
|
+
}),
|
|
34
|
+
formatResult: (_args, result) => {
|
|
35
|
+
if (result.isError || result.content === "(empty directory)")
|
|
36
|
+
return { summary: "0 entries" };
|
|
37
|
+
const lines = result.content.split("\n").filter(Boolean);
|
|
38
|
+
return { summary: `${lines.length} entries` };
|
|
39
|
+
},
|
|
40
|
+
async execute(args) {
|
|
41
|
+
const dirPath = args.path ?? ".";
|
|
42
|
+
const absPath = path.resolve(getCwd(), dirPath);
|
|
43
|
+
try {
|
|
44
|
+
const entries = await fs.readdir(absPath, {
|
|
45
|
+
withFileTypes: true,
|
|
46
|
+
});
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
const fullPath = path.join(absPath, e.name);
|
|
50
|
+
try {
|
|
51
|
+
const stat = await fs.stat(fullPath);
|
|
52
|
+
const size = e.isDirectory() ? "-" : formatSize(stat.size);
|
|
53
|
+
const mtime = stat.mtime.toISOString().slice(0, 16).replace("T", " ");
|
|
54
|
+
lines.push(`${mtime} ${size.padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
lines.push(`${"?".padStart(16)} ${"?".padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
content: lines.join("\n") || "(empty directory)",
|
|
62
|
+
exitCode: 0,
|
|
63
|
+
isError: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
return { content: `Error: ${msg}`, exitCode: 1, isError: true };
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../types.js";
|
|
2
|
+
/** Tracks the last-read state of a file for deduplication. */
|
|
3
|
+
export interface FileReadState {
|
|
4
|
+
mtimeMs: number;
|
|
5
|
+
offset: number;
|
|
6
|
+
limit: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Shared cache — keyed by absolute path. */
|
|
9
|
+
export type FileReadCache = Map<string, FileReadState>;
|
|
10
|
+
export declare function createReadFileTool(getCwd: () => string, cache?: FileReadCache): ToolDefinition;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export function createReadFileTool(getCwd, cache) {
|
|
4
|
+
return {
|
|
5
|
+
name: "read_file",
|
|
6
|
+
description: "Read a file's contents with line numbers. Use offset and limit for large files. " +
|
|
7
|
+
"Always read a file before editing it. " +
|
|
8
|
+
"If the file hasn't changed since last read, returns a stub to save context.",
|
|
9
|
+
input_schema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
path: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Absolute or relative file path",
|
|
15
|
+
},
|
|
16
|
+
offset: {
|
|
17
|
+
type: "number",
|
|
18
|
+
description: "Starting line number (1-indexed)",
|
|
19
|
+
},
|
|
20
|
+
limit: {
|
|
21
|
+
type: "number",
|
|
22
|
+
description: "Max lines to read",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["path"],
|
|
26
|
+
},
|
|
27
|
+
showOutput: false,
|
|
28
|
+
getDisplayInfo: (args) => ({
|
|
29
|
+
kind: "read",
|
|
30
|
+
icon: "◆",
|
|
31
|
+
locations: [{ path: args.path }],
|
|
32
|
+
}),
|
|
33
|
+
formatResult: (_args, result) => {
|
|
34
|
+
if (result.isError)
|
|
35
|
+
return {};
|
|
36
|
+
if (result.content.startsWith("File unchanged"))
|
|
37
|
+
return { summary: "cached" };
|
|
38
|
+
const lines = result.content.split("\n").filter(l => !l.startsWith("["));
|
|
39
|
+
return { summary: `${lines.length} lines` };
|
|
40
|
+
},
|
|
41
|
+
async execute(args) {
|
|
42
|
+
const filePath = args.path;
|
|
43
|
+
const absPath = path.resolve(getCwd(), filePath);
|
|
44
|
+
const reqOffset = args.offset ?? 1;
|
|
45
|
+
const reqLimit = args.limit;
|
|
46
|
+
try {
|
|
47
|
+
const stat = await fs.stat(absPath);
|
|
48
|
+
// Deduplication: if the file hasn't changed and same range, return stub
|
|
49
|
+
if (cache) {
|
|
50
|
+
const prev = cache.get(absPath);
|
|
51
|
+
if (prev &&
|
|
52
|
+
prev.mtimeMs === stat.mtimeMs &&
|
|
53
|
+
prev.offset === reqOffset &&
|
|
54
|
+
prev.limit === reqLimit) {
|
|
55
|
+
return {
|
|
56
|
+
content: "File unchanged since last read. The content from the earlier read_file result in this conversation is still current — refer to that instead of re-reading.",
|
|
57
|
+
exitCode: 0,
|
|
58
|
+
isError: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check file size before reading to avoid OOM on huge files
|
|
63
|
+
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
|
64
|
+
if (stat.size > MAX_FILE_SIZE && !args.offset && !args.limit) {
|
|
65
|
+
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
66
|
+
return {
|
|
67
|
+
content: `File is ${sizeMB}MB (${stat.size} bytes) — too large to read in full. Use offset and limit to read specific sections, e.g. offset=1 limit=200.`,
|
|
68
|
+
exitCode: 1,
|
|
69
|
+
isError: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const content = await fs.readFile(absPath, "utf-8");
|
|
73
|
+
const lines = content.split("\n");
|
|
74
|
+
const start = reqOffset - 1; // 1-indexed → 0-indexed
|
|
75
|
+
const end = reqLimit ? start + reqLimit : lines.length;
|
|
76
|
+
const slice = lines.slice(start, end);
|
|
77
|
+
// Add line numbers (1-indexed)
|
|
78
|
+
const numbered = slice
|
|
79
|
+
.map((line, i) => `${start + i + 1}\t${line}`)
|
|
80
|
+
.join("\n");
|
|
81
|
+
const truncated = end < lines.length;
|
|
82
|
+
const suffix = truncated
|
|
83
|
+
? `\n[${lines.length - end} more lines, use offset=${end + 1} to continue]`
|
|
84
|
+
: "";
|
|
85
|
+
// Update cache on successful read
|
|
86
|
+
if (cache) {
|
|
87
|
+
cache.set(absPath, {
|
|
88
|
+
mtimeMs: stat.mtimeMs,
|
|
89
|
+
offset: reqOffset,
|
|
90
|
+
limit: reqLimit,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return { content: numbered + suffix, exitCode: 0, isError: false };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
97
|
+
return { content: `Error: ${msg}`, exitCode: 1, isError: true };
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -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,84 @@
|
|
|
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 with lasting effects in the user's live shell (cd, export, install packages, start servers, apply changes). Output is shown directly to the user but NOT returned to you by default — set return_output=true if you need to inspect the result.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
command: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Command to execute in user's shell",
|
|
18
|
+
},
|
|
19
|
+
timeout: {
|
|
20
|
+
type: "number",
|
|
21
|
+
description: "Timeout in seconds (default: 30)",
|
|
22
|
+
},
|
|
23
|
+
return_output: {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
default: false,
|
|
26
|
+
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.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["command"],
|
|
30
|
+
},
|
|
31
|
+
showOutput: false,
|
|
32
|
+
modifiesFiles: true,
|
|
33
|
+
getDisplayInfo: () => ({
|
|
34
|
+
kind: "execute",
|
|
35
|
+
icon: "▷",
|
|
36
|
+
locations: [],
|
|
37
|
+
}),
|
|
38
|
+
async execute(args) {
|
|
39
|
+
const command = args.command;
|
|
40
|
+
const timeoutSec = args.timeout ?? 30;
|
|
41
|
+
const returnOutput = args.return_output ?? false;
|
|
42
|
+
// Execute via the shell-exec extension's async pipe with timeout
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
const execPromise = opts.bus.emitPipeAsync("shell:exec-request", {
|
|
46
|
+
command,
|
|
47
|
+
output: "",
|
|
48
|
+
cwd: opts.getCwd(),
|
|
49
|
+
exitCode: null,
|
|
50
|
+
done: false,
|
|
51
|
+
});
|
|
52
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeoutSec * 1000));
|
|
53
|
+
result = await Promise.race([execPromise, timeoutPromise]);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
57
|
+
if (msg === "timeout") {
|
|
58
|
+
return {
|
|
59
|
+
content: `Command timed out after ${timeoutSec}s.`,
|
|
60
|
+
exitCode: -1,
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { content: `Error: ${msg}`, exitCode: -1, isError: true };
|
|
65
|
+
}
|
|
66
|
+
const exitCode = result.exitCode ?? 0;
|
|
67
|
+
const isError = exitCode !== 0 && exitCode !== null;
|
|
68
|
+
if (returnOutput) {
|
|
69
|
+
return {
|
|
70
|
+
content: result.output || "(no output)",
|
|
71
|
+
exitCode,
|
|
72
|
+
isError,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
content: isError
|
|
77
|
+
? `Command failed with exit code ${exitCode}.`
|
|
78
|
+
: "Command executed.",
|
|
79
|
+
exitCode,
|
|
80
|
+
isError,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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 a new file or completely overwrite an existing one. Creates parent directories if needed. " +
|
|
8
|
+
"ALWAYS prefer edit_file for modifying existing files — only use write_file for new files or complete rewrites.",
|
|
9
|
+
input_schema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
path: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Absolute or relative file path",
|
|
15
|
+
},
|
|
16
|
+
content: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "File content to write",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["path", "content"],
|
|
22
|
+
},
|
|
23
|
+
showOutput: true,
|
|
24
|
+
modifiesFiles: true,
|
|
25
|
+
requiresPermission: true,
|
|
26
|
+
getDisplayInfo: (args) => ({
|
|
27
|
+
kind: "write",
|
|
28
|
+
icon: "✎",
|
|
29
|
+
locations: [{ path: args.path }],
|
|
30
|
+
}),
|
|
31
|
+
formatResult: (_args, result) => {
|
|
32
|
+
if (result.isError)
|
|
33
|
+
return {};
|
|
34
|
+
const m = result.content.match(/\((\+\d+(?:\s-\d+)?)\)/);
|
|
35
|
+
return m ? { summary: m[1] } : {};
|
|
36
|
+
},
|
|
37
|
+
async execute(args, onChunk) {
|
|
38
|
+
const filePath = args.path;
|
|
39
|
+
const content = args.content;
|
|
40
|
+
const absPath = path.resolve(getCwd(), filePath);
|
|
41
|
+
try {
|
|
42
|
+
let oldContent = null;
|
|
43
|
+
try {
|
|
44
|
+
oldContent = await fs.readFile(absPath, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// New file
|
|
48
|
+
}
|
|
49
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
50
|
+
await fs.writeFile(absPath, content);
|
|
51
|
+
// Compute and stream diff for display
|
|
52
|
+
const diff = computeDiff(oldContent, content);
|
|
53
|
+
if (onChunk && diff.hunks.length > 0) {
|
|
54
|
+
for (const hunk of diff.hunks) {
|
|
55
|
+
for (const line of hunk.lines) {
|
|
56
|
+
if (line.type === "added")
|
|
57
|
+
onChunk(`+${line.text}\n`);
|
|
58
|
+
else if (line.type === "removed")
|
|
59
|
+
onChunk(`-${line.text}\n`);
|
|
60
|
+
else
|
|
61
|
+
onChunk(` ${line.text}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const stats = diff.isNewFile
|
|
66
|
+
? `+${diff.added}`
|
|
67
|
+
: `+${diff.added} -${diff.removed}`;
|
|
68
|
+
return {
|
|
69
|
+
content: oldContent === null
|
|
70
|
+
? `Created ${absPath} (${stats})`
|
|
71
|
+
: `Wrote ${absPath} (${stats})`,
|
|
72
|
+
exitCode: 0,
|
|
73
|
+
isError: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
return { content: `Error: ${msg}`, exitCode: 1, isError: true };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
/** Structured result display — returned by formatResult or computed by defaults. */
|
|
25
|
+
export interface ToolResultDisplay {
|
|
26
|
+
/** One-line summary shown next to ✓/✗ (e.g. "42 papers found", "+3/-1"). */
|
|
27
|
+
summary?: string;
|
|
28
|
+
/** Structured content to render below the status line. */
|
|
29
|
+
body?: ToolResultBody;
|
|
30
|
+
}
|
|
31
|
+
export type ToolResultBody = {
|
|
32
|
+
kind: "diff";
|
|
33
|
+
diff: unknown;
|
|
34
|
+
filePath: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "lines";
|
|
37
|
+
lines: string[];
|
|
38
|
+
maxLines?: number;
|
|
39
|
+
};
|
|
40
|
+
export interface ToolDisplayInfo {
|
|
41
|
+
kind: "read" | "write" | "execute" | "search" | "display";
|
|
42
|
+
locations?: {
|
|
43
|
+
path: string;
|
|
44
|
+
line?: number | null;
|
|
45
|
+
}[];
|
|
46
|
+
/** Custom icon character for TUI display (e.g., "◆", "⌕"). When set, the TUI shows
|
|
47
|
+
* icon + detail only. When absent, the tool name is shown alongside the detail. */
|
|
48
|
+
icon?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ToolDefinition {
|
|
51
|
+
name: string;
|
|
52
|
+
/** Short label for TUI display (e.g. "search" instead of "ads_search"). Defaults to name. */
|
|
53
|
+
displayName?: string;
|
|
54
|
+
description: string;
|
|
55
|
+
input_schema: Record<string, unknown>;
|
|
56
|
+
execute(args: Record<string, unknown>, onChunk?: (chunk: string) => void): Promise<ToolResult>;
|
|
57
|
+
/** Whether to stream tool output to the TUI (default: true). */
|
|
58
|
+
showOutput?: boolean;
|
|
59
|
+
/** Whether this tool may modify files — triggers file watcher (default: false). */
|
|
60
|
+
modifiesFiles?: boolean;
|
|
61
|
+
/** Whether to gate execution via permission:request (default: false). */
|
|
62
|
+
requiresPermission?: boolean;
|
|
63
|
+
/** Derive display metadata (icon kind, file paths) for the TUI. */
|
|
64
|
+
getDisplayInfo?: (args: Record<string, unknown>) => ToolDisplayInfo;
|
|
65
|
+
/**
|
|
66
|
+
* Format a short display string for the TUI when this tool is called.
|
|
67
|
+
* Return a concise summary of the args (e.g. the query, the file path).
|
|
68
|
+
* When absent, the TUI derives the detail from common arg fields (command, path, pattern).
|
|
69
|
+
*/
|
|
70
|
+
formatCall?: (args: Record<string, unknown>) => string;
|
|
71
|
+
/**
|
|
72
|
+
* Format result display for the TUI after execution completes.
|
|
73
|
+
* Return a summary string and/or structured body to render.
|
|
74
|
+
* When absent, defaults are computed based on tool kind.
|
|
75
|
+
* Extensions can further override via bus.onPipe("agent:tool-completed", ...).
|
|
76
|
+
*/
|
|
77
|
+
formatResult?: (args: Record<string, unknown>, result: ToolResult) => ToolResultDisplay;
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/core.d.ts
CHANGED
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core kernel — the minimum viable agent-sh.
|
|
3
3
|
*
|
|
4
|
-
* Wires up EventBus + ContextManager +
|
|
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
|
|
6
|
+
* subscribing to bus events.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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({
|
|
15
|
-
* core.bus.on("agent:response-chunk", ({
|
|
16
|
-
*
|
|
17
|
-
* core.
|
|
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 {
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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): Promise<string>;
|
|
40
|
+
/** Convenience: emit agent:cancel-request. */
|
|
41
|
+
cancel(): void;
|
|
34
42
|
/** Build an ExtensionContext for loading extensions against this core. */
|
|
35
43
|
extensionContext(opts: {
|
|
36
44
|
quit: () => void;
|
|
37
45
|
}): ExtensionContext;
|
|
38
|
-
/** Tear down the agent
|
|
46
|
+
/** Tear down the agent and clean up. */
|
|
39
47
|
kill(): void;
|
|
40
48
|
}
|
|
41
49
|
export declare function createCore(config: AgentShellConfig): AgentShellCore;
|