@flue/sdk 0.1.3 → 0.2.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.
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/index.d.mts +2 -2
- package/dist/{command-helpers-BPcSV93o.d.mts → command-helpers-C8SHLdaA.d.mts} +1 -1
- package/dist/index.d.mts +22 -2
- package/dist/index.mjs +292 -82
- package/dist/internal.d.mts +1 -1
- package/dist/node/index.d.mts +2 -2
- package/dist/sandbox.d.mts +1 -1
- package/dist/{types-BZPltYah.d.mts → types-C0nqbu6Z.d.mts} +17 -2
- package/package.json +2 -1
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-
|
|
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-C0nqbu6Z.mjs";
|
|
2
2
|
import { Type } from "@mariozechner/pi-ai";
|
|
3
3
|
|
|
4
4
|
//#region src/client.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SessionStore, b as SessionEnv, s as Command } from "../types-
|
|
2
|
-
import { t as CommandExecutor } from "../command-helpers-
|
|
1
|
+
import { S as SessionStore, b as SessionEnv, s as Command } from "../types-C0nqbu6Z.mjs";
|
|
2
|
+
import { t as CommandExecutor } from "../command-helpers-C8SHLdaA.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/cloudflare/virtual-sandbox.d.ts
|
|
5
5
|
interface VirtualSandboxOptions {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
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-
|
|
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-C0nqbu6Z.mjs";
|
|
2
2
|
import { AgentTool } from "@mariozechner/pi-agent-core";
|
|
3
3
|
|
|
4
4
|
//#region src/build.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Build a workspace into a deployable artifact.
|
|
7
|
+
*
|
|
8
|
+
* `options.workspaceDir` is treated as an explicit workspace root — the directory
|
|
9
|
+
* directly containing agents/ and roles/. No .flue/ waterfall is performed here;
|
|
10
|
+
* callers that want waterfall behavior (e.g. the CLI when --workspace is omitted)
|
|
11
|
+
* should use `resolveWorkspaceFromCwd` first.
|
|
12
|
+
*
|
|
7
13
|
* AGENTS.md and .agents/skills/ are NOT bundled — discovered at runtime from session cwd.
|
|
8
14
|
*/
|
|
9
15
|
declare function build(options: BuildOptions): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a Flue workspace directory from the current working directory,
|
|
18
|
+
* using the two-layout convention. Intended for the CLI when `--workspace` is
|
|
19
|
+
* not provided — callers that pass an explicit workspace path should skip this
|
|
20
|
+
* and pass the path straight to `build()`.
|
|
21
|
+
*
|
|
22
|
+
* Two supported layouts, checked in order:
|
|
23
|
+
* 1. `<cwd>/.flue/` — use this when Flue is embedded in an existing project.
|
|
24
|
+
* 2. `<cwd>/` — use this when the project itself is the Flue workspace.
|
|
25
|
+
*
|
|
26
|
+
* If `.flue/` exists, it wins unconditionally — no mixing with the bare layout.
|
|
27
|
+
* Returns null if neither is present so the caller can produce a helpful error.
|
|
28
|
+
*/
|
|
29
|
+
declare function resolveWorkspaceFromCwd(cwd: string): string | null;
|
|
10
30
|
//#endregion
|
|
11
31
|
//#region src/agent.d.ts
|
|
12
32
|
declare const BUILTIN_TOOL_NAMES: Set<string>;
|
|
13
33
|
declare function createTools(env: SessionEnv): AgentTool<any>[];
|
|
14
34
|
//#endregion
|
|
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 };
|
|
35
|
+
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, resolveWorkspaceFromCwd };
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,189 @@ import * as esbuild from "esbuild";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { packageUpSync } from "package-up";
|
|
6
|
+
import { parse } from "jsonc-parser";
|
|
6
7
|
|
|
8
|
+
//#region src/cloudflare-wrangler-merge.ts
|
|
9
|
+
/**
|
|
10
|
+
* Merge Flue's Cloudflare additions into the user's wrangler config.
|
|
11
|
+
*
|
|
12
|
+
* Philosophy: the user's wrangler.jsonc is the source of truth. Flue contributes
|
|
13
|
+
* the pieces it owns (the Worker entrypoint, its per-agent Durable Object
|
|
14
|
+
* bindings, the Sandbox DO, the migration tag) and leaves everything else
|
|
15
|
+
* untouched. The merged result is written to `dist/wrangler.jsonc` so the
|
|
16
|
+
* deployed Worker sees both.
|
|
17
|
+
*
|
|
18
|
+
* We use `jsonc-parser` (the same library wrangler uses internally) for
|
|
19
|
+
* reading. TOML is intentionally unsupported — Cloudflare itself recommends
|
|
20
|
+
* wrangler.jsonc for new projects, and supporting both formats here would
|
|
21
|
+
* double the surface area for no real benefit. Users with wrangler.toml get a
|
|
22
|
+
* clear error directing them to convert.
|
|
23
|
+
*/
|
|
24
|
+
/** Minimum compatibility_date Flue supports. */
|
|
25
|
+
const MIN_COMPATIBILITY_DATE = "2024-04-03";
|
|
26
|
+
/** compatibility_flag Flue requires for pi-ai's process.env-based API key lookup. */
|
|
27
|
+
const REQUIRED_COMPAT_FLAG = "nodejs_compat";
|
|
28
|
+
/**
|
|
29
|
+
* Read the user's wrangler config from `outputDir` if present.
|
|
30
|
+
*
|
|
31
|
+
* Looks for `wrangler.jsonc` then `wrangler.json` (in that order — jsonc is the
|
|
32
|
+
* recommended format). If a `wrangler.toml` is present instead, throws with a
|
|
33
|
+
* clear conversion hint. Returns an empty config if no file is present.
|
|
34
|
+
*/
|
|
35
|
+
function readUserWranglerConfig(outputDir) {
|
|
36
|
+
const jsoncPath = path.join(outputDir, "wrangler.jsonc");
|
|
37
|
+
const jsonPath = path.join(outputDir, "wrangler.json");
|
|
38
|
+
const tomlPath = path.join(outputDir, "wrangler.toml");
|
|
39
|
+
const foundPath = fs.existsSync(jsoncPath) ? jsoncPath : fs.existsSync(jsonPath) ? jsonPath : null;
|
|
40
|
+
if (!foundPath) {
|
|
41
|
+
if (fs.existsSync(tomlPath)) throw new Error(`[flue] Found wrangler.toml at ${tomlPath}. Flue only supports wrangler.jsonc (the format Cloudflare recommends for new projects). Convert your config to wrangler.jsonc — you can use any online TOML-to-JSON converter, or copy the fields over by hand.`);
|
|
42
|
+
return {
|
|
43
|
+
config: {},
|
|
44
|
+
path: null
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const source = fs.readFileSync(foundPath, "utf-8");
|
|
48
|
+
const errors = [];
|
|
49
|
+
const parsed = parse(source, errors, { allowTrailingComma: true });
|
|
50
|
+
if (errors.length > 0) {
|
|
51
|
+
const summary = errors.slice(0, 3).map((e) => `offset ${e.offset}: error code ${e.error}`).join("; ");
|
|
52
|
+
throw new Error(`[flue] Failed to parse ${foundPath}: ${summary}. Please fix syntax errors in your wrangler config before building.`);
|
|
53
|
+
}
|
|
54
|
+
if (parsed === void 0 || parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`[flue] ${foundPath} did not contain a JSON object at the top level. A wrangler config must be an object.`);
|
|
55
|
+
return {
|
|
56
|
+
config: parsed,
|
|
57
|
+
path: foundPath
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate that the user's wrangler config meets Flue's minimum runtime
|
|
62
|
+
* requirements. Throws a clear error describing the fix if it doesn't.
|
|
63
|
+
*
|
|
64
|
+
* We're intentionally strict here rather than silently massaging bad configs —
|
|
65
|
+
* the failure modes when these are wrong (missing nodejs_compat, old
|
|
66
|
+
* compat_date) produce confusing runtime errors, and surfacing the problem at
|
|
67
|
+
* build time is much friendlier.
|
|
68
|
+
*/
|
|
69
|
+
function validateUserWranglerConfig(config) {
|
|
70
|
+
if (Array.isArray(config.compatibility_flags)) {
|
|
71
|
+
if (!config.compatibility_flags.includes(REQUIRED_COMPAT_FLAG)) throw new Error(`[flue] Your wrangler config's "compatibility_flags" is missing "${REQUIRED_COMPAT_FLAG}". Flue relies on it at runtime (e.g. for API key resolution via process.env). Add "${REQUIRED_COMPAT_FLAG}" to the list.`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof config.compatibility_date === "string") {
|
|
74
|
+
const userDate = config.compatibility_date;
|
|
75
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(userDate)) throw new Error(`[flue] Your wrangler config's "compatibility_date" ("${userDate}") is not in YYYY-MM-DD format.`);
|
|
76
|
+
if (userDate < MIN_COMPATIBILITY_DATE) throw new Error(`[flue] Your wrangler config's "compatibility_date" is "${userDate}". Flue requires at least "${MIN_COMPATIBILITY_DATE}" for SQLite-backed Durable Object support and nodejs_compat v2. Bump the date (set it to today unless you have a specific reason).`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Produce the merged wrangler config: start from the user's, layer Flue's
|
|
81
|
+
* contributions on top. Pure function — caller handles reading and writing.
|
|
82
|
+
*/
|
|
83
|
+
function mergeFlueAdditions(userConfig, additions) {
|
|
84
|
+
const merged = { ...userConfig };
|
|
85
|
+
merged.main = additions.main;
|
|
86
|
+
if (typeof merged.name !== "string" || merged.name.length === 0) merged.name = additions.defaultName;
|
|
87
|
+
if (typeof merged.compatibility_date !== "string") merged.compatibility_date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
88
|
+
const existingFlags = Array.isArray(merged.compatibility_flags) ? merged.compatibility_flags.filter((f) => typeof f === "string") : [];
|
|
89
|
+
if (!existingFlags.includes(REQUIRED_COMPAT_FLAG)) existingFlags.push(REQUIRED_COMPAT_FLAG);
|
|
90
|
+
merged.compatibility_flags = existingFlags;
|
|
91
|
+
const existingDo = typeof merged.durable_objects === "object" && merged.durable_objects !== null ? merged.durable_objects : {};
|
|
92
|
+
const existingBindings = Array.isArray(existingDo.bindings) ? existingDo.bindings : [];
|
|
93
|
+
const existingBindingNames = new Set(existingBindings.filter((b) => typeof b === "object" && b !== null).map((b) => b.name).filter((n) => typeof n === "string"));
|
|
94
|
+
const flueBindingsToAdd = additions.doBindings.filter((b) => !existingBindingNames.has(b.name));
|
|
95
|
+
merged.durable_objects = {
|
|
96
|
+
...existingDo,
|
|
97
|
+
bindings: [...existingBindings, ...flueBindingsToAdd]
|
|
98
|
+
};
|
|
99
|
+
const existingMigrations = Array.isArray(merged.migrations) ? merged.migrations : [];
|
|
100
|
+
const existingMigrationTags = new Set(existingMigrations.filter((m) => typeof m === "object" && m !== null).map((m) => m.tag).filter((t) => typeof t === "string"));
|
|
101
|
+
const migrationsOut = [...existingMigrations];
|
|
102
|
+
if (!existingMigrationTags.has(additions.migration.tag)) migrationsOut.push(additions.migration);
|
|
103
|
+
merged.migrations = migrationsOut;
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Return the list of `class_name`s declared in the user's wrangler
|
|
108
|
+
* `durable_objects.bindings` that contain the literal substring `Sandbox`
|
|
109
|
+
* (case-sensitive).
|
|
110
|
+
*
|
|
111
|
+
* This is Flue's convention for wiring `@cloudflare/sandbox`: any DO binding
|
|
112
|
+
* whose class name contains `Sandbox` triggers an automatic re-export in the
|
|
113
|
+
* generated Worker entry:
|
|
114
|
+
*
|
|
115
|
+
* export { Sandbox as <class_name> } from '@cloudflare/sandbox';
|
|
116
|
+
*
|
|
117
|
+
* The alias lets users pick arbitrary class names (e.g. `PyBoxSandbox`,
|
|
118
|
+
* `SupportSandbox`) while still pointing at the single class shipped by the
|
|
119
|
+
* `@cloudflare/sandbox` package. Each distinct `class_name` can be paired with
|
|
120
|
+
* a different container image in the user's `containers[]` config.
|
|
121
|
+
*
|
|
122
|
+
* Returns unique, sorted class names. Non-object bindings or bindings without
|
|
123
|
+
* a string `class_name` are ignored.
|
|
124
|
+
*/
|
|
125
|
+
function detectSandboxBindings(userConfig) {
|
|
126
|
+
const doObj = userConfig.durable_objects;
|
|
127
|
+
if (typeof doObj !== "object" || doObj === null) return [];
|
|
128
|
+
const bindings = doObj.bindings;
|
|
129
|
+
if (!Array.isArray(bindings)) return [];
|
|
130
|
+
const found = /* @__PURE__ */ new Set();
|
|
131
|
+
for (const entry of bindings) {
|
|
132
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
133
|
+
const className = entry.class_name;
|
|
134
|
+
if (typeof className !== "string") continue;
|
|
135
|
+
if (className.includes("Sandbox")) found.add(className);
|
|
136
|
+
}
|
|
137
|
+
return Array.from(found).sort();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* When the user has declared one or more `Sandbox`-named DO bindings, verify
|
|
141
|
+
* that `@cloudflare/sandbox` is declared in the nearest package.json. Surfaces
|
|
142
|
+
* a friendly, actionable error at build time rather than letting esbuild emit
|
|
143
|
+
* a confusing module-resolution failure.
|
|
144
|
+
*
|
|
145
|
+
* The check is lenient: if no package.json can be located or parsed, we skip
|
|
146
|
+
* silently and let esbuild's own error path take over. This avoids false
|
|
147
|
+
* positives in unusual project layouts.
|
|
148
|
+
*/
|
|
149
|
+
function assertSandboxPackageInstalled(sandboxClassNames, searchDirs) {
|
|
150
|
+
if (sandboxClassNames.length === 0) return;
|
|
151
|
+
for (const dir of searchDirs) {
|
|
152
|
+
let current = dir;
|
|
153
|
+
while (current !== path.dirname(current)) {
|
|
154
|
+
const pkgPath = path.join(current, "package.json");
|
|
155
|
+
if (fs.existsSync(pkgPath)) try {
|
|
156
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
157
|
+
if ("@cloudflare/sandbox" in {
|
|
158
|
+
...pkg.dependencies ?? {},
|
|
159
|
+
...pkg.devDependencies ?? {},
|
|
160
|
+
...pkg.peerDependencies ?? {},
|
|
161
|
+
...pkg.optionalDependencies ?? {}
|
|
162
|
+
}) return;
|
|
163
|
+
} catch {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
current = path.dirname(current);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`[flue] Your wrangler config declares DO binding(s) whose class_name contains "Sandbox" (${sandboxClassNames.join(", ")}), but @cloudflare/sandbox is not in your package.json. Install it: \`npm install @cloudflare/sandbox\`.`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Write the wrangler deploy-redirect file at `<outputDir>/.wrangler/deploy/config.json`
|
|
173
|
+
* so that `wrangler deploy` run from `outputDir` automatically picks up the
|
|
174
|
+
* generated `dist/wrangler.jsonc`.
|
|
175
|
+
*
|
|
176
|
+
* This is wrangler's own native redirection mechanism (the same one Astro's
|
|
177
|
+
* Cloudflare adapter uses). We only write the file if one doesn't already
|
|
178
|
+
* exist — if the user has set one up, respect their intent.
|
|
179
|
+
*/
|
|
180
|
+
function writeDeployRedirectIfMissing(outputDir) {
|
|
181
|
+
const redirectDir = path.join(outputDir, ".wrangler", "deploy");
|
|
182
|
+
const redirectPath = path.join(redirectDir, "config.json");
|
|
183
|
+
if (fs.existsSync(redirectPath)) return;
|
|
184
|
+
fs.mkdirSync(redirectDir, { recursive: true });
|
|
185
|
+
fs.writeFileSync(redirectPath, JSON.stringify({ configPath: "../../dist/wrangler.jsonc" }, null, 2) + "\n", "utf-8");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
7
189
|
//#region src/build-plugin-cloudflare.ts
|
|
8
190
|
var CloudflarePlugin = class {
|
|
9
191
|
name = "cloudflare";
|
|
@@ -11,6 +193,23 @@ var CloudflarePlugin = class {
|
|
|
11
193
|
const { agents, roles } = ctx;
|
|
12
194
|
const rolesJson = JSON.stringify(roles);
|
|
13
195
|
const webhookAgents = agents.filter((a) => a.triggers.webhook);
|
|
196
|
+
const agentImports = agents.map((a) => {
|
|
197
|
+
return `import ${agentVarName$1(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
|
|
198
|
+
}).join("\n");
|
|
199
|
+
const manifest = JSON.stringify({ agents: agents.map((a) => ({
|
|
200
|
+
name: a.name,
|
|
201
|
+
triggers: a.triggers
|
|
202
|
+
})) }, null, 2);
|
|
203
|
+
const agentClasses = webhookAgents.map((a) => {
|
|
204
|
+
const className = agentClassName(a.name);
|
|
205
|
+
const handlerVar = agentVarName$1(a.name);
|
|
206
|
+
return `export class ${className} extends Agent {
|
|
207
|
+
async onRequest(request) {
|
|
208
|
+
return handleAgentRequest(request, this, ${JSON.stringify(a.name)}, ${handlerVar});
|
|
209
|
+
}
|
|
210
|
+
}`;
|
|
211
|
+
}).join("\n\n");
|
|
212
|
+
const { config: userConfig } = readUserWranglerConfig(ctx.outputDir);
|
|
14
213
|
return `
|
|
15
214
|
// Auto-generated by @flue/sdk build (cloudflare)
|
|
16
215
|
import { Agent, routeAgentRequest } from 'agents';
|
|
@@ -19,19 +218,14 @@ import { getModel } from '@mariozechner/pi-ai';
|
|
|
19
218
|
import { createFlueContext, InMemorySessionStore, bashToSessionEnv } from '@flue/sdk/internal';
|
|
20
219
|
import { setCloudflareContext, clearCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
|
|
21
220
|
|
|
22
|
-
${
|
|
23
|
-
return `import ${agentVarName$1(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
|
|
24
|
-
}).join("\n")}
|
|
221
|
+
${agentImports}
|
|
25
222
|
|
|
26
223
|
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
27
224
|
|
|
28
225
|
const roles = ${rolesJson};
|
|
29
226
|
const skills = {};
|
|
30
227
|
const systemPrompt = '';
|
|
31
|
-
const manifest = ${
|
|
32
|
-
name: a.name,
|
|
33
|
-
triggers: a.triggers
|
|
34
|
-
})) }, null, 2)};
|
|
228
|
+
const manifest = ${manifest};
|
|
35
229
|
|
|
36
230
|
// ─── Infrastructure ─────────────────────────────────────────────────────────
|
|
37
231
|
|
|
@@ -81,13 +275,15 @@ async function createLocalEnv() {
|
|
|
81
275
|
throw new Error(
|
|
82
276
|
"[flue] 'local' sandbox is not supported on Cloudflare Workers. " +
|
|
83
277
|
"Use the default empty sandbox, pass a custom Bash instance, " +
|
|
84
|
-
"or
|
|
278
|
+
"or pass a sandbox instance (from any SDK — e.g. @cloudflare/sandbox " +
|
|
279
|
+
"or a Flue connector) to init({ sandbox })."
|
|
85
280
|
);
|
|
86
281
|
}
|
|
87
282
|
|
|
88
283
|
/**
|
|
89
|
-
* Detect and wrap
|
|
90
|
-
* Returns SessionEnv if the
|
|
284
|
+
* Detect and wrap external sandbox instances (e.g. from @cloudflare/sandbox's
|
|
285
|
+
* getSandbox()). Returns SessionEnv if the object quacks like a container
|
|
286
|
+
* sandbox, null otherwise.
|
|
91
287
|
*/
|
|
92
288
|
function resolveSandbox(sandbox) {
|
|
93
289
|
if (
|
|
@@ -265,18 +461,15 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
265
461
|
|
|
266
462
|
// ─── Per-Agent Durable Object Classes ──────────────────────────────────────
|
|
267
463
|
|
|
268
|
-
${
|
|
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")}
|
|
464
|
+
${agentClasses}
|
|
277
465
|
|
|
278
|
-
//
|
|
279
|
-
|
|
466
|
+
// ─── User-declared Sandbox re-exports ──────────────────────────────────────
|
|
467
|
+
// One line per DO binding in the user's wrangler.jsonc whose class_name
|
|
468
|
+
// contains "Sandbox". Flue aliases the single \`Sandbox\` class shipped by
|
|
469
|
+
// \`@cloudflare/sandbox\` so each user-chosen class_name resolves at the
|
|
470
|
+
// bundle's top level. The binding + container image configuration is owned
|
|
471
|
+
// by the user's wrangler.jsonc.
|
|
472
|
+
${detectSandboxBindings(userConfig).map((name) => `export { Sandbox as ${name} } from '@cloudflare/sandbox';`).join("\n")}
|
|
280
473
|
|
|
281
474
|
// ─── Worker Fetch Handler ───────────────────────────────────────────────────
|
|
282
475
|
|
|
@@ -311,51 +504,42 @@ export default {
|
|
|
311
504
|
esbuildOptions(_ctx) {
|
|
312
505
|
return {
|
|
313
506
|
target: "esnext",
|
|
314
|
-
external: [
|
|
507
|
+
external: [
|
|
508
|
+
"node:*",
|
|
509
|
+
"cloudflare:*",
|
|
510
|
+
"node-liblzma",
|
|
511
|
+
"@mongodb-js/zstd"
|
|
512
|
+
]
|
|
315
513
|
};
|
|
316
514
|
}
|
|
317
515
|
additionalOutputs(ctx) {
|
|
318
516
|
const outputs = {};
|
|
319
|
-
const
|
|
517
|
+
const flueBindings = ctx.agents.filter((a) => a.triggers.webhook).map((a) => ({
|
|
320
518
|
class_name: agentClassName(a.name),
|
|
321
519
|
name: agentClassName(a.name)
|
|
322
|
-
}))
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const allSqliteClasses = allBindings.map((b) => b.class_name);
|
|
327
|
-
const workerName = ctx.agentDir.split("/").pop() ?? "flue-agents";
|
|
328
|
-
outputs["wrangler.jsonc"] = JSON.stringify({
|
|
329
|
-
$schema: "https://workers.cloudflare.com/schema/wrangler.json",
|
|
330
|
-
name: workerName,
|
|
520
|
+
}));
|
|
521
|
+
const flueSqliteClasses = flueBindings.map((b) => b.class_name);
|
|
522
|
+
const additions = {
|
|
523
|
+
defaultName: ctx.outputDir.split("/").pop() ?? "flue-agents",
|
|
331
524
|
main: "server.mjs",
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
" python3 python3-pip \\",
|
|
351
|
-
" && rm -rf /var/lib/apt/lists/*",
|
|
352
|
-
"",
|
|
353
|
-
"WORKDIR /workspace",
|
|
354
|
-
"",
|
|
355
|
-
"# Keep container alive",
|
|
356
|
-
"CMD [\"sleep\", \"infinity\"]",
|
|
357
|
-
""
|
|
358
|
-
].join("\n");
|
|
525
|
+
doBindings: flueBindings,
|
|
526
|
+
migration: {
|
|
527
|
+
tag: "flue-v1",
|
|
528
|
+
new_sqlite_classes: flueSqliteClasses
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const { config: userConfig, path: userConfigPath } = readUserWranglerConfig(ctx.outputDir);
|
|
532
|
+
if (userConfigPath) console.log(`[flue] Merging with user wrangler config: ${userConfigPath}`);
|
|
533
|
+
validateUserWranglerConfig(userConfig);
|
|
534
|
+
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
535
|
+
if (sandboxClassNames.length > 0) {
|
|
536
|
+
assertSandboxPackageInstalled(sandboxClassNames, [ctx.outputDir, ctx.workspaceDir]);
|
|
537
|
+
for (const className of sandboxClassNames) console.log(`[flue] Detected Sandbox-named DO binding "${className}" — re-exporting from @cloudflare/sandbox.`);
|
|
538
|
+
}
|
|
539
|
+
const merged = mergeFlueAdditions(userConfig, additions);
|
|
540
|
+
if (typeof merged.$schema !== "string") merged.$schema = "https://workers.cloudflare.com/schema/wrangler.json";
|
|
541
|
+
outputs["wrangler.jsonc"] = JSON.stringify(merged, null, 2);
|
|
542
|
+
writeDeployRedirectIfMissing(ctx.outputDir);
|
|
359
543
|
return outputs;
|
|
360
544
|
}
|
|
361
545
|
};
|
|
@@ -609,7 +793,8 @@ process.on('SIGTERM', () => { server.close(); process.exit(0); });
|
|
|
609
793
|
esbuildOptions(_ctx) {
|
|
610
794
|
return {
|
|
611
795
|
platform: "node",
|
|
612
|
-
target: "node22"
|
|
796
|
+
target: "node22",
|
|
797
|
+
external: ["node-liblzma", "@mongodb-js/zstd"]
|
|
613
798
|
};
|
|
614
799
|
}
|
|
615
800
|
};
|
|
@@ -621,16 +806,24 @@ function agentVarName(name) {
|
|
|
621
806
|
//#region src/build.ts
|
|
622
807
|
/**
|
|
623
808
|
* Build a workspace into a deployable artifact.
|
|
809
|
+
*
|
|
810
|
+
* `options.workspaceDir` is treated as an explicit workspace root — the directory
|
|
811
|
+
* directly containing agents/ and roles/. No .flue/ waterfall is performed here;
|
|
812
|
+
* callers that want waterfall behavior (e.g. the CLI when --workspace is omitted)
|
|
813
|
+
* should use `resolveWorkspaceFromCwd` first.
|
|
814
|
+
*
|
|
624
815
|
* AGENTS.md and .agents/skills/ are NOT bundled — discovered at runtime from session cwd.
|
|
625
816
|
*/
|
|
626
817
|
async function build(options) {
|
|
627
|
-
const
|
|
818
|
+
const workspaceDir = path.resolve(options.workspaceDir);
|
|
819
|
+
const outputDir = path.resolve(options.outputDir);
|
|
628
820
|
const plugin = resolvePlugin(options);
|
|
629
|
-
console.log(`[flue] Building workspace: ${
|
|
821
|
+
console.log(`[flue] Building workspace: ${workspaceDir}`);
|
|
822
|
+
console.log(`[flue] Output: ${outputDir}/dist`);
|
|
630
823
|
console.log(`[flue] Target: ${plugin.name}`);
|
|
631
|
-
const roles = discoverRoles(
|
|
632
|
-
const agents = discoverAgents(
|
|
633
|
-
if (agents.length === 0) throw new Error(`No
|
|
824
|
+
const roles = discoverRoles(workspaceDir);
|
|
825
|
+
const agents = discoverAgents(workspaceDir);
|
|
826
|
+
if (agents.length === 0) throw new Error(`[flue] No agent files found.\n\nExpected at: ${path.join(workspaceDir, "agents")}/\nAdd at least one agent file (e.g. hello.ts).`);
|
|
634
827
|
const webhookAgents = agents.filter((a) => a.triggers.webhook);
|
|
635
828
|
const cronAgents = agents.filter((a) => a.triggers.cron);
|
|
636
829
|
const triggerlessAgents = agents.filter((a) => !a.triggers.webhook && !a.triggers.cron);
|
|
@@ -640,7 +833,7 @@ async function build(options) {
|
|
|
640
833
|
if (cronAgents.length > 0) console.log(`[flue] Cron agents (manifest only): ${cronAgents.map((a) => `${a.name} (${a.triggers.cron})`).join(", ")}`);
|
|
641
834
|
if (triggerlessAgents.length > 0) console.log(`[flue] CLI-only agents (no HTTP route in deployed build): ${triggerlessAgents.map((a) => a.name).join(", ")}`);
|
|
642
835
|
console.log(`[flue] AGENTS.md and .agents/skills/ will be discovered at runtime from session cwd`);
|
|
643
|
-
const distDir = path.join(
|
|
836
|
+
const distDir = path.join(outputDir, "dist");
|
|
644
837
|
fs.mkdirSync(distDir, { recursive: true });
|
|
645
838
|
const manifest = { agents: agents.map((a) => ({
|
|
646
839
|
name: a.name,
|
|
@@ -652,7 +845,8 @@ async function build(options) {
|
|
|
652
845
|
const ctx = {
|
|
653
846
|
agents,
|
|
654
847
|
roles,
|
|
655
|
-
|
|
848
|
+
workspaceDir,
|
|
849
|
+
outputDir,
|
|
656
850
|
options
|
|
657
851
|
};
|
|
658
852
|
const serverCode = plugin.generateEntryPoint(ctx);
|
|
@@ -660,9 +854,9 @@ async function build(options) {
|
|
|
660
854
|
const outPath = path.join(distDir, "server.mjs");
|
|
661
855
|
fs.writeFileSync(entryPath, serverCode, "utf-8");
|
|
662
856
|
try {
|
|
663
|
-
const nodePathsSet = collectNodePaths(
|
|
857
|
+
const nodePathsSet = collectNodePaths(workspaceDir);
|
|
664
858
|
const { external: pluginExternal = [], ...pluginEsbuildOpts } = plugin.esbuildOptions(ctx);
|
|
665
|
-
const userExternals = getUserExternals(
|
|
859
|
+
const userExternals = getUserExternals(workspaceDir);
|
|
666
860
|
await esbuild.build({
|
|
667
861
|
entryPoints: [entryPath],
|
|
668
862
|
bundle: true,
|
|
@@ -705,8 +899,27 @@ function resolvePlugin(options) {
|
|
|
705
899
|
default: throw new Error(`[flue] Unknown target: "${options.target}". Supported targets: node, cloudflare`);
|
|
706
900
|
}
|
|
707
901
|
}
|
|
708
|
-
|
|
709
|
-
|
|
902
|
+
/**
|
|
903
|
+
* Resolve a Flue workspace directory from the current working directory,
|
|
904
|
+
* using the two-layout convention. Intended for the CLI when `--workspace` is
|
|
905
|
+
* not provided — callers that pass an explicit workspace path should skip this
|
|
906
|
+
* and pass the path straight to `build()`.
|
|
907
|
+
*
|
|
908
|
+
* Two supported layouts, checked in order:
|
|
909
|
+
* 1. `<cwd>/.flue/` — use this when Flue is embedded in an existing project.
|
|
910
|
+
* 2. `<cwd>/` — use this when the project itself is the Flue workspace.
|
|
911
|
+
*
|
|
912
|
+
* If `.flue/` exists, it wins unconditionally — no mixing with the bare layout.
|
|
913
|
+
* Returns null if neither is present so the caller can produce a helpful error.
|
|
914
|
+
*/
|
|
915
|
+
function resolveWorkspaceFromCwd(cwd) {
|
|
916
|
+
const dotFlue = path.join(cwd, ".flue");
|
|
917
|
+
if (fs.existsSync(dotFlue)) return dotFlue;
|
|
918
|
+
if (fs.existsSync(path.join(cwd, "agents"))) return cwd;
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
function discoverRoles(workspaceRoot) {
|
|
922
|
+
const rolesDir = path.join(workspaceRoot, "roles");
|
|
710
923
|
if (!fs.existsSync(rolesDir)) return {};
|
|
711
924
|
const roles = {};
|
|
712
925
|
for (const entry of fs.readdirSync(rolesDir)) {
|
|
@@ -724,12 +937,9 @@ function discoverRoles(agentDir) {
|
|
|
724
937
|
}
|
|
725
938
|
return roles;
|
|
726
939
|
}
|
|
727
|
-
function discoverAgents(
|
|
728
|
-
|
|
729
|
-
if (!fs.existsSync(agentsDir))
|
|
730
|
-
agentsDir = path.join(agentDir, ".flue", "workflows");
|
|
731
|
-
if (!fs.existsSync(agentsDir)) return [];
|
|
732
|
-
}
|
|
940
|
+
function discoverAgents(workspaceRoot) {
|
|
941
|
+
const agentsDir = path.join(workspaceRoot, "agents");
|
|
942
|
+
if (!fs.existsSync(agentsDir)) return [];
|
|
733
943
|
return fs.readdirSync(agentsDir).filter((f) => /\.(ts|js|mts|mjs)$/.test(f)).map((f) => {
|
|
734
944
|
const filePath = path.join(agentsDir, f);
|
|
735
945
|
const triggers = parseTriggers(filePath);
|
|
@@ -753,8 +963,8 @@ function parseTriggers(filePath) {
|
|
|
753
963
|
return result;
|
|
754
964
|
}
|
|
755
965
|
/** Externalize user's direct deps (bare name + subpath wildcard). */
|
|
756
|
-
function getUserExternals(
|
|
757
|
-
const pkgPath = packageUpSync({ cwd:
|
|
966
|
+
function getUserExternals(workspaceDir) {
|
|
967
|
+
const pkgPath = packageUpSync({ cwd: workspaceDir });
|
|
758
968
|
if (!pkgPath) return [];
|
|
759
969
|
try {
|
|
760
970
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -767,9 +977,9 @@ function getUserExternals(agentDir) {
|
|
|
767
977
|
return [];
|
|
768
978
|
}
|
|
769
979
|
}
|
|
770
|
-
function collectNodePaths(
|
|
980
|
+
function collectNodePaths(workspaceDir) {
|
|
771
981
|
const nodePathsSet = /* @__PURE__ */ new Set();
|
|
772
|
-
for (const startDir of [
|
|
982
|
+
for (const startDir of [workspaceDir, getSDKDir()]) {
|
|
773
983
|
let dir = startDir;
|
|
774
984
|
while (dir !== path.dirname(dir)) {
|
|
775
985
|
const nm = path.join(dir, "node_modules");
|
|
@@ -788,4 +998,4 @@ function getSDKDir() {
|
|
|
788
998
|
}
|
|
789
999
|
|
|
790
1000
|
//#endregion
|
|
791
|
-
export { BUILTIN_TOOL_NAMES, build, createTools };
|
|
1001
|
+
export { BUILTIN_TOOL_NAMES, build, createTools, resolveWorkspaceFromCwd };
|
package/dist/internal.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SessionStore, y as SessionData } from "./types-
|
|
1
|
+
import { S as SessionStore, y as SessionData } from "./types-C0nqbu6Z.mjs";
|
|
2
2
|
import { FlueContextConfig, FlueContextInternal, createFlueContext } from "./client.mjs";
|
|
3
3
|
import { bashToSessionEnv } from "./sandbox.mjs";
|
|
4
4
|
import "valibot";
|
package/dist/node/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { s as Command } from "../types-
|
|
2
|
-
import { t as CommandExecutor } from "../command-helpers-
|
|
1
|
+
import { s as Command } from "../types-C0nqbu6Z.mjs";
|
|
2
|
+
import { t as CommandExecutor } from "../command-helpers-C8SHLdaA.mjs";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
|
|
5
5
|
//#region src/node/define-command.d.ts
|
package/dist/sandbox.d.mts
CHANGED
|
@@ -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-
|
|
1
|
+
import { b as SessionEnv, c as CommandDef, r as BashLike, u as FileStat, v as SandboxFactory, w as ShellResult } from "./types-C0nqbu6Z.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/sandbox.d.ts
|
|
4
4
|
declare function bashToSessionEnv(bash: BashLike): SessionEnv;
|
|
@@ -322,7 +322,10 @@ interface AgentInfo {
|
|
|
322
322
|
interface BuildContext {
|
|
323
323
|
agents: AgentInfo[];
|
|
324
324
|
roles: Record<string, Role>;
|
|
325
|
-
|
|
325
|
+
/** The workspace root: the directory directly containing agents/ and roles/. */
|
|
326
|
+
workspaceDir: string;
|
|
327
|
+
/** Where dist/ is written. Typically the project root, independent of workspaceDir. */
|
|
328
|
+
outputDir: string;
|
|
326
329
|
options: BuildOptions;
|
|
327
330
|
}
|
|
328
331
|
/** Controls the build output format for a target platform. */
|
|
@@ -334,7 +337,19 @@ interface BuildPlugin {
|
|
|
334
337
|
additionalOutputs?(ctx: BuildContext): Record<string, string>;
|
|
335
338
|
}
|
|
336
339
|
interface BuildOptions {
|
|
337
|
-
|
|
340
|
+
/**
|
|
341
|
+
* The workspace directory: the directory directly containing agents/ and
|
|
342
|
+
* roles/. Pass an explicit path — no .flue/ waterfall is performed here.
|
|
343
|
+
* Callers that want the waterfall behavior (e.g. the CLI when --workspace
|
|
344
|
+
* is omitted) should resolve it themselves with `resolveWorkspaceFromCwd`.
|
|
345
|
+
*/
|
|
346
|
+
workspaceDir: string;
|
|
347
|
+
/**
|
|
348
|
+
* Where to write the dist/ directory. Independent of workspaceDir — typically
|
|
349
|
+
* the project root, so platform config like wrangler.jsonc ends up where the
|
|
350
|
+
* deploy tool expects it.
|
|
351
|
+
*/
|
|
352
|
+
outputDir: string;
|
|
338
353
|
target?: 'node' | 'cloudflare';
|
|
339
354
|
/** Overrides `target` when provided. */
|
|
340
355
|
plugin?: BuildPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flue/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"agentfs-sdk": "^0.6.4",
|
|
44
44
|
"esbuild": "^0.25.0",
|
|
45
45
|
"hono": "^4.7.0",
|
|
46
|
+
"jsonc-parser": "^3.3.1",
|
|
46
47
|
"just-bash": "^2.14.2",
|
|
47
48
|
"package-up": "^5.0.0",
|
|
48
49
|
"valibot": "^1.0.0"
|