@flue/sdk 0.1.2 → 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/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
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-C8tsaK1j.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-C0nqbu6Z.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,5 +1,5 @@
1
1
  import { r as discoverSessionContext } from "./agent-BYG0nVbQ.mjs";
2
- import { n as Session } from "./session-BRLCNVG1.mjs";
2
+ import { n as Session } from "./session-CiAMTsLZ.mjs";
3
3
  import { bashToSessionEnv } from "./sandbox.mjs";
4
4
  import { Type } from "@mariozechner/pi-ai";
5
5
 
@@ -32,7 +32,7 @@ function createFlueContext(config) {
32
32
  skills: localContext.skills,
33
33
  model: sessionModel
34
34
  };
35
- return new Session(config.sessionId, sessionConfig, env, store, savedData, currentEventCallback);
35
+ return new Session(config.sessionId, sessionConfig, env, store, savedData, currentEventCallback, options?.commands);
36
36
  },
37
37
  setEventCallback(callback) {
38
38
  currentEventCallback = callback;
@@ -54,12 +54,6 @@ async function resolveSessionEnv(sessionId, sandbox, config) {
54
54
  }
55
55
  return sandbox.createSessionEnv({ sessionId });
56
56
  }
57
- function defineCommand(name, execute) {
58
- return {
59
- name,
60
- execute
61
- };
62
- }
63
57
 
64
58
  //#endregion
65
- export { Type, createFlueContext, defineCommand };
59
+ export { Type, createFlueContext };
@@ -1,4 +1,5 @@
1
- import { S as SessionStore, b as SessionEnv } from "../types-C8tsaK1j.mjs";
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";
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,6 +1,7 @@
1
1
  import "../agent-BYG0nVbQ.mjs";
2
- import "../session-BRLCNVG1.mjs";
2
+ import "../session-CiAMTsLZ.mjs";
3
3
  import { createSandboxSessionEnv } from "../sandbox.mjs";
4
+ import { t as normalizeExecutor } from "../command-helpers-CxRhK1my.mjs";
4
5
  import { Workspace, WorkspaceFileSystem } from "@cloudflare/shell";
5
6
 
6
7
  //#region src/cloudflare/context.ts
@@ -105,6 +106,28 @@ async function getVirtualSandbox(bucket, options) {
105
106
  });
106
107
  }
107
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
+
108
131
  //#endregion
109
132
  //#region src/cloudflare/cf-sandbox.ts
110
133
  /** Wraps a @cloudflare/sandbox instance (from getSandbox()) into SessionEnv. */
@@ -204,4 +227,4 @@ function store() {
204
227
  }
205
228
 
206
229
  //#endregion
207
- 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-C0nqbu6Z.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,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-C8tsaK1j.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-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
- ${agents.map((a) => {
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 = ${JSON.stringify({ agents: agents.map((a) => ({
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 use getSandbox() from @cloudflare/sandbox for container sandboxes."
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 @cloudflare/sandbox instances (from getSandbox()).
90
- * Returns SessionEnv if the sandbox is a CF sandbox, null otherwise.
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
- ${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")}
464
+ ${agentClasses}
277
465
 
278
- // Re-export Sandbox DO class for wrangler binding
279
- export { Sandbox } from '@cloudflare/sandbox';
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: ["node:*", "cloudflare:*"]
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 allBindings = [...ctx.agents.filter((a) => a.triggers.webhook).map((a) => ({
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
- class_name: "Sandbox",
324
- name: "Sandbox"
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
- compatibility_date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
333
- compatibility_flags: ["nodejs_compat"],
334
- containers: [{
335
- class_name: "Sandbox",
336
- image: "./Dockerfile"
337
- }],
338
- durable_objects: { bindings: allBindings },
339
- migrations: [{
340
- new_sqlite_classes: allSqliteClasses,
341
- tag: "v1"
342
- }]
343
- }, null, 2);
344
- outputs["Dockerfile"] = [
345
- "FROM node:22-slim",
346
- "",
347
- "# Install common tools for agent sandboxes",
348
- "RUN apt-get update && apt-get install -y \\",
349
- " git curl wget \\",
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 agentDir = path.resolve(options.agentDir);
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: ${agentDir}`);
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(agentDir);
632
- const agents = discoverAgents(agentDir);
633
- if (agents.length === 0) throw new Error(`No agents found in ${path.join(agentDir, ".flue/agents/")}`);
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(agentDir, "dist");
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
- agentDir,
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(agentDir);
857
+ const nodePathsSet = collectNodePaths(workspaceDir);
664
858
  const { external: pluginExternal = [], ...pluginEsbuildOpts } = plugin.esbuildOptions(ctx);
665
- const userExternals = getUserExternals(agentDir);
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
- function discoverRoles(agentDir) {
709
- const rolesDir = path.join(agentDir, ".flue", "roles");
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(agentDir) {
728
- let agentsDir = path.join(agentDir, ".flue", "agents");
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(agentDir) {
757
- const pkgPath = packageUpSync({ cwd: agentDir });
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(agentDir) {
980
+ function collectNodePaths(workspaceDir) {
771
981
  const nodePathsSet = /* @__PURE__ */ new Set();
772
- for (const startDir of [agentDir, getSDKDir()]) {
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 };
@@ -1,4 +1,4 @@
1
- import { S as SessionStore, y as SessionData } from "./types-C8tsaK1j.mjs";
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/internal.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./agent-BYG0nVbQ.mjs";
2
- import { t as InMemorySessionStore } from "./session-BRLCNVG1.mjs";
2
+ import { t as InMemorySessionStore } from "./session-CiAMTsLZ.mjs";
3
3
  import { bashToSessionEnv } from "./sandbox.mjs";
4
4
  import { createFlueContext } from "./client.mjs";
5
5
 
@@ -0,0 +1,14 @@
1
+ import { s as Command } from "../types-C0nqbu6Z.mjs";
2
+ import { t as CommandExecutor } from "../command-helpers-C8SHLdaA.mjs";
3
+ import { execFile } from "node:child_process";
4
+
5
+ //#region src/node/define-command.d.ts
6
+ /**
7
+ * Options forwarded directly to Node's `child_process.execFile`. Full pass-through.
8
+ */
9
+ type CommandOptions = NonNullable<Parameters<typeof execFile>[2]>;
10
+ declare function defineCommand(name: string): Command;
11
+ declare function defineCommand(name: string, options: CommandOptions): Command;
12
+ declare function defineCommand(name: string, execute: CommandExecutor): Command;
13
+ //#endregion
14
+ export { type CommandOptions, defineCommand };
@@ -0,0 +1,75 @@
1
+ import { t as normalizeExecutor } from "../command-helpers-CxRhK1my.mjs";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+
5
+ //#region src/node/define-command.ts
6
+ /**
7
+ * Node-specific `defineCommand`. Supports three forms:
8
+ *
9
+ * ```ts
10
+ * defineCommand('agent-browser');
11
+ * defineCommand('gh', { env: { GH_TOKEN: process.env.GH_TOKEN } });
12
+ * defineCommand('gh', async (args) => ({ stdout: '...' }));
13
+ * ```
14
+ *
15
+ * Forms A and B shell out via `child_process.execFile`. Form C lets the user
16
+ * implement the command however they like. All three forms benefit from
17
+ * return-shape normalization and throw-catching — no `try`/`catch` or
18
+ * `return { stdout, stderr, exitCode: 0 }` boilerplate required.
19
+ */
20
+ const execFileAsync = promisify(execFile);
21
+ /**
22
+ * Essential, non-sensitive environment variables automatically forwarded to
23
+ * pass-through commands (forms A and B). Users can override any of these —
24
+ * or add their own (e.g. `GH_TOKEN`) — via `options.env`. Anything not listed
25
+ * here (API keys, tokens, secrets, etc.) stays on the host and is NEVER
26
+ * exposed to the spawned process unless the caller opts in explicitly.
27
+ *
28
+ * If you need full control over the env, use the function form:
29
+ * `defineCommand('gh', async (args) => { ... })`.
30
+ */
31
+ const DEFAULT_ENV = {
32
+ PATH: process.env.PATH,
33
+ HOME: process.env.HOME,
34
+ USER: process.env.USER,
35
+ LOGNAME: process.env.LOGNAME,
36
+ HOSTNAME: process.env.HOSTNAME,
37
+ SHELL: process.env.SHELL,
38
+ LANG: process.env.LANG,
39
+ LC_ALL: process.env.LC_ALL,
40
+ LC_CTYPE: process.env.LC_CTYPE,
41
+ TZ: process.env.TZ,
42
+ TERM: process.env.TERM,
43
+ TMPDIR: process.env.TMPDIR,
44
+ TMP: process.env.TMP,
45
+ TEMP: process.env.TEMP
46
+ };
47
+ function defineCommand(name, arg) {
48
+ if (typeof arg === "function") return {
49
+ name,
50
+ execute: normalizeExecutor(arg)
51
+ };
52
+ const userOpts = arg ?? {};
53
+ const mergedOpts = {
54
+ maxBuffer: 50 * 1024 * 1024,
55
+ ...userOpts,
56
+ env: {
57
+ ...DEFAULT_ENV,
58
+ ...userOpts.env ?? {}
59
+ }
60
+ };
61
+ const executor = async (args) => {
62
+ const { stdout, stderr } = await execFileAsync(name, args, mergedOpts);
63
+ return {
64
+ stdout: String(stdout ?? ""),
65
+ stderr: String(stderr ?? "")
66
+ };
67
+ };
68
+ return {
69
+ name,
70
+ execute: normalizeExecutor(executor)
71
+ };
72
+ }
73
+
74
+ //#endregion
75
+ export { defineCommand };
@@ -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-C8tsaK1j.mjs";
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;
package/dist/sandbox.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./agent-BYG0nVbQ.mjs";
2
- import { r as normalizePath } from "./session-BRLCNVG1.mjs";
2
+ import { r as normalizePath } from "./session-CiAMTsLZ.mjs";
3
3
 
4
4
  //#region src/sandbox.ts
5
5
  function bashToSessionEnv(bash) {
@@ -508,11 +508,13 @@ var Session = class Session {
508
508
  compactionAbortController;
509
509
  eventCallback;
510
510
  builtinTools;
511
- constructor(id, config, env, store, existingData, onAgentEvent) {
511
+ sessionCommands;
512
+ constructor(id, config, env, store, existingData, onAgentEvent, sessionCommands) {
512
513
  this.id = id;
513
514
  this.config = config;
514
515
  this.env = env;
515
516
  this.store = store;
517
+ this.sessionCommands = sessionCommands ?? [];
516
518
  this.metadata = existingData?.metadata ?? {};
517
519
  this.createdAt = existingData?.createdAt;
518
520
  this.lastCompaction = existingData?.lastCompaction;
@@ -586,8 +588,9 @@ var Session = class Session {
586
588
  const promptWithRole = this.injectRoleInstructions(text, options?.role);
587
589
  const schema = options?.result;
588
590
  const fullPrompt = buildPromptText(promptWithRole, schema);
589
- if (options?.commands) this.assertCommandSupport(options.commands);
590
- const registeredCommandNames = options?.commands ? this.registerCommands(options.commands) : [];
591
+ const effectiveCommands = this.mergeCommands(options?.commands);
592
+ if (effectiveCommands.length > 0) this.assertCommandSupport(effectiveCommands);
593
+ const registeredCommandNames = this.registerCommands(effectiveCommands);
591
594
  const registeredToolNames = options?.tools ? this.registerCustomTools(options.tools) : [];
592
595
  try {
593
596
  await this.agent.prompt(fullPrompt);
@@ -616,8 +619,9 @@ var Session = class Session {
616
619
  const schema = options?.result;
617
620
  const skillPrompt = buildSkillPrompt(registeredSkill.instructions, options?.args, schema);
618
621
  const promptWithRole = this.injectRoleInstructions(skillPrompt, options?.role);
619
- if (options?.commands) this.assertCommandSupport(options.commands);
620
- const registeredCommandNames = options?.commands ? this.registerCommands(options.commands) : [];
622
+ const effectiveCommands = this.mergeCommands(options?.commands);
623
+ if (effectiveCommands.length > 0) this.assertCommandSupport(effectiveCommands);
624
+ const registeredCommandNames = this.registerCommands(effectiveCommands);
621
625
  const registeredToolNames = options?.tools ? this.registerCustomTools(options.tools) : [];
622
626
  try {
623
627
  await this.agent.prompt(promptWithRole);
@@ -632,8 +636,9 @@ var Session = class Session {
632
636
  }
633
637
  }
634
638
  async shell(command, options) {
635
- if (options?.commands) this.assertCommandSupport(options.commands);
636
- const registeredNames = options?.commands ? this.registerCommands(options.commands) : [];
639
+ const effectiveCommands = this.mergeCommands(options?.commands);
640
+ if (effectiveCommands.length > 0) this.assertCommandSupport(effectiveCommands);
641
+ const registeredNames = this.registerCommands(effectiveCommands);
637
642
  try {
638
643
  const result = await this.env.exec(command, {
639
644
  env: options?.env,
@@ -750,6 +755,19 @@ var Session = class Session {
750
755
  if (commands.length === 0) return;
751
756
  if (!this.env.commandSupport) throw new Error("[flue] Cannot use commands: this environment does not support command registration. Commands are only available in isolate sandbox mode. Remote sandboxes handle command execution at the platform level.");
752
757
  }
758
+ /**
759
+ * Merge session-wide `commands` (from init()) with per-call commands. When
760
+ * both define a command with the same name, the per-call entry wins for
761
+ * that call.
762
+ */
763
+ mergeCommands(perCall) {
764
+ if (!perCall || perCall.length === 0) return this.sessionCommands;
765
+ if (this.sessionCommands.length === 0) return perCall;
766
+ const byName = /* @__PURE__ */ new Map();
767
+ for (const cmd of this.sessionCommands) byName.set(cmd.name, cmd);
768
+ for (const cmd of perCall) byName.set(cmd.name, cmd);
769
+ return Array.from(byName.values());
770
+ }
753
771
  registerCommands(commands) {
754
772
  if (!this.env.commandSupport || commands.length === 0) return [];
755
773
  const names = [];
@@ -146,6 +146,13 @@ interface SessionInit {
146
146
  * Precedence (highest wins): per-call `model` > role `model` > session `model` > build-time default.
147
147
  */
148
148
  model?: string;
149
+ /**
150
+ * Session-wide commands. Every prompt(), skill(), and shell() call inherits
151
+ * this list. Per-call `commands` are merged on top — if a per-call command
152
+ * shares a name with a session command, the per-call version wins for that
153
+ * call.
154
+ */
155
+ commands?: Command[];
149
156
  }
150
157
  interface FlueSession {
151
158
  prompt<S extends v.GenericSchema>(text: string, options: PromptOptions<S> & {
@@ -315,7 +322,10 @@ interface AgentInfo {
315
322
  interface BuildContext {
316
323
  agents: AgentInfo[];
317
324
  roles: Record<string, Role>;
318
- agentDir: string;
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;
319
329
  options: BuildOptions;
320
330
  }
321
331
  /** Controls the build output format for a target platform. */
@@ -327,7 +337,19 @@ interface BuildPlugin {
327
337
  additionalOutputs?(ctx: BuildContext): Record<string, string>;
328
338
  }
329
339
  interface BuildOptions {
330
- agentDir: string;
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;
331
353
  target?: 'node' | 'cloudflare';
332
354
  /** Overrides `target` when provided. */
333
355
  plugin?: BuildPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flue/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
@@ -23,6 +23,10 @@
23
23
  "./cloudflare": {
24
24
  "types": "./dist/cloudflare/index.d.mts",
25
25
  "import": "./dist/cloudflare/index.mjs"
26
+ },
27
+ "./node": {
28
+ "types": "./dist/node/index.d.mts",
29
+ "import": "./dist/node/index.mjs"
26
30
  }
27
31
  },
28
32
  "main": "./dist/index.mjs",
@@ -39,6 +43,7 @@
39
43
  "agentfs-sdk": "^0.6.4",
40
44
  "esbuild": "^0.25.0",
41
45
  "hono": "^4.7.0",
46
+ "jsonc-parser": "^3.3.1",
42
47
  "just-bash": "^2.14.2",
43
48
  "package-up": "^5.0.0",
44
49
  "valibot": "^1.0.0"