@flue/sdk 0.1.3 → 0.3.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 DELETED
@@ -1,791 +0,0 @@
1
- import { a as parseFrontmatterFile, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BYG0nVbQ.mjs";
2
- import * as esbuild from "esbuild";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import { packageUpSync } from "package-up";
6
-
7
- //#region src/build-plugin-cloudflare.ts
8
- var CloudflarePlugin = class {
9
- name = "cloudflare";
10
- generateEntryPoint(ctx) {
11
- const { agents, roles } = ctx;
12
- const rolesJson = JSON.stringify(roles);
13
- const webhookAgents = agents.filter((a) => a.triggers.webhook);
14
- return `
15
- // Auto-generated by @flue/sdk build (cloudflare)
16
- import { Agent, routeAgentRequest } from 'agents';
17
- import { Bash, InMemoryFs } from 'just-bash';
18
- import { getModel } from '@mariozechner/pi-ai';
19
- import { createFlueContext, InMemorySessionStore, bashToSessionEnv } from '@flue/sdk/internal';
20
- import { setCloudflareContext, clearCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
21
-
22
- ${agents.map((a) => {
23
- return `import ${agentVarName$1(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
24
- }).join("\n")}
25
-
26
- // ─── Config ─────────────────────────────────────────────────────────────────
27
-
28
- const roles = ${rolesJson};
29
- const skills = {};
30
- const systemPrompt = '';
31
- const manifest = ${JSON.stringify({ agents: agents.map((a) => ({
32
- name: a.name,
33
- triggers: a.triggers
34
- })) }, null, 2)};
35
-
36
- // ─── Infrastructure ─────────────────────────────────────────────────────────
37
-
38
- // No build-time model default. The user sets model at runtime via
39
- // \`init({ model: "provider/model-id" })\` for a session default, or via
40
- // \`{ model: "provider/model-id" }\` on any individual prompt/skill/task call.
41
- const model = undefined;
42
-
43
- function resolveModel(modelString) {
44
- const slash = modelString.indexOf('/');
45
- if (slash === -1) {
46
- throw new Error(
47
- '[flue] Invalid model "' + modelString + '". ' +
48
- 'Use the "provider/model-id" format (e.g. "anthropic/claude-haiku-4-5").'
49
- );
50
- }
51
- const provider = modelString.slice(0, slash);
52
- const modelId = modelString.slice(slash + 1);
53
- const resolved = getModel(provider, modelId);
54
- if (!resolved) {
55
- throw new Error(
56
- '[flue] Unknown model "' + modelString + '". ' +
57
- 'Provider "' + provider + '" / model id "' + modelId + '" ' +
58
- 'is not registered with @mariozechner/pi-ai.'
59
- );
60
- }
61
- return resolved;
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
- ${webhookAgents.map((a) => {
269
- const className = agentClassName(a.name);
270
- const handlerVar = agentVarName$1(a.name);
271
- return `export class ${className} extends Agent {
272
- async onRequest(request) {
273
- return handleAgentRequest(request, this, ${JSON.stringify(a.name)}, ${handlerVar});
274
- }
275
- }`;
276
- }).join("\n\n")}
277
-
278
- // Re-export Sandbox DO class for wrangler binding
279
- export { Sandbox } from '@cloudflare/sandbox';
280
-
281
- // ─── Worker Fetch Handler ───────────────────────────────────────────────────
282
-
283
- export default {
284
- async fetch(request, env) {
285
- const url = new URL(request.url);
286
-
287
- // Health check
288
- if (url.pathname === '/health') {
289
- return new Response(JSON.stringify({ status: 'ok' }), {
290
- headers: { 'content-type': 'application/json' },
291
- });
292
- }
293
-
294
- // Agent manifest
295
- if (url.pathname === '/agents' && request.method === 'GET') {
296
- return new Response(JSON.stringify(manifest), {
297
- headers: { 'content-type': 'application/json' },
298
- });
299
- }
300
-
301
- // Route to per-agent DOs via the Agents SDK
302
- // URL: /agents/<agent-name>/<session-id>
303
- const response = await routeAgentRequest(request, env);
304
- if (response) return response;
305
-
306
- return new Response('Not found', { status: 404 });
307
- },
308
- };
309
- `;
310
- }
311
- esbuildOptions(_ctx) {
312
- return {
313
- target: "esnext",
314
- external: ["node:*", "cloudflare:*"]
315
- };
316
- }
317
- additionalOutputs(ctx) {
318
- const outputs = {};
319
- const allBindings = [...ctx.agents.filter((a) => a.triggers.webhook).map((a) => ({
320
- class_name: agentClassName(a.name),
321
- name: agentClassName(a.name)
322
- })), {
323
- class_name: "Sandbox",
324
- name: "Sandbox"
325
- }];
326
- const allSqliteClasses = allBindings.map((b) => b.class_name);
327
- const workerName = ctx.agentDir.split("/").pop() ?? "flue-agents";
328
- outputs["wrangler.jsonc"] = JSON.stringify({
329
- $schema: "https://workers.cloudflare.com/schema/wrangler.json",
330
- name: workerName,
331
- main: "server.mjs",
332
- compatibility_date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
333
- compatibility_flags: ["nodejs_compat"],
334
- containers: [{
335
- class_name: "Sandbox",
336
- image: "./Dockerfile"
337
- }],
338
- durable_objects: { bindings: allBindings },
339
- migrations: [{
340
- new_sqlite_classes: allSqliteClasses,
341
- tag: "v1"
342
- }]
343
- }, null, 2);
344
- outputs["Dockerfile"] = [
345
- "FROM node:22-slim",
346
- "",
347
- "# Install common tools for agent sandboxes",
348
- "RUN apt-get update && apt-get install -y \\",
349
- " git curl wget \\",
350
- " python3 python3-pip \\",
351
- " && rm -rf /var/lib/apt/lists/*",
352
- "",
353
- "WORKDIR /workspace",
354
- "",
355
- "# Keep container alive",
356
- "CMD [\"sleep\", \"infinity\"]",
357
- ""
358
- ].join("\n");
359
- return outputs;
360
- }
361
- };
362
- function agentVarName$1(name) {
363
- return "handler_" + name.replace(/[^a-zA-Z0-9]/g, "_");
364
- }
365
- /**
366
- * Convert agent name to a PascalCase DO class name.
367
- * "hello" → "Hello", "with-cloudflare" → "WithCloudflare"
368
- *
369
- * routeAgentRequest() converts binding names to kebab-case for URL matching,
370
- * so "WithCloudflare" → "with-cloudflare" → URL /agents/with-cloudflare/:id
371
- */
372
- function agentClassName(name) {
373
- return name.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
374
- }
375
-
376
- //#endregion
377
- //#region src/build-plugin-node.ts
378
- var NodePlugin = class {
379
- name = "node";
380
- generateEntryPoint(ctx) {
381
- const { agents, roles } = ctx;
382
- const rolesJson = JSON.stringify(roles);
383
- const webhookAgents = agents.filter((a) => a.triggers.webhook);
384
- return `
385
- // Auto-generated by @flue/sdk build (node)
386
- import { Hono } from 'hono';
387
- import { streamSSE } from 'hono/streaming';
388
- import { serve } from '@hono/node-server';
389
- import { Bash, InMemoryFs, MountableFs, ReadWriteFs } from 'just-bash';
390
- import { getModel } from '@mariozechner/pi-ai';
391
- import { createFlueContext, InMemorySessionStore, bashToSessionEnv } from '@flue/sdk/internal';
392
- import { randomUUID } from 'node:crypto';
393
-
394
- ${agents.map((a) => {
395
- return `import ${agentVarName(a.name)} from '${a.filePath.replace(/\\/g, "/")}';`;
396
- }).join("\n")}
397
-
398
- // ─── Config ─────────────────────────────────────────────────────────────────
399
-
400
- const skills = {};
401
- const roles = ${rolesJson};
402
- const systemPrompt = '';
403
-
404
- const handlers = {
405
- ${agents.map((a) => ` ${JSON.stringify(a.name)}: ${agentVarName(a.name)},`).join("\n")}
406
- };
407
-
408
- const webhookAgents = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
409
-
410
- // When the CLI starts this server via \`flue run\`, it sets FLUE_MODE=local.
411
- // In local mode the HTTP route accepts any registered agent (including
412
- // trigger-less CI-only agents). In any other mode the route is restricted to
413
- // agents with \`webhook: true\`, preventing accidental public exposure of
414
- // agents that the user only intended to invoke from their CI pipeline.
415
- const isLocalMode = process.env.FLUE_MODE === 'local';
416
-
417
- const manifest = ${JSON.stringify({ agents: agents.map((a) => ({
418
- name: a.name,
419
- triggers: a.triggers
420
- })) }, null, 2)};
421
-
422
- // ─── Infrastructure ─────────────────────────────────────────────────────────
423
-
424
- // No build-time model default. The user sets model at runtime via
425
- // \`init({ model: "provider/model-id" })\` for a session default, or via
426
- // \`{ model: "provider/model-id" }\` on any individual prompt/skill/task call.
427
- const model = undefined;
428
-
429
- function resolveModel(modelString) {
430
- const slash = modelString.indexOf('/');
431
- if (slash === -1) {
432
- throw new Error(
433
- '[flue] Invalid model "' + modelString + '". ' +
434
- 'Use the "provider/model-id" format (e.g. "anthropic/claude-haiku-4-5").'
435
- );
436
- }
437
- const provider = modelString.slice(0, slash);
438
- const modelId = modelString.slice(slash + 1);
439
- const resolved = getModel(provider, modelId);
440
- if (!resolved) {
441
- throw new Error(
442
- '[flue] Unknown model "' + modelString + '". ' +
443
- 'Provider "' + provider + '" / model id "' + modelId + '" ' +
444
- 'is not registered with @mariozechner/pi-ai.'
445
- );
446
- }
447
- return resolved;
448
- }
449
-
450
- // ─── Sandbox Environments ───────────────────────────────────────────────────
451
-
452
- /**
453
- * Create an empty in-memory sandbox (default).
454
- * Uses InMemoryFs (no real filesystem access) with sensible defaults:
455
- * cwd = /home/user, /tmp exists, /bin and /usr/bin exist.
456
- */
457
- async function createDefaultEnv() {
458
- const bash = new Bash({
459
- fs: new InMemoryFs(),
460
- network: { dangerouslyAllowFullInternetAccess: true },
461
- });
462
- return bashToSessionEnv(bash);
463
- }
464
-
465
- /**
466
- * Create a local sandbox backed by the host filesystem.
467
- * Mounts process.cwd() at /workspace via ReadWriteFs + MountableFs.
468
- */
469
- async function createLocalEnv() {
470
- const rwfs = new ReadWriteFs({ root: process.cwd() });
471
- const fs = new MountableFs({ base: new InMemoryFs() });
472
- fs.mount('/workspace', rwfs);
473
- const bash = new Bash({
474
- fs,
475
- cwd: '/workspace',
476
- network: { dangerouslyAllowFullInternetAccess: true },
477
- });
478
- return bashToSessionEnv(bash);
479
- }
480
-
481
- // Default persistence store for Node — in-memory, process lifetime.
482
- const defaultStore = new InMemorySessionStore();
483
-
484
- function createContextForRequest(sessionId, payload) {
485
- return createFlueContext({
486
- sessionId,
487
- payload,
488
- env: process.env,
489
- agentConfig: {
490
- systemPrompt, skills, roles, model, resolveModel,
491
- },
492
- createDefaultEnv,
493
- createLocalEnv,
494
- defaultStore,
495
- });
496
- }
497
-
498
- // ─── Server ─────────────────────────────────────────────────────────────────
499
-
500
- const app = new Hono();
501
-
502
- app.get('/health', (c) => c.json({ status: 'ok' }));
503
- app.get('/agents', (c) => c.json(manifest));
504
-
505
- // Session ID is required in the URL
506
- app.post('/agents/:name', (c) => {
507
- return c.json({
508
- error: 'Session ID is required. Use /agents/:name/:sessionId',
509
- }, 400);
510
- });
511
-
512
- app.post('/agents/:name/:sessionId', async (c) => {
513
- const name = c.req.param('name');
514
- const sessionId = c.req.param('sessionId');
515
-
516
- if (!handlers[name]) {
517
- return c.json({ error: 'Agent not found' }, 404);
518
- }
519
- if (!webhookAgents.has(name) && !isLocalMode) {
520
- return c.json({ error: 'Agent "' + name + '" is not web-accessible (no webhook trigger)' }, 404);
521
- }
522
-
523
- const handler = handlers[name];
524
- let payload;
525
- try {
526
- payload = await c.req.json();
527
- } catch {
528
- payload = {};
529
- }
530
-
531
- const accept = c.req.header('accept') || '';
532
- const isWebhook = c.req.header('x-webhook') === 'true';
533
- const isSSE = accept.includes('text/event-stream') && !isWebhook;
534
-
535
- // Fire-and-forget (webhook mode)
536
- if (isWebhook) {
537
- const requestId = randomUUID();
538
- const ctx = createContextForRequest(sessionId, payload);
539
- handler(ctx).then(
540
- (result) => {
541
- ctx.setEventCallback(undefined);
542
- console.log('[flue] Webhook handler complete:', name, result !== undefined ? JSON.stringify(result) : '(no return)');
543
- },
544
- (err) => {
545
- ctx.setEventCallback(undefined);
546
- console.error('[flue] Webhook handler error:', name, err);
547
- },
548
- );
549
- return c.json({ status: 'accepted', requestId }, 202);
550
- }
551
-
552
- // SSE streaming mode
553
- if (isSSE) {
554
- return streamSSE(c, async (stream) => {
555
- let eventId = 0;
556
- const ctx = createContextForRequest(sessionId, payload);
557
- ctx.setEventCallback((event) => {
558
- stream.writeSSE({ data: JSON.stringify(event), event: event.type, id: String(eventId++) }).catch(() => {});
559
- });
560
-
561
- try {
562
- const result = await handler(ctx);
563
- await stream.writeSSE({
564
- data: JSON.stringify({ type: 'result', data: result !== undefined ? result : null }),
565
- event: 'result',
566
- id: String(eventId++),
567
- });
568
- } catch (err) {
569
- await stream.writeSSE({
570
- data: JSON.stringify({ type: 'error', error: String(err) }),
571
- event: 'error',
572
- id: String(eventId++),
573
- });
574
- } finally {
575
- ctx.setEventCallback(undefined);
576
- }
577
- });
578
- }
579
-
580
- // Sync mode (default)
581
- try {
582
- const ctx = createContextForRequest(sessionId, payload);
583
- const result = await handler(ctx);
584
- ctx.setEventCallback(undefined);
585
- return c.json({ result: result !== undefined ? result : null });
586
- } catch (err) {
587
- console.error('[flue] Agent error:', name, err);
588
- return c.json({ error: String(err) }, 500);
589
- }
590
- });
591
-
592
- // ─── Start ──────────────────────────────────────────────────────────────────
593
-
594
- const port = parseInt(process.env.PORT || '3000', 10);
595
-
596
- const server = serve({ fetch: app.fetch, port });
597
- console.log('[flue] Server listening on http://localhost:' + port);
598
- if (isLocalMode) {
599
- console.log('[flue] Mode: local (all agents invokable, including trigger-less)');
600
- console.log('[flue] Available agents: ' + ${JSON.stringify(agents.map((a) => a.name).join(", "))});
601
- } else {
602
- console.log('[flue] Available agents: ' + ${JSON.stringify(webhookAgents.map((a) => a.name).join(", "))});
603
- }
604
-
605
- process.on('SIGINT', () => { server.close(); process.exit(0); });
606
- process.on('SIGTERM', () => { server.close(); process.exit(0); });
607
- `;
608
- }
609
- esbuildOptions(_ctx) {
610
- return {
611
- platform: "node",
612
- target: "node22"
613
- };
614
- }
615
- };
616
- function agentVarName(name) {
617
- return "handler_" + name.replace(/[^a-zA-Z0-9]/g, "_");
618
- }
619
-
620
- //#endregion
621
- //#region src/build.ts
622
- /**
623
- * Build a workspace into a deployable artifact.
624
- * AGENTS.md and .agents/skills/ are NOT bundled — discovered at runtime from session cwd.
625
- */
626
- async function build(options) {
627
- const agentDir = path.resolve(options.agentDir);
628
- const plugin = resolvePlugin(options);
629
- console.log(`[flue] Building workspace: ${agentDir}`);
630
- console.log(`[flue] Target: ${plugin.name}`);
631
- const roles = discoverRoles(agentDir);
632
- const agents = discoverAgents(agentDir);
633
- if (agents.length === 0) throw new Error(`No agents found in ${path.join(agentDir, ".flue/agents/")}`);
634
- const webhookAgents = agents.filter((a) => a.triggers.webhook);
635
- const cronAgents = agents.filter((a) => a.triggers.cron);
636
- const triggerlessAgents = agents.filter((a) => !a.triggers.webhook && !a.triggers.cron);
637
- console.log(`[flue] Found ${Object.keys(roles).length} role(s): ${Object.keys(roles).join(", ") || "(none)"}`);
638
- console.log(`[flue] Found ${agents.length} agent(s): ${agents.map((a) => a.name).join(", ")}`);
639
- if (webhookAgents.length > 0) console.log(`[flue] Webhook agents: ${webhookAgents.map((a) => a.name).join(", ")}`);
640
- if (cronAgents.length > 0) console.log(`[flue] Cron agents (manifest only): ${cronAgents.map((a) => `${a.name} (${a.triggers.cron})`).join(", ")}`);
641
- if (triggerlessAgents.length > 0) console.log(`[flue] CLI-only agents (no HTTP route in deployed build): ${triggerlessAgents.map((a) => a.name).join(", ")}`);
642
- console.log(`[flue] AGENTS.md and .agents/skills/ will be discovered at runtime from session cwd`);
643
- const distDir = path.join(agentDir, "dist");
644
- fs.mkdirSync(distDir, { recursive: true });
645
- const manifest = { agents: agents.map((a) => ({
646
- name: a.name,
647
- triggers: a.triggers
648
- })) };
649
- const manifestPath = path.join(distDir, "manifest.json");
650
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
651
- console.log(`[flue] Generated: ${manifestPath}`);
652
- const ctx = {
653
- agents,
654
- roles,
655
- agentDir,
656
- options
657
- };
658
- const serverCode = plugin.generateEntryPoint(ctx);
659
- const entryPath = path.join(distDir, "_entry_server.ts");
660
- const outPath = path.join(distDir, "server.mjs");
661
- fs.writeFileSync(entryPath, serverCode, "utf-8");
662
- try {
663
- const nodePathsSet = collectNodePaths(agentDir);
664
- const { external: pluginExternal = [], ...pluginEsbuildOpts } = plugin.esbuildOptions(ctx);
665
- const userExternals = getUserExternals(agentDir);
666
- await esbuild.build({
667
- entryPoints: [entryPath],
668
- bundle: true,
669
- outfile: outPath,
670
- format: "esm",
671
- external: [...pluginExternal, ...userExternals],
672
- nodePaths: [...nodePathsSet],
673
- logLevel: "warning",
674
- loader: {
675
- ".ts": "ts",
676
- ".node": "empty"
677
- },
678
- treeShaking: true,
679
- sourcemap: true,
680
- ...pluginEsbuildOpts
681
- });
682
- console.log(`[flue] Built: ${outPath}`);
683
- } finally {
684
- try {
685
- fs.unlinkSync(entryPath);
686
- } catch {}
687
- }
688
- if (plugin.additionalOutputs) {
689
- const outputs = plugin.additionalOutputs(ctx);
690
- for (const [filename, content] of Object.entries(outputs)) {
691
- const filePath = path.join(distDir, filename);
692
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
693
- fs.writeFileSync(filePath, content, "utf-8");
694
- console.log(`[flue] Generated: ${filePath}`);
695
- }
696
- }
697
- console.log(`[flue] Build complete. Output: ${distDir}`);
698
- }
699
- function resolvePlugin(options) {
700
- if (options.plugin) return options.plugin;
701
- 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");
702
- switch (options.target) {
703
- case "node": return new NodePlugin();
704
- case "cloudflare": return new CloudflarePlugin();
705
- default: throw new Error(`[flue] Unknown target: "${options.target}". Supported targets: node, cloudflare`);
706
- }
707
- }
708
- function discoverRoles(agentDir) {
709
- const rolesDir = path.join(agentDir, ".flue", "roles");
710
- if (!fs.existsSync(rolesDir)) return {};
711
- const roles = {};
712
- for (const entry of fs.readdirSync(rolesDir)) {
713
- if (!/\.(md|markdown)$/i.test(entry)) continue;
714
- const filePath = path.join(rolesDir, entry);
715
- const content = fs.readFileSync(filePath, "utf-8");
716
- const name = entry.replace(/\.(md|markdown)$/i, "");
717
- const parsed = parseFrontmatterFile(content, name);
718
- roles[name] = {
719
- name,
720
- description: parsed.description,
721
- instructions: parsed.body,
722
- model: parsed.frontmatter.model
723
- };
724
- }
725
- return roles;
726
- }
727
- function discoverAgents(agentDir) {
728
- let agentsDir = path.join(agentDir, ".flue", "agents");
729
- if (!fs.existsSync(agentsDir)) {
730
- agentsDir = path.join(agentDir, ".flue", "workflows");
731
- if (!fs.existsSync(agentsDir)) return [];
732
- }
733
- return fs.readdirSync(agentsDir).filter((f) => /\.(ts|js|mts|mjs)$/.test(f)).map((f) => {
734
- const filePath = path.join(agentsDir, f);
735
- const triggers = parseTriggers(filePath);
736
- return {
737
- name: f.replace(/\.(ts|js|mts|mjs)$/, ""),
738
- filePath,
739
- triggers
740
- };
741
- });
742
- }
743
- /** Extract trigger config via regex. Only triggers are parsed at build time (needed for routing). */
744
- function parseTriggers(filePath) {
745
- const source = fs.readFileSync(filePath, "utf-8");
746
- const result = {};
747
- const triggersExportMatch = source.match(/export\s+const\s+triggers\s*=\s*\{([^}]*)\}/);
748
- if (!triggersExportMatch) return result;
749
- const triggersBlock = triggersExportMatch[1] ?? "";
750
- if (/webhook\s*:\s*true/.test(triggersBlock)) result.webhook = true;
751
- const cronMatch = triggersBlock.match(/cron\s*:\s*['"]([^'"]+)['"]/);
752
- if (cronMatch?.[1]) result.cron = cronMatch[1];
753
- return result;
754
- }
755
- /** Externalize user's direct deps (bare name + subpath wildcard). */
756
- function getUserExternals(agentDir) {
757
- const pkgPath = packageUpSync({ cwd: agentDir });
758
- if (!pkgPath) return [];
759
- try {
760
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
761
- return Object.keys({
762
- ...pkg.dependencies,
763
- ...pkg.devDependencies,
764
- ...pkg.peerDependencies
765
- }).flatMap((name) => [name, `${name}/*`]);
766
- } catch {
767
- return [];
768
- }
769
- }
770
- function collectNodePaths(agentDir) {
771
- const nodePathsSet = /* @__PURE__ */ new Set();
772
- for (const startDir of [agentDir, getSDKDir()]) {
773
- let dir = startDir;
774
- while (dir !== path.dirname(dir)) {
775
- const nm = path.join(dir, "node_modules");
776
- if (fs.existsSync(nm)) nodePathsSet.add(nm);
777
- dir = path.dirname(dir);
778
- }
779
- }
780
- return nodePathsSet;
781
- }
782
- function getSDKDir() {
783
- try {
784
- return path.dirname(new URL(import.meta.url).pathname);
785
- } catch {
786
- return __dirname;
787
- }
788
- }
789
-
790
- //#endregion
791
- export { BUILTIN_TOOL_NAMES, build, createTools };