libfx 0.0.7 → 0.0.8-dev.857.gba60fd94fa57

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/node.js CHANGED
@@ -1,32 +1,56 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
+ import { closeSync } from "node:fs";
2
3
  import { createRequire } from "node:module";
4
+ import { Socket } from "node:net";
3
5
  import { homedir } from "node:os";
4
6
  import { isAbsolute, resolve } from "node:path";
5
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { CoreOutput } from "./core-output.js";
9
+ import { loadModule, withModuleFailure } from "./wasm-module.js";
6
10
  import {
7
11
  createFxAgent as createWasmAgent,
8
12
  createFxTerminal as createWasmTerminal,
9
13
  encodeXtermKeyEvent,
10
14
  fxSdkApiVersion,
15
+ listModels,
11
16
  supportsJspi,
12
17
  xtermAdapter,
13
18
  } from "./fx-sdk.js";
14
19
 
15
- export { encodeXtermKeyEvent, fxSdkApiVersion, supportsJspi, xtermAdapter };
20
+ export { encodeXtermKeyEvent, fxSdkApiVersion, listModels, supportsJspi, xtermAdapter };
16
21
  export const libfxApiVersion = 2;
22
+ const nativeCoreApiVersion = 3;
17
23
 
18
24
  const fetchOperationStale = 0;
19
25
  const fetchOperationApplied = 1;
20
26
  const fetchOperationBackpressure = 2;
21
27
 
22
- const require = createRequire(import.meta.url);
28
+ const nodeRequire = createRequire(import.meta.url);
23
29
  const defaultCoreWasm = new URL("./fx-core.wasm", import.meta.url);
24
30
  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
31
  let nativeBackendPromise;
32
+ const wasmFilePromises = new Map();
33
+
34
+ const backendReasonCodes = {
35
+ unsupportedPlatform: "LIBFX_UNSUPPORTED_PLATFORM",
36
+ missingArtifact: "LIBFX_NATIVE_ARTIFACT_MISSING",
37
+ nativeLoad: "LIBFX_NATIVE_LOAD_FAILED",
38
+ nativeApi: "LIBFX_NATIVE_API_MISMATCH",
39
+ missingSurface: "LIBFX_NATIVE_SURFACE_MISSING",
40
+ disabledNative: "LIBFX_NATIVE_DISABLED",
41
+ jspiUnavailable: "LIBFX_JSPI_UNAVAILABLE",
42
+ wasmLoad: "LIBFX_WASM_LOAD_FAILED",
43
+ };
44
+
45
+ function bundledAssetUrl(asset) {
46
+ if (asset.protocol !== "" || typeof asset.href !== "string" ||
47
+ typeof __webpack_base_uri__ === "undefined" || typeof __webpack_public_path__ !== "string" ||
48
+ !asset.href.startsWith(__webpack_public_path__)) {
49
+ throw new TypeError("Bundled asset URL has no filesystem mapping");
50
+ }
51
+ // Webpack's relative URL is a public asset address, not a Node URL instance.
52
+ return new URL(asset.href.slice(__webpack_public_path__.length), __webpack_base_uri__);
53
+ }
30
54
 
31
55
  function jspiFallbackError(surface, nativeError) {
32
56
  const nativeDetail = nativeError ? ` Native loading failed: ${nativeError.message}.` : " No compatible native addon was found.";
@@ -43,8 +67,10 @@ function jspiFallbackError(surface, nativeError) {
43
67
  async function loadNativeCandidate(candidate) {
44
68
  if (candidate == null) return null;
45
69
  if (candidate instanceof URL) {
70
+ if (candidate.protocol === "") candidate = bundledAssetUrl(candidate);
46
71
  if (candidate.protocol === "file:" && candidate.pathname.endsWith(".node")) {
47
- return require(fileURLToPath(candidate));
72
+ // Bundlers trace the asset URL; Node must load the native file at runtime.
73
+ return Reflect.apply(nodeRequire, undefined, [fileURLToPath(candidate)]);
48
74
  }
49
75
  const imported = await import(candidate.href);
50
76
  return imported.default ?? imported;
@@ -53,83 +79,273 @@ async function loadNativeCandidate(candidate) {
53
79
  if (typeof candidate !== "string") {
54
80
  throw new TypeError("nativeAddon must be a module, path, URL, false, or undefined");
55
81
  }
56
- if (candidate.endsWith(".node")) return require(isAbsolute(candidate) ? candidate : resolve(candidate));
82
+ if (candidate.endsWith(".node")) {
83
+ return Reflect.apply(nodeRequire, undefined, [isAbsolute(candidate) ? candidate : resolve(candidate)]);
84
+ }
57
85
  const imported = await import(candidate.startsWith("file:") ? candidate : pathToFileURL(candidate).href);
58
86
  return imported.default ?? imported;
59
87
  }
60
88
 
89
+ function defaultNativeCandidate() {
90
+ // Local path bindings let deployment tracers retain these assets in the generated CommonJS entry.
91
+ if (process.platform === "linux" && process.arch === "x64") {
92
+ const asset = new URL("./libfx.linux-x64.node", import.meta.url);
93
+ if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
94
+ const path = fileURLToPath(asset);
95
+ return path;
96
+ }
97
+ if (process.platform === "linux" && process.arch === "arm64") {
98
+ const asset = new URL("./libfx.linux-arm64.node", import.meta.url);
99
+ if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
100
+ const path = fileURLToPath(asset);
101
+ return path;
102
+ }
103
+ if (process.platform === "darwin" && process.arch === "x64") {
104
+ const asset = new URL("./libfx.darwin-x64.node", import.meta.url);
105
+ if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
106
+ const path = fileURLToPath(asset);
107
+ return path;
108
+ }
109
+ if (process.platform === "darwin" && process.arch === "arm64") {
110
+ const asset = new URL("./libfx.darwin-arm64.node", import.meta.url);
111
+ if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
112
+ const path = fileURLToPath(asset);
113
+ return path;
114
+ }
115
+ return null;
116
+ }
117
+
61
118
  function validateNativeBackend(backend) {
62
119
  if (!backend) return null;
63
120
  const hasLowLevelCore = typeof backend.createCore === "function";
64
- if ((hasLowLevelCore && backend.libfxApiVersion !== libfxApiVersion) ||
65
- (!hasLowLevelCore && backend.libfxApiVersion !== undefined && backend.libfxApiVersion !== libfxApiVersion)) {
121
+ const expectedVersion = hasLowLevelCore ? nativeCoreApiVersion : libfxApiVersion;
122
+ if ((hasLowLevelCore || backend.libfxApiVersion !== undefined) && backend.libfxApiVersion !== expectedVersion) {
66
123
  const actualVersion = backend.libfxApiVersion ?? "missing";
67
- throw new Error(`native addon API version ${actualVersion} is incompatible with libfx API version ${libfxApiVersion}`);
124
+ throw new Error(`native addon API version ${actualVersion} is incompatible with expected API version ${expectedVersion}`);
68
125
  }
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()");
126
+ if (typeof backend.createCore !== "function" && typeof backend.createFxTerminal !== "function") {
127
+ throw new Error("native addon must export createCore() or createFxTerminal()");
72
128
  }
73
129
  return backend;
74
130
  }
75
131
 
132
+ function missingArtifact(error) {
133
+ return error?.code === "ENOENT" || error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
134
+ }
135
+
136
+ function nativeCandidateFilePath(candidate) {
137
+ if (candidate instanceof URL) {
138
+ if (candidate.protocol === "") candidate = bundledAssetUrl(candidate);
139
+ return candidate.protocol === "file:" ? fileURLToPath(candidate) : null;
140
+ }
141
+ if (typeof candidate !== "string") return null;
142
+ if (candidate.startsWith("file:")) return fileURLToPath(new URL(candidate));
143
+ return URL.canParse(candidate) ? null : resolve(candidate);
144
+ }
145
+
146
+ async function nativeArtifactMissing(candidate) {
147
+ try {
148
+ const path = nativeCandidateFilePath(candidate);
149
+ if (path === null) return false;
150
+ await access(path);
151
+ return false;
152
+ } catch (error) {
153
+ return missingArtifact(error);
154
+ }
155
+ }
156
+
157
+ function validationFailure(error) {
158
+ return error?.message?.startsWith("native addon API version ") ? "api" : "surface";
159
+ }
160
+
161
+ async function loadAndValidateNativeCandidate(candidate, artifactMissing = false) {
162
+ let backend;
163
+ try {
164
+ backend = await loadNativeCandidate(candidate);
165
+ } catch (error) {
166
+ return { backend: null, error, failure: artifactMissing ? "missing" : "load" };
167
+ }
168
+ try {
169
+ return { backend: validateNativeBackend(backend), error: null, failure: null };
170
+ } catch (error) {
171
+ return { backend: null, error, failure: validationFailure(error) };
172
+ }
173
+ }
174
+
76
175
  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 };
176
+ const candidate = defaultNativeCandidate();
177
+ if (!candidate) {
178
+ return { backend: null, error: null, failure: "unsupported" };
179
+ }
180
+ try {
181
+ await access(candidate);
182
+ } catch (error) {
183
+ if (missingArtifact(error)) {
184
+ return { backend: null, error: null, probeError: error, failure: "missing" };
89
185
  }
186
+ return { backend: null, error, failure: "load" };
90
187
  }
91
- return { backend: null, error: null };
188
+ return loadAndValidateNativeCandidate(candidate);
92
189
  }
93
190
 
94
191
  async function resolveNativeBackend(nativeAddon) {
95
- if (nativeAddon === false) return { backend: null, error: null };
192
+ if (nativeAddon === false) return { backend: null, error: null, failure: "disabled" };
96
193
  if (nativeAddon !== undefined) {
97
- try {
98
- return { backend: validateNativeBackend(await loadNativeCandidate(nativeAddon)), error: null };
99
- } catch (error) {
100
- return { backend: null, error };
101
- }
194
+ return loadAndValidateNativeCandidate(nativeAddon, await nativeArtifactMissing(nativeAddon));
102
195
  }
103
196
  nativeBackendPromise ??= discoverNativeBackend();
104
197
  return nativeBackendPromise;
105
198
  }
106
199
 
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;
200
+ function wasmInput(input) {
201
+ const path = wasmFilePath(input);
202
+ if (path === null) {
203
+ if (input instanceof URL) return input.href;
204
+ return input;
205
+ }
206
+ const cached = wasmFilePromises.get(path);
207
+ if (cached) return cached;
208
+ const pendingRead = readFile(path);
209
+ let pending;
210
+ pending = withModuleFailure(pendingRead, () => {
211
+ if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
212
+ });
213
+ wasmFilePromises.set(path, pending);
214
+ pendingRead.catch(() => {
215
+ if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
216
+ });
217
+ return pending;
111
218
  }
112
219
 
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");
220
+ function wasmFilePath(input) {
221
+ if (input instanceof URL) {
222
+ if (input.protocol === "") input = bundledAssetUrl(input);
223
+ if (input.protocol === "file:") return fileURLToPath(input);
120
224
  }
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");
225
+ if (typeof input === "string" && !URL.canParse(input)) return resolve(input);
226
+ return null;
227
+ }
228
+
229
+ function reason(code, message, error) {
230
+ const causeCode = error?.code;
231
+ return {
232
+ code,
233
+ message,
234
+ ...(typeof causeCode === "string" || typeof causeCode === "number" ? { causeCode } : {}),
235
+ };
236
+ }
237
+
238
+ function nativeFailureReason(result) {
239
+ const detailError = result.probeError ?? result.error;
240
+ switch (result.failure) {
241
+ case "unsupported":
242
+ return reason(
243
+ backendReasonCodes.unsupportedPlatform,
244
+ `native addon is not available for ${process.platform}-${process.arch}`,
245
+ );
246
+ case "missing":
247
+ return reason(
248
+ backendReasonCodes.missingArtifact,
249
+ `native addon artifact was not found${detailError?.message ? `: ${detailError.message}` : ""}`,
250
+ detailError,
251
+ );
252
+ case "load":
253
+ return reason(
254
+ backendReasonCodes.nativeLoad,
255
+ `native addon failed to load${result.error?.message ? `: ${result.error.message}` : ""}`,
256
+ result.error,
257
+ );
258
+ case "api":
259
+ return reason(backendReasonCodes.nativeApi, result.error.message, result.error);
260
+ case "surface":
261
+ return reason(backendReasonCodes.missingSurface, result.error.message, result.error);
262
+ case "disabled":
263
+ return reason(backendReasonCodes.disabledNative, "native addon loading is disabled");
264
+ default:
265
+ return reason(backendReasonCodes.nativeLoad, "native addon is unavailable", result.error);
266
+ }
267
+ }
268
+
269
+ function validateBackendInfoOptions(value) {
270
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
271
+ throw new TypeError("getBackendInfo() options must be an object");
272
+ }
273
+ const { nativeAddon, backend = "auto", surface = "agent", ...options } = value;
274
+ for (const key of Object.keys(options)) {
275
+ if (key !== "wasm") {
276
+ throw new TypeError(`getBackendInfo() does not accept ${key}`);
277
+ }
278
+ }
279
+ if (!new Set(["agent", "terminal"]).has(surface)) {
280
+ throw new TypeError('surface must be "agent" or "terminal"');
281
+ }
282
+ if (!new Set(["auto", "native", "wasm"]).has(backend)) {
283
+ throw new TypeError('backend must be "auto", "native", or "wasm"');
284
+ }
285
+ if (nativeAddon !== undefined && nativeAddon !== false &&
286
+ typeof nativeAddon !== "string" && !(nativeAddon instanceof URL) &&
287
+ (typeof nativeAddon !== "object" || nativeAddon === null)) {
288
+ throw new TypeError("nativeAddon must be a module, path, URL, false, or undefined");
289
+ }
290
+ const validWasm = options.wasm === undefined || typeof options.wasm === "string" || options.wasm instanceof URL ||
291
+ options.wasm instanceof Promise || options.wasm instanceof WebAssembly.Module || options.wasm instanceof Response ||
292
+ options.wasm instanceof ArrayBuffer || ArrayBuffer.isView(options.wasm);
293
+ if (Object.hasOwn(options, "wasm") && !validWasm) {
294
+ throw new TypeError("wasm must be a URL, Response, ArrayBuffer, typed array, or WebAssembly.Module");
295
+ }
296
+ return { ...options, surface, backend, nativeAddon };
297
+ }
298
+
299
+ export async function getBackendInfo(value = {}) {
300
+ const { surface, backend, nativeAddon, wasm } = validateBackendInfoOptions(value);
301
+ const attempts = [];
302
+ if (backend !== "wasm") {
303
+ const native = await resolveNativeBackend(nativeAddon);
304
+ const nativeMethod = surface === "agent" ? "createCore" : "createFxTerminal";
305
+ if (typeof native.backend?.[nativeMethod] === "function") {
306
+ attempts.push({ backend: "native", available: true, reason: null });
307
+ return { surface, backend: "native", attempts };
308
+ }
309
+ const failureReason = native.backend
310
+ ? reason(backendReasonCodes.missingSurface, `native addon does not provide ${nativeMethod}()`)
311
+ : nativeFailureReason(native);
312
+ attempts.push({ backend: "native", available: false, reason: failureReason });
313
+ if (backend === "native") return { surface, backend: "unavailable", attempts };
314
+ }
315
+
316
+ if (!supportsJspi()) {
317
+ attempts.push({
318
+ backend: "wasm-jspi",
319
+ available: false,
320
+ reason: reason(
321
+ backendReasonCodes.jspiUnavailable,
322
+ "WebAssembly backend requires JavaScript Promise Integration (JSPI)",
323
+ ),
324
+ });
325
+ return { surface, backend: "unavailable", attempts };
326
+ }
327
+ const defaultWasm = surface === "agent" ? defaultCoreWasm : defaultTermWasm;
328
+ const wasmSource = wasm ?? defaultWasm;
329
+ try {
330
+ await loadModule(await wasmInput(wasmSource));
331
+ attempts.push({ backend: "wasm-jspi", available: true, reason: null });
332
+ return { surface, backend: "wasm-jspi", attempts };
333
+ } catch (error) {
334
+ attempts.push({
335
+ backend: "wasm-jspi",
336
+ available: false,
337
+ reason: reason(
338
+ backendReasonCodes.wasmLoad,
339
+ `WebAssembly asset failed to load or compile: ${error?.message ?? String(error)}`,
340
+ error,
341
+ ),
342
+ });
343
+ return { surface, backend: "unavailable", attempts };
125
344
  }
126
345
  }
127
346
 
128
347
  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);
348
+ const { apiKey, model, gatewayChatUrl } = options;
133
349
  const core = addon.createCore({
134
350
  apiKey,
135
351
  home: options.home ?? homedir(),
@@ -137,9 +353,24 @@ function createNativeCoreRuntime(addon, options) {
137
353
  ...(model === undefined ? {} : { model }),
138
354
  ...(gatewayChatUrl === undefined ? {} : { gatewayChatUrl }),
139
355
  });
356
+ let readyFd;
357
+ let readySocket;
358
+ try {
359
+ readyFd = addon.takeCoreReadyFd(core);
360
+ readySocket = new Socket({ fd: readyFd, readable: true, writable: false });
361
+ } catch (error) {
362
+ if (readyFd !== undefined) {
363
+ try { closeSync(readyFd); } catch {}
364
+ }
365
+ addon.destroyCore(core);
366
+ throw error;
367
+ }
368
+ const readyClosed = new Promise((resolve) => readySocket.once("close", resolve));
140
369
  let exitedResolve;
141
370
  let lineHandler = null;
142
- let lineBuffer = "";
371
+ const output = new CoreOutput((message, size) => lineHandler(message, size));
372
+ let draining = false;
373
+ let outputError;
143
374
  let settled = false;
144
375
  let fetchState = null;
145
376
  const exited = new Promise((resolve) => { exitedResolve = resolve; });
@@ -147,13 +378,15 @@ function createNativeCoreRuntime(addon, options) {
147
378
  fetchState?.controller.abort();
148
379
  try { addon.abortCoreFetch(core); } catch {}
149
380
  };
150
- const finish = (code) => {
381
+ const finish = (code, error) => {
151
382
  if (settled) return;
152
383
  settled = true;
153
- clearInterval(timer);
384
+ outputError = error;
385
+ output.close();
154
386
  abortHostEffects();
155
387
  try { addon.destroyCore(core); } catch {}
156
- exitedResolve(code);
388
+ readySocket.destroy();
389
+ void readyClosed.then(() => exitedResolve(code));
157
390
  };
158
391
  const pumpFetch = async (request) => {
159
392
  const controller = new AbortController();
@@ -197,10 +430,14 @@ function createNativeCoreRuntime(addon, options) {
197
430
  } catch {}
198
431
  }
199
432
  } finally {
200
- if (fetchState === state) fetchState = null;
433
+ if (fetchState === state) {
434
+ fetchState = null;
435
+ queueMicrotask(drainReady);
436
+ }
201
437
  }
202
438
  };
203
- const timer = setInterval(() => {
439
+ function drainReady() {
440
+ if (settled) return;
204
441
  try {
205
442
  if (fetchState) {
206
443
  if (!fetchState.controller.signal.aborted && !addon.coreFetchActive(core, fetchState.handle)) {
@@ -210,29 +447,55 @@ function createNativeCoreRuntime(addon, options) {
210
447
  const fetchRequest = addon.takeCoreFetch(core);
211
448
  if (fetchRequest) void pumpFetch(JSON.parse(fetchRequest.toString("utf8")));
212
449
  }
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
- }
450
+ if (addon.coreExitCode(core) !== 0) {
451
+ finish(1, new Error("native output delivery failed"));
452
+ return;
223
453
  }
224
- if (addon.coreExited(core)) finish(addon.coreExitCode(core));
225
- } catch {
226
- finish(1);
454
+ void drainOutput();
455
+ } catch (error) {
456
+ finish(1, error);
227
457
  }
228
- }, 2);
458
+ }
459
+ async function drainOutput() {
460
+ if (draining || settled) return;
461
+ draining = true;
462
+ try {
463
+ while (!settled) {
464
+ const chunk = addon.drainCore(core);
465
+ if (!chunk.length) break;
466
+ const pending = output.write(chunk);
467
+ if (pending) await pending;
468
+ }
469
+ if (!settled && addon.coreExited(core)) {
470
+ output.finish();
471
+ finish(addon.coreExitCode(core));
472
+ }
473
+ } catch (error) {
474
+ finish(1, error);
475
+ } finally {
476
+ draining = false;
477
+ }
478
+ }
479
+
480
+ readySocket.on("data", drainReady);
481
+ readySocket.on("end", () => { drainReady(); if (!settled) finish(1); });
482
+ readySocket.on("error", () => finish(1));
483
+ readySocket.on("close", () => { if (!settled) finish(1); });
484
+ // Some runtimes defer descriptor adoption until connect().
485
+ if (readySocket.pending) {
486
+ try { readySocket.connect({ fd: readyFd }); } catch (error) { finish(1); throw error; }
487
+ }
229
488
 
230
489
  return {
231
490
  exited,
491
+ get error() { return outputError; },
232
492
  write(data) { addon.writeCore(core, Buffer.from(data)); },
233
493
  closeStdin() { addon.closeCore(core); },
234
494
  abortHostEffects,
235
- abort() { abortHostEffects(); addon.closeCore(core); },
495
+ abort(error) {
496
+ if (error) finish(1, error);
497
+ else { abortHostEffects(); addon.closeCore(core); }
498
+ },
236
499
  setLineHandler(handler) { lineHandler = handler; },
237
500
  };
238
501
  }
@@ -248,22 +511,20 @@ function createNativeAgent(addon, options) {
248
511
 
249
512
  async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
250
513
  const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
251
- validateGatewayChatUrl(runtimeOptions.env?.FX_GATEWAY_CHAT_URL);
252
514
  if (!new Set(["auto", "native", "wasm"]).has(backend)) {
253
515
  throw new TypeError('backend must be "auto", "native", or "wasm"');
254
516
  }
255
517
 
256
518
  let nativeError;
519
+ let nativeAttempted = false;
257
520
  if (backend !== "wasm") {
258
521
  const native = await resolveNativeBackend(nativeAddon);
259
522
  nativeError = native.error;
260
- if (typeof native.backend?.[nativeMethod] === "function" ||
261
- (surface === "agent" && typeof native.backend?.createCore === "function")) {
523
+ if (typeof native.backend?.[nativeMethod] === "function") {
524
+ nativeAttempted = true;
262
525
  try {
263
- if (typeof native.backend?.[nativeMethod] === "function") {
264
- return await native.backend[nativeMethod](runtimeOptions);
265
- }
266
- return await createNativeAgent(native.backend, runtimeOptions);
526
+ if (surface === "agent") return await createNativeAgent(native.backend, runtimeOptions);
527
+ return await native.backend[nativeMethod](runtimeOptions);
267
528
  } catch (error) {
268
529
  nativeError = error;
269
530
  if (backend === "native") throw error;
@@ -276,15 +537,25 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
276
537
  }
277
538
  }
278
539
 
279
- if (!supportsJspi()) throw jspiFallbackError(surface, nativeError);
280
- return wasmFactory({
281
- ...runtimeOptions,
282
- wasm: await wasmBytes(runtimeOptions.wasm ?? defaultWasm),
283
- });
540
+ if (!supportsJspi()) {
541
+ if (nativeAttempted) throw nativeError;
542
+ throw jspiFallbackError(surface, nativeError);
543
+ }
544
+ const wasmSource = runtimeOptions.wasm ?? defaultWasm;
545
+ return wasmFactory({ ...runtimeOptions, wasm: await wasmInput(wasmSource) });
284
546
  }
285
547
 
286
- export function createFxAgent(options = {}) {
287
- return createWithFallback("agent", "createFxAgent", createWasmAgent, defaultCoreWasm, options);
548
+ export async function createFxAgent(options = {}) {
549
+ if (options != null && Object.hasOwn(Object(options), "env")) {
550
+ throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
551
+ }
552
+ return createWithFallback(
553
+ "agent",
554
+ "createCore",
555
+ createWasmAgent,
556
+ defaultCoreWasm,
557
+ options,
558
+ );
288
559
  }
289
560
 
290
561
  export function createFxTerminal(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.7",
3
+ "version": "0.0.8-dev.857.gba60fd94fa57",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,13 +17,22 @@
17
17
  },
18
18
  "exports": {
19
19
  ".": {
20
- "node": "./node.js",
20
+ "node": {
21
+ "import": "./node.js",
22
+ "require": "./node.cjs"
23
+ },
21
24
  "browser": "./browser.js",
22
25
  "default": "./browser.js"
23
26
  },
24
- "./node": "./node.js",
27
+ "./node": {
28
+ "import": "./node.js",
29
+ "require": "./node.cjs"
30
+ },
25
31
  "./browser": "./browser.js",
26
- "./wasm": "./fx-sdk.js"
32
+ "./wasm": "./fx-sdk.js",
33
+ "./mcp": "./mcp.js",
34
+ "./skills": "./skills.js",
35
+ "./skills/node": "./skills-node.js"
27
36
  },
28
37
  "engines": {
29
38
  "node": ">=20"
package/skills-node.js ADDED
@@ -0,0 +1,29 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
3
+
4
+ function parseFrontmatter(source) {
5
+ if (!source.startsWith("---\n")) return { metadata: {}, body: source };
6
+ const end = source.indexOf("\n---\n", 4);
7
+ if (end < 0) throw new Error("SKILL.md has unterminated frontmatter");
8
+ const metadata = {};
9
+ for (const line of source.slice(4, end).split("\n")) {
10
+ const separator = line.indexOf(":");
11
+ if (separator < 0) continue;
12
+ metadata[line.slice(0, separator).trim()] = line.slice(separator + 1).trim();
13
+ }
14
+ return { metadata, body: source.slice(end + 5).trim() };
15
+ }
16
+
17
+ export async function loadSkillFile(path, options = {}) {
18
+ const source = await (options.readFile ?? readFile)(path, "utf8");
19
+ const { metadata, body } = parseFrontmatter(source);
20
+ return {
21
+ name: metadata.name || basename(path).replace(/\.md$/i, ""),
22
+ description: metadata.description || "",
23
+ instructions: body,
24
+ resources: options.resources ?? [],
25
+ tools: options.tools ?? [],
26
+ };
27
+ }
28
+
29
+ export { createSkillsAdapter } from "./skills.js";