@cfio/cohort-sync 0.8.0 → 0.8.1

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
@@ -16,9 +16,9 @@ __export(keychain_exports, {
16
16
  setCredential: () => setCredential
17
17
  });
18
18
  import { execFile } from "node:child_process";
19
- import os3 from "node:os";
19
+ import os4 from "node:os";
20
20
  function assertMacOS(operation) {
21
- if (os3.platform() !== "darwin") {
21
+ if (os4.platform() !== "darwin") {
22
22
  throw new Error(
23
23
  `cohort-sync: ${operation} requires macOS Keychain. On Linux/Windows, set your API key in OpenClaw config: plugins.entries.cohort-sync.config.apiKey`
24
24
  );
@@ -100,6 +100,7 @@ var init_keychain = __esm({
100
100
 
101
101
  // src/hooks.ts
102
102
  import fs3 from "node:fs";
103
+ import os3 from "node:os";
103
104
  import path3 from "node:path";
104
105
 
105
106
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
@@ -2930,6 +2931,9 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
2930
2931
  logger.info("cohort-sync: full sync complete");
2931
2932
  }
2932
2933
 
2934
+ // src/convex-bridge.ts
2935
+ import { createHash } from "crypto";
2936
+
2933
2937
  // ../../node_modules/.pnpm/convex@1.33.0_patch_hash=l43bztwr6e2lbmpd6ao6hmcg24_react@19.2.1/node_modules/convex/dist/esm/index.js
2934
2938
  var version = "1.33.0";
2935
2939
 
@@ -7860,14 +7864,14 @@ var require_node_gyp_build = __commonJS({
7860
7864
  "../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
7861
7865
  var fs4 = __require("fs");
7862
7866
  var path4 = __require("path");
7863
- var os4 = __require("os");
7867
+ var os5 = __require("os");
7864
7868
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
7865
7869
  var vars = process.config && process.config.variables || {};
7866
7870
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
7867
7871
  var abi = process.versions.modules;
7868
7872
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
7869
- var arch = process.env.npm_config_arch || os4.arch();
7870
- var platform = process.env.npm_config_platform || os4.platform();
7873
+ var arch = process.env.npm_config_arch || os5.arch();
7874
+ var platform = process.env.npm_config_platform || os5.platform();
7871
7875
  var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
7872
7876
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
7873
7877
  var uv = (process.versions.uv || "").split(".")[0];
@@ -10219,7 +10223,7 @@ var require_websocket = __commonJS({
10219
10223
  var http = __require("http");
10220
10224
  var net = __require("net");
10221
10225
  var tls = __require("tls");
10222
- var { randomBytes, createHash } = __require("crypto");
10226
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
10223
10227
  var { Duplex, Readable } = __require("stream");
10224
10228
  var { URL: URL2 } = __require("url");
10225
10229
  var PerMessageDeflate = require_permessage_deflate();
@@ -10876,7 +10880,7 @@ var require_websocket = __commonJS({
10876
10880
  abortHandshake(websocket, socket, "Invalid Upgrade header");
10877
10881
  return;
10878
10882
  }
10879
- const digest = createHash("sha1").update(key + GUID).digest("base64");
10883
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
10880
10884
  if (res.headers["sec-websocket-accept"] !== digest) {
10881
10885
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
10882
10886
  return;
@@ -11141,7 +11145,7 @@ var require_websocket_server = __commonJS({
11141
11145
  var EventEmitter = __require("events");
11142
11146
  var http = __require("http");
11143
11147
  var { Duplex } = __require("stream");
11144
- var { createHash } = __require("crypto");
11148
+ var { createHash: createHash2 } = __require("crypto");
11145
11149
  var extension = require_extension();
11146
11150
  var PerMessageDeflate = require_permessage_deflate();
11147
11151
  var subprotocol = require_subprotocol();
@@ -11436,7 +11440,7 @@ var require_websocket_server = __commonJS({
11436
11440
  );
11437
11441
  }
11438
11442
  if (this._state > RUNNING) return abortHandshake(socket, 503);
11439
- const digest = createHash("sha1").update(key + GUID).digest("base64");
11443
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
11440
11444
  const headers = [
11441
11445
  "HTTP/1.1 101 Switching Protocols",
11442
11446
  "Upgrade: websocket",
@@ -11773,7 +11777,6 @@ function reverseResolveAgentName(cohortName, forwardMap) {
11773
11777
  }
11774
11778
 
11775
11779
  // src/commands.ts
11776
- var cronRunNowPoll = null;
11777
11780
  async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
11778
11781
  if (cmd.type === "restart") {
11779
11782
  logger.info("cohort-sync: restart command, terminating in 500ms");
@@ -11806,41 +11809,7 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
11806
11809
  });
11807
11810
  break;
11808
11811
  case "cronRunNow": {
11809
- const runResult = await gwClient.request(
11810
- "cron.run",
11811
- { jobId: cmd.payload?.jobId }
11812
- );
11813
- if (runResult?.ok && runResult?.ran) {
11814
- const jobId = cmd.payload?.jobId;
11815
- let polls = 0;
11816
- if (cronRunNowPoll) clearInterval(cronRunNowPoll);
11817
- const pollInterval = setInterval(async () => {
11818
- polls++;
11819
- if (polls >= 15) {
11820
- clearInterval(pollInterval);
11821
- cronRunNowPoll = null;
11822
- return;
11823
- }
11824
- try {
11825
- if (!gwClient || !gwClient.isAlive()) {
11826
- clearInterval(pollInterval);
11827
- cronRunNowPoll = null;
11828
- return;
11829
- }
11830
- const pollResult = await gwClient.request("cron.list");
11831
- const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
11832
- const job = freshJobs.find((j) => j.id === jobId);
11833
- if (job && !job.state?.runningAtMs) {
11834
- clearInterval(pollInterval);
11835
- cronRunNowPoll = null;
11836
- const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11837
- await pushCronSnapshot(cfg.apiKey, mapped);
11838
- }
11839
- } catch {
11840
- }
11841
- }, 2e3);
11842
- cronRunNowPoll = pollInterval;
11843
- }
11812
+ await gwClient.request("cron.run", { jobId: cmd.payload?.jobId });
11844
11813
  break;
11845
11814
  }
11846
11815
  case "cronCreate": {
@@ -11875,7 +11844,7 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
11875
11844
  }
11876
11845
  if (gwClient.isAlive()) {
11877
11846
  try {
11878
- const snapResult = await gwClient.request("cron.list");
11847
+ const snapResult = await gwClient.request("cron.list", { includeDisabled: true });
11879
11848
  const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
11880
11849
  const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11881
11850
  await pushCronSnapshot(cfg.apiKey, mapped);
@@ -11889,7 +11858,14 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
11889
11858
  }
11890
11859
 
11891
11860
  // src/convex-bridge.ts
11861
+ function hashApiKey(key) {
11862
+ return createHash("sha256").update(key).digest("hex");
11863
+ }
11892
11864
  function deriveConvexUrl(apiUrl) {
11865
+ const normalized = apiUrl.replace(/\/+$/, "");
11866
+ if (/^https?:\/\/api\.cohort\.bot$/i.test(normalized)) {
11867
+ return normalized.replace(/api\.cohort\.bot$/i, "ws.cohort.bot");
11868
+ }
11893
11869
  return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
11894
11870
  }
11895
11871
  var savedLogger = null;
@@ -11953,7 +11929,7 @@ async function pushTelemetry(apiKey, data) {
11953
11929
  const c = getClient();
11954
11930
  if (!c) return;
11955
11931
  try {
11956
- await c.mutation(upsertTelemetryFromPlugin, { apiKey, ...data });
11932
+ await c.mutation(upsertTelemetryFromPlugin, { apiKeyHash: hashApiKey(apiKey), ...data });
11957
11933
  } catch (err) {
11958
11934
  getLogger().error(`cohort-sync: pushTelemetry failed: ${err}`);
11959
11935
  }
@@ -11962,7 +11938,7 @@ async function pushSessions(apiKey, agentName, sessions) {
11962
11938
  const c = getClient();
11963
11939
  if (!c) return;
11964
11940
  try {
11965
- await c.mutation(upsertSessionsFromPlugin, { apiKey, agentName, sessions });
11941
+ await c.mutation(upsertSessionsFromPlugin, { apiKeyHash: hashApiKey(apiKey), agentName, sessions });
11966
11942
  } catch (err) {
11967
11943
  getLogger().error(`cohort-sync: pushSessions failed: ${err}`);
11968
11944
  }
@@ -11972,7 +11948,7 @@ async function pushActivity(apiKey, entries) {
11972
11948
  const c = getClient();
11973
11949
  if (!c) return;
11974
11950
  try {
11975
- await c.mutation(pushActivityFromPluginRef, { apiKey, entries });
11951
+ await c.mutation(pushActivityFromPluginRef, { apiKeyHash: hashApiKey(apiKey), entries });
11976
11952
  } catch (err) {
11977
11953
  getLogger().error(`cohort-sync: pushActivity failed: ${err}`);
11978
11954
  }
@@ -11981,7 +11957,7 @@ async function pushCronSnapshot(apiKey, jobs) {
11981
11957
  const c = getClient();
11982
11958
  if (!c) return false;
11983
11959
  try {
11984
- await c.mutation(upsertCronSnapshotFromPluginRef, { apiKey, jobs });
11960
+ await c.mutation(upsertCronSnapshotFromPluginRef, { apiKeyHash: hashApiKey(apiKey), jobs });
11985
11961
  return true;
11986
11962
  } catch (err) {
11987
11963
  getLogger().error(`cohort-sync: pushCronSnapshot failed: ${err}`);
@@ -11994,7 +11970,7 @@ async function callAddCommentFromPlugin(apiKey, args) {
11994
11970
  throw new Error("Convex client not initialized \u2014 subscription may not be active");
11995
11971
  }
11996
11972
  return await c.mutation(addCommentFromPluginRef, {
11997
- apiKey,
11973
+ apiKeyHash: hashApiKey(apiKey),
11998
11974
  taskNumber: args.taskNumber,
11999
11975
  agentName: args.agentName,
12000
11976
  content: args.content,
@@ -12097,9 +12073,10 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
12097
12073
  const openclawAgentId = reverseNameMap[agentName] ?? agentName;
12098
12074
  logger.info(`cohort-sync: subscribing to notifications for agent "${agentName}" (openclawId: "${openclawAgentId}")`);
12099
12075
  let processing = false;
12076
+ const apiKeyHash = hashApiKey(cfg.apiKey);
12100
12077
  const unsubscribe = c.onUpdate(
12101
12078
  getUndeliveredForPlugin,
12102
- { agent: agentName, apiKey: cfg.apiKey },
12079
+ { agent: agentName, apiKeyHash },
12103
12080
  async (notifications) => {
12104
12081
  if (processing) return;
12105
12082
  processing = true;
@@ -12110,7 +12087,7 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
12110
12087
  logger.info(`cohort-sync: injected notification for task #${n.taskNumber} (${agentName})`);
12111
12088
  await c.mutation(markDeliveredByPlugin, {
12112
12089
  notificationId: n._id,
12113
- apiKey: cfg.apiKey
12090
+ apiKeyHash
12114
12091
  });
12115
12092
  } catch (err) {
12116
12093
  logger.warn(`cohort-sync: failed to inject notification ${n._id}: ${String(err)}`);
@@ -12134,9 +12111,10 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12134
12111
  return null;
12135
12112
  }
12136
12113
  let processing = false;
12114
+ const apiKeyHash = hashApiKey(cfg.apiKey);
12137
12115
  const unsubscribe = c.onUpdate(
12138
12116
  getPendingCommandsForPlugin,
12139
- { apiKey: cfg.apiKey },
12117
+ { apiKeyHash },
12140
12118
  async (commands) => {
12141
12119
  if (processing) return;
12142
12120
  if (commands.length === 0) return;
@@ -12147,7 +12125,7 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12147
12125
  try {
12148
12126
  await c.mutation(acknowledgeCommandRef, {
12149
12127
  commandId: cmd._id,
12150
- apiKey: cfg.apiKey
12128
+ apiKeyHash
12151
12129
  });
12152
12130
  await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger);
12153
12131
  if (cmd.type === "restart") return;
@@ -12156,7 +12134,7 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12156
12134
  try {
12157
12135
  await c.mutation(failCommandRef, {
12158
12136
  commandId: cmd._id,
12159
- apiKey: cfg.apiKey,
12137
+ apiKeyHash,
12160
12138
  reason: String(err).slice(0, 500)
12161
12139
  });
12162
12140
  } catch (failErr) {
@@ -12302,7 +12280,7 @@ function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
12302
12280
  clientId: "gateway-client",
12303
12281
  clientMode: "backend",
12304
12282
  role: "operator",
12305
- scopes: ["operator.read", "operator.write"],
12283
+ scopes: ["operator.read", "operator.write", "operator.admin"],
12306
12284
  signedAtMs,
12307
12285
  token,
12308
12286
  nonce,
@@ -12324,7 +12302,7 @@ function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
12324
12302
  mode: "backend"
12325
12303
  },
12326
12304
  role: "operator",
12327
- scopes: ["operator.read", "operator.write"],
12305
+ scopes: ["operator.read", "operator.write", "operator.admin"],
12328
12306
  auth: { token },
12329
12307
  device: {
12330
12308
  id: identity.deviceId,
@@ -13495,37 +13473,35 @@ async function fetchAgentContext(apiKey, apiUrl, logger) {
13495
13473
  return POCKET_GUIDE;
13496
13474
  }
13497
13475
  }
13498
- async function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13476
+ function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13499
13477
  const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
13500
- await client2.connect();
13501
13478
  persistentGwClient = client2;
13502
13479
  gwClientInitialized = true;
13503
- registerCronEventHandlers(client2, cfg, resolveAgentName);
13504
- if (client2.availableEvents.has("shutdown")) {
13505
- client2.on("shutdown", () => {
13506
- diag("GW_CLIENT_SHUTDOWN_EVENT", {});
13507
- logger.info("cohort-sync: gateway shutdown event received");
13508
- });
13509
- }
13510
- client2.onReconnect = async () => {
13511
- diag("GW_CLIENT_RECONNECTED_RESUBSCRIBE", {});
13480
+ const onConnected = async () => {
13481
+ diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
13482
+ logger.info(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
13512
13483
  registerCronEventHandlers(client2, cfg, resolveAgentName);
13484
+ if (client2.availableEvents.has("shutdown")) {
13485
+ client2.on("shutdown", () => {
13486
+ diag("GW_CLIENT_SHUTDOWN_EVENT", {});
13487
+ logger.info("cohort-sync: gateway shutdown event received");
13488
+ });
13489
+ }
13513
13490
  try {
13514
- const cronResult2 = await client2.request("cron.list", { includeDisabled: true });
13515
- const jobs2 = Array.isArray(cronResult2) ? cronResult2 : cronResult2?.jobs ?? [];
13516
- const mapped2 = jobs2.map((j) => mapCronJob(j, resolveAgentName));
13517
- await pushCronSnapshot(cfg.apiKey, mapped2);
13518
- diag("GW_CLIENT_RECONNECT_CRON_PUSH", { count: mapped2.length });
13491
+ const cronResult = await client2.request("cron.list", { includeDisabled: true });
13492
+ const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13493
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13494
+ await pushCronSnapshot(cfg.apiKey, mapped);
13495
+ diag("GW_CLIENT_CRON_PUSH", { count: mapped.length });
13519
13496
  } catch (err) {
13520
- diag("GW_CLIENT_RECONNECT_CRON_FAILED", { error: String(err) });
13497
+ diag("GW_CLIENT_CRON_PUSH_FAILED", { error: String(err) });
13521
13498
  }
13522
13499
  };
13523
- diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
13524
- const cronResult = await client2.request("cron.list", { includeDisabled: true });
13525
- const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13526
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13527
- await pushCronSnapshot(cfg.apiKey, mapped);
13528
- diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
13500
+ client2.onReconnect = onConnected;
13501
+ client2.connect().then(() => onConnected()).catch((err) => {
13502
+ diag("GW_CLIENT_INITIAL_CONNECT_DEFERRED", { error: String(err) });
13503
+ logger.warn(`cohort-sync: GW connect will retry: ${String(err)}`);
13504
+ });
13529
13505
  }
13530
13506
  function registerHooks(api, cfg) {
13531
13507
  STATE_FILE_PATH = path3.join(cfg.stateDir, "session-state.json");
@@ -13535,6 +13511,12 @@ function registerHooks(api, cfg) {
13535
13511
  const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
13536
13512
  createClient(convexUrl);
13537
13513
  setLogger(logger);
13514
+ gatewayPort = api.config?.gateway?.port ?? null;
13515
+ gatewayToken = resolveGatewayToken(api);
13516
+ if (gatewayPort && gatewayToken) {
13517
+ initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
13518
+ }
13519
+ const cronStorePath = api.config?.cron?.store ?? path3.join(os3.homedir(), ".openclaw", "cron", "jobs.json");
13538
13520
  logger.info(`cohort-sync: registerHooks v${PLUGIN_VERSION}`);
13539
13521
  diag("REGISTER_HOOKS", {
13540
13522
  PLUGIN_VERSION,
@@ -13873,6 +13855,19 @@ Do not attempt to make more comments until ${resetAt}.`
13873
13855
  tracker2.markTelemetryPushed(agentName);
13874
13856
  }
13875
13857
  }
13858
+ const sessionKey = ctx.sessionKey;
13859
+ if (sessionKey && sessionKey.includes(":cron:")) {
13860
+ try {
13861
+ const raw = fs3.readFileSync(cronStorePath, "utf8");
13862
+ const store = JSON.parse(raw);
13863
+ const jobs = store.jobs ?? [];
13864
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13865
+ await pushCronSnapshot(cfg.apiKey, mapped);
13866
+ diag("CRON_AGENT_END_PUSH", { count: mapped.length, sessionKey });
13867
+ } catch (err) {
13868
+ diag("CRON_AGENT_END_PUSH_FAILED", { error: String(err) });
13869
+ }
13870
+ }
13876
13871
  if (event.success === false) {
13877
13872
  const entry = buildActivityEntry(agentName, "agent_end", {
13878
13873
  success: false,
@@ -14352,14 +14347,19 @@ function registerCohortCli(ctx, cfg) {
14352
14347
 
14353
14348
  // index.ts
14354
14349
  init_keychain();
14355
- var DEFAULT_API_URL = "https://fortunate-chipmunk-286.convex.site";
14356
14350
  var plugin = {
14357
14351
  id: "cohort-sync",
14358
14352
  name: "Cohort Sync",
14359
14353
  description: "Syncs agent status and skills to Cohort dashboard",
14360
14354
  register(api) {
14361
14355
  const cfg = api.pluginConfig;
14362
- const apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
14356
+ const apiUrl = cfg?.apiUrl;
14357
+ if (!apiUrl) {
14358
+ api.logger.error(
14359
+ "cohort-sync: apiUrl is required in plugin config \u2014 set it in your OpenClaw configuration"
14360
+ );
14361
+ return;
14362
+ }
14363
14363
  api.registerCli(
14364
14364
  (ctx) => registerCohortCli(ctx, {
14365
14365
  apiUrl,
@@ -3,7 +3,7 @@
3
3
  "uiHints": {
4
4
  "apiUrl": {
5
5
  "label": "Cohort API URL",
6
- "placeholder": "https://fortunate-chipmunk-286.convex.site",
6
+ "placeholder": "https://api.cohort.bot",
7
7
  "help": "Base URL for the Cohort telemetry API"
8
8
  },
9
9
  "apiKey": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "convexUrl": {
27
27
  "label": "Convex WebSocket URL",
28
- "placeholder": "https://fortunate-chipmunk-286.convex.cloud",
28
+ "placeholder": "https://ws.cohort.bot",
29
29
  "advanced": true,
30
30
  "help": "Override the auto-derived Convex WebSocket URL (rarely needed)"
31
31
  }
@@ -54,5 +54,5 @@
54
54
  }
55
55
  }
56
56
  },
57
- "version": "0.8.0"
57
+ "version": "0.8.1"
58
58
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Syncs agent status and skills to 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.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",