@cfio/cohort-sync 0.16.0 → 0.18.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
@@ -11861,7 +11861,10 @@ function closeBridge() {
11861
11861
  }
11862
11862
  var authCircuitOpen = false;
11863
11863
  function isUnauthorizedError(err) {
11864
- 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";
11865
11868
  }
11866
11869
  function tripAuthCircuit() {
11867
11870
  if (authCircuitOpen) return;
@@ -11880,6 +11883,7 @@ var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredBy
11880
11883
  var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
11881
11884
  var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
11882
11885
  var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
11886
+ var getChannelsForPlugin = makeFunctionReference("cloudGatewayChannels:listForPlugin");
11883
11887
  var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
11884
11888
  async function pushTelemetry(apiKey2, data) {
11885
11889
  if (authCircuitOpen) return;
@@ -12210,6 +12214,41 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12210
12214
  logger.info("cohort-sync: command subscription active");
12211
12215
  return unsubscribe;
12212
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
+ }
12213
12252
 
12214
12253
  // src/gateway-client.ts
12215
12254
  import crypto2 from "node:crypto";
@@ -12305,7 +12344,13 @@ var ALLOWED_METHODS = /* @__PURE__ */ new Set([
12305
12344
  "agent",
12306
12345
  "snapshot",
12307
12346
  "system.presence",
12308
- "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"
12309
12354
  ]);
12310
12355
  function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
12311
12356
  const signedAtMs = Date.now();
@@ -13378,7 +13423,7 @@ function dumpCtx(ctx) {
13378
13423
  function dumpEvent(event) {
13379
13424
  return dumpCtx(event);
13380
13425
  }
13381
- var PLUGIN_VERSION = true ? "0.16.0" : "unknown";
13426
+ var PLUGIN_VERSION = true ? "0.18.0" : "unknown";
13382
13427
  function resolveGatewayToken(api) {
13383
13428
  const token = api.config?.gateway?.auth?.token;
13384
13429
  return typeof token === "string" ? token : null;
@@ -13507,6 +13552,19 @@ function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13507
13552
  });
13508
13553
  return client2;
13509
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
+ }
13510
13568
  async function handleGatewayStart(event, state) {
13511
13569
  if (!state) {
13512
13570
  return;
@@ -13567,6 +13625,39 @@ async function handleGatewayStart(event, state) {
13567
13625
  for (const agentId of allAgentIds) {
13568
13626
  resolvedNameMap[agentId] = state.resolveAgentName(agentId);
13569
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
+ }
13570
13661
  await startNotificationSubscription(
13571
13662
  event.port,
13572
13663
  cfg,
@@ -14009,6 +14100,13 @@ function registerHookHandlers(api, logger, getState) {
14009
14100
  clearInterval(state.keepaliveInterval);
14010
14101
  state.keepaliveInterval = null;
14011
14102
  }
14103
+ if (state.channelsUnsubscriber) {
14104
+ try {
14105
+ state.channelsUnsubscriber();
14106
+ } catch {
14107
+ }
14108
+ state.channelsUnsubscriber = null;
14109
+ }
14012
14110
  state.activityBatch.drain();
14013
14111
  if (!isRestart) {
14014
14112
  const shutdownConfigIds = (config?.agents?.list ?? []).map((a) => a.id);
@@ -14156,11 +14254,82 @@ function initializeHookState(api, cfg) {
14156
14254
  gwClientInitialized,
14157
14255
  keepaliveInterval: null,
14158
14256
  commandUnsubscriber: commandUnsub,
14257
+ channelsUnsubscriber: null,
14159
14258
  api,
14160
14259
  latestPluginVersion: null
14161
14260
  };
14162
14261
  }
14163
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
+ var ALLOWED_PAIRING_KINDS = /* @__PURE__ */ new Set([
14285
+ "telegram",
14286
+ "discord",
14287
+ "slack",
14288
+ "whatsapp",
14289
+ "signal",
14290
+ "imessage"
14291
+ ]);
14292
+ var PAIRING_CODE_RE = /^[A-Z0-9]{4,16}$/;
14293
+ async function invokePairingApprove(params) {
14294
+ const kind = typeof params.kind === "string" ? params.kind : "";
14295
+ const code2 = typeof params.code === "string" ? params.code : "";
14296
+ if (!ALLOWED_PAIRING_KINDS.has(kind)) {
14297
+ return { ok: false, error: `invalid channel kind: ${kind || "(empty)"}` };
14298
+ }
14299
+ if (!PAIRING_CODE_RE.test(code2)) {
14300
+ return { ok: false, error: "invalid pairing code (expected 4\u201316 uppercase alphanumerics)" };
14301
+ }
14302
+ return {
14303
+ ok: false,
14304
+ error: "pairing-approve via gateway-method is a stub; call openclaw pairing approve via Fly machines exec instead (see convex/cohortChannels.ts:runTelegramPairingApprove)"
14305
+ };
14306
+ }
14307
+ function registerGatewayMethods(api, getGatewayClient) {
14308
+ api.registerGatewayMethod(
14309
+ "cohort-sync/secrets-reload",
14310
+ async ({ respond }) => {
14311
+ const result = await invokeSecretsReload(getGatewayClient());
14312
+ respond(true, result);
14313
+ },
14314
+ // operator.write is the channel attach/detach surface — same scope used
14315
+ // for any write-side channels operation. Admins also pass (admin is a
14316
+ // superset).
14317
+ { scope: "operator.write" }
14318
+ );
14319
+ api.registerGatewayMethod(
14320
+ "cohort-sync/pairing-approve",
14321
+ async ({ respond, params }) => {
14322
+ const result = await invokePairingApprove(
14323
+ params ?? {}
14324
+ );
14325
+ respond(true, result);
14326
+ },
14327
+ // Same scope rationale as secrets-reload — pairing approve is a channel
14328
+ // write operation.
14329
+ { scope: "operator.write" }
14330
+ );
14331
+ }
14332
+
14164
14333
  // src/pocket-guide.ts
14165
14334
  var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
14166
14335
 
@@ -14236,6 +14405,7 @@ var plugin = {
14236
14405
  return;
14237
14406
  }
14238
14407
  registerHookHandlers(api, api.logger, () => sharedHookState);
14408
+ registerGatewayMethods(api, () => sharedHookState?.persistentGwClient ?? null);
14239
14409
  const gatewayPort = api.config?.gateway?.port ?? 18789;
14240
14410
  api.registerHook(
14241
14411
  "gateway:startup",
@@ -55,5 +55,5 @@
55
55
  }
56
56
  }
57
57
  },
58
- "version": "0.16.0"
58
+ "version": "0.18.0"
59
59
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.16.0",
3
+ "version": "0.18.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.16.0",
3
+ "version": "0.18.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",