@flowy-agent-store/client 0.1.0-beta.6 → 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/dist/index.mjs CHANGED
@@ -275,7 +275,7 @@ var WebSocketTransport = class {
275
275
  /** Header the server mints the per-call connection id under. */
276
276
  const CONNECTION_HEADER = "x-app-server-connection-id";
277
277
  /** Protocol version this binding announces on the one-shot handshake. */
278
- const PROTOCOL_VERSION = "fp-7";
278
+ const PROTOCOL_VERSION = "fp-12";
279
279
  /**
280
280
  * Protocol method → REST route. Verified from handler bodies, not from the
281
281
  * route list (a route existing does not prove which method it mirrors).
@@ -470,6 +470,26 @@ const HTTP_ROUTES = {
470
470
  path: "/connectors/:connector_id/auth-logout",
471
471
  source: "connector_auth_logout_route() -> connector_auth_logout_impl"
472
472
  },
473
+ "connector/credential/get": {
474
+ verb: "GET",
475
+ path: "/connectors/:connector_id/credential",
476
+ source: "connector_credential_get_route() -> connector_credential_get_impl"
477
+ },
478
+ "connector/credential/set": {
479
+ verb: "POST",
480
+ path: "/connectors/:connector_id/credential",
481
+ source: "connector_credential_set_route() -> connector_credential_set_impl"
482
+ },
483
+ "connector/credential/clear": {
484
+ verb: "POST",
485
+ path: "/connectors/:connector_id/credential/clear",
486
+ source: "connector_credential_clear_route() -> connector_credential_clear_impl"
487
+ },
488
+ "connector/register": {
489
+ verb: "POST",
490
+ path: "/connectors",
491
+ source: "register_connector_route() -> register_connector_impl"
492
+ },
473
493
  "import/run": {
474
494
  verb: "POST",
475
495
  path: "/imports",
@@ -559,6 +579,11 @@ const HTTP_ROUTES = {
559
579
  verb: "POST",
560
580
  path: "/store/:marketplace_id/entries/:entry_name/install",
561
581
  source: "store_install_entry_route() -> store_install_entry_impl"
582
+ },
583
+ "store/update-entry": {
584
+ verb: "POST",
585
+ path: "/store/:marketplace_id/entries/:entry_name/update",
586
+ source: "store_update_entry_route() -> store_update_entry_impl"
562
587
  }
563
588
  };
564
589
  /** Methods that are WebSocket-only by design — named so the error explains itself. */
@@ -780,6 +805,19 @@ var AgentClient = class {
780
805
  get(agentId) {
781
806
  return this.transport.request("agent/get", { agent_id: agentId });
782
807
  }
808
+ /**
809
+ * Export one AgentDefinition as a portable {@link ExpertPack}.
810
+ *
811
+ * Requires WebSocket: like the rest of this family, `agent/export` has no HTTP
812
+ * route. The expert must be installed and enabled, or the host answers
813
+ * `agent_not_installed` / `agent_disabled`; a host whose `[expert_export]`
814
+ * table refuses the id answers `policy_denied`. Check
815
+ * `capabilities.expert_export` for the *seam* — it does not promise this id is
816
+ * exportable.
817
+ */
818
+ export(agentId) {
819
+ return this.transport.request("agent/export", { agent_id: agentId });
820
+ }
783
821
  };
784
822
  //#endregion
785
823
  //#region src/conversations.ts
@@ -1066,6 +1104,13 @@ var ConversationSubscription = class {
1066
1104
  };
1067
1105
  //#endregion
1068
1106
  //#region src/connectors.ts
1107
+ /**
1108
+ * How long {@link ConnectorClient.waitForAuth} waits by default: the host's own
1109
+ * callback window is 120s, so a shorter client budget would give up on flows the
1110
+ * host is still willing to finish.
1111
+ */
1112
+ const DEFAULT_AUTH_TIMEOUT_MS = 12e4;
1113
+ const DEFAULT_AUTH_POLL_MS = 500;
1069
1114
  var ConnectorClient = class {
1070
1115
  transport;
1071
1116
  constructor(transport) {
@@ -1099,17 +1144,117 @@ var ConnectorClient = class {
1099
1144
  test(connectorId) {
1100
1145
  return this.transport.request("connector/test", { connector_id: connectorId });
1101
1146
  }
1102
- /** Check the current OAuth state for a connector. */
1147
+ /**
1148
+ * Check the current OAuth state for a connector.
1149
+ *
1150
+ * Read `error` too: a flow that failed after the browser step is reported as
1151
+ * `state: "not_authenticated"` **plus** `error`, because the wire has no
1152
+ * failing state to flip to. {@link waitForAuth} is the loop that does this.
1153
+ */
1103
1154
  authStatus(connectorId) {
1104
1155
  return this.transport.request("connector/auth/status", { connector_id: connectorId });
1105
1156
  }
1106
1157
  /**
1158
+ * The credential form a connector needs filled in, as **this caller** sees it
1159
+ * (doc `34` §6.1).
1160
+ *
1161
+ * `mode` says which kind of authentication applies (`none` / `oauth` / `token`),
1162
+ * `status` whether anything is still missing, and `missing` names the keys.
1163
+ * `fields` is everything needed to render the form — labels, placeholders,
1164
+ * descriptions and the marketplace's "where do I get a key" link, in both
1165
+ * languages. No secret ever comes back: only `plain` fields carry a `value`.
1166
+ */
1167
+ credentials(connectorId) {
1168
+ return this.transport.request("connector/credential/get", { connector_id: connectorId });
1169
+ }
1170
+ /**
1171
+ * Store what the user typed and get the new state back.
1172
+ *
1173
+ * Only keys the connector's own declaration names are accepted — the form is
1174
+ * the whole write surface. `secret` fields go to the host's credential store
1175
+ * under the caller's own namespace, `plain` fields into the connector's
1176
+ * configuration, so a shared host never resolves one user's token for another.
1177
+ */
1178
+ setCredentials(connectorId, values) {
1179
+ return this.transport.request("connector/credential/set", {
1180
+ connector_id: connectorId,
1181
+ values
1182
+ });
1183
+ }
1184
+ /**
1185
+ * Forget stored credentials. `keys` omitted = every secret field of this
1186
+ * connector. Idempotent: clearing what is not there succeeds.
1187
+ */
1188
+ clearCredentials(connectorId, keys) {
1189
+ return this.transport.request("connector/credential/clear", {
1190
+ connector_id: connectorId,
1191
+ ...keys ? { keys } : {}
1192
+ });
1193
+ }
1194
+ /**
1195
+ * Register a connector this host never imported — your own MCP server, handed
1196
+ * over together with the template that says what it needs (doc `34` §6.5).
1197
+ *
1198
+ * The **template is the whole declaration**: whatever `${secret:NAME}` the URL,
1199
+ * headers or env name becomes that connector's credential form, so there is no
1200
+ * `token-schema.json` to ship and nothing to install from a marketplace. The
1201
+ * connector's own non-secret settings travel in `values`.
1202
+ *
1203
+ * Secrets do **not** travel here: hand them over with {@link setCredentials},
1204
+ * which keeps exactly one write surface for values. The returned detail already
1205
+ * carries the credential block, so `missing` tells you which keys are still
1206
+ * needed.
1207
+ *
1208
+ * Re-registering the same name updates it; a builtin server's name is refused.
1209
+ * The row is created **disabled** — enabling it still requires a probe that
1210
+ * passes — and this method is on the installation-owner-only surface, because
1211
+ * choosing where the host reaches is the owner's call.
1212
+ */
1213
+ register(registration) {
1214
+ return this.transport.request("connector/register", registration);
1215
+ }
1216
+ /**
1107
1217
  * Kick off the OAuth browser flow on the trusted host. Returns the start
1108
- * acknowledgement immediately; poll `authStatus` until `authenticated`.
1218
+ * acknowledgement immediately; follow it with {@link waitForAuth}.
1219
+ *
1220
+ * `state: "error"` here is a failure that happened **before** the browser was
1221
+ * opened (endpoint discovery, client identity, binding the callback, launching
1222
+ * the browser): nothing was shown to the user, so `error` is the whole story
1223
+ * and there is nothing to wait for.
1109
1224
  */
1110
1225
  authStart(connectorId) {
1111
1226
  return this.transport.request("connector/auth/start", { connector_id: connectorId });
1112
1227
  }
1228
+ /**
1229
+ * Wait for an authorization that was started (in the browser, or by a
1230
+ * previous `authStart`) to finish — and **report why** when it does not.
1231
+ *
1232
+ * A hand-rolled `while (state !== "authenticated") poll()` loop ends in "it
1233
+ * never became authenticated", with the reason sitting unread in the status it
1234
+ * already fetched: a token exchange the authorization server refused (a
1235
+ * throttling gateway answers `slow_down`), a callback timeout, a CSRF/path
1236
+ * mismatch. Those land in `authStatus().error` *only*, and the state stays
1237
+ * `not_authenticated` — there is no failing state on the wire.
1238
+ *
1239
+ * Resolves for every domain outcome (`authenticated` / `error` / `timeout`);
1240
+ * rejects only when the status read itself fails (transport or protocol), the
1241
+ * same rule as `connector/call`. The wait is bounded by `timeoutMs`; a caller
1242
+ * that needs to stop earlier can race the returned promise.
1243
+ */
1244
+ async waitForAuth(connectorId, options = {}) {
1245
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS);
1246
+ const pollMs = options.pollMs ?? DEFAULT_AUTH_POLL_MS;
1247
+ for (;;) {
1248
+ const status = await this.authStatus(connectorId);
1249
+ if (status.state === "authenticated") return { state: "authenticated" };
1250
+ if (status.error) return {
1251
+ state: "error",
1252
+ error: status.error
1253
+ };
1254
+ if (Date.now() >= deadline) return { state: "timeout" };
1255
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
1256
+ }
1257
+ }
1113
1258
  /** Revoke the OAuth token for a connector. */
1114
1259
  logout(connectorId) {
1115
1260
  return this.transport.request("connector/auth/logout", { connector_id: connectorId }).then(() => void 0);
@@ -1529,6 +1674,7 @@ var StoreError = class extends Error {
1529
1674
  const DEFAULT_READY_TIMEOUT_MS = 3e4;
1530
1675
  const DEFAULT_READY_POLL_MS = 400;
1531
1676
  const DEFAULT_READY_POLL_MAX_MS = 4e3;
1677
+ const DEFAULT_READY_PROBE_MS = 5e3;
1532
1678
  /** Every string a `LocalizedText` carries, for haystack building. */
1533
1679
  function localizedValues(text) {
1534
1680
  if (typeof text === "string") return [text];
@@ -1570,11 +1716,13 @@ var StoreClient = class {
1570
1716
  readyTimeoutMs;
1571
1717
  readyPollMs;
1572
1718
  readyPollMaxMs;
1719
+ readyProbeMs;
1573
1720
  constructor(host, options = {}) {
1574
1721
  this.host = host;
1575
1722
  this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
1576
1723
  this.readyPollMs = options.readyPollMs ?? DEFAULT_READY_POLL_MS;
1577
1724
  this.readyPollMaxMs = options.readyPollMaxMs ?? DEFAULT_READY_POLL_MAX_MS;
1725
+ this.readyProbeMs = options.readyProbeMs ?? DEFAULT_READY_PROBE_MS;
1578
1726
  }
1579
1727
  /** Every item across enabled marketplaces, with install state. */
1580
1728
  async list() {
@@ -1606,15 +1754,45 @@ var StoreClient = class {
1606
1754
  /**
1607
1755
  * What a caller can offer for an item's pending update.
1608
1756
  *
1609
- * `"none"` means there is nothing to do; `"uninstall_reinstall"` means the
1610
- * entry is installed at a different version and the only path the wire offers
1611
- * is to release it and install again (the version-aware re-import then picks
1612
- * up the current version). `"unknown"` is returned rather than guessing when
1613
- * the item carries no snapshot to release.
1757
+ * `"none"` means there is nothing to do; `"update"` means the entry is
1758
+ * installed at a different version and `update()` is the action that lands it
1759
+ * (`store/update-entry`, `36` D3); `"unknown"` is returned rather than
1760
+ * guessing when the item carries no snapshot to replace.
1614
1761
  */
1615
1762
  updateHint(item) {
1616
1763
  if (!item.installed || !item.update_available) return "none";
1617
- return item.snapshot_id ? "uninstall_reinstall" : "unknown";
1764
+ return item.snapshot_id ? "update" : "unknown";
1765
+ }
1766
+ /**
1767
+ * Upgrade an installed item to the version its marketplace advertises.
1768
+ *
1769
+ * The host does the ordering: new version installed first, previous one
1770
+ * released only when that succeeded. So a failure resolves `ok: false` with
1771
+ * the previous installation still in place (`releasedCount === 0`) — the
1772
+ * caller never has to reason about a half-upgraded item.
1773
+ *
1774
+ * Refuses before the round trip when there is nothing to replace: an item that
1775
+ * is not installed (`not_installed`) or is already current is not a failure of
1776
+ * *this* call, and `updateHint()` is the way to ask first.
1777
+ */
1778
+ async update(item, options = {}) {
1779
+ this.assertUsable(item, options.signal);
1780
+ this.requireSnapshot(item);
1781
+ const result = await this.host.updateStoreEntry(item.marketplace_id, item.entry_name);
1782
+ const components = result.outcomes ?? [];
1783
+ const outcome = {
1784
+ snapshotId: result.snapshot_id,
1785
+ reused: result.reused,
1786
+ components,
1787
+ ok: result.errors.length === 0 && components.every((component) => component.ok),
1788
+ errors: result.errors,
1789
+ warnings: result.warnings,
1790
+ fromVersion: result.previous_version ?? void 0,
1791
+ toVersion: result.version,
1792
+ releasedCount: result.released_count ?? 0
1793
+ };
1794
+ if (options.waitForReady === false) return outcome;
1795
+ return this.awaitReady(outcome, options);
1618
1796
  }
1619
1797
  /**
1620
1798
  * Import (when needed) and install one store item, then wait until it is
@@ -1628,7 +1806,9 @@ var StoreClient = class {
1628
1806
  snapshotId: result.snapshot_id,
1629
1807
  reused: result.reused,
1630
1808
  components,
1631
- ok: result.errors.length === 0 && components.every((component) => component.ok)
1809
+ ok: result.errors.length === 0 && components.every((component) => component.ok),
1810
+ errors: result.errors,
1811
+ warnings: result.warnings
1632
1812
  };
1633
1813
  if (options.waitForReady === false) return outcome;
1634
1814
  return this.awaitReady(outcome, options);
@@ -1647,7 +1827,9 @@ var StoreClient = class {
1647
1827
  snapshotId,
1648
1828
  reused: true,
1649
1829
  components: [],
1650
- ok: true
1830
+ ok: true,
1831
+ errors: [],
1832
+ warnings: []
1651
1833
  };
1652
1834
  const status = await this.host.uninstallInstall(snapshotId, componentIds);
1653
1835
  return this.outcomeFromStatus(snapshotId, status, false);
@@ -1667,7 +1849,9 @@ var StoreClient = class {
1667
1849
  snapshotId,
1668
1850
  reused: true,
1669
1851
  components: [],
1670
- ok: true
1852
+ ok: true,
1853
+ errors: [],
1854
+ warnings: []
1671
1855
  };
1672
1856
  const status = enabled ? await this.host.enableInstall(snapshotId, componentIds) : await this.host.disableInstall(snapshotId, componentIds);
1673
1857
  return this.outcomeFromStatus(snapshotId, status, false);
@@ -1686,11 +1870,14 @@ var StoreClient = class {
1686
1870
  }
1687
1871
  outcomeFromStatus(snapshotId, status, reused) {
1688
1872
  const components = status.outcomes ?? [];
1873
+ const errors = status.errors ?? [];
1689
1874
  return {
1690
1875
  snapshotId,
1691
1876
  reused,
1692
1877
  components,
1693
- ok: (status.errors ?? []).length === 0 && components.every((component) => component.ok)
1878
+ ok: errors.length === 0 && components.every((component) => component.ok),
1879
+ errors,
1880
+ warnings: []
1694
1881
  };
1695
1882
  }
1696
1883
  /**
@@ -1708,11 +1895,15 @@ var StoreClient = class {
1708
1895
  if (connectors.length > 0) await this.host.enableInstall(snapshotId, connectors.map((component) => component.id));
1709
1896
  const deadline = Date.now() + (options.timeoutMs ?? this.readyTimeoutMs);
1710
1897
  let interval = this.readyPollMs;
1898
+ let nextProbeAt = 0;
1711
1899
  let last = status.components;
1712
1900
  for (;;) {
1901
+ const now = Date.now();
1902
+ const allowProbe = now >= nextProbeAt;
1903
+ if (allowProbe) nextProbeAt = now + this.readyProbeMs;
1713
1904
  const blocked = (await Promise.all(last.filter((component) => component.state !== "not-installed").map(async (component) => ({
1714
1905
  component,
1715
- verdict: await this.readiness(component)
1906
+ verdict: await this.readiness(component, allowProbe)
1716
1907
  })))).find((entry) => !entry.verdict.ready);
1717
1908
  if (!blocked) return {
1718
1909
  ...outcome,
@@ -1742,8 +1933,12 @@ var StoreClient = class {
1742
1933
  * unknown kind is reported ready rather than blocking: this layer cannot judge
1743
1934
  * a component it does not understand, and a false timeout on a future kind
1744
1935
  * would be worse than passing it through.
1936
+ *
1937
+ * `allowProbe` gates the one kind whose readiness needs a real connection (a
1938
+ * connector); the other kinds answer from local reads, so the cadence does not
1939
+ * apply to them.
1745
1940
  */
1746
- async readiness(component) {
1941
+ async readiness(component, allowProbe) {
1747
1942
  try {
1748
1943
  switch (component.kind) {
1749
1944
  case "skill": return (await this.host.skills.list()).some((skill) => skill.name === component.name) ? { ready: true } : { ready: false };
@@ -1752,8 +1947,9 @@ var StoreClient = class {
1752
1947
  await this.host.teams.get(component.id);
1753
1948
  return { ready: true };
1754
1949
  case "connector": {
1755
- if ((await this.host.connectors.test(component.id)).success) return { ready: true };
1950
+ if (allowProbe && (await this.host.connectors.test(component.id)).success) return { ready: true };
1756
1951
  const status = await this.host.connectors.status(component.id);
1952
+ if (status.status === "connected") return { ready: true };
1757
1953
  if (status.status === "authorization_required" || status.status === "reauthorization_required") return {
1758
1954
  ready: false,
1759
1955
  terminal: "authorization_required"
@@ -1782,6 +1978,21 @@ var TeamClient = class {
1782
1978
  get(teamId) {
1783
1979
  return this.transport.request("team/get", { team_id: teamId });
1784
1980
  }
1981
+ /**
1982
+ * Export one TeamDefinition as a portable {@link ExpertPack}, with every member
1983
+ * expanded and the **leader first**.
1984
+ *
1985
+ * All-or-nothing: if any member is uninstalled, disabled, or refused by the
1986
+ * host's `[expert_export]` table, the whole call fails rather than returning a
1987
+ * partial roster — the same posture `team/run` takes before it creates
1988
+ * anything. `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
1989
+ */
1990
+ export(teamId, teamVersion) {
1991
+ return this.transport.request("team/export", {
1992
+ team_id: teamId,
1993
+ team_version: teamVersion
1994
+ });
1995
+ }
1785
1996
  };
1786
1997
  //#endregion
1787
1998
  //#region src/models.ts
@@ -2025,6 +2236,20 @@ var AppServerClient = class {
2025
2236
  entry_name: entryName
2026
2237
  });
2027
2238
  }
2239
+ /**
2240
+ * Upgrade an installed entry to the version its marketplace advertises.
2241
+ *
2242
+ * The host installs the new version before releasing the old one, so a failure
2243
+ * leaves the previous installation usable (`released_count: 0` is the machine
2244
+ * readable form of that). Refused for an entry that is not installed — this is
2245
+ * never a second way to install something.
2246
+ */
2247
+ async updateStoreEntry(marketplaceId, entryName) {
2248
+ return this.transport.request("store/update-entry", {
2249
+ marketplace_id: marketplaceId,
2250
+ entry_name: entryName
2251
+ });
2252
+ }
2028
2253
  };
2029
2254
  //#endregion
2030
2255
  //#region src/conversation-handle.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowy-agent-store/client",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "Transport-agnostic App Server client (docs/agent-store/07 §2.2)",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -25,7 +25,7 @@
25
25
  "dist"
26
26
  ],
27
27
  "dependencies": {
28
- "@flowy-agent-store/protocol": "0.1.0-beta.6"
28
+ "@flowy-agent-store/protocol": "0.1.0-beta.8"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsdown",