@flowy-agent-store/sdk 0.1.0-beta.7 → 0.1.0-beta.8

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 CHANGED
@@ -15,7 +15,17 @@ const items = await harness.store.search("software");
15
15
  // 安装:导入 + 注册,并等到组件真的可用(默认 waitForReady: true)
16
16
  const outcome = await harness.store.install(items[0]);
17
17
  if (outcome.readyIssue === "authorization_required") {
18
- await harness.connectors.authStart(outcome.readyComponentId!);
18
+ const id = outcome.readyComponentId!;
19
+ // 失败有两条通道,别只看一条:浏览器**之前**的失败由 authStart 同步回
20
+ // state:"error";**之后**的失败(换 token 被拒、回调超时、被限流)只会出现在
21
+ // authStatus 的 error 里,state 一直停在 not_authenticated。
22
+ // waitForAuth 把第二条通道包成一次调用,并在结束时说清原因。
23
+ const started = await harness.connectors.authStart(id);
24
+ if (started.state === "error") throw new Error(started.error ?? "authorization failed to start");
25
+ const auth = await harness.connectors.waitForAuth(id);
26
+ if (auth.state !== "authenticated") {
27
+ throw new Error(auth.state === "error" ? auth.error : "timed out in the browser");
28
+ }
19
29
  }
20
30
 
21
31
  // 禁用 / 启用:连接器改 mcp_servers.enabled,专家/专家团改 Preset.enabled
@@ -29,14 +39,49 @@ if (!released.ok) console.warn(released.components.filter((c) => !c.ok));
29
39
  await harness.close();
30
40
  ```
31
41
 
42
+ ## 导出专家 / 专家团并写到目录(`exportAgent` / `exportTeam` / `materializePack`)
43
+
44
+ `agents.export` / `teams.export` 只给**定义**(`ExpertPack`),技能是**引用**(`{name, id}`,没有字节)。
45
+ 下面三个函数把「定义 + 技能字节」一次**写成一个目录**(`docs/agent-store/35`;它把 doc `32` §6.5 的
46
+ 11 行配方提升成了 API):
47
+
48
+ ```ts
49
+ import { exportTeam } from "@flowy-agent-store/sdk";
50
+
51
+ // 多 agent 的专家团:递归展开 + 整包失败(成员缺一即失败并点名),leader 在首位
52
+ const result = await exportTeam(harness, teamId, "./frontend-backend-experts");
53
+ // 单 agent 专家用 exportAgent(harness, agentId, dir)
54
+ // 已有 pack(比如缓存/网络传来的)用 materializePack(harness, pack, dir) 只写目录
55
+
56
+ result.pack; // ExpertPack —— 内存里也拿得到,不落盘也能用
57
+ result.writtenSkills; // 实际写入的技能名(去重后)
58
+ result.danglingSkills; // 声明了但本机取不到的技能:{ id, error },如实上报,不静默跳过
59
+ ```
60
+
61
+ 写出的布局:
62
+
63
+ ```text
64
+ <dir>/
65
+ expert-pack.json # 线上 pack 逐字节
66
+ persona.md # 仅 agent 形态(团没有自己的 persona)
67
+ members/<id>/persona.md # 仅 team 形态,每个成员一个
68
+ skills/<name>/… # 成员声明去重后的技能字节
69
+ ```
70
+
71
+ 错误语义:wire 导出失败(`agent_not_installed` / `agent_disabled` / `policy_denied` /
72
+ `version_mismatch` / `response_too_large`…)**先于任何写盘**,不留半成品目录;`skill/files`
73
+ 失败进 `danglingSkills` 并继续;`skill/file` 在列文件成功后失败则**抛出**——那是宿主 I/O 错误,
74
+ 吞掉会造出「列了文件却缺内容」的残目录。
75
+
32
76
  **`launchHarness` 解析出的对象就是那个 client**(`docs/agent-store/31` §5 方案 B):`harness.store` / `harness.conversations` / `harness.models` 直接可用,没有 `.client` 一跳;子进程是 `harness.server`(`readiness` / `dataDir` / `exited`),握手响应是 `harness.handshake`。
33
77
 
34
78
  五个动词都在 SDK 里可用,但**它们不是 `@flowy-agent-store/sdk` 自己实现的**:本包只负责进程与传输,返回的对象就是一个 `@flowy-agent-store/client` 的 `AppServerClient`(sdk 另挂 `server` / `handshake` / `close()`),其中 `store` 子客户端把 `install` / `uninstall` / `setEnabled` 编排成一条状态机(`search → install → … → uninstall`)。这样分层是刻意的——见 `docs/agent-store/12-sdk-packaging.md` §2 的职责划分。
35
79
 
36
80
  几条使用上的事实(都是实测结论,不是约定):
37
81
 
38
- - `store.install` 默认**等到可用**才 resolve。技能拷完即可用,连接器不然:它注册出来是 **disabled** 的(安装器的既定默认),所以就绪检查会先把它 enable,再跑一次探针——只轮询状态永远到不了 `connected`,因为状态派生自上一次探针。
82
+ - `store.install` 默认**等到可用**才 resolve。技能拷完即可用,连接器不然:它注册出来是 **disabled** 的(安装器的既定默认),所以就绪检查会先把它 enable,再跑一次探针——只轮询状态永远到不了 `connected`,因为状态派生自上一次探针。探针**有节奏**:首轮(以及之后每 `readyProbeMs`,默认 5s)真连一次,其余轮次只读状态。这是刻意的:一次探针会解析已存 token(临近过期就刷新,401 再刷新一次),按 400ms 的轮询间隔去探针,30 秒预算里能对连接器及其授权服务器打出约 10 次连接 / 最多约 20 次 token 请求,而没有任何人点过按钮。
39
83
  - 就绪超时**不会丢掉安装结果**:返回成功的安装 + `ready: false` / `readyIssue: "ready_timeout"`。需要 OAuth 的连接器立刻返回 `authorization_required`,不把超时预算烧光。
84
+ - `connectors.waitForAuth(id)` 是「等授权完成」的写法:它轮询 `authStatus`,一拿到 `error` 就立刻带原因返回(`{state:"error", error}`),成功返回 `{state:"authenticated"}`,预算用完返回 `{state:"timeout"}`;只有状态读本身失败才 reject。手写 `while (state !== "authenticated")` 循环的典型结局是「它就是没变成 authenticated」——而原因(换 token 被拒、回调超时、`slow_down` 限流)早就躺在它每次读到的 `error` 里。
40
85
  - 没有「更新」动词。`store.checkUpdates()` 只报告哪些条目有新版,`store.updateHint(item)` 告诉你该怎么做——当前是 `"uninstall_reinstall"`:卸载后重装,安装路径会按版本漂移重新导入,取到市场当前版本。
41
86
  - 卸载是**可重入**的:产物已不在算成功。部分失败时那些组件保持已安装、`ok: false` 且点名到组件,重试有意义。
42
87
  - `store.setEnabled(item, false)` 对技能只翻一个目录层标记,返回 `code: "skill_disable_flag_only"`——技能语料没有启用状态,要让技能离开运行时只能 `uninstall`。这个 code 会原样透出,不会被吞。
@@ -64,4 +109,8 @@ await harness.close();
64
109
  - 二进制定位:`bin` 参数 → `AGENT_STORE_BIN` → 平台 runtime 包的 `vendor/` → `PATH`;找不到直接报错(**不下载**,见 P2)。
65
110
  - 就绪行 `protocol_version` 与 SDK 不一致时杀掉子进程并报错(含两端版本)。该值是**契约指纹**,不是版本号:任何 wire 变更都会 bump。
66
111
 
67
- 不在本包范围内(`07-typescript-sdk.md` §非目标):`config/*` 与 `skill/*` 写面。它们的**类型**在 `@flowy-agent-store/protocol`(wire 上存在的方法就有形状),但**没有** typed 方法——因为这些面跟着宿主自己的设置文件与技能目录走,是最易变的部分。这是可发现性边界,不是权限边界:`harness.transport` 是公开的,服务端按磁盘归属判定可写性。要用就直连 `transport.request`,并自担变动。
112
+ 不在本包范围内(`07-typescript-sdk.md` §非目标):`config/*` 与 `skill/*` **写**面。它们的**类型**在
113
+ `@flowy-agent-store/protocol`(wire 上存在的方法就有形状),但**没有** typed 方法——因为这些面跟着
114
+ 宿主自己的设置文件与技能目录走,是最易变的部分。**技能的读面是例外**:`skill/files` / `skill/file`
115
+ 有 typed 方法(`harness.skills.*`),且被上面的导出函数消费。这是可发现性边界,不是权限边界:
116
+ `harness.transport` 是公开的,服务端按磁盘归属判定可写性。要用就直连 `transport.request`,并自担变动。
package/dist/index.cjs CHANGED
@@ -3,8 +3,8 @@ let _flowy_agent_store_client = require("@flowy-agent-store/client");
3
3
  let node_fs = require("node:fs");
4
4
  let node_module = require("node:module");
5
5
  let node_path = require("node:path");
6
- let node_child_process = require("node:child_process");
7
6
  let node_fs_promises = require("node:fs/promises");
7
+ let node_child_process = require("node:child_process");
8
8
  let node_os = require("node:os");
9
9
  let node_readline = require("node:readline");
10
10
  let _flowy_agent_store_protocol = require("@flowy-agent-store/protocol");
@@ -66,6 +66,109 @@ function findOnPath(name) {
66
66
  }
67
67
  }
68
68
  //#endregion
69
+ //#region src/export.ts
70
+ /**
71
+ * Export an expert (single agent) or an expert **team** together with its
72
+ * skills, written out as a directory (doc `35`).
73
+ *
74
+ * The wire face (`agent/export` / `team/export`, docs/agent-store/32) carries
75
+ * skills **by reference** — `{name, id}`, never bytes — so "export the team's
76
+ * skills" means: walk the pack, fetch every referenced skill through
77
+ * `skill/files` + `skill/file`, and write the whole thing to a directory the
78
+ * *caller* owns. Doc `32` §6.5 originally shipped this as an 11-line recipe in
79
+ * prose; doc `35` promotes it into these three functions (and records why the
80
+ * earlier decision against an SDK helper was reversed).
81
+ *
82
+ * Everything here runs in the caller's process. The host never writes a byte:
83
+ * there is no server-side directory-writing endpoint by design (doc `32` §2
84
+ * item 7), which is why this file is allowed to import `node:fs` — the same
85
+ * privilege `spawn.ts` already has — while `@flowy-agent-store/client` must
86
+ * never gain it.
87
+ */
88
+ /**
89
+ * Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
90
+ * "Materialize" here means exactly that: the in-memory pack object plus the
91
+ * skill bytes it references become files on disk; the pack data itself is
92
+ * unchanged.
93
+ *
94
+ * ```text
95
+ * <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
96
+ * <dir>/persona.md # kind === "agent" only: pack.persona.instructions
97
+ * <dir>/members/<id>/persona.md # kind === "team" only, one per member
98
+ * <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
99
+ * ```
100
+ *
101
+ * Error semantics (doc `35` §6): the pack is fetched **before** anything is
102
+ * written, so a failed wire export leaves no directory behind; a `skill/files`
103
+ * rejection lands in {@link ExportResult.danglingSkills} and the loop
104
+ * continues; a `skill/file` rejection after a successful listing **throws** —
105
+ * that is a host I/O failure, and swallowing it would produce a directory that
106
+ * lists files it does not contain. No rollback on mid-write failure.
107
+ */
108
+ async function materializePack(deps, pack, dir) {
109
+ await (0, node_fs_promises.mkdir)(dir, { recursive: true });
110
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "expert-pack.json"), JSON.stringify(pack, null, 2));
111
+ if (pack.kind === "agent") await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "persona.md"), pack.persona.instructions);
112
+ for (const member of pack.team?.members ?? []) {
113
+ const target = (0, node_path.join)(dir, "members", member.id, "persona.md");
114
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(target), { recursive: true });
115
+ await (0, node_fs_promises.writeFile)(target, member.persona.instructions);
116
+ }
117
+ const refs = [...pack.skills, ...(pack.team?.members ?? []).flatMap((m) => m.skills)];
118
+ const seen = /* @__PURE__ */ new Set();
119
+ const writtenSkills = [];
120
+ const danglingSkills = [];
121
+ for (const ref of refs) {
122
+ if (seen.has(ref.id)) continue;
123
+ seen.add(ref.id);
124
+ let files;
125
+ try {
126
+ files = (await deps.skills.files(ref.id)).files;
127
+ } catch (error) {
128
+ danglingSkills.push({
129
+ id: ref.id,
130
+ error: String(error)
131
+ });
132
+ continue;
133
+ }
134
+ for (const file of files) {
135
+ const target = (0, node_path.join)(dir, "skills", ref.name, file.path);
136
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(target), { recursive: true });
137
+ await (0, node_fs_promises.writeFile)(target, await deps.skills.readFile(ref.id, file.path));
138
+ }
139
+ writtenSkills.push(ref.name);
140
+ }
141
+ return {
142
+ pack,
143
+ dir,
144
+ writtenSkills,
145
+ danglingSkills
146
+ };
147
+ }
148
+ /**
149
+ * Export one single-agent expert and write it out as a directory.
150
+ *
151
+ * Same preconditions as the wire method: the expert must be installed and
152
+ * enabled, or the host answers `agent_not_installed` / `agent_disabled` /
153
+ * `preset_disabled`; a host whose `[expert_export]` table refuses the id
154
+ * answers `policy_denied` (the capability bit only reports the seam). Check
155
+ * `capabilities.expert_export` for the seam, not for this id.
156
+ */
157
+ async function exportAgent(deps, agentId, dir) {
158
+ return materializePack(deps, await deps.agents.export(agentId), dir);
159
+ }
160
+ /**
161
+ * Export one expert team and write it out as a directory, leader first.
162
+ *
163
+ * All-or-nothing stays on the server: if any member is uninstalled, disabled,
164
+ * or refused by policy, the whole call fails and nothing is written. A pack
165
+ * over 1 MiB fails with `response_too_large` rather than being truncated.
166
+ * `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
167
+ */
168
+ async function exportTeam(deps, teamId, dir, teamVersion) {
169
+ return materializePack(deps, await deps.teams.export(teamId, teamVersion), dir);
170
+ }
171
+ //#endregion
69
172
  //#region src/readiness.ts
70
173
  /** Parse one stdout line; `null` when it is not the readiness line. */
71
174
  function parseReadinessLine(line) {
@@ -311,7 +414,10 @@ async function launchHarness(options) {
311
414
  }
312
415
  //#endregion
313
416
  exports.assertProtocolCompatible = assertProtocolCompatible;
417
+ exports.exportAgent = exportAgent;
418
+ exports.exportTeam = exportTeam;
314
419
  exports.launchHarness = launchHarness;
420
+ exports.materializePack = materializePack;
315
421
  exports.parseReadinessLine = parseReadinessLine;
316
422
  exports.resolveAppServerBin = resolveAppServerBin;
317
423
  exports.spawnAppServer = spawnAppServer;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,90 @@
1
1
  import { AppServerClient } from "@flowy-agent-store/client";
2
- import { ClientCapabilities, ClientInfo, InitializeResult } from "@flowy-agent-store/protocol";
2
+ import { ClientCapabilities, ClientInfo, ExpertPack, InitializeResult } from "@flowy-agent-store/protocol";
3
3
  //#region src/bin.d.ts
4
4
  export declare function resolveAppServerBin(explicit?: string): string;
5
5
  //#endregion
6
+ //#region src/export.d.ts
7
+ /**
8
+ * The three wire faces this module needs, narrowed from {@link Harness} on
9
+ * purpose: unit tests hand a fake, and the dependency surface stays explicit
10
+ * instead of quietly reaching `store` / `conversations`. A real harness
11
+ * satisfies this structurally (`Harness` extends `AppServerClient`, which owns
12
+ * `agents` / `teams` / `skills`).
13
+ */
14
+ interface ExportDeps {
15
+ agents: {
16
+ export(agentId: string): Promise<ExpertPack>;
17
+ };
18
+ teams: {
19
+ export(teamId: string, teamVersion?: string): Promise<ExpertPack>;
20
+ };
21
+ skills: {
22
+ files(skillId: string): Promise<{
23
+ files: {
24
+ path: string;
25
+ }[];
26
+ }>;
27
+ readFile(skillId: string, path: string): Promise<Uint8Array>;
28
+ };
29
+ }
30
+ interface ExportResult {
31
+ /** The pack is also returned in memory: a consumer that never touches the disk needs no second call. */
32
+ pack: ExpertPack;
33
+ dir: string;
34
+ /** Skill names actually written (deduplicated). */
35
+ writtenSkills: string[];
36
+ /**
37
+ * Skills the pack **declares** but this host cannot resolve (`skill/files`
38
+ * rejected them). Reported honestly, never skipped in silence: the pack
39
+ * states declarations, resolvability is a host fact the consumer has to
40
+ * judge (doc `32` §5 R8, live criterion EX-008).
41
+ */
42
+ danglingSkills: {
43
+ id: string;
44
+ error: string;
45
+ }[];
46
+ }
47
+ /**
48
+ * Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
49
+ * "Materialize" here means exactly that: the in-memory pack object plus the
50
+ * skill bytes it references become files on disk; the pack data itself is
51
+ * unchanged.
52
+ *
53
+ * ```text
54
+ * <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
55
+ * <dir>/persona.md # kind === "agent" only: pack.persona.instructions
56
+ * <dir>/members/<id>/persona.md # kind === "team" only, one per member
57
+ * <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
58
+ * ```
59
+ *
60
+ * Error semantics (doc `35` §6): the pack is fetched **before** anything is
61
+ * written, so a failed wire export leaves no directory behind; a `skill/files`
62
+ * rejection lands in {@link ExportResult.danglingSkills} and the loop
63
+ * continues; a `skill/file` rejection after a successful listing **throws** —
64
+ * that is a host I/O failure, and swallowing it would produce a directory that
65
+ * lists files it does not contain. No rollback on mid-write failure.
66
+ */
67
+ export declare function materializePack(deps: ExportDeps, pack: ExpertPack, dir: string): Promise<ExportResult>;
68
+ /**
69
+ * Export one single-agent expert and write it out as a directory.
70
+ *
71
+ * Same preconditions as the wire method: the expert must be installed and
72
+ * enabled, or the host answers `agent_not_installed` / `agent_disabled` /
73
+ * `preset_disabled`; a host whose `[expert_export]` table refuses the id
74
+ * answers `policy_denied` (the capability bit only reports the seam). Check
75
+ * `capabilities.expert_export` for the seam, not for this id.
76
+ */
77
+ export declare function exportAgent(deps: ExportDeps, agentId: string, dir: string): Promise<ExportResult>;
78
+ /**
79
+ * Export one expert team and write it out as a directory, leader first.
80
+ *
81
+ * All-or-nothing stays on the server: if any member is uninstalled, disabled,
82
+ * or refused by policy, the whole call fails and nothing is written. A pack
83
+ * over 1 MiB fails with `response_too_large` rather than being truncated.
84
+ * `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
85
+ */
86
+ export declare function exportTeam(deps: ExportDeps, teamId: string, dir: string, teamVersion?: string): Promise<ExportResult>;
87
+ //#endregion
6
88
  //#region src/readiness.d.ts
7
89
  /**
8
90
  * Readiness protocol (docs/agent-store/12 P0-1).
@@ -114,4 +196,4 @@ export interface Harness extends AppServerClient {
114
196
  */
115
197
  export declare function launchHarness(options: HarnessOptions): Promise<Harness>;
116
198
  //#endregion
117
- export type { ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
199
+ export type { ExportDeps, ExportResult, ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,90 @@
1
1
  import { AppServerClient } from "@flowy-agent-store/client";
2
- import { ClientCapabilities, ClientInfo, InitializeResult } from "@flowy-agent-store/protocol";
2
+ import { ClientCapabilities, ClientInfo, ExpertPack, InitializeResult } from "@flowy-agent-store/protocol";
3
3
  //#region src/bin.d.ts
4
4
  export declare function resolveAppServerBin(explicit?: string): string;
5
5
  //#endregion
6
+ //#region src/export.d.ts
7
+ /**
8
+ * The three wire faces this module needs, narrowed from {@link Harness} on
9
+ * purpose: unit tests hand a fake, and the dependency surface stays explicit
10
+ * instead of quietly reaching `store` / `conversations`. A real harness
11
+ * satisfies this structurally (`Harness` extends `AppServerClient`, which owns
12
+ * `agents` / `teams` / `skills`).
13
+ */
14
+ interface ExportDeps {
15
+ agents: {
16
+ export(agentId: string): Promise<ExpertPack>;
17
+ };
18
+ teams: {
19
+ export(teamId: string, teamVersion?: string): Promise<ExpertPack>;
20
+ };
21
+ skills: {
22
+ files(skillId: string): Promise<{
23
+ files: {
24
+ path: string;
25
+ }[];
26
+ }>;
27
+ readFile(skillId: string, path: string): Promise<Uint8Array>;
28
+ };
29
+ }
30
+ interface ExportResult {
31
+ /** The pack is also returned in memory: a consumer that never touches the disk needs no second call. */
32
+ pack: ExpertPack;
33
+ dir: string;
34
+ /** Skill names actually written (deduplicated). */
35
+ writtenSkills: string[];
36
+ /**
37
+ * Skills the pack **declares** but this host cannot resolve (`skill/files`
38
+ * rejected them). Reported honestly, never skipped in silence: the pack
39
+ * states declarations, resolvability is a host fact the consumer has to
40
+ * judge (doc `32` §5 R8, live criterion EX-008).
41
+ */
42
+ danglingSkills: {
43
+ id: string;
44
+ error: string;
45
+ }[];
46
+ }
47
+ /**
48
+ * Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
49
+ * "Materialize" here means exactly that: the in-memory pack object plus the
50
+ * skill bytes it references become files on disk; the pack data itself is
51
+ * unchanged.
52
+ *
53
+ * ```text
54
+ * <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
55
+ * <dir>/persona.md # kind === "agent" only: pack.persona.instructions
56
+ * <dir>/members/<id>/persona.md # kind === "team" only, one per member
57
+ * <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
58
+ * ```
59
+ *
60
+ * Error semantics (doc `35` §6): the pack is fetched **before** anything is
61
+ * written, so a failed wire export leaves no directory behind; a `skill/files`
62
+ * rejection lands in {@link ExportResult.danglingSkills} and the loop
63
+ * continues; a `skill/file` rejection after a successful listing **throws** —
64
+ * that is a host I/O failure, and swallowing it would produce a directory that
65
+ * lists files it does not contain. No rollback on mid-write failure.
66
+ */
67
+ export declare function materializePack(deps: ExportDeps, pack: ExpertPack, dir: string): Promise<ExportResult>;
68
+ /**
69
+ * Export one single-agent expert and write it out as a directory.
70
+ *
71
+ * Same preconditions as the wire method: the expert must be installed and
72
+ * enabled, or the host answers `agent_not_installed` / `agent_disabled` /
73
+ * `preset_disabled`; a host whose `[expert_export]` table refuses the id
74
+ * answers `policy_denied` (the capability bit only reports the seam). Check
75
+ * `capabilities.expert_export` for the seam, not for this id.
76
+ */
77
+ export declare function exportAgent(deps: ExportDeps, agentId: string, dir: string): Promise<ExportResult>;
78
+ /**
79
+ * Export one expert team and write it out as a directory, leader first.
80
+ *
81
+ * All-or-nothing stays on the server: if any member is uninstalled, disabled,
82
+ * or refused by policy, the whole call fails and nothing is written. A pack
83
+ * over 1 MiB fails with `response_too_large` rather than being truncated.
84
+ * `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
85
+ */
86
+ export declare function exportTeam(deps: ExportDeps, teamId: string, dir: string, teamVersion?: string): Promise<ExportResult>;
87
+ //#endregion
6
88
  //#region src/readiness.d.ts
7
89
  /**
8
90
  * Readiness protocol (docs/agent-store/12 P0-1).
@@ -114,4 +196,4 @@ export interface Harness extends AppServerClient {
114
196
  */
115
197
  export declare function launchHarness(options: HarnessOptions): Promise<Harness>;
116
198
  //#endregion
117
- export type { ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
199
+ export type { ExportDeps, ExportResult, ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { AppServerClient, WebSocketTransport, isLoopbackUrl } from "@flowy-agent-store/client";
3
3
  import { existsSync } from "node:fs";
4
- import { delimiter, join } from "node:path";
4
+ import { delimiter, dirname, join } from "node:path";
5
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
5
6
  import { spawn } from "node:child_process";
6
- import { mkdtemp, rm } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { createInterface } from "node:readline";
9
9
  import { APP_SERVER_PROTOCOL_VERSION } from "@flowy-agent-store/protocol";
@@ -65,6 +65,109 @@ function findOnPath(name) {
65
65
  }
66
66
  }
67
67
  //#endregion
68
+ //#region src/export.ts
69
+ /**
70
+ * Export an expert (single agent) or an expert **team** together with its
71
+ * skills, written out as a directory (doc `35`).
72
+ *
73
+ * The wire face (`agent/export` / `team/export`, docs/agent-store/32) carries
74
+ * skills **by reference** — `{name, id}`, never bytes — so "export the team's
75
+ * skills" means: walk the pack, fetch every referenced skill through
76
+ * `skill/files` + `skill/file`, and write the whole thing to a directory the
77
+ * *caller* owns. Doc `32` §6.5 originally shipped this as an 11-line recipe in
78
+ * prose; doc `35` promotes it into these three functions (and records why the
79
+ * earlier decision against an SDK helper was reversed).
80
+ *
81
+ * Everything here runs in the caller's process. The host never writes a byte:
82
+ * there is no server-side directory-writing endpoint by design (doc `32` §2
83
+ * item 7), which is why this file is allowed to import `node:fs` — the same
84
+ * privilege `spawn.ts` already has — while `@flowy-agent-store/client` must
85
+ * never gain it.
86
+ */
87
+ /**
88
+ * Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
89
+ * "Materialize" here means exactly that: the in-memory pack object plus the
90
+ * skill bytes it references become files on disk; the pack data itself is
91
+ * unchanged.
92
+ *
93
+ * ```text
94
+ * <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
95
+ * <dir>/persona.md # kind === "agent" only: pack.persona.instructions
96
+ * <dir>/members/<id>/persona.md # kind === "team" only, one per member
97
+ * <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
98
+ * ```
99
+ *
100
+ * Error semantics (doc `35` §6): the pack is fetched **before** anything is
101
+ * written, so a failed wire export leaves no directory behind; a `skill/files`
102
+ * rejection lands in {@link ExportResult.danglingSkills} and the loop
103
+ * continues; a `skill/file` rejection after a successful listing **throws** —
104
+ * that is a host I/O failure, and swallowing it would produce a directory that
105
+ * lists files it does not contain. No rollback on mid-write failure.
106
+ */
107
+ async function materializePack(deps, pack, dir) {
108
+ await mkdir(dir, { recursive: true });
109
+ await writeFile(join(dir, "expert-pack.json"), JSON.stringify(pack, null, 2));
110
+ if (pack.kind === "agent") await writeFile(join(dir, "persona.md"), pack.persona.instructions);
111
+ for (const member of pack.team?.members ?? []) {
112
+ const target = join(dir, "members", member.id, "persona.md");
113
+ await mkdir(dirname(target), { recursive: true });
114
+ await writeFile(target, member.persona.instructions);
115
+ }
116
+ const refs = [...pack.skills, ...(pack.team?.members ?? []).flatMap((m) => m.skills)];
117
+ const seen = /* @__PURE__ */ new Set();
118
+ const writtenSkills = [];
119
+ const danglingSkills = [];
120
+ for (const ref of refs) {
121
+ if (seen.has(ref.id)) continue;
122
+ seen.add(ref.id);
123
+ let files;
124
+ try {
125
+ files = (await deps.skills.files(ref.id)).files;
126
+ } catch (error) {
127
+ danglingSkills.push({
128
+ id: ref.id,
129
+ error: String(error)
130
+ });
131
+ continue;
132
+ }
133
+ for (const file of files) {
134
+ const target = join(dir, "skills", ref.name, file.path);
135
+ await mkdir(dirname(target), { recursive: true });
136
+ await writeFile(target, await deps.skills.readFile(ref.id, file.path));
137
+ }
138
+ writtenSkills.push(ref.name);
139
+ }
140
+ return {
141
+ pack,
142
+ dir,
143
+ writtenSkills,
144
+ danglingSkills
145
+ };
146
+ }
147
+ /**
148
+ * Export one single-agent expert and write it out as a directory.
149
+ *
150
+ * Same preconditions as the wire method: the expert must be installed and
151
+ * enabled, or the host answers `agent_not_installed` / `agent_disabled` /
152
+ * `preset_disabled`; a host whose `[expert_export]` table refuses the id
153
+ * answers `policy_denied` (the capability bit only reports the seam). Check
154
+ * `capabilities.expert_export` for the seam, not for this id.
155
+ */
156
+ async function exportAgent(deps, agentId, dir) {
157
+ return materializePack(deps, await deps.agents.export(agentId), dir);
158
+ }
159
+ /**
160
+ * Export one expert team and write it out as a directory, leader first.
161
+ *
162
+ * All-or-nothing stays on the server: if any member is uninstalled, disabled,
163
+ * or refused by policy, the whole call fails and nothing is written. A pack
164
+ * over 1 MiB fails with `response_too_large` rather than being truncated.
165
+ * `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
166
+ */
167
+ async function exportTeam(deps, teamId, dir, teamVersion) {
168
+ return materializePack(deps, await deps.teams.export(teamId, teamVersion), dir);
169
+ }
170
+ //#endregion
68
171
  //#region src/readiness.ts
69
172
  /** Parse one stdout line; `null` when it is not the readiness line. */
70
173
  function parseReadinessLine(line) {
@@ -309,4 +412,4 @@ async function launchHarness(options) {
309
412
  }
310
413
  }
311
414
  //#endregion
312
- export { assertProtocolCompatible, launchHarness, parseReadinessLine, resolveAppServerBin, spawnAppServer };
415
+ export { assertProtocolCompatible, exportAgent, exportTeam, launchHarness, materializePack, parseReadinessLine, resolveAppServerBin, spawnAppServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowy-agent-store/sdk",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "Node.js host for the Agent Store App Server: spawn the runtime binary and connect over loopback (docs/agent-store/12 P1-3)",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -25,8 +25,8 @@
25
25
  "dist"
26
26
  ],
27
27
  "dependencies": {
28
- "@flowy-agent-store/client": "0.1.0-beta.7",
29
- "@flowy-agent-store/protocol": "0.1.0-beta.7"
28
+ "@flowy-agent-store/client": "0.1.0-beta.8",
29
+ "@flowy-agent-store/protocol": "0.1.0-beta.8"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsdown",
@@ -40,10 +40,10 @@
40
40
  "module": "./dist/index.mjs",
41
41
  "types": "./dist/index.d.mts",
42
42
  "optionalDependencies": {
43
- "@flowy-agent-store/runtime-win32-x64": "0.1.0-beta.7",
44
- "@flowy-agent-store/runtime-linux-x64": "0.1.0-beta.7",
45
- "@flowy-agent-store/runtime-linux-arm64": "0.1.0-beta.7",
46
- "@flowy-agent-store/runtime-darwin-x64": "0.1.0-beta.7",
47
- "@flowy-agent-store/runtime-darwin-arm64": "0.1.0-beta.7"
43
+ "@flowy-agent-store/runtime-win32-x64": "0.1.0-beta.8",
44
+ "@flowy-agent-store/runtime-linux-x64": "0.1.0-beta.8",
45
+ "@flowy-agent-store/runtime-linux-arm64": "0.1.0-beta.8",
46
+ "@flowy-agent-store/runtime-darwin-x64": "0.1.0-beta.8",
47
+ "@flowy-agent-store/runtime-darwin-arm64": "0.1.0-beta.8"
48
48
  }
49
49
  }