@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.
- package/README.md +24 -9
- package/dist/agent-BB4lwAd5.mjs +453 -0
- package/dist/client.d.mts +26 -0
- package/dist/client.mjs +78 -0
- package/dist/cloudflare/index.d.mts +35 -0
- package/dist/cloudflare/index.mjs +241 -0
- package/dist/command-helpers-DdAfbnom.d.mts +21 -0
- package/dist/command-helpers-hTZKWK13.mjs +37 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.mjs +1568 -0
- package/dist/internal.d.mts +29 -0
- package/dist/internal.mjs +39 -0
- package/dist/mcp-BVF-sOBZ.d.mts +22 -0
- package/dist/mcp-DOgMtp8y.mjs +285 -0
- package/dist/node/index.d.mts +14 -0
- package/dist/node/index.mjs +75 -0
- package/dist/sandbox.d.mts +29 -0
- package/dist/sandbox.mjs +132 -0
- package/dist/session-DukL3zwF.mjs +1303 -0
- package/dist/types-T8pE1xIS.d.mts +461 -0
- package/package.json +11 -3
|
@@ -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 };
|
package/dist/sandbox.mjs
ADDED
|
@@ -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 };
|