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.
Files changed (83) hide show
  1. package/README.md +37 -115
  2. package/dist/agent/agent-loop.d.ts +86 -0
  3. package/dist/agent/agent-loop.js +704 -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 +119 -0
  12. package/dist/agent/system-prompt.d.ts +14 -0
  13. package/dist/agent/system-prompt.js +103 -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 +71 -0
  18. package/dist/agent/tools/display.d.ts +13 -0
  19. package/dist/agent/tools/display.js +70 -0
  20. package/dist/agent/tools/edit-file.d.ts +2 -0
  21. package/dist/agent/tools/edit-file.js +148 -0
  22. package/dist/agent/tools/glob.d.ts +2 -0
  23. package/dist/agent/tools/glob.js +87 -0
  24. package/dist/agent/tools/grep.d.ts +2 -0
  25. package/dist/agent/tools/grep.js +168 -0
  26. package/dist/agent/tools/list-skills.d.ts +2 -0
  27. package/dist/agent/tools/list-skills.js +28 -0
  28. package/dist/agent/tools/ls.d.ts +2 -0
  29. package/dist/agent/tools/ls.js +72 -0
  30. package/dist/agent/tools/read-file.d.ts +10 -0
  31. package/dist/agent/tools/read-file.js +101 -0
  32. package/dist/agent/tools/user-shell.d.ts +13 -0
  33. package/dist/agent/tools/user-shell.js +84 -0
  34. package/dist/agent/tools/write-file.d.ts +2 -0
  35. package/dist/agent/tools/write-file.js +82 -0
  36. package/dist/agent/types.d.ts +78 -0
  37. package/dist/agent/types.js +1 -0
  38. package/dist/core.d.ts +22 -14
  39. package/dist/core.js +256 -36
  40. package/dist/event-bus.d.ts +98 -17
  41. package/dist/event-bus.js +10 -1
  42. package/dist/extension-loader.d.ts +1 -1
  43. package/dist/extension-loader.js +10 -1
  44. package/dist/extensions/command-suggest.d.ts +10 -0
  45. package/dist/extensions/command-suggest.js +41 -0
  46. package/dist/extensions/slash-commands.d.ts +1 -1
  47. package/dist/extensions/slash-commands.js +161 -64
  48. package/dist/extensions/tui-renderer.js +426 -126
  49. package/dist/index.js +110 -129
  50. package/dist/input-handler.js +78 -9
  51. package/dist/output-parser.d.ts +7 -0
  52. package/dist/output-parser.js +27 -0
  53. package/dist/settings.d.ts +53 -2
  54. package/dist/settings.js +46 -3
  55. package/dist/shell.js +35 -28
  56. package/dist/types.d.ts +33 -6
  57. package/dist/utils/box-frame.d.ts +3 -1
  58. package/dist/utils/box-frame.js +12 -5
  59. package/dist/utils/diff.js +10 -0
  60. package/dist/utils/llm-client.d.ts +45 -0
  61. package/dist/utils/llm-client.js +60 -0
  62. package/dist/utils/markdown.d.ts +1 -0
  63. package/dist/utils/markdown.js +25 -3
  64. package/dist/utils/stream-transform.js +20 -47
  65. package/dist/utils/tool-display.d.ts +4 -0
  66. package/dist/utils/tool-display.js +35 -8
  67. package/examples/extensions/claude-code-bridge/README.md +35 -0
  68. package/examples/extensions/claude-code-bridge/index.ts +194 -0
  69. package/examples/extensions/claude-code-bridge/package.json +11 -0
  70. package/examples/extensions/openrouter.ts +87 -0
  71. package/examples/extensions/pi-bridge/README.md +35 -0
  72. package/examples/extensions/pi-bridge/index.ts +263 -0
  73. package/examples/extensions/pi-bridge/package.json +13 -0
  74. package/examples/extensions/secret-guard.ts +100 -0
  75. package/examples/extensions/subagents.ts +87 -0
  76. package/package.json +3 -5
  77. package/dist/acp-client.d.ts +0 -105
  78. package/dist/acp-client.js +0 -684
  79. package/dist/extensions/shell-exec.d.ts +0 -24
  80. package/dist/extensions/shell-exec.js +0 -188
  81. package/dist/mcp-server.d.ts +0 -13
  82. package/dist/mcp-server.js +0 -234
  83. package/examples/pi-agent-sh.ts +0 -166
@@ -0,0 +1,27 @@
1
+ import type { ChatCompletionMessageParam } from "../utils/llm-client.js";
2
+ /**
3
+ * Manages the OpenAI chat messages array for the agent loop.
4
+ * Separate from ContextManager — this is the LLM conversation,
5
+ * not the shell history.
6
+ */
7
+ export declare class ConversationState {
8
+ private messages;
9
+ addUserMessage(text: string): void;
10
+ addAssistantMessage(content: string | null, toolCalls?: {
11
+ id: string;
12
+ function: {
13
+ name: string;
14
+ arguments: string;
15
+ };
16
+ }[]): void;
17
+ addToolResult(toolCallId: string, content: string): void;
18
+ /** Inject a system-level note into the conversation (e.g. context change). */
19
+ addSystemNote(text: string): void;
20
+ getMessages(): ChatCompletionMessageParam[];
21
+ /**
22
+ * Simple compaction — drop oldest turns, keeping the first user message
23
+ * (original task context) and the most recent turns.
24
+ */
25
+ compact(maxTurns: number): void;
26
+ clear(): void;
27
+ }
@@ -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,119 @@
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
+ const resultDisplay = tool.formatResult?.(args, result);
66
+ bus.emitTransform("agent:tool-completed", {
67
+ toolCallId: tc.id,
68
+ exitCode: result.exitCode,
69
+ rawOutput: result.content,
70
+ kind: display.kind,
71
+ resultDisplay,
72
+ });
73
+ }
74
+ const content = result.isError ? `Error: ${result.content}` : result.content;
75
+ conversation.addToolResult(tc.id, content);
76
+ }
77
+ }
78
+ return fullResponseText;
79
+ }
80
+ /** Stream a single LLM response. */
81
+ async function streamOnce(llmClient, systemPrompt, conversation, apiTools, model, signal) {
82
+ let text = "";
83
+ const pendingToolCalls = [];
84
+ const stream = await llmClient.stream({
85
+ messages: [
86
+ { role: "system", content: systemPrompt },
87
+ ...conversation.getMessages(),
88
+ ],
89
+ tools: apiTools.length > 0 ? apiTools : undefined,
90
+ model,
91
+ signal,
92
+ });
93
+ for await (const chunk of stream) {
94
+ if (signal?.aborted)
95
+ break;
96
+ const choice = chunk.choices[0];
97
+ if (!choice)
98
+ continue;
99
+ const delta = choice.delta;
100
+ if (delta?.content) {
101
+ text += delta.content;
102
+ }
103
+ if (delta?.tool_calls) {
104
+ for (const tc of delta.tool_calls) {
105
+ const idx = tc.index;
106
+ if (!pendingToolCalls[idx]) {
107
+ pendingToolCalls[idx] = { id: tc.id, name: tc.function.name, argumentsJson: "" };
108
+ }
109
+ if (tc.function?.arguments) {
110
+ pendingToolCalls[idx].argumentsJson += tc.function.arguments;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ const assistantToolCalls = pendingToolCalls.length
116
+ ? pendingToolCalls.map(tc => ({ id: tc.id, function: { name: tc.name, arguments: tc.argumentsJson } }))
117
+ : undefined;
118
+ return { text, toolCalls: pendingToolCalls, assistantContent: text || null, assistantToolCalls };
119
+ }
@@ -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# Tool Decision Guide\n\nYou have three categories of tools \u2014 choose based on who needs the output and\nwhether the command has lasting effects:\n\n**Scratchpad tools** (bash, read_file, grep, glob, ls, edit_file, write_file):\nUse these to investigate, search, read, and modify files. Output is returned\nto you for reasoning \u2014 the user doesn't see it directly.\n\n**Display** (display):\nUse this to show output to the user in their terminal. The user sees the\noutput directly, but it is NOT returned to you. Use when:\n- The user asks to see something (cat a file, git log, git diff, man page)\n- The output is for the user to read, not for you to process\n\n**Live shell** (user_shell):\nUse this to run commands with lasting effects in the user's real shell. Use for:\n- Commands that affect shell state (cd, export, source)\n- Installing packages, starting servers, running builds\n- Any command where the user wants real side effects\n- Set return_output=true only if you need to inspect the result\n\nDefault to scratchpad tools for your own investigation. Use display when the\nuser is the intended audience. Use user_shell when the command has real effects.\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";
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,103 @@
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
+ # Tool Decision Guide
44
+
45
+ You have three categories of tools — choose based on who needs the output and
46
+ whether the command has lasting effects:
47
+
48
+ **Scratchpad tools** (bash, read_file, grep, glob, ls, edit_file, write_file):
49
+ Use these to investigate, search, read, and modify files. Output is returned
50
+ to you for reasoning — the user doesn't see it directly.
51
+
52
+ **Display** (display):
53
+ Use this to show output to the user in their terminal. The user sees the
54
+ output directly, but it is NOT returned to you. Use when:
55
+ - The user asks to see something (cat a file, git log, git diff, man page)
56
+ - The output is for the user to read, not for you to process
57
+
58
+ **Live shell** (user_shell):
59
+ Use this to run commands with lasting effects in the user's real shell. Use for:
60
+ - Commands that affect shell state (cd, export, source)
61
+ - Installing packages, starting servers, running builds
62
+ - Any command where the user wants real side effects
63
+ - Set return_output=true only if you need to inspect the result
64
+
65
+ Default to scratchpad tools for your own investigation. Use display when the
66
+ user is the intended audience. Use user_shell when the command has real effects.
67
+
68
+ # Tool Usage Guidelines
69
+ - Use read_file before editing a file you haven't seen
70
+ - Prefer edit_file over write_file for modifying existing files
71
+ - Use grep/glob to find files before reading them
72
+ - Keep bash commands focused; avoid long-running blocking commands
73
+ - Always check command exit codes for errors`;
74
+ /**
75
+ * Build the dynamic context — injected as a user message before each query.
76
+ * Contains everything that changes: tools, shell context, conventions, cwd.
77
+ *
78
+ * Runs through the "agent:dynamic-context" pipe so extensions can append.
79
+ */
80
+ export function buildDynamicContext(tools, contextManager) {
81
+ const sections = [];
82
+ // Tools
83
+ sections.push("# Available Tools\n" +
84
+ tools.map((t) => `- ${t.name}: ${t.description}`).join("\n"));
85
+ // Project conventions (CLAUDE.md / AGENT.md)
86
+ const conventions = loadConventionFiles(contextManager.getCwd());
87
+ if (conventions.length > 0) {
88
+ sections.push("# Project Conventions\n\n" + conventions.join("\n\n"));
89
+ }
90
+ // Skills hint
91
+ const skills = discoverSkills(contextManager.getCwd());
92
+ if (skills.length > 0) {
93
+ sections.push(`You have access to ${skills.length} skill(s). Use the list_skills tool to see them, then read_file to load one.`);
94
+ }
95
+ // Shell context
96
+ const shellContext = contextManager.getContext();
97
+ if (shellContext) {
98
+ sections.push(shellContext);
99
+ }
100
+ // Metadata
101
+ sections.push(`Current date: ${new Date().toISOString().split("T")[0]}\nWorking directory: ${contextManager.getCwd()}`);
102
+ return sections.join("\n\n");
103
+ }
@@ -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;