@flue/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,775 @@
1
+ import { a as createTools, i as BUILTIN_TOOL_NAMES, s as parseFrontmatterFile, t as InMemorySessionStore } from "./session-BD0MEuO3.mjs";
2
+ import { createFlueContext } from "./client.mjs";
3
+ import * as esbuild from "esbuild";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { packageUpSync } from "package-up";
7
+
8
+ //#region src/build-plugin-cloudflare.ts
9
+ var CloudflarePlugin = class {
10
+ name = "cloudflare";
11
+ generateEntryPoint(ctx) {
12
+ const { agents, roles, options, resolveSDKImport } = ctx;
13
+ const modelProvider = options.model?.provider ?? "anthropic";
14
+ const modelId = options.model?.modelId ?? "claude-haiku-4-5";
15
+ const rolesJson = JSON.stringify(roles);
16
+ const webhookAgents = agents.filter((a) => a.triggers.webhook);
17
+ const agentImports = agents.map((a) => {
18
+ return `import ${agentVarName$1(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
19
+ }).join("\n");
20
+ const manifest = JSON.stringify({ agents: agents.map((a) => ({
21
+ name: a.name,
22
+ triggers: a.triggers
23
+ })) }, null, 2);
24
+ const agentClasses = webhookAgents.map((a) => {
25
+ const className = agentClassName(a.name);
26
+ const handlerVar = agentVarName$1(a.name);
27
+ return `export class ${className} extends Agent {
28
+ async onRequest(request) {
29
+ return handleAgentRequest(request, this, ${JSON.stringify(a.name)}, ${handlerVar});
30
+ }
31
+ }`;
32
+ }).join("\n\n");
33
+ return `
34
+ // Auto-generated by @flue/sdk build (cloudflare)
35
+ import { Agent, routeAgentRequest } from 'agents';
36
+ import { Bash, InMemoryFs } from 'just-bash';
37
+ import { getModel } from '@mariozechner/pi-ai';
38
+ import { createFlueContext } from '${resolveSDKImport("client")}';
39
+ import { InMemorySessionStore } from '${resolveSDKImport("session")}';
40
+ import { bashToSessionEnv } from '${resolveSDKImport("sandbox")}';
41
+ import { setCloudflareContext, clearCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
42
+
43
+ ${agentImports}
44
+
45
+ // ─── Config ─────────────────────────────────────────────────────────────────
46
+
47
+ const roles = ${rolesJson};
48
+ const skills = {};
49
+ const systemPrompt = '';
50
+ const manifest = ${manifest};
51
+
52
+ // ─── Infrastructure ─────────────────────────────────────────────────────────
53
+
54
+ const model = getModel(${JSON.stringify(modelProvider)}, ${JSON.stringify(modelId)});
55
+
56
+ function resolveModel(modelString) {
57
+ const slash = modelString.indexOf('/');
58
+ if (slash !== -1) {
59
+ return getModel(modelString.slice(0, slash), modelString.slice(slash + 1));
60
+ }
61
+ return getModel(${JSON.stringify(modelProvider)}, modelString);
62
+ }
63
+
64
+ // ─── Sandbox Environments ───────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Create an empty in-memory sandbox (default).
68
+ */
69
+ async function createDefaultEnv() {
70
+ const bash = new Bash({
71
+ fs: new InMemoryFs(),
72
+ network: { dangerouslyAllowFullInternetAccess: true },
73
+ });
74
+ return bashToSessionEnv(bash);
75
+ }
76
+
77
+ /**
78
+ * 'local' sandbox is not available on Cloudflare Workers.
79
+ */
80
+ async function createLocalEnv() {
81
+ throw new Error(
82
+ "[flue] 'local' sandbox is not supported on Cloudflare Workers. " +
83
+ "Use the default empty sandbox, pass a custom Bash instance, " +
84
+ "or use getSandbox() from @cloudflare/sandbox for container sandboxes."
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Detect and wrap @cloudflare/sandbox instances (from getSandbox()).
90
+ * Returns SessionEnv if the sandbox is a CF sandbox, null otherwise.
91
+ */
92
+ function resolveSandbox(sandbox) {
93
+ if (
94
+ sandbox && typeof sandbox === 'object' &&
95
+ typeof sandbox.exec === 'function' &&
96
+ typeof sandbox.readFile === 'function' &&
97
+ typeof sandbox.destroy === 'function' &&
98
+ !('getCwd' in sandbox) && !('fs' in sandbox)
99
+ ) {
100
+ return cfSandboxToSessionEnv(sandbox);
101
+ }
102
+ return null;
103
+ }
104
+
105
+ // Fallback in-memory store (used if no DO storage is available).
106
+ const memoryStore = new InMemorySessionStore();
107
+
108
+ // Create a DO-backed session store from the Durable Object's SQL storage.
109
+ function createDOStore(sql) {
110
+ // Ensure the table exists
111
+ sql.exec(
112
+ 'CREATE TABLE IF NOT EXISTS flue_sessions (id TEXT PRIMARY KEY, data TEXT NOT NULL, updated_at INTEGER NOT NULL)'
113
+ );
114
+ return {
115
+ async save(id, data) {
116
+ const json = JSON.stringify(data);
117
+ sql.exec(
118
+ 'INSERT OR REPLACE INTO flue_sessions (id, data, updated_at) VALUES (?, ?, ?)',
119
+ id, json, Date.now()
120
+ );
121
+ },
122
+ async load(id) {
123
+ const rows = sql.exec('SELECT data FROM flue_sessions WHERE id = ?', id).toArray();
124
+ if (rows.length === 0) return null;
125
+ return JSON.parse(rows[0].data);
126
+ },
127
+ async delete(id) {
128
+ sql.exec('DELETE FROM flue_sessions WHERE id = ?', id);
129
+ },
130
+ };
131
+ }
132
+
133
+ function createContextForRequest(sessionId, payload, doInstance) {
134
+ // Use DO SQLite storage by default, fall back to in-memory
135
+ const defaultStore = doInstance?.ctx?.storage?.sql
136
+ ? createDOStore(doInstance.ctx.storage.sql)
137
+ : memoryStore;
138
+
139
+ return createFlueContext({
140
+ sessionId,
141
+ payload,
142
+ env: doInstance?.env ?? {},
143
+ agentConfig: {
144
+ systemPrompt, skills, roles, model, resolveModel,
145
+ },
146
+ createDefaultEnv,
147
+ createLocalEnv,
148
+ defaultStore,
149
+ resolveSandbox,
150
+ });
151
+ }
152
+
153
+ // ─── Shared Request Handler ────────────────────────────────────────────────
154
+
155
+ async function handleAgentRequest(request, doInstance, agentName, handler) {
156
+ // Session ID is the DO "room name" set by routeAgentRequest
157
+ const sessionId = doInstance.name;
158
+
159
+ // Parse payload
160
+ let payload;
161
+ try {
162
+ payload = await request.json();
163
+ } catch {
164
+ payload = {};
165
+ }
166
+
167
+ // Set up Cloudflare context for runtime primitives
168
+ setCloudflareContext({ env: doInstance.env, agentInstance: doInstance, storage: doInstance.ctx.storage });
169
+
170
+ const accept = request.headers.get('accept') || '';
171
+ const isWebhook = request.headers.get('x-webhook') === 'true';
172
+ const isSSE = accept.includes('text/event-stream') && !isWebhook;
173
+
174
+ try {
175
+ // Fire-and-forget (webhook mode)
176
+ if (isWebhook) {
177
+ const requestId = crypto.randomUUID();
178
+ const ctx = createContextForRequest(sessionId, payload, doInstance);
179
+ handler(ctx).then(
180
+ (result) => {
181
+ ctx.setEventCallback(undefined);
182
+ clearCloudflareContext();
183
+ console.log('[flue] Webhook handler complete:', agentName,
184
+ result !== undefined ? JSON.stringify(result) : '(no return)');
185
+ },
186
+ (err) => {
187
+ ctx.setEventCallback(undefined);
188
+ clearCloudflareContext();
189
+ console.error('[flue] Webhook handler error:', agentName, err);
190
+ },
191
+ );
192
+ return new Response(JSON.stringify({ status: 'accepted', requestId }), {
193
+ status: 202,
194
+ headers: { 'content-type': 'application/json' },
195
+ });
196
+ }
197
+
198
+ // SSE streaming mode
199
+ if (isSSE) {
200
+ const { readable, writable } = new TransformStream();
201
+ const writer = writable.getWriter();
202
+ const encoder = new TextEncoder();
203
+ let eventId = 0;
204
+
205
+ const writeSSE = async (data, event) => {
206
+ const lines = [];
207
+ if (event) lines.push('event: ' + event);
208
+ lines.push('id: ' + eventId++);
209
+ lines.push('data: ' + JSON.stringify(data));
210
+ lines.push('', '');
211
+ await writer.write(encoder.encode(lines.join('\\n')));
212
+ };
213
+
214
+ const ctx = createContextForRequest(sessionId, payload, doInstance);
215
+ ctx.setEventCallback((event) => {
216
+ writeSSE(event, event.type).catch(() => {});
217
+ });
218
+
219
+ (async () => {
220
+ try {
221
+ const result = await handler(ctx);
222
+ await writeSSE(
223
+ { type: 'result', data: result !== undefined ? result : null },
224
+ 'result',
225
+ );
226
+ } catch (err) {
227
+ await writeSSE(
228
+ { type: 'error', error: String(err) },
229
+ 'error',
230
+ );
231
+ } finally {
232
+ ctx.setEventCallback(undefined);
233
+ clearCloudflareContext();
234
+ await writer.close();
235
+ }
236
+ })();
237
+
238
+ return new Response(readable, {
239
+ headers: {
240
+ 'content-type': 'text/event-stream',
241
+ 'cache-control': 'no-cache',
242
+ 'connection': 'keep-alive',
243
+ },
244
+ });
245
+ }
246
+
247
+ // Sync mode (default)
248
+ const ctx = createContextForRequest(sessionId, payload, doInstance);
249
+ const result = await handler(ctx);
250
+ ctx.setEventCallback(undefined);
251
+ clearCloudflareContext();
252
+ return new Response(
253
+ JSON.stringify({ result: result !== undefined ? result : null }),
254
+ { headers: { 'content-type': 'application/json' } },
255
+ );
256
+ } catch (err) {
257
+ clearCloudflareContext();
258
+ console.error('[flue] Agent error:', agentName, err);
259
+ return new Response(
260
+ JSON.stringify({ error: String(err) }),
261
+ { status: 500, headers: { 'content-type': 'application/json' } },
262
+ );
263
+ }
264
+ }
265
+
266
+ // ─── Per-Agent Durable Object Classes ──────────────────────────────────────
267
+
268
+ ${agentClasses}
269
+
270
+ // Re-export Sandbox DO class for wrangler binding
271
+ export { Sandbox } from '@cloudflare/sandbox';
272
+
273
+ // ─── Worker Fetch Handler ───────────────────────────────────────────────────
274
+
275
+ export default {
276
+ async fetch(request, env) {
277
+ const url = new URL(request.url);
278
+
279
+ // Health check
280
+ if (url.pathname === '/health') {
281
+ return new Response(JSON.stringify({ status: 'ok' }), {
282
+ headers: { 'content-type': 'application/json' },
283
+ });
284
+ }
285
+
286
+ // Agent manifest
287
+ if (url.pathname === '/agents' && request.method === 'GET') {
288
+ return new Response(JSON.stringify(manifest), {
289
+ headers: { 'content-type': 'application/json' },
290
+ });
291
+ }
292
+
293
+ // Route to per-agent DOs via the Agents SDK
294
+ // URL: /agents/<agent-name>/<session-id>
295
+ const response = await routeAgentRequest(request, env);
296
+ if (response) return response;
297
+
298
+ return new Response('Not found', { status: 404 });
299
+ },
300
+ };
301
+ `;
302
+ }
303
+ esbuildOptions(_ctx) {
304
+ return {
305
+ target: "esnext",
306
+ external: ["node:*", "cloudflare:*"]
307
+ };
308
+ }
309
+ additionalOutputs(ctx) {
310
+ const outputs = {};
311
+ const allBindings = [...ctx.agents.filter((a) => a.triggers.webhook).map((a) => ({
312
+ class_name: agentClassName(a.name),
313
+ name: agentClassName(a.name)
314
+ })), {
315
+ class_name: "Sandbox",
316
+ name: "Sandbox"
317
+ }];
318
+ const allSqliteClasses = allBindings.map((b) => b.class_name);
319
+ const workerName = ctx.agentDir.split("/").pop() ?? "flue-agents";
320
+ outputs["wrangler.jsonc"] = JSON.stringify({
321
+ $schema: "https://workers.cloudflare.com/schema/wrangler.json",
322
+ name: workerName,
323
+ main: "server.mjs",
324
+ compatibility_date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
325
+ compatibility_flags: ["nodejs_compat"],
326
+ containers: [{
327
+ class_name: "Sandbox",
328
+ image: "./Dockerfile"
329
+ }],
330
+ durable_objects: { bindings: allBindings },
331
+ migrations: [{
332
+ new_sqlite_classes: allSqliteClasses,
333
+ tag: "v1"
334
+ }]
335
+ }, null, 2);
336
+ outputs["Dockerfile"] = [
337
+ "FROM node:22-slim",
338
+ "",
339
+ "# Install common tools for agent sandboxes",
340
+ "RUN apt-get update && apt-get install -y \\",
341
+ " git curl wget \\",
342
+ " python3 python3-pip \\",
343
+ " && rm -rf /var/lib/apt/lists/*",
344
+ "",
345
+ "WORKDIR /workspace",
346
+ "",
347
+ "# Keep container alive",
348
+ "CMD [\"sleep\", \"infinity\"]",
349
+ ""
350
+ ].join("\n");
351
+ return outputs;
352
+ }
353
+ };
354
+ function agentVarName$1(name) {
355
+ return "handler_" + name.replace(/[^a-zA-Z0-9]/g, "_");
356
+ }
357
+ /**
358
+ * Convert agent name to a PascalCase DO class name.
359
+ * "hello" → "Hello", "with-cloudflare" → "WithCloudflare"
360
+ *
361
+ * routeAgentRequest() converts binding names to kebab-case for URL matching,
362
+ * so "WithCloudflare" → "with-cloudflare" → URL /agents/with-cloudflare/:id
363
+ */
364
+ function agentClassName(name) {
365
+ return name.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
366
+ }
367
+
368
+ //#endregion
369
+ //#region src/build-plugin-node.ts
370
+ var NodePlugin = class {
371
+ name = "node";
372
+ generateEntryPoint(ctx) {
373
+ const { agents, roles, options, resolveSDKImport } = ctx;
374
+ const modelProvider = options.model?.provider ?? "anthropic";
375
+ const modelId = options.model?.modelId ?? "claude-haiku-4-5";
376
+ const rolesJson = JSON.stringify(roles);
377
+ const webhookAgents = agents.filter((a) => a.triggers.webhook);
378
+ const agentImports = agents.map((a) => {
379
+ return `import ${agentVarName(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
380
+ }).join("\n");
381
+ const handlerMapEntries = agents.map((a) => ` ${JSON.stringify(a.name)}: ${agentVarName(a.name)},`).join("\n");
382
+ const webhookNames = JSON.stringify(webhookAgents.map((a) => a.name));
383
+ const manifest = JSON.stringify({ agents: agents.map((a) => ({
384
+ name: a.name,
385
+ triggers: a.triggers
386
+ })) }, null, 2);
387
+ return `
388
+ // Auto-generated by @flue/sdk build (node)
389
+ import { Hono } from 'hono';
390
+ import { streamSSE } from 'hono/streaming';
391
+ import { serve } from '@hono/node-server';
392
+ import { Bash, InMemoryFs, MountableFs, ReadWriteFs } from 'just-bash';
393
+ import { getModel } from '@mariozechner/pi-ai';
394
+ import { createFlueContext } from '${resolveSDKImport("client")}';
395
+ import { InMemorySessionStore } from '${resolveSDKImport("session")}';
396
+ import { bashToSessionEnv } from '${resolveSDKImport("sandbox")}';
397
+ import { randomUUID } from 'node:crypto';
398
+
399
+ ${agentImports}
400
+
401
+ // ─── Config ─────────────────────────────────────────────────────────────────
402
+
403
+ const skills = {};
404
+ const roles = ${rolesJson};
405
+ const systemPrompt = '';
406
+
407
+ const handlers = {
408
+ ${handlerMapEntries}
409
+ };
410
+
411
+ const webhookAgents = new Set(${webhookNames});
412
+
413
+ const manifest = ${manifest};
414
+
415
+ // ─── Infrastructure ─────────────────────────────────────────────────────────
416
+
417
+ const model = getModel(${JSON.stringify(modelProvider)}, ${JSON.stringify(modelId)});
418
+
419
+ function resolveModel(modelString) {
420
+ const slash = modelString.indexOf('/');
421
+ if (slash !== -1) {
422
+ return getModel(modelString.slice(0, slash), modelString.slice(slash + 1));
423
+ }
424
+ return getModel(${JSON.stringify(modelProvider)}, modelString);
425
+ }
426
+
427
+ // ─── Sandbox Environments ───────────────────────────────────────────────────
428
+
429
+ /**
430
+ * Create an empty in-memory sandbox (default).
431
+ * Uses InMemoryFs (no real filesystem access) with sensible defaults:
432
+ * cwd = /home/user, /tmp exists, /bin and /usr/bin exist.
433
+ */
434
+ async function createDefaultEnv() {
435
+ const bash = new Bash({
436
+ fs: new InMemoryFs(),
437
+ network: { dangerouslyAllowFullInternetAccess: true },
438
+ });
439
+ return bashToSessionEnv(bash);
440
+ }
441
+
442
+ /**
443
+ * Create a local sandbox backed by the host filesystem.
444
+ * Mounts process.cwd() at /workspace via ReadWriteFs + MountableFs.
445
+ */
446
+ async function createLocalEnv() {
447
+ const rwfs = new ReadWriteFs({ root: process.cwd() });
448
+ const fs = new MountableFs({ base: new InMemoryFs() });
449
+ fs.mount('/workspace', rwfs);
450
+ const bash = new Bash({
451
+ fs,
452
+ cwd: '/workspace',
453
+ network: { dangerouslyAllowFullInternetAccess: true },
454
+ });
455
+ return bashToSessionEnv(bash);
456
+ }
457
+
458
+ // Default persistence store for Node — in-memory, process lifetime.
459
+ const defaultStore = new InMemorySessionStore();
460
+
461
+ function createContextForRequest(sessionId, payload) {
462
+ return createFlueContext({
463
+ sessionId,
464
+ payload,
465
+ env: process.env,
466
+ agentConfig: {
467
+ systemPrompt, skills, roles, model, resolveModel,
468
+ },
469
+ createDefaultEnv,
470
+ createLocalEnv,
471
+ defaultStore,
472
+ });
473
+ }
474
+
475
+ // ─── Server ─────────────────────────────────────────────────────────────────
476
+
477
+ const app = new Hono();
478
+
479
+ app.get('/health', (c) => c.json({ status: 'ok' }));
480
+ app.get('/agents', (c) => c.json(manifest));
481
+
482
+ // Session ID is required in the URL
483
+ app.post('/agents/:name', (c) => {
484
+ return c.json({
485
+ error: 'Session ID is required. Use /agents/:name/:sessionId',
486
+ }, 400);
487
+ });
488
+
489
+ app.post('/agents/:name/:sessionId', async (c) => {
490
+ const name = c.req.param('name');
491
+ const sessionId = c.req.param('sessionId');
492
+
493
+ if (!handlers[name]) {
494
+ return c.json({ error: 'Agent not found' }, 404);
495
+ }
496
+ if (!webhookAgents.has(name)) {
497
+ return c.json({ error: 'Agent "' + name + '" is not web-accessible (no webhook trigger)' }, 404);
498
+ }
499
+
500
+ const handler = handlers[name];
501
+ let payload;
502
+ try {
503
+ payload = await c.req.json();
504
+ } catch {
505
+ payload = {};
506
+ }
507
+
508
+ const accept = c.req.header('accept') || '';
509
+ const isWebhook = c.req.header('x-webhook') === 'true';
510
+ const isSSE = accept.includes('text/event-stream') && !isWebhook;
511
+
512
+ // Fire-and-forget (webhook mode)
513
+ if (isWebhook) {
514
+ const requestId = randomUUID();
515
+ const ctx = createContextForRequest(sessionId, payload);
516
+ handler(ctx).then(
517
+ (result) => {
518
+ ctx.setEventCallback(undefined);
519
+ console.log('[flue] Webhook handler complete:', name, result !== undefined ? JSON.stringify(result) : '(no return)');
520
+ },
521
+ (err) => {
522
+ ctx.setEventCallback(undefined);
523
+ console.error('[flue] Webhook handler error:', name, err);
524
+ },
525
+ );
526
+ return c.json({ status: 'accepted', requestId }, 202);
527
+ }
528
+
529
+ // SSE streaming mode
530
+ if (isSSE) {
531
+ return streamSSE(c, async (stream) => {
532
+ let eventId = 0;
533
+ const ctx = createContextForRequest(sessionId, payload);
534
+ ctx.setEventCallback((event) => {
535
+ stream.writeSSE({ data: JSON.stringify(event), event: event.type, id: String(eventId++) }).catch(() => {});
536
+ });
537
+
538
+ try {
539
+ const result = await handler(ctx);
540
+ await stream.writeSSE({
541
+ data: JSON.stringify({ type: 'result', data: result !== undefined ? result : null }),
542
+ event: 'result',
543
+ id: String(eventId++),
544
+ });
545
+ } catch (err) {
546
+ await stream.writeSSE({
547
+ data: JSON.stringify({ type: 'error', error: String(err) }),
548
+ event: 'error',
549
+ id: String(eventId++),
550
+ });
551
+ } finally {
552
+ ctx.setEventCallback(undefined);
553
+ }
554
+ });
555
+ }
556
+
557
+ // Sync mode (default)
558
+ try {
559
+ const ctx = createContextForRequest(sessionId, payload);
560
+ const result = await handler(ctx);
561
+ ctx.setEventCallback(undefined);
562
+ return c.json({ result: result !== undefined ? result : null });
563
+ } catch (err) {
564
+ console.error('[flue] Agent error:', name, err);
565
+ return c.json({ error: String(err) }, 500);
566
+ }
567
+ });
568
+
569
+ // ─── Start ──────────────────────────────────────────────────────────────────
570
+
571
+ const port = parseInt(process.env.PORT || '3000', 10);
572
+
573
+ const server = serve({ fetch: app.fetch, port });
574
+ console.log('[flue] Server listening on http://localhost:' + port);
575
+ console.log('[flue] Available agents: ' + ${JSON.stringify(webhookAgents.map((a) => a.name).join(", "))});
576
+
577
+ process.on('SIGINT', () => { server.close(); process.exit(0); });
578
+ process.on('SIGTERM', () => { server.close(); process.exit(0); });
579
+ `;
580
+ }
581
+ esbuildOptions(_ctx) {
582
+ return {
583
+ platform: "node",
584
+ target: "node22"
585
+ };
586
+ }
587
+ };
588
+ function agentVarName(name) {
589
+ return "handler_" + name.replace(/[^a-zA-Z0-9]/g, "_");
590
+ }
591
+
592
+ //#endregion
593
+ //#region src/build.ts
594
+ /**
595
+ * Build a workspace into a deployable artifact.
596
+ * AGENTS.md and .agents/skills/ are NOT bundled — discovered at runtime from session cwd.
597
+ */
598
+ async function build(options) {
599
+ const agentDir = path.resolve(options.agentDir);
600
+ const plugin = resolvePlugin(options);
601
+ console.log(`[flue] Building workspace: ${agentDir}`);
602
+ console.log(`[flue] Target: ${plugin.name}`);
603
+ const roles = discoverRoles(agentDir);
604
+ const agents = discoverAgents(agentDir);
605
+ if (agents.length === 0) throw new Error(`No agents found in ${path.join(agentDir, ".flue/agents/")}`);
606
+ for (const agent of agents) if (!(agent.triggers.webhook || agent.triggers.cron)) throw new Error(`[flue] Agent "${agent.name}" has no triggers configured. Add a triggers export to your agent file:\n\n export const triggers = { webhook: true };\n\nAvailable triggers: webhook (HTTP endpoint), cron (scheduled)`);
607
+ const webhookAgents = agents.filter((a) => a.triggers.webhook);
608
+ const cronAgents = agents.filter((a) => a.triggers.cron);
609
+ console.log(`[flue] Found ${Object.keys(roles).length} role(s): ${Object.keys(roles).join(", ") || "(none)"}`);
610
+ console.log(`[flue] Found ${agents.length} agent(s): ${agents.map((a) => a.name).join(", ")}`);
611
+ if (webhookAgents.length > 0) console.log(`[flue] Webhook agents: ${webhookAgents.map((a) => a.name).join(", ")}`);
612
+ if (cronAgents.length > 0) console.log(`[flue] Cron agents (manifest only): ${cronAgents.map((a) => `${a.name} (${a.triggers.cron})`).join(", ")}`);
613
+ console.log(`[flue] AGENTS.md and .agents/skills/ will be discovered at runtime from session cwd`);
614
+ const distDir = path.join(agentDir, "dist");
615
+ fs.mkdirSync(distDir, { recursive: true });
616
+ const manifest = { agents: agents.map((a) => ({
617
+ name: a.name,
618
+ triggers: a.triggers
619
+ })) };
620
+ const manifestPath = path.join(distDir, "manifest.json");
621
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
622
+ console.log(`[flue] Generated: ${manifestPath}`);
623
+ const ctx = {
624
+ agents,
625
+ roles,
626
+ agentDir,
627
+ options,
628
+ resolveSDKImport: resolveSDKImportFn
629
+ };
630
+ const serverCode = plugin.generateEntryPoint(ctx);
631
+ const entryPath = path.join(distDir, "_entry_server.ts");
632
+ const outPath = path.join(distDir, "server.mjs");
633
+ fs.writeFileSync(entryPath, serverCode, "utf-8");
634
+ try {
635
+ const nodePathsSet = collectNodePaths(agentDir);
636
+ const { external: pluginExternal = [], ...pluginEsbuildOpts } = plugin.esbuildOptions(ctx);
637
+ const userExternals = getUserExternals(agentDir);
638
+ await esbuild.build({
639
+ entryPoints: [entryPath],
640
+ bundle: true,
641
+ outfile: outPath,
642
+ format: "esm",
643
+ external: [...pluginExternal, ...userExternals],
644
+ nodePaths: [...nodePathsSet],
645
+ logLevel: "warning",
646
+ loader: {
647
+ ".ts": "ts",
648
+ ".node": "empty"
649
+ },
650
+ treeShaking: true,
651
+ sourcemap: true,
652
+ ...pluginEsbuildOpts
653
+ });
654
+ console.log(`[flue] Built: ${outPath}`);
655
+ } finally {
656
+ try {
657
+ fs.unlinkSync(entryPath);
658
+ } catch {}
659
+ }
660
+ if (plugin.additionalOutputs) {
661
+ const outputs = plugin.additionalOutputs(ctx);
662
+ for (const [filename, content] of Object.entries(outputs)) {
663
+ const filePath = path.join(distDir, filename);
664
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
665
+ fs.writeFileSync(filePath, content, "utf-8");
666
+ console.log(`[flue] Generated: ${filePath}`);
667
+ }
668
+ }
669
+ console.log(`[flue] Build complete. Output: ${distDir}`);
670
+ }
671
+ function resolvePlugin(options) {
672
+ if (options.plugin) return options.plugin;
673
+ if (!options.target) throw new Error("[flue] No build target specified. Use --target to choose a target:\n flue build --target node\n flue build --target cloudflare");
674
+ switch (options.target) {
675
+ case "node": return new NodePlugin();
676
+ case "cloudflare": return new CloudflarePlugin();
677
+ default: throw new Error(`[flue] Unknown target: "${options.target}". Supported targets: node, cloudflare`);
678
+ }
679
+ }
680
+ function discoverRoles(agentDir) {
681
+ const rolesDir = path.join(agentDir, ".flue", "roles");
682
+ if (!fs.existsSync(rolesDir)) return {};
683
+ const roles = {};
684
+ for (const entry of fs.readdirSync(rolesDir)) {
685
+ if (!/\.(md|markdown)$/i.test(entry)) continue;
686
+ const filePath = path.join(rolesDir, entry);
687
+ const content = fs.readFileSync(filePath, "utf-8");
688
+ const name = entry.replace(/\.(md|markdown)$/i, "");
689
+ const parsed = parseFrontmatterFile(content, name);
690
+ roles[name] = {
691
+ name,
692
+ description: parsed.description,
693
+ instructions: parsed.body,
694
+ model: parsed.frontmatter.model
695
+ };
696
+ }
697
+ return roles;
698
+ }
699
+ function discoverAgents(agentDir) {
700
+ let agentsDir = path.join(agentDir, ".flue", "agents");
701
+ if (!fs.existsSync(agentsDir)) {
702
+ agentsDir = path.join(agentDir, ".flue", "workflows");
703
+ if (!fs.existsSync(agentsDir)) return [];
704
+ }
705
+ return fs.readdirSync(agentsDir).filter((f) => /\.(ts|js|mts|mjs)$/.test(f)).map((f) => {
706
+ const filePath = path.join(agentsDir, f);
707
+ const triggers = parseTriggers(filePath);
708
+ return {
709
+ name: f.replace(/\.(ts|js|mts|mjs)$/, ""),
710
+ filePath,
711
+ triggers
712
+ };
713
+ });
714
+ }
715
+ /** Extract trigger config via regex. Only triggers are parsed at build time (needed for routing). */
716
+ function parseTriggers(filePath) {
717
+ const source = fs.readFileSync(filePath, "utf-8");
718
+ const result = {};
719
+ const triggersExportMatch = source.match(/export\s+const\s+triggers\s*=\s*\{([^}]*)\}/);
720
+ if (!triggersExportMatch) return result;
721
+ const triggersBlock = triggersExportMatch[1] ?? "";
722
+ if (/webhook\s*:\s*true/.test(triggersBlock)) result.webhook = true;
723
+ const cronMatch = triggersBlock.match(/cron\s*:\s*['"]([^'"]+)['"]/);
724
+ if (cronMatch?.[1]) result.cron = cronMatch[1];
725
+ return result;
726
+ }
727
+ /** Externalize user's direct deps (bare name + subpath wildcard). */
728
+ function getUserExternals(agentDir) {
729
+ const pkgPath = packageUpSync({ cwd: agentDir });
730
+ if (!pkgPath) return [];
731
+ try {
732
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
733
+ return Object.keys({
734
+ ...pkg.dependencies,
735
+ ...pkg.devDependencies,
736
+ ...pkg.peerDependencies
737
+ }).flatMap((name) => [name, `${name}/*`]);
738
+ } catch {
739
+ return [];
740
+ }
741
+ }
742
+ function collectNodePaths(agentDir) {
743
+ const nodePathsSet = /* @__PURE__ */ new Set();
744
+ for (const startDir of [agentDir, getSDKDir()]) {
745
+ let dir = startDir;
746
+ while (dir !== path.dirname(dir)) {
747
+ const nm = path.join(dir, "node_modules");
748
+ if (fs.existsSync(nm)) nodePathsSet.add(nm);
749
+ dir = path.dirname(dir);
750
+ }
751
+ }
752
+ return nodePathsSet;
753
+ }
754
+ function getSDKDir() {
755
+ try {
756
+ return path.dirname(new URL(import.meta.url).pathname);
757
+ } catch {
758
+ return __dirname;
759
+ }
760
+ }
761
+ function getSDKSrcDir() {
762
+ const thisDir = getSDKDir();
763
+ if (thisDir.endsWith("/dist") || thisDir.endsWith("\\dist")) {
764
+ const srcDir = path.join(path.dirname(thisDir), "src");
765
+ if (fs.existsSync(srcDir)) return srcDir;
766
+ }
767
+ return thisDir;
768
+ }
769
+ function resolveSDKImportFn(module) {
770
+ const srcDir = getSDKSrcDir();
771
+ return path.join(srcDir, `${module}.ts`).replace(/\\/g, "/");
772
+ }
773
+
774
+ //#endregion
775
+ export { BUILTIN_TOOL_NAMES, InMemorySessionStore, build, createFlueContext, createTools };