@flowy-agent-store/sdk 0.1.0-beta.10
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/LICENSE +201 -0
- package/README.md +138 -0
- package/dist/index.cjs +475 -0
- package/dist/index.d.cts +248 -0
- package/dist/index.d.mts +248 -0
- package/dist/index.mjs +467 -0
- package/package.json +49 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _flowy_agent_store_client = require("@flowy-agent-store/client");
|
|
3
|
+
let node_fs = require("node:fs");
|
|
4
|
+
let node_module = require("node:module");
|
|
5
|
+
let node_path = require("node:path");
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let node_child_process = require("node:child_process");
|
|
8
|
+
let node_os = require("node:os");
|
|
9
|
+
let node_readline = require("node:readline");
|
|
10
|
+
let _flowy_agent_store_protocol = require("@flowy-agent-store/protocol");
|
|
11
|
+
//#region src/bin.ts
|
|
12
|
+
/**
|
|
13
|
+
* Locate the `flowy-agent-store` runtime binary.
|
|
14
|
+
*
|
|
15
|
+
* Order (docs/agent-store/12 §6):
|
|
16
|
+
* 1. explicit `bin` argument (skips every other lookup)
|
|
17
|
+
* 2. `AGENT_STORE_BIN` environment variable
|
|
18
|
+
* 3. platform runtime package (`@flowy-agent-store/runtime-<platform>-<arch>`,
|
|
19
|
+
* an optionalDependency of `@flowy-agent-store/sdk`) resolved via
|
|
20
|
+
* `createRequire(import.meta.url).resolve` — the binary ships inside the
|
|
21
|
+
* package (`vendor/flowy-agent-store[.exe]`)
|
|
22
|
+
* 4. `flowy-agent-store[.exe]` on `PATH`
|
|
23
|
+
*
|
|
24
|
+
* Anything else is a hard error — the SDK never downloads or guesses.
|
|
25
|
+
*/
|
|
26
|
+
const VENDOR_DIR = "vendor";
|
|
27
|
+
const BIN_NAMES = ["flowy-agent-store", "flowy-agent-store.exe"];
|
|
28
|
+
function resolveAppServerBin(explicit) {
|
|
29
|
+
if (explicit && (0, node_fs.existsSync)(explicit)) return explicit;
|
|
30
|
+
const candidates = [];
|
|
31
|
+
if (explicit) candidates.push(explicit);
|
|
32
|
+
const fromEnv = process.env["AGENT_STORE_BIN"];
|
|
33
|
+
if (fromEnv) candidates.push(fromEnv);
|
|
34
|
+
const fromRuntimePackage = findInRuntimePackage();
|
|
35
|
+
if (fromRuntimePackage) candidates.push(fromRuntimePackage);
|
|
36
|
+
for (const name of BIN_NAMES) {
|
|
37
|
+
const found = findOnPath(name);
|
|
38
|
+
if (found) candidates.push(found);
|
|
39
|
+
}
|
|
40
|
+
const hit = candidates.find((candidate) => (0, node_fs.existsSync)(candidate));
|
|
41
|
+
if (!hit) throw new Error(`cannot find the flowy-agent-store runtime binary: pass \`bin\`, set AGENT_STORE_BIN, install @flowy-agent-store/runtime-${process.platform}-${process.arch} alongside @flowy-agent-store/sdk, or put flowy-agent-store[.exe] on PATH`);
|
|
42
|
+
return hit;
|
|
43
|
+
}
|
|
44
|
+
/** Probe the platform runtime package for a vendored binary (optional dep). */
|
|
45
|
+
function findInRuntimePackage() {
|
|
46
|
+
const pkg = `@flowy-agent-store/runtime-${process.platform}-${process.arch}`;
|
|
47
|
+
try {
|
|
48
|
+
const pkgRoot = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`${pkg}/package.json`).slice(0, -12);
|
|
49
|
+
for (const name of BIN_NAMES) {
|
|
50
|
+
const candidate = (0, node_path.join)(pkgRoot, VENDOR_DIR, name);
|
|
51
|
+
if ((0, node_fs.existsSync)(candidate)) return candidate;
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function findOnPath(name) {
|
|
59
|
+
const pathEnv = process.env["PATH"] ?? process.env["Path"] ?? "";
|
|
60
|
+
for (const dir of pathEnv.split(node_path.delimiter)) {
|
|
61
|
+
if (!dir) continue;
|
|
62
|
+
const full = (0, node_path.join)(dir, name);
|
|
63
|
+
try {
|
|
64
|
+
if ((0, node_fs.existsSync)(full)) return full;
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/export.ts
|
|
70
|
+
/**
|
|
71
|
+
* Export an expert (single agent) or an expert **team** together with its
|
|
72
|
+
* skills and its package-level assets, written out as a directory (docs `35`
|
|
73
|
+
* and `38`).
|
|
74
|
+
*
|
|
75
|
+
* The wire face (`agent/export` / `team/export`, docs/agent-store/32) carries
|
|
76
|
+
* skills **by reference** — `{name, id}`, never bytes — and, since
|
|
77
|
+
* `pack_format 2`, the plugin-level **assets** by reference too (`{path, kind,
|
|
78
|
+
* size, digest}`, doc 38). "Export the team" therefore means: walk the pack,
|
|
79
|
+
* fetch every referenced skill through `skill/files` + `skill/file`, fetch
|
|
80
|
+
* every asset through `import/assets` + `import/asset`, and write the whole
|
|
81
|
+
* thing to a directory the *caller* owns. Doc `32` §6.5 originally shipped
|
|
82
|
+
* this as an 11-line recipe in prose; doc `35` promoted it into these
|
|
83
|
+
* functions (and records why the earlier decision against an SDK helper was
|
|
84
|
+
* reversed); doc `38` extends the layout with the package's own files.
|
|
85
|
+
*
|
|
86
|
+
* Everything here runs in the caller's process. The host never writes a byte:
|
|
87
|
+
* there is no server-side directory-writing endpoint by design (doc `32` §2
|
|
88
|
+
* item 7), which is why this file is allowed to import `node:fs` — the same
|
|
89
|
+
* privilege `spawn.ts` already has — while `@flowy-agent-store/client` must
|
|
90
|
+
* never gain it.
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Reject a caller-supplied relative path lexically, before touching the disk.
|
|
94
|
+
*
|
|
95
|
+
* The server already rejects traversal on both the skill and the asset faces
|
|
96
|
+
* (docs/agent-store/24 §4.4, 38 §2.3); this second gate keeps a hostile
|
|
97
|
+
* *inventory* from turning `join(dir, path)` into a write outside the target
|
|
98
|
+
* directory. Same rules as the server's lexical pass: no `..`, no absolute
|
|
99
|
+
* paths, no backslashes, no drive/UNC prefixes.
|
|
100
|
+
*/
|
|
101
|
+
function rejectUnsafeRelative(path) {
|
|
102
|
+
if (path.length === 0 || path.includes("\\") || path.startsWith("/") || path.includes(":")) throw new Error(`unsafe asset path from the host: ${path}`);
|
|
103
|
+
if (path.split("/").some((segment) => segment === "..")) throw new Error(`unsafe asset path from the host: ${path}`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Write an in-hand pack to `dir` as a real directory (docs `35` §5 and `38`
|
|
107
|
+
* §3.5). "Materialize" here means exactly that: the in-memory pack object plus
|
|
108
|
+
* the skill and asset bytes it references become files on disk; the pack data
|
|
109
|
+
* itself is unchanged.
|
|
110
|
+
*
|
|
111
|
+
* ```text
|
|
112
|
+
* <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
|
|
113
|
+
* <dir>/persona.md # kind === "agent" only: pack.persona.instructions
|
|
114
|
+
* <dir>/members/<id>/persona.md # kind === "team" only, one per member
|
|
115
|
+
* <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
|
|
116
|
+
* <dir>/avatars/… # pack.assets, kind === "avatar", at their snapshot paths
|
|
117
|
+
* <dir>/bin/… · scripts/… # pack.assets, kind === "script" (doc 38 D5)
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* Error semantics (doc `35` §6): the pack is fetched **before** anything is
|
|
121
|
+
* written, so a failed wire export leaves no directory behind; a `skill/files`
|
|
122
|
+
* rejection lands in {@link ExportResult.danglingSkills} and the loop
|
|
123
|
+
* continues; a `skill/file` or `import/asset` rejection after a successful
|
|
124
|
+
* listing **throws** — that is a host I/O failure, and swallowing it would
|
|
125
|
+
* produce a directory that lists files it does not contain. No rollback on
|
|
126
|
+
* mid-write failure.
|
|
127
|
+
*/
|
|
128
|
+
async function materializePack(deps, pack, dir) {
|
|
129
|
+
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
130
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "expert-pack.json"), JSON.stringify(pack, null, 2));
|
|
131
|
+
if (pack.kind === "agent") await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "persona.md"), pack.persona.instructions);
|
|
132
|
+
for (const member of pack.team?.members ?? []) {
|
|
133
|
+
const target = (0, node_path.join)(dir, "members", member.id, "persona.md");
|
|
134
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(target), { recursive: true });
|
|
135
|
+
await (0, node_fs_promises.writeFile)(target, member.persona.instructions);
|
|
136
|
+
}
|
|
137
|
+
const refs = [...pack.skills, ...(pack.team?.members ?? []).flatMap((m) => m.skills)];
|
|
138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
139
|
+
const writtenSkills = [];
|
|
140
|
+
const danglingSkills = [];
|
|
141
|
+
for (const ref of refs) {
|
|
142
|
+
if (seen.has(ref.id)) continue;
|
|
143
|
+
seen.add(ref.id);
|
|
144
|
+
let files;
|
|
145
|
+
try {
|
|
146
|
+
files = (await deps.skills.files(ref.id)).files;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
danglingSkills.push({
|
|
149
|
+
id: ref.id,
|
|
150
|
+
error: String(error)
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const target = (0, node_path.join)(dir, "skills", ref.name, file.path);
|
|
156
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(target), { recursive: true });
|
|
157
|
+
await (0, node_fs_promises.writeFile)(target, await deps.skills.readFile(ref.id, file.path));
|
|
158
|
+
}
|
|
159
|
+
writtenSkills.push(ref.name);
|
|
160
|
+
}
|
|
161
|
+
const writtenAssets = [];
|
|
162
|
+
for (const asset of pack.assets) {
|
|
163
|
+
rejectUnsafeRelative(asset.path);
|
|
164
|
+
const target = (0, node_path.join)(dir, asset.path);
|
|
165
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(target), { recursive: true });
|
|
166
|
+
await (0, node_fs_promises.writeFile)(target, await deps.readImportAsset(pack.provenance.snapshot_id, asset.path));
|
|
167
|
+
writtenAssets.push(asset.path);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
pack,
|
|
171
|
+
dir,
|
|
172
|
+
writtenSkills,
|
|
173
|
+
writtenAssets,
|
|
174
|
+
danglingSkills
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Export one single-agent expert and write it out as a directory.
|
|
179
|
+
*
|
|
180
|
+
* Same preconditions as the wire method: the expert must be installed and
|
|
181
|
+
* enabled, or the host answers `agent_not_installed` / `agent_disabled` /
|
|
182
|
+
* `preset_disabled`; a host whose `[expert_export]` table refuses the id
|
|
183
|
+
* answers `policy_denied` (the capability bit only reports the seam). Check
|
|
184
|
+
* `capabilities.expert_export` for the seam, not for this id.
|
|
185
|
+
*/
|
|
186
|
+
async function exportAgent(deps, agentId, dir) {
|
|
187
|
+
return materializePack(deps, await deps.agents.export(agentId), dir);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Export one expert team and write it out as a directory, leader first.
|
|
191
|
+
*
|
|
192
|
+
* All-or-nothing stays on the server: if any member is uninstalled, disabled,
|
|
193
|
+
* or refused by policy, the whole call fails and nothing is written. A pack
|
|
194
|
+
* over 1 MiB fails with `response_too_large` rather than being truncated.
|
|
195
|
+
* `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
|
|
196
|
+
*/
|
|
197
|
+
async function exportTeam(deps, teamId, dir, teamVersion) {
|
|
198
|
+
return materializePack(deps, await deps.teams.export(teamId, teamVersion), dir);
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/readiness.ts
|
|
202
|
+
/** Parse one stdout line; `null` when it is not the readiness line. */
|
|
203
|
+
function parseReadinessLine(line) {
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(line);
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
211
|
+
const record = parsed;
|
|
212
|
+
if (record["agent_store"] !== "listening") return null;
|
|
213
|
+
const { host, port, url, protocol_version, version, auth } = record;
|
|
214
|
+
if (typeof host !== "string" || typeof port !== "number" || typeof url !== "string" || typeof protocol_version !== "string" || typeof version !== "string" || typeof auth !== "string") return null;
|
|
215
|
+
return {
|
|
216
|
+
host,
|
|
217
|
+
port,
|
|
218
|
+
url,
|
|
219
|
+
protocol_version,
|
|
220
|
+
version,
|
|
221
|
+
auth
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/spawn.ts
|
|
226
|
+
/**
|
|
227
|
+
* Spawn the `agent-store` runtime as a child process and wait for its
|
|
228
|
+
* readiness line (docs/agent-store/12 P0-1 / P1-3).
|
|
229
|
+
*
|
|
230
|
+
* The child is always loopback-only (`--host 127.0.0.1 --no-open`) with its
|
|
231
|
+
* own data directory (SDK独占, P0-3): a fresh temp dir by default, or an
|
|
232
|
+
* explicit one the caller owns. A server-side single-instance lock fails the
|
|
233
|
+
* spawn fast when the directory is already held — that error propagates.
|
|
234
|
+
*/
|
|
235
|
+
const DEFAULT_READY_TIMEOUT_MS = 12e4;
|
|
236
|
+
const STDERR_TAIL_LINES = 50;
|
|
237
|
+
const KILL_GRACE_MS = 2e3;
|
|
238
|
+
/** Fail when the runtime speaks a different wire protocol (§7 acceptance). */
|
|
239
|
+
function assertProtocolCompatible(runtimeVersion) {
|
|
240
|
+
if (runtimeVersion !== _flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION) throw new Error(`protocol version mismatch: runtime speaks ${runtimeVersion}, SDK expects ${_flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION}`);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The host-policy environment the typed options above expand to.
|
|
244
|
+
*
|
|
245
|
+
* The spawned host cannot be handed backend CLI flags: `apps/agent-store` parses
|
|
246
|
+
* its own argv (`--host` / `--port` / `--data-dir` / `--no-open`) and has no
|
|
247
|
+
* `--agent-store-config`, so a flag would make clap exit before the readiness
|
|
248
|
+
* line. The parent environment is therefore the only channel a launcher has —
|
|
249
|
+
* the same conclusion `NOMI_LOG_LEVEL` already documents for the log filter.
|
|
250
|
+
*/
|
|
251
|
+
function hostPolicyEnv(options) {
|
|
252
|
+
const env = {};
|
|
253
|
+
if (options.marketDownload !== void 0) env["AGENT_STORE_MARKET_DOWNLOAD"] = options.marketDownload;
|
|
254
|
+
if (options.configPath !== void 0) env["AGENT_STORE_CONFIG"] = options.configPath;
|
|
255
|
+
return env;
|
|
256
|
+
}
|
|
257
|
+
async function spawnAppServer(options = {}) {
|
|
258
|
+
const bin = resolveAppServerBin(options.bin);
|
|
259
|
+
const ownedDir = options.dataDir === void 0;
|
|
260
|
+
const dataDir = ownedDir ? await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "agent-store-sdk-")) : options.dataDir;
|
|
261
|
+
const port = options.port ?? 0;
|
|
262
|
+
const policyEnv = {
|
|
263
|
+
...hostPolicyEnv(options),
|
|
264
|
+
...options.env
|
|
265
|
+
};
|
|
266
|
+
const { child, exited } = spawnRuntimeChild(bin, [
|
|
267
|
+
"--host",
|
|
268
|
+
"127.0.0.1",
|
|
269
|
+
"--port",
|
|
270
|
+
String(port),
|
|
271
|
+
"--data-dir",
|
|
272
|
+
dataDir,
|
|
273
|
+
"--no-open",
|
|
274
|
+
...options.extraArgs ?? []
|
|
275
|
+
], {
|
|
276
|
+
...Object.keys(policyEnv).length > 0 ? { env: policyEnv } : {},
|
|
277
|
+
cwd: options.cwd,
|
|
278
|
+
onExit: options.onExit
|
|
279
|
+
});
|
|
280
|
+
const stderrTail = [];
|
|
281
|
+
child.stderr?.on("data", (chunk) => {
|
|
282
|
+
for (const line of String(chunk).split(/\r?\n/)) {
|
|
283
|
+
if (line.length === 0) continue;
|
|
284
|
+
stderrTail.push(line);
|
|
285
|
+
if (stderrTail.length > STDERR_TAIL_LINES) stderrTail.shift();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const stopChild = async () => {
|
|
289
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
290
|
+
child.kill();
|
|
291
|
+
if (!await new Promise((resolve) => {
|
|
292
|
+
const timer = setTimeout(() => resolve(false), KILL_GRACE_MS);
|
|
293
|
+
child.once("exit", () => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
resolve(true);
|
|
296
|
+
});
|
|
297
|
+
})) child.kill("SIGKILL");
|
|
298
|
+
};
|
|
299
|
+
const fail = async (message) => {
|
|
300
|
+
await stopChild();
|
|
301
|
+
if (ownedDir) await (0, node_fs_promises.rm)(dataDir, {
|
|
302
|
+
recursive: true,
|
|
303
|
+
force: true
|
|
304
|
+
}).catch(() => void 0);
|
|
305
|
+
const tail = stderrTail.length > 0 ? `\nstderr tail:\n${stderrTail.join("\n")}` : "";
|
|
306
|
+
throw new Error(`${message}${tail}`);
|
|
307
|
+
};
|
|
308
|
+
try {
|
|
309
|
+
const readiness = await waitForReadiness(child, options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS);
|
|
310
|
+
try {
|
|
311
|
+
assertProtocolCompatible(readiness.protocol_version);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
await fail(error.message);
|
|
314
|
+
}
|
|
315
|
+
let closed = false;
|
|
316
|
+
return {
|
|
317
|
+
readiness,
|
|
318
|
+
dataDir,
|
|
319
|
+
exited,
|
|
320
|
+
close: async () => {
|
|
321
|
+
if (closed) return;
|
|
322
|
+
closed = true;
|
|
323
|
+
await stopChild();
|
|
324
|
+
if (ownedDir) await (0, node_fs_promises.rm)(dataDir, {
|
|
325
|
+
recursive: true,
|
|
326
|
+
force: true
|
|
327
|
+
}).catch(() => void 0);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
await stopChild();
|
|
332
|
+
if (ownedDir) await (0, node_fs_promises.rm)(dataDir, {
|
|
333
|
+
recursive: true,
|
|
334
|
+
force: true
|
|
335
|
+
}).catch(() => void 0);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Spawn the runtime process and start tracking its exit.
|
|
341
|
+
*
|
|
342
|
+
* Internal (not part of the package entry): exported so the lifecycle and
|
|
343
|
+
* `env` / `cwd` passthrough contract can be exercised with a synthetic child,
|
|
344
|
+
* which is what the managed CLI args normally make impossible to inject.
|
|
345
|
+
*/
|
|
346
|
+
function spawnRuntimeChild(bin, args, options = {}) {
|
|
347
|
+
const child = (0, node_child_process.spawn)(bin, args, {
|
|
348
|
+
stdio: [
|
|
349
|
+
"ignore",
|
|
350
|
+
"pipe",
|
|
351
|
+
"pipe"
|
|
352
|
+
],
|
|
353
|
+
windowsHide: true,
|
|
354
|
+
...options.env ? { env: {
|
|
355
|
+
...process.env,
|
|
356
|
+
...options.env
|
|
357
|
+
} } : {},
|
|
358
|
+
...options.cwd ? { cwd: options.cwd } : {}
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
child,
|
|
362
|
+
exited: new Promise((resolve) => {
|
|
363
|
+
child.once("exit", (code, signal) => {
|
|
364
|
+
const info = {
|
|
365
|
+
code,
|
|
366
|
+
signal
|
|
367
|
+
};
|
|
368
|
+
resolve(info);
|
|
369
|
+
try {
|
|
370
|
+
options.onExit?.(info);
|
|
371
|
+
} catch {}
|
|
372
|
+
});
|
|
373
|
+
})
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/** Internal: exported for the stdout-backpressure regression test only. */
|
|
377
|
+
function waitForReadiness(child, timeoutMs) {
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
if (!child.stdout) {
|
|
380
|
+
reject(/* @__PURE__ */ new Error("spawned runtime has no stdout pipe"));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const stdout = child.stdout;
|
|
384
|
+
const lines = (0, node_readline.createInterface)({ input: stdout });
|
|
385
|
+
let settled = false;
|
|
386
|
+
const fail = (error) => {
|
|
387
|
+
if (settled) return;
|
|
388
|
+
settled = true;
|
|
389
|
+
lines.close();
|
|
390
|
+
reject(error);
|
|
391
|
+
};
|
|
392
|
+
const timer = setTimeout(() => {
|
|
393
|
+
fail(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for the runtime readiness line`));
|
|
394
|
+
}, timeoutMs);
|
|
395
|
+
lines.on("line", (line) => {
|
|
396
|
+
if (settled) return;
|
|
397
|
+
const readiness = parseReadinessLine(line);
|
|
398
|
+
if (!readiness) return;
|
|
399
|
+
settled = true;
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
lines.close();
|
|
402
|
+
stdout.resume();
|
|
403
|
+
resolve(readiness);
|
|
404
|
+
});
|
|
405
|
+
child.once("error", (error) => {
|
|
406
|
+
fail(/* @__PURE__ */ new Error(`failed to spawn the runtime: ${error.message}`));
|
|
407
|
+
});
|
|
408
|
+
child.once("exit", (code) => {
|
|
409
|
+
fail(/* @__PURE__ */ new Error(`runtime exited before reporting readiness (code ${code})`));
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/index.ts
|
|
415
|
+
/**
|
|
416
|
+
* `@flowy-agent-store/sdk` — spawn the runtime, connect over loopback, hand back
|
|
417
|
+
* a ready `AppServerClient`.
|
|
418
|
+
*
|
|
419
|
+
* The object `launchHarness` resolves to **is** that client (doc `31`): the
|
|
420
|
+
* `conversations` / `store` / `agents` surface sits on it directly, and the
|
|
421
|
+
* process it owns is reachable as `server` / `handshake` / `close()`. That is
|
|
422
|
+
* the shape both external references use — Codex's
|
|
423
|
+
* `with Codex(...) as codex: codex.thread_start(...)` and Kimi Code's
|
|
424
|
+
* `const harness = createKimiHarness(...); harness.createSession(...)` — the
|
|
425
|
+
* factory returns the main object, lifecycle included, with no `.client` hop.
|
|
426
|
+
*/
|
|
427
|
+
/**
|
|
428
|
+
* Spawn a loopback runtime with its own data dir and return a connected,
|
|
429
|
+
* initialized client. Rejects non-loopback contact outright (P0-4): this
|
|
430
|
+
* entry point only ever dials the process it just spawned.
|
|
431
|
+
*/
|
|
432
|
+
async function launchHarness(options) {
|
|
433
|
+
const server = await spawnAppServer(options);
|
|
434
|
+
const wsUrl = `ws://${server.readiness.host}:${server.readiness.port}/api/app-server/ws`;
|
|
435
|
+
if (!(0, _flowy_agent_store_client.isLoopbackUrl)(wsUrl)) {
|
|
436
|
+
await server.close();
|
|
437
|
+
throw new Error(`refusing to connect outside loopback: ${wsUrl}`);
|
|
438
|
+
}
|
|
439
|
+
const client = new _flowy_agent_store_client.AppServerClient({
|
|
440
|
+
transport: new _flowy_agent_store_client.WebSocketTransport(wsUrl, {
|
|
441
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
442
|
+
token: options.token
|
|
443
|
+
}),
|
|
444
|
+
client: options.client,
|
|
445
|
+
capabilities: options.capabilities
|
|
446
|
+
});
|
|
447
|
+
const closeTransport = client.close.bind(client);
|
|
448
|
+
try {
|
|
449
|
+
const handshake = await client.connect();
|
|
450
|
+
let closed = false;
|
|
451
|
+
return Object.assign(client, {
|
|
452
|
+
server,
|
|
453
|
+
handshake,
|
|
454
|
+
close: async () => {
|
|
455
|
+
if (closed) return;
|
|
456
|
+
closed = true;
|
|
457
|
+
closeTransport();
|
|
458
|
+
await server.close();
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
client.close();
|
|
463
|
+
await server.close();
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
exports.assertProtocolCompatible = assertProtocolCompatible;
|
|
469
|
+
exports.exportAgent = exportAgent;
|
|
470
|
+
exports.exportTeam = exportTeam;
|
|
471
|
+
exports.launchHarness = launchHarness;
|
|
472
|
+
exports.materializePack = materializePack;
|
|
473
|
+
exports.parseReadinessLine = parseReadinessLine;
|
|
474
|
+
exports.resolveAppServerBin = resolveAppServerBin;
|
|
475
|
+
exports.spawnAppServer = spawnAppServer;
|