@cfio/cohort-sync 0.4.8 → 0.6.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
@@ -88,9 +88,8 @@ var init_keychain = __esm({
88
88
  });
89
89
 
90
90
  // src/hooks.ts
91
- import fs2 from "node:fs";
92
- import os2 from "node:os";
93
- import path2 from "node:path";
91
+ import fs3 from "node:fs";
92
+ import path3 from "node:path";
94
93
 
95
94
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
96
95
  var value_exports = {};
@@ -2700,9 +2699,6 @@ var Type = type_exports2;
2700
2699
 
2701
2700
  // src/sync.ts
2702
2701
  import { execSync } from "node:child_process";
2703
- import fs from "node:fs";
2704
- import os from "node:os";
2705
- import path from "node:path";
2706
2702
 
2707
2703
  // ../../node_modules/.pnpm/convex@1.33.0_patch_hash=l43bztwr6e2lbmpd6ao6hmcg24_react@19.2.1/node_modules/convex/dist/esm/index.js
2708
2704
  var version = "1.33.0";
@@ -4558,12 +4554,12 @@ function createApi(pathParts = []) {
4558
4554
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
4559
4555
  );
4560
4556
  }
4561
- const path3 = pathParts.slice(0, -1).join("/");
4557
+ const path4 = pathParts.slice(0, -1).join("/");
4562
4558
  const exportName = pathParts[pathParts.length - 1];
4563
4559
  if (exportName === "default") {
4564
- return path3;
4560
+ return path4;
4565
4561
  } else {
4566
- return path3 + ":" + exportName;
4562
+ return path4 + ":" + exportName;
4567
4563
  }
4568
4564
  } else if (prop === Symbol.toStringTag) {
4569
4565
  return "FunctionReference";
@@ -7632,8 +7628,8 @@ var require_constants = __commonJS({
7632
7628
  });
7633
7629
  var require_node_gyp_build = __commonJS({
7634
7630
  "../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
7635
- var fs3 = __require("fs");
7636
- var path3 = __require("path");
7631
+ var fs4 = __require("fs");
7632
+ var path4 = __require("path");
7637
7633
  var os3 = __require("os");
7638
7634
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
7639
7635
  var vars = process.config && process.config.variables || {};
@@ -7650,21 +7646,21 @@ var require_node_gyp_build = __commonJS({
7650
7646
  return runtimeRequire(load.resolve(dir));
7651
7647
  }
7652
7648
  load.resolve = load.path = function(dir) {
7653
- dir = path3.resolve(dir || ".");
7649
+ dir = path4.resolve(dir || ".");
7654
7650
  try {
7655
- var name = runtimeRequire(path3.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7651
+ var name = runtimeRequire(path4.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7656
7652
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
7657
7653
  } catch (err) {
7658
7654
  }
7659
7655
  if (!prebuildsOnly) {
7660
- var release = getFirst(path3.join(dir, "build/Release"), matchBuild);
7656
+ var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
7661
7657
  if (release) return release;
7662
- var debug = getFirst(path3.join(dir, "build/Debug"), matchBuild);
7658
+ var debug = getFirst(path4.join(dir, "build/Debug"), matchBuild);
7663
7659
  if (debug) return debug;
7664
7660
  }
7665
7661
  var prebuild = resolve(dir);
7666
7662
  if (prebuild) return prebuild;
7667
- var nearby = resolve(path3.dirname(process.execPath));
7663
+ var nearby = resolve(path4.dirname(process.execPath));
7668
7664
  if (nearby) return nearby;
7669
7665
  var target = [
7670
7666
  "platform=" + platform,
@@ -7681,26 +7677,26 @@ var require_node_gyp_build = __commonJS({
7681
7677
  ].filter(Boolean).join(" ");
7682
7678
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
7683
7679
  function resolve(dir2) {
7684
- var tuples = readdirSync(path3.join(dir2, "prebuilds")).map(parseTuple);
7680
+ var tuples = readdirSync(path4.join(dir2, "prebuilds")).map(parseTuple);
7685
7681
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
7686
7682
  if (!tuple) return;
7687
- var prebuilds = path3.join(dir2, "prebuilds", tuple.name);
7683
+ var prebuilds = path4.join(dir2, "prebuilds", tuple.name);
7688
7684
  var parsed = readdirSync(prebuilds).map(parseTags);
7689
7685
  var candidates = parsed.filter(matchTags(runtime, abi));
7690
7686
  var winner = candidates.sort(compareTags(runtime))[0];
7691
- if (winner) return path3.join(prebuilds, winner.file);
7687
+ if (winner) return path4.join(prebuilds, winner.file);
7692
7688
  }
7693
7689
  };
7694
7690
  function readdirSync(dir) {
7695
7691
  try {
7696
- return fs3.readdirSync(dir);
7692
+ return fs4.readdirSync(dir);
7697
7693
  } catch (err) {
7698
7694
  return [];
7699
7695
  }
7700
7696
  }
7701
7697
  function getFirst(dir, filter) {
7702
7698
  var files = readdirSync(dir).filter(filter);
7703
- return files[0] && path3.join(dir, files[0]);
7699
+ return files[0] && path4.join(dir, files[0]);
7704
7700
  }
7705
7701
  function matchBuild(name) {
7706
7702
  return /\.node$/.test(name);
@@ -7787,7 +7783,7 @@ var require_node_gyp_build = __commonJS({
7787
7783
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
7788
7784
  }
7789
7785
  function isAlpine(platform2) {
7790
- return platform2 === "linux" && fs3.existsSync("/etc/alpine-release");
7786
+ return platform2 === "linux" && fs4.existsSync("/etc/alpine-release");
7791
7787
  }
7792
7788
  load.parseTags = parseTags;
7793
7789
  load.matchTags = matchTags;
@@ -11506,8 +11502,47 @@ var _systemSchema = defineSchema({
11506
11502
  })
11507
11503
  });
11508
11504
 
11505
+ // src/cron-mapping.ts
11506
+ function formatSchedule(s) {
11507
+ switch (s.kind) {
11508
+ case "cron":
11509
+ return s.expr ?? "";
11510
+ case "every":
11511
+ return `every ${humanizeMs(s.everyMs ?? 0)}`;
11512
+ case "at":
11513
+ return `once at ${s.at ?? ""}`;
11514
+ default:
11515
+ return s.kind;
11516
+ }
11517
+ }
11518
+ function humanizeMs(ms) {
11519
+ if (ms >= 864e5) return `${Math.round(ms / 864e5)}d`;
11520
+ if (ms >= 36e5) return `${Math.round(ms / 36e5)}h`;
11521
+ if (ms >= 6e4) return `${Math.round(ms / 6e4)}m`;
11522
+ return `${Math.round(ms / 1e3)}s`;
11523
+ }
11524
+ function mapCronJob(job, resolveAgentName) {
11525
+ return {
11526
+ id: job.id,
11527
+ text: job.name,
11528
+ schedule: formatSchedule(job.schedule),
11529
+ enabled: job.enabled,
11530
+ agentId: resolveAgentName(job.agentId ?? "main"),
11531
+ nextRun: job.state?.nextRunAtMs,
11532
+ lastRun: job.state?.lastRunAtMs,
11533
+ lastStatus: job.state?.lastRunStatus,
11534
+ lastError: job.state?.lastError ?? null
11535
+ };
11536
+ }
11537
+ function reverseResolveAgentName(cohortName, forwardMap) {
11538
+ const lower = cohortName.toLowerCase();
11539
+ for (const [openclawId, name] of Object.entries(forwardMap)) {
11540
+ if (name.toLowerCase() === lower) return openclawId;
11541
+ }
11542
+ return cohortName;
11543
+ }
11544
+
11509
11545
  // src/subscription.ts
11510
- import { execFileSync } from "node:child_process";
11511
11546
  function deriveConvexUrl(apiUrl) {
11512
11547
  return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
11513
11548
  }
@@ -11605,7 +11640,11 @@ function getHotState() {
11605
11640
  lastKnownRoster: [],
11606
11641
  intervals: { heartbeat: null, activityFlush: null },
11607
11642
  activityBuffer: [],
11608
- channelAgentBridge: {}
11643
+ channelAgentBridge: {},
11644
+ gatewayPort: null,
11645
+ gatewayToken: null,
11646
+ gatewayProtocolClient: null,
11647
+ commandSubscription: null
11609
11648
  };
11610
11649
  globalThis[HOT_KEY] = state;
11611
11650
  }
@@ -11736,7 +11775,7 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11736
11775
  const c = getOrCreateClient();
11737
11776
  if (!c) {
11738
11777
  logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
11739
- return;
11778
+ return null;
11740
11779
  }
11741
11780
  let processing = false;
11742
11781
  const unsubscribe = c.onUpdate(
@@ -11760,19 +11799,113 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11760
11799
  process.kill(process.pid, "SIGTERM");
11761
11800
  return;
11762
11801
  }
11763
- if (cmd.type.startsWith("cron") && cmd.payload?.jobId) {
11764
- handleCronCommand(cmd.type, cmd.payload.jobId, logger);
11802
+ if (cmd.type.startsWith("cron")) {
11803
+ const hotState = getHotState();
11804
+ const port = hotState.gatewayPort;
11805
+ const token = hotState.gatewayToken;
11806
+ if (!port || !token) {
11807
+ logger.warn(`cohort-sync: no gateway port/token, cannot execute ${cmd.type}`);
11808
+ continue;
11809
+ }
11810
+ const gwClient = new GatewayClient(port, token, logger);
11765
11811
  try {
11766
- const freshJobs = fetchCronJobs(logger);
11767
- if (freshJobs !== null) {
11768
- const resolvedJobs = resolveAgentName ? freshJobs.map((j) => ({ ...j, agentId: j.agentId ? resolveAgentName(j.agentId) : j.agentId })) : freshJobs;
11769
- await pushCronSnapshot(cfg.apiKey, resolvedJobs);
11812
+ await gwClient.connect();
11813
+ const nameMap = cfg.agentNameMap ?? {};
11814
+ switch (cmd.type) {
11815
+ case "cronEnable":
11816
+ await gwClient.request("cron.update", {
11817
+ jobId: cmd.payload?.jobId,
11818
+ patch: { enabled: true }
11819
+ });
11820
+ break;
11821
+ case "cronDisable":
11822
+ await gwClient.request("cron.update", {
11823
+ jobId: cmd.payload?.jobId,
11824
+ patch: { enabled: false }
11825
+ });
11826
+ break;
11827
+ case "cronDelete":
11828
+ await gwClient.request("cron.remove", {
11829
+ jobId: cmd.payload?.jobId
11830
+ });
11831
+ break;
11832
+ case "cronRunNow": {
11833
+ const runResult = await gwClient.request(
11834
+ "cron.run",
11835
+ { jobId: cmd.payload?.jobId }
11836
+ );
11837
+ if (runResult?.ok && runResult?.ran) {
11838
+ const jobId = cmd.payload?.jobId;
11839
+ let polls = 0;
11840
+ const pollInterval = setInterval(async () => {
11841
+ polls++;
11842
+ if (polls >= 15) {
11843
+ clearInterval(pollInterval);
11844
+ return;
11845
+ }
11846
+ try {
11847
+ const pollClient = getHotState().gatewayProtocolClient;
11848
+ if (!pollClient || !pollClient.isAlive()) {
11849
+ clearInterval(pollInterval);
11850
+ return;
11851
+ }
11852
+ const pollResult = await pollClient.request("cron.list");
11853
+ const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
11854
+ const job = freshJobs.find((j) => j.id === jobId);
11855
+ if (job && !job.state?.runningAtMs) {
11856
+ clearInterval(pollInterval);
11857
+ const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11858
+ await pushCronSnapshot(cfg.apiKey, mapped);
11859
+ }
11860
+ } catch {
11861
+ }
11862
+ }, 2e3);
11863
+ }
11864
+ break;
11865
+ }
11866
+ case "cronCreate": {
11867
+ const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
11868
+ await gwClient.request("cron.add", {
11869
+ job: {
11870
+ agentId,
11871
+ name: cmd.payload?.name,
11872
+ enabled: true,
11873
+ schedule: cmd.payload?.schedule,
11874
+ payload: { kind: "agentTurn", message: cmd.payload?.message },
11875
+ sessionTarget: "isolated",
11876
+ wakeMode: "now"
11877
+ }
11878
+ });
11879
+ break;
11880
+ }
11881
+ case "cronUpdate": {
11882
+ const patch = {};
11883
+ if (cmd.payload?.name) patch.name = cmd.payload.name;
11884
+ if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
11885
+ if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
11886
+ if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
11887
+ await gwClient.request("cron.update", {
11888
+ jobId: cmd.payload?.jobId,
11889
+ patch
11890
+ });
11891
+ break;
11892
+ }
11893
+ default:
11894
+ logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
11895
+ }
11896
+ if (gwClient.isAlive()) {
11897
+ try {
11898
+ const snapResult = await gwClient.request("cron.list");
11899
+ const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
11900
+ const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11901
+ await pushCronSnapshot(cfg.apiKey, mapped);
11902
+ } catch (snapErr) {
11903
+ logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
11904
+ }
11770
11905
  }
11771
- } catch (snapErr) {
11772
- logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
11906
+ } finally {
11907
+ gwClient.close();
11773
11908
  }
11774
- } else if (cmd.type.startsWith("cron")) {
11775
- logger.warn(`cohort-sync: cron command missing jobId: ${cmd.type}`);
11776
11909
  } else {
11777
11910
  logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
11778
11911
  }
@@ -11788,34 +11921,8 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11788
11921
  logger.error(`cohort-sync: command subscription error: ${String(err)}`);
11789
11922
  }
11790
11923
  );
11791
- unsubscribers.push(unsubscribe);
11792
- getHotState().unsubscribers = [...unsubscribers];
11793
11924
  logger.info("cohort-sync: command subscription active");
11794
- }
11795
- function handleCronCommand(type, jobId, logger) {
11796
- const subcommandMap = {
11797
- cronEnable: ["cron", "enable", jobId],
11798
- cronDisable: ["cron", "disable", jobId],
11799
- cronRunNow: ["cron", "run", jobId],
11800
- cronDelete: ["cron", "delete", jobId, "--force"]
11801
- };
11802
- const args = subcommandMap[type];
11803
- if (!args) {
11804
- logger.warn(`cohort-sync: unknown cron command type: ${type}`);
11805
- return;
11806
- }
11807
- const timeout = type === "cronRunNow" ? 3e4 : 15e3;
11808
- try {
11809
- execFileSync("openclaw", args, {
11810
- encoding: "utf8",
11811
- timeout,
11812
- stdio: ["ignore", "pipe", "ignore"]
11813
- });
11814
- logger.info(`cohort-sync: cron command ${type} executed for job ${jobId}`);
11815
- } catch (err) {
11816
- logger.error(`cohort-sync: cron command ${type} failed for job ${jobId}: ${String(err)}`);
11817
- throw err;
11818
- }
11925
+ return unsubscribe;
11819
11926
  }
11820
11927
  async function callAddCommentFromPlugin(apiKey, args) {
11821
11928
  const c = getOrCreateClient();
@@ -11845,6 +11952,20 @@ function closeSubscription() {
11845
11952
  } catch {
11846
11953
  }
11847
11954
  }
11955
+ if (state.commandSubscription) {
11956
+ try {
11957
+ state.commandSubscription();
11958
+ } catch {
11959
+ }
11960
+ state.commandSubscription = null;
11961
+ }
11962
+ if (state.gatewayProtocolClient) {
11963
+ try {
11964
+ state.gatewayProtocolClient.close();
11965
+ } catch {
11966
+ }
11967
+ state.gatewayProtocolClient = null;
11968
+ }
11848
11969
  client?.close();
11849
11970
  client = null;
11850
11971
  clearHotState();
@@ -11918,10 +12039,6 @@ function getChannelAgent(channelId) {
11918
12039
  }
11919
12040
 
11920
12041
  // src/sync.ts
11921
- var cronStorePath = null;
11922
- function setCronStorePath(p) {
11923
- cronStorePath = p;
11924
- }
11925
12042
  function extractJson(raw) {
11926
12043
  const jsonStart = raw.search(/[\[{]/);
11927
12044
  const jsonEndBracket = raw.lastIndexOf("]");
@@ -11953,73 +12070,35 @@ function fetchSkills(logger) {
11953
12070
  return [];
11954
12071
  }
11955
12072
  }
11956
- function fetchCronJobs(logger) {
11957
- try {
11958
- const storePath = cronStorePath ?? path.join(os.homedir(), ".openclaw", "cron", "jobs.json");
11959
- if (!fs.existsSync(storePath)) {
11960
- logger.warn(`cohort-sync: cron store not found at ${storePath}`);
11961
- return null;
11962
- }
11963
- const raw = fs.readFileSync(storePath, "utf-8");
11964
- const parsed = JSON.parse(raw);
11965
- const jobs = Array.isArray(parsed?.jobs) ? parsed.jobs : [];
11966
- return jobs.map((j) => ({
11967
- id: String(j.id ?? "unknown"),
11968
- text: String(j.name ?? ""),
11969
- schedule: formatSchedule(j.schedule),
11970
- ...j.state?.nextRunAtMs != null ? { nextRun: Number(j.state.nextRunAtMs) } : {},
11971
- ...j.state?.lastRunAtMs != null ? { lastRun: Number(j.state.lastRunAtMs) } : {},
11972
- ...j.state?.lastStatus ? { lastStatus: String(j.state.lastStatus) } : {},
11973
- enabled: j.enabled !== false,
11974
- ...j.agentId != null ? { agentId: String(j.agentId) } : {}
11975
- }));
11976
- } catch (err) {
11977
- logger.warn(`cohort-sync: failed to read cron store: ${String(err)}`);
11978
- return null;
11979
- }
11980
- }
11981
- function formatSchedule(schedule) {
11982
- if (typeof schedule === "string") return schedule;
11983
- if (schedule && typeof schedule === "object") {
11984
- const s = schedule;
11985
- if (s.kind === "cron" && typeof s.expr === "string") return s.expr;
11986
- if (s.kind === "interval" && typeof s.everyMs === "number") {
11987
- const mins = Math.floor(Number(s.everyMs) / 6e4);
11988
- if (mins > 0) return `*/${mins} * * * *`;
11989
- return `every ${s.everyMs}ms`;
11990
- }
11991
- }
11992
- return String(schedule);
11993
- }
11994
12073
  var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
11995
12074
  function normalizeStatus(status) {
11996
12075
  return VALID_STATUSES.has(status) ? status : "idle";
11997
12076
  }
11998
- async function v1Get(apiUrl, apiKey, path3) {
11999
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path3}`, {
12077
+ async function v1Get(apiUrl, apiKey, path4) {
12078
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
12000
12079
  headers: { Authorization: `Bearer ${apiKey}` },
12001
12080
  signal: AbortSignal.timeout(1e4)
12002
12081
  });
12003
- if (!res.ok) throw new Error(`GET ${path3} \u2192 ${res.status}`);
12082
+ if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
12004
12083
  return res.json();
12005
12084
  }
12006
- async function v1Patch(apiUrl, apiKey, path3, body) {
12007
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path3}`, {
12085
+ async function v1Patch(apiUrl, apiKey, path4, body) {
12086
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
12008
12087
  method: "PATCH",
12009
12088
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
12010
12089
  body: JSON.stringify(body),
12011
12090
  signal: AbortSignal.timeout(1e4)
12012
12091
  });
12013
- if (!res.ok) throw new Error(`PATCH ${path3} \u2192 ${res.status}`);
12092
+ if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
12014
12093
  }
12015
- async function v1Post(apiUrl, apiKey, path3, body) {
12016
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path3}`, {
12094
+ async function v1Post(apiUrl, apiKey, path4, body) {
12095
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
12017
12096
  method: "POST",
12018
12097
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
12019
12098
  body: JSON.stringify(body),
12020
12099
  signal: AbortSignal.timeout(1e4)
12021
12100
  });
12022
- if (!res.ok) throw new Error(`POST ${path3} \u2192 ${res.status}`);
12101
+ if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
12023
12102
  }
12024
12103
  async function checkForUpdate(currentVersion, logger) {
12025
12104
  try {
@@ -12187,6 +12266,543 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
12187
12266
  logger.info("cohort-sync: full sync complete");
12188
12267
  }
12189
12268
 
12269
+ // src/gateway-client.ts
12270
+ import crypto2 from "node:crypto";
12271
+
12272
+ // src/diag.ts
12273
+ import fs from "node:fs";
12274
+ import path from "node:path";
12275
+ import os from "node:os";
12276
+ var LOG_DIR = path.join(os.homedir(), ".openclaw", "logs", "cohort-sync");
12277
+ var LOG_PATH = path.join(LOG_DIR, "diag.log");
12278
+ var LOG_PATH_ROTATED = path.join(LOG_DIR, "diag.log.1");
12279
+ var MAX_LOG_SIZE = 5 * 1024 * 1024;
12280
+ var isDebug = process.env.COHORT_SYNC_DEBUG === "1";
12281
+ try {
12282
+ fs.mkdirSync(LOG_DIR, { recursive: true, mode: 448 });
12283
+ } catch {
12284
+ }
12285
+ var writesSinceCheck = 0;
12286
+ function diag(label, data) {
12287
+ if (!isDebug && !label.startsWith("HOOK_") && !label.startsWith("MODULE_") && !label.startsWith("REGISTER_") && !label.startsWith("GW_WS_AUTH") && !label.startsWith("GW_WS_CLOSED") && !label.startsWith("GW_WS_ERROR") && !label.startsWith("GW_CLIENT_") && !label.startsWith("HEARTBEAT_CRON")) {
12288
+ return;
12289
+ }
12290
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
12291
+ const sanitized = data ? " " + JSON.stringify(data, (_k, v2) => {
12292
+ if (typeof v2 === "string" && v2.length > 200) return v2.slice(0, 200) + "\u2026";
12293
+ return v2;
12294
+ }) : "";
12295
+ const line = `[${ts}] ${label}${sanitized}
12296
+ `;
12297
+ try {
12298
+ fs.appendFileSync(LOG_PATH, line, { mode: 384 });
12299
+ if (++writesSinceCheck >= 100) {
12300
+ writesSinceCheck = 0;
12301
+ const stat = fs.statSync(LOG_PATH);
12302
+ if (stat.size > MAX_LOG_SIZE) {
12303
+ try {
12304
+ fs.unlinkSync(LOG_PATH_ROTATED);
12305
+ } catch {
12306
+ }
12307
+ fs.renameSync(LOG_PATH, LOG_PATH_ROTATED);
12308
+ }
12309
+ }
12310
+ } catch {
12311
+ }
12312
+ }
12313
+
12314
+ // src/device-identity-crypto.ts
12315
+ import crypto from "node:crypto";
12316
+ import fs2 from "node:fs";
12317
+ import path2 from "node:path";
12318
+ import os2 from "node:os";
12319
+ var IDENTITY_PATH = path2.join(os2.homedir(), ".openclaw", "extensions", "cohort-sync", ".device-identity.json");
12320
+ function loadOrCreateDeviceIdentity() {
12321
+ try {
12322
+ const data = JSON.parse(fs2.readFileSync(IDENTITY_PATH, "utf-8"));
12323
+ if (data.deviceId && data.publicKeyPem && data.privateKeyPem) {
12324
+ diag("GW_CLIENT_DEVICE_IDENTITY_LOADED", { deviceId: data.deviceId });
12325
+ return data;
12326
+ }
12327
+ } catch {
12328
+ }
12329
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
12330
+ const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
12331
+ const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
12332
+ const publicKeyDer = publicKey.export({ type: "spki", format: "der" });
12333
+ const rawPublicKey = publicKeyDer.subarray(publicKeyDer.length - 32);
12334
+ const deviceId = crypto.createHash("sha256").update(rawPublicKey).digest("hex");
12335
+ const identity = { deviceId, publicKeyPem, privateKeyPem };
12336
+ try {
12337
+ fs2.writeFileSync(IDENTITY_PATH, JSON.stringify(identity, null, 2), { mode: 384 });
12338
+ diag("GW_CLIENT_DEVICE_IDENTITY_CREATED", { deviceId });
12339
+ } catch (err) {
12340
+ diag("GW_CLIENT_DEVICE_IDENTITY_WRITE_FAILED", { error: String(err) });
12341
+ }
12342
+ return identity;
12343
+ }
12344
+ function normalizeMetadata(value) {
12345
+ if (typeof value !== "string") return "";
12346
+ const trimmed = value.trim();
12347
+ if (!trimmed) return "";
12348
+ return trimmed.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
12349
+ }
12350
+ function buildDeviceAuthPayloadV3(params) {
12351
+ return [
12352
+ "v3",
12353
+ params.deviceId,
12354
+ params.clientId,
12355
+ params.clientMode,
12356
+ params.role,
12357
+ params.scopes.join(","),
12358
+ String(params.signedAtMs),
12359
+ params.token ?? "",
12360
+ params.nonce,
12361
+ normalizeMetadata(params.platform),
12362
+ normalizeMetadata(params.deviceFamily)
12363
+ ].join("|");
12364
+ }
12365
+ function signPayload(privateKeyPem, payload) {
12366
+ const key = crypto.createPrivateKey(privateKeyPem);
12367
+ const signature = crypto.sign(null, Buffer.from(payload, "utf-8"), key);
12368
+ return signature.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
12369
+ }
12370
+
12371
+ // src/gateway-client.ts
12372
+ var ALLOWED_METHODS = /* @__PURE__ */ new Set([
12373
+ "cron.list",
12374
+ "cron.add",
12375
+ "cron.update",
12376
+ "cron.remove",
12377
+ "cron.run",
12378
+ "cron.runs",
12379
+ "cron.status",
12380
+ "channels.status",
12381
+ "sessions.list",
12382
+ "sessions.preview",
12383
+ "agent",
12384
+ "snapshot",
12385
+ "system.presence"
12386
+ ]);
12387
+ function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
12388
+ const signedAtMs = Date.now();
12389
+ const payload = buildDeviceAuthPayloadV3({
12390
+ deviceId: identity.deviceId,
12391
+ clientId: "gateway-client",
12392
+ clientMode: "backend",
12393
+ role: "operator",
12394
+ scopes: ["operator.read", "operator.write"],
12395
+ signedAtMs,
12396
+ token,
12397
+ nonce,
12398
+ platform: process.platform,
12399
+ deviceFamily: null
12400
+ });
12401
+ const signature = signPayload(identity.privateKeyPem, payload);
12402
+ return {
12403
+ type: "req",
12404
+ id,
12405
+ method: "connect",
12406
+ params: {
12407
+ minProtocol: 3,
12408
+ maxProtocol: 3,
12409
+ client: {
12410
+ id: "gateway-client",
12411
+ version: pluginVersion,
12412
+ platform: process.platform,
12413
+ mode: "backend"
12414
+ },
12415
+ role: "operator",
12416
+ scopes: ["operator.read", "operator.write"],
12417
+ auth: { token },
12418
+ device: {
12419
+ id: identity.deviceId,
12420
+ publicKey: identity.publicKeyPem,
12421
+ signature,
12422
+ signedAt: signedAtMs,
12423
+ nonce
12424
+ },
12425
+ locale: "en-US",
12426
+ userAgent: `cohort-sync/${pluginVersion}`
12427
+ }
12428
+ };
12429
+ }
12430
+ function parseHelloOk(response) {
12431
+ if (!response.ok) {
12432
+ const msg = response.error?.message ?? "unknown error";
12433
+ throw new Error(`Gateway connect error: ${msg}`);
12434
+ }
12435
+ const payload = response.payload;
12436
+ if (payload?.type === "hello-error") {
12437
+ const msg = payload.message ?? "unknown error";
12438
+ throw new Error(`hello-error: ${msg}`);
12439
+ }
12440
+ if (payload?.type !== "hello-ok") {
12441
+ throw new Error(`Unexpected payload type: ${String(payload?.type ?? "missing")}`);
12442
+ }
12443
+ const policy = payload.policy ?? {};
12444
+ const methods = payload.methods ?? [];
12445
+ const events = payload.events ?? [];
12446
+ return {
12447
+ methods: new Set(methods),
12448
+ events: new Set(events),
12449
+ tickIntervalMs: policy.tickIntervalMs ?? 15e3,
12450
+ snapshot: payload.snapshot
12451
+ };
12452
+ }
12453
+ function getPendingRequests() {
12454
+ const g = globalThis;
12455
+ const hot = g.__cohort_sync__ ?? (g.__cohort_sync__ = {});
12456
+ if (!hot.pendingGatewayRequests) hot.pendingGatewayRequests = /* @__PURE__ */ new Map();
12457
+ return hot.pendingGatewayRequests;
12458
+ }
12459
+ var GatewayClient2 = class {
12460
+ port;
12461
+ logger;
12462
+ ws = null;
12463
+ alive = false;
12464
+ tickWatchdog = null;
12465
+ reconnectTimer = null;
12466
+ reconnectAttempts = 0;
12467
+ closed = false;
12468
+ eventHandlers = /* @__PURE__ */ new Map();
12469
+ pluginVersion;
12470
+ tickIntervalMs = 15e3;
12471
+ // default; overwritten by hello-ok response
12472
+ deviceIdentity;
12473
+ /** Public Sets populated from hello-ok — consumers can inspect gateway capabilities */
12474
+ availableMethods = /* @__PURE__ */ new Set();
12475
+ availableEvents = /* @__PURE__ */ new Set();
12476
+ /**
12477
+ * @param port - Gateway WebSocket port
12478
+ * @param token - Auth token (stored in closure via constructor param, NOT on this or globalThis)
12479
+ * @param logger - RuntimeLogger from plugin SDK
12480
+ * @param pluginVersion - Version string for the connect frame userAgent
12481
+ */
12482
+ constructor(port, token, logger, pluginVersion = "0.5.0") {
12483
+ this.port = port;
12484
+ this.logger = logger;
12485
+ this.pluginVersion = pluginVersion;
12486
+ this.deviceIdentity = loadOrCreateDeviceIdentity();
12487
+ this._getToken = () => token;
12488
+ }
12489
+ /** Token accessor — closure over constructor param */
12490
+ _getToken;
12491
+ /**
12492
+ * Connect to the gateway, perform the protocol v3 handshake, and start
12493
+ * the tick watchdog.
12494
+ *
12495
+ * Flow:
12496
+ * 1. Open WebSocket to ws://127.0.0.1:{port}
12497
+ * 2. Wait up to 500ms for connect.challenge event (optional)
12498
+ * 3. Send connect request frame
12499
+ * 4. Wait for hello-ok response
12500
+ * 5. Start tick watchdog at 2.5x tickIntervalMs
12501
+ */
12502
+ async connect() {
12503
+ if (this.closed) throw new Error("GatewayClient has been closed");
12504
+ diag("GW_CLIENT_CONNECTING", { port: this.port });
12505
+ return new Promise((resolve, reject) => {
12506
+ const ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
12507
+ this.ws = ws;
12508
+ let settled = false;
12509
+ const settle = (err) => {
12510
+ if (settled) return;
12511
+ settled = true;
12512
+ if (err) {
12513
+ this.alive = false;
12514
+ reject(err);
12515
+ } else {
12516
+ resolve();
12517
+ }
12518
+ };
12519
+ const handshakeTimeout = setTimeout(() => {
12520
+ settle(new Error("Gateway handshake timed out after 10000ms"));
12521
+ ws.close();
12522
+ }, 1e4);
12523
+ ws.addEventListener("open", () => {
12524
+ diag("GW_CLIENT_WS_OPEN", { port: this.port });
12525
+ let challengeReceived = false;
12526
+ let challengeNonce = "";
12527
+ const challengeTimer = setTimeout(() => {
12528
+ if (!challengeReceived) {
12529
+ diag("GW_CLIENT_NO_CHALLENGE", { waited: 500 });
12530
+ sendConnect();
12531
+ }
12532
+ }, 500);
12533
+ const onChallengeMessage = (event) => {
12534
+ try {
12535
+ const data = JSON.parse(String(event.data));
12536
+ if (data.type === "event" && data.event === "connect.challenge") {
12537
+ challengeReceived = true;
12538
+ clearTimeout(challengeTimer);
12539
+ challengeNonce = data.payload?.nonce ?? "";
12540
+ diag("GW_CLIENT_CHALLENGE_RECEIVED", { hasNonce: !!challengeNonce });
12541
+ sendConnect();
12542
+ }
12543
+ } catch {
12544
+ }
12545
+ };
12546
+ ws.addEventListener("message", onChallengeMessage);
12547
+ const sendConnect = () => {
12548
+ ws.removeEventListener("message", onChallengeMessage);
12549
+ const id = crypto2.randomUUID();
12550
+ const frame = buildConnectFrame(
12551
+ id,
12552
+ this._getToken(),
12553
+ this.pluginVersion,
12554
+ this.deviceIdentity,
12555
+ challengeNonce
12556
+ );
12557
+ diag("GW_CLIENT_SENDING_CONNECT", { id, protocol: 3 });
12558
+ ws.send(JSON.stringify(frame));
12559
+ const onHelloMessage = (event) => {
12560
+ try {
12561
+ const data = JSON.parse(String(event.data));
12562
+ if (data.type === "res" && data.id === id) {
12563
+ ws.removeEventListener("message", onHelloMessage);
12564
+ clearTimeout(handshakeTimeout);
12565
+ try {
12566
+ const result = parseHelloOk(data);
12567
+ this.availableMethods = result.methods;
12568
+ this.availableEvents = result.events;
12569
+ this.tickIntervalMs = result.tickIntervalMs;
12570
+ this.alive = true;
12571
+ this.reconnectAttempts = 0;
12572
+ diag("GW_CLIENT_HELLO_OK", {
12573
+ methods: result.methods.size,
12574
+ events: result.events.size,
12575
+ tickIntervalMs: result.tickIntervalMs
12576
+ });
12577
+ this.startTickWatchdog(result.tickIntervalMs);
12578
+ ws.addEventListener("message", (ev) => this.handleMessage(ev));
12579
+ if (result.snapshot) {
12580
+ this.emit("snapshot", result.snapshot);
12581
+ }
12582
+ this.logger.info("cohort-sync: gateway client connected (protocol v3)");
12583
+ settle();
12584
+ } catch (err) {
12585
+ diag("GW_CLIENT_HELLO_FAILED", { error: String(err) });
12586
+ settle(err instanceof Error ? err : new Error(String(err)));
12587
+ ws.close();
12588
+ }
12589
+ }
12590
+ } catch {
12591
+ }
12592
+ };
12593
+ ws.addEventListener("message", onHelloMessage);
12594
+ };
12595
+ });
12596
+ ws.addEventListener("close", () => {
12597
+ clearTimeout(handshakeTimeout);
12598
+ this.alive = false;
12599
+ this.stopTickWatchdog();
12600
+ diag("GW_CLIENT_WS_CLOSED", { port: this.port });
12601
+ this.logger.warn("cohort-sync: gateway client WebSocket closed");
12602
+ const pending = getPendingRequests();
12603
+ for (const [, entry] of pending) {
12604
+ clearTimeout(entry.timer);
12605
+ entry.reject(new Error("Gateway WebSocket closed"));
12606
+ }
12607
+ pending.clear();
12608
+ if (!settled) {
12609
+ settle(new Error("Gateway WebSocket closed during handshake"));
12610
+ }
12611
+ if (!this.closed) {
12612
+ this.scheduleReconnect();
12613
+ }
12614
+ });
12615
+ ws.addEventListener("error", (err) => {
12616
+ diag("GW_CLIENT_WS_ERROR", { error: String(err) });
12617
+ this.logger.error(`cohort-sync: gateway client WS error: ${String(err)}`);
12618
+ });
12619
+ });
12620
+ }
12621
+ /**
12622
+ * Whether the WebSocket is open and the handshake completed successfully.
12623
+ */
12624
+ isAlive() {
12625
+ return this.alive && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
12626
+ }
12627
+ /**
12628
+ * Register an event handler for gateway events (tick, cron.changed, etc.).
12629
+ */
12630
+ on(event, handler) {
12631
+ let handlers = this.eventHandlers.get(event);
12632
+ if (!handlers) {
12633
+ handlers = /* @__PURE__ */ new Set();
12634
+ this.eventHandlers.set(event, handlers);
12635
+ }
12636
+ handlers.add(handler);
12637
+ }
12638
+ /**
12639
+ * Send a request to the gateway and wait for the response.
12640
+ *
12641
+ * Validates the method against ALLOWED_METHODS to prevent accidentally
12642
+ * calling admin or unauthorized methods.
12643
+ */
12644
+ async request(method, params, timeoutMs = 1e4) {
12645
+ if (!ALLOWED_METHODS.has(method)) {
12646
+ throw new Error(`Method "${method}" is not in ALLOWED_METHODS`);
12647
+ }
12648
+ if (!this.isAlive()) {
12649
+ throw new Error("Gateway client is not connected");
12650
+ }
12651
+ const id = crypto2.randomUUID();
12652
+ const frame = {
12653
+ type: "req",
12654
+ id,
12655
+ method,
12656
+ params
12657
+ };
12658
+ const pending = getPendingRequests();
12659
+ return new Promise((resolve, reject) => {
12660
+ const timer = setTimeout(() => {
12661
+ pending.delete(id);
12662
+ reject(new Error(`Gateway method "${method}" timed out after ${timeoutMs}ms`));
12663
+ }, timeoutMs);
12664
+ pending.set(id, {
12665
+ resolve,
12666
+ reject,
12667
+ timer
12668
+ });
12669
+ this.ws.send(JSON.stringify(frame));
12670
+ });
12671
+ }
12672
+ /**
12673
+ * Clean shutdown — close WebSocket, clear timers, reject pending requests.
12674
+ */
12675
+ close() {
12676
+ this.closed = true;
12677
+ this.alive = false;
12678
+ this.stopTickWatchdog();
12679
+ if (this.reconnectTimer) {
12680
+ clearTimeout(this.reconnectTimer);
12681
+ this.reconnectTimer = null;
12682
+ }
12683
+ const pending = getPendingRequests();
12684
+ for (const [, entry] of pending) {
12685
+ clearTimeout(entry.timer);
12686
+ entry.reject(new Error("Gateway client closed"));
12687
+ }
12688
+ pending.clear();
12689
+ if (this.ws) {
12690
+ this.ws.close();
12691
+ this.ws = null;
12692
+ }
12693
+ diag("GW_CLIENT_CLOSED", { port: this.port });
12694
+ this.logger.info("cohort-sync: gateway client closed");
12695
+ }
12696
+ // -------------------------------------------------------------------------
12697
+ // Private methods
12698
+ // -------------------------------------------------------------------------
12699
+ /**
12700
+ * Route incoming WebSocket messages to the appropriate handler.
12701
+ *
12702
+ * Frame types:
12703
+ * - "res" → resolve/reject a pending request
12704
+ * - "event" → dispatch to registered event handlers; reset tick watchdog on tick
12705
+ */
12706
+ handleMessage(event) {
12707
+ try {
12708
+ const data = JSON.parse(String(event.data));
12709
+ if (data.type === "res") {
12710
+ this.handleResponse(data);
12711
+ } else if (data.type === "event") {
12712
+ this.handleEvent(data);
12713
+ }
12714
+ } catch {
12715
+ }
12716
+ }
12717
+ handleResponse(frame) {
12718
+ const pending = getPendingRequests();
12719
+ const entry = pending.get(frame.id);
12720
+ if (!entry) return;
12721
+ pending.delete(frame.id);
12722
+ clearTimeout(entry.timer);
12723
+ if (frame.ok) {
12724
+ entry.resolve(frame.payload);
12725
+ } else {
12726
+ entry.reject(new Error(`Gateway method failed: ${frame.error?.message ?? "unknown error"}`));
12727
+ }
12728
+ }
12729
+ handleEvent(frame) {
12730
+ if (frame.event === "tick") {
12731
+ this.resetTickWatchdog();
12732
+ }
12733
+ this.emit(frame.event, frame.payload);
12734
+ }
12735
+ emit(event, payload) {
12736
+ const handlers = this.eventHandlers.get(event);
12737
+ if (handlers) {
12738
+ for (const handler of handlers) {
12739
+ try {
12740
+ handler(payload);
12741
+ } catch (err) {
12742
+ this.logger.error(`cohort-sync: event handler error for "${event}": ${String(err)}`);
12743
+ }
12744
+ }
12745
+ }
12746
+ }
12747
+ /**
12748
+ * Start the tick watchdog timer.
12749
+ *
12750
+ * The gateway sends tick events at tickIntervalMs. If we don't receive
12751
+ * one within 2.5x that interval, the connection is considered dead.
12752
+ */
12753
+ startTickWatchdog(tickIntervalMs) {
12754
+ this.stopTickWatchdog();
12755
+ const watchdogMs = Math.round(tickIntervalMs * 2.5);
12756
+ this.tickWatchdog = setTimeout(() => {
12757
+ diag("GW_CLIENT_TICK_TIMEOUT", { watchdogMs });
12758
+ this.logger.warn(`cohort-sync: tick watchdog expired after ${watchdogMs}ms \u2014 closing connection`);
12759
+ this.alive = false;
12760
+ this.ws?.close();
12761
+ }, watchdogMs);
12762
+ }
12763
+ resetTickWatchdog() {
12764
+ if (this.tickWatchdog) {
12765
+ this.startTickWatchdog(this.tickIntervalMs);
12766
+ }
12767
+ }
12768
+ stopTickWatchdog() {
12769
+ if (this.tickWatchdog) {
12770
+ clearTimeout(this.tickWatchdog);
12771
+ this.tickWatchdog = null;
12772
+ }
12773
+ }
12774
+ /**
12775
+ * Schedule a reconnection attempt with exponential backoff and jitter.
12776
+ *
12777
+ * Backoff: 1s base, doubles each attempt, capped at 30s.
12778
+ * Jitter: +/- 25% randomization to avoid thundering herd.
12779
+ */
12780
+ scheduleReconnect() {
12781
+ if (this.closed) return;
12782
+ const baseMs = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
12783
+ const jitter = baseMs * 0.25 * (Math.random() * 2 - 1);
12784
+ const delayMs = Math.round(Math.max(baseMs + jitter, 1e3));
12785
+ this.reconnectAttempts++;
12786
+ diag("GW_CLIENT_RECONNECT_SCHEDULED", {
12787
+ attempt: this.reconnectAttempts,
12788
+ delayMs
12789
+ });
12790
+ this.reconnectTimer = setTimeout(async () => {
12791
+ this.reconnectTimer = null;
12792
+ try {
12793
+ diag("GW_CLIENT_RECONNECTING", { attempt: this.reconnectAttempts });
12794
+ await this.connect();
12795
+ diag("GW_CLIENT_RECONNECTED", { attempt: this.reconnectAttempts });
12796
+ } catch (err) {
12797
+ diag("GW_CLIENT_RECONNECT_FAILED", {
12798
+ attempt: this.reconnectAttempts,
12799
+ error: String(err)
12800
+ });
12801
+ }
12802
+ }, delayMs);
12803
+ }
12804
+ };
12805
+
12190
12806
  // src/agent-state.ts
12191
12807
  import { basename } from "node:path";
12192
12808
 
@@ -12686,20 +13302,6 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
12686
13302
 
12687
13303
  // src/hooks.ts
12688
13304
  var BUILD_ID = "B9-ACCOUNTID-20260311";
12689
- var DIAG_LOG_PATH = "/tmp/cohort-sync-diag.log";
12690
- function diag(label, data) {
12691
- const ts = (/* @__PURE__ */ new Date()).toISOString();
12692
- const payload = data ? " " + JSON.stringify(data, (_k, v2) => {
12693
- if (typeof v2 === "string" && v2.length > 200) return v2.slice(0, 200) + "\u2026";
12694
- return v2;
12695
- }) : "";
12696
- const line = `[${ts}] ${label}${payload}
12697
- `;
12698
- try {
12699
- fs2.appendFileSync(DIAG_LOG_PATH, line);
12700
- } catch {
12701
- }
12702
- }
12703
13305
  function dumpCtx(ctx) {
12704
13306
  if (!ctx || typeof ctx !== "object") return { _raw: String(ctx) };
12705
13307
  const out = {};
@@ -12724,16 +13326,64 @@ function dumpEvent(event) {
12724
13326
  }
12725
13327
  var PLUGIN_VERSION = "unknown";
12726
13328
  try {
12727
- const pkgPath = path2.join(path2.dirname(new URL(import.meta.url).pathname), "package.json");
12728
- const pkgJson = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
13329
+ const pkgPath = path3.join(path3.dirname(new URL(import.meta.url).pathname), "package.json");
13330
+ const pkgJson = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
12729
13331
  PLUGIN_VERSION = pkgJson.version ?? "unknown";
12730
13332
  } catch {
12731
13333
  }
12732
13334
  diag("MODULE_LOADED", { BUILD_ID, PLUGIN_VERSION });
13335
+ var lastCronSnapshotJson = "";
13336
+ function resolveGatewayToken(api) {
13337
+ const rawToken = api.config?.gateway?.auth?.token;
13338
+ if (typeof rawToken === "string") return rawToken;
13339
+ if (rawToken && typeof rawToken === "object" && rawToken.source === "env") {
13340
+ return process.env[rawToken.id] ?? null;
13341
+ }
13342
+ return null;
13343
+ }
13344
+ async function quickCronSync(port, token, cfg, resolveAgentName, logger) {
13345
+ const client2 = new GatewayClient2(port, token, logger, PLUGIN_VERSION);
13346
+ try {
13347
+ await client2.connect();
13348
+ const result = await client2.request("cron.list", { includeDisabled: true });
13349
+ diag("GW_CLIENT_CRON_LIST_RESULT", { type: typeof result, isArray: Array.isArray(result), keys: result && typeof result === "object" ? Object.keys(result) : [], length: Array.isArray(result) ? result.length : void 0 });
13350
+ const jobs = Array.isArray(result) ? result : result?.jobs ?? [];
13351
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13352
+ const serialized = JSON.stringify(mapped);
13353
+ if (serialized !== lastCronSnapshotJson) {
13354
+ await pushCronSnapshot(cfg.apiKey, mapped);
13355
+ lastCronSnapshotJson = serialized;
13356
+ diag("HEARTBEAT_CRON_PUSHED", { count: mapped.length });
13357
+ } else {
13358
+ diag("HEARTBEAT_CRON_UNCHANGED", {});
13359
+ }
13360
+ } finally {
13361
+ client2.close();
13362
+ }
13363
+ }
13364
+ function registerCronEventHandlers(client2, cfg, resolveAgentName) {
13365
+ if (client2.availableEvents.has("cron")) {
13366
+ let debounceTimer = null;
13367
+ client2.on("cron", () => {
13368
+ if (debounceTimer) clearTimeout(debounceTimer);
13369
+ debounceTimer = setTimeout(async () => {
13370
+ try {
13371
+ const cronResult = await client2.request("cron.list", { includeDisabled: true });
13372
+ const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13373
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13374
+ await pushCronSnapshot(cfg.apiKey, mapped);
13375
+ diag("CRON_EVENT_PUSHED", { count: mapped.length });
13376
+ } catch (err) {
13377
+ diag("CRON_EVENT_PUSH_FAILED", { error: String(err) });
13378
+ }
13379
+ }, 2e3);
13380
+ });
13381
+ }
13382
+ }
12733
13383
  function parseIdentityFile(workspaceDir) {
12734
13384
  try {
12735
- const filePath = path2.join(workspaceDir, "IDENTITY.md");
12736
- const content = fs2.readFileSync(filePath, "utf-8");
13385
+ const filePath = path3.join(workspaceDir, "IDENTITY.md");
13386
+ const content = fs3.readFileSync(filePath, "utf-8");
12737
13387
  const identity = {};
12738
13388
  for (const line of content.split(/\r?\n/)) {
12739
13389
  const cleaned = line.trim().replace(/^\s*-\s*/, "");
@@ -12771,8 +13421,8 @@ function getOrCreateTracker() {
12771
13421
  state.tracker = fresh;
12772
13422
  return fresh;
12773
13423
  }
12774
- var STATE_FILE_PATH = path2.join(
12775
- path2.dirname(new URL(import.meta.url).pathname),
13424
+ var STATE_FILE_PATH = path3.join(
13425
+ path3.dirname(new URL(import.meta.url).pathname),
12776
13426
  ".session-state.json"
12777
13427
  );
12778
13428
  function saveSessionsToDisk(tracker) {
@@ -12789,14 +13439,14 @@ function saveSessionsToDisk(tracker) {
12789
13439
  data.sessions.push({ agentName: name, key });
12790
13440
  }
12791
13441
  }
12792
- fs2.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
13442
+ fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
12793
13443
  } catch {
12794
13444
  }
12795
13445
  }
12796
13446
  function loadSessionsFromDisk(tracker, logger) {
12797
13447
  try {
12798
- if (!fs2.existsSync(STATE_FILE_PATH)) return;
12799
- const data = JSON.parse(fs2.readFileSync(STATE_FILE_PATH, "utf8"));
13448
+ if (!fs3.existsSync(STATE_FILE_PATH)) return;
13449
+ const data = JSON.parse(fs3.readFileSync(STATE_FILE_PATH, "utf8"));
12800
13450
  if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
12801
13451
  logger.info("cohort-sync: disk session state too old (>24h), skipping");
12802
13452
  return;
@@ -12854,9 +13504,6 @@ function registerHooks(api, cfg) {
12854
13504
  agentIds: (config?.agents?.list ?? []).map((a) => a.id),
12855
13505
  agentMessageProviders: (config?.agents?.list ?? []).map((a) => ({ id: a.id, mp: a.messageProvider }))
12856
13506
  });
12857
- const cronPath = path2.join(os2.homedir(), ".openclaw", "cron", "jobs.json");
12858
- setCronStorePath(cronPath);
12859
- diag("CRON_STORE_PATH", { cronPath, exists: fs2.existsSync(cronPath) });
12860
13507
  setConvexUrl(cfg);
12861
13508
  setLogger(logger);
12862
13509
  restoreFromHotReload(logger);
@@ -12977,6 +13624,28 @@ function registerHooks(api, cfg) {
12977
13624
  }, 3e3);
12978
13625
  saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
12979
13626
  logger.info("cohort-sync: intervals created (heartbeat=2m, activityFlush=3s)");
13627
+ {
13628
+ const hotState = getHotState();
13629
+ if (hotState.commandSubscription) {
13630
+ hotState.commandSubscription();
13631
+ hotState.commandSubscription = null;
13632
+ }
13633
+ const unsub = initCommandSubscription(cfg, logger, resolveAgentName);
13634
+ hotState.commandSubscription = unsub;
13635
+ }
13636
+ {
13637
+ const port = api.config?.gateway?.port;
13638
+ const token = resolveGatewayToken(api);
13639
+ if (port && token) {
13640
+ const hotState = getHotState();
13641
+ hotState.gatewayPort = port;
13642
+ hotState.gatewayToken = token;
13643
+ diag("REGISTER_HOOKS_CRON_SYNC", { port });
13644
+ quickCronSync(port, token, cfg, resolveAgentName, logger).catch((err) => {
13645
+ diag("REGISTER_HOOKS_CRON_SYNC_FAILED", { error: String(err) });
13646
+ });
13647
+ }
13648
+ }
12980
13649
  api.registerTool((toolCtx) => {
12981
13650
  const agentId = toolCtx.agentId ?? "main";
12982
13651
  const agentName = resolveAgentName(agentId);
@@ -13126,18 +13795,6 @@ Do not attempt to make more comments until ${resetAt}.`
13126
13795
  }
13127
13796
  }
13128
13797
  saveSessionsToDisk(tracker);
13129
- try {
13130
- const cronJobs2 = fetchCronJobs(logger);
13131
- if (cronJobs2 !== null) {
13132
- const resolvedJobs = cronJobs2.map((job) => ({
13133
- ...job,
13134
- agentId: job.agentId ? resolveAgentName(job.agentId) : job.agentId
13135
- }));
13136
- await pushCronSnapshot(cfg.apiKey, resolvedJobs);
13137
- }
13138
- } catch (err) {
13139
- logger.warn(`cohort-sync: heartbeat cron push failed: ${String(err)}`);
13140
- }
13141
13798
  logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
13142
13799
  }
13143
13800
  async function flushActivityBuffer() {
@@ -13175,6 +13832,31 @@ Do not attempt to make more comments until ${resetAt}.`
13175
13832
  }
13176
13833
  }
13177
13834
  diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: { ...getHotState().channelAgentBridge } });
13835
+ const hotState = getHotState();
13836
+ hotState.gatewayPort = event.port;
13837
+ const token = resolveGatewayToken(api);
13838
+ if (token) {
13839
+ diag("GW_CLIENT_CONNECTING", { port: event.port, hasToken: true });
13840
+ try {
13841
+ const client2 = new GatewayClient2(event.port, token, logger, PLUGIN_VERSION);
13842
+ await client2.connect();
13843
+ hotState.gatewayProtocolClient = client2;
13844
+ registerCronEventHandlers(client2, cfg, resolveAgentName);
13845
+ diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
13846
+ const cronResult = await client2.request("cron.list", { includeDisabled: true });
13847
+ const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13848
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13849
+ await pushCronSnapshot(cfg.apiKey, mapped);
13850
+ lastCronSnapshotJson = JSON.stringify(mapped);
13851
+ diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
13852
+ } catch (err) {
13853
+ diag("GW_CLIENT_CONNECT_FAILED", { error: String(err) });
13854
+ logger.error(`cohort-sync: gateway client connect failed: ${String(err)}`);
13855
+ }
13856
+ } else {
13857
+ diag("GW_CLIENT_NO_TOKEN", {});
13858
+ logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
13859
+ }
13178
13860
  await initSubscription(
13179
13861
  event.port,
13180
13862
  cfg,
@@ -13183,7 +13865,6 @@ Do not attempt to make more comments until ${resetAt}.`
13183
13865
  ).catch((err) => {
13184
13866
  logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
13185
13867
  });
13186
- initCommandSubscription(cfg, logger, resolveAgentName);
13187
13868
  const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
13188
13869
  for (const agentId of allAgentIds) {
13189
13870
  const agentName = resolveAgentName(agentId);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.4.8",
3
+ "version": "0.6.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.4.8",
3
+ "version": "0.6.0",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",