@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 +4 -9
- package/dist/client.d.mts +2 -7
- package/dist/client.mjs +3 -9
- package/dist/cloudflare/index.d.mts +6 -2
- package/dist/cloudflare/index.mjs +25 -2
- package/dist/command-helpers-C8SHLdaA.d.mts +21 -0
- package/dist/command-helpers-CxRhK1my.mjs +37 -0
- package/dist/index.d.mts +22 -2
- package/dist/index.mjs +292 -82
- package/dist/internal.d.mts +1 -1
- package/dist/internal.mjs +1 -1
- package/dist/node/index.d.mts +14 -0
- package/dist/node/index.mjs +75 -0
- package/dist/sandbox.d.mts +1 -1
- package/dist/sandbox.mjs +1 -1
- package/dist/{session-BRLCNVG1.mjs → session-CiAMTsLZ.mjs} +25 -7
- package/dist/{types-C8tsaK1j.d.mts → types-C0nqbu6Z.d.mts} +24 -2
- package/package.json +6 -1
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 {
|
|
98
|
-
import {
|
|
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'
|
|
110
|
-
const gh = defineCommand('gh',
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
59
|
+
export { Type, createFlueContext };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { S as SessionStore, b as SessionEnv } from "../types-
|
|
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-
|
|
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-
|
|
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/internal.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./agent-BYG0nVbQ.mjs";
|
|
2
|
-
import { t as InMemorySessionStore } from "./session-
|
|
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 };
|
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;
|
package/dist/sandbox.mjs
CHANGED
|
@@ -508,11 +508,13 @@ var Session = class Session {
|
|
|
508
508
|
compactionAbortController;
|
|
509
509
|
eventCallback;
|
|
510
510
|
builtinTools;
|
|
511
|
-
|
|
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
|
-
|
|
590
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
-
|
|
636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|