@flue/sdk 0.3.0 → 0.3.1

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.
@@ -0,0 +1,29 @@
1
+ import { S as SessionData, T as SessionStore } from "./types-T8pE1xIS.mjs";
2
+ import "./mcp-BVF-sOBZ.mjs";
3
+ import { FlueContextConfig, FlueContextInternal, createFlueContext } from "./client.mjs";
4
+ import { bashFactoryToSessionEnv } from "./sandbox.mjs";
5
+ import { getModel } from "@mariozechner/pi-ai";
6
+ import "valibot";
7
+
8
+ //#region src/session.d.ts
9
+ /** In-memory session store. Sessions persist for the lifetime of the process. */
10
+ declare class InMemorySessionStore implements SessionStore {
11
+ private store;
12
+ save(id: string, data: SessionData): Promise<void>;
13
+ load(id: string): Promise<SessionData | null>;
14
+ delete(id: string): Promise<void>;
15
+ }
16
+ //#endregion
17
+ //#region src/internal.d.ts
18
+ /**
19
+ * Resolve a `provider/model-id` string into a pi-ai `Model` object.
20
+ * Lives here (rather than in the generated entry point) so that user
21
+ * projects don't have to declare `@mariozechner/pi-ai` as a direct
22
+ * dependency — wrangler's bundler resolves bare specifiers from the entry
23
+ * file's location, which on pnpm-isolated installs doesn't see Flue's
24
+ * transitive deps. Centralizing the resolver here keeps `_entry.ts`
25
+ * dependency-free apart from `@flue/sdk/*`.
26
+ */
27
+ declare function resolveModel(modelString: string): ReturnType<typeof getModel>;
28
+ //#endregion
29
+ export { type FlueContextConfig, type FlueContextInternal, InMemorySessionStore, bashFactoryToSessionEnv, createFlueContext, resolveModel };
@@ -0,0 +1,39 @@
1
+ import "./agent-BB4lwAd5.mjs";
2
+ import { t as InMemorySessionStore } from "./session-DukL3zwF.mjs";
3
+ import { bashFactoryToSessionEnv } from "./sandbox.mjs";
4
+ import "./mcp-DOgMtp8y.mjs";
5
+ import { createFlueContext } from "./client.mjs";
6
+ import { getModel } from "@mariozechner/pi-ai";
7
+
8
+ //#region src/internal.ts
9
+ /**
10
+ * Internal runtime helpers consumed by the generated server entry point.
11
+ *
12
+ * This subpath is NOT part of the public API. It exists solely so the build
13
+ * plugins (Node, Cloudflare) can emit stable bare-specifier imports that
14
+ * resolve through normal package-exports resolution at both build time and
15
+ * runtime, for both workspace-linked and published-npm installs.
16
+ *
17
+ * User agent code should never import from here.
18
+ */
19
+ /**
20
+ * Resolve a `provider/model-id` string into a pi-ai `Model` object.
21
+ * Lives here (rather than in the generated entry point) so that user
22
+ * projects don't have to declare `@mariozechner/pi-ai` as a direct
23
+ * dependency — wrangler's bundler resolves bare specifiers from the entry
24
+ * file's location, which on pnpm-isolated installs doesn't see Flue's
25
+ * transitive deps. Centralizing the resolver here keeps `_entry.ts`
26
+ * dependency-free apart from `@flue/sdk/*`.
27
+ */
28
+ function resolveModel(modelString) {
29
+ const slash = modelString.indexOf("/");
30
+ if (slash === -1) throw new Error(`[flue] Invalid model "${modelString}". Use the "provider/model-id" format (e.g. "anthropic/claude-haiku-4-5").`);
31
+ const provider = modelString.slice(0, slash);
32
+ const modelId = modelString.slice(slash + 1);
33
+ const resolved = getModel(provider, modelId);
34
+ if (!resolved) throw new Error(`[flue] Unknown model "${modelString}". Provider "${provider}" / model id "${modelId}" is not registered with @mariozechner/pi-ai.`);
35
+ return resolved;
36
+ }
37
+
38
+ //#endregion
39
+ export { InMemorySessionStore, bashFactoryToSessionEnv, createFlueContext, resolveModel };
@@ -0,0 +1,22 @@
1
+ import { j as ToolDef } from "./types-T8pE1xIS.mjs";
2
+
3
+ //#region src/mcp.d.ts
4
+ type McpTransport = 'streamable-http' | 'sse';
5
+ interface McpServerOptions {
6
+ url: string | URL;
7
+ /** Defaults to modern streamable HTTP. Use 'sse' for legacy MCP servers. */
8
+ transport?: McpTransport;
9
+ headers?: HeadersInit;
10
+ requestInit?: RequestInit;
11
+ fetch?: typeof fetch;
12
+ clientName?: string;
13
+ clientVersion?: string;
14
+ }
15
+ interface McpServerConnection {
16
+ name: string;
17
+ tools: ToolDef[];
18
+ close(): Promise<void>;
19
+ }
20
+ declare function connectMcpServer(name: string, options: McpServerOptions): Promise<McpServerConnection>;
21
+ //#endregion
22
+ export { connectMcpServer as i, McpServerOptions as n, McpTransport as r, McpServerConnection as t };
@@ -0,0 +1,285 @@
1
+ import { r as discoverSessionContext } from "./agent-BB4lwAd5.mjs";
2
+ import { a as assertRoleExists, n as Session, o as createScopedEnv, r as deleteSessionTree, s as mergeCommands } from "./session-DukL3zwF.mjs";
3
+ import { createCwdSessionEnv } from "./sandbox.mjs";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+
7
+ //#region src/agent-client.ts
8
+ const DEFAULT_SESSION_ID = "default";
9
+ var AgentClient = class {
10
+ sessions = {
11
+ get: (id, options) => this.openSession(id, "get", options),
12
+ create: (id, options) => this.openSession(id, "create", options),
13
+ delete: (id) => this.deleteSession(id)
14
+ };
15
+ openSessions = /* @__PURE__ */ new Map();
16
+ destroyed = false;
17
+ constructor(id, config, env, store, eventCallback, agentCommands = [], agentTools = []) {
18
+ this.id = id;
19
+ this.config = config;
20
+ this.env = env;
21
+ this.store = store;
22
+ this.eventCallback = eventCallback;
23
+ this.agentCommands = agentCommands;
24
+ this.agentTools = agentTools;
25
+ }
26
+ async session(id, options) {
27
+ return this.openSession(id, "get-or-create", options);
28
+ }
29
+ async shell(command, options) {
30
+ this.assertActive();
31
+ const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
32
+ const result = await (await createScopedEnv(this.env, effectiveCommands)).exec(command, {
33
+ env: options?.env,
34
+ cwd: options?.cwd
35
+ });
36
+ return {
37
+ stdout: result.stdout,
38
+ stderr: result.stderr,
39
+ exitCode: result.exitCode
40
+ };
41
+ }
42
+ async destroy() {
43
+ if (this.destroyed) return;
44
+ this.destroyed = true;
45
+ for (const session of Array.from(this.openSessions.values())) session.close();
46
+ this.openSessions.clear();
47
+ await this.env.cleanup();
48
+ }
49
+ async openSession(id, mode, options) {
50
+ this.assertActive();
51
+ assertRoleExists(this.config.roles, options?.role);
52
+ const sessionId = normalizeSessionId(id);
53
+ const open = this.openSessions.get(sessionId);
54
+ if (open) {
55
+ if (mode === "create") throw new Error(`[flue] Session "${sessionId}" already exists for agent "${this.id}".`);
56
+ if (options?.role !== void 0 && options.role !== open.role) throw new Error(`[flue] Session "${sessionId}" is already open with role ${JSON.stringify(open.role ?? null)}; cannot reopen with role ${JSON.stringify(options.role)}.`);
57
+ return open;
58
+ }
59
+ const storageKey = createSessionStorageKey(this.id, sessionId);
60
+ const existingData = await this.store.load(storageKey);
61
+ if (mode === "get" && !existingData) throw new Error(`[flue] Session "${sessionId}" does not exist for agent "${this.id}".`);
62
+ if (mode === "create" && existingData) throw new Error(`[flue] Session "${sessionId}" already exists for agent "${this.id}".`);
63
+ let data = existingData;
64
+ if (!data && mode !== "get") {
65
+ data = createEmptySessionData();
66
+ await this.store.save(storageKey, data);
67
+ }
68
+ const session = new Session({
69
+ id: sessionId,
70
+ storageKey,
71
+ config: this.config,
72
+ env: this.env,
73
+ store: this.store,
74
+ existingData: data,
75
+ onAgentEvent: this.eventCallback,
76
+ agentCommands: this.agentCommands,
77
+ agentTools: this.agentTools,
78
+ sessionRole: options?.role,
79
+ taskDepth: 0,
80
+ createTaskSession: (taskOptions) => this.createTaskSession(taskOptions),
81
+ onDelete: () => this.openSessions.delete(sessionId)
82
+ });
83
+ this.openSessions.set(sessionId, session);
84
+ return session;
85
+ }
86
+ async deleteSession(id) {
87
+ this.assertActive();
88
+ const sessionId = normalizeSessionId(id);
89
+ const open = this.openSessions.get(sessionId);
90
+ if (open) {
91
+ await open.delete();
92
+ return;
93
+ }
94
+ await deleteSessionTree(this.store, createSessionStorageKey(this.id, sessionId));
95
+ }
96
+ async createTaskSession(options) {
97
+ this.assertActive();
98
+ assertRoleExists(this.config.roles, options.role);
99
+ const sessionId = `task:${options.parentSessionId}:${options.taskId}`;
100
+ const taskEnv = options.cwd ? createCwdSessionEnv(options.parentEnv, options.parentEnv.resolvePath(options.cwd)) : options.parentEnv;
101
+ const localContext = await discoverSessionContext(taskEnv);
102
+ const taskConfig = {
103
+ ...this.config,
104
+ systemPrompt: localContext.systemPrompt,
105
+ skills: localContext.skills
106
+ };
107
+ const storageKey = createSessionStorageKey(this.id, sessionId);
108
+ const data = createEmptySessionData();
109
+ data.metadata = {
110
+ parentSessionId: options.parentSessionId,
111
+ taskId: options.taskId,
112
+ cwd: taskEnv.cwd,
113
+ role: options.role,
114
+ depth: options.depth
115
+ };
116
+ await this.store.save(storageKey, data);
117
+ const eventCallback = this.eventCallback ? (event) => {
118
+ this.eventCallback?.({
119
+ ...event,
120
+ parentSessionId: event.parentSessionId ?? options.parentSessionId,
121
+ taskId: event.taskId ?? options.taskId
122
+ });
123
+ } : void 0;
124
+ return new Session({
125
+ id: sessionId,
126
+ storageKey,
127
+ config: taskConfig,
128
+ env: taskEnv,
129
+ store: this.store,
130
+ existingData: data,
131
+ onAgentEvent: eventCallback,
132
+ agentCommands: options.commands,
133
+ agentTools: this.agentTools,
134
+ sessionRole: options.role,
135
+ taskDepth: options.depth,
136
+ createTaskSession: (childOptions) => this.createTaskSession(childOptions)
137
+ });
138
+ }
139
+ assertActive() {
140
+ if (this.destroyed) throw new Error(`[flue] Agent "${this.id}" has been destroyed.`);
141
+ }
142
+ };
143
+ function normalizeSessionId(id) {
144
+ return id ?? DEFAULT_SESSION_ID;
145
+ }
146
+ function createSessionStorageKey(agentId, sessionId) {
147
+ return `agent-session:${JSON.stringify([agentId, sessionId])}`;
148
+ }
149
+ function createEmptySessionData() {
150
+ const now = (/* @__PURE__ */ new Date()).toISOString();
151
+ return {
152
+ version: 2,
153
+ entries: [],
154
+ leafId: null,
155
+ metadata: {},
156
+ createdAt: now,
157
+ updatedAt: now
158
+ };
159
+ }
160
+
161
+ //#endregion
162
+ //#region src/mcp.ts
163
+ async function connectMcpServer(name, options) {
164
+ const url = options.url instanceof URL ? options.url : new URL(options.url);
165
+ const requestInit = mergeRequestInit(options.requestInit, options.headers);
166
+ const transport = await createTransport(url, options.transport ?? "streamable-http", requestInit, options.fetch);
167
+ const client = new Client({
168
+ name: options.clientName ?? "flue",
169
+ version: options.clientVersion ?? "0.0.0"
170
+ });
171
+ try {
172
+ await client.connect(transport);
173
+ const { tools } = await client.listTools();
174
+ return {
175
+ name,
176
+ tools: createMcpTools(name, client, tools),
177
+ close: () => client.close()
178
+ };
179
+ } catch (error) {
180
+ await client.close().catch(() => void 0);
181
+ throw error;
182
+ }
183
+ }
184
+ async function createTransport(url, transport, requestInit, fetchImpl) {
185
+ if (transport === "sse") {
186
+ const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
187
+ return new SSEClientTransport(url, {
188
+ requestInit,
189
+ fetch: fetchImpl
190
+ });
191
+ }
192
+ return new StreamableHTTPClientTransport(url, {
193
+ requestInit,
194
+ fetch: fetchImpl
195
+ });
196
+ }
197
+ function createMcpTools(serverName, client, tools) {
198
+ const names = /* @__PURE__ */ new Set();
199
+ return tools.map((tool) => {
200
+ const toolName = createToolName(serverName, tool.name);
201
+ if (names.has(toolName)) throw new Error(`[flue] MCP tools from server "${serverName}" produced duplicate tool name "${toolName}".`);
202
+ names.add(toolName);
203
+ return {
204
+ name: toolName,
205
+ description: createToolDescription(serverName, tool),
206
+ parameters: normalizeInputSchema(tool.inputSchema),
207
+ async execute(args, signal) {
208
+ if (signal?.aborted) throw new Error("Operation aborted");
209
+ const result = await client.callTool({
210
+ name: tool.name,
211
+ arguments: args
212
+ }, void 0, { signal });
213
+ const text = formatMcpResult(result);
214
+ if (result.isError) throw new Error(text);
215
+ return text;
216
+ }
217
+ };
218
+ });
219
+ }
220
+ function mergeRequestInit(requestInit, headers) {
221
+ if (!headers) return requestInit ?? {};
222
+ const mergedHeaders = new Headers(requestInit?.headers);
223
+ for (const [key, value] of new Headers(headers)) mergedHeaders.set(key, value);
224
+ return {
225
+ ...requestInit,
226
+ headers: mergedHeaders
227
+ };
228
+ }
229
+ function createToolName(serverName, toolName) {
230
+ return `mcp__${sanitizeToolNamePart(serverName)}__${sanitizeToolNamePart(toolName)}`;
231
+ }
232
+ function sanitizeToolNamePart(value) {
233
+ return value.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || "unnamed";
234
+ }
235
+ function createToolDescription(serverName, tool) {
236
+ const originalName = tool.name;
237
+ const title = tool.title ?? tool.annotations?.title;
238
+ const parts = [`MCP tool "${originalName}" from server "${serverName}".`];
239
+ if (title && title !== originalName) parts.push(`Title: ${title}.`);
240
+ if (tool.description) parts.push(tool.description);
241
+ return parts.join(" ");
242
+ }
243
+ function normalizeInputSchema(schema) {
244
+ return {
245
+ ...schema,
246
+ type: schema.type ?? "object",
247
+ properties: schema.properties ?? {},
248
+ required: schema.required
249
+ };
250
+ }
251
+ function formatMcpResult(result) {
252
+ const parts = [];
253
+ if (result.structuredContent !== void 0) parts.push(`Structured content:\n${JSON.stringify(result.structuredContent, null, 2)}`);
254
+ for (const item of result.content ?? []) {
255
+ if (item.type === "text") {
256
+ parts.push(item.text);
257
+ continue;
258
+ }
259
+ if (item.type === "image") {
260
+ parts.push(`[Image: ${item.mimeType}, ${item.data.length} base64 chars]`);
261
+ continue;
262
+ }
263
+ if (item.type === "audio") {
264
+ parts.push(`[Audio: ${item.mimeType}, ${item.data.length} base64 chars]`);
265
+ continue;
266
+ }
267
+ if (item.type === "resource") {
268
+ const resource = item.resource;
269
+ if ("text" in resource) parts.push(`[Resource: ${resource.uri}]\n${resource.text}`);
270
+ else parts.push(`[Resource: ${resource.uri}, ${resource.blob.length} base64 chars]`);
271
+ continue;
272
+ }
273
+ if (item.type === "resource_link") {
274
+ const description = item.description ? ` - ${item.description}` : "";
275
+ parts.push(`[Resource link: ${item.name} (${item.uri})${description}]`);
276
+ continue;
277
+ }
278
+ parts.push(JSON.stringify(item));
279
+ }
280
+ if (parts.length === 0 && "toolResult" in result) parts.push(JSON.stringify(result.toolResult, null, 2));
281
+ return parts.filter(Boolean).join("\n\n") || "(MCP tool returned no content)";
282
+ }
283
+
284
+ //#endregion
285
+ export { AgentClient as n, connectMcpServer as t };
@@ -0,0 +1,14 @@
1
+ import { l as Command } from "../types-T8pE1xIS.mjs";
2
+ import { t as CommandExecutor } from "../command-helpers-DdAfbnom.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-hTZKWK13.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 };
@@ -0,0 +1,29 @@
1
+ import { C as SessionEnv, D as ShellResult, d as FileStat, i as BashFactory, u as CommandDef, x as SandboxFactory } from "./types-T8pE1xIS.mjs";
2
+
3
+ //#region src/sandbox.d.ts
4
+ declare function createCwdSessionEnv(parentEnv: SessionEnv, cwd: string): SessionEnv;
5
+ declare function bashFactoryToSessionEnv(factory: BashFactory): Promise<SessionEnv>;
6
+ /** Interface that remote sandbox providers must implement. */
7
+ interface SandboxApi {
8
+ readFile(path: string): Promise<string>;
9
+ readFileBuffer(path: string): Promise<Uint8Array>;
10
+ writeFile(path: string, content: string | Uint8Array): Promise<void>;
11
+ stat(path: string): Promise<FileStat>;
12
+ readdir(path: string): Promise<string[]>;
13
+ exists(path: string): Promise<boolean>;
14
+ mkdir(path: string, options?: {
15
+ recursive?: boolean;
16
+ }): Promise<void>;
17
+ rm(path: string, options?: {
18
+ recursive?: boolean;
19
+ force?: boolean;
20
+ }): Promise<void>;
21
+ exec(command: string, options?: {
22
+ cwd?: string;
23
+ env?: Record<string, string>;
24
+ }): Promise<ShellResult>;
25
+ }
26
+ /** Wrap a SandboxApi into SessionEnv. No just-bash, no intermediate filesystem layer. */
27
+ declare function createSandboxSessionEnv(api: SandboxApi, cwd: string, cleanup?: () => Promise<void>): SessionEnv;
28
+ //#endregion
29
+ export { type CommandDef, type FileStat, SandboxApi, type SandboxFactory, type SessionEnv, bashFactoryToSessionEnv, createCwdSessionEnv, createSandboxSessionEnv };
@@ -0,0 +1,132 @@
1
+ import "./agent-BB4lwAd5.mjs";
2
+ import { i as normalizePath, o as createScopedEnv } from "./session-DukL3zwF.mjs";
3
+
4
+ //#region src/sandbox.ts
5
+ function createCwdSessionEnv(parentEnv, cwd) {
6
+ const scopedCwd = normalizePath(cwd);
7
+ const resolvePath = (p) => {
8
+ if (p.startsWith("/")) return normalizePath(p);
9
+ if (scopedCwd === "/") return normalizePath("/" + p);
10
+ return normalizePath(scopedCwd + "/" + p);
11
+ };
12
+ return {
13
+ exec: (cmd, opts) => parentEnv.exec(cmd, {
14
+ cwd: opts?.cwd ?? scopedCwd,
15
+ env: opts?.env
16
+ }),
17
+ scope: async (options) => createCwdSessionEnv(await createScopedEnv(parentEnv, options?.commands ?? []), scopedCwd),
18
+ readFile: (p) => parentEnv.readFile(resolvePath(p)),
19
+ readFileBuffer: (p) => parentEnv.readFileBuffer(resolvePath(p)),
20
+ writeFile: (p, c) => parentEnv.writeFile(resolvePath(p), c),
21
+ stat: (p) => parentEnv.stat(resolvePath(p)),
22
+ readdir: (p) => parentEnv.readdir(resolvePath(p)),
23
+ exists: (p) => parentEnv.exists(resolvePath(p)),
24
+ mkdir: (p, o) => parentEnv.mkdir(resolvePath(p), o),
25
+ rm: (p, o) => parentEnv.rm(resolvePath(p), o),
26
+ cwd: scopedCwd,
27
+ resolvePath,
28
+ cleanup: () => parentEnv.cleanup()
29
+ };
30
+ }
31
+ async function bashFactoryToSessionEnv(factory) {
32
+ const seen = /* @__PURE__ */ new WeakSet();
33
+ async function createBash() {
34
+ const bash = await factory();
35
+ assertBashLike(bash);
36
+ if (seen.has(bash)) throw new Error("[flue] BashFactory must return a fresh Bash-like instance for each operation. Share the filesystem object in the factory closure to persist files across calls.");
37
+ seen.add(bash);
38
+ return bash;
39
+ }
40
+ async function createBashScopedEnv(commands) {
41
+ const scoped = await createBash();
42
+ registerCommands(scoped, commands);
43
+ return createBashSessionEnv(scoped, createBashScopedEnv);
44
+ }
45
+ return createBashSessionEnv(await createBash(), createBashScopedEnv);
46
+ }
47
+ function createBashSessionEnv(bash, createScope) {
48
+ const fs = bash.fs;
49
+ const cwd = bash.getCwd();
50
+ const resolve = (p) => p.startsWith("/") ? p : fs.resolvePath(cwd, p);
51
+ return {
52
+ exec: (cmd, opts) => bash.exec(cmd, opts),
53
+ scope: (options) => createScope(options?.commands ?? []),
54
+ readFile: (p) => fs.readFile(resolve(p)),
55
+ readFileBuffer: (p) => fs.readFileBuffer(resolve(p)),
56
+ writeFile: async (p, content) => {
57
+ const resolved = resolve(p);
58
+ const dir = resolved.replace(/\/[^/]*$/, "");
59
+ if (dir && dir !== resolved) try {
60
+ await fs.mkdir(dir, { recursive: true });
61
+ } catch {}
62
+ await fs.writeFile(resolved, content);
63
+ },
64
+ stat: (p) => fs.stat(resolve(p)),
65
+ readdir: (p) => fs.readdir(resolve(p)),
66
+ exists: (p) => fs.exists(resolve(p)),
67
+ mkdir: (p, o) => fs.mkdir(resolve(p), o),
68
+ rm: (p, o) => fs.rm(resolve(p), o),
69
+ cwd,
70
+ resolvePath: resolve,
71
+ cleanup: async () => {}
72
+ };
73
+ }
74
+ function registerCommands(bash, commands) {
75
+ if (commands.length === 0) return;
76
+ if (typeof bash.registerCommand !== "function") throw new Error("[flue] Cannot use commands: this Bash-like sandbox does not support command registration.");
77
+ for (const cmd of commands) bash.registerCommand({
78
+ name: cmd.name,
79
+ execute: cmd.execute
80
+ });
81
+ }
82
+ function assertBashLike(value) {
83
+ if (typeof value !== "object" || value === null || !("exec" in value) || !("getCwd" in value) || !("fs" in value) || typeof value.exec !== "function" || typeof value.getCwd !== "function" || typeof value.fs !== "object") throw new Error("[flue] BashFactory must return a Bash-like object.");
84
+ }
85
+ /** Wrap a SandboxApi into SessionEnv. No just-bash, no intermediate filesystem layer. */
86
+ function createSandboxSessionEnv(api, cwd, cleanup) {
87
+ const resolvePath = (p) => {
88
+ if (p.startsWith("/")) return normalizePath(p);
89
+ if (cwd === "/") return normalizePath("/" + p);
90
+ return normalizePath(cwd + "/" + p);
91
+ };
92
+ return {
93
+ async exec(command, options) {
94
+ return api.exec(command, {
95
+ cwd: options?.cwd ?? cwd,
96
+ env: options?.env
97
+ });
98
+ },
99
+ async readFile(path) {
100
+ return api.readFile(resolvePath(path));
101
+ },
102
+ async readFileBuffer(path) {
103
+ return api.readFileBuffer(resolvePath(path));
104
+ },
105
+ async writeFile(path, content) {
106
+ return api.writeFile(resolvePath(path), content);
107
+ },
108
+ async stat(path) {
109
+ return api.stat(resolvePath(path));
110
+ },
111
+ async readdir(path) {
112
+ return api.readdir(resolvePath(path));
113
+ },
114
+ async exists(path) {
115
+ return api.exists(resolvePath(path));
116
+ },
117
+ async mkdir(path, options) {
118
+ return api.mkdir(resolvePath(path), options);
119
+ },
120
+ async rm(path, options) {
121
+ return api.rm(resolvePath(path), options);
122
+ },
123
+ cwd,
124
+ resolvePath,
125
+ async cleanup() {
126
+ if (cleanup) await cleanup();
127
+ }
128
+ };
129
+ }
130
+
131
+ //#endregion
132
+ export { bashFactoryToSessionEnv, createCwdSessionEnv, createSandboxSessionEnv };