@flue/sdk 0.2.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.
@@ -1,6 +1,8 @@
1
- import { S as SessionStore, y as SessionData } from "./types-C0nqbu6Z.mjs";
1
+ import { S as SessionData, T as SessionStore } from "./types-T8pE1xIS.mjs";
2
+ import "./mcp-BVF-sOBZ.mjs";
2
3
  import { FlueContextConfig, FlueContextInternal, createFlueContext } from "./client.mjs";
3
- import { bashToSessionEnv } from "./sandbox.mjs";
4
+ import { bashFactoryToSessionEnv } from "./sandbox.mjs";
5
+ import { getModel } from "@mariozechner/pi-ai";
4
6
  import "valibot";
5
7
 
6
8
  //#region src/session.d.ts
@@ -12,4 +14,16 @@ declare class InMemorySessionStore implements SessionStore {
12
14
  delete(id: string): Promise<void>;
13
15
  }
14
16
  //#endregion
15
- export { type FlueContextConfig, type FlueContextInternal, InMemorySessionStore, bashToSessionEnv, createFlueContext };
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 };
package/dist/internal.mjs CHANGED
@@ -1,6 +1,39 @@
1
- import "./agent-BYG0nVbQ.mjs";
2
- import { t as InMemorySessionStore } from "./session-CiAMTsLZ.mjs";
3
- import { bashToSessionEnv } from "./sandbox.mjs";
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";
4
5
  import { createFlueContext } from "./client.mjs";
6
+ import { getModel } from "@mariozechner/pi-ai";
5
7
 
6
- export { InMemorySessionStore, bashToSessionEnv, createFlueContext };
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 };
@@ -1,5 +1,5 @@
1
- import { s as Command } from "../types-C0nqbu6Z.mjs";
2
- import { t as CommandExecutor } from "../command-helpers-C8SHLdaA.mjs";
1
+ import { l as Command } from "../types-T8pE1xIS.mjs";
2
+ import { t as CommandExecutor } from "../command-helpers-DdAfbnom.mjs";
3
3
  import { execFile } from "node:child_process";
4
4
 
5
5
  //#region src/node/define-command.d.ts
@@ -1,4 +1,4 @@
1
- import { t as normalizeExecutor } from "../command-helpers-CxRhK1my.mjs";
1
+ import { t as normalizeExecutor } from "../command-helpers-hTZKWK13.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
 
@@ -1,7 +1,8 @@
1
- import { b as SessionEnv, c as CommandDef, r as BashLike, u as FileStat, v as SandboxFactory, w as ShellResult } from "./types-C0nqbu6Z.mjs";
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
2
 
3
3
  //#region src/sandbox.d.ts
4
- declare function bashToSessionEnv(bash: BashLike): SessionEnv;
4
+ declare function createCwdSessionEnv(parentEnv: SessionEnv, cwd: string): SessionEnv;
5
+ declare function bashFactoryToSessionEnv(factory: BashFactory): Promise<SessionEnv>;
5
6
  /** Interface that remote sandbox providers must implement. */
6
7
  interface SandboxApi {
7
8
  readFile(path: string): Promise<string>;
@@ -25,4 +26,4 @@ interface SandboxApi {
25
26
  /** Wrap a SandboxApi into SessionEnv. No just-bash, no intermediate filesystem layer. */
26
27
  declare function createSandboxSessionEnv(api: SandboxApi, cwd: string, cleanup?: () => Promise<void>): SessionEnv;
27
28
  //#endregion
28
- export { type CommandDef, type FileStat, SandboxApi, type SandboxFactory, type SessionEnv, bashToSessionEnv, createSandboxSessionEnv };
29
+ export { type CommandDef, type FileStat, SandboxApi, type SandboxFactory, type SessionEnv, bashFactoryToSessionEnv, createCwdSessionEnv, createSandboxSessionEnv };
package/dist/sandbox.mjs CHANGED
@@ -1,35 +1,56 @@
1
- import "./agent-BYG0nVbQ.mjs";
2
- import { r as normalizePath } from "./session-CiAMTsLZ.mjs";
1
+ import "./agent-BB4lwAd5.mjs";
2
+ import { i as normalizePath, o as createScopedEnv } from "./session-DukL3zwF.mjs";
3
3
 
4
4
  //#region src/sandbox.ts
5
- function bashToSessionEnv(bash) {
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) {
6
48
  const fs = bash.fs;
7
49
  const cwd = bash.getCwd();
8
50
  const resolve = (p) => p.startsWith("/") ? p : fs.resolvePath(cwd, p);
9
- let commandSupport;
10
- if (typeof bash.registerCommand === "function") {
11
- const registerCommand = bash.registerCommand.bind(bash);
12
- commandSupport = {
13
- register(cmd) {
14
- registerCommand({
15
- name: cmd.name,
16
- execute: cmd.execute
17
- });
18
- },
19
- unregister(name) {
20
- registerCommand({
21
- name,
22
- execute: async () => ({
23
- stdout: "",
24
- stderr: name + ": command not available (not registered for this call)",
25
- exitCode: 127
26
- })
27
- });
28
- }
29
- };
30
- }
31
51
  return {
32
52
  exec: (cmd, opts) => bash.exec(cmd, opts),
53
+ scope: (options) => createScope(options?.commands ?? []),
33
54
  readFile: (p) => fs.readFile(resolve(p)),
34
55
  readFileBuffer: (p) => fs.readFileBuffer(resolve(p)),
35
56
  writeFile: async (p, content) => {
@@ -47,10 +68,20 @@ function bashToSessionEnv(bash) {
47
68
  rm: (p, o) => fs.rm(resolve(p), o),
48
69
  cwd,
49
70
  resolvePath: resolve,
50
- commandSupport,
51
71
  cleanup: async () => {}
52
72
  };
53
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
+ }
54
85
  /** Wrap a SandboxApi into SessionEnv. No just-bash, no intermediate filesystem layer. */
55
86
  function createSandboxSessionEnv(api, cwd, cleanup) {
56
87
  const resolvePath = (p) => {
@@ -91,7 +122,6 @@ function createSandboxSessionEnv(api, cwd, cleanup) {
91
122
  },
92
123
  cwd,
93
124
  resolvePath,
94
- commandSupport: void 0,
95
125
  async cleanup() {
96
126
  if (cleanup) await cleanup();
97
127
  }
@@ -99,4 +129,4 @@ function createSandboxSessionEnv(api, cwd, cleanup) {
99
129
  }
100
130
 
101
131
  //#endregion
102
- export { bashToSessionEnv, createSandboxSessionEnv };
132
+ export { bashFactoryToSessionEnv, createCwdSessionEnv, createSandboxSessionEnv };