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,59 @@
1
+ /**
2
+ * Manages the OpenAI chat messages array for the agent loop.
3
+ * Separate from ContextManager — this is the LLM conversation,
4
+ * not the shell history.
5
+ */
6
+ export class ConversationState {
7
+ messages = [];
8
+ addUserMessage(text) {
9
+ this.messages.push({ role: "user", content: text });
10
+ }
11
+ addAssistantMessage(content, toolCalls) {
12
+ if (toolCalls?.length) {
13
+ this.messages.push({
14
+ role: "assistant",
15
+ content: content ?? null,
16
+ tool_calls: toolCalls.map((tc) => ({
17
+ id: tc.id,
18
+ type: "function",
19
+ function: tc.function,
20
+ })),
21
+ });
22
+ }
23
+ else {
24
+ this.messages.push({ role: "assistant", content: content ?? "" });
25
+ }
26
+ }
27
+ addToolResult(toolCallId, content) {
28
+ this.messages.push({
29
+ role: "tool",
30
+ tool_call_id: toolCallId,
31
+ content,
32
+ });
33
+ }
34
+ /** Inject a system-level note into the conversation (e.g. context change). */
35
+ addSystemNote(text) {
36
+ this.messages.push({ role: "user", content: text });
37
+ }
38
+ getMessages() {
39
+ return this.messages;
40
+ }
41
+ /**
42
+ * Simple compaction — drop oldest turns, keeping the first user message
43
+ * (original task context) and the most recent turns.
44
+ */
45
+ compact(maxTurns) {
46
+ if (this.messages.length <= maxTurns * 2)
47
+ return;
48
+ const first = this.messages[0];
49
+ const recent = this.messages.slice(-(maxTurns * 2));
50
+ this.messages = [
51
+ first,
52
+ { role: "user", content: "[Earlier conversation turns omitted for context space]" },
53
+ ...recent,
54
+ ];
55
+ }
56
+ clear() {
57
+ this.messages = [];
58
+ }
59
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Agent backend exports.
3
+ *
4
+ * The default backend is AgentLoop (in-process, OpenAI-compatible API).
5
+ * Extensions can register alternative backends via agent:register-backend.
6
+ */
7
+ export type { AgentBackend } from "./types.js";
8
+ export type { ToolDefinition, ToolResult, ToolDisplayInfo } from "./types.js";
9
+ export { AgentLoop } from "./agent-loop.js";
10
+ export { ToolRegistry } from "./tool-registry.js";
11
+ export { runSubagent, type SubagentOptions } from "./subagent.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Agent backend exports.
3
+ *
4
+ * The default backend is AgentLoop (in-process, OpenAI-compatible API).
5
+ * Extensions can register alternative backends via agent:register-backend.
6
+ */
7
+ export { AgentLoop } from "./agent-loop.js";
8
+ export { ToolRegistry } from "./tool-registry.js";
9
+ export { runSubagent } from "./subagent.js";
@@ -0,0 +1,25 @@
1
+ export interface Skill {
2
+ name: string;
3
+ description: string;
4
+ filePath: string;
5
+ baseDir: string;
6
+ }
7
+ /**
8
+ * Discover global skills (stable across cwd changes).
9
+ * Default: ~/.agents/skills/, plus any skillPaths from settings.
10
+ */
11
+ export declare function discoverGlobalSkills(): Skill[];
12
+ /**
13
+ * Discover project-level skills from .agents/skills/ in cwd hierarchy.
14
+ * Scans from cwd up to git root.
15
+ */
16
+ export declare function discoverProjectSkills(cwd: string): Skill[];
17
+ /**
18
+ * Discover all skills (global + project).
19
+ */
20
+ export declare function discoverSkills(cwd: string): Skill[];
21
+ /**
22
+ * Load the full content of a skill (frontmatter stripped).
23
+ * Returns XML-wrapped content suitable for injection into conversation.
24
+ */
25
+ export declare function loadSkillContent(skill: Skill): string | null;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Skill discovery and loading.
3
+ *
4
+ * Follows the Agent Skills standard (agentskills.io):
5
+ * - Skills are directories containing a SKILL.md with YAML frontmatter
6
+ * - Frontmatter must include `name` and `description`
7
+ * - Full content is loaded on-demand (only names/descriptions in system prompt)
8
+ *
9
+ * Discovery locations:
10
+ * Global: ~/.agent-sh/skills/ (default), plus skillPaths from settings
11
+ * Project: .agents/skills/ in cwd and ancestor dirs (up to git root)
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import * as os from "node:os";
16
+ import { getSettings } from "../settings.js";
17
+ /** Parse YAML frontmatter from a SKILL.md file. */
18
+ function parseFrontmatter(content) {
19
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
20
+ if (!match)
21
+ return null;
22
+ const meta = {};
23
+ for (const line of match[1].split("\n")) {
24
+ const colon = line.indexOf(":");
25
+ if (colon > 0) {
26
+ const key = line.slice(0, colon).trim();
27
+ const value = line.slice(colon + 1).trim();
28
+ meta[key] = value;
29
+ }
30
+ }
31
+ return { meta, body: match[2] };
32
+ }
33
+ /** Load a single skill from a SKILL.md file. */
34
+ function loadSkillFromFile(filePath) {
35
+ try {
36
+ const content = fs.readFileSync(filePath, "utf-8");
37
+ const parsed = parseFrontmatter(content);
38
+ if (!parsed)
39
+ return null;
40
+ const name = parsed.meta.name;
41
+ const description = parsed.meta.description;
42
+ if (!name || !description)
43
+ return null;
44
+ if (parsed.meta["disable-model-invocation"] === "true")
45
+ return null;
46
+ return {
47
+ name,
48
+ description,
49
+ filePath,
50
+ baseDir: path.dirname(filePath),
51
+ };
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /** Recursively scan a directory for SKILL.md files. */
58
+ function scanDir(dir) {
59
+ const skills = [];
60
+ let entries;
61
+ try {
62
+ entries = fs.readdirSync(dir, { withFileTypes: true });
63
+ }
64
+ catch {
65
+ return skills;
66
+ }
67
+ // If this directory has a SKILL.md, it's a skill root — don't recurse further
68
+ const skillMd = path.join(dir, "SKILL.md");
69
+ try {
70
+ fs.accessSync(skillMd);
71
+ const skill = loadSkillFromFile(skillMd);
72
+ if (skill)
73
+ skills.push(skill);
74
+ return skills;
75
+ }
76
+ catch {
77
+ // No SKILL.md here — check subdirectories
78
+ }
79
+ for (const entry of entries) {
80
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
81
+ continue;
82
+ const fullPath = path.join(dir, entry.name);
83
+ const isDir = entry.isDirectory() ||
84
+ (entry.isSymbolicLink() && (() => { try {
85
+ return fs.statSync(fullPath).isDirectory();
86
+ }
87
+ catch {
88
+ return false;
89
+ } })());
90
+ if (isDir) {
91
+ skills.push(...scanDir(fullPath));
92
+ }
93
+ }
94
+ return skills;
95
+ }
96
+ /** Find the git root from a directory. */
97
+ function findGitRoot(dir) {
98
+ let current = path.resolve(dir);
99
+ while (true) {
100
+ try {
101
+ fs.accessSync(path.join(current, ".git"));
102
+ return current;
103
+ }
104
+ catch {
105
+ const parent = path.dirname(current);
106
+ if (parent === current)
107
+ return null;
108
+ current = parent;
109
+ }
110
+ }
111
+ }
112
+ /** Expand ~ to home directory. */
113
+ function expandHome(p) {
114
+ if (p.startsWith("~/") || p === "~") {
115
+ return path.join(os.homedir(), p.slice(1));
116
+ }
117
+ return p;
118
+ }
119
+ function addUnique(target, source, seen) {
120
+ for (const skill of source) {
121
+ if (!seen.has(skill.name)) {
122
+ seen.add(skill.name);
123
+ target.push(skill);
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * Discover global skills (stable across cwd changes).
129
+ * Default: ~/.agents/skills/, plus any skillPaths from settings.
130
+ */
131
+ export function discoverGlobalSkills() {
132
+ const seen = new Set();
133
+ const skills = [];
134
+ addUnique(skills, scanDir(path.join(os.homedir(), ".agent-sh", "skills")), seen);
135
+ const settings = getSettings();
136
+ for (const p of settings.skillPaths ?? []) {
137
+ addUnique(skills, scanDir(path.resolve(expandHome(p))), seen);
138
+ }
139
+ return skills;
140
+ }
141
+ /**
142
+ * Discover project-level skills from .agents/skills/ in cwd hierarchy.
143
+ * Scans from cwd up to git root.
144
+ */
145
+ export function discoverProjectSkills(cwd) {
146
+ const seen = new Set();
147
+ const skills = [];
148
+ const gitRoot = findGitRoot(cwd);
149
+ let current = path.resolve(cwd);
150
+ while (true) {
151
+ addUnique(skills, scanDir(path.join(current, ".agents", "skills")), seen);
152
+ if (gitRoot && current === gitRoot)
153
+ break;
154
+ const parent = path.dirname(current);
155
+ if (parent === current)
156
+ break;
157
+ current = parent;
158
+ }
159
+ return skills;
160
+ }
161
+ /**
162
+ * Discover all skills (global + project).
163
+ */
164
+ export function discoverSkills(cwd) {
165
+ const seen = new Set();
166
+ const skills = [];
167
+ addUnique(skills, discoverGlobalSkills(), seen);
168
+ addUnique(skills, discoverProjectSkills(cwd), seen);
169
+ return skills;
170
+ }
171
+ /**
172
+ * Load the full content of a skill (frontmatter stripped).
173
+ * Returns XML-wrapped content suitable for injection into conversation.
174
+ */
175
+ export function loadSkillContent(skill) {
176
+ try {
177
+ const content = fs.readFileSync(skill.filePath, "utf-8");
178
+ const parsed = parseFrontmatter(content);
179
+ if (!parsed)
180
+ return content;
181
+ return `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${parsed.body.trim()}\n</skill>`;
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Subagent runner — executes a focused agent loop with its own context.
3
+ *
4
+ * Unlike the main AgentLoop, a subagent:
5
+ * - Has its own conversation (starts fresh, stays focused)
6
+ * - Has its own system prompt (specialized for the task)
7
+ * - Runs to completion and returns the final text
8
+ * - Optionally emits tool events to the bus for TUI rendering
9
+ *
10
+ * Used by the subagent extension to delegate tasks from the main agent.
11
+ */
12
+ import type { EventBus } from "../event-bus.js";
13
+ import type { LlmClient } from "../utils/llm-client.js";
14
+ import type { ToolDefinition } from "./types.js";
15
+ export interface SubagentOptions {
16
+ /** LLM client to use. */
17
+ llmClient: LlmClient;
18
+ /** Tools available to the subagent. */
19
+ tools: ToolDefinition[];
20
+ /** System prompt for this subagent. */
21
+ systemPrompt: string;
22
+ /** The task to perform. */
23
+ task: string;
24
+ /** Model override (optional, defaults to llmClient's model). */
25
+ model?: string;
26
+ /** Event bus for TUI events (optional — silent if omitted). */
27
+ bus?: EventBus;
28
+ /** Abort signal for cancellation. */
29
+ signal?: AbortSignal;
30
+ /** Max tool loop iterations (default 20). */
31
+ maxIterations?: number;
32
+ }
33
+ /**
34
+ * Run a subagent to completion.
35
+ * Returns the final response text.
36
+ */
37
+ export declare function runSubagent(opts: SubagentOptions): Promise<string>;
@@ -0,0 +1,117 @@
1
+ import { ConversationState } from "./conversation-state.js";
2
+ /**
3
+ * Run a subagent to completion.
4
+ * Returns the final response text.
5
+ */
6
+ export async function runSubagent(opts) {
7
+ const { llmClient, tools, systemPrompt, task, model, bus, signal, maxIterations = 20, } = opts;
8
+ const toolMap = new Map(tools.map(t => [t.name, t]));
9
+ const apiTools = tools.map(t => ({
10
+ type: "function",
11
+ function: {
12
+ name: t.name,
13
+ description: t.description,
14
+ parameters: t.input_schema,
15
+ },
16
+ }));
17
+ const conversation = new ConversationState();
18
+ conversation.addUserMessage(task);
19
+ let fullResponseText = "";
20
+ let iterations = 0;
21
+ while (iterations++ < maxIterations) {
22
+ if (signal?.aborted)
23
+ break;
24
+ // Stream LLM response
25
+ const { text, toolCalls, assistantContent, assistantToolCalls } = await streamOnce(llmClient, systemPrompt, conversation, apiTools, model, signal);
26
+ fullResponseText += text;
27
+ conversation.addAssistantMessage(assistantContent, assistantToolCalls);
28
+ // No tool calls → done
29
+ if (toolCalls.length === 0)
30
+ break;
31
+ // Execute tools
32
+ for (const tc of toolCalls) {
33
+ if (signal?.aborted)
34
+ break;
35
+ const tool = toolMap.get(tc.name);
36
+ if (!tool) {
37
+ conversation.addToolResult(tc.id, `Error: Unknown tool "${tc.name}"`);
38
+ continue;
39
+ }
40
+ let args;
41
+ try {
42
+ args = JSON.parse(tc.argumentsJson);
43
+ }
44
+ catch {
45
+ conversation.addToolResult(tc.id, `Error: Invalid JSON arguments for ${tc.name}`);
46
+ continue;
47
+ }
48
+ // Emit tool events for TUI (if bus provided)
49
+ if (bus) {
50
+ const display = tool.getDisplayInfo?.(args) ?? { kind: "execute" };
51
+ bus.emit("agent:tool-started", {
52
+ title: tc.name,
53
+ toolCallId: tc.id,
54
+ kind: display.kind,
55
+ locations: display.locations,
56
+ rawInput: args,
57
+ });
58
+ }
59
+ const onChunk = bus && tool.showOutput !== false
60
+ ? (chunk) => { bus.emit("agent:tool-output-chunk", { chunk }); }
61
+ : undefined;
62
+ const result = await tool.execute(args, onChunk);
63
+ if (bus) {
64
+ const display = tool.getDisplayInfo?.(args) ?? { kind: "execute" };
65
+ bus.emit("agent:tool-completed", {
66
+ toolCallId: tc.id,
67
+ exitCode: result.exitCode,
68
+ rawOutput: result.content,
69
+ kind: display.kind,
70
+ });
71
+ }
72
+ const content = result.isError ? `Error: ${result.content}` : result.content;
73
+ conversation.addToolResult(tc.id, content);
74
+ }
75
+ }
76
+ return fullResponseText;
77
+ }
78
+ /** Stream a single LLM response. */
79
+ async function streamOnce(llmClient, systemPrompt, conversation, apiTools, model, signal) {
80
+ let text = "";
81
+ const pendingToolCalls = [];
82
+ const stream = await llmClient.stream({
83
+ messages: [
84
+ { role: "system", content: systemPrompt },
85
+ ...conversation.getMessages(),
86
+ ],
87
+ tools: apiTools.length > 0 ? apiTools : undefined,
88
+ model,
89
+ signal,
90
+ });
91
+ for await (const chunk of stream) {
92
+ if (signal?.aborted)
93
+ break;
94
+ const choice = chunk.choices[0];
95
+ if (!choice)
96
+ continue;
97
+ const delta = choice.delta;
98
+ if (delta?.content) {
99
+ text += delta.content;
100
+ }
101
+ if (delta?.tool_calls) {
102
+ for (const tc of delta.tool_calls) {
103
+ const idx = tc.index;
104
+ if (!pendingToolCalls[idx]) {
105
+ pendingToolCalls[idx] = { id: tc.id, name: tc.function.name, argumentsJson: "" };
106
+ }
107
+ if (tc.function?.arguments) {
108
+ pendingToolCalls[idx].argumentsJson += tc.function.arguments;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ const assistantToolCalls = pendingToolCalls.length
114
+ ? pendingToolCalls.map(tc => ({ id: tc.id, function: { name: tc.name, arguments: tc.argumentsJson } }))
115
+ : undefined;
116
+ return { text, toolCalls: pendingToolCalls, assistantContent: text || null, assistantToolCalls };
117
+ }
@@ -0,0 +1,14 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ import type { ContextManager } from "../context-manager.js";
3
+ /**
4
+ * Static system prompt — identical across all queries, cacheable.
5
+ * Contains only identity and behavioral instructions.
6
+ */
7
+ export declare const STATIC_SYSTEM_PROMPT = "You are an AI coding assistant embedded in agent-sh, a terminal shell.\nYou have access to the user's shell environment and can read, write, and execute code.\nYou share the user's working directory, environment variables, and shell history.\n\n# Input Modes\n\nThe user interacts with you through two modes:\n\nEXECUTE mode (triggered by '>'): The user is asking questions or requesting tasks.\nUse your internal tools (bash, file operations, etc.) to accomplish tasks.\nDo NOT use user_shell in this mode unless the user explicitly asks to run\nsomething in their live shell.\n\nHELP mode (triggered by '?'): The user wants a command run in their live shell.\nYou may use your tools to investigate first (read files, grep, etc.), but the\nfinal action must be running the command via user_shell with return_output=false.\nThe user sees the output directly \u2014 you don't need to see or summarize it.\nDo not explain, confirm, or comment on the result \u2014 just run it and stop.\n\nEach prompt includes a per-query mode instruction \u2014 follow it.\n\n# Tool Usage Guidelines\n- Use read_file before editing a file you haven't seen\n- Prefer edit_file over write_file for modifying existing files\n- Use grep/glob to find files before reading them\n- Keep bash commands focused; avoid long-running blocking commands\n- Always check command exit codes for errors\n- user_shell runs commands in the user's live terminal \u2014 use for cd, export, source, etc.\n- user_shell output is shown directly to the user but NOT returned to you by default.\n Set return_output=true if you need to inspect the result to answer a question.";
8
+ /**
9
+ * Build the dynamic context — injected as a user message before each query.
10
+ * Contains everything that changes: tools, shell context, conventions, cwd.
11
+ *
12
+ * Runs through the "agent:dynamic-context" pipe so extensions can append.
13
+ */
14
+ export declare function buildDynamicContext(tools: ToolDefinition[], contextManager: ContextManager): string;
@@ -0,0 +1,98 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { discoverSkills } from "./skills.js";
4
+ /** File names to scan for project conventions (checked in order). */
5
+ const CONVENTION_FILES = ["CLAUDE.md", "AGENT.md"];
6
+ /**
7
+ * Scan from `dir` upward for project convention files.
8
+ * Returns contents ordered root-first (general → specific).
9
+ */
10
+ function loadConventionFiles(dir) {
11
+ const files = [];
12
+ let current = path.resolve(dir);
13
+ while (true) {
14
+ for (const name of CONVENTION_FILES) {
15
+ const candidate = path.join(current, name);
16
+ try {
17
+ const content = fs.readFileSync(candidate, "utf-8").trim();
18
+ if (content) {
19
+ files.push({ path: candidate, content });
20
+ break;
21
+ }
22
+ }
23
+ catch {
24
+ // File doesn't exist
25
+ }
26
+ }
27
+ const parent = path.dirname(current);
28
+ if (parent === current)
29
+ break;
30
+ current = parent;
31
+ }
32
+ files.reverse();
33
+ return files.map(f => `<!-- ${f.path} -->\n${f.content}`);
34
+ }
35
+ /**
36
+ * Static system prompt — identical across all queries, cacheable.
37
+ * Contains only identity and behavioral instructions.
38
+ */
39
+ export const STATIC_SYSTEM_PROMPT = `You are an AI coding assistant embedded in agent-sh, a terminal shell.
40
+ You have access to the user's shell environment and can read, write, and execute code.
41
+ You share the user's working directory, environment variables, and shell history.
42
+
43
+ # Input Modes
44
+
45
+ The user interacts with you through two modes:
46
+
47
+ EXECUTE mode (triggered by '>'): The user is asking questions or requesting tasks.
48
+ Use your internal tools (bash, file operations, etc.) to accomplish tasks.
49
+ Do NOT use user_shell in this mode unless the user explicitly asks to run
50
+ something in their live shell.
51
+
52
+ HELP mode (triggered by '?'): The user wants a command run in their live shell.
53
+ You may use your tools to investigate first (read files, grep, etc.), but the
54
+ final action must be running the command via user_shell with return_output=false.
55
+ The user sees the output directly — you don't need to see or summarize it.
56
+ Do not explain, confirm, or comment on the result — just run it and stop.
57
+
58
+ Each prompt includes a per-query mode instruction — follow it.
59
+
60
+ # Tool Usage Guidelines
61
+ - Use read_file before editing a file you haven't seen
62
+ - Prefer edit_file over write_file for modifying existing files
63
+ - Use grep/glob to find files before reading them
64
+ - Keep bash commands focused; avoid long-running blocking commands
65
+ - Always check command exit codes for errors
66
+ - user_shell runs commands in the user's live terminal — use for cd, export, source, etc.
67
+ - user_shell output is shown directly to the user but NOT returned to you by default.
68
+ Set return_output=true if you need to inspect the result to answer a question.`;
69
+ /**
70
+ * Build the dynamic context — injected as a user message before each query.
71
+ * Contains everything that changes: tools, shell context, conventions, cwd.
72
+ *
73
+ * Runs through the "agent:dynamic-context" pipe so extensions can append.
74
+ */
75
+ export function buildDynamicContext(tools, contextManager) {
76
+ const sections = [];
77
+ // Tools
78
+ sections.push("# Available Tools\n" +
79
+ tools.map((t) => `- ${t.name}: ${t.description}`).join("\n"));
80
+ // Project conventions (CLAUDE.md / AGENT.md)
81
+ const conventions = loadConventionFiles(contextManager.getCwd());
82
+ if (conventions.length > 0) {
83
+ sections.push("# Project Conventions\n\n" + conventions.join("\n\n"));
84
+ }
85
+ // Skills hint
86
+ const skills = discoverSkills(contextManager.getCwd());
87
+ if (skills.length > 0) {
88
+ sections.push(`You have access to ${skills.length} skill(s). Use the list_skills tool to see them, then read_file to load one.`);
89
+ }
90
+ // Shell context
91
+ const shellContext = contextManager.getContext();
92
+ if (shellContext) {
93
+ sections.push(shellContext);
94
+ }
95
+ // Metadata
96
+ sections.push(`Current date: ${new Date().toISOString().split("T")[0]}\nWorking directory: ${contextManager.getCwd()}`);
97
+ return sections.join("\n\n");
98
+ }
@@ -0,0 +1,15 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ import type { ChatCompletionTool } from "../utils/llm-client.js";
3
+ /**
4
+ * Registry for agent tools. Holds tool definitions and converts them
5
+ * to OpenAI-compatible function schemas for API calls.
6
+ */
7
+ export declare class ToolRegistry {
8
+ private tools;
9
+ register(tool: ToolDefinition): void;
10
+ unregister(name: string): void;
11
+ get(name: string): ToolDefinition | undefined;
12
+ all(): ToolDefinition[];
13
+ /** Convert to OpenAI-compatible tool schemas for API calls. */
14
+ toAPITools(): ChatCompletionTool[];
15
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Registry for agent tools. Holds tool definitions and converts them
3
+ * to OpenAI-compatible function schemas for API calls.
4
+ */
5
+ export class ToolRegistry {
6
+ tools = new Map();
7
+ register(tool) {
8
+ this.tools.set(tool.name, tool);
9
+ }
10
+ unregister(name) {
11
+ this.tools.delete(name);
12
+ }
13
+ get(name) {
14
+ return this.tools.get(name);
15
+ }
16
+ all() {
17
+ return Array.from(this.tools.values());
18
+ }
19
+ /** Convert to OpenAI-compatible tool schemas for API calls. */
20
+ toAPITools() {
21
+ return this.all().map((t) => ({
22
+ type: "function",
23
+ function: {
24
+ name: t.name,
25
+ description: t.description,
26
+ parameters: t.input_schema,
27
+ },
28
+ }));
29
+ }
30
+ }
@@ -0,0 +1,7 @@
1
+ import type { EventBus } from "../../event-bus.js";
2
+ import type { ToolDefinition } from "../types.js";
3
+ export declare function createBashTool(opts: {
4
+ getCwd: () => string;
5
+ getEnv: () => Record<string, string>;
6
+ bus: EventBus;
7
+ }): ToolDefinition;
@@ -0,0 +1,62 @@
1
+ import { executeCommand } from "../../executor.js";
2
+ export function createBashTool(opts) {
3
+ return {
4
+ name: "bash",
5
+ description: "Execute a bash command in an isolated subprocess. Output is captured and returned to you. Does not affect the user's shell state.",
6
+ input_schema: {
7
+ type: "object",
8
+ properties: {
9
+ command: {
10
+ type: "string",
11
+ description: "The bash command to execute",
12
+ },
13
+ timeout: {
14
+ type: "number",
15
+ description: "Timeout in seconds (default: 60)",
16
+ },
17
+ },
18
+ required: ["command"],
19
+ },
20
+ showOutput: true,
21
+ modifiesFiles: true,
22
+ requiresPermission: true,
23
+ getDisplayInfo: (args) => ({
24
+ kind: "execute",
25
+ locations: [],
26
+ }),
27
+ async execute(args, onChunk) {
28
+ const command = args.command;
29
+ const timeout = (args.timeout ?? 60) * 1000;
30
+ // Let extensions intercept before execution
31
+ const intercepted = opts.bus.emitPipe("agent:terminal-intercept", {
32
+ command,
33
+ cwd: opts.getCwd(),
34
+ intercepted: false,
35
+ output: "",
36
+ });
37
+ if (intercepted.intercepted) {
38
+ return {
39
+ content: intercepted.output,
40
+ exitCode: 0,
41
+ isError: false,
42
+ };
43
+ }
44
+ const { session, done } = executeCommand({
45
+ command,
46
+ cwd: opts.getCwd(),
47
+ env: opts.getEnv(),
48
+ timeout,
49
+ onOutput: onChunk,
50
+ });
51
+ await done;
52
+ const content = session.truncated
53
+ ? `[output truncated, showing last portion]\n${session.output}`
54
+ : session.output;
55
+ return {
56
+ content: content || "(no output)",
57
+ exitCode: session.exitCode,
58
+ isError: session.exitCode !== 0,
59
+ };
60
+ },
61
+ };
62
+ }