@cfio/cohort-sync 0.15.0 → 0.17.0

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.js CHANGED
@@ -11826,7 +11826,13 @@ function createClient(convexUrl) {
11826
11826
  }
11827
11827
  savedConvexUrl = convexUrl;
11828
11828
  authCircuitOpen = false;
11829
- client = new ConvexClient(convexUrl);
11829
+ const nativeWS = globalThis.WebSocket;
11830
+ delete globalThis.WebSocket;
11831
+ try {
11832
+ client = new ConvexClient(convexUrl);
11833
+ } finally {
11834
+ if (nativeWS) globalThis.WebSocket = nativeWS;
11835
+ }
11830
11836
  return client;
11831
11837
  }
11832
11838
  function getClient() {
@@ -11855,7 +11861,10 @@ function closeBridge() {
11855
11861
  }
11856
11862
  var authCircuitOpen = false;
11857
11863
  function isUnauthorizedError(err) {
11858
- return String(err).includes("Unauthorized");
11864
+ if (!(err instanceof ConvexError)) return false;
11865
+ const data = err.data;
11866
+ if (!data || typeof data !== "object") return false;
11867
+ return data.code === "UNAUTHORIZED";
11859
11868
  }
11860
11869
  function tripAuthCircuit() {
11861
11870
  if (authCircuitOpen) return;
@@ -11874,6 +11883,7 @@ var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredBy
11874
11883
  var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
11875
11884
  var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
11876
11885
  var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
11886
+ var getChannelsForPlugin = makeFunctionReference("cloudGatewayChannels:listForPlugin");
11877
11887
  var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
11878
11888
  async function pushTelemetry(apiKey2, data) {
11879
11889
  if (authCircuitOpen) return;
@@ -12204,6 +12214,41 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12204
12214
  logger.info("cohort-sync: command subscription active");
12205
12215
  return unsubscribe;
12206
12216
  }
12217
+ function subscribeChannels(apiKey2, onUpdate, logger) {
12218
+ const c = getClient();
12219
+ if (!c) {
12220
+ (logger ?? getLogger()).warn(
12221
+ "cohort-sync: no ConvexClient \u2014 channels subscription skipped"
12222
+ );
12223
+ return null;
12224
+ }
12225
+ const apiKeyHash = hashApiKey(apiKey2);
12226
+ const unsubscribe = c.onUpdate(
12227
+ getChannelsForPlugin,
12228
+ { apiKeyHash },
12229
+ (rows) => {
12230
+ try {
12231
+ onUpdate(rows);
12232
+ } catch (err) {
12233
+ (logger ?? getLogger()).error(
12234
+ `cohort-sync: channels onUpdate handler threw: ${String(err)}`
12235
+ );
12236
+ }
12237
+ },
12238
+ (err) => {
12239
+ if (isUnauthorizedError(err)) {
12240
+ tripAuthCircuit();
12241
+ return;
12242
+ }
12243
+ (logger ?? getLogger()).error(
12244
+ `cohort-sync: channels subscription error: ${String(err)}`
12245
+ );
12246
+ }
12247
+ );
12248
+ unsubscribers.push(unsubscribe);
12249
+ (logger ?? getLogger()).info("cohort-sync: channels subscription active");
12250
+ return unsubscribe;
12251
+ }
12207
12252
 
12208
12253
  // src/gateway-client.ts
12209
12254
  import crypto2 from "node:crypto";
@@ -12299,7 +12344,13 @@ var ALLOWED_METHODS = /* @__PURE__ */ new Set([
12299
12344
  "agent",
12300
12345
  "snapshot",
12301
12346
  "system.presence",
12302
- "gateway.restart"
12347
+ "gateway.restart",
12348
+ // `secrets.reload` re-resolves SecretRefs and atomically swaps the runtime
12349
+ // snapshot. The plugin's `cohort-sync/secrets-reload` gateway-method wrapper
12350
+ // (see `gateway-methods.ts`) proxies inbound calls to this outbound RPC so
12351
+ // the Cohort web app (and #608 channel attach/detach flows) can trigger a
12352
+ // reload without minting an admin-scope key.
12353
+ "secrets.reload"
12303
12354
  ]);
12304
12355
  function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
12305
12356
  const signedAtMs = Date.now();
@@ -13372,7 +13423,7 @@ function dumpCtx(ctx) {
13372
13423
  function dumpEvent(event) {
13373
13424
  return dumpCtx(event);
13374
13425
  }
13375
- var PLUGIN_VERSION = true ? "0.15.0" : "unknown";
13426
+ var PLUGIN_VERSION = true ? "0.17.0" : "unknown";
13376
13427
  function resolveGatewayToken(api) {
13377
13428
  const token = api.config?.gateway?.auth?.token;
13378
13429
  return typeof token === "string" ? token : null;
@@ -13501,6 +13552,19 @@ function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13501
13552
  });
13502
13553
  return client2;
13503
13554
  }
13555
+ function applyChannelRowsToBridge(rows, setChannel, resolveAgentName) {
13556
+ let count = 0;
13557
+ for (const row of rows) {
13558
+ const agentName = resolveAgentName(row.agentId);
13559
+ setChannel(row.kind, agentName);
13560
+ count++;
13561
+ if (row.accountId) {
13562
+ setChannel(`${row.kind}:${row.accountId}`, agentName);
13563
+ count++;
13564
+ }
13565
+ }
13566
+ return count;
13567
+ }
13504
13568
  async function handleGatewayStart(event, state) {
13505
13569
  if (!state) {
13506
13570
  return;
@@ -13561,6 +13625,39 @@ async function handleGatewayStart(event, state) {
13561
13625
  for (const agentId of allAgentIds) {
13562
13626
  resolvedNameMap[agentId] = state.resolveAgentName(agentId);
13563
13627
  }
13628
+ if (state.channelsUnsubscriber) {
13629
+ state.channelsUnsubscriber();
13630
+ state.channelsUnsubscriber = null;
13631
+ }
13632
+ try {
13633
+ const unsubChannels = subscribeChannels(
13634
+ cfg.apiKey,
13635
+ (rows) => {
13636
+ try {
13637
+ const count = applyChannelRowsToBridge(
13638
+ rows,
13639
+ setChannelAgent,
13640
+ state.resolveAgentName
13641
+ );
13642
+ logger.debug("cohort-sync: channels snapshot applied", {
13643
+ rowCount: rows.length,
13644
+ bridgeEntries: count,
13645
+ bridge: Object.fromEntries(getChannelAgentBridge())
13646
+ });
13647
+ } catch (err) {
13648
+ logger.warn(
13649
+ `cohort-sync: applyChannelRowsToBridge failed: ${String(err)}`
13650
+ );
13651
+ }
13652
+ },
13653
+ logger
13654
+ );
13655
+ if (unsubChannels) state.channelsUnsubscriber = unsubChannels;
13656
+ } catch (err) {
13657
+ logger.warn(
13658
+ `cohort-sync: channels subscription init failed: ${String(err)}`
13659
+ );
13660
+ }
13564
13661
  await startNotificationSubscription(
13565
13662
  event.port,
13566
13663
  cfg,
@@ -14003,6 +14100,13 @@ function registerHookHandlers(api, logger, getState) {
14003
14100
  clearInterval(state.keepaliveInterval);
14004
14101
  state.keepaliveInterval = null;
14005
14102
  }
14103
+ if (state.channelsUnsubscriber) {
14104
+ try {
14105
+ state.channelsUnsubscriber();
14106
+ } catch {
14107
+ }
14108
+ state.channelsUnsubscriber = null;
14109
+ }
14006
14110
  state.activityBatch.drain();
14007
14111
  if (!isRestart) {
14008
14112
  const shutdownConfigIds = (config?.agents?.list ?? []).map((a) => a.id);
@@ -14150,11 +14254,47 @@ function initializeHookState(api, cfg) {
14150
14254
  gwClientInitialized,
14151
14255
  keepaliveInterval: null,
14152
14256
  commandUnsubscriber: commandUnsub,
14257
+ channelsUnsubscriber: null,
14153
14258
  api,
14154
14259
  latestPluginVersion: null
14155
14260
  };
14156
14261
  }
14157
14262
 
14263
+ // src/gateway-methods.ts
14264
+ async function invokeSecretsReload(gwClient) {
14265
+ if (!gwClient || !gwClient.isAlive()) {
14266
+ return { ok: false, error: "gateway client not connected" };
14267
+ }
14268
+ try {
14269
+ const result = await gwClient.request(
14270
+ "secrets.reload",
14271
+ void 0,
14272
+ 3e4
14273
+ // Channel restart can take a few seconds — give it headroom.
14274
+ );
14275
+ const warningCount = typeof result?.warningCount === "number" ? result.warningCount : 0;
14276
+ return { ok: true, warningCount };
14277
+ } catch (err) {
14278
+ return {
14279
+ ok: false,
14280
+ error: err instanceof Error ? err.message : String(err)
14281
+ };
14282
+ }
14283
+ }
14284
+ function registerGatewayMethods(api, getGatewayClient) {
14285
+ api.registerGatewayMethod(
14286
+ "cohort-sync/secrets-reload",
14287
+ async ({ respond }) => {
14288
+ const result = await invokeSecretsReload(getGatewayClient());
14289
+ respond(true, result);
14290
+ },
14291
+ // operator.write is the channel attach/detach surface — same scope used
14292
+ // for any write-side channels operation. Admins also pass (admin is a
14293
+ // superset).
14294
+ { scope: "operator.write" }
14295
+ );
14296
+ }
14297
+
14158
14298
  // src/pocket-guide.ts
14159
14299
  var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
14160
14300
 
@@ -14230,6 +14370,7 @@ var plugin = {
14230
14370
  return;
14231
14371
  }
14232
14372
  registerHookHandlers(api, api.logger, () => sharedHookState);
14373
+ registerGatewayMethods(api, () => sharedHookState?.persistentGwClient ?? null);
14233
14374
  const gatewayPort = api.config?.gateway?.port ?? 18789;
14234
14375
  api.registerHook(
14235
14376
  "gateway:startup",
@@ -55,5 +55,5 @@
55
55
  }
56
56
  }
57
57
  },
58
- "version": "0.15.0"
58
+ "version": "0.17.0"
59
59
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",