agent-sh 0.3.1 → 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.
- package/README.md +66 -96
- package/dist/agent/agent-loop.d.ts +85 -0
- package/dist/agent/agent-loop.js +611 -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 +117 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +98 -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 +62 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +95 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +55 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +77 -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 +43 -0
- package/dist/agent/tools/read-file.d.ts +2 -0
- package/dist/agent/tools/read-file.js +55 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +57 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +74 -0
- package/dist/agent/types.d.ts +44 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +24 -14
- package/dist/core.js +260 -36
- package/dist/event-bus.d.ts +84 -14
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.js +12 -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 +111 -53
- package/dist/index.js +124 -120
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +152 -45
- 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 +45 -2
- package/dist/shell.js +36 -27
- package/dist/types.d.ts +46 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/line-editor.js +4 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.js +2 -2
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.js +15 -5
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -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 +265 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -100
- package/dist/acp-client.js +0 -656
- 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,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,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,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,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,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,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,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 +
|
|
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, 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
|
|
48
|
+
/** Tear down the agent and clean up. */
|
|
39
49
|
kill(): void;
|
|
40
50
|
}
|
|
41
51
|
export declare function createCore(config: AgentShellConfig): AgentShellCore;
|