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