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/README.md +251 -275
- package/browser.js +2 -1
- package/core-output.js +58 -0
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +675 -211
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/mcp.js +118 -0
- package/node.cjs +2583 -0
- package/node.js +358 -87
- package/package.json +13 -4
- package/skills-node.js +29 -0
- package/skills.js +44 -0
- package/wasm-module.js +50 -0
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
|
|
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
|
-
|
|
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"))
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
124
|
+
throw new Error(`native addon API version ${actualVersion} is incompatible with expected API version ${expectedVersion}`);
|
|
68
125
|
}
|
|
69
|
-
if (typeof backend.
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
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
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
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 (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
384
|
+
outputError = error;
|
|
385
|
+
output.close();
|
|
154
386
|
abortHostEffects();
|
|
155
387
|
try { addon.destroyCore(core); } catch {}
|
|
156
|
-
|
|
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)
|
|
433
|
+
if (fetchState === state) {
|
|
434
|
+
fetchState = null;
|
|
435
|
+
queueMicrotask(drainReady);
|
|
436
|
+
}
|
|
201
437
|
}
|
|
202
438
|
};
|
|
203
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
225
|
-
} catch {
|
|
226
|
-
finish(1);
|
|
454
|
+
void drainOutput();
|
|
455
|
+
} catch (error) {
|
|
456
|
+
finish(1, error);
|
|
227
457
|
}
|
|
228
|
-
}
|
|
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() {
|
|
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
|
-
|
|
523
|
+
if (typeof native.backend?.[nativeMethod] === "function") {
|
|
524
|
+
nativeAttempted = true;
|
|
262
525
|
try {
|
|
263
|
-
if (
|
|
264
|
-
|
|
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())
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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.
|
|
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":
|
|
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":
|
|
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";
|