@cfio/cohort-sync 0.5.0 → 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,8 +88,8 @@ var init_keychain = __esm({
88
88
  });
89
89
 
90
90
  // src/hooks.ts
91
- import fs from "node:fs";
92
- import path from "node:path";
91
+ import fs3 from "node:fs";
92
+ import path3 from "node:path";
93
93
 
94
94
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
95
95
  var value_exports = {};
@@ -4554,12 +4554,12 @@ function createApi(pathParts = []) {
4554
4554
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
4555
4555
  );
4556
4556
  }
4557
- const path2 = pathParts.slice(0, -1).join("/");
4557
+ const path4 = pathParts.slice(0, -1).join("/");
4558
4558
  const exportName = pathParts[pathParts.length - 1];
4559
4559
  if (exportName === "default") {
4560
- return path2;
4560
+ return path4;
4561
4561
  } else {
4562
- return path2 + ":" + exportName;
4562
+ return path4 + ":" + exportName;
4563
4563
  }
4564
4564
  } else if (prop === Symbol.toStringTag) {
4565
4565
  return "FunctionReference";
@@ -7628,16 +7628,16 @@ var require_constants = __commonJS({
7628
7628
  });
7629
7629
  var require_node_gyp_build = __commonJS({
7630
7630
  "../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
7631
- var fs2 = __require("fs");
7632
- var path2 = __require("path");
7633
- var os = __require("os");
7631
+ var fs4 = __require("fs");
7632
+ var path4 = __require("path");
7633
+ var os3 = __require("os");
7634
7634
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
7635
7635
  var vars = process.config && process.config.variables || {};
7636
7636
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
7637
7637
  var abi = process.versions.modules;
7638
7638
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
7639
- var arch = process.env.npm_config_arch || os.arch();
7640
- var platform = process.env.npm_config_platform || os.platform();
7639
+ var arch = process.env.npm_config_arch || os3.arch();
7640
+ var platform = process.env.npm_config_platform || os3.platform();
7641
7641
  var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
7642
7642
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
7643
7643
  var uv = (process.versions.uv || "").split(".")[0];
@@ -7646,21 +7646,21 @@ var require_node_gyp_build = __commonJS({
7646
7646
  return runtimeRequire(load.resolve(dir));
7647
7647
  }
7648
7648
  load.resolve = load.path = function(dir) {
7649
- dir = path2.resolve(dir || ".");
7649
+ dir = path4.resolve(dir || ".");
7650
7650
  try {
7651
- var name = runtimeRequire(path2.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7651
+ var name = runtimeRequire(path4.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7652
7652
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
7653
7653
  } catch (err) {
7654
7654
  }
7655
7655
  if (!prebuildsOnly) {
7656
- var release = getFirst(path2.join(dir, "build/Release"), matchBuild);
7656
+ var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
7657
7657
  if (release) return release;
7658
- var debug = getFirst(path2.join(dir, "build/Debug"), matchBuild);
7658
+ var debug = getFirst(path4.join(dir, "build/Debug"), matchBuild);
7659
7659
  if (debug) return debug;
7660
7660
  }
7661
7661
  var prebuild = resolve(dir);
7662
7662
  if (prebuild) return prebuild;
7663
- var nearby = resolve(path2.dirname(process.execPath));
7663
+ var nearby = resolve(path4.dirname(process.execPath));
7664
7664
  if (nearby) return nearby;
7665
7665
  var target = [
7666
7666
  "platform=" + platform,
@@ -7677,26 +7677,26 @@ var require_node_gyp_build = __commonJS({
7677
7677
  ].filter(Boolean).join(" ");
7678
7678
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
7679
7679
  function resolve(dir2) {
7680
- var tuples = readdirSync(path2.join(dir2, "prebuilds")).map(parseTuple);
7680
+ var tuples = readdirSync(path4.join(dir2, "prebuilds")).map(parseTuple);
7681
7681
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
7682
7682
  if (!tuple) return;
7683
- var prebuilds = path2.join(dir2, "prebuilds", tuple.name);
7683
+ var prebuilds = path4.join(dir2, "prebuilds", tuple.name);
7684
7684
  var parsed = readdirSync(prebuilds).map(parseTags);
7685
7685
  var candidates = parsed.filter(matchTags(runtime, abi));
7686
7686
  var winner = candidates.sort(compareTags(runtime))[0];
7687
- if (winner) return path2.join(prebuilds, winner.file);
7687
+ if (winner) return path4.join(prebuilds, winner.file);
7688
7688
  }
7689
7689
  };
7690
7690
  function readdirSync(dir) {
7691
7691
  try {
7692
- return fs2.readdirSync(dir);
7692
+ return fs4.readdirSync(dir);
7693
7693
  } catch (err) {
7694
7694
  return [];
7695
7695
  }
7696
7696
  }
7697
7697
  function getFirst(dir, filter) {
7698
7698
  var files = readdirSync(dir).filter(filter);
7699
- return files[0] && path2.join(dir, files[0]);
7699
+ return files[0] && path4.join(dir, files[0]);
7700
7700
  }
7701
7701
  function matchBuild(name) {
7702
7702
  return /\.node$/.test(name);
@@ -7783,7 +7783,7 @@ var require_node_gyp_build = __commonJS({
7783
7783
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
7784
7784
  }
7785
7785
  function isAlpine(platform2) {
7786
- return platform2 === "linux" && fs2.existsSync("/etc/alpine-release");
7786
+ return platform2 === "linux" && fs4.existsSync("/etc/alpine-release");
7787
7787
  }
7788
7788
  load.parseTags = parseTags;
7789
7789
  load.matchTags = matchTags;
@@ -11502,110 +11502,6 @@ var _systemSchema = defineSchema({
11502
11502
  })
11503
11503
  });
11504
11504
 
11505
- // src/gateway-rpc.ts
11506
- import crypto from "node:crypto";
11507
- function buildRequestFrame(id, method, params) {
11508
- return { id, type: "req", method, params };
11509
- }
11510
- function getPendingRequests() {
11511
- const g = globalThis;
11512
- const hot = g.__cohort_sync__ ?? (g.__cohort_sync__ = {});
11513
- if (!hot.pendingGatewayRequests) hot.pendingGatewayRequests = /* @__PURE__ */ new Map();
11514
- return hot.pendingGatewayRequests;
11515
- }
11516
- var authReady = null;
11517
- function openGatewayConnection(port, token, logger) {
11518
- const ws = new WebSocket(`ws://127.0.0.1:${port}`);
11519
- let resolveAuth;
11520
- let rejectAuth;
11521
- let authSettled = false;
11522
- authReady = new Promise((res, rej) => {
11523
- resolveAuth = res;
11524
- rejectAuth = rej;
11525
- });
11526
- ws.addEventListener("open", () => {
11527
- ws.send(JSON.stringify({
11528
- minProtocol: 1,
11529
- maxProtocol: 1,
11530
- client: {
11531
- id: "gateway-client",
11532
- version: "1.0.0",
11533
- platform: process.platform,
11534
- mode: "backend"
11535
- },
11536
- auth: { token }
11537
- }));
11538
- logger.info(`cohort-sync: gateway WS connected to port ${port}, awaiting hello-ok`);
11539
- });
11540
- ws.addEventListener("message", (event) => {
11541
- try {
11542
- const data = JSON.parse(String(event.data));
11543
- if (data.type === "hello-ok") {
11544
- authSettled = true;
11545
- resolveAuth();
11546
- logger.info("cohort-sync: gateway WS authenticated");
11547
- return;
11548
- }
11549
- if (data.type === "hello-error" || data.type === "error") {
11550
- authSettled = true;
11551
- rejectAuth(new Error(`Gateway auth failed: ${data.message ?? data.error ?? "unknown"}`));
11552
- ws.close();
11553
- return;
11554
- }
11555
- const pending = getPendingRequests();
11556
- if (data.type === "res" && data.id && pending.has(data.id)) {
11557
- const entry = pending.get(data.id);
11558
- pending.delete(data.id);
11559
- clearTimeout(entry.timer);
11560
- if (data.ok) {
11561
- entry.resolve(data.payload);
11562
- } else {
11563
- entry.reject(new Error(`Gateway method failed: ${data.error?.message ?? "unknown"}`));
11564
- }
11565
- }
11566
- } catch {
11567
- }
11568
- });
11569
- ws.addEventListener("close", () => {
11570
- logger.warn("cohort-sync: gateway WS closed");
11571
- if (!authSettled) {
11572
- authSettled = true;
11573
- rejectAuth(new Error("Gateway WS closed during auth"));
11574
- }
11575
- const pending = getPendingRequests();
11576
- for (const [, { reject, timer }] of pending) {
11577
- clearTimeout(timer);
11578
- reject(new Error("Gateway WS closed"));
11579
- }
11580
- pending.clear();
11581
- });
11582
- ws.addEventListener("error", (err) => {
11583
- logger.error(`cohort-sync: gateway WS error: ${String(err)}`);
11584
- });
11585
- return ws;
11586
- }
11587
- async function callGatewayMethod(ws, method, params, timeoutMs = 1e4) {
11588
- if (!ws || ws.readyState !== WebSocket.OPEN) {
11589
- throw new Error("Gateway WS not connected");
11590
- }
11591
- if (authReady) await authReady;
11592
- const id = crypto.randomUUID();
11593
- const frame = buildRequestFrame(id, method, params);
11594
- const pending = getPendingRequests();
11595
- return new Promise((resolve, reject) => {
11596
- const timer = setTimeout(() => {
11597
- pending.delete(id);
11598
- reject(new Error(`Gateway method ${method} timed out after ${timeoutMs}ms`));
11599
- }, timeoutMs);
11600
- pending.set(id, {
11601
- resolve,
11602
- reject,
11603
- timer
11604
- });
11605
- ws.send(JSON.stringify(frame));
11606
- });
11607
- }
11608
-
11609
11505
  // src/cron-mapping.ts
11610
11506
  function formatSchedule(s) {
11611
11507
  switch (s.kind) {
@@ -11745,9 +11641,9 @@ function getHotState() {
11745
11641
  intervals: { heartbeat: null, activityFlush: null },
11746
11642
  activityBuffer: [],
11747
11643
  channelAgentBridge: {},
11748
- gatewayWs: null,
11749
11644
  gatewayPort: null,
11750
11645
  gatewayToken: null,
11646
+ gatewayProtocolClient: null,
11751
11647
  commandSubscription: null
11752
11648
  };
11753
11649
  globalThis[HOT_KEY] = state;
@@ -11904,92 +11800,111 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11904
11800
  return;
11905
11801
  }
11906
11802
  if (cmd.type.startsWith("cron")) {
11907
- const ws = getHotState().gatewayWs;
11908
- if (!ws || ws.readyState !== WebSocket.OPEN) {
11909
- logger.warn(`cohort-sync: gateway WS not connected, cannot execute ${cmd.type}`);
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}`);
11910
11808
  continue;
11911
11809
  }
11912
- const nameMap = cfg.agentNameMap ?? {};
11913
- if (cmd.type === "cronEnable") {
11914
- await callGatewayMethod(ws, "cohort-sync/cron-update", {
11915
- jobId: cmd.payload?.jobId,
11916
- patch: { enabled: true }
11917
- });
11918
- } else if (cmd.type === "cronDisable") {
11919
- await callGatewayMethod(ws, "cohort-sync/cron-update", {
11920
- jobId: cmd.payload?.jobId,
11921
- patch: { enabled: false }
11922
- });
11923
- } else if (cmd.type === "cronDelete") {
11924
- await callGatewayMethod(ws, "cohort-sync/cron-remove", {
11925
- jobId: cmd.payload?.jobId
11926
- });
11927
- } else if (cmd.type === "cronRunNow") {
11928
- const runResult = await callGatewayMethod(
11929
- ws,
11930
- "cohort-sync/cron-run",
11931
- { jobId: cmd.payload?.jobId }
11932
- );
11933
- if (runResult?.ok && runResult?.ran) {
11934
- const jobId = cmd.payload?.jobId;
11935
- let polls = 0;
11936
- const pollInterval = setInterval(async () => {
11937
- polls++;
11938
- if (polls >= 15) {
11939
- clearInterval(pollInterval);
11940
- return;
11810
+ const gwClient = new GatewayClient(port, token, logger);
11811
+ try {
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);
11941
11863
  }
11942
- try {
11943
- const pollWs = getHotState().gatewayWs;
11944
- if (!pollWs || pollWs.readyState !== WebSocket.OPEN) {
11945
- clearInterval(pollInterval);
11946
- return;
11947
- }
11948
- const freshJobs = await callGatewayMethod(pollWs, "cohort-sync/cron-list");
11949
- const job = freshJobs.find((j) => j.id === jobId);
11950
- if (job && !job.state?.runningAtMs) {
11951
- clearInterval(pollInterval);
11952
- const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11953
- await pushCronSnapshot(cfg.apiKey, mapped);
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"
11954
11877
  }
11955
- } catch {
11956
- }
11957
- }, 2e3);
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}`);
11958
11895
  }
11959
- } else if (cmd.type === "cronCreate") {
11960
- const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
11961
- await callGatewayMethod(ws, "cohort-sync/cron-add", {
11962
- job: {
11963
- agentId,
11964
- name: cmd.payload?.name,
11965
- enabled: true,
11966
- schedule: cmd.payload?.schedule,
11967
- payload: { kind: "agentTurn", message: cmd.payload?.message },
11968
- sessionTarget: "isolated",
11969
- wakeMode: "now"
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)}`);
11970
11904
  }
11971
- });
11972
- } else if (cmd.type === "cronUpdate") {
11973
- const patch = {};
11974
- if (cmd.payload?.name) patch.name = cmd.payload.name;
11975
- if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
11976
- if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
11977
- if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
11978
- await callGatewayMethod(ws, "cohort-sync/cron-update", {
11979
- jobId: cmd.payload?.jobId,
11980
- patch
11981
- });
11982
- } else {
11983
- logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
11984
- }
11985
- if (ws.readyState === WebSocket.OPEN) {
11986
- try {
11987
- const freshJobs = await callGatewayMethod(ws, "cohort-sync/cron-list");
11988
- const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11989
- await pushCronSnapshot(cfg.apiKey, mapped);
11990
- } catch (snapErr) {
11991
- logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
11992
11905
  }
11906
+ } finally {
11907
+ gwClient.close();
11993
11908
  }
11994
11909
  } else {
11995
11910
  logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
@@ -12044,6 +11959,13 @@ function closeSubscription() {
12044
11959
  }
12045
11960
  state.commandSubscription = null;
12046
11961
  }
11962
+ if (state.gatewayProtocolClient) {
11963
+ try {
11964
+ state.gatewayProtocolClient.close();
11965
+ } catch {
11966
+ }
11967
+ state.gatewayProtocolClient = null;
11968
+ }
12047
11969
  client?.close();
12048
11970
  client = null;
12049
11971
  clearHotState();
@@ -12152,31 +12074,31 @@ var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
12152
12074
  function normalizeStatus(status) {
12153
12075
  return VALID_STATUSES.has(status) ? status : "idle";
12154
12076
  }
12155
- async function v1Get(apiUrl, apiKey, path2) {
12156
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
12077
+ async function v1Get(apiUrl, apiKey, path4) {
12078
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
12157
12079
  headers: { Authorization: `Bearer ${apiKey}` },
12158
12080
  signal: AbortSignal.timeout(1e4)
12159
12081
  });
12160
- if (!res.ok) throw new Error(`GET ${path2} \u2192 ${res.status}`);
12082
+ if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
12161
12083
  return res.json();
12162
12084
  }
12163
- async function v1Patch(apiUrl, apiKey, path2, body) {
12164
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
12085
+ async function v1Patch(apiUrl, apiKey, path4, body) {
12086
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
12165
12087
  method: "PATCH",
12166
12088
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
12167
12089
  body: JSON.stringify(body),
12168
12090
  signal: AbortSignal.timeout(1e4)
12169
12091
  });
12170
- if (!res.ok) throw new Error(`PATCH ${path2} \u2192 ${res.status}`);
12092
+ if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
12171
12093
  }
12172
- async function v1Post(apiUrl, apiKey, path2, body) {
12173
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
12094
+ async function v1Post(apiUrl, apiKey, path4, body) {
12095
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
12174
12096
  method: "POST",
12175
12097
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
12176
12098
  body: JSON.stringify(body),
12177
12099
  signal: AbortSignal.timeout(1e4)
12178
12100
  });
12179
- if (!res.ok) throw new Error(`POST ${path2} \u2192 ${res.status}`);
12101
+ if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
12180
12102
  }
12181
12103
  async function checkForUpdate(currentVersion, logger) {
12182
12104
  try {
@@ -12344,64 +12266,542 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
12344
12266
  logger.info("cohort-sync: full sync complete");
12345
12267
  }
12346
12268
 
12347
- // src/gateway-methods.ts
12348
- function registerCronGatewayMethods(api) {
12349
- api.registerGatewayMethod("cohort-sync/cron-list", async ({ context, respond }) => {
12350
- try {
12351
- const jobs = await context.cron.list({ includeDisabled: true });
12352
- respond(true, jobs);
12353
- } catch (err) {
12354
- respond(false, void 0, {
12355
- code: "INTERNAL_ERROR",
12356
- message: (err instanceof Error ? err.message : String(err)).slice(0, 200)
12357
- });
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
+ }
12358
12309
  }
12359
- });
12360
- api.registerGatewayMethod("cohort-sync/cron-run", async ({ context, params, respond }) => {
12361
- try {
12362
- const result = await context.cron.run(params.jobId, "force");
12363
- respond(true, result);
12364
- } catch (err) {
12365
- respond(false, void 0, {
12366
- code: "CRON_RUN_FAILED",
12367
- message: (err instanceof Error ? err.message : String(err)).slice(0, 200)
12368
- });
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;
12369
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
12370
12400
  });
12371
- api.registerGatewayMethod("cohort-sync/cron-update", async ({ context, params, respond }) => {
12372
- try {
12373
- const result = await context.cron.update(params.jobId, params.patch);
12374
- respond(true, result);
12375
- } catch (err) {
12376
- respond(false, void 0, {
12377
- code: "CRON_UPDATE_FAILED",
12378
- message: (err instanceof Error ? err.message : String(err)).slice(0, 200)
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
+ }
12379
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);
12380
12635
  }
12381
- });
12382
- api.registerGatewayMethod("cohort-sync/cron-remove", async ({ context, params, respond }) => {
12383
- try {
12384
- const result = await context.cron.remove(params.jobId);
12385
- respond(true, result);
12386
- } catch (err) {
12387
- respond(false, void 0, {
12388
- code: "CRON_REMOVE_FAILED",
12389
- message: (err instanceof Error ? err.message : String(err)).slice(0, 200)
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
12390
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;
12391
12682
  }
12392
- });
12393
- api.registerGatewayMethod("cohort-sync/cron-add", async ({ context, params, respond }) => {
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) {
12394
12707
  try {
12395
- const result = await context.cron.add(params.job);
12396
- respond(true, result);
12397
- } catch (err) {
12398
- respond(false, void 0, {
12399
- code: "CRON_ADD_FAILED",
12400
- message: (err instanceof Error ? err.message : String(err)).slice(0, 200)
12401
- });
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 {
12402
12715
  }
12403
- });
12404
- }
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
+ };
12405
12805
 
12406
12806
  // src/agent-state.ts
12407
12807
  import { basename } from "node:path";
@@ -12902,20 +13302,6 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
12902
13302
 
12903
13303
  // src/hooks.ts
12904
13304
  var BUILD_ID = "B9-ACCOUNTID-20260311";
12905
- var DIAG_LOG_PATH = "/tmp/cohort-sync-diag.log";
12906
- function diag(label, data) {
12907
- const ts = (/* @__PURE__ */ new Date()).toISOString();
12908
- const payload = data ? " " + JSON.stringify(data, (_k, v2) => {
12909
- if (typeof v2 === "string" && v2.length > 200) return v2.slice(0, 200) + "\u2026";
12910
- return v2;
12911
- }) : "";
12912
- const line = `[${ts}] ${label}${payload}
12913
- `;
12914
- try {
12915
- fs.appendFileSync(DIAG_LOG_PATH, line);
12916
- } catch {
12917
- }
12918
- }
12919
13305
  function dumpCtx(ctx) {
12920
13306
  if (!ctx || typeof ctx !== "object") return { _raw: String(ctx) };
12921
13307
  const out = {};
@@ -12940,16 +13326,64 @@ function dumpEvent(event) {
12940
13326
  }
12941
13327
  var PLUGIN_VERSION = "unknown";
12942
13328
  try {
12943
- const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "package.json");
12944
- const pkgJson = JSON.parse(fs.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"));
12945
13331
  PLUGIN_VERSION = pkgJson.version ?? "unknown";
12946
13332
  } catch {
12947
13333
  }
12948
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
+ }
12949
13383
  function parseIdentityFile(workspaceDir) {
12950
13384
  try {
12951
- const filePath = path.join(workspaceDir, "IDENTITY.md");
12952
- const content = fs.readFileSync(filePath, "utf-8");
13385
+ const filePath = path3.join(workspaceDir, "IDENTITY.md");
13386
+ const content = fs3.readFileSync(filePath, "utf-8");
12953
13387
  const identity = {};
12954
13388
  for (const line of content.split(/\r?\n/)) {
12955
13389
  const cleaned = line.trim().replace(/^\s*-\s*/, "");
@@ -12987,8 +13421,8 @@ function getOrCreateTracker() {
12987
13421
  state.tracker = fresh;
12988
13422
  return fresh;
12989
13423
  }
12990
- var STATE_FILE_PATH = path.join(
12991
- path.dirname(new URL(import.meta.url).pathname),
13424
+ var STATE_FILE_PATH = path3.join(
13425
+ path3.dirname(new URL(import.meta.url).pathname),
12992
13426
  ".session-state.json"
12993
13427
  );
12994
13428
  function saveSessionsToDisk(tracker) {
@@ -13005,14 +13439,14 @@ function saveSessionsToDisk(tracker) {
13005
13439
  data.sessions.push({ agentName: name, key });
13006
13440
  }
13007
13441
  }
13008
- fs.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
13442
+ fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
13009
13443
  } catch {
13010
13444
  }
13011
13445
  }
13012
13446
  function loadSessionsFromDisk(tracker, logger) {
13013
13447
  try {
13014
- if (!fs.existsSync(STATE_FILE_PATH)) return;
13015
- const data = JSON.parse(fs.readFileSync(STATE_FILE_PATH, "utf8"));
13448
+ if (!fs3.existsSync(STATE_FILE_PATH)) return;
13449
+ const data = JSON.parse(fs3.readFileSync(STATE_FILE_PATH, "utf8"));
13016
13450
  if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
13017
13451
  logger.info("cohort-sync: disk session state too old (>24h), skipping");
13018
13452
  return;
@@ -13059,7 +13493,6 @@ function registerHooks(api, cfg) {
13059
13493
  const tracker = getOrCreateTracker();
13060
13494
  let heartbeatInterval = null;
13061
13495
  let activityFlushInterval = null;
13062
- registerCronGatewayMethods(api);
13063
13496
  logger.info(`cohort-sync: registerHooks [${BUILD_ID}]`);
13064
13497
  diag("REGISTER_HOOKS", {
13065
13498
  BUILD_ID,
@@ -13200,6 +13633,19 @@ function registerHooks(api, cfg) {
13200
13633
  const unsub = initCommandSubscription(cfg, logger, resolveAgentName);
13201
13634
  hotState.commandSubscription = unsub;
13202
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
+ }
13203
13649
  api.registerTool((toolCtx) => {
13204
13650
  const agentId = toolCtx.agentId ?? "main";
13205
13651
  const agentName = resolveAgentName(agentId);
@@ -13349,18 +13795,6 @@ Do not attempt to make more comments until ${resetAt}.`
13349
13795
  }
13350
13796
  }
13351
13797
  saveSessionsToDisk(tracker);
13352
- const ws = getHotState().gatewayWs;
13353
- if (ws && ws.readyState === WebSocket.OPEN) {
13354
- try {
13355
- const jobs = await callGatewayMethod(ws, "cohort-sync/cron-list");
13356
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13357
- await pushCronSnapshot(cfg.apiKey, mapped);
13358
- } catch (err) {
13359
- logger.warn(`cohort-sync: cron list failed: ${String(err)}`);
13360
- }
13361
- } else {
13362
- logger.warn("cohort-sync: gateway WS not connected \u2014 skipping cron snapshot");
13363
- }
13364
13798
  logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
13365
13799
  }
13366
13800
  async function flushActivityBuffer() {
@@ -13400,20 +13834,28 @@ Do not attempt to make more comments until ${resetAt}.`
13400
13834
  diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: { ...getHotState().channelAgentBridge } });
13401
13835
  const hotState = getHotState();
13402
13836
  hotState.gatewayPort = event.port;
13403
- const rawToken = api.config?.gateway?.auth?.token;
13404
- if (typeof rawToken === "string") {
13405
- hotState.gatewayToken = rawToken;
13406
- } else if (rawToken && typeof rawToken === "object" && rawToken.source === "env") {
13407
- hotState.gatewayToken = process.env[rawToken.id] ?? null;
13408
- } else {
13409
- hotState.gatewayToken = null;
13410
- }
13411
- if (!hotState.gatewayWs || hotState.gatewayWs.readyState !== WebSocket.OPEN) {
13412
- if (hotState.gatewayToken) {
13413
- hotState.gatewayWs = openGatewayConnection(event.port, hotState.gatewayToken, logger);
13414
- } else {
13415
- logger.warn("cohort-sync: no gateway auth token in config \u2014 cron operations disabled");
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)}`);
13416
13855
  }
13856
+ } else {
13857
+ diag("GW_CLIENT_NO_TOKEN", {});
13858
+ logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
13417
13859
  }
13418
13860
  await initSubscription(
13419
13861
  event.port,
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.5.0",
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.5.0",
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",