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
@@ -2,90 +2,187 @@
2
2
  * Slash commands extension.
3
3
  *
4
4
  * Registers built-in slash commands on the event bus:
5
+ * - Listens for "command:register" to accept commands from extensions
5
6
  * - Responds to "autocomplete:request" pipe for /-prefixed completions
6
7
  * - Handles "command:execute" events and dispatches to matching handler
7
8
  * - Uses "ui:info"/"ui:error" for user feedback (no direct TUI dependency)
9
+ *
10
+ * Argument completion is composable: any extension can onPipe("autocomplete:request")
11
+ * and check payload.command / payload.commandArgs to add completions for any command.
8
12
  */
9
- import { execSync } from "node:child_process";
10
13
  import { palette as p } from "../utils/palette.js";
11
- export default function activate({ bus, getAcpClient, quit }) {
12
- const commands = [
13
- {
14
- name: "/help",
15
- description: "Show available commands",
16
- handler: () => {
17
- const lines = commands.map((c) => ` ${p.accent}${c.name.padEnd(12)}${p.reset} ${c.description}`);
18
- bus.emit("ui:info", { message: "Available commands:\n" + lines.join("\n") });
19
- },
20
- },
21
- {
22
- name: "/clear",
23
- description: "Start a new agent session",
24
- handler: async () => {
25
- try {
26
- await getAcpClient().resetSession();
27
- bus.emit("ui:info", { message: "Session cleared." });
28
- }
29
- catch (err) {
30
- bus.emit("ui:error", {
31
- message: `Failed to reset session: ${err instanceof Error ? err.message : String(err)}`,
32
- });
33
- }
34
- },
14
+ import { discoverSkills, loadSkillContent } from "../agent/skills.js";
15
+ export default function activate({ bus, contextManager }) {
16
+ const commands = new Map();
17
+ const register = (cmd) => {
18
+ const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
19
+ commands.set(name, { ...cmd, name });
20
+ };
21
+ // ── Built-in commands ─────────────────────────────────────────
22
+ register({
23
+ name: "/help",
24
+ description: "Show available commands",
25
+ handler: () => {
26
+ const maxLen = Math.max(...[...commands.values()].map(c => c.name.length));
27
+ const pad = maxLen + 2;
28
+ const lines = [...commands.values()].map((c) => ` ${p.accent}${c.name.padEnd(pad)}${p.reset} ${c.description}`);
29
+ bus.emit("ui:info", { message: "Available commands:\n" + lines.join("\n") });
35
30
  },
36
- {
37
- name: "/copy",
38
- description: "Copy last agent response to clipboard",
39
- handler: () => {
40
- const text = getAcpClient().getLastResponseText();
41
- if (!text) {
42
- bus.emit("ui:info", { message: "No agent response to copy." });
43
- return;
44
- }
45
- try {
46
- if (process.platform === "darwin") {
47
- execSync("pbcopy", { input: text });
48
- }
49
- else {
50
- execSync("xclip -selection clipboard", { input: text });
51
- }
52
- bus.emit("ui:info", { message: "Copied to clipboard." });
53
- }
54
- catch {
55
- bus.emit("ui:error", { message: "Failed to copy to clipboard." });
56
- }
57
- },
31
+ });
32
+ register({
33
+ name: "/model",
34
+ description: "Cycle to next model, or switch to a specific one",
35
+ handler: (args) => {
36
+ const name = args.trim();
37
+ if (!name) {
38
+ const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
39
+ const current = models.find((m) => m.model === active);
40
+ const label = current
41
+ ? `${current.model}${current.provider ? ` [${current.provider}]` : ""}`
42
+ : active ?? "none";
43
+ bus.emit("ui:info", { message: `Model: ${label}` });
44
+ }
45
+ else {
46
+ bus.emit("config:switch-model", { model: name });
47
+ }
58
48
  },
59
- {
60
- name: "/compact",
61
- description: "Ask agent to summarize the conversation",
62
- handler: async () => {
63
- await getAcpClient().sendPrompt("Please provide a concise summary of our conversation so far and the current state of the work.");
64
- },
49
+ });
50
+ register({
51
+ name: "/thinking",
52
+ description: "Set thinking/reasoning effort level",
53
+ handler: (args) => {
54
+ const level = args.trim();
55
+ if (!level) {
56
+ const { level: current, levels, supported } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
57
+ const status = supported ? current : `${current} (not supported by current model)`;
58
+ bus.emit("ui:info", { message: `Thinking: ${status} (options: ${levels.join(", ")})` });
59
+ }
60
+ else {
61
+ bus.emit("config:set-thinking", { level });
62
+ }
65
63
  },
66
- {
67
- name: "/quit",
68
- description: "Exit agent-sh",
69
- handler: () => {
70
- quit();
71
- },
64
+ });
65
+ register({
66
+ name: "/backend",
67
+ description: "List or switch agent backend",
68
+ handler: (args) => {
69
+ const name = args.trim();
70
+ if (!name) {
71
+ bus.emit("config:list-backends", {});
72
+ }
73
+ else {
74
+ bus.emit("config:switch-backend", { name });
75
+ }
72
76
  },
73
- ];
74
- // Provide command completions for /-prefixed input
77
+ });
78
+ // ── Extension registration ────────────────────────────────────
79
+ bus.on("command:register", (cmd) => {
80
+ register(cmd);
81
+ });
82
+ // ── Skill commands (/skill:<name>) ────────────────────────────
83
+ const getSkills = () => {
84
+ const cwd = contextManager?.getCwd() ?? process.cwd();
85
+ return discoverSkills(cwd);
86
+ };
87
+ const handleSkillCommand = (skillName, args) => {
88
+ const skills = getSkills();
89
+ const skill = skills.find(s => s.name === skillName);
90
+ if (!skill) {
91
+ bus.emit("ui:error", { message: `Unknown skill: ${skillName}` });
92
+ return;
93
+ }
94
+ const content = loadSkillContent(skill);
95
+ if (!content) {
96
+ bus.emit("ui:error", { message: `Failed to load skill: ${skillName}` });
97
+ return;
98
+ }
99
+ const query = args.trim()
100
+ ? `${content}\n\n${args.trim()}`
101
+ : content;
102
+ bus.emit("agent:submit", { query });
103
+ };
104
+ // ── Autocomplete: command names ───────────────────────────────
75
105
  bus.onPipe("autocomplete:request", (payload) => {
76
106
  if (!payload.buffer.startsWith("/"))
77
107
  return payload;
108
+ // Argument completion is handled by separate pipe handlers below
109
+ if (payload.command)
110
+ return payload;
78
111
  const prefix = payload.buffer.toLowerCase();
79
- const matching = commands
112
+ const matching = [...commands.values()]
80
113
  .filter((c) => c.name.toLowerCase().startsWith(prefix))
81
114
  .map((c) => ({ name: c.name, description: c.description }));
115
+ // Skill commands
116
+ if (prefix.startsWith("/skill:") || "/skill:".startsWith(prefix)) {
117
+ const skills = getSkills();
118
+ for (const skill of skills) {
119
+ const name = `/skill:${skill.name}`;
120
+ if (name.toLowerCase().startsWith(prefix)) {
121
+ matching.push({ name, description: skill.description });
122
+ }
123
+ }
124
+ }
82
125
  if (matching.length === 0)
83
126
  return payload;
84
127
  return { ...payload, items: [...payload.items, ...matching] };
85
128
  });
86
- // Handle command execution
129
+ // ── Autocomplete: /model arguments ─────────────────────────────
130
+ bus.onPipe("autocomplete:request", (payload) => {
131
+ if (payload.command !== "/model")
132
+ return payload;
133
+ const partial = (payload.commandArgs ?? "").toLowerCase();
134
+ const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
135
+ const items = models
136
+ .filter((m) => m.model.toLowerCase().includes(partial))
137
+ .slice(0, 15)
138
+ .map((m) => ({
139
+ name: `/model ${m.model}`,
140
+ description: `${m.provider ? `[${m.provider}]` : ""}${m.model === active ? " (active)" : ""}`,
141
+ }));
142
+ if (items.length === 0)
143
+ return payload;
144
+ return { ...payload, items: [...payload.items, ...items] };
145
+ });
146
+ // ── Autocomplete: /thinking arguments ─────────────────────────
147
+ bus.onPipe("autocomplete:request", (payload) => {
148
+ if (payload.command !== "/thinking")
149
+ return payload;
150
+ const partial = (payload.commandArgs ?? "").toLowerCase();
151
+ const { level: current, levels } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
152
+ const items = levels
153
+ .filter((l) => l.startsWith(partial))
154
+ .map((l) => ({
155
+ name: `/thinking ${l}`,
156
+ description: l === current ? "(active)" : "",
157
+ }));
158
+ if (items.length === 0)
159
+ return payload;
160
+ return { ...payload, items: [...payload.items, ...items] };
161
+ });
162
+ // ── Autocomplete: /backend arguments ──────────────────────────
163
+ bus.onPipe("autocomplete:request", (payload) => {
164
+ if (payload.command !== "/backend")
165
+ return payload;
166
+ const partial = (payload.commandArgs ?? "").toLowerCase();
167
+ const { names, active } = bus.emitPipe("config:get-backends", { names: [], active: null });
168
+ const items = names
169
+ .filter((n) => n.toLowerCase().startsWith(partial))
170
+ .map((n) => ({
171
+ name: `/backend ${n}`,
172
+ description: n === active ? "(active)" : "",
173
+ }));
174
+ if (items.length === 0)
175
+ return payload;
176
+ return { ...payload, items: [...payload.items, ...items] };
177
+ });
178
+ // ── Dispatch ──────────────────────────────────────────────────
87
179
  bus.on("command:execute", (e) => {
88
- const cmd = commands.find((c) => c.name === e.name);
180
+ if (e.name.startsWith("/skill:")) {
181
+ const skillName = e.name.slice("/skill:".length);
182
+ handleSkillCommand(skillName, e.args);
183
+ return;
184
+ }
185
+ const cmd = commands.get(e.name);
89
186
  if (cmd) {
90
187
  const result = cmd.handler(e.args);
91
188
  if (result instanceof Promise) {