libfx 0.0.7 → 0.0.8-dev.820.g1d9d3b63d6ea
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 +240 -275
- package/browser.js +2 -1
- package/core-output.js +58 -0
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +658 -208
- 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 +2542 -0
- package/node.js +331 -88
- 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,46 @@
|
|
|
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
|
+
};
|
|
30
44
|
|
|
31
45
|
function jspiFallbackError(surface, nativeError) {
|
|
32
46
|
const nativeDetail = nativeError ? ` Native loading failed: ${nativeError.message}.` : " No compatible native addon was found.";
|
|
@@ -44,7 +58,8 @@ async function loadNativeCandidate(candidate) {
|
|
|
44
58
|
if (candidate == null) return null;
|
|
45
59
|
if (candidate instanceof URL) {
|
|
46
60
|
if (candidate.protocol === "file:" && candidate.pathname.endsWith(".node")) {
|
|
47
|
-
|
|
61
|
+
// Bundlers trace the asset URL; Node must load the native file at runtime.
|
|
62
|
+
return Reflect.apply(nodeRequire, undefined, [fileURLToPath(candidate)]);
|
|
48
63
|
}
|
|
49
64
|
const imported = await import(candidate.href);
|
|
50
65
|
return imported.default ?? imported;
|
|
@@ -53,83 +68,256 @@ async function loadNativeCandidate(candidate) {
|
|
|
53
68
|
if (typeof candidate !== "string") {
|
|
54
69
|
throw new TypeError("nativeAddon must be a module, path, URL, false, or undefined");
|
|
55
70
|
}
|
|
56
|
-
if (candidate.endsWith(".node"))
|
|
71
|
+
if (candidate.endsWith(".node")) {
|
|
72
|
+
return Reflect.apply(nodeRequire, undefined, [isAbsolute(candidate) ? candidate : resolve(candidate)]);
|
|
73
|
+
}
|
|
57
74
|
const imported = await import(candidate.startsWith("file:") ? candidate : pathToFileURL(candidate).href);
|
|
58
75
|
return imported.default ?? imported;
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
function defaultNativeCandidate() {
|
|
79
|
+
if (process.platform === "linux" && process.arch === "x64") {
|
|
80
|
+
return new URL("./libfx.linux-x64.node", import.meta.url);
|
|
81
|
+
}
|
|
82
|
+
if (process.platform === "linux" && process.arch === "arm64") {
|
|
83
|
+
return new URL("./libfx.linux-arm64.node", import.meta.url);
|
|
84
|
+
}
|
|
85
|
+
if (process.platform === "darwin" && process.arch === "x64") {
|
|
86
|
+
return new URL("./libfx.darwin-x64.node", import.meta.url);
|
|
87
|
+
}
|
|
88
|
+
if (process.platform === "darwin" && process.arch === "arm64") {
|
|
89
|
+
return new URL("./libfx.darwin-arm64.node", import.meta.url);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
61
94
|
function validateNativeBackend(backend) {
|
|
62
95
|
if (!backend) return null;
|
|
63
96
|
const hasLowLevelCore = typeof backend.createCore === "function";
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
const expectedVersion = hasLowLevelCore ? nativeCoreApiVersion : libfxApiVersion;
|
|
98
|
+
if ((hasLowLevelCore || backend.libfxApiVersion !== undefined) && backend.libfxApiVersion !== expectedVersion) {
|
|
66
99
|
const actualVersion = backend.libfxApiVersion ?? "missing";
|
|
67
|
-
throw new Error(`native addon API version ${actualVersion} is incompatible with
|
|
100
|
+
throw new Error(`native addon API version ${actualVersion} is incompatible with expected API version ${expectedVersion}`);
|
|
68
101
|
}
|
|
69
|
-
if (typeof backend.
|
|
70
|
-
|
|
71
|
-
throw new Error("native addon must export createFxAgent(), createCore(), or createFxTerminal()");
|
|
102
|
+
if (typeof backend.createCore !== "function" && typeof backend.createFxTerminal !== "function") {
|
|
103
|
+
throw new Error("native addon must export createCore() or createFxTerminal()");
|
|
72
104
|
}
|
|
73
105
|
return backend;
|
|
74
106
|
}
|
|
75
107
|
|
|
108
|
+
function missingArtifact(error) {
|
|
109
|
+
return error?.code === "ENOENT" || error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function nativeCandidateFilePath(candidate) {
|
|
113
|
+
if (candidate instanceof URL) return candidate.protocol === "file:" ? fileURLToPath(candidate) : null;
|
|
114
|
+
if (typeof candidate !== "string") return null;
|
|
115
|
+
if (candidate.startsWith("file:")) return fileURLToPath(new URL(candidate));
|
|
116
|
+
return URL.canParse(candidate) ? null : resolve(candidate);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function nativeArtifactMissing(candidate) {
|
|
120
|
+
try {
|
|
121
|
+
const path = nativeCandidateFilePath(candidate);
|
|
122
|
+
if (path === null) return false;
|
|
123
|
+
await access(path);
|
|
124
|
+
return false;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return missingArtifact(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validationFailure(error) {
|
|
131
|
+
return error?.message?.startsWith("native addon API version ") ? "api" : "surface";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function loadAndValidateNativeCandidate(candidate, artifactMissing = false) {
|
|
135
|
+
let backend;
|
|
136
|
+
try {
|
|
137
|
+
backend = await loadNativeCandidate(candidate);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { backend: null, error, failure: artifactMissing ? "missing" : "load" };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return { backend: validateNativeBackend(backend), error: null, failure: null };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { backend: null, error, failure: validationFailure(error) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
76
148
|
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 };
|
|
149
|
+
const candidate = defaultNativeCandidate();
|
|
150
|
+
if (!candidate) {
|
|
151
|
+
return { backend: null, error: null, failure: "unsupported" };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await access(fileURLToPath(candidate));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (missingArtifact(error)) {
|
|
157
|
+
return { backend: null, error: null, probeError: error, failure: "missing" };
|
|
89
158
|
}
|
|
159
|
+
return { backend: null, error, failure: "load" };
|
|
90
160
|
}
|
|
91
|
-
return
|
|
161
|
+
return loadAndValidateNativeCandidate(candidate);
|
|
92
162
|
}
|
|
93
163
|
|
|
94
164
|
async function resolveNativeBackend(nativeAddon) {
|
|
95
|
-
if (nativeAddon === false) return { backend: null, error: null };
|
|
165
|
+
if (nativeAddon === false) return { backend: null, error: null, failure: "disabled" };
|
|
96
166
|
if (nativeAddon !== undefined) {
|
|
97
|
-
|
|
98
|
-
return { backend: validateNativeBackend(await loadNativeCandidate(nativeAddon)), error: null };
|
|
99
|
-
} catch (error) {
|
|
100
|
-
return { backend: null, error };
|
|
101
|
-
}
|
|
167
|
+
return loadAndValidateNativeCandidate(nativeAddon, await nativeArtifactMissing(nativeAddon));
|
|
102
168
|
}
|
|
103
169
|
nativeBackendPromise ??= discoverNativeBackend();
|
|
104
170
|
return nativeBackendPromise;
|
|
105
171
|
}
|
|
106
172
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
173
|
+
function wasmInput(input) {
|
|
174
|
+
const path = wasmFilePath(input);
|
|
175
|
+
if (path === null) {
|
|
176
|
+
if (input instanceof URL) return input.href;
|
|
177
|
+
return input;
|
|
178
|
+
}
|
|
179
|
+
const cached = wasmFilePromises.get(path);
|
|
180
|
+
if (cached) return cached;
|
|
181
|
+
const pendingRead = readFile(path);
|
|
182
|
+
let pending;
|
|
183
|
+
pending = withModuleFailure(pendingRead, () => {
|
|
184
|
+
if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
|
|
185
|
+
});
|
|
186
|
+
wasmFilePromises.set(path, pending);
|
|
187
|
+
pendingRead.catch(() => {
|
|
188
|
+
if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
|
|
189
|
+
});
|
|
190
|
+
return pending;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function wasmFilePath(input) {
|
|
194
|
+
if (input instanceof URL && input.protocol === "file:") return fileURLToPath(input);
|
|
195
|
+
if (typeof input === "string" && !URL.canParse(input)) return resolve(input);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function reason(code, message, error) {
|
|
200
|
+
const causeCode = error?.code;
|
|
201
|
+
return {
|
|
202
|
+
code,
|
|
203
|
+
message,
|
|
204
|
+
...(typeof causeCode === "string" || typeof causeCode === "number" ? { causeCode } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function nativeFailureReason(result) {
|
|
209
|
+
const detailError = result.probeError ?? result.error;
|
|
210
|
+
switch (result.failure) {
|
|
211
|
+
case "unsupported":
|
|
212
|
+
return reason(
|
|
213
|
+
backendReasonCodes.unsupportedPlatform,
|
|
214
|
+
`native addon is not available for ${process.platform}-${process.arch}`,
|
|
215
|
+
);
|
|
216
|
+
case "missing":
|
|
217
|
+
return reason(
|
|
218
|
+
backendReasonCodes.missingArtifact,
|
|
219
|
+
`native addon artifact was not found${detailError?.message ? `: ${detailError.message}` : ""}`,
|
|
220
|
+
detailError,
|
|
221
|
+
);
|
|
222
|
+
case "load":
|
|
223
|
+
return reason(
|
|
224
|
+
backendReasonCodes.nativeLoad,
|
|
225
|
+
`native addon failed to load${result.error?.message ? `: ${result.error.message}` : ""}`,
|
|
226
|
+
result.error,
|
|
227
|
+
);
|
|
228
|
+
case "api":
|
|
229
|
+
return reason(backendReasonCodes.nativeApi, result.error.message, result.error);
|
|
230
|
+
case "surface":
|
|
231
|
+
return reason(backendReasonCodes.missingSurface, result.error.message, result.error);
|
|
232
|
+
case "disabled":
|
|
233
|
+
return reason(backendReasonCodes.disabledNative, "native addon loading is disabled");
|
|
234
|
+
default:
|
|
235
|
+
return reason(backendReasonCodes.nativeLoad, "native addon is unavailable", result.error);
|
|
236
|
+
}
|
|
111
237
|
}
|
|
112
238
|
|
|
113
|
-
function
|
|
114
|
-
if (value
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
239
|
+
function validateBackendInfoOptions(value) {
|
|
240
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
241
|
+
throw new TypeError("getBackendInfo() options must be an object");
|
|
242
|
+
}
|
|
243
|
+
const options = { ...value };
|
|
244
|
+
for (const key of Object.keys(options)) {
|
|
245
|
+
if (!new Set(["surface", "backend", "nativeAddon", "wasm"]).has(key)) {
|
|
246
|
+
throw new TypeError(`getBackendInfo() does not accept ${key}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const surface = options.surface ?? "agent";
|
|
250
|
+
if (!new Set(["agent", "terminal"]).has(surface)) {
|
|
251
|
+
throw new TypeError('surface must be "agent" or "terminal"');
|
|
252
|
+
}
|
|
253
|
+
const backend = options.backend ?? "auto";
|
|
254
|
+
if (!new Set(["auto", "native", "wasm"]).has(backend)) {
|
|
255
|
+
throw new TypeError('backend must be "auto", "native", or "wasm"');
|
|
256
|
+
}
|
|
257
|
+
if (Object.hasOwn(options, "nativeAddon") && options.nativeAddon !== undefined && options.nativeAddon !== false &&
|
|
258
|
+
typeof options.nativeAddon !== "string" && !(options.nativeAddon instanceof URL) &&
|
|
259
|
+
(typeof options.nativeAddon !== "object" || options.nativeAddon === null)) {
|
|
260
|
+
throw new TypeError("nativeAddon must be a module, path, URL, false, or undefined");
|
|
261
|
+
}
|
|
262
|
+
const validWasm = options.wasm === undefined || typeof options.wasm === "string" || options.wasm instanceof URL ||
|
|
263
|
+
options.wasm instanceof Promise || options.wasm instanceof WebAssembly.Module || options.wasm instanceof Response ||
|
|
264
|
+
options.wasm instanceof ArrayBuffer || ArrayBuffer.isView(options.wasm);
|
|
265
|
+
if (Object.hasOwn(options, "wasm") && !validWasm) {
|
|
266
|
+
throw new TypeError("wasm must be a URL, Response, ArrayBuffer, typed array, or WebAssembly.Module");
|
|
267
|
+
}
|
|
268
|
+
return { ...options, surface, backend };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function getBackendInfo(value = {}) {
|
|
272
|
+
const { surface, backend, nativeAddon, wasm } = validateBackendInfoOptions(value);
|
|
273
|
+
const attempts = [];
|
|
274
|
+
if (backend !== "wasm") {
|
|
275
|
+
const native = await resolveNativeBackend(nativeAddon);
|
|
276
|
+
const nativeMethod = surface === "agent" ? "createCore" : "createFxTerminal";
|
|
277
|
+
if (typeof native.backend?.[nativeMethod] === "function") {
|
|
278
|
+
attempts.push({ backend: "native", available: true, reason: null });
|
|
279
|
+
return { surface, backend: "native", attempts };
|
|
280
|
+
}
|
|
281
|
+
const failureReason = native.backend
|
|
282
|
+
? reason(backendReasonCodes.missingSurface, `native addon does not provide ${nativeMethod}()`)
|
|
283
|
+
: nativeFailureReason(native);
|
|
284
|
+
attempts.push({ backend: "native", available: false, reason: failureReason });
|
|
285
|
+
if (backend === "native") return { surface, backend: "unavailable", attempts };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!supportsJspi()) {
|
|
289
|
+
attempts.push({
|
|
290
|
+
backend: "wasm-jspi",
|
|
291
|
+
available: false,
|
|
292
|
+
reason: reason(
|
|
293
|
+
backendReasonCodes.jspiUnavailable,
|
|
294
|
+
"WebAssembly backend requires JavaScript Promise Integration (JSPI)",
|
|
295
|
+
),
|
|
296
|
+
});
|
|
297
|
+
return { surface, backend: "unavailable", attempts };
|
|
298
|
+
}
|
|
299
|
+
const defaultWasm = surface === "agent" ? defaultCoreWasm : defaultTermWasm;
|
|
300
|
+
const wasmSource = wasm ?? defaultWasm;
|
|
301
|
+
try {
|
|
302
|
+
await loadModule(wasmInput(wasmSource));
|
|
303
|
+
attempts.push({ backend: "wasm-jspi", available: true, reason: null });
|
|
304
|
+
return { surface, backend: "wasm-jspi", attempts };
|
|
305
|
+
} catch (error) {
|
|
306
|
+
attempts.push({
|
|
307
|
+
backend: "wasm-jspi",
|
|
308
|
+
available: false,
|
|
309
|
+
reason: reason(
|
|
310
|
+
backendReasonCodes.wasmLoad,
|
|
311
|
+
`WebAssembly asset failed to load or compile: ${error?.message ?? String(error)}`,
|
|
312
|
+
error,
|
|
313
|
+
),
|
|
314
|
+
});
|
|
315
|
+
return { surface, backend: "unavailable", attempts };
|
|
125
316
|
}
|
|
126
317
|
}
|
|
127
318
|
|
|
128
319
|
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);
|
|
320
|
+
const { apiKey, model, gatewayChatUrl } = options;
|
|
133
321
|
const core = addon.createCore({
|
|
134
322
|
apiKey,
|
|
135
323
|
home: options.home ?? homedir(),
|
|
@@ -137,9 +325,24 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
137
325
|
...(model === undefined ? {} : { model }),
|
|
138
326
|
...(gatewayChatUrl === undefined ? {} : { gatewayChatUrl }),
|
|
139
327
|
});
|
|
328
|
+
let readyFd;
|
|
329
|
+
let readySocket;
|
|
330
|
+
try {
|
|
331
|
+
readyFd = addon.takeCoreReadyFd(core);
|
|
332
|
+
readySocket = new Socket({ fd: readyFd, readable: true, writable: false });
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (readyFd !== undefined) {
|
|
335
|
+
try { closeSync(readyFd); } catch {}
|
|
336
|
+
}
|
|
337
|
+
addon.destroyCore(core);
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
const readyClosed = new Promise((resolve) => readySocket.once("close", resolve));
|
|
140
341
|
let exitedResolve;
|
|
141
342
|
let lineHandler = null;
|
|
142
|
-
|
|
343
|
+
const output = new CoreOutput((message, size) => lineHandler(message, size));
|
|
344
|
+
let draining = false;
|
|
345
|
+
let outputError;
|
|
143
346
|
let settled = false;
|
|
144
347
|
let fetchState = null;
|
|
145
348
|
const exited = new Promise((resolve) => { exitedResolve = resolve; });
|
|
@@ -147,13 +350,15 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
147
350
|
fetchState?.controller.abort();
|
|
148
351
|
try { addon.abortCoreFetch(core); } catch {}
|
|
149
352
|
};
|
|
150
|
-
const finish = (code) => {
|
|
353
|
+
const finish = (code, error) => {
|
|
151
354
|
if (settled) return;
|
|
152
355
|
settled = true;
|
|
153
|
-
|
|
356
|
+
outputError = error;
|
|
357
|
+
output.close();
|
|
154
358
|
abortHostEffects();
|
|
155
359
|
try { addon.destroyCore(core); } catch {}
|
|
156
|
-
|
|
360
|
+
readySocket.destroy();
|
|
361
|
+
void readyClosed.then(() => exitedResolve(code));
|
|
157
362
|
};
|
|
158
363
|
const pumpFetch = async (request) => {
|
|
159
364
|
const controller = new AbortController();
|
|
@@ -197,10 +402,14 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
197
402
|
} catch {}
|
|
198
403
|
}
|
|
199
404
|
} finally {
|
|
200
|
-
if (fetchState === state)
|
|
405
|
+
if (fetchState === state) {
|
|
406
|
+
fetchState = null;
|
|
407
|
+
queueMicrotask(drainReady);
|
|
408
|
+
}
|
|
201
409
|
}
|
|
202
410
|
};
|
|
203
|
-
|
|
411
|
+
function drainReady() {
|
|
412
|
+
if (settled) return;
|
|
204
413
|
try {
|
|
205
414
|
if (fetchState) {
|
|
206
415
|
if (!fetchState.controller.signal.aborted && !addon.coreFetchActive(core, fetchState.handle)) {
|
|
@@ -210,29 +419,55 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
210
419
|
const fetchRequest = addon.takeCoreFetch(core);
|
|
211
420
|
if (fetchRequest) void pumpFetch(JSON.parse(fetchRequest.toString("utf8")));
|
|
212
421
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
422
|
+
if (addon.coreExitCode(core) !== 0) {
|
|
423
|
+
finish(1, new Error("native output delivery failed"));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
void drainOutput();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
finish(1, error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function drainOutput() {
|
|
432
|
+
if (draining || settled) return;
|
|
433
|
+
draining = true;
|
|
434
|
+
try {
|
|
435
|
+
while (!settled) {
|
|
436
|
+
const chunk = addon.drainCore(core);
|
|
437
|
+
if (!chunk.length) break;
|
|
438
|
+
const pending = output.write(chunk);
|
|
439
|
+
if (pending) await pending;
|
|
223
440
|
}
|
|
224
|
-
if (addon.coreExited(core))
|
|
225
|
-
|
|
226
|
-
|
|
441
|
+
if (!settled && addon.coreExited(core)) {
|
|
442
|
+
output.finish();
|
|
443
|
+
finish(addon.coreExitCode(core));
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
finish(1, error);
|
|
447
|
+
} finally {
|
|
448
|
+
draining = false;
|
|
227
449
|
}
|
|
228
|
-
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
readySocket.on("data", drainReady);
|
|
453
|
+
readySocket.on("end", () => { drainReady(); if (!settled) finish(1); });
|
|
454
|
+
readySocket.on("error", () => finish(1));
|
|
455
|
+
readySocket.on("close", () => { if (!settled) finish(1); });
|
|
456
|
+
// Some runtimes defer descriptor adoption until connect().
|
|
457
|
+
if (readySocket.pending) {
|
|
458
|
+
try { readySocket.connect({ fd: readyFd }); } catch (error) { finish(1); throw error; }
|
|
459
|
+
}
|
|
229
460
|
|
|
230
461
|
return {
|
|
231
462
|
exited,
|
|
463
|
+
get error() { return outputError; },
|
|
232
464
|
write(data) { addon.writeCore(core, Buffer.from(data)); },
|
|
233
465
|
closeStdin() { addon.closeCore(core); },
|
|
234
466
|
abortHostEffects,
|
|
235
|
-
abort() {
|
|
467
|
+
abort(error) {
|
|
468
|
+
if (error) finish(1, error);
|
|
469
|
+
else { abortHostEffects(); addon.closeCore(core); }
|
|
470
|
+
},
|
|
236
471
|
setLineHandler(handler) { lineHandler = handler; },
|
|
237
472
|
};
|
|
238
473
|
}
|
|
@@ -248,22 +483,20 @@ function createNativeAgent(addon, options) {
|
|
|
248
483
|
|
|
249
484
|
async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
|
|
250
485
|
const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
|
|
251
|
-
validateGatewayChatUrl(runtimeOptions.env?.FX_GATEWAY_CHAT_URL);
|
|
252
486
|
if (!new Set(["auto", "native", "wasm"]).has(backend)) {
|
|
253
487
|
throw new TypeError('backend must be "auto", "native", or "wasm"');
|
|
254
488
|
}
|
|
255
489
|
|
|
256
490
|
let nativeError;
|
|
491
|
+
let nativeAttempted = false;
|
|
257
492
|
if (backend !== "wasm") {
|
|
258
493
|
const native = await resolveNativeBackend(nativeAddon);
|
|
259
494
|
nativeError = native.error;
|
|
260
|
-
if (typeof native.backend?.[nativeMethod] === "function"
|
|
261
|
-
|
|
495
|
+
if (typeof native.backend?.[nativeMethod] === "function") {
|
|
496
|
+
nativeAttempted = true;
|
|
262
497
|
try {
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return await createNativeAgent(native.backend, runtimeOptions);
|
|
498
|
+
if (surface === "agent") return await createNativeAgent(native.backend, runtimeOptions);
|
|
499
|
+
return await native.backend[nativeMethod](runtimeOptions);
|
|
267
500
|
} catch (error) {
|
|
268
501
|
nativeError = error;
|
|
269
502
|
if (backend === "native") throw error;
|
|
@@ -276,15 +509,25 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
276
509
|
}
|
|
277
510
|
}
|
|
278
511
|
|
|
279
|
-
if (!supportsJspi())
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
512
|
+
if (!supportsJspi()) {
|
|
513
|
+
if (nativeAttempted) throw nativeError;
|
|
514
|
+
throw jspiFallbackError(surface, nativeError);
|
|
515
|
+
}
|
|
516
|
+
const wasmSource = runtimeOptions.wasm ?? defaultWasm;
|
|
517
|
+
return wasmFactory({ ...runtimeOptions, wasm: wasmInput(wasmSource) });
|
|
284
518
|
}
|
|
285
519
|
|
|
286
|
-
export function createFxAgent(options = {}) {
|
|
287
|
-
|
|
520
|
+
export async function createFxAgent(options = {}) {
|
|
521
|
+
if (options != null && Object.hasOwn(Object(options), "env")) {
|
|
522
|
+
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
523
|
+
}
|
|
524
|
+
return createWithFallback(
|
|
525
|
+
"agent",
|
|
526
|
+
"createCore",
|
|
527
|
+
createWasmAgent,
|
|
528
|
+
defaultCoreWasm,
|
|
529
|
+
options,
|
|
530
|
+
);
|
|
288
531
|
}
|
|
289
532
|
|
|
290
533
|
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.820.g1d9d3b63d6ea",
|
|
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";
|
package/skills.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const maxSkills = 64;
|
|
2
|
+
const maxInstructionsBytes = 64 * 1024;
|
|
3
|
+
|
|
4
|
+
function escapeAttribute(value) {
|
|
5
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createSkillsAdapter(records) {
|
|
9
|
+
if (!Array.isArray(records) || records.length > maxSkills) {
|
|
10
|
+
throw new TypeError("skills must be an array with at most 64 records");
|
|
11
|
+
}
|
|
12
|
+
const names = new Set();
|
|
13
|
+
const sections = [];
|
|
14
|
+
const tools = [];
|
|
15
|
+
for (const [index, record] of records.entries()) {
|
|
16
|
+
if (!record || typeof record.name !== "string" || typeof record.instructions !== "string") {
|
|
17
|
+
throw new TypeError(`skill ${index} requires name and instructions`);
|
|
18
|
+
}
|
|
19
|
+
if (names.has(record.name)) throw new TypeError(`duplicate skill name: ${record.name}`);
|
|
20
|
+
names.add(record.name);
|
|
21
|
+
const resources = (record.resources ?? []).map((resource) => {
|
|
22
|
+
if (typeof resource?.uri !== "string" || typeof resource?.text !== "string") {
|
|
23
|
+
throw new TypeError(`skill ${record.name} has an invalid resource`);
|
|
24
|
+
}
|
|
25
|
+
return `<resource uri="${escapeAttribute(resource.uri)}">\n${resource.text}\n</resource>`;
|
|
26
|
+
}).join("\n");
|
|
27
|
+
sections.push([
|
|
28
|
+
`<skill name="${escapeAttribute(record.name)}">`,
|
|
29
|
+
record.description ? `<description>${record.description}</description>` : "",
|
|
30
|
+
record.instructions,
|
|
31
|
+
resources,
|
|
32
|
+
"</skill>",
|
|
33
|
+
].filter(Boolean).join("\n"));
|
|
34
|
+
if (record.tools !== undefined) {
|
|
35
|
+
if (!Array.isArray(record.tools)) throw new TypeError(`skill ${record.name} tools must be an array`);
|
|
36
|
+
tools.push(...record.tools);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const instructions = sections.join("\n\n");
|
|
40
|
+
if (new TextEncoder().encode(instructions).length > maxInstructionsBytes) {
|
|
41
|
+
throw new RangeError(`skill instructions exceed the ${maxInstructionsBytes} byte libfx limit`);
|
|
42
|
+
}
|
|
43
|
+
return { instructions, tools };
|
|
44
|
+
}
|