@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/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;