ashlrcode 1.0.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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/package.json +46 -0
  4. package/src/__tests__/branded-types.test.ts +47 -0
  5. package/src/__tests__/context.test.ts +163 -0
  6. package/src/__tests__/cost-tracker.test.ts +274 -0
  7. package/src/__tests__/cron.test.ts +197 -0
  8. package/src/__tests__/dream.test.ts +204 -0
  9. package/src/__tests__/error-handler.test.ts +192 -0
  10. package/src/__tests__/features.test.ts +69 -0
  11. package/src/__tests__/file-history.test.ts +177 -0
  12. package/src/__tests__/hooks.test.ts +145 -0
  13. package/src/__tests__/keybindings.test.ts +159 -0
  14. package/src/__tests__/model-patches.test.ts +82 -0
  15. package/src/__tests__/permissions-rules.test.ts +121 -0
  16. package/src/__tests__/permissions.test.ts +108 -0
  17. package/src/__tests__/project-config.test.ts +63 -0
  18. package/src/__tests__/retry.test.ts +321 -0
  19. package/src/__tests__/router.test.ts +158 -0
  20. package/src/__tests__/session-compact.test.ts +191 -0
  21. package/src/__tests__/session.test.ts +145 -0
  22. package/src/__tests__/skill-registry.test.ts +130 -0
  23. package/src/__tests__/speculation.test.ts +196 -0
  24. package/src/__tests__/tasks-v2.test.ts +267 -0
  25. package/src/__tests__/telemetry.test.ts +149 -0
  26. package/src/__tests__/tool-executor.test.ts +141 -0
  27. package/src/__tests__/tool-registry.test.ts +166 -0
  28. package/src/__tests__/undercover.test.ts +93 -0
  29. package/src/__tests__/workflow.test.ts +195 -0
  30. package/src/agent/async-context.ts +64 -0
  31. package/src/agent/context.ts +245 -0
  32. package/src/agent/cron.ts +189 -0
  33. package/src/agent/dream.ts +165 -0
  34. package/src/agent/error-handler.ts +108 -0
  35. package/src/agent/ipc.ts +256 -0
  36. package/src/agent/kairos.ts +207 -0
  37. package/src/agent/loop.ts +314 -0
  38. package/src/agent/model-patches.ts +68 -0
  39. package/src/agent/speculation.ts +219 -0
  40. package/src/agent/sub-agent.ts +125 -0
  41. package/src/agent/system-prompt.ts +231 -0
  42. package/src/agent/team.ts +220 -0
  43. package/src/agent/tool-executor.ts +162 -0
  44. package/src/agent/workflow.ts +189 -0
  45. package/src/agent/worktree-manager.ts +86 -0
  46. package/src/autopilot/queue.ts +186 -0
  47. package/src/autopilot/scanner.ts +245 -0
  48. package/src/autopilot/types.ts +58 -0
  49. package/src/bridge/bridge-client.ts +57 -0
  50. package/src/bridge/bridge-server.ts +81 -0
  51. package/src/cli.ts +1120 -0
  52. package/src/config/features.ts +51 -0
  53. package/src/config/git.ts +137 -0
  54. package/src/config/hooks.ts +201 -0
  55. package/src/config/permissions.ts +251 -0
  56. package/src/config/project-config.ts +63 -0
  57. package/src/config/remote-settings.ts +163 -0
  58. package/src/config/settings-sync.ts +170 -0
  59. package/src/config/settings.ts +113 -0
  60. package/src/config/undercover.ts +76 -0
  61. package/src/config/upgrade-notice.ts +65 -0
  62. package/src/mcp/client.ts +197 -0
  63. package/src/mcp/manager.ts +125 -0
  64. package/src/mcp/oauth.ts +252 -0
  65. package/src/mcp/types.ts +61 -0
  66. package/src/persistence/memory.ts +129 -0
  67. package/src/persistence/session.ts +289 -0
  68. package/src/planning/plan-mode.ts +128 -0
  69. package/src/planning/plan-tools.ts +138 -0
  70. package/src/providers/anthropic.ts +177 -0
  71. package/src/providers/cost-tracker.ts +184 -0
  72. package/src/providers/retry.ts +264 -0
  73. package/src/providers/router.ts +159 -0
  74. package/src/providers/types.ts +79 -0
  75. package/src/providers/xai.ts +217 -0
  76. package/src/repl.tsx +1384 -0
  77. package/src/setup.ts +119 -0
  78. package/src/skills/loader.ts +78 -0
  79. package/src/skills/registry.ts +78 -0
  80. package/src/skills/types.ts +11 -0
  81. package/src/state/file-history.ts +264 -0
  82. package/src/telemetry/event-log.ts +116 -0
  83. package/src/tools/agent.ts +133 -0
  84. package/src/tools/ask-user.ts +229 -0
  85. package/src/tools/bash.ts +146 -0
  86. package/src/tools/config.ts +147 -0
  87. package/src/tools/diff.ts +137 -0
  88. package/src/tools/file-edit.ts +123 -0
  89. package/src/tools/file-read.ts +82 -0
  90. package/src/tools/file-write.ts +82 -0
  91. package/src/tools/glob.ts +76 -0
  92. package/src/tools/grep.ts +187 -0
  93. package/src/tools/ls.ts +77 -0
  94. package/src/tools/lsp.ts +375 -0
  95. package/src/tools/mcp-resources.ts +83 -0
  96. package/src/tools/mcp-tool.ts +47 -0
  97. package/src/tools/memory.ts +148 -0
  98. package/src/tools/notebook-edit.ts +133 -0
  99. package/src/tools/peers.ts +113 -0
  100. package/src/tools/powershell.ts +83 -0
  101. package/src/tools/registry.ts +114 -0
  102. package/src/tools/send-message.ts +75 -0
  103. package/src/tools/sleep.ts +50 -0
  104. package/src/tools/snip.ts +143 -0
  105. package/src/tools/tasks.ts +349 -0
  106. package/src/tools/team.ts +309 -0
  107. package/src/tools/todo-write.ts +93 -0
  108. package/src/tools/tool-search.ts +83 -0
  109. package/src/tools/types.ts +52 -0
  110. package/src/tools/web-browser.ts +263 -0
  111. package/src/tools/web-fetch.ts +118 -0
  112. package/src/tools/web-search.ts +107 -0
  113. package/src/tools/workflow.ts +188 -0
  114. package/src/tools/worktree.ts +143 -0
  115. package/src/types/branded.ts +22 -0
  116. package/src/ui/App.tsx +184 -0
  117. package/src/ui/BuddyPanel.tsx +52 -0
  118. package/src/ui/PermissionPrompt.tsx +29 -0
  119. package/src/ui/banner.ts +217 -0
  120. package/src/ui/buddy-ai.ts +108 -0
  121. package/src/ui/buddy.ts +466 -0
  122. package/src/ui/context-bar.ts +60 -0
  123. package/src/ui/effort.ts +65 -0
  124. package/src/ui/keybindings.ts +143 -0
  125. package/src/ui/markdown.ts +271 -0
  126. package/src/ui/message-renderer.ts +73 -0
  127. package/src/ui/mode.ts +80 -0
  128. package/src/ui/notifications.ts +57 -0
  129. package/src/ui/speech-bubble.ts +95 -0
  130. package/src/ui/spinner.ts +116 -0
  131. package/src/ui/theme.ts +98 -0
  132. package/src/version.ts +5 -0
  133. package/src/voice/voice-mode.ts +169 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * WebBrowserTool — headless browser automation for JavaScript-heavy pages.
3
+ * Uses Puppeteer (if installed) for full browser rendering.
4
+ * Fallback: errors with install instructions.
5
+ *
6
+ * Gated behind BROWSER_TOOL feature flag.
7
+ */
8
+
9
+ import type { Tool, ToolContext } from "./types.ts";
10
+
11
+ interface BrowserResult {
12
+ title: string;
13
+ url: string;
14
+ text: string;
15
+ links: Array<{ text: string; href: string }>;
16
+ screenshot?: string; // base64
17
+ }
18
+
19
+ async function getPuppeteer(): Promise<any | null> {
20
+ try {
21
+ // Dynamic import — puppeteer may not be installed
22
+ // @ts-expect-error — optional peer dependency
23
+ return await import("puppeteer");
24
+ } catch {
25
+ try {
26
+ // @ts-expect-error — optional peer dependency
27
+ return await import("puppeteer-core");
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ }
33
+
34
+ async function findChromePath(): Promise<string | null> {
35
+ const paths =
36
+ process.platform === "darwin"
37
+ ? [
38
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
39
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
40
+ ]
41
+ : process.platform === "linux"
42
+ ? ["/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium"]
43
+ : [
44
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
45
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
46
+ ];
47
+
48
+ const { existsSync } = await import("fs");
49
+ for (const p of paths) {
50
+ if (existsSync(p)) return p;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ let _browser: any = null;
56
+
57
+ async function getBrowser(): Promise<any> {
58
+ if (_browser?.isConnected?.()) return _browser;
59
+ _browser = null;
60
+
61
+ const puppeteer = await getPuppeteer();
62
+ if (!puppeteer) {
63
+ throw new Error("Puppeteer not installed. Run: bun add puppeteer");
64
+ }
65
+
66
+ const chromePath = await findChromePath();
67
+ _browser = await puppeteer.launch({
68
+ headless: "new",
69
+ executablePath: chromePath ?? undefined,
70
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
71
+ });
72
+
73
+ return _browser;
74
+ }
75
+
76
+ async function browsePage(
77
+ url: string,
78
+ action: string,
79
+ selector?: string,
80
+ value?: string,
81
+ waitMs?: number
82
+ ): Promise<BrowserResult> {
83
+ const browser = await getBrowser();
84
+ const page = await browser.newPage();
85
+
86
+ try {
87
+ await page.setViewport({ width: 1280, height: 720 });
88
+ await page.setUserAgent(
89
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AshlrCode/1.0"
90
+ );
91
+
92
+ await page.goto(url, { waitUntil: "networkidle2", timeout: 30_000 });
93
+
94
+ if (waitMs) await page.waitForTimeout(waitMs);
95
+
96
+ switch (action) {
97
+ case "read":
98
+ break; // Just read the page
99
+ case "click":
100
+ if (selector) {
101
+ await page.waitForSelector(selector, { timeout: 5000 });
102
+ await page.click(selector);
103
+ await page.waitForTimeout(1000);
104
+ }
105
+ break;
106
+ case "type":
107
+ if (selector && value) {
108
+ await page.waitForSelector(selector, { timeout: 5000 });
109
+ await page.type(selector, value);
110
+ }
111
+ break;
112
+ case "screenshot": {
113
+ const screenshotBuffer = await page.screenshot({ encoding: "base64" });
114
+ return {
115
+ title: await page.title(),
116
+ url: page.url(),
117
+ text: "",
118
+ links: [],
119
+ screenshot: screenshotBuffer as string,
120
+ };
121
+ }
122
+ }
123
+
124
+ // Extract page content after JS has rendered.
125
+ // The callback runs inside the browser context (Puppeteer serializes it),
126
+ // so DOM globals like `document` exist at runtime but not in our TS lib.
127
+ const result: { title: string; text: string; links: Array<{ text: string; href: string }> } =
128
+ await page.evaluate(`
129
+ (() => {
130
+ const title = document.title;
131
+ const bodyText = (document.body && document.body.innerText || "").slice(0, 10000);
132
+ const links = Array.from(document.querySelectorAll("a[href]"))
133
+ .slice(0, 20)
134
+ .map(a => ({
135
+ text: (a.textContent || "").trim().slice(0, 50),
136
+ href: a.href,
137
+ }));
138
+ return { title, text: bodyText, links };
139
+ })()
140
+ `);
141
+
142
+ return { ...result, url: page.url() };
143
+ } finally {
144
+ await page.close();
145
+ }
146
+ }
147
+
148
+ export const webBrowserTool: Tool = {
149
+ name: "WebBrowser",
150
+
151
+ prompt() {
152
+ return `Browse web pages with full JavaScript rendering. Use for:
153
+ - Reading JavaScript-heavy SPAs that WebFetch can't handle
154
+ - Clicking buttons and filling forms
155
+ - Taking screenshots of web pages
156
+ - Extracting text from dynamically-rendered content
157
+
158
+ Requires Puppeteer (bun add puppeteer). Actions: read, click, type, screenshot.`;
159
+ },
160
+
161
+ inputSchema() {
162
+ return {
163
+ type: "object",
164
+ properties: {
165
+ url: { type: "string", description: "URL to navigate to" },
166
+ action: {
167
+ type: "string",
168
+ enum: ["read", "click", "type", "screenshot"],
169
+ description: "Action to perform on the page",
170
+ },
171
+ selector: {
172
+ type: "string",
173
+ description: "CSS selector for click/type actions",
174
+ },
175
+ value: {
176
+ type: "string",
177
+ description: "Text to type (for type action)",
178
+ },
179
+ waitMs: {
180
+ type: "number",
181
+ description: "Extra wait time in ms after page load",
182
+ },
183
+ },
184
+ required: ["url", "action"],
185
+ };
186
+ },
187
+
188
+ isReadOnly() {
189
+ return true;
190
+ },
191
+ isDestructive() {
192
+ return false;
193
+ },
194
+ isConcurrencySafe() {
195
+ return false;
196
+ },
197
+
198
+ validateInput(input) {
199
+ if (!input.url || typeof input.url !== "string") return "url is required";
200
+ if (!input.action || typeof input.action !== "string") return "action is required";
201
+
202
+ const validActions = ["read", "click", "type", "screenshot"];
203
+ if (!validActions.includes(input.action as string)) {
204
+ return `Invalid action: ${input.action}. Must be one of: ${validActions.join(", ")}`;
205
+ }
206
+
207
+ try {
208
+ new URL(input.url as string);
209
+ } catch {
210
+ return "Invalid URL";
211
+ }
212
+
213
+ if (input.action === "click" && !input.selector) {
214
+ return "selector is required for click action";
215
+ }
216
+ if (input.action === "type" && (!input.selector || !input.value)) {
217
+ return "selector and value are required for type action";
218
+ }
219
+
220
+ return null;
221
+ },
222
+
223
+ async call(input, _context) {
224
+ const url = input.url as string;
225
+ const action = input.action as string;
226
+ const selector = input.selector as string | undefined;
227
+ const value = input.value as string | undefined;
228
+ const waitMs = input.waitMs as number | undefined;
229
+
230
+ try {
231
+ const result = await browsePage(url, action, selector, value, waitMs);
232
+
233
+ if (result.screenshot) {
234
+ return `Screenshot captured (base64, ${result.screenshot.length} chars)\nTitle: ${result.title}\nURL: ${result.url}`;
235
+ }
236
+
237
+ const lines: string[] = [];
238
+ lines.push(`Title: ${result.title}`);
239
+ lines.push(`URL: ${result.url}`);
240
+ lines.push("");
241
+ lines.push(result.text.slice(0, 8000));
242
+
243
+ if (result.links.length > 0) {
244
+ lines.push("\nLinks:");
245
+ for (const link of result.links.slice(0, 10)) {
246
+ lines.push(` [${link.text}](${link.href})`);
247
+ }
248
+ }
249
+
250
+ return lines.join("\n");
251
+ } catch (err) {
252
+ return `Browser error: ${err instanceof Error ? err.message : String(err)}`;
253
+ }
254
+ },
255
+ };
256
+
257
+ /** Gracefully shut down the shared browser instance. */
258
+ export async function shutdownBrowser(): Promise<void> {
259
+ if (_browser) {
260
+ await _browser.close().catch(() => {});
261
+ _browser = null;
262
+ }
263
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * WebFetch tool — HTTP requests for research and exploration.
3
+ */
4
+
5
+ import type { Tool, ToolContext } from "./types.ts";
6
+
7
+ const MAX_RESPONSE_SIZE = 50_000; // chars
8
+
9
+ export const webFetchTool: Tool = {
10
+ name: "WebFetch",
11
+
12
+ prompt() {
13
+ return "Fetch a URL and return its content. Useful for reading documentation, APIs, or web pages. Returns text content, truncated if too large.";
14
+ },
15
+
16
+ inputSchema() {
17
+ return {
18
+ type: "object",
19
+ properties: {
20
+ url: {
21
+ type: "string",
22
+ description: "The URL to fetch",
23
+ },
24
+ method: {
25
+ type: "string",
26
+ enum: ["GET", "POST", "PUT", "DELETE"],
27
+ description: "HTTP method (default: GET)",
28
+ },
29
+ headers: {
30
+ type: "object",
31
+ description: "Additional HTTP headers",
32
+ },
33
+ body: {
34
+ type: "string",
35
+ description: "Request body (for POST/PUT)",
36
+ },
37
+ },
38
+ required: ["url"],
39
+ };
40
+ },
41
+
42
+ isReadOnly() {
43
+ return true;
44
+ },
45
+ isDestructive() {
46
+ return false;
47
+ },
48
+ isConcurrencySafe() {
49
+ return true;
50
+ },
51
+
52
+ validateInput(input) {
53
+ if (!input.url || typeof input.url !== "string") {
54
+ return "url is required";
55
+ }
56
+ try {
57
+ new URL(input.url as string);
58
+ } catch {
59
+ return "Invalid URL";
60
+ }
61
+ return null;
62
+ },
63
+
64
+ async call(input, _context) {
65
+ const url = input.url as string;
66
+ const method = (input.method as string) ?? "GET";
67
+ const headers = (input.headers as Record<string, string>) ?? {};
68
+ const body = input.body as string | undefined;
69
+
70
+ try {
71
+ const response = await fetch(url, {
72
+ method,
73
+ headers: {
74
+ "User-Agent": "AshlrCode/0.1.0",
75
+ ...headers,
76
+ },
77
+ body: method !== "GET" ? body : undefined,
78
+ signal: AbortSignal.timeout(30_000),
79
+ });
80
+
81
+ const contentType = response.headers.get("content-type") ?? "";
82
+ const status = response.status;
83
+
84
+ let text = await response.text();
85
+
86
+ // Truncate if too large
87
+ if (text.length > MAX_RESPONSE_SIZE) {
88
+ text =
89
+ text.slice(0, MAX_RESPONSE_SIZE) +
90
+ `\n\n[... truncated at ${MAX_RESPONSE_SIZE} chars, total: ${text.length} chars]`;
91
+ }
92
+
93
+ // Strip HTML tags for readability if it's HTML
94
+ if (contentType.includes("text/html")) {
95
+ text = stripHtml(text);
96
+ }
97
+
98
+ return `HTTP ${status} ${response.statusText}\nContent-Type: ${contentType}\n\n${text}`;
99
+ } catch (err) {
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ return `Fetch error: ${message}`;
102
+ }
103
+ },
104
+ };
105
+
106
+ function stripHtml(html: string): string {
107
+ return html
108
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
109
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
110
+ .replace(/<[^>]+>/g, " ")
111
+ .replace(/\s{2,}/g, " ")
112
+ .replace(/&nbsp;/g, " ")
113
+ .replace(/&amp;/g, "&")
114
+ .replace(/&lt;/g, "<")
115
+ .replace(/&gt;/g, ">")
116
+ .replace(/&quot;/g, '"')
117
+ .trim();
118
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * WebSearch tool — search the web using DuckDuckGo HTML API.
3
+ */
4
+
5
+ import type { Tool, ToolContext } from "./types.ts";
6
+
7
+ export const webSearchTool: Tool = {
8
+ name: "WebSearch",
9
+
10
+ prompt() {
11
+ return "Search the web and return top results with titles, URLs, and snippets. Uses DuckDuckGo.";
12
+ },
13
+
14
+ inputSchema() {
15
+ return {
16
+ type: "object",
17
+ properties: {
18
+ query: {
19
+ type: "string",
20
+ description: "Search query",
21
+ },
22
+ maxResults: {
23
+ type: "number",
24
+ description: "Maximum number of results (default: 5)",
25
+ },
26
+ },
27
+ required: ["query"],
28
+ };
29
+ },
30
+
31
+ isReadOnly() {
32
+ return true;
33
+ },
34
+ isDestructive() {
35
+ return false;
36
+ },
37
+ isConcurrencySafe() {
38
+ return true;
39
+ },
40
+
41
+ validateInput(input) {
42
+ if (!input.query || typeof input.query !== "string") {
43
+ return "query is required";
44
+ }
45
+ return null;
46
+ },
47
+
48
+ async call(input, _context) {
49
+ const query = input.query as string;
50
+ const maxResults = (input.maxResults as number) ?? 5;
51
+
52
+ try {
53
+ // Use DuckDuckGo HTML search (no API key needed)
54
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
55
+ const response = await fetch(url, {
56
+ headers: {
57
+ "User-Agent": "AshlrCode/1.0",
58
+ },
59
+ signal: AbortSignal.timeout(15_000),
60
+ });
61
+
62
+ const html = await response.text();
63
+
64
+ // Extract results from DDG HTML
65
+ const results = extractDDGResults(html, maxResults);
66
+
67
+ if (results.length === 0) {
68
+ return `No results found for: "${query}"`;
69
+ }
70
+
71
+ return results
72
+ .map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`)
73
+ .join("\n\n");
74
+ } catch (err) {
75
+ const message = err instanceof Error ? err.message : String(err);
76
+ return `Search error: ${message}`;
77
+ }
78
+ },
79
+ };
80
+
81
+ interface SearchResult {
82
+ title: string;
83
+ url: string;
84
+ snippet: string;
85
+ }
86
+
87
+ function extractDDGResults(html: string, max: number): SearchResult[] {
88
+ const results: SearchResult[] = [];
89
+
90
+ // Match result blocks in DDG HTML
91
+ const resultRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
92
+
93
+ let match;
94
+ while ((match = resultRegex.exec(html)) !== null && results.length < max) {
95
+ const url = decodeURIComponent(
96
+ match[1]!.replace(/.*uddg=/, "").replace(/&.*/, "")
97
+ );
98
+ const title = match[2]!.replace(/<[^>]+>/g, "").trim();
99
+ const snippet = match[3]!.replace(/<[^>]+>/g, "").trim();
100
+
101
+ if (title && url) {
102
+ results.push({ title, url, snippet });
103
+ }
104
+ }
105
+
106
+ return results;
107
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * WorkflowTool — manage and run multi-step automation workflows.
3
+ * Workflows define a sequence of prompts, shell commands, and tool calls
4
+ * stored in .ashlrcode/workflows/.
5
+ */
6
+
7
+ import type { Tool, ToolContext } from "./types.ts";
8
+ import {
9
+ listWorkflows,
10
+ loadWorkflow,
11
+ deleteWorkflow,
12
+ createWorkflow,
13
+ executeWorkflow,
14
+ markWorkflowRun,
15
+ type WorkflowStep,
16
+ } from "../agent/workflow.ts";
17
+
18
+ export const workflowTool: Tool = {
19
+ name: "Workflow",
20
+
21
+ prompt() {
22
+ return `Manage and run multi-step workflows. Workflows define a sequence of prompts, shell commands, and tool calls that execute in order.
23
+
24
+ Actions:
25
+ - list: Show all saved workflows
26
+ - run: Execute a workflow by ID
27
+ - create: Define a new workflow with steps
28
+ - delete: Remove a workflow by ID
29
+
30
+ Each step has a type: "prompt" (send text to the LLM), "command" (run a shell command), or "tool" (invoke a tool by name). Steps run sequentially and halt on failure unless continueOnError is set.`;
31
+ },
32
+
33
+ inputSchema() {
34
+ return {
35
+ type: "object",
36
+ properties: {
37
+ action: {
38
+ type: "string",
39
+ enum: ["list", "run", "create", "delete"],
40
+ description: "What to do",
41
+ },
42
+ workflowId: {
43
+ type: "string",
44
+ description: "Workflow ID (required for run/delete)",
45
+ },
46
+ name: {
47
+ type: "string",
48
+ description: "Workflow name (required for create)",
49
+ },
50
+ description: {
51
+ type: "string",
52
+ description: "Workflow description (for create)",
53
+ },
54
+ steps: {
55
+ type: "array",
56
+ description:
57
+ 'Array of workflow steps (for create). Each step: { name: string, type: "prompt"|"command"|"tool", value: string, input?: object, continueOnError?: boolean }',
58
+ items: {
59
+ type: "object",
60
+ properties: {
61
+ name: { type: "string" },
62
+ type: { type: "string", enum: ["prompt", "command", "tool"] },
63
+ value: { type: "string" },
64
+ input: { type: "object" },
65
+ continueOnError: { type: "boolean" },
66
+ },
67
+ required: ["name", "type", "value"],
68
+ },
69
+ },
70
+ },
71
+ required: ["action"],
72
+ };
73
+ },
74
+
75
+ isReadOnly() {
76
+ return false;
77
+ },
78
+ isDestructive() {
79
+ return false;
80
+ },
81
+ isConcurrencySafe() {
82
+ return false;
83
+ },
84
+
85
+ validateInput(input) {
86
+ const action = input.action as string;
87
+ if (!action) return "action is required";
88
+ if (!["list", "run", "create", "delete"].includes(action))
89
+ return `Invalid action: ${action}`;
90
+ if ((action === "run" || action === "delete") && !input.workflowId)
91
+ return "workflowId is required for run/delete";
92
+ if (action === "create") {
93
+ if (!input.name) return "name is required for create";
94
+ if (!input.steps || !Array.isArray(input.steps) || input.steps.length === 0)
95
+ return "steps array is required for create (must have at least one step)";
96
+ }
97
+ return null;
98
+ },
99
+
100
+ async call(input, context) {
101
+ const action = input.action as string;
102
+
103
+ if (action === "list") {
104
+ const workflows = await listWorkflows();
105
+ if (workflows.length === 0)
106
+ return "No workflows found. Create one with action: create.";
107
+ return workflows
108
+ .map(
109
+ (w) =>
110
+ `${w.id} — ${w.name} (${w.steps.length} steps, run ${w.runCount}x)${w.description ? `\n ${w.description}` : ""}`,
111
+ )
112
+ .join("\n");
113
+ }
114
+
115
+ if (action === "delete") {
116
+ const ok = await deleteWorkflow(input.workflowId as string);
117
+ return ok ? "Workflow deleted." : "Workflow not found.";
118
+ }
119
+
120
+ if (action === "create") {
121
+ const wf = await createWorkflow(
122
+ input.name as string,
123
+ (input.description as string) ?? "",
124
+ input.steps as WorkflowStep[],
125
+ );
126
+ return `Workflow "${wf.name}" created (${wf.id}, ${wf.steps.length} steps)`;
127
+ }
128
+
129
+ if (action === "run") {
130
+ const wf = await loadWorkflow(input.workflowId as string);
131
+ if (!wf) return "Workflow not found.";
132
+
133
+ const result = await executeWorkflow(wf, {
134
+ // Prompt execution: placeholder — real agent integration would
135
+ // feed the prompt into the conversation loop
136
+ runPrompt: async (prompt) =>
137
+ `[Would execute prompt: ${prompt.slice(0, 200)}]`,
138
+
139
+ // Command execution: runs via Bun.spawn
140
+ runCommand: async (cmd) => {
141
+ const proc = Bun.spawn(["bash", "-c", cmd], {
142
+ cwd: context.cwd,
143
+ stdout: "pipe",
144
+ stderr: "pipe",
145
+ });
146
+ const stdout = await new Response(proc.stdout).text();
147
+ const stderr = await new Response(proc.stderr).text();
148
+ const exitCode = await proc.exited;
149
+ const output = (stdout + (stderr ? `\n${stderr}` : "")).trim();
150
+ return { output, exitCode };
151
+ },
152
+
153
+ // Tool execution: placeholder — real integration would dispatch
154
+ // through the ToolRegistry
155
+ runTool: async (name, toolInput) => {
156
+ return {
157
+ result: `[Would call tool: ${name} with ${JSON.stringify(toolInput).slice(0, 200)}]`,
158
+ isError: false,
159
+ };
160
+ },
161
+
162
+ onStepStart: (step, i) => {
163
+ console.log(
164
+ ` Step ${i + 1}/${wf.steps.length}: ${step.name} (${step.type})`,
165
+ );
166
+ },
167
+ onStepEnd: (_step, _i, success, _output) => {
168
+ console.log(` ${success ? "OK" : "FAIL"}: ${_step.name}`);
169
+ },
170
+ });
171
+
172
+ await markWorkflowRun(wf.id);
173
+
174
+ const lines = [
175
+ `Workflow: ${result.workflow}`,
176
+ `Steps: ${result.stepsCompleted}/${result.stepsTotal}`,
177
+ `Duration: ${result.durationMs}ms`,
178
+ "",
179
+ ];
180
+ for (const r of result.results) {
181
+ lines.push(`${r.success ? "[OK]" : "[FAIL]"} ${r.step}: ${r.output.slice(0, 200)}`);
182
+ }
183
+ return lines.join("\n");
184
+ }
185
+
186
+ return `Unknown action: ${action}`;
187
+ },
188
+ };