@flue/sdk 0.1.1 → 0.1.2

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.
@@ -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-C8tsaK1j.mjs";
2
2
  import { Type } from "@mariozechner/pi-ai";
3
3
 
4
4
  //#region src/client.d.ts
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-BRLCNVG1.mjs";
2
3
  import { bashToSessionEnv } from "./sandbox.mjs";
3
4
  import { Type } from "@mariozechner/pi-ai";
4
5
 
@@ -1,4 +1,4 @@
1
- import { S as SessionStore, b as SessionEnv } from "../types-C97_qJ21.mjs";
1
+ import { S as SessionStore, b as SessionEnv } from "../types-C8tsaK1j.mjs";
2
2
 
3
3
  //#region src/cloudflare/virtual-sandbox.d.ts
4
4
  interface VirtualSandboxOptions {
@@ -1,4 +1,5 @@
1
- import "../session-0gnaB_aY.mjs";
1
+ import "../agent-BYG0nVbQ.mjs";
2
+ import "../session-BRLCNVG1.mjs";
2
3
  import { createSandboxSessionEnv } from "../sandbox.mjs";
3
4
  import { Workspace, WorkspaceFileSystem } from "@cloudflare/shell";
4
5
 
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-C8tsaK1j.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 };
package/dist/index.mjs CHANGED
@@ -1,5 +1,4 @@
1
- import { a as createTools, i as BUILTIN_TOOL_NAMES, s as parseFrontmatterFile, t as InMemorySessionStore } from "./session-0gnaB_aY.mjs";
2
- import { createFlueContext } from "./client.mjs";
1
+ import { a as parseFrontmatterFile, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BYG0nVbQ.mjs";
3
2
  import * as esbuild from "esbuild";
4
3
  import * as fs from "node:fs";
5
4
  import * as path from "node:path";
@@ -9,43 +8,30 @@ import { packageUpSync } from "package-up";
9
8
  var CloudflarePlugin = class {
10
9
  name = "cloudflare";
11
10
  generateEntryPoint(ctx) {
12
- const { agents, roles, resolveSDKImport } = ctx;
11
+ const { agents, roles } = ctx;
13
12
  const rolesJson = JSON.stringify(roles);
14
13
  const webhookAgents = agents.filter((a) => a.triggers.webhook);
15
- const agentImports = agents.map((a) => {
16
- return `import ${agentVarName$1(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
17
- }).join("\n");
18
- const manifest = JSON.stringify({ agents: agents.map((a) => ({
19
- name: a.name,
20
- triggers: a.triggers
21
- })) }, null, 2);
22
- const agentClasses = webhookAgents.map((a) => {
23
- const className = agentClassName(a.name);
24
- const handlerVar = agentVarName$1(a.name);
25
- return `export class ${className} extends Agent {
26
- async onRequest(request) {
27
- return handleAgentRequest(request, this, ${JSON.stringify(a.name)}, ${handlerVar});
28
- }
29
- }`;
30
- }).join("\n\n");
31
14
  return `
32
15
  // Auto-generated by @flue/sdk build (cloudflare)
33
16
  import { Agent, routeAgentRequest } from 'agents';
34
17
  import { Bash, InMemoryFs } from 'just-bash';
35
18
  import { getModel } from '@mariozechner/pi-ai';
36
- import { createFlueContext } from '${resolveSDKImport("client")}';
37
- import { InMemorySessionStore } from '${resolveSDKImport("session")}';
38
- import { bashToSessionEnv } from '${resolveSDKImport("sandbox")}';
19
+ import { createFlueContext, InMemorySessionStore, bashToSessionEnv } from '@flue/sdk/internal';
39
20
  import { setCloudflareContext, clearCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
40
21
 
41
- ${agentImports}
22
+ ${agents.map((a) => {
23
+ return `import ${agentVarName$1(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
24
+ }).join("\n")}
42
25
 
43
26
  // ─── Config ─────────────────────────────────────────────────────────────────
44
27
 
45
28
  const roles = ${rolesJson};
46
29
  const skills = {};
47
30
  const systemPrompt = '';
48
- const manifest = ${manifest};
31
+ const manifest = ${JSON.stringify({ agents: agents.map((a) => ({
32
+ name: a.name,
33
+ triggers: a.triggers
34
+ })) }, null, 2)};
49
35
 
50
36
  // ─── Infrastructure ─────────────────────────────────────────────────────────
51
37
 
@@ -279,7 +265,15 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
279
265
 
280
266
  // ─── Per-Agent Durable Object Classes ──────────────────────────────────────
281
267
 
282
- ${agentClasses}
268
+ ${webhookAgents.map((a) => {
269
+ const className = agentClassName(a.name);
270
+ const handlerVar = agentVarName$1(a.name);
271
+ return `export class ${className} extends Agent {
272
+ async onRequest(request) {
273
+ return handleAgentRequest(request, this, ${JSON.stringify(a.name)}, ${handlerVar});
274
+ }
275
+ }`;
276
+ }).join("\n\n")}
283
277
 
284
278
  // Re-export Sandbox DO class for wrangler binding
285
279
  export { Sandbox } from '@cloudflare/sandbox';
@@ -384,18 +378,9 @@ function agentClassName(name) {
384
378
  var NodePlugin = class {
385
379
  name = "node";
386
380
  generateEntryPoint(ctx) {
387
- const { agents, roles, resolveSDKImport } = ctx;
381
+ const { agents, roles } = ctx;
388
382
  const rolesJson = JSON.stringify(roles);
389
383
  const webhookAgents = agents.filter((a) => a.triggers.webhook);
390
- const agentImports = agents.map((a) => {
391
- return `import ${agentVarName(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
392
- }).join("\n");
393
- const handlerMapEntries = agents.map((a) => ` ${JSON.stringify(a.name)}: ${agentVarName(a.name)},`).join("\n");
394
- const webhookNames = JSON.stringify(webhookAgents.map((a) => a.name));
395
- const manifest = JSON.stringify({ agents: agents.map((a) => ({
396
- name: a.name,
397
- triggers: a.triggers
398
- })) }, null, 2);
399
384
  return `
400
385
  // Auto-generated by @flue/sdk build (node)
401
386
  import { Hono } from 'hono';
@@ -403,12 +388,12 @@ import { streamSSE } from 'hono/streaming';
403
388
  import { serve } from '@hono/node-server';
404
389
  import { Bash, InMemoryFs, MountableFs, ReadWriteFs } from 'just-bash';
405
390
  import { getModel } from '@mariozechner/pi-ai';
406
- import { createFlueContext } from '${resolveSDKImport("client")}';
407
- import { InMemorySessionStore } from '${resolveSDKImport("session")}';
408
- import { bashToSessionEnv } from '${resolveSDKImport("sandbox")}';
391
+ import { createFlueContext, InMemorySessionStore, bashToSessionEnv } from '@flue/sdk/internal';
409
392
  import { randomUUID } from 'node:crypto';
410
393
 
411
- ${agentImports}
394
+ ${agents.map((a) => {
395
+ return `import ${agentVarName(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
396
+ }).join("\n")}
412
397
 
413
398
  // ─── Config ─────────────────────────────────────────────────────────────────
414
399
 
@@ -417,12 +402,22 @@ const roles = ${rolesJson};
417
402
  const systemPrompt = '';
418
403
 
419
404
  const handlers = {
420
- ${handlerMapEntries}
405
+ ${agents.map((a) => ` ${JSON.stringify(a.name)}: ${agentVarName(a.name)},`).join("\n")}
421
406
  };
422
407
 
423
- const webhookAgents = new Set(${webhookNames});
408
+ const webhookAgents = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
424
409
 
425
- const manifest = ${manifest};
410
+ // When the CLI starts this server via \`flue run\`, it sets FLUE_MODE=local.
411
+ // In local mode the HTTP route accepts any registered agent (including
412
+ // trigger-less CI-only agents). In any other mode the route is restricted to
413
+ // agents with \`webhook: true\`, preventing accidental public exposure of
414
+ // agents that the user only intended to invoke from their CI pipeline.
415
+ const isLocalMode = process.env.FLUE_MODE === 'local';
416
+
417
+ const manifest = ${JSON.stringify({ agents: agents.map((a) => ({
418
+ name: a.name,
419
+ triggers: a.triggers
420
+ })) }, null, 2)};
426
421
 
427
422
  // ─── Infrastructure ─────────────────────────────────────────────────────────
428
423
 
@@ -521,7 +516,7 @@ app.post('/agents/:name/:sessionId', async (c) => {
521
516
  if (!handlers[name]) {
522
517
  return c.json({ error: 'Agent not found' }, 404);
523
518
  }
524
- if (!webhookAgents.has(name)) {
519
+ if (!webhookAgents.has(name) && !isLocalMode) {
525
520
  return c.json({ error: 'Agent "' + name + '" is not web-accessible (no webhook trigger)' }, 404);
526
521
  }
527
522
 
@@ -600,7 +595,12 @@ const port = parseInt(process.env.PORT || '3000', 10);
600
595
 
601
596
  const server = serve({ fetch: app.fetch, port });
602
597
  console.log('[flue] Server listening on http://localhost:' + port);
603
- console.log('[flue] Available agents: ' + ${JSON.stringify(webhookAgents.map((a) => a.name).join(", "))});
598
+ if (isLocalMode) {
599
+ console.log('[flue] Mode: local (all agents invokable, including trigger-less)');
600
+ console.log('[flue] Available agents: ' + ${JSON.stringify(agents.map((a) => a.name).join(", "))});
601
+ } else {
602
+ console.log('[flue] Available agents: ' + ${JSON.stringify(webhookAgents.map((a) => a.name).join(", "))});
603
+ }
604
604
 
605
605
  process.on('SIGINT', () => { server.close(); process.exit(0); });
606
606
  process.on('SIGTERM', () => { server.close(); process.exit(0); });
@@ -631,13 +631,14 @@ async function build(options) {
631
631
  const roles = discoverRoles(agentDir);
632
632
  const agents = discoverAgents(agentDir);
633
633
  if (agents.length === 0) throw new Error(`No agents found in ${path.join(agentDir, ".flue/agents/")}`);
634
- for (const agent of agents) if (!(agent.triggers.webhook || agent.triggers.cron)) throw new Error(`[flue] Agent "${agent.name}" has no triggers configured. Add a triggers export to your agent file:\n\n export const triggers = { webhook: true };\n\nAvailable triggers: webhook (HTTP endpoint), cron (scheduled)`);
635
634
  const webhookAgents = agents.filter((a) => a.triggers.webhook);
636
635
  const cronAgents = agents.filter((a) => a.triggers.cron);
636
+ const triggerlessAgents = agents.filter((a) => !a.triggers.webhook && !a.triggers.cron);
637
637
  console.log(`[flue] Found ${Object.keys(roles).length} role(s): ${Object.keys(roles).join(", ") || "(none)"}`);
638
638
  console.log(`[flue] Found ${agents.length} agent(s): ${agents.map((a) => a.name).join(", ")}`);
639
639
  if (webhookAgents.length > 0) console.log(`[flue] Webhook agents: ${webhookAgents.map((a) => a.name).join(", ")}`);
640
640
  if (cronAgents.length > 0) console.log(`[flue] Cron agents (manifest only): ${cronAgents.map((a) => `${a.name} (${a.triggers.cron})`).join(", ")}`);
641
+ if (triggerlessAgents.length > 0) console.log(`[flue] CLI-only agents (no HTTP route in deployed build): ${triggerlessAgents.map((a) => a.name).join(", ")}`);
641
642
  console.log(`[flue] AGENTS.md and .agents/skills/ will be discovered at runtime from session cwd`);
642
643
  const distDir = path.join(agentDir, "dist");
643
644
  fs.mkdirSync(distDir, { recursive: true });
@@ -652,8 +653,7 @@ async function build(options) {
652
653
  agents,
653
654
  roles,
654
655
  agentDir,
655
- options,
656
- resolveSDKImport: resolveSDKImportFn
656
+ options
657
657
  };
658
658
  const serverCode = plugin.generateEntryPoint(ctx);
659
659
  const entryPath = path.join(distDir, "_entry_server.ts");
@@ -786,18 +786,6 @@ function getSDKDir() {
786
786
  return __dirname;
787
787
  }
788
788
  }
789
- function getSDKSrcDir() {
790
- const thisDir = getSDKDir();
791
- if (thisDir.endsWith("/dist") || thisDir.endsWith("\\dist")) {
792
- const srcDir = path.join(path.dirname(thisDir), "src");
793
- if (fs.existsSync(srcDir)) return srcDir;
794
- }
795
- return thisDir;
796
- }
797
- function resolveSDKImportFn(module) {
798
- const srcDir = getSDKSrcDir();
799
- return path.join(srcDir, `${module}.ts`).replace(/\\/g, "/");
800
- }
801
789
 
802
790
  //#endregion
803
- export { BUILTIN_TOOL_NAMES, InMemorySessionStore, build, createFlueContext, createTools };
791
+ export { BUILTIN_TOOL_NAMES, build, createTools };
@@ -0,0 +1,15 @@
1
+ import { S as SessionStore, y as SessionData } from "./types-C8tsaK1j.mjs";
2
+ import { FlueContextConfig, FlueContextInternal, createFlueContext } from "./client.mjs";
3
+ import { bashToSessionEnv } from "./sandbox.mjs";
4
+ import "valibot";
5
+
6
+ //#region src/session.d.ts
7
+ /** In-memory session store. Sessions persist for the lifetime of the process. */
8
+ declare class InMemorySessionStore implements SessionStore {
9
+ private store;
10
+ save(id: string, data: SessionData): Promise<void>;
11
+ load(id: string): Promise<SessionData | null>;
12
+ delete(id: string): Promise<void>;
13
+ }
14
+ //#endregion
15
+ export { type FlueContextConfig, type FlueContextInternal, InMemorySessionStore, bashToSessionEnv, createFlueContext };
@@ -0,0 +1,6 @@
1
+ import "./agent-BYG0nVbQ.mjs";
2
+ import { t as InMemorySessionStore } from "./session-BRLCNVG1.mjs";
3
+ import { bashToSessionEnv } from "./sandbox.mjs";
4
+ import { createFlueContext } from "./client.mjs";
5
+
6
+ export { InMemorySessionStore, bashToSessionEnv, createFlueContext };
@@ -1,4 +1,4 @@
1
- import { b as SessionEnv, c as CommandDef, r as BashLike, u as FileStat, v as SandboxFactory, w as ShellResult } from "./types-C97_qJ21.mjs";
1
+ import { b as SessionEnv, c as CommandDef, r as BashLike, u as FileStat, v as SandboxFactory, w as ShellResult } from "./types-C8tsaK1j.mjs";
2
2
 
3
3
  //#region src/sandbox.d.ts
4
4
  declare function bashToSessionEnv(bash: BashLike): SessionEnv;
package/dist/sandbox.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { r as normalizePath } from "./session-0gnaB_aY.mjs";
1
+ import "./agent-BYG0nVbQ.mjs";
2
+ import { r as normalizePath } from "./session-BRLCNVG1.mjs";
2
3
 
3
4
  //#region src/sandbox.ts
4
5
  function bashToSessionEnv(bash) {
@@ -1,437 +1,9 @@
1
+ import { i as loadSkillByPath, n as createTools, r as discoverSessionContext, t as BUILTIN_TOOL_NAMES } from "./agent-BYG0nVbQ.mjs";
2
+ import { completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
1
3
  import { Agent } from "@mariozechner/pi-agent-core";
2
- import { Type, completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
3
4
  import { toJsonSchema } from "@valibot/to-json-schema";
4
5
  import * as v from "valibot";
5
6
 
6
- //#region src/context.ts
7
- /** Parse optional YAML frontmatter (--- delimited). Basic `key: value` only. */
8
- function parseFrontmatterFile(content, defaultName) {
9
- const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
10
- if (!frontmatterMatch) return {
11
- name: defaultName,
12
- description: "",
13
- body: content.trim(),
14
- frontmatter: {}
15
- };
16
- const rawFrontmatter = frontmatterMatch[1] ?? "";
17
- const body = frontmatterMatch[2] ?? "";
18
- const frontmatter = {};
19
- for (const line of rawFrontmatter.split("\n")) {
20
- const match = line.match(/^(\w+):\s*(.+)$/);
21
- if (match?.[1] && match[2]) frontmatter[match[1]] = match[2].trim();
22
- }
23
- return {
24
- name: frontmatter.name || defaultName,
25
- description: frontmatter.description || "",
26
- body: body.trim(),
27
- frontmatter
28
- };
29
- }
30
- /** Read AGENTS.md (and CLAUDE.md if present) from a directory. Returns concatenated contents. */
31
- async function readAgentsMd(env, basePath) {
32
- const parts = [];
33
- for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
34
- const filePath = basePath.endsWith("/") ? basePath + filename : `${basePath}/${filename}`;
35
- if (await env.exists(filePath)) {
36
- const content = await env.readFile(filePath);
37
- parts.push(content.trim());
38
- }
39
- }
40
- return parts.join("\n\n");
41
- }
42
- /**
43
- * Load a skill directly by relative path under `.agents/skills/`.
44
- *
45
- * The path is taken as-is — no extension is auto-appended. Callers reference
46
- * the full filename, e.g. `'triage/reproduce.md'`. Returns `null` if the file
47
- * doesn't exist.
48
- *
49
- * Used as a fallback by `session.skill()` when the requested name doesn't match
50
- * a discovered skill's frontmatter `name:` field. Lets users organise skills as
51
- * a pack of sibling markdown files under one directory (orchestration SKILL.md
52
- * + stage files) without forcing each stage into its own `SKILL.md` subdirectory.
53
- */
54
- async function loadSkillByPath(env, basePath, relPath) {
55
- const filePath = `${basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`}/${relPath}`;
56
- if (!await env.exists(filePath)) return null;
57
- const parsed = parseFrontmatterFile(await env.readFile(filePath), relPath.replace(/\.(md|markdown)$/i, ""));
58
- return {
59
- name: parsed.name,
60
- description: parsed.description,
61
- instructions: parsed.body
62
- };
63
- }
64
- /** Discover skills from .agents/skills/<name>/SKILL.md under basePath. */
65
- async function discoverLocalSkills(env, basePath) {
66
- const skillsDir = basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`;
67
- if (!await env.exists(skillsDir)) return {};
68
- const skills = {};
69
- const entries = await env.readdir(skillsDir);
70
- for (const entry of entries) {
71
- const skillDir = `${skillsDir}/${entry}`;
72
- try {
73
- if (!(await env.stat(skillDir)).isDirectory) continue;
74
- } catch {
75
- continue;
76
- }
77
- const skillMdPath = `${skillDir}/SKILL.md`;
78
- if (!await env.exists(skillMdPath)) continue;
79
- const parsed = parseFrontmatterFile(await env.readFile(skillMdPath), entry);
80
- skills[parsed.name] = {
81
- name: parsed.name,
82
- description: parsed.description,
83
- instructions: parsed.body
84
- };
85
- }
86
- return skills;
87
- }
88
- function composeSystemPrompt(agentsMd, skills, env) {
89
- const parts = [];
90
- if (agentsMd) parts.push(agentsMd);
91
- const skillEntries = Object.values(skills);
92
- if (skillEntries.length > 0) {
93
- parts.push("", "## Available Skills", "");
94
- for (const skill of skillEntries) {
95
- const desc = skill.description ? ` - ${skill.description}` : "";
96
- parts.push(`- **${skill.name}**${desc}`);
97
- }
98
- }
99
- if (env) {
100
- const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
101
- weekday: "short",
102
- year: "numeric",
103
- month: "short",
104
- day: "numeric"
105
- });
106
- parts.push("", `Date: ${date}`);
107
- parts.push(`Working directory: ${env.cwd}`);
108
- if (env.directoryListing && env.directoryListing.length > 0) parts.push("", "Directory structure:", env.directoryListing.join("\n"));
109
- }
110
- return parts.join("\n");
111
- }
112
- /** Discover AGENTS.md, local skills, and directory listing from the session's cwd. */
113
- async function discoverSessionContext(env) {
114
- const cwd = env.cwd;
115
- const agentsMd = await readAgentsMd(env, cwd);
116
- const skills = await discoverLocalSkills(env, cwd);
117
- let directoryListing;
118
- try {
119
- directoryListing = await env.readdir(cwd);
120
- } catch {}
121
- return {
122
- systemPrompt: composeSystemPrompt(agentsMd, skills, {
123
- cwd,
124
- directoryListing
125
- }),
126
- skills
127
- };
128
- }
129
-
130
- //#endregion
131
- //#region src/agent.ts
132
- const MAX_READ_LINES = 2e3;
133
- const MAX_READ_BYTES = 50 * 1024;
134
- const MAX_GREP_MATCHES = 100;
135
- const MAX_GREP_LINE_LENGTH = 500;
136
- const MAX_GLOB_RESULTS = 1e3;
137
- const BUILTIN_TOOL_NAMES = new Set([
138
- "read",
139
- "write",
140
- "edit",
141
- "bash",
142
- "grep",
143
- "glob"
144
- ]);
145
- function createTools(env) {
146
- return [
147
- createReadTool(env),
148
- createWriteTool(env),
149
- createEditTool(env),
150
- createBashTool(env),
151
- createGrepTool(env),
152
- createGlobTool(env)
153
- ];
154
- }
155
- function createReadTool(env) {
156
- return {
157
- name: "read",
158
- label: "Read File",
159
- 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.",
160
- parameters: Type.Object({
161
- path: Type.String({ description: "Path to the file to read" }),
162
- offset: Type.Optional(Type.Number({ description: "Line number to start from (1-indexed)" })),
163
- limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
164
- }),
165
- async execute(_toolCallId, params, signal) {
166
- throwIfAborted(signal);
167
- try {
168
- if ((await env.stat(params.path)).isDirectory) {
169
- const entries = await env.readdir(params.path);
170
- return {
171
- content: [{
172
- type: "text",
173
- text: entries.join("\n") || "(empty directory)"
174
- }],
175
- details: {
176
- path: params.path,
177
- isDirectory: true,
178
- entries: entries.length
179
- }
180
- };
181
- }
182
- } catch {}
183
- const allLines = (await env.readFile(params.path)).split("\n");
184
- const startLine = params.offset ? Math.max(0, params.offset - 1) : 0;
185
- if (startLine >= allLines.length) throw new Error(`Offset ${params.offset} is beyond end of file (${allLines.length} lines total)`);
186
- const endLine = params.limit ? startLine + params.limit : allLines.length;
187
- const { text: truncatedText, wasTruncated } = truncateHead(allLines.slice(startLine, endLine), MAX_READ_LINES, MAX_READ_BYTES);
188
- let output = truncatedText;
189
- if (wasTruncated) {
190
- const shownEnd = startLine + truncatedText.split("\n").length;
191
- output += `\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${allLines.length}. Use offset=${shownEnd + 1} to continue.]`;
192
- }
193
- return {
194
- content: [{
195
- type: "text",
196
- text: output
197
- }],
198
- details: {
199
- path: params.path,
200
- lines: allLines.length
201
- }
202
- };
203
- }
204
- };
205
- }
206
- function createWriteTool(env) {
207
- return {
208
- name: "write",
209
- label: "Write File",
210
- description: "Write content to a file. Creates the file and parent directories if they do not exist.",
211
- parameters: Type.Object({
212
- path: Type.String({ description: "Path to the file to write" }),
213
- content: Type.String({ description: "Content to write to the file" })
214
- }),
215
- async execute(_toolCallId, params, signal) {
216
- throwIfAborted(signal);
217
- const resolved = env.resolvePath(params.path);
218
- const dir = resolved.replace(/\/[^/]*$/, "");
219
- if (dir && dir !== resolved) await env.mkdir(dir, { recursive: true });
220
- await env.writeFile(resolved, params.content);
221
- return {
222
- content: [{
223
- type: "text",
224
- text: `Successfully wrote ${params.content.length} bytes to ${params.path}`
225
- }],
226
- details: {
227
- path: params.path,
228
- size: params.content.length
229
- }
230
- };
231
- }
232
- };
233
- }
234
- function createEditTool(env) {
235
- return {
236
- name: "edit",
237
- label: "Edit File",
238
- description: "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.",
239
- parameters: Type.Object({
240
- path: Type.String({ description: "Path to the file to edit" }),
241
- oldText: Type.String({ description: "Exact text to find (must be unique)" }),
242
- newText: Type.String({ description: "Replacement text" }),
243
- replaceAll: Type.Optional(Type.Boolean({ description: "Replace all occurrences" }))
244
- }),
245
- async execute(_toolCallId, params, signal) {
246
- throwIfAborted(signal);
247
- const content = await env.readFile(params.path);
248
- if (params.replaceAll) {
249
- const newContent = content.replaceAll(params.oldText, params.newText);
250
- if (newContent === content) throw new Error(`Could not find the text in ${params.path}. No changes made.`);
251
- await env.writeFile(params.path, newContent);
252
- const count = content.split(params.oldText).length - 1;
253
- return {
254
- content: [{
255
- type: "text",
256
- text: `Replaced ${count} occurrences in ${params.path}`
257
- }],
258
- details: {
259
- path: params.path,
260
- replacements: count
261
- }
262
- };
263
- }
264
- const occurrences = countOccurrences(content, params.oldText);
265
- 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.`);
266
- 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.`);
267
- const newContent = content.replace(params.oldText, params.newText);
268
- await env.writeFile(params.path, newContent);
269
- return {
270
- content: [{
271
- type: "text",
272
- text: `Successfully edited ${params.path}`
273
- }],
274
- details: { path: params.path }
275
- };
276
- }
277
- };
278
- }
279
- function createBashTool(env) {
280
- return {
281
- name: "bash",
282
- label: "Run Command",
283
- description: "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.",
284
- parameters: Type.Object({
285
- command: Type.String({ description: "Bash command to execute" }),
286
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" }))
287
- }),
288
- async execute(_toolCallId, params, signal) {
289
- throwIfAborted(signal);
290
- return formatBashResult(await env.exec(params.command), params.command);
291
- }
292
- };
293
- }
294
- function formatBashResult(result, command) {
295
- const { text: output } = truncateTail((result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(), MAX_READ_LINES, MAX_READ_BYTES);
296
- if (result.exitCode !== 0) throw new Error(`${output}\n\nCommand exited with code ${result.exitCode}`);
297
- return {
298
- content: [{
299
- type: "text",
300
- text: output || "(no output)"
301
- }],
302
- details: {
303
- command,
304
- exitCode: result.exitCode
305
- }
306
- };
307
- }
308
- function createGrepTool(env) {
309
- return {
310
- name: "grep",
311
- label: "Search Files",
312
- description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.",
313
- parameters: Type.Object({
314
- pattern: Type.String({ description: "Search pattern (regex)" }),
315
- path: Type.Optional(Type.String({ description: "Directory or file to search (default: .)" })),
316
- include: Type.Optional(Type.String({ description: "Glob filter, e.g. \"*.ts\"" }))
317
- }),
318
- async execute(_toolCallId, params, signal) {
319
- throwIfAborted(signal);
320
- const searchPath = params.path || ".";
321
- let cmd = `grep -rn "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
322
- if (params.include) cmd = `grep -rn --include="${escapeShellArg(params.include)}" "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
323
- const result = await env.exec(cmd);
324
- if (result.exitCode === 1 && !result.stdout.trim()) return {
325
- content: [{
326
- type: "text",
327
- text: "No matches found."
328
- }],
329
- details: { matchCount: 0 }
330
- };
331
- if (result.exitCode > 1) throw new Error(`grep failed: ${result.stderr}`);
332
- const lines = result.stdout.trim().split("\n");
333
- 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");
334
- if (lines.length > MAX_GREP_MATCHES) finalOutput += `\n\n[Showing ${MAX_GREP_MATCHES} of ${lines.length} matches. Narrow your search.]`;
335
- return {
336
- content: [{
337
- type: "text",
338
- text: finalOutput
339
- }],
340
- details: { matchCount: Math.min(lines.length, MAX_GREP_MATCHES) }
341
- };
342
- }
343
- };
344
- }
345
- function createGlobTool(env) {
346
- return {
347
- name: "glob",
348
- label: "Find Files",
349
- description: "Find files by glob pattern. Returns matching file paths.",
350
- parameters: Type.Object({
351
- pattern: Type.String({ description: "Glob pattern, e.g. \"**/*.ts\"" }),
352
- path: Type.Optional(Type.String({ description: "Directory to search in (default: .)" }))
353
- }),
354
- async execute(_toolCallId, params, signal) {
355
- throwIfAborted(signal);
356
- const cmd = `find ${escapeShellArg(params.path || ".")} -type f -name "${escapeShellArg(params.pattern)}" 2>/dev/null | head -${MAX_GLOB_RESULTS}`;
357
- const result = await env.exec(cmd);
358
- if (result.exitCode !== 0 && !result.stdout.trim()) return {
359
- content: [{
360
- type: "text",
361
- text: "No files found matching pattern."
362
- }],
363
- details: { matchCount: 0 }
364
- };
365
- const paths = result.stdout.trim().split("\n").filter(Boolean);
366
- if (paths.length === 0) return {
367
- content: [{
368
- type: "text",
369
- text: "No files found matching pattern."
370
- }],
371
- details: { matchCount: 0 }
372
- };
373
- return {
374
- content: [{
375
- type: "text",
376
- text: paths.join("\n")
377
- }],
378
- details: { matchCount: paths.length }
379
- };
380
- }
381
- };
382
- }
383
- function throwIfAborted(signal) {
384
- if (signal?.aborted) throw new Error("Operation aborted");
385
- }
386
- function countOccurrences(str, substr) {
387
- let count = 0;
388
- let pos = str.indexOf(substr, 0);
389
- while (pos !== -1) {
390
- count++;
391
- pos = str.indexOf(substr, pos + substr.length);
392
- }
393
- return count;
394
- }
395
- function escapeShellArg(arg) {
396
- return arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
397
- }
398
- function truncateHead(lines, maxLines, maxBytes) {
399
- let result = "";
400
- let lineCount = 0;
401
- let wasTruncated = false;
402
- for (const line of lines) {
403
- if (lineCount >= maxLines) {
404
- wasTruncated = true;
405
- break;
406
- }
407
- const next = lineCount === 0 ? line : "\n" + line;
408
- if (result.length + next.length > maxBytes) {
409
- wasTruncated = true;
410
- break;
411
- }
412
- result += next;
413
- lineCount++;
414
- }
415
- return {
416
- text: result,
417
- wasTruncated
418
- };
419
- }
420
- function truncateTail(text, maxLines, maxBytes) {
421
- const lines = text.split("\n");
422
- if (lines.length <= maxLines && text.length <= maxBytes) return {
423
- text,
424
- wasTruncated: false
425
- };
426
- let result = lines.slice(-maxLines).join("\n");
427
- if (result.length > maxBytes) result = result.slice(-maxBytes);
428
- return {
429
- text: result,
430
- wasTruncated: true
431
- };
432
- }
433
-
434
- //#endregion
435
7
  //#region src/compaction.ts
436
8
  const DEFAULT_COMPACTION_SETTINGS = {
437
9
  enabled: true,
@@ -1350,4 +922,4 @@ function normalizePath(p) {
1350
922
  }
1351
923
 
1352
924
  //#endregion
1353
- export { createTools as a, BUILTIN_TOOL_NAMES as i, Session as n, discoverSessionContext as o, normalizePath as r, parseFrontmatterFile as s, InMemorySessionStore as t };
925
+ export { Session as n, normalizePath as r, InMemorySessionStore as t };
@@ -1,5 +1,5 @@
1
- import { AgentMessage } from "@mariozechner/pi-agent-core";
2
1
  import { Model, TSchema } from "@mariozechner/pi-ai";
2
+ import { AgentMessage } from "@mariozechner/pi-agent-core";
3
3
  import * as v from "valibot";
4
4
 
5
5
  //#region src/types.d.ts
@@ -317,7 +317,6 @@ interface BuildContext {
317
317
  roles: Record<string, Role>;
318
318
  agentDir: string;
319
319
  options: BuildOptions;
320
- resolveSDKImport: (module: string) => string;
321
320
  }
322
321
  /** Controls the build output format for a target platform. */
323
322
  interface BuildPlugin {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flue/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
@@ -16,6 +16,10 @@
16
16
  "types": "./dist/sandbox.d.mts",
17
17
  "import": "./dist/sandbox.mjs"
18
18
  },
19
+ "./internal": {
20
+ "types": "./dist/internal.d.mts",
21
+ "import": "./dist/internal.mjs"
22
+ },
19
23
  "./cloudflare": {
20
24
  "types": "./dist/cloudflare/index.d.mts",
21
25
  "import": "./dist/cloudflare/index.mjs"