@flue/sdk 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,9 +94,8 @@ A triage agent that runs in CI whenever an issue is opened on GitHub. The `"loca
94
94
 
95
95
  ```ts
96
96
  // .flue/agents/triage.ts
97
- import { defineCommand, type FlueContext } from '@flue/sdk/client';
98
- import { execFile } from 'node:child_process';
99
- import { promisify } from 'node:util';
97
+ import { type FlueContext } from '@flue/sdk/client';
98
+ import { defineCommand } from '@flue/sdk/node';
100
99
  import * as v from 'valibot';
101
100
 
102
101
  // Because we are running this in CI, we don't need to expose this as an HTTP endpoint.
@@ -106,12 +105,8 @@ export const triggers = {};
106
105
  // Connect privileged CLIs to your agent without leaking sensitive keys and secrets.
107
106
  // Secrets are hooked up inside the command definition here, so your agent never sees them.
108
107
  // Commands are controlled per-prompt, so you can be as granular with access as you need.
109
- const npm = defineCommand('npm', async (args) => promisify(execFile)('npm', args));
110
- const gh = defineCommand('gh', async (args) =>
111
- promisify(execFile)('gh', args, {
112
- env: { GH_TOKEN: process.env.GH_TOKEN },
113
- }),
114
- );
108
+ const npm = defineCommand('npm');
109
+ const gh = defineCommand('gh', { env: { GH_TOKEN: process.env.GH_TOKEN } });
115
110
 
116
111
  export default async function ({ init, payload }: FlueContext) {
117
112
  // 'local' mounts the host filesystem at /workspace — ideal for CI
@@ -0,0 +1,432 @@
1
+ import { Type } from "@mariozechner/pi-ai";
2
+
3
+ //#region src/context.ts
4
+ /** Parse optional YAML frontmatter (--- delimited). Basic `key: value` only. */
5
+ function parseFrontmatterFile(content, defaultName) {
6
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
7
+ if (!frontmatterMatch) return {
8
+ name: defaultName,
9
+ description: "",
10
+ body: content.trim(),
11
+ frontmatter: {}
12
+ };
13
+ const rawFrontmatter = frontmatterMatch[1] ?? "";
14
+ const body = frontmatterMatch[2] ?? "";
15
+ const frontmatter = {};
16
+ for (const line of rawFrontmatter.split("\n")) {
17
+ const match = line.match(/^(\w+):\s*(.+)$/);
18
+ if (match?.[1] && match[2]) frontmatter[match[1]] = match[2].trim();
19
+ }
20
+ return {
21
+ name: frontmatter.name || defaultName,
22
+ description: frontmatter.description || "",
23
+ body: body.trim(),
24
+ frontmatter
25
+ };
26
+ }
27
+ /** Read AGENTS.md (and CLAUDE.md if present) from a directory. Returns concatenated contents. */
28
+ async function readAgentsMd(env, basePath) {
29
+ const parts = [];
30
+ for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
31
+ const filePath = basePath.endsWith("/") ? basePath + filename : `${basePath}/${filename}`;
32
+ if (await env.exists(filePath)) {
33
+ const content = await env.readFile(filePath);
34
+ parts.push(content.trim());
35
+ }
36
+ }
37
+ return parts.join("\n\n");
38
+ }
39
+ /**
40
+ * Load a skill directly by relative path under `.agents/skills/`.
41
+ *
42
+ * The path is taken as-is — no extension is auto-appended. Callers reference
43
+ * the full filename, e.g. `'triage/reproduce.md'`. Returns `null` if the file
44
+ * doesn't exist.
45
+ *
46
+ * Used as a fallback by `session.skill()` when the requested name doesn't match
47
+ * a discovered skill's frontmatter `name:` field. Lets users organise skills as
48
+ * a pack of sibling markdown files under one directory (orchestration SKILL.md
49
+ * + stage files) without forcing each stage into its own `SKILL.md` subdirectory.
50
+ */
51
+ async function loadSkillByPath(env, basePath, relPath) {
52
+ const filePath = `${basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`}/${relPath}`;
53
+ if (!await env.exists(filePath)) return null;
54
+ const parsed = parseFrontmatterFile(await env.readFile(filePath), relPath.replace(/\.(md|markdown)$/i, ""));
55
+ return {
56
+ name: parsed.name,
57
+ description: parsed.description,
58
+ instructions: parsed.body
59
+ };
60
+ }
61
+ /** Discover skills from .agents/skills/<name>/SKILL.md under basePath. */
62
+ async function discoverLocalSkills(env, basePath) {
63
+ const skillsDir = basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`;
64
+ if (!await env.exists(skillsDir)) return {};
65
+ const skills = {};
66
+ const entries = await env.readdir(skillsDir);
67
+ for (const entry of entries) {
68
+ const skillDir = `${skillsDir}/${entry}`;
69
+ try {
70
+ if (!(await env.stat(skillDir)).isDirectory) continue;
71
+ } catch {
72
+ continue;
73
+ }
74
+ const skillMdPath = `${skillDir}/SKILL.md`;
75
+ if (!await env.exists(skillMdPath)) continue;
76
+ const parsed = parseFrontmatterFile(await env.readFile(skillMdPath), entry);
77
+ skills[parsed.name] = {
78
+ name: parsed.name,
79
+ description: parsed.description,
80
+ instructions: parsed.body
81
+ };
82
+ }
83
+ return skills;
84
+ }
85
+ function composeSystemPrompt(agentsMd, skills, env) {
86
+ const parts = [];
87
+ if (agentsMd) parts.push(agentsMd);
88
+ const skillEntries = Object.values(skills);
89
+ if (skillEntries.length > 0) {
90
+ parts.push("", "## Available Skills", "");
91
+ for (const skill of skillEntries) {
92
+ const desc = skill.description ? ` - ${skill.description}` : "";
93
+ parts.push(`- **${skill.name}**${desc}`);
94
+ }
95
+ }
96
+ if (env) {
97
+ const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
98
+ weekday: "short",
99
+ year: "numeric",
100
+ month: "short",
101
+ day: "numeric"
102
+ });
103
+ parts.push("", `Date: ${date}`);
104
+ parts.push(`Working directory: ${env.cwd}`);
105
+ if (env.directoryListing && env.directoryListing.length > 0) parts.push("", "Directory structure:", env.directoryListing.join("\n"));
106
+ }
107
+ return parts.join("\n");
108
+ }
109
+ /** Discover AGENTS.md, local skills, and directory listing from the session's cwd. */
110
+ async function discoverSessionContext(env) {
111
+ const cwd = env.cwd;
112
+ const agentsMd = await readAgentsMd(env, cwd);
113
+ const skills = await discoverLocalSkills(env, cwd);
114
+ let directoryListing;
115
+ try {
116
+ directoryListing = await env.readdir(cwd);
117
+ } catch {}
118
+ return {
119
+ systemPrompt: composeSystemPrompt(agentsMd, skills, {
120
+ cwd,
121
+ directoryListing
122
+ }),
123
+ skills
124
+ };
125
+ }
126
+
127
+ //#endregion
128
+ //#region src/agent.ts
129
+ const MAX_READ_LINES = 2e3;
130
+ const MAX_READ_BYTES = 50 * 1024;
131
+ const MAX_GREP_MATCHES = 100;
132
+ const MAX_GREP_LINE_LENGTH = 500;
133
+ const MAX_GLOB_RESULTS = 1e3;
134
+ const BUILTIN_TOOL_NAMES = new Set([
135
+ "read",
136
+ "write",
137
+ "edit",
138
+ "bash",
139
+ "grep",
140
+ "glob"
141
+ ]);
142
+ function createTools(env) {
143
+ return [
144
+ createReadTool(env),
145
+ createWriteTool(env),
146
+ createEditTool(env),
147
+ createBashTool(env),
148
+ createGrepTool(env),
149
+ createGlobTool(env)
150
+ ];
151
+ }
152
+ function createReadTool(env) {
153
+ return {
154
+ name: "read",
155
+ label: "Read File",
156
+ description: "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.",
157
+ parameters: Type.Object({
158
+ path: Type.String({ description: "Path to the file to read" }),
159
+ offset: Type.Optional(Type.Number({ description: "Line number to start from (1-indexed)" })),
160
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
161
+ }),
162
+ async execute(_toolCallId, params, signal) {
163
+ throwIfAborted(signal);
164
+ try {
165
+ if ((await env.stat(params.path)).isDirectory) {
166
+ const entries = await env.readdir(params.path);
167
+ return {
168
+ content: [{
169
+ type: "text",
170
+ text: entries.join("\n") || "(empty directory)"
171
+ }],
172
+ details: {
173
+ path: params.path,
174
+ isDirectory: true,
175
+ entries: entries.length
176
+ }
177
+ };
178
+ }
179
+ } catch {}
180
+ const allLines = (await env.readFile(params.path)).split("\n");
181
+ const startLine = params.offset ? Math.max(0, params.offset - 1) : 0;
182
+ if (startLine >= allLines.length) throw new Error(`Offset ${params.offset} is beyond end of file (${allLines.length} lines total)`);
183
+ const endLine = params.limit ? startLine + params.limit : allLines.length;
184
+ const { text: truncatedText, wasTruncated } = truncateHead(allLines.slice(startLine, endLine), MAX_READ_LINES, MAX_READ_BYTES);
185
+ let output = truncatedText;
186
+ if (wasTruncated) {
187
+ const shownEnd = startLine + truncatedText.split("\n").length;
188
+ output += `\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${allLines.length}. Use offset=${shownEnd + 1} to continue.]`;
189
+ }
190
+ return {
191
+ content: [{
192
+ type: "text",
193
+ text: output
194
+ }],
195
+ details: {
196
+ path: params.path,
197
+ lines: allLines.length
198
+ }
199
+ };
200
+ }
201
+ };
202
+ }
203
+ function createWriteTool(env) {
204
+ return {
205
+ name: "write",
206
+ label: "Write File",
207
+ description: "Write content to a file. Creates the file and parent directories if they do not exist.",
208
+ parameters: Type.Object({
209
+ path: Type.String({ description: "Path to the file to write" }),
210
+ content: Type.String({ description: "Content to write to the file" })
211
+ }),
212
+ async execute(_toolCallId, params, signal) {
213
+ throwIfAborted(signal);
214
+ const resolved = env.resolvePath(params.path);
215
+ const dir = resolved.replace(/\/[^/]*$/, "");
216
+ if (dir && dir !== resolved) await env.mkdir(dir, { recursive: true });
217
+ await env.writeFile(resolved, params.content);
218
+ return {
219
+ content: [{
220
+ type: "text",
221
+ text: `Successfully wrote ${params.content.length} bytes to ${params.path}`
222
+ }],
223
+ details: {
224
+ path: params.path,
225
+ size: params.content.length
226
+ }
227
+ };
228
+ }
229
+ };
230
+ }
231
+ function createEditTool(env) {
232
+ return {
233
+ name: "edit",
234
+ label: "Edit File",
235
+ description: "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.",
236
+ parameters: Type.Object({
237
+ path: Type.String({ description: "Path to the file to edit" }),
238
+ oldText: Type.String({ description: "Exact text to find (must be unique)" }),
239
+ newText: Type.String({ description: "Replacement text" }),
240
+ replaceAll: Type.Optional(Type.Boolean({ description: "Replace all occurrences" }))
241
+ }),
242
+ async execute(_toolCallId, params, signal) {
243
+ throwIfAborted(signal);
244
+ const content = await env.readFile(params.path);
245
+ if (params.replaceAll) {
246
+ const newContent = content.replaceAll(params.oldText, params.newText);
247
+ if (newContent === content) throw new Error(`Could not find the text in ${params.path}. No changes made.`);
248
+ await env.writeFile(params.path, newContent);
249
+ const count = content.split(params.oldText).length - 1;
250
+ return {
251
+ content: [{
252
+ type: "text",
253
+ text: `Replaced ${count} occurrences in ${params.path}`
254
+ }],
255
+ details: {
256
+ path: params.path,
257
+ replacements: count
258
+ }
259
+ };
260
+ }
261
+ const occurrences = countOccurrences(content, params.oldText);
262
+ if (occurrences === 0) throw new Error(`Could not find the exact text in ${params.path}. Make sure your oldText matches exactly, including whitespace and indentation.`);
263
+ if (occurrences > 1) throw new Error(`Found ${occurrences} occurrences of the text in ${params.path}. Provide more surrounding context to make the match unique, or use replaceAll.`);
264
+ const newContent = content.replace(params.oldText, params.newText);
265
+ await env.writeFile(params.path, newContent);
266
+ return {
267
+ content: [{
268
+ type: "text",
269
+ text: `Successfully edited ${params.path}`
270
+ }],
271
+ details: { path: params.path }
272
+ };
273
+ }
274
+ };
275
+ }
276
+ function createBashTool(env) {
277
+ return {
278
+ name: "bash",
279
+ label: "Run Command",
280
+ description: "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.",
281
+ parameters: Type.Object({
282
+ command: Type.String({ description: "Bash command to execute" }),
283
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" }))
284
+ }),
285
+ async execute(_toolCallId, params, signal) {
286
+ throwIfAborted(signal);
287
+ return formatBashResult(await env.exec(params.command), params.command);
288
+ }
289
+ };
290
+ }
291
+ function formatBashResult(result, command) {
292
+ const { text: output } = truncateTail((result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(), MAX_READ_LINES, MAX_READ_BYTES);
293
+ if (result.exitCode !== 0) throw new Error(`${output}\n\nCommand exited with code ${result.exitCode}`);
294
+ return {
295
+ content: [{
296
+ type: "text",
297
+ text: output || "(no output)"
298
+ }],
299
+ details: {
300
+ command,
301
+ exitCode: result.exitCode
302
+ }
303
+ };
304
+ }
305
+ function createGrepTool(env) {
306
+ return {
307
+ name: "grep",
308
+ label: "Search Files",
309
+ description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.",
310
+ parameters: Type.Object({
311
+ pattern: Type.String({ description: "Search pattern (regex)" }),
312
+ path: Type.Optional(Type.String({ description: "Directory or file to search (default: .)" })),
313
+ include: Type.Optional(Type.String({ description: "Glob filter, e.g. \"*.ts\"" }))
314
+ }),
315
+ async execute(_toolCallId, params, signal) {
316
+ throwIfAborted(signal);
317
+ const searchPath = params.path || ".";
318
+ let cmd = `grep -rn "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
319
+ if (params.include) cmd = `grep -rn --include="${escapeShellArg(params.include)}" "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
320
+ const result = await env.exec(cmd);
321
+ if (result.exitCode === 1 && !result.stdout.trim()) return {
322
+ content: [{
323
+ type: "text",
324
+ text: "No matches found."
325
+ }],
326
+ details: { matchCount: 0 }
327
+ };
328
+ if (result.exitCode > 1) throw new Error(`grep failed: ${result.stderr}`);
329
+ const lines = result.stdout.trim().split("\n");
330
+ let finalOutput = lines.slice(0, MAX_GREP_MATCHES).map((line) => line.length > MAX_GREP_LINE_LENGTH ? line.slice(0, MAX_GREP_LINE_LENGTH) + "..." : line).join("\n");
331
+ if (lines.length > MAX_GREP_MATCHES) finalOutput += `\n\n[Showing ${MAX_GREP_MATCHES} of ${lines.length} matches. Narrow your search.]`;
332
+ return {
333
+ content: [{
334
+ type: "text",
335
+ text: finalOutput
336
+ }],
337
+ details: { matchCount: Math.min(lines.length, MAX_GREP_MATCHES) }
338
+ };
339
+ }
340
+ };
341
+ }
342
+ function createGlobTool(env) {
343
+ return {
344
+ name: "glob",
345
+ label: "Find Files",
346
+ description: "Find files by glob pattern. Returns matching file paths.",
347
+ parameters: Type.Object({
348
+ pattern: Type.String({ description: "Glob pattern, e.g. \"**/*.ts\"" }),
349
+ path: Type.Optional(Type.String({ description: "Directory to search in (default: .)" }))
350
+ }),
351
+ async execute(_toolCallId, params, signal) {
352
+ throwIfAborted(signal);
353
+ const cmd = `find ${escapeShellArg(params.path || ".")} -type f -name "${escapeShellArg(params.pattern)}" 2>/dev/null | head -${MAX_GLOB_RESULTS}`;
354
+ const result = await env.exec(cmd);
355
+ if (result.exitCode !== 0 && !result.stdout.trim()) return {
356
+ content: [{
357
+ type: "text",
358
+ text: "No files found matching pattern."
359
+ }],
360
+ details: { matchCount: 0 }
361
+ };
362
+ const paths = result.stdout.trim().split("\n").filter(Boolean);
363
+ if (paths.length === 0) return {
364
+ content: [{
365
+ type: "text",
366
+ text: "No files found matching pattern."
367
+ }],
368
+ details: { matchCount: 0 }
369
+ };
370
+ return {
371
+ content: [{
372
+ type: "text",
373
+ text: paths.join("\n")
374
+ }],
375
+ details: { matchCount: paths.length }
376
+ };
377
+ }
378
+ };
379
+ }
380
+ function throwIfAborted(signal) {
381
+ if (signal?.aborted) throw new Error("Operation aborted");
382
+ }
383
+ function countOccurrences(str, substr) {
384
+ let count = 0;
385
+ let pos = str.indexOf(substr, 0);
386
+ while (pos !== -1) {
387
+ count++;
388
+ pos = str.indexOf(substr, pos + substr.length);
389
+ }
390
+ return count;
391
+ }
392
+ function escapeShellArg(arg) {
393
+ return arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
394
+ }
395
+ function truncateHead(lines, maxLines, maxBytes) {
396
+ let result = "";
397
+ let lineCount = 0;
398
+ let wasTruncated = false;
399
+ for (const line of lines) {
400
+ if (lineCount >= maxLines) {
401
+ wasTruncated = true;
402
+ break;
403
+ }
404
+ const next = lineCount === 0 ? line : "\n" + line;
405
+ if (result.length + next.length > maxBytes) {
406
+ wasTruncated = true;
407
+ break;
408
+ }
409
+ result += next;
410
+ lineCount++;
411
+ }
412
+ return {
413
+ text: result,
414
+ wasTruncated
415
+ };
416
+ }
417
+ function truncateTail(text, maxLines, maxBytes) {
418
+ const lines = text.split("\n");
419
+ if (lines.length <= maxLines && text.length <= maxBytes) return {
420
+ text,
421
+ wasTruncated: false
422
+ };
423
+ let result = lines.slice(-maxLines).join("\n");
424
+ if (result.length > maxBytes) result = result.slice(-maxBytes);
425
+ return {
426
+ text: result,
427
+ wasTruncated: true
428
+ };
429
+ }
430
+
431
+ //#endregion
432
+ export { parseFrontmatterFile as a, loadSkillByPath as i, createTools as n, discoverSessionContext as r, BUILTIN_TOOL_NAMES as t };
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, b as SessionEnv, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, l as CommandSupport, m as FlueSession, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-C97_qJ21.mjs";
1
+ import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, b as SessionEnv, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, l as CommandSupport, m as FlueSession, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-BZPltYah.mjs";
2
2
  import { Type } from "@mariozechner/pi-ai";
3
3
 
4
4
  //#region src/client.d.ts
@@ -21,10 +21,5 @@ interface FlueContextInternal extends FlueContext {
21
21
  setEventCallback(callback: FlueEventCallback | undefined): void;
22
22
  }
23
23
  declare function createFlueContext(config: FlueContextConfig): FlueContextInternal;
24
- declare function defineCommand(name: string, execute: (args: string[]) => Promise<{
25
- stdout: string;
26
- stderr: string;
27
- exitCode: number;
28
- }>): Command;
29
24
  //#endregion
30
- export { type BashLike, type Command, type CommandSupport, type FileStat, type FlueContext, FlueContextConfig, FlueContextInternal, type FlueEvent, type FlueEventCallback, type FlueSession, type PromptOptions, type PromptResponse, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type SkillOptions, type TaskOptions, type ToolDef, Type, createFlueContext, defineCommand };
25
+ export { type BashLike, type Command, type CommandSupport, type FileStat, type FlueContext, FlueContextConfig, FlueContextInternal, type FlueEvent, type FlueEventCallback, type FlueSession, type PromptOptions, type PromptResponse, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type SkillOptions, type TaskOptions, type ToolDef, Type, createFlueContext };
package/dist/client.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { n as Session, o as discoverSessionContext } from "./session-0gnaB_aY.mjs";
1
+ import { r as discoverSessionContext } from "./agent-BYG0nVbQ.mjs";
2
+ import { n as Session } from "./session-CiAMTsLZ.mjs";
2
3
  import { bashToSessionEnv } from "./sandbox.mjs";
3
4
  import { Type } from "@mariozechner/pi-ai";
4
5
 
@@ -31,7 +32,7 @@ function createFlueContext(config) {
31
32
  skills: localContext.skills,
32
33
  model: sessionModel
33
34
  };
34
- return new Session(config.sessionId, sessionConfig, env, store, savedData, currentEventCallback);
35
+ return new Session(config.sessionId, sessionConfig, env, store, savedData, currentEventCallback, options?.commands);
35
36
  },
36
37
  setEventCallback(callback) {
37
38
  currentEventCallback = callback;
@@ -53,12 +54,6 @@ async function resolveSessionEnv(sessionId, sandbox, config) {
53
54
  }
54
55
  return sandbox.createSessionEnv({ sessionId });
55
56
  }
56
- function defineCommand(name, execute) {
57
- return {
58
- name,
59
- execute
60
- };
61
- }
62
57
 
63
58
  //#endregion
64
- export { Type, createFlueContext, defineCommand };
59
+ export { Type, createFlueContext };
@@ -1,4 +1,5 @@
1
- import { S as SessionStore, b as SessionEnv } from "../types-C97_qJ21.mjs";
1
+ import { S as SessionStore, b as SessionEnv, s as Command } from "../types-BZPltYah.mjs";
2
+ import { t as CommandExecutor } from "../command-helpers-BPcSV93o.mjs";
2
3
 
3
4
  //#region src/cloudflare/virtual-sandbox.d.ts
4
5
  interface VirtualSandboxOptions {
@@ -8,6 +9,9 @@ interface VirtualSandboxOptions {
8
9
  declare function getVirtualSandbox(): Promise<any>;
9
10
  declare function getVirtualSandbox(bucket: unknown, options?: VirtualSandboxOptions): Promise<any>;
10
11
  //#endregion
12
+ //#region src/cloudflare/define-command.d.ts
13
+ declare function defineCommand(name: string, execute: CommandExecutor): Command;
14
+ //#endregion
11
15
  //#region src/cloudflare/cf-sandbox.d.ts
12
16
  declare function cfSandboxToSessionEnv(sandbox: any, cwd?: string): Promise<SessionEnv>;
13
17
  //#endregion
@@ -33,4 +37,4 @@ declare function setCloudflareContext(ctx: CloudflareContext): void;
33
37
  declare function getCloudflareContext(): CloudflareContext;
34
38
  declare function clearCloudflareContext(): void;
35
39
  //#endregion
36
- export { type CloudflareContext, type VirtualSandboxOptions, cfSandboxToSessionEnv, clearCloudflareContext, getCloudflareContext, getVirtualSandbox, setCloudflareContext, store };
40
+ export { type CloudflareContext, type VirtualSandboxOptions, cfSandboxToSessionEnv, clearCloudflareContext, defineCommand, getCloudflareContext, getVirtualSandbox, setCloudflareContext, store };
@@ -1,5 +1,7 @@
1
- import "../session-0gnaB_aY.mjs";
1
+ import "../agent-BYG0nVbQ.mjs";
2
+ import "../session-CiAMTsLZ.mjs";
2
3
  import { createSandboxSessionEnv } from "../sandbox.mjs";
4
+ import { t as normalizeExecutor } from "../command-helpers-CxRhK1my.mjs";
3
5
  import { Workspace, WorkspaceFileSystem } from "@cloudflare/shell";
4
6
 
5
7
  //#region src/cloudflare/context.ts
@@ -104,6 +106,28 @@ async function getVirtualSandbox(bucket, options) {
104
106
  });
105
107
  }
106
108
 
109
+ //#endregion
110
+ //#region src/cloudflare/define-command.ts
111
+ /**
112
+ * Cloudflare-specific `defineCommand`. Function form only — Workers cannot
113
+ * spawn host processes, so there is no pass-through sugar. The user supplies
114
+ * an executor (typically `fetch`-based or SDK-based) and benefits from
115
+ * return-shape normalization plus automatic throw-catching.
116
+ *
117
+ * ```ts
118
+ * const issues = defineCommand('issues', async (args) => {
119
+ * const res = await fetch(`https://api.github.com/...`);
120
+ * return { stdout: await res.text() };
121
+ * });
122
+ * ```
123
+ */
124
+ function defineCommand(name, execute) {
125
+ return {
126
+ name,
127
+ execute: normalizeExecutor(execute)
128
+ };
129
+ }
130
+
107
131
  //#endregion
108
132
  //#region src/cloudflare/cf-sandbox.ts
109
133
  /** Wraps a @cloudflare/sandbox instance (from getSandbox()) into SessionEnv. */
@@ -203,4 +227,4 @@ function store() {
203
227
  }
204
228
 
205
229
  //#endregion
206
- export { cfSandboxToSessionEnv, clearCloudflareContext, getCloudflareContext, getVirtualSandbox, setCloudflareContext, store };
230
+ export { cfSandboxToSessionEnv, clearCloudflareContext, defineCommand, getCloudflareContext, getVirtualSandbox, setCloudflareContext, store };
@@ -0,0 +1,21 @@
1
+ import { w as ShellResult } from "./types-BZPltYah.mjs";
2
+
3
+ //#region src/command-helpers.d.ts
4
+ /**
5
+ * Loose return shape accepted from user-supplied command executors. All forms
6
+ * are normalized to a full `ShellResult` by `normalizeExecutor()`.
7
+ */
8
+ type CommandExecutorResult = ShellResult | {
9
+ stdout?: string;
10
+ stderr?: string;
11
+ exitCode?: number;
12
+ } | string | void;
13
+ /**
14
+ * User-supplied command executor. Can return a full `ShellResult`, a partial
15
+ * `{ stdout?, stderr?, exitCode? }` object, a bare string (treated as stdout),
16
+ * or void (empty success). Thrown errors are caught and converted to an
17
+ * `exitCode`-bearing `ShellResult` — no `try`/`catch` needed at the call site.
18
+ */
19
+ type CommandExecutor = (args: string[]) => Promise<CommandExecutorResult>;
20
+ //#endregion
21
+ export { CommandExecutor as t };
@@ -0,0 +1,37 @@
1
+ //#region src/command-helpers.ts
2
+ /**
3
+ * Wrap a user-supplied `CommandExecutor` to always resolve with a full
4
+ * `ShellResult`. Applies loose-return normalization and catches throws.
5
+ */
6
+ function normalizeExecutor(executor) {
7
+ return async (args) => {
8
+ try {
9
+ const raw = await executor(args);
10
+ if (raw === void 0 || raw === null) return {
11
+ stdout: "",
12
+ stderr: "",
13
+ exitCode: 0
14
+ };
15
+ if (typeof raw === "string") return {
16
+ stdout: raw,
17
+ stderr: "",
18
+ exitCode: 0
19
+ };
20
+ return {
21
+ stdout: raw.stdout ?? "",
22
+ stderr: raw.stderr ?? "",
23
+ exitCode: raw.exitCode ?? 0
24
+ };
25
+ } catch (err) {
26
+ const e = err ?? {};
27
+ return {
28
+ stdout: typeof e.stdout === "string" ? e.stdout : "",
29
+ stderr: typeof e.stderr === "string" ? e.stderr : String(err),
30
+ exitCode: typeof e.code === "number" ? e.code : 1
31
+ };
32
+ }
33
+ };
34
+ }
35
+
36
+ //#endregion
37
+ export { normalizeExecutor as t };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,5 @@
1
- import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, T as Skill, _ as Role, a as BuildOptions, b as SessionEnv, c as CommandDef, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, i as BuildContext, l as CommandSupport, m as FlueSession, n as AgentInfo, o as BuildPlugin, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-C97_qJ21.mjs";
2
- import { FlueContextConfig, FlueContextInternal, createFlueContext } from "./client.mjs";
1
+ import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, T as Skill, _ as Role, a as BuildOptions, b as SessionEnv, c as CommandDef, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, i as BuildContext, l as CommandSupport, m as FlueSession, n as AgentInfo, o as BuildPlugin, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-BZPltYah.mjs";
3
2
  import { AgentTool } from "@mariozechner/pi-agent-core";
4
- import "valibot";
5
3
 
6
4
  //#region src/build.d.ts
7
5
  /**
@@ -10,17 +8,8 @@ import "valibot";
10
8
  */
11
9
  declare function build(options: BuildOptions): Promise<void>;
12
10
  //#endregion
13
- //#region src/session.d.ts
14
- /** In-memory session store. Sessions persist for the lifetime of the process. */
15
- declare class InMemorySessionStore implements SessionStore {
16
- private store;
17
- save(id: string, data: SessionData): Promise<void>;
18
- load(id: string): Promise<SessionData | null>;
19
- delete(id: string): Promise<void>;
20
- }
21
- //#endregion
22
11
  //#region src/agent.d.ts
23
12
  declare const BUILTIN_TOOL_NAMES: Set<string>;
24
13
  declare function createTools(env: SessionEnv): AgentTool<any>[];
25
14
  //#endregion
26
- export { type AgentConfig, type AgentInfo, BUILTIN_TOOL_NAMES, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, type CommandSupport, type FileStat, type FlueContext, type FlueContextConfig, type FlueContextInternal, type FlueEvent, type FlueEventCallback, type FlueSession, InMemorySessionStore, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, build, createFlueContext, createTools };
15
+ export { type AgentConfig, type AgentInfo, BUILTIN_TOOL_NAMES, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, type CommandSupport, type FileStat, type FlueContext, type FlueEvent, type FlueEventCallback, type FlueSession, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, build, createTools };