@cfio/cohort-sync 0.4.6 → 0.5.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 fs2 from "node:fs";
92
- import path2 from "node:path";
91
+ import fs from "node:fs";
92
+ import path 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 = {};
@@ -2699,9 +2699,6 @@ var Type = type_exports2;
2699
2699
 
2700
2700
  // src/sync.ts
2701
2701
  import { execSync } from "node:child_process";
2702
- import fs from "node:fs";
2703
- import os from "node:os";
2704
- import path from "node:path";
2705
2702
 
2706
2703
  // ../../node_modules/.pnpm/convex@1.33.0_patch_hash=l43bztwr6e2lbmpd6ao6hmcg24_react@19.2.1/node_modules/convex/dist/esm/index.js
2707
2704
  var version = "1.33.0";
@@ -4557,12 +4554,12 @@ function createApi(pathParts = []) {
4557
4554
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
4558
4555
  );
4559
4556
  }
4560
- const path3 = pathParts.slice(0, -1).join("/");
4557
+ const path2 = pathParts.slice(0, -1).join("/");
4561
4558
  const exportName = pathParts[pathParts.length - 1];
4562
4559
  if (exportName === "default") {
4563
- return path3;
4560
+ return path2;
4564
4561
  } else {
4565
- return path3 + ":" + exportName;
4562
+ return path2 + ":" + exportName;
4566
4563
  }
4567
4564
  } else if (prop === Symbol.toStringTag) {
4568
4565
  return "FunctionReference";
@@ -7631,16 +7628,16 @@ var require_constants = __commonJS({
7631
7628
  });
7632
7629
  var require_node_gyp_build = __commonJS({
7633
7630
  "../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
7634
- var fs3 = __require("fs");
7635
- var path3 = __require("path");
7636
- var os2 = __require("os");
7631
+ var fs2 = __require("fs");
7632
+ var path2 = __require("path");
7633
+ var os = __require("os");
7637
7634
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
7638
7635
  var vars = process.config && process.config.variables || {};
7639
7636
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
7640
7637
  var abi = process.versions.modules;
7641
7638
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
7642
- var arch = process.env.npm_config_arch || os2.arch();
7643
- var platform = process.env.npm_config_platform || os2.platform();
7639
+ var arch = process.env.npm_config_arch || os.arch();
7640
+ var platform = process.env.npm_config_platform || os.platform();
7644
7641
  var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
7645
7642
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
7646
7643
  var uv = (process.versions.uv || "").split(".")[0];
@@ -7649,21 +7646,21 @@ var require_node_gyp_build = __commonJS({
7649
7646
  return runtimeRequire(load.resolve(dir));
7650
7647
  }
7651
7648
  load.resolve = load.path = function(dir) {
7652
- dir = path3.resolve(dir || ".");
7649
+ dir = path2.resolve(dir || ".");
7653
7650
  try {
7654
- var name = runtimeRequire(path3.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7651
+ var name = runtimeRequire(path2.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7655
7652
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
7656
7653
  } catch (err) {
7657
7654
  }
7658
7655
  if (!prebuildsOnly) {
7659
- var release = getFirst(path3.join(dir, "build/Release"), matchBuild);
7656
+ var release = getFirst(path2.join(dir, "build/Release"), matchBuild);
7660
7657
  if (release) return release;
7661
- var debug = getFirst(path3.join(dir, "build/Debug"), matchBuild);
7658
+ var debug = getFirst(path2.join(dir, "build/Debug"), matchBuild);
7662
7659
  if (debug) return debug;
7663
7660
  }
7664
7661
  var prebuild = resolve(dir);
7665
7662
  if (prebuild) return prebuild;
7666
- var nearby = resolve(path3.dirname(process.execPath));
7663
+ var nearby = resolve(path2.dirname(process.execPath));
7667
7664
  if (nearby) return nearby;
7668
7665
  var target = [
7669
7666
  "platform=" + platform,
@@ -7680,26 +7677,26 @@ var require_node_gyp_build = __commonJS({
7680
7677
  ].filter(Boolean).join(" ");
7681
7678
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
7682
7679
  function resolve(dir2) {
7683
- var tuples = readdirSync(path3.join(dir2, "prebuilds")).map(parseTuple);
7680
+ var tuples = readdirSync(path2.join(dir2, "prebuilds")).map(parseTuple);
7684
7681
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
7685
7682
  if (!tuple) return;
7686
- var prebuilds = path3.join(dir2, "prebuilds", tuple.name);
7683
+ var prebuilds = path2.join(dir2, "prebuilds", tuple.name);
7687
7684
  var parsed = readdirSync(prebuilds).map(parseTags);
7688
7685
  var candidates = parsed.filter(matchTags(runtime, abi));
7689
7686
  var winner = candidates.sort(compareTags(runtime))[0];
7690
- if (winner) return path3.join(prebuilds, winner.file);
7687
+ if (winner) return path2.join(prebuilds, winner.file);
7691
7688
  }
7692
7689
  };
7693
7690
  function readdirSync(dir) {
7694
7691
  try {
7695
- return fs3.readdirSync(dir);
7692
+ return fs2.readdirSync(dir);
7696
7693
  } catch (err) {
7697
7694
  return [];
7698
7695
  }
7699
7696
  }
7700
7697
  function getFirst(dir, filter) {
7701
7698
  var files = readdirSync(dir).filter(filter);
7702
- return files[0] && path3.join(dir, files[0]);
7699
+ return files[0] && path2.join(dir, files[0]);
7703
7700
  }
7704
7701
  function matchBuild(name) {
7705
7702
  return /\.node$/.test(name);
@@ -7786,7 +7783,7 @@ var require_node_gyp_build = __commonJS({
7786
7783
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
7787
7784
  }
7788
7785
  function isAlpine(platform2) {
7789
- return platform2 === "linux" && fs3.existsSync("/etc/alpine-release");
7786
+ return platform2 === "linux" && fs2.existsSync("/etc/alpine-release");
7790
7787
  }
7791
7788
  load.parseTags = parseTags;
7792
7789
  load.matchTags = matchTags;
@@ -11505,8 +11502,151 @@ var _systemSchema = defineSchema({
11505
11502
  })
11506
11503
  });
11507
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
+ // src/cron-mapping.ts
11610
+ function formatSchedule(s) {
11611
+ switch (s.kind) {
11612
+ case "cron":
11613
+ return s.expr ?? "";
11614
+ case "every":
11615
+ return `every ${humanizeMs(s.everyMs ?? 0)}`;
11616
+ case "at":
11617
+ return `once at ${s.at ?? ""}`;
11618
+ default:
11619
+ return s.kind;
11620
+ }
11621
+ }
11622
+ function humanizeMs(ms) {
11623
+ if (ms >= 864e5) return `${Math.round(ms / 864e5)}d`;
11624
+ if (ms >= 36e5) return `${Math.round(ms / 36e5)}h`;
11625
+ if (ms >= 6e4) return `${Math.round(ms / 6e4)}m`;
11626
+ return `${Math.round(ms / 1e3)}s`;
11627
+ }
11628
+ function mapCronJob(job, resolveAgentName) {
11629
+ return {
11630
+ id: job.id,
11631
+ text: job.name,
11632
+ schedule: formatSchedule(job.schedule),
11633
+ enabled: job.enabled,
11634
+ agentId: resolveAgentName(job.agentId ?? "main"),
11635
+ nextRun: job.state?.nextRunAtMs,
11636
+ lastRun: job.state?.lastRunAtMs,
11637
+ lastStatus: job.state?.lastRunStatus,
11638
+ lastError: job.state?.lastError ?? null
11639
+ };
11640
+ }
11641
+ function reverseResolveAgentName(cohortName, forwardMap) {
11642
+ const lower = cohortName.toLowerCase();
11643
+ for (const [openclawId, name] of Object.entries(forwardMap)) {
11644
+ if (name.toLowerCase() === lower) return openclawId;
11645
+ }
11646
+ return cohortName;
11647
+ }
11648
+
11508
11649
  // src/subscription.ts
11509
- import { execFileSync } from "node:child_process";
11510
11650
  function deriveConvexUrl(apiUrl) {
11511
11651
  return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
11512
11652
  }
@@ -11604,7 +11744,11 @@ function getHotState() {
11604
11744
  lastKnownRoster: [],
11605
11745
  intervals: { heartbeat: null, activityFlush: null },
11606
11746
  activityBuffer: [],
11607
- channelAgentBridge: {}
11747
+ channelAgentBridge: {},
11748
+ gatewayWs: null,
11749
+ gatewayPort: null,
11750
+ gatewayToken: null,
11751
+ commandSubscription: null
11608
11752
  };
11609
11753
  globalThis[HOT_KEY] = state;
11610
11754
  }
@@ -11735,7 +11879,7 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11735
11879
  const c = getOrCreateClient();
11736
11880
  if (!c) {
11737
11881
  logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
11738
- return;
11882
+ return null;
11739
11883
  }
11740
11884
  let processing = false;
11741
11885
  const unsubscribe = c.onUpdate(
@@ -11759,19 +11903,94 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11759
11903
  process.kill(process.pid, "SIGTERM");
11760
11904
  return;
11761
11905
  }
11762
- if (cmd.type.startsWith("cron") && cmd.payload?.jobId) {
11763
- handleCronCommand(cmd.type, cmd.payload.jobId, logger);
11764
- try {
11765
- const freshJobs = fetchCronJobs(logger);
11766
- if (freshJobs !== null) {
11767
- const resolvedJobs = resolveAgentName ? freshJobs.map((j) => ({ ...j, agentId: j.agentId ? resolveAgentName(j.agentId) : j.agentId })) : freshJobs;
11768
- await pushCronSnapshot(cfg.apiKey, resolvedJobs);
11906
+ 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}`);
11910
+ continue;
11911
+ }
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;
11941
+ }
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);
11954
+ }
11955
+ } catch {
11956
+ }
11957
+ }, 2e3);
11958
+ }
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"
11970
+ }
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)}`);
11769
11992
  }
11770
- } catch (snapErr) {
11771
- logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
11772
11993
  }
11773
- } else if (cmd.type.startsWith("cron")) {
11774
- logger.warn(`cohort-sync: cron command missing jobId: ${cmd.type}`);
11775
11994
  } else {
11776
11995
  logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
11777
11996
  }
@@ -11787,34 +12006,8 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
11787
12006
  logger.error(`cohort-sync: command subscription error: ${String(err)}`);
11788
12007
  }
11789
12008
  );
11790
- unsubscribers.push(unsubscribe);
11791
- getHotState().unsubscribers = [...unsubscribers];
11792
12009
  logger.info("cohort-sync: command subscription active");
11793
- }
11794
- function handleCronCommand(type, jobId, logger) {
11795
- const subcommandMap = {
11796
- cronEnable: ["cron", "enable", jobId],
11797
- cronDisable: ["cron", "disable", jobId],
11798
- cronRunNow: ["cron", "run", jobId],
11799
- cronDelete: ["cron", "delete", jobId, "--force"]
11800
- };
11801
- const args = subcommandMap[type];
11802
- if (!args) {
11803
- logger.warn(`cohort-sync: unknown cron command type: ${type}`);
11804
- return;
11805
- }
11806
- const timeout = type === "cronRunNow" ? 3e4 : 15e3;
11807
- try {
11808
- execFileSync("openclaw", args, {
11809
- encoding: "utf8",
11810
- timeout,
11811
- stdio: ["ignore", "pipe", "ignore"]
11812
- });
11813
- logger.info(`cohort-sync: cron command ${type} executed for job ${jobId}`);
11814
- } catch (err) {
11815
- logger.error(`cohort-sync: cron command ${type} failed for job ${jobId}: ${String(err)}`);
11816
- throw err;
11817
- }
12010
+ return unsubscribe;
11818
12011
  }
11819
12012
  async function callAddCommentFromPlugin(apiKey, args) {
11820
12013
  const c = getOrCreateClient();
@@ -11844,6 +12037,13 @@ function closeSubscription() {
11844
12037
  } catch {
11845
12038
  }
11846
12039
  }
12040
+ if (state.commandSubscription) {
12041
+ try {
12042
+ state.commandSubscription();
12043
+ } catch {
12044
+ }
12045
+ state.commandSubscription = null;
12046
+ }
11847
12047
  client?.close();
11848
12048
  client = null;
11849
12049
  clearHotState();
@@ -11917,10 +12117,6 @@ function getChannelAgent(channelId) {
11917
12117
  }
11918
12118
 
11919
12119
  // src/sync.ts
11920
- var cronStorePath = null;
11921
- function setCronStorePath(p) {
11922
- cronStorePath = p;
11923
- }
11924
12120
  function extractJson(raw) {
11925
12121
  const jsonStart = raw.search(/[\[{]/);
11926
12122
  const jsonEndBracket = raw.lastIndexOf("]");
@@ -11952,73 +12148,35 @@ function fetchSkills(logger) {
11952
12148
  return [];
11953
12149
  }
11954
12150
  }
11955
- function fetchCronJobs(logger) {
11956
- try {
11957
- const storePath = cronStorePath ?? path.join(os.homedir(), ".openclaw", "cron", "jobs.json");
11958
- if (!fs.existsSync(storePath)) {
11959
- logger.warn(`cohort-sync: cron store not found at ${storePath}`);
11960
- return null;
11961
- }
11962
- const raw = fs.readFileSync(storePath, "utf-8");
11963
- const parsed = JSON.parse(raw);
11964
- const jobs = Array.isArray(parsed?.jobs) ? parsed.jobs : [];
11965
- return jobs.map((j) => ({
11966
- id: String(j.id ?? "unknown"),
11967
- text: String(j.name ?? ""),
11968
- schedule: formatSchedule(j.schedule),
11969
- ...j.state?.nextRunAtMs != null ? { nextRun: Number(j.state.nextRunAtMs) } : {},
11970
- ...j.state?.lastRunAtMs != null ? { lastRun: Number(j.state.lastRunAtMs) } : {},
11971
- ...j.state?.lastStatus ? { lastStatus: String(j.state.lastStatus) } : {},
11972
- enabled: j.enabled !== false,
11973
- ...j.agentId != null ? { agentId: String(j.agentId) } : {}
11974
- }));
11975
- } catch (err) {
11976
- logger.warn(`cohort-sync: failed to read cron store: ${String(err)}`);
11977
- return null;
11978
- }
11979
- }
11980
- function formatSchedule(schedule) {
11981
- if (typeof schedule === "string") return schedule;
11982
- if (schedule && typeof schedule === "object") {
11983
- const s = schedule;
11984
- if (s.kind === "cron" && typeof s.expr === "string") return s.expr;
11985
- if (s.kind === "interval" && typeof s.everyMs === "number") {
11986
- const mins = Math.floor(Number(s.everyMs) / 6e4);
11987
- if (mins > 0) return `*/${mins} * * * *`;
11988
- return `every ${s.everyMs}ms`;
11989
- }
11990
- }
11991
- return String(schedule);
11992
- }
11993
12151
  var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
11994
12152
  function normalizeStatus(status) {
11995
12153
  return VALID_STATUSES.has(status) ? status : "idle";
11996
12154
  }
11997
- async function v1Get(apiUrl, apiKey, path3) {
11998
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path3}`, {
12155
+ async function v1Get(apiUrl, apiKey, path2) {
12156
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
11999
12157
  headers: { Authorization: `Bearer ${apiKey}` },
12000
12158
  signal: AbortSignal.timeout(1e4)
12001
12159
  });
12002
- if (!res.ok) throw new Error(`GET ${path3} \u2192 ${res.status}`);
12160
+ if (!res.ok) throw new Error(`GET ${path2} \u2192 ${res.status}`);
12003
12161
  return res.json();
12004
12162
  }
12005
- async function v1Patch(apiUrl, apiKey, path3, body) {
12006
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path3}`, {
12163
+ async function v1Patch(apiUrl, apiKey, path2, body) {
12164
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
12007
12165
  method: "PATCH",
12008
12166
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
12009
12167
  body: JSON.stringify(body),
12010
12168
  signal: AbortSignal.timeout(1e4)
12011
12169
  });
12012
- if (!res.ok) throw new Error(`PATCH ${path3} \u2192 ${res.status}`);
12170
+ if (!res.ok) throw new Error(`PATCH ${path2} \u2192 ${res.status}`);
12013
12171
  }
12014
- async function v1Post(apiUrl, apiKey, path3, body) {
12015
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path3}`, {
12172
+ async function v1Post(apiUrl, apiKey, path2, body) {
12173
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
12016
12174
  method: "POST",
12017
12175
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
12018
12176
  body: JSON.stringify(body),
12019
12177
  signal: AbortSignal.timeout(1e4)
12020
12178
  });
12021
- if (!res.ok) throw new Error(`POST ${path3} \u2192 ${res.status}`);
12179
+ if (!res.ok) throw new Error(`POST ${path2} \u2192 ${res.status}`);
12022
12180
  }
12023
12181
  async function checkForUpdate(currentVersion, logger) {
12024
12182
  try {
@@ -12186,6 +12344,65 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
12186
12344
  logger.info("cohort-sync: full sync complete");
12187
12345
  }
12188
12346
 
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
+ });
12358
+ }
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
+ });
12369
+ }
12370
+ });
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)
12379
+ });
12380
+ }
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)
12390
+ });
12391
+ }
12392
+ });
12393
+ api.registerGatewayMethod("cohort-sync/cron-add", async ({ context, params, respond }) => {
12394
+ 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
+ });
12402
+ }
12403
+ });
12404
+ }
12405
+
12189
12406
  // src/agent-state.ts
12190
12407
  import { basename } from "node:path";
12191
12408
 
@@ -12695,7 +12912,7 @@ function diag(label, data) {
12695
12912
  const line = `[${ts}] ${label}${payload}
12696
12913
  `;
12697
12914
  try {
12698
- fs2.appendFileSync(DIAG_LOG_PATH, line);
12915
+ fs.appendFileSync(DIAG_LOG_PATH, line);
12699
12916
  } catch {
12700
12917
  }
12701
12918
  }
@@ -12723,16 +12940,16 @@ function dumpEvent(event) {
12723
12940
  }
12724
12941
  var PLUGIN_VERSION = "unknown";
12725
12942
  try {
12726
- const pkgPath = path2.join(path2.dirname(new URL(import.meta.url).pathname), "package.json");
12727
- const pkgJson = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
12943
+ const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "package.json");
12944
+ const pkgJson = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
12728
12945
  PLUGIN_VERSION = pkgJson.version ?? "unknown";
12729
12946
  } catch {
12730
12947
  }
12731
12948
  diag("MODULE_LOADED", { BUILD_ID, PLUGIN_VERSION });
12732
12949
  function parseIdentityFile(workspaceDir) {
12733
12950
  try {
12734
- const filePath = path2.join(workspaceDir, "IDENTITY.md");
12735
- const content = fs2.readFileSync(filePath, "utf-8");
12951
+ const filePath = path.join(workspaceDir, "IDENTITY.md");
12952
+ const content = fs.readFileSync(filePath, "utf-8");
12736
12953
  const identity = {};
12737
12954
  for (const line of content.split(/\r?\n/)) {
12738
12955
  const cleaned = line.trim().replace(/^\s*-\s*/, "");
@@ -12770,8 +12987,8 @@ function getOrCreateTracker() {
12770
12987
  state.tracker = fresh;
12771
12988
  return fresh;
12772
12989
  }
12773
- var STATE_FILE_PATH = path2.join(
12774
- path2.dirname(new URL(import.meta.url).pathname),
12990
+ var STATE_FILE_PATH = path.join(
12991
+ path.dirname(new URL(import.meta.url).pathname),
12775
12992
  ".session-state.json"
12776
12993
  );
12777
12994
  function saveSessionsToDisk(tracker) {
@@ -12788,14 +13005,14 @@ function saveSessionsToDisk(tracker) {
12788
13005
  data.sessions.push({ agentName: name, key });
12789
13006
  }
12790
13007
  }
12791
- fs2.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
13008
+ fs.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
12792
13009
  } catch {
12793
13010
  }
12794
13011
  }
12795
13012
  function loadSessionsFromDisk(tracker, logger) {
12796
13013
  try {
12797
- if (!fs2.existsSync(STATE_FILE_PATH)) return;
12798
- const data = JSON.parse(fs2.readFileSync(STATE_FILE_PATH, "utf8"));
13014
+ if (!fs.existsSync(STATE_FILE_PATH)) return;
13015
+ const data = JSON.parse(fs.readFileSync(STATE_FILE_PATH, "utf8"));
12799
13016
  if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
12800
13017
  logger.info("cohort-sync: disk session state too old (>24h), skipping");
12801
13018
  return;
@@ -12842,6 +13059,7 @@ function registerHooks(api, cfg) {
12842
13059
  const tracker = getOrCreateTracker();
12843
13060
  let heartbeatInterval = null;
12844
13061
  let activityFlushInterval = null;
13062
+ registerCronGatewayMethods(api);
12845
13063
  logger.info(`cohort-sync: registerHooks [${BUILD_ID}]`);
12846
13064
  diag("REGISTER_HOOKS", {
12847
13065
  BUILD_ID,
@@ -12853,18 +13071,6 @@ function registerHooks(api, cfg) {
12853
13071
  agentIds: (config?.agents?.list ?? []).map((a) => a.id),
12854
13072
  agentMessageProviders: (config?.agents?.list ?? []).map((a) => ({ id: a.id, mp: a.messageProvider }))
12855
13073
  });
12856
- const resolvedCronPath = typeof api.resolvePath === "function" ? api.resolvePath("cron/jobs.json") : null;
12857
- const runtimeKeys = api.runtime ? Object.keys(api.runtime) : [];
12858
- const runtimeMethods = api.runtime ? Object.keys(api.runtime).filter((k) => typeof api.runtime[k] === "function") : [];
12859
- diag("CONFIG_DUMP", {
12860
- resolvedCronPath,
12861
- cronPathExists: resolvedCronPath ? fs2.existsSync(resolvedCronPath) : false,
12862
- runtimeKeys,
12863
- runtimeMethods
12864
- });
12865
- if (resolvedCronPath && typeof resolvedCronPath === "string") {
12866
- setCronStorePath(resolvedCronPath);
12867
- }
12868
13074
  setConvexUrl(cfg);
12869
13075
  setLogger(logger);
12870
13076
  restoreFromHotReload(logger);
@@ -12985,6 +13191,15 @@ function registerHooks(api, cfg) {
12985
13191
  }, 3e3);
12986
13192
  saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
12987
13193
  logger.info("cohort-sync: intervals created (heartbeat=2m, activityFlush=3s)");
13194
+ {
13195
+ const hotState = getHotState();
13196
+ if (hotState.commandSubscription) {
13197
+ hotState.commandSubscription();
13198
+ hotState.commandSubscription = null;
13199
+ }
13200
+ const unsub = initCommandSubscription(cfg, logger, resolveAgentName);
13201
+ hotState.commandSubscription = unsub;
13202
+ }
12988
13203
  api.registerTool((toolCtx) => {
12989
13204
  const agentId = toolCtx.agentId ?? "main";
12990
13205
  const agentName = resolveAgentName(agentId);
@@ -13134,17 +13349,17 @@ Do not attempt to make more comments until ${resetAt}.`
13134
13349
  }
13135
13350
  }
13136
13351
  saveSessionsToDisk(tracker);
13137
- try {
13138
- const cronJobs2 = fetchCronJobs(logger);
13139
- if (cronJobs2 !== null) {
13140
- const resolvedJobs = cronJobs2.map((job) => ({
13141
- ...job,
13142
- agentId: job.agentId ? resolveAgentName(job.agentId) : job.agentId
13143
- }));
13144
- await pushCronSnapshot(cfg.apiKey, resolvedJobs);
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)}`);
13145
13360
  }
13146
- } catch (err) {
13147
- logger.warn(`cohort-sync: heartbeat cron push failed: ${String(err)}`);
13361
+ } else {
13362
+ logger.warn("cohort-sync: gateway WS not connected \u2014 skipping cron snapshot");
13148
13363
  }
13149
13364
  logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
13150
13365
  }
@@ -13183,6 +13398,23 @@ Do not attempt to make more comments until ${resetAt}.`
13183
13398
  }
13184
13399
  }
13185
13400
  diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: { ...getHotState().channelAgentBridge } });
13401
+ const hotState = getHotState();
13402
+ 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");
13416
+ }
13417
+ }
13186
13418
  await initSubscription(
13187
13419
  event.port,
13188
13420
  cfg,
@@ -13191,7 +13423,6 @@ Do not attempt to make more comments until ${resetAt}.`
13191
13423
  ).catch((err) => {
13192
13424
  logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
13193
13425
  });
13194
- initCommandSubscription(cfg, logger, resolveAgentName);
13195
13426
  const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
13196
13427
  for (const agentId of allAgentIds) {
13197
13428
  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.6",
3
+ "version": "0.5.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.6",
3
+ "version": "0.5.0",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",