@flue/sdk 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/index.d.mts +39 -1
- package/dist/index.mjs +107 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -318,6 +318,17 @@ Defaults to port `3583` ("FLUE" on a phone keypad). Override with `--port`.
|
|
|
318
318
|
|
|
319
319
|
`flue dev --target cloudflare` requires `wrangler` as a peer dependency in your project (`npm install --save-dev wrangler`).
|
|
320
320
|
|
|
321
|
+
#### Loading environment variables
|
|
322
|
+
|
|
323
|
+
Pass `--env <path>` to load a `.env`-format file. Works for both targets:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
flue dev --target node --env .env
|
|
327
|
+
flue dev --target cloudflare --env .env
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Repeatable; later files override earlier ones on key collision. Shell-set env vars win over file values. Edits to the file trigger a reload. Same flag works for `flue run`.
|
|
331
|
+
|
|
321
332
|
### Trigger From the CLI (`flue run`)
|
|
322
333
|
|
|
323
334
|
Build and run any agent locally, perfect for running in CI or for one-shot scripted invocations. Production-shaped — builds the deployable artifact and starts it once.
|
package/dist/index.d.mts
CHANGED
|
@@ -43,6 +43,24 @@ interface DevOptions {
|
|
|
43
43
|
target: 'node' | 'cloudflare';
|
|
44
44
|
/** Defaults to 3583 ("FLUE" on a phone keypad). */
|
|
45
45
|
port?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Absolute paths to env files (`.env`-format) to load before starting the
|
|
48
|
+
* dev server. Repeatable; later files override earlier ones on key
|
|
49
|
+
* collision (matching wrangler's `envFiles` semantics and standard
|
|
50
|
+
* dotenv composition patterns).
|
|
51
|
+
*
|
|
52
|
+
* - Node: parsed with `node:util.parseEnv` and merged into the child
|
|
53
|
+
* server process's env. Shell-set env vars win over file values.
|
|
54
|
+
* - Cloudflare: passed through to wrangler's `unstable_startWorker` as
|
|
55
|
+
* `envFiles`, which loads them as `secret_text` bindings.
|
|
56
|
+
*
|
|
57
|
+
* If empty/undefined, no env loading happens. Cloudflare's auto-discovery
|
|
58
|
+
* of `.dev.vars` is disabled in either case (we always pass an explicit
|
|
59
|
+
* `envFiles` array to wrangler so its default search is suppressed).
|
|
60
|
+
*
|
|
61
|
+
* Each path must exist; otherwise dev fails fast with a clear error.
|
|
62
|
+
*/
|
|
63
|
+
envFiles?: string[];
|
|
46
64
|
}
|
|
47
65
|
/** Default port for `flue dev`. F=3, L=5, U=8, E=3 on a phone keypad. */
|
|
48
66
|
declare const DEFAULT_DEV_PORT = 3583;
|
|
@@ -53,6 +71,26 @@ declare const DEFAULT_DEV_PORT = 3583;
|
|
|
53
71
|
* — the user is editing code, after all, and we want to recover when they fix it.
|
|
54
72
|
*/
|
|
55
73
|
declare function dev(options: DevOptions): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve and validate a list of env-file paths. Returns absolute paths.
|
|
76
|
+
*
|
|
77
|
+
* Throws a friendly `[flue]`-prefixed error if any path doesn't exist. The
|
|
78
|
+
* goal of `--env` is explicitness — silent skip on a typo would defeat
|
|
79
|
+
* the purpose.
|
|
80
|
+
*/
|
|
81
|
+
declare function resolveEnvFiles(envFiles: string[] | undefined, cwd: string): string[];
|
|
82
|
+
/**
|
|
83
|
+
* Parse one or more `.env`-format files and return their merged contents.
|
|
84
|
+
* Later files override earlier files on key collision.
|
|
85
|
+
*
|
|
86
|
+
* Uses Node's built-in `util.parseEnv` (Node 20.6+; Flue requires Node 22+).
|
|
87
|
+
* No `dotenv` package needed.
|
|
88
|
+
*
|
|
89
|
+
* Parse-only — doesn't touch `process.env`. Caller composes with
|
|
90
|
+
* `process.env` as needed (typical pattern: spread file vars first, then
|
|
91
|
+
* `process.env`, so shell-set values win).
|
|
92
|
+
*/
|
|
93
|
+
declare function parseEnvFiles(absolutePaths: string[]): Record<string, string>;
|
|
56
94
|
//#endregion
|
|
57
95
|
//#region src/agent.d.ts
|
|
58
96
|
declare const BUILTIN_TOOL_NAMES: Set<string>;
|
|
@@ -75,4 +113,4 @@ interface CreateToolsOptions {
|
|
|
75
113
|
}
|
|
76
114
|
declare function createTools(env: SessionEnv, options?: CreateToolsOptions): AgentTool<any>[];
|
|
77
115
|
//#endregion
|
|
78
|
-
export { type AgentConfig, type AgentInfo, type AgentInit, BUILTIN_TOOL_NAMES, type BashFactory, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, DEFAULT_DEV_PORT, type DevOptions, type FileStat, type FlueAgent, type FlueContext, type FlueEvent, type FlueEventCallback, type FlueSession, type FlueSessions, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionOptions, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, type ToolParameters, build, createTools, dev, resolveWorkspaceFromCwd };
|
|
116
|
+
export { type AgentConfig, type AgentInfo, type AgentInit, BUILTIN_TOOL_NAMES, type BashFactory, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, DEFAULT_DEV_PORT, type DevOptions, type FileStat, type FlueAgent, type FlueContext, type FlueEvent, type FlueEventCallback, type FlueSession, type FlueSessions, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionOptions, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, type ToolParameters, build, createTools, dev, parseEnvFiles, resolveEnvFiles, resolveWorkspaceFromCwd };
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { packageUpSync } from "package-up";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { parseEnv } from "node:util";
|
|
7
8
|
|
|
8
9
|
//#region src/cloudflare-wrangler-merge.ts
|
|
9
10
|
/**
|
|
@@ -153,6 +154,38 @@ function mergeFlueAdditions(userConfig, additions) {
|
|
|
153
154
|
return merged;
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
157
|
+
* Strip wrangler-normalizer defaults that cause spurious warnings when wrangler
|
|
158
|
+
* re-parses our generated dist/wrangler.jsonc.
|
|
159
|
+
*
|
|
160
|
+
* Background: `unstable_readConfig` returns a fully-normalized `Unstable_Config`
|
|
161
|
+
* with every section populated to a default — including `unsafe: {}`. Wrangler's
|
|
162
|
+
* own validator then emits a `"unsafe" fields are experimental` warning whenever
|
|
163
|
+
* the field is *present*, regardless of whether it's empty. So our merged file,
|
|
164
|
+
* which inherits the empty default, would trip the warning at every dev start
|
|
165
|
+
* and every deploy.
|
|
166
|
+
*
|
|
167
|
+
* We delete `unsafe` only when it's an empty object (the exact shape wrangler's
|
|
168
|
+
* normalizer produces). If a user has actually written `unsafe: {...}` in their
|
|
169
|
+
* own wrangler.jsonc, the value will be non-empty and we leave it alone — the
|
|
170
|
+
* warning in that case is wrangler's intended diagnostic, not noise.
|
|
171
|
+
*
|
|
172
|
+
* Other normalizer-defaulted-empty fields (`vars: {}`, `kv_namespaces: []`,
|
|
173
|
+
* `python_modules: { exclude: ['**\/*.pyc'] }`, etc.) are left in place. They're
|
|
174
|
+
* harmless: wrangler doesn't warn about them, dist/wrangler.jsonc is an
|
|
175
|
+
* internal build artifact, and stripping them only saves bytes. Only `unsafe`
|
|
176
|
+
* has a user-visible side effect we need to fix.
|
|
177
|
+
*
|
|
178
|
+
* If wrangler adds another field to its `experimental()` warning list in a
|
|
179
|
+
* future version (today there are only two: `unsafe` and `secrets`), this
|
|
180
|
+
* function is the place to extend.
|
|
181
|
+
*
|
|
182
|
+
* Mutates `merged` in place to match the shallow-clone pattern in
|
|
183
|
+
* `mergeFlueAdditions`.
|
|
184
|
+
*/
|
|
185
|
+
function stripNoisyWranglerDefaults(merged) {
|
|
186
|
+
if ("unsafe" in merged && typeof merged.unsafe === "object" && merged.unsafe !== null && !Array.isArray(merged.unsafe) && Object.keys(merged.unsafe).length === 0) delete merged.unsafe;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
156
189
|
* Return the list of `class_name`s declared in the user's wrangler
|
|
157
190
|
* `durable_objects.bindings` that contain the literal substring `Sandbox`
|
|
158
191
|
* (case-sensitive).
|
|
@@ -637,6 +670,7 @@ export default {
|
|
|
637
670
|
for (const className of sandboxClassNames) console.log(`[flue] Detected Sandbox-named DO binding "${className}" — re-exporting from @cloudflare/sandbox.`);
|
|
638
671
|
}
|
|
639
672
|
const merged = mergeFlueAdditions(userConfig, additions);
|
|
673
|
+
stripNoisyWranglerDefaults(merged);
|
|
640
674
|
if (typeof merged.$schema !== "string") merged.$schema = "https://workers.cloudflare.com/schema/wrangler.json";
|
|
641
675
|
outputs["wrangler.jsonc"] = JSON.stringify(merged, null, 2);
|
|
642
676
|
writeDeployRedirectIfMissing(ctx.outputDir);
|
|
@@ -1155,6 +1189,8 @@ async function dev(options) {
|
|
|
1155
1189
|
const workspaceDir = path.resolve(options.workspaceDir);
|
|
1156
1190
|
const outputDir = path.resolve(options.outputDir);
|
|
1157
1191
|
const port = options.port ?? DEFAULT_DEV_PORT;
|
|
1192
|
+
const envFiles = resolveEnvFiles(options.envFiles, outputDir);
|
|
1193
|
+
for (const f of envFiles) console.error(`[flue] Loading env from: ${f}`);
|
|
1158
1194
|
const buildOptions = {
|
|
1159
1195
|
workspaceDir,
|
|
1160
1196
|
outputDir,
|
|
@@ -1172,10 +1208,12 @@ async function dev(options) {
|
|
|
1172
1208
|
console.error(`[flue] Built in ${Date.now() - initialStart}ms`);
|
|
1173
1209
|
const reloader = options.target === "node" ? new NodeReloader({
|
|
1174
1210
|
outputDir,
|
|
1175
|
-
port
|
|
1211
|
+
port,
|
|
1212
|
+
envFiles
|
|
1176
1213
|
}) : await createCloudflareReloader({
|
|
1177
1214
|
outputDir,
|
|
1178
|
-
port
|
|
1215
|
+
port,
|
|
1216
|
+
envFiles
|
|
1179
1217
|
});
|
|
1180
1218
|
await reloader.start();
|
|
1181
1219
|
if (reloader.url) {
|
|
@@ -1188,14 +1226,17 @@ async function dev(options) {
|
|
|
1188
1226
|
}
|
|
1189
1227
|
console.error(`[flue] Press Ctrl+C to stop\n`);
|
|
1190
1228
|
const rebuilder = createRebuilder(buildOptions, reloader);
|
|
1229
|
+
const envFileSet = new Set(envFiles);
|
|
1191
1230
|
const watcher = createWatcher({
|
|
1192
1231
|
workspaceDir,
|
|
1193
1232
|
outputDir,
|
|
1194
1233
|
target: options.target,
|
|
1234
|
+
envFiles,
|
|
1195
1235
|
onChange: (relPath) => {
|
|
1196
1236
|
if (!reloader.shouldRebuildOn(relPath)) return;
|
|
1237
|
+
const isEnvFile = envFileSet.has(relPath);
|
|
1197
1238
|
console.error(`[flue] Change detected: ${relPath}`);
|
|
1198
|
-
rebuilder.schedule();
|
|
1239
|
+
rebuilder.schedule(isEnvFile);
|
|
1199
1240
|
}
|
|
1200
1241
|
});
|
|
1201
1242
|
let shuttingDown = false;
|
|
@@ -1224,31 +1265,40 @@ async function dev(options) {
|
|
|
1224
1265
|
function createRebuilder(buildOptions, reloader) {
|
|
1225
1266
|
let running = false;
|
|
1226
1267
|
let queued = false;
|
|
1268
|
+
let queuedForce = false;
|
|
1269
|
+
let pendingForce = false;
|
|
1227
1270
|
let debounceTimer = null;
|
|
1228
|
-
const runOnce = async () => {
|
|
1271
|
+
const runOnce = async (force) => {
|
|
1229
1272
|
running = true;
|
|
1230
1273
|
const start = Date.now();
|
|
1231
1274
|
console.error(`[flue] Rebuilding...`);
|
|
1232
1275
|
try {
|
|
1233
1276
|
const { changed } = await build(buildOptions);
|
|
1234
|
-
await reloader.reload(changed);
|
|
1277
|
+
await reloader.reload(changed || force);
|
|
1235
1278
|
console.error(`[flue] Reloaded in ${Date.now() - start}ms\n`);
|
|
1236
1279
|
} catch (err) {
|
|
1237
1280
|
console.error(`[flue] Rebuild failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1238
1281
|
} finally {
|
|
1239
1282
|
running = false;
|
|
1240
1283
|
if (queued) {
|
|
1284
|
+
const nextForce = queuedForce;
|
|
1241
1285
|
queued = false;
|
|
1242
|
-
|
|
1286
|
+
queuedForce = false;
|
|
1287
|
+
runOnce(nextForce);
|
|
1243
1288
|
}
|
|
1244
1289
|
}
|
|
1245
1290
|
};
|
|
1246
|
-
return { schedule() {
|
|
1291
|
+
return { schedule(forceReload = false) {
|
|
1292
|
+
if (forceReload) pendingForce = true;
|
|
1247
1293
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1248
1294
|
debounceTimer = setTimeout(() => {
|
|
1249
1295
|
debounceTimer = null;
|
|
1250
|
-
|
|
1251
|
-
|
|
1296
|
+
const force = pendingForce;
|
|
1297
|
+
pendingForce = false;
|
|
1298
|
+
if (running) {
|
|
1299
|
+
queued = true;
|
|
1300
|
+
if (force) queuedForce = true;
|
|
1301
|
+
} else runOnce(force);
|
|
1252
1302
|
}, 150);
|
|
1253
1303
|
} };
|
|
1254
1304
|
}
|
|
@@ -1267,7 +1317,7 @@ function createRebuilder(buildOptions, reloader) {
|
|
|
1267
1317
|
* - editor backup/swap suffixes
|
|
1268
1318
|
*/
|
|
1269
1319
|
function createWatcher(options) {
|
|
1270
|
-
const { workspaceDir, outputDir, target, onChange } = options;
|
|
1320
|
+
const { workspaceDir, outputDir, target, envFiles, onChange } = options;
|
|
1271
1321
|
const watchers = [];
|
|
1272
1322
|
const isIgnoredPath = (relPath) => {
|
|
1273
1323
|
const parts = relPath.replace(/\\/g, "/").split("/");
|
|
@@ -1307,6 +1357,10 @@ function createWatcher(options) {
|
|
|
1307
1357
|
watchers.push(w);
|
|
1308
1358
|
} catch {}
|
|
1309
1359
|
}
|
|
1360
|
+
for (const envPath of envFiles) try {
|
|
1361
|
+
const w = fs.watch(envPath, () => onChange(envPath));
|
|
1362
|
+
watchers.push(w);
|
|
1363
|
+
} catch {}
|
|
1310
1364
|
return { close() {
|
|
1311
1365
|
for (const w of watchers) try {
|
|
1312
1366
|
w.close();
|
|
@@ -1318,10 +1372,12 @@ var NodeReloader = class {
|
|
|
1318
1372
|
serverPath;
|
|
1319
1373
|
outputDir;
|
|
1320
1374
|
port;
|
|
1375
|
+
envFiles;
|
|
1321
1376
|
url;
|
|
1322
1377
|
constructor(opts) {
|
|
1323
1378
|
this.outputDir = opts.outputDir;
|
|
1324
1379
|
this.port = opts.port;
|
|
1380
|
+
this.envFiles = opts.envFiles;
|
|
1325
1381
|
this.serverPath = path.join(this.outputDir, "dist", "server.mjs");
|
|
1326
1382
|
this.url = `http://localhost:${this.port}`;
|
|
1327
1383
|
}
|
|
@@ -1346,6 +1402,7 @@ var NodeReloader = class {
|
|
|
1346
1402
|
} catch {}
|
|
1347
1403
|
}
|
|
1348
1404
|
async spawnAndWait() {
|
|
1405
|
+
const fromFiles = parseEnvFiles(this.envFiles);
|
|
1349
1406
|
const child = spawn("node", [this.serverPath], {
|
|
1350
1407
|
stdio: [
|
|
1351
1408
|
"ignore",
|
|
@@ -1354,6 +1411,7 @@ var NodeReloader = class {
|
|
|
1354
1411
|
],
|
|
1355
1412
|
cwd: this.outputDir,
|
|
1356
1413
|
env: {
|
|
1414
|
+
...fromFiles,
|
|
1357
1415
|
...process.env,
|
|
1358
1416
|
PORT: String(this.port),
|
|
1359
1417
|
FLUE_MODE: "local"
|
|
@@ -1436,11 +1494,13 @@ var CloudflareReloader = class {
|
|
|
1436
1494
|
outputDir;
|
|
1437
1495
|
port;
|
|
1438
1496
|
configPath;
|
|
1497
|
+
envFiles;
|
|
1439
1498
|
url;
|
|
1440
1499
|
constructor(wrangler, opts) {
|
|
1441
1500
|
this.wrangler = wrangler;
|
|
1442
1501
|
this.outputDir = opts.outputDir;
|
|
1443
1502
|
this.port = opts.port;
|
|
1503
|
+
this.envFiles = opts.envFiles;
|
|
1444
1504
|
this.configPath = path.join(this.outputDir, "dist", "wrangler.jsonc");
|
|
1445
1505
|
}
|
|
1446
1506
|
async start() {
|
|
@@ -1467,6 +1527,7 @@ var CloudflareReloader = class {
|
|
|
1467
1527
|
* baked into the entry).
|
|
1468
1528
|
*/
|
|
1469
1529
|
shouldRebuildOn(relPath) {
|
|
1530
|
+
if (this.envFiles.includes(relPath)) return true;
|
|
1470
1531
|
const normalized = relPath.replace(/\\/g, "/");
|
|
1471
1532
|
if (normalized === "wrangler.jsonc" || normalized === "wrangler.json" || normalized === "wrangler.toml") return true;
|
|
1472
1533
|
if (normalized.startsWith("agents/")) return true;
|
|
@@ -1491,6 +1552,7 @@ var CloudflareReloader = class {
|
|
|
1491
1552
|
if (!fs.existsSync(this.configPath)) throw new Error(`[flue] Expected ${this.configPath} after build, but it doesn't exist. Did the Cloudflare build succeed?`);
|
|
1492
1553
|
this.worker = await this.wrangler.unstable_startWorker({
|
|
1493
1554
|
config: this.configPath,
|
|
1555
|
+
envFiles: this.envFiles,
|
|
1494
1556
|
build: { nodejsCompatMode: "v2" },
|
|
1495
1557
|
dev: {
|
|
1496
1558
|
server: {
|
|
@@ -1518,6 +1580,40 @@ var CloudflareReloader = class {
|
|
|
1518
1580
|
}
|
|
1519
1581
|
}
|
|
1520
1582
|
};
|
|
1583
|
+
/**
|
|
1584
|
+
* Resolve and validate a list of env-file paths. Returns absolute paths.
|
|
1585
|
+
*
|
|
1586
|
+
* Throws a friendly `[flue]`-prefixed error if any path doesn't exist. The
|
|
1587
|
+
* goal of `--env` is explicitness — silent skip on a typo would defeat
|
|
1588
|
+
* the purpose.
|
|
1589
|
+
*/
|
|
1590
|
+
function resolveEnvFiles(envFiles, cwd) {
|
|
1591
|
+
if (!envFiles || envFiles.length === 0) return [];
|
|
1592
|
+
return envFiles.map((p) => {
|
|
1593
|
+
const abs = path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
1594
|
+
if (!fs.existsSync(abs)) throw new Error(`[flue] --env points at a path that doesn't exist: ${p}`);
|
|
1595
|
+
return abs;
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Parse one or more `.env`-format files and return their merged contents.
|
|
1600
|
+
* Later files override earlier files on key collision.
|
|
1601
|
+
*
|
|
1602
|
+
* Uses Node's built-in `util.parseEnv` (Node 20.6+; Flue requires Node 22+).
|
|
1603
|
+
* No `dotenv` package needed.
|
|
1604
|
+
*
|
|
1605
|
+
* Parse-only — doesn't touch `process.env`. Caller composes with
|
|
1606
|
+
* `process.env` as needed (typical pattern: spread file vars first, then
|
|
1607
|
+
* `process.env`, so shell-set values win).
|
|
1608
|
+
*/
|
|
1609
|
+
function parseEnvFiles(absolutePaths) {
|
|
1610
|
+
const merged = {};
|
|
1611
|
+
for (const p of absolutePaths) {
|
|
1612
|
+
const parsed = parseEnv(fs.readFileSync(p, "utf-8"));
|
|
1613
|
+
Object.assign(merged, parsed);
|
|
1614
|
+
}
|
|
1615
|
+
return merged;
|
|
1616
|
+
}
|
|
1521
1617
|
async function waitForHealth(baseUrl, timeoutMs) {
|
|
1522
1618
|
const start = Date.now();
|
|
1523
1619
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -1565,4 +1661,4 @@ function pickExampleAgentName(outputDir, workspaceDir) {
|
|
|
1565
1661
|
}
|
|
1566
1662
|
|
|
1567
1663
|
//#endregion
|
|
1568
|
-
export { BUILTIN_TOOL_NAMES, DEFAULT_DEV_PORT, build, createTools, dev, resolveWorkspaceFromCwd };
|
|
1664
|
+
export { BUILTIN_TOOL_NAMES, DEFAULT_DEV_PORT, build, createTools, dev, parseEnvFiles, resolveEnvFiles, resolveWorkspaceFromCwd };
|