libfx 0.0.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/fx-term.wasm ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
package/node.js ADDED
@@ -0,0 +1,292 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, resolve } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import {
7
+ createFxAgent as createWasmAgent,
8
+ createFxTerminal as createWasmTerminal,
9
+ encodeXtermKeyEvent,
10
+ fxSdkApiVersion,
11
+ supportsJspi,
12
+ xtermAdapter,
13
+ } from "./fx-sdk.js";
14
+
15
+ export { encodeXtermKeyEvent, fxSdkApiVersion, supportsJspi, xtermAdapter };
16
+ export const libfxApiVersion = 2;
17
+
18
+ const fetchOperationStale = 0;
19
+ const fetchOperationApplied = 1;
20
+ const fetchOperationBackpressure = 2;
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const defaultCoreWasm = new URL("./fx-core.wasm", import.meta.url);
24
+ const defaultTermWasm = new URL("./fx-term.wasm", import.meta.url);
25
+ const defaultNativeCandidates = [
26
+ "./libfx.node",
27
+ `./libfx.${process.platform}-${process.arch}.node`,
28
+ ];
29
+ let nativeBackendPromise;
30
+
31
+ function jspiFallbackError(surface, nativeError) {
32
+ const nativeDetail = nativeError ? ` Native loading failed: ${nativeError.message}.` : " No compatible native addon was found.";
33
+ const error = new Error(
34
+ `libfx could not start the ${surface} backend.${nativeDetail} ` +
35
+ "The WebAssembly fallback requires JavaScript Promise Integration (JSPI). " +
36
+ "Run Node with --experimental-wasm-jspi or install a libfx package containing a compatible native addon.",
37
+ );
38
+ error.code = "LIBFX_JSPI_REQUIRED";
39
+ error.cause = nativeError;
40
+ return error;
41
+ }
42
+
43
+ async function loadNativeCandidate(candidate) {
44
+ if (candidate == null) return null;
45
+ if (candidate instanceof URL) {
46
+ if (candidate.protocol === "file:" && candidate.pathname.endsWith(".node")) {
47
+ return require(fileURLToPath(candidate));
48
+ }
49
+ const imported = await import(candidate.href);
50
+ return imported.default ?? imported;
51
+ }
52
+ if (typeof candidate === "object") return candidate.default ?? candidate;
53
+ if (typeof candidate !== "string") {
54
+ throw new TypeError("nativeAddon must be a module, path, URL, false, or undefined");
55
+ }
56
+ if (candidate.endsWith(".node")) return require(isAbsolute(candidate) ? candidate : resolve(candidate));
57
+ const imported = await import(candidate.startsWith("file:") ? candidate : pathToFileURL(candidate).href);
58
+ return imported.default ?? imported;
59
+ }
60
+
61
+ function validateNativeBackend(backend) {
62
+ if (!backend) return null;
63
+ const hasLowLevelCore = typeof backend.createCore === "function";
64
+ if ((hasLowLevelCore && backend.libfxApiVersion !== libfxApiVersion) ||
65
+ (!hasLowLevelCore && backend.libfxApiVersion !== undefined && backend.libfxApiVersion !== libfxApiVersion)) {
66
+ const actualVersion = backend.libfxApiVersion ?? "missing";
67
+ throw new Error(`native addon API version ${actualVersion} is incompatible with libfx API version ${libfxApiVersion}`);
68
+ }
69
+ if (typeof backend.createFxAgent !== "function" && typeof backend.createCore !== "function" &&
70
+ typeof backend.createFxTerminal !== "function") {
71
+ throw new Error("native addon must export createFxAgent(), createCore(), or createFxTerminal()");
72
+ }
73
+ return backend;
74
+ }
75
+
76
+ async function discoverNativeBackend() {
77
+ for (const relativePath of defaultNativeCandidates) {
78
+ const url = new URL(relativePath, import.meta.url);
79
+ try {
80
+ await access(fileURLToPath(url));
81
+ } catch (error) {
82
+ if (error?.code === "ENOENT") continue;
83
+ return { backend: null, error };
84
+ }
85
+ try {
86
+ return { backend: validateNativeBackend(await loadNativeCandidate(url)), error: null };
87
+ } catch (error) {
88
+ return { backend: null, error };
89
+ }
90
+ }
91
+ return { backend: null, error: null };
92
+ }
93
+
94
+ async function resolveNativeBackend(nativeAddon) {
95
+ if (nativeAddon === false) return { backend: null, error: null };
96
+ if (nativeAddon !== undefined) {
97
+ try {
98
+ return { backend: validateNativeBackend(await loadNativeCandidate(nativeAddon)), error: null };
99
+ } catch (error) {
100
+ return { backend: null, error };
101
+ }
102
+ }
103
+ nativeBackendPromise ??= discoverNativeBackend();
104
+ return nativeBackendPromise;
105
+ }
106
+
107
+ async function wasmBytes(input) {
108
+ if (input instanceof URL && input.protocol === "file:") return readFile(input);
109
+ if (typeof input === "string" && !URL.canParse(input)) return readFile(input);
110
+ return input;
111
+ }
112
+
113
+ function validateGatewayChatUrl(value) {
114
+ if (value === undefined) return;
115
+ if (typeof value !== "string") throw new TypeError("FX_GATEWAY_CHAT_URL must be a string");
116
+ let url;
117
+ try { url = new URL(value); } catch { throw new TypeError("FX_GATEWAY_CHAT_URL must be a valid URL"); }
118
+ if (url.username || url.password || url.hash) {
119
+ throw new TypeError("FX_GATEWAY_CHAT_URL must not contain credentials or a fragment");
120
+ }
121
+ if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model") return;
122
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
123
+ if (url.protocol !== "http:" || !loopback || !url.port) {
124
+ throw new TypeError("FX_GATEWAY_CHAT_URL must use the canonical Gateway or explicit loopback HTTP");
125
+ }
126
+ }
127
+
128
+ function createNativeCoreRuntime(addon, options) {
129
+ const apiKey = options.env?.AI_GATEWAY_API_KEY;
130
+ const model = options.env?.FX_MODEL;
131
+ const gatewayChatUrl = options.env?.FX_GATEWAY_CHAT_URL;
132
+ validateGatewayChatUrl(gatewayChatUrl);
133
+ const core = addon.createCore({
134
+ apiKey,
135
+ home: options.home ?? homedir(),
136
+ workspaceRoot: options.workspaceRoot ?? process.cwd(),
137
+ ...(model === undefined ? {} : { model }),
138
+ ...(gatewayChatUrl === undefined ? {} : { gatewayChatUrl }),
139
+ });
140
+ let exitedResolve;
141
+ let lineHandler = null;
142
+ let lineBuffer = "";
143
+ let settled = false;
144
+ let fetchState = null;
145
+ const exited = new Promise((resolve) => { exitedResolve = resolve; });
146
+ const abortHostEffects = () => {
147
+ fetchState?.controller.abort();
148
+ try { addon.abortCoreFetch(core); } catch {}
149
+ };
150
+ const finish = (code) => {
151
+ if (settled) return;
152
+ settled = true;
153
+ clearInterval(timer);
154
+ abortHostEffects();
155
+ try { addon.destroyCore(core); } catch {}
156
+ exitedResolve(code);
157
+ };
158
+ const pumpFetch = async (request) => {
159
+ const controller = new AbortController();
160
+ const state = { handle: request.handle, controller };
161
+ fetchState = state;
162
+ try {
163
+ const response = await (options.fetch ?? globalThis.fetch)(request.url, {
164
+ method: request.method,
165
+ headers: new Headers(JSON.parse(request.headers).map(({ name, value }) => [name, value])),
166
+ body: request.body?.length ? Buffer.from(request.body, "base64") : undefined,
167
+ signal: controller.signal,
168
+ });
169
+ const started = addon.startCoreFetchResponse(core, state.handle, response.status);
170
+ if (started === fetchOperationStale) return;
171
+ if (started !== fetchOperationApplied) throw new Error(`invalid native fetch start result ${started}`);
172
+ if (response.body) {
173
+ for await (const chunk of response.body) {
174
+ const buffer = Buffer.from(chunk);
175
+ let offset = 0;
176
+ while (offset < buffer.length) {
177
+ const end = Math.min(offset + 64 * 1024, buffer.length);
178
+ const pushed = addon.pushCoreFetchResponse(core, state.handle, buffer.subarray(offset, end));
179
+ if (pushed === fetchOperationApplied) {
180
+ offset = end;
181
+ continue;
182
+ }
183
+ if (pushed === fetchOperationStale) return;
184
+ if (pushed !== fetchOperationBackpressure) throw new Error(`invalid native fetch push result ${pushed}`);
185
+ await new Promise((resolve) => setTimeout(resolve, 2));
186
+ }
187
+ }
188
+ }
189
+ const finished = addon.finishCoreFetch(core, state.handle);
190
+ if (finished !== fetchOperationApplied && finished !== fetchOperationStale) {
191
+ throw new Error(`invalid native fetch finish result ${finished}`);
192
+ }
193
+ } catch (error) {
194
+ if (error?.name !== "AbortError" || !controller.signal.aborted) {
195
+ try {
196
+ if (addon.coreFetchActive(core, state.handle)) addon.failCoreFetch(core, state.handle);
197
+ } catch {}
198
+ }
199
+ } finally {
200
+ if (fetchState === state) fetchState = null;
201
+ }
202
+ };
203
+ const timer = setInterval(() => {
204
+ try {
205
+ if (fetchState) {
206
+ if (!fetchState.controller.signal.aborted && !addon.coreFetchActive(core, fetchState.handle)) {
207
+ fetchState.controller.abort();
208
+ }
209
+ } else {
210
+ const fetchRequest = addon.takeCoreFetch(core);
211
+ if (fetchRequest) void pumpFetch(JSON.parse(fetchRequest.toString("utf8")));
212
+ }
213
+ const chunk = addon.drainCore(core);
214
+ if (chunk.length && lineHandler) {
215
+ lineBuffer += chunk.toString("utf8");
216
+ for (;;) {
217
+ const newline = lineBuffer.indexOf("\n");
218
+ if (newline < 0) break;
219
+ const line = lineBuffer.slice(0, newline);
220
+ lineBuffer = lineBuffer.slice(newline + 1);
221
+ if (line) lineHandler(JSON.parse(line));
222
+ }
223
+ }
224
+ if (addon.coreExited(core)) finish(addon.coreExitCode(core));
225
+ } catch {
226
+ finish(1);
227
+ }
228
+ }, 2);
229
+
230
+ return {
231
+ exited,
232
+ write(data) { addon.writeCore(core, Buffer.from(data)); },
233
+ closeStdin() { addon.closeCore(core); },
234
+ abortHostEffects,
235
+ abort() { abortHostEffects(); addon.closeCore(core); },
236
+ setLineHandler(handler) { lineHandler = handler; },
237
+ };
238
+ }
239
+
240
+ function createNativeAgent(addon, options) {
241
+ return createWasmAgent({
242
+ ...options,
243
+ runtimeFactory(runtimeOptions) {
244
+ return createNativeCoreRuntime(addon, runtimeOptions);
245
+ },
246
+ });
247
+ }
248
+
249
+ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
250
+ const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
251
+ validateGatewayChatUrl(runtimeOptions.env?.FX_GATEWAY_CHAT_URL);
252
+ if (!new Set(["auto", "native", "wasm"]).has(backend)) {
253
+ throw new TypeError('backend must be "auto", "native", or "wasm"');
254
+ }
255
+
256
+ let nativeError;
257
+ if (backend !== "wasm") {
258
+ const native = await resolveNativeBackend(nativeAddon);
259
+ nativeError = native.error;
260
+ if (typeof native.backend?.[nativeMethod] === "function" ||
261
+ (surface === "agent" && typeof native.backend?.createCore === "function")) {
262
+ try {
263
+ if (typeof native.backend?.[nativeMethod] === "function") {
264
+ return await native.backend[nativeMethod](runtimeOptions);
265
+ }
266
+ return await createNativeAgent(native.backend, runtimeOptions);
267
+ } catch (error) {
268
+ nativeError = error;
269
+ if (backend === "native") throw error;
270
+ }
271
+ }
272
+ if (backend === "native") {
273
+ const error = nativeError ?? new Error(`native addon does not provide ${nativeMethod}()`);
274
+ error.code ??= "LIBFX_NATIVE_UNAVAILABLE";
275
+ throw error;
276
+ }
277
+ }
278
+
279
+ if (!supportsJspi()) throw jspiFallbackError(surface, nativeError);
280
+ return wasmFactory({
281
+ ...runtimeOptions,
282
+ wasm: await wasmBytes(runtimeOptions.wasm ?? defaultWasm),
283
+ });
284
+ }
285
+
286
+ export function createFxAgent(options = {}) {
287
+ return createWithFallback("agent", "createFxAgent", createWasmAgent, defaultCoreWasm, options);
288
+ }
289
+
290
+ export function createFxTerminal(options = {}) {
291
+ return createWithFallback("terminal", "createFxTerminal", createWasmTerminal, defaultTermWasm, options);
292
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "libfx",
3
+ "version": "0.0.1",
4
+ "description": "Embed fx agents and terminals in JavaScript hosts",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vercel-labs/fx.git",
9
+ "directory": "sdk"
10
+ },
11
+ "homepage": "https://github.com/vercel-labs/fx/tree/main/sdk#readme",
12
+ "bugs": "https://github.com/vercel-labs/fx/issues",
13
+ "scripts": {
14
+ "test:node-wasm": "node tests/test-node-wasm.mjs",
15
+ "test:node-napi": "node tests/test-node-napi.mjs",
16
+ "test:browser-wasm": "node tests/test-browser-wasm.mjs"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "node": "./node.js",
21
+ "browser": "./browser.js",
22
+ "default": "./browser.js"
23
+ },
24
+ "./node": "./node.js",
25
+ "./browser": "./browser.js",
26
+ "./wasm": "./fx-sdk.js"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ },
35
+ "license": "Apache-2.0"
36
+ }