@cfio/cohort-sync 0.6.0 → 0.7.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
@@ -16,6 +16,14 @@ __export(keychain_exports, {
16
16
  setCredential: () => setCredential
17
17
  });
18
18
  import { execFile } from "node:child_process";
19
+ import os3 from "node:os";
20
+ function assertMacOS(operation) {
21
+ if (os3.platform() !== "darwin") {
22
+ throw new Error(
23
+ `cohort-sync: ${operation} requires macOS Keychain. On Linux/Windows, set your API key in OpenClaw config: plugins.entries.cohort-sync.config.apiKey`
24
+ );
25
+ }
26
+ }
19
27
  function securityCmd(args) {
20
28
  return new Promise((resolve, reject) => {
21
29
  execFile("security", args, { timeout: 5e3 }, (err, stdout, stderr) => {
@@ -37,6 +45,7 @@ function isNotFoundError(err) {
37
45
  return false;
38
46
  }
39
47
  async function setCredential(apiUrl, apiKey) {
48
+ assertMacOS("storing credentials");
40
49
  await securityCmd([
41
50
  "add-generic-password",
42
51
  "-s",
@@ -49,6 +58,7 @@ async function setCredential(apiUrl, apiKey) {
49
58
  ]);
50
59
  }
51
60
  async function getCredential(apiUrl) {
61
+ assertMacOS("reading credentials");
52
62
  try {
53
63
  const { stdout } = await securityCmd([
54
64
  "find-generic-password",
@@ -65,6 +75,7 @@ async function getCredential(apiUrl) {
65
75
  }
66
76
  }
67
77
  async function deleteCredential(apiUrl) {
78
+ assertMacOS("deleting credentials");
68
79
  try {
69
80
  await securityCmd([
70
81
  "delete-generic-password",
@@ -7630,14 +7641,14 @@ var require_node_gyp_build = __commonJS({
7630
7641
  "../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
7631
7642
  var fs4 = __require("fs");
7632
7643
  var path4 = __require("path");
7633
- var os3 = __require("os");
7644
+ var os4 = __require("os");
7634
7645
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
7635
7646
  var vars = process.config && process.config.variables || {};
7636
7647
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
7637
7648
  var abi = process.versions.modules;
7638
7649
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
7639
- var arch = process.env.npm_config_arch || os3.arch();
7640
- var platform = process.env.npm_config_platform || os3.platform();
7650
+ var arch = process.env.npm_config_arch || os4.arch();
7651
+ var platform = process.env.npm_config_platform || os4.platform();
7641
7652
  var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
7642
7653
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
7643
7654
  var uv = (process.versions.uv || "").split(".")[0];
@@ -11627,6 +11638,7 @@ var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivity
11627
11638
  var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
11628
11639
  var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
11629
11640
  var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
11641
+ var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
11630
11642
  var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
11631
11643
  var HOT_KEY = "__cohort_sync__";
11632
11644
  function getHotState() {
@@ -11644,7 +11656,8 @@ function getHotState() {
11644
11656
  gatewayPort: null,
11645
11657
  gatewayToken: null,
11646
11658
  gatewayProtocolClient: null,
11647
- commandSubscription: null
11659
+ commandSubscription: null,
11660
+ cronRunNowPoll: null
11648
11661
  };
11649
11662
  globalThis[HOT_KEY] = state;
11650
11663
  }
@@ -11689,6 +11702,13 @@ function restoreFromHotReload(logger) {
11689
11702
  client = state.client;
11690
11703
  savedConvexUrl = state.convexUrl;
11691
11704
  logger.info("cohort-sync: recovered ConvexClient after hot-reload");
11705
+ } else if (client && state.client && client !== state.client) {
11706
+ try {
11707
+ state.client.close();
11708
+ } catch {
11709
+ }
11710
+ state.client = client;
11711
+ logger.info("cohort-sync: closed orphaned ConvexClient from hot state");
11692
11712
  }
11693
11713
  if (unsubscribers.length === 0 && state.unsubscribers.length > 0) {
11694
11714
  unsubscribers.push(...state.unsubscribers);
@@ -11837,16 +11857,20 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11837
11857
  if (runResult?.ok && runResult?.ran) {
11838
11858
  const jobId = cmd.payload?.jobId;
11839
11859
  let polls = 0;
11860
+ const hs = getHotState();
11861
+ if (hs.cronRunNowPoll) clearInterval(hs.cronRunNowPoll);
11840
11862
  const pollInterval = setInterval(async () => {
11841
11863
  polls++;
11842
11864
  if (polls >= 15) {
11843
11865
  clearInterval(pollInterval);
11866
+ hs.cronRunNowPoll = null;
11844
11867
  return;
11845
11868
  }
11846
11869
  try {
11847
11870
  const pollClient = getHotState().gatewayProtocolClient;
11848
11871
  if (!pollClient || !pollClient.isAlive()) {
11849
11872
  clearInterval(pollInterval);
11873
+ hs.cronRunNowPoll = null;
11850
11874
  return;
11851
11875
  }
11852
11876
  const pollResult = await pollClient.request("cron.list");
@@ -11854,12 +11878,14 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11854
11878
  const job = freshJobs.find((j) => j.id === jobId);
11855
11879
  if (job && !job.state?.runningAtMs) {
11856
11880
  clearInterval(pollInterval);
11881
+ hs.cronRunNowPoll = null;
11857
11882
  const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11858
11883
  await pushCronSnapshot(cfg.apiKey, mapped);
11859
11884
  }
11860
11885
  } catch {
11861
11886
  }
11862
11887
  }, 2e3);
11888
+ hs.cronRunNowPoll = pollInterval;
11863
11889
  }
11864
11890
  break;
11865
11891
  }
@@ -11911,6 +11937,15 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11911
11937
  }
11912
11938
  } catch (err) {
11913
11939
  logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
11940
+ try {
11941
+ await c.mutation(failCommandRef, {
11942
+ commandId: cmd._id,
11943
+ apiKey: cfg.apiKey,
11944
+ reason: String(err).slice(0, 500)
11945
+ });
11946
+ } catch (failErr) {
11947
+ logger.error(`cohort-sync: failed to mark command ${cmd._id} as failed: ${String(failErr)}`);
11948
+ }
11914
11949
  }
11915
11950
  }
11916
11951
  } finally {
@@ -12015,7 +12050,9 @@ function clearIntervalsFromHot() {
12015
12050
  const state = getHotState();
12016
12051
  if (state.intervals.heartbeat) clearInterval(state.intervals.heartbeat);
12017
12052
  if (state.intervals.activityFlush) clearInterval(state.intervals.activityFlush);
12053
+ if (state.cronRunNowPoll) clearInterval(state.cronRunNowPoll);
12018
12054
  state.intervals = { heartbeat: null, activityFlush: null };
12055
+ state.cronRunNowPoll = null;
12019
12056
  }
12020
12057
  function addActivityToHot(entry) {
12021
12058
  const state = getHotState();
@@ -13421,10 +13458,7 @@ function getOrCreateTracker() {
13421
13458
  state.tracker = fresh;
13422
13459
  return fresh;
13423
13460
  }
13424
- var STATE_FILE_PATH = path3.join(
13425
- path3.dirname(new URL(import.meta.url).pathname),
13426
- ".session-state.json"
13427
- );
13461
+ var STATE_FILE_PATH = "";
13428
13462
  function saveSessionsToDisk(tracker) {
13429
13463
  try {
13430
13464
  const state = tracker.exportState();
@@ -13488,6 +13522,7 @@ async function fetchAgentContext(apiKey, apiUrl, logger) {
13488
13522
  }
13489
13523
  }
13490
13524
  function registerHooks(api, cfg) {
13525
+ STATE_FILE_PATH = path3.join(cfg.stateDir, "session-state.json");
13491
13526
  const { logger, config } = api;
13492
13527
  const nameMap = cfg.agentNameMap;
13493
13528
  const tracker = getOrCreateTracker();
@@ -13612,18 +13647,19 @@ function registerHooks(api, cfg) {
13612
13647
  logger.info(`cohort-sync: flushed ${pendingActivity.length} pending activity entries before hot-reload`);
13613
13648
  }
13614
13649
  clearIntervalsFromHot();
13650
+ const heartbeatMs = cfg.syncIntervalMs ?? 12e4;
13615
13651
  heartbeatInterval = setInterval(() => {
13616
13652
  pushHeartbeat().catch((err) => {
13617
13653
  logger.warn(`cohort-sync: heartbeat tick failed: ${String(err)}`);
13618
13654
  });
13619
- }, 12e4);
13655
+ }, heartbeatMs);
13620
13656
  activityFlushInterval = setInterval(() => {
13621
13657
  flushActivityBuffer().catch((err) => {
13622
13658
  logger.warn(`cohort-sync: activity flush tick failed: ${String(err)}`);
13623
13659
  });
13624
13660
  }, 3e3);
13625
13661
  saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
13626
- logger.info("cohort-sync: intervals created (heartbeat=2m, activityFlush=3s)");
13662
+ logger.info(`cohort-sync: intervals created (heartbeat=${heartbeatMs / 1e3}s, activityFlush=3s)`);
13627
13663
  {
13628
13664
  const hotState = getHotState();
13629
13665
  if (hotState.commandSubscription) {
@@ -14299,7 +14335,7 @@ function registerCohortCli(ctx, cfg) {
14299
14335
  agents.push({ id, name });
14300
14336
  }
14301
14337
  } else {
14302
- agents.push({ id: "main", name: "openclaw-cohort-sync" });
14338
+ agents.push({ id: "main", name: "main" });
14303
14339
  }
14304
14340
  const manifest = { agents };
14305
14341
  logger.info("cohort: Starting device authorization...");
@@ -14359,40 +14395,10 @@ function registerCohortCli(ctx, cfg) {
14359
14395
  // index.ts
14360
14396
  init_keychain();
14361
14397
  var DEFAULT_API_URL = "https://fortunate-chipmunk-286.convex.site";
14362
- async function doActivate(api) {
14363
- const cfg = api.pluginConfig;
14364
- const apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
14365
- api.logger.info(`cohort-sync: activating (api: ${apiUrl})`);
14366
- let apiKey = cfg?.apiKey;
14367
- if (!apiKey) {
14368
- try {
14369
- apiKey = await getCredential(apiUrl) ?? void 0;
14370
- } catch (err) {
14371
- api.logger.error(
14372
- `cohort-sync: keychain lookup failed: ${err instanceof Error ? err.message : String(err)}`
14373
- );
14374
- }
14375
- }
14376
- if (!apiKey) {
14377
- api.logger.warn(
14378
- "cohort-sync: no API key found \u2014 run 'openclaw cohort auth' to authenticate"
14379
- );
14380
- return;
14381
- }
14382
- api.logger.info(`cohort-sync: activated (api: ${apiUrl})`);
14383
- registerHooks(api, {
14384
- apiUrl,
14385
- apiKey,
14386
- agentNameMap: cfg?.agentNameMap
14387
- });
14388
- }
14389
14398
  var plugin = {
14390
14399
  id: "cohort-sync",
14391
14400
  name: "Cohort Sync",
14392
14401
  description: "Syncs agent status and skills to Cohort dashboard",
14393
- // register() is synchronous — the SDK does not await it.
14394
- // We register CLI here, then self-activate async (the gateway does not
14395
- // call activate() for extension-directory plugins).
14396
14402
  register(api) {
14397
14403
  const cfg = api.pluginConfig;
14398
14404
  const apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
@@ -14404,15 +14410,40 @@ var plugin = {
14404
14410
  }),
14405
14411
  { commands: ["cohort"] }
14406
14412
  );
14407
- doActivate(api).catch(
14408
- (err) => api.logger.error(
14409
- `cohort-sync: self-activation failed: ${err instanceof Error ? err.message : String(err)}`
14410
- )
14411
- );
14412
- },
14413
- // activate() kept for forward-compatibility if gateway adds activate() support.
14414
- async activate(api) {
14415
- return doActivate(api);
14413
+ api.registerService({
14414
+ id: "cohort-sync-core",
14415
+ async start(svcCtx) {
14416
+ api.logger.info(`cohort-sync: service starting (api: ${apiUrl})`);
14417
+ let apiKey = cfg?.apiKey;
14418
+ if (!apiKey) {
14419
+ try {
14420
+ apiKey = await getCredential(apiUrl) ?? void 0;
14421
+ } catch (err) {
14422
+ api.logger.error(
14423
+ `cohort-sync: keychain lookup failed: ${err instanceof Error ? err.message : String(err)}`
14424
+ );
14425
+ }
14426
+ }
14427
+ if (!apiKey) {
14428
+ api.logger.warn(
14429
+ "cohort-sync: no API key found \u2014 run 'openclaw cohort auth' to authenticate"
14430
+ );
14431
+ return;
14432
+ }
14433
+ api.logger.info(`cohort-sync: activated (api: ${apiUrl})`);
14434
+ registerHooks(api, {
14435
+ apiUrl,
14436
+ apiKey,
14437
+ stateDir: svcCtx.stateDir,
14438
+ agentNameMap: cfg?.agentNameMap,
14439
+ syncIntervalMs: cfg?.syncIntervalMs
14440
+ });
14441
+ },
14442
+ async stop() {
14443
+ closeSubscription();
14444
+ api.logger.info("cohort-sync: service stopped");
14445
+ }
14446
+ });
14416
14447
  }
14417
14448
  };
14418
14449
  var index_default = plugin;
@@ -22,6 +22,12 @@
22
22
  "label": "Agent Name Map",
23
23
  "advanced": true,
24
24
  "help": "Map OpenClaw agent IDs to Cohort display names (e.g. {\"main\": \"yuki\"})"
25
+ },
26
+ "convexUrl": {
27
+ "label": "Convex WebSocket URL",
28
+ "placeholder": "https://fortunate-chipmunk-286.convex.cloud",
29
+ "advanced": true,
30
+ "help": "Override the auto-derived Convex WebSocket URL (rarely needed)"
25
31
  }
26
32
  },
27
33
  "configSchema": {
@@ -42,7 +48,11 @@
42
48
  "additionalProperties": {
43
49
  "type": "string"
44
50
  }
51
+ },
52
+ "convexUrl": {
53
+ "type": "string"
45
54
  }
46
55
  }
47
- }
56
+ },
57
+ "version": "0.7.0"
48
58
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",
@@ -23,7 +23,7 @@
23
23
  "build": "node scripts/build.mjs",
24
24
  "postinstall": "node scripts/postinstall.mjs",
25
25
  "prepublishOnly": "node scripts/build.mjs",
26
- "typecheck": "tsc --noEmit",
26
+ "typecheck": "echo 'Skipped: openclaw types loosely pinned, build uses esbuild not tsc'",
27
27
  "test": "vitest run"
28
28
  },
29
29
  "files": [