@cfio/cohort-sync 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +830 -357
- package/dist/openclaw.plugin.json +11 -1
- package/dist/package.json +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,6 +16,14 @@ __export(keychain_exports, {
|
|
|
16
16
|
setCredential: () => setCredential
|
|
17
17
|
});
|
|
18
18
|
import { execFile } from "node:child_process";
|
|
19
|
+
import os3 from "node:os";
|
|
20
|
+
function assertMacOS(operation) {
|
|
21
|
+
if (os3.platform() !== "darwin") {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`cohort-sync: ${operation} requires macOS Keychain. On Linux/Windows, set your API key in OpenClaw config: plugins.entries.cohort-sync.config.apiKey`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
19
27
|
function securityCmd(args) {
|
|
20
28
|
return new Promise((resolve, reject) => {
|
|
21
29
|
execFile("security", args, { timeout: 5e3 }, (err, stdout, stderr) => {
|
|
@@ -37,6 +45,7 @@ function isNotFoundError(err) {
|
|
|
37
45
|
return false;
|
|
38
46
|
}
|
|
39
47
|
async function setCredential(apiUrl, apiKey) {
|
|
48
|
+
assertMacOS("storing credentials");
|
|
40
49
|
await securityCmd([
|
|
41
50
|
"add-generic-password",
|
|
42
51
|
"-s",
|
|
@@ -49,6 +58,7 @@ async function setCredential(apiUrl, apiKey) {
|
|
|
49
58
|
]);
|
|
50
59
|
}
|
|
51
60
|
async function getCredential(apiUrl) {
|
|
61
|
+
assertMacOS("reading credentials");
|
|
52
62
|
try {
|
|
53
63
|
const { stdout } = await securityCmd([
|
|
54
64
|
"find-generic-password",
|
|
@@ -65,6 +75,7 @@ async function getCredential(apiUrl) {
|
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
async function deleteCredential(apiUrl) {
|
|
78
|
+
assertMacOS("deleting credentials");
|
|
68
79
|
try {
|
|
69
80
|
await securityCmd([
|
|
70
81
|
"delete-generic-password",
|
|
@@ -88,8 +99,8 @@ var init_keychain = __esm({
|
|
|
88
99
|
});
|
|
89
100
|
|
|
90
101
|
// src/hooks.ts
|
|
91
|
-
import
|
|
92
|
-
import
|
|
102
|
+
import fs3 from "node:fs";
|
|
103
|
+
import path3 from "node:path";
|
|
93
104
|
|
|
94
105
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
95
106
|
var value_exports = {};
|
|
@@ -4554,12 +4565,12 @@ function createApi(pathParts = []) {
|
|
|
4554
4565
|
`API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
|
|
4555
4566
|
);
|
|
4556
4567
|
}
|
|
4557
|
-
const
|
|
4568
|
+
const path4 = pathParts.slice(0, -1).join("/");
|
|
4558
4569
|
const exportName = pathParts[pathParts.length - 1];
|
|
4559
4570
|
if (exportName === "default") {
|
|
4560
|
-
return
|
|
4571
|
+
return path4;
|
|
4561
4572
|
} else {
|
|
4562
|
-
return
|
|
4573
|
+
return path4 + ":" + exportName;
|
|
4563
4574
|
}
|
|
4564
4575
|
} else if (prop === Symbol.toStringTag) {
|
|
4565
4576
|
return "FunctionReference";
|
|
@@ -7628,16 +7639,16 @@ var require_constants = __commonJS({
|
|
|
7628
7639
|
});
|
|
7629
7640
|
var require_node_gyp_build = __commonJS({
|
|
7630
7641
|
"../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
7631
|
-
var
|
|
7632
|
-
var
|
|
7633
|
-
var
|
|
7642
|
+
var fs4 = __require("fs");
|
|
7643
|
+
var path4 = __require("path");
|
|
7644
|
+
var os4 = __require("os");
|
|
7634
7645
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
7635
7646
|
var vars = process.config && process.config.variables || {};
|
|
7636
7647
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
7637
7648
|
var abi = process.versions.modules;
|
|
7638
7649
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
7639
|
-
var arch = process.env.npm_config_arch ||
|
|
7640
|
-
var platform = process.env.npm_config_platform ||
|
|
7650
|
+
var arch = process.env.npm_config_arch || os4.arch();
|
|
7651
|
+
var platform = process.env.npm_config_platform || os4.platform();
|
|
7641
7652
|
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
|
|
7642
7653
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
7643
7654
|
var uv = (process.versions.uv || "").split(".")[0];
|
|
@@ -7646,21 +7657,21 @@ var require_node_gyp_build = __commonJS({
|
|
|
7646
7657
|
return runtimeRequire(load.resolve(dir));
|
|
7647
7658
|
}
|
|
7648
7659
|
load.resolve = load.path = function(dir) {
|
|
7649
|
-
dir =
|
|
7660
|
+
dir = path4.resolve(dir || ".");
|
|
7650
7661
|
try {
|
|
7651
|
-
var name = runtimeRequire(
|
|
7662
|
+
var name = runtimeRequire(path4.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
|
|
7652
7663
|
if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
|
|
7653
7664
|
} catch (err) {
|
|
7654
7665
|
}
|
|
7655
7666
|
if (!prebuildsOnly) {
|
|
7656
|
-
var release = getFirst(
|
|
7667
|
+
var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
|
|
7657
7668
|
if (release) return release;
|
|
7658
|
-
var debug = getFirst(
|
|
7669
|
+
var debug = getFirst(path4.join(dir, "build/Debug"), matchBuild);
|
|
7659
7670
|
if (debug) return debug;
|
|
7660
7671
|
}
|
|
7661
7672
|
var prebuild = resolve(dir);
|
|
7662
7673
|
if (prebuild) return prebuild;
|
|
7663
|
-
var nearby = resolve(
|
|
7674
|
+
var nearby = resolve(path4.dirname(process.execPath));
|
|
7664
7675
|
if (nearby) return nearby;
|
|
7665
7676
|
var target = [
|
|
7666
7677
|
"platform=" + platform,
|
|
@@ -7677,26 +7688,26 @@ var require_node_gyp_build = __commonJS({
|
|
|
7677
7688
|
].filter(Boolean).join(" ");
|
|
7678
7689
|
throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
|
|
7679
7690
|
function resolve(dir2) {
|
|
7680
|
-
var tuples = readdirSync(
|
|
7691
|
+
var tuples = readdirSync(path4.join(dir2, "prebuilds")).map(parseTuple);
|
|
7681
7692
|
var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
|
|
7682
7693
|
if (!tuple) return;
|
|
7683
|
-
var prebuilds =
|
|
7694
|
+
var prebuilds = path4.join(dir2, "prebuilds", tuple.name);
|
|
7684
7695
|
var parsed = readdirSync(prebuilds).map(parseTags);
|
|
7685
7696
|
var candidates = parsed.filter(matchTags(runtime, abi));
|
|
7686
7697
|
var winner = candidates.sort(compareTags(runtime))[0];
|
|
7687
|
-
if (winner) return
|
|
7698
|
+
if (winner) return path4.join(prebuilds, winner.file);
|
|
7688
7699
|
}
|
|
7689
7700
|
};
|
|
7690
7701
|
function readdirSync(dir) {
|
|
7691
7702
|
try {
|
|
7692
|
-
return
|
|
7703
|
+
return fs4.readdirSync(dir);
|
|
7693
7704
|
} catch (err) {
|
|
7694
7705
|
return [];
|
|
7695
7706
|
}
|
|
7696
7707
|
}
|
|
7697
7708
|
function getFirst(dir, filter) {
|
|
7698
7709
|
var files = readdirSync(dir).filter(filter);
|
|
7699
|
-
return files[0] &&
|
|
7710
|
+
return files[0] && path4.join(dir, files[0]);
|
|
7700
7711
|
}
|
|
7701
7712
|
function matchBuild(name) {
|
|
7702
7713
|
return /\.node$/.test(name);
|
|
@@ -7783,7 +7794,7 @@ var require_node_gyp_build = __commonJS({
|
|
|
7783
7794
|
return typeof window !== "undefined" && window.process && window.process.type === "renderer";
|
|
7784
7795
|
}
|
|
7785
7796
|
function isAlpine(platform2) {
|
|
7786
|
-
return platform2 === "linux" &&
|
|
7797
|
+
return platform2 === "linux" && fs4.existsSync("/etc/alpine-release");
|
|
7787
7798
|
}
|
|
7788
7799
|
load.parseTags = parseTags;
|
|
7789
7800
|
load.matchTags = matchTags;
|
|
@@ -11502,110 +11513,6 @@ var _systemSchema = defineSchema({
|
|
|
11502
11513
|
})
|
|
11503
11514
|
});
|
|
11504
11515
|
|
|
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
11516
|
// src/cron-mapping.ts
|
|
11610
11517
|
function formatSchedule(s) {
|
|
11611
11518
|
switch (s.kind) {
|
|
@@ -11731,6 +11638,7 @@ var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivity
|
|
|
11731
11638
|
var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
|
|
11732
11639
|
var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
|
|
11733
11640
|
var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
|
|
11641
|
+
var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
|
|
11734
11642
|
var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
|
|
11735
11643
|
var HOT_KEY = "__cohort_sync__";
|
|
11736
11644
|
function getHotState() {
|
|
@@ -11745,10 +11653,11 @@ function getHotState() {
|
|
|
11745
11653
|
intervals: { heartbeat: null, activityFlush: null },
|
|
11746
11654
|
activityBuffer: [],
|
|
11747
11655
|
channelAgentBridge: {},
|
|
11748
|
-
gatewayWs: null,
|
|
11749
11656
|
gatewayPort: null,
|
|
11750
11657
|
gatewayToken: null,
|
|
11751
|
-
|
|
11658
|
+
gatewayProtocolClient: null,
|
|
11659
|
+
commandSubscription: null,
|
|
11660
|
+
cronRunNowPoll: null
|
|
11752
11661
|
};
|
|
11753
11662
|
globalThis[HOT_KEY] = state;
|
|
11754
11663
|
}
|
|
@@ -11793,6 +11702,13 @@ function restoreFromHotReload(logger) {
|
|
|
11793
11702
|
client = state.client;
|
|
11794
11703
|
savedConvexUrl = state.convexUrl;
|
|
11795
11704
|
logger.info("cohort-sync: recovered ConvexClient after hot-reload");
|
|
11705
|
+
} else if (client && state.client && client !== state.client) {
|
|
11706
|
+
try {
|
|
11707
|
+
state.client.close();
|
|
11708
|
+
} catch {
|
|
11709
|
+
}
|
|
11710
|
+
state.client = client;
|
|
11711
|
+
logger.info("cohort-sync: closed orphaned ConvexClient from hot state");
|
|
11796
11712
|
}
|
|
11797
11713
|
if (unsubscribers.length === 0 && state.unsubscribers.length > 0) {
|
|
11798
11714
|
unsubscribers.push(...state.unsubscribers);
|
|
@@ -11904,98 +11820,132 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
|
|
|
11904
11820
|
return;
|
|
11905
11821
|
}
|
|
11906
11822
|
if (cmd.type.startsWith("cron")) {
|
|
11907
|
-
const
|
|
11908
|
-
|
|
11909
|
-
|
|
11823
|
+
const hotState = getHotState();
|
|
11824
|
+
const port = hotState.gatewayPort;
|
|
11825
|
+
const token = hotState.gatewayToken;
|
|
11826
|
+
if (!port || !token) {
|
|
11827
|
+
logger.warn(`cohort-sync: no gateway port/token, cannot execute ${cmd.type}`);
|
|
11910
11828
|
continue;
|
|
11911
11829
|
}
|
|
11912
|
-
const
|
|
11913
|
-
|
|
11914
|
-
await
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11830
|
+
const gwClient = new GatewayClient(port, token, logger);
|
|
11831
|
+
try {
|
|
11832
|
+
await gwClient.connect();
|
|
11833
|
+
const nameMap = cfg.agentNameMap ?? {};
|
|
11834
|
+
switch (cmd.type) {
|
|
11835
|
+
case "cronEnable":
|
|
11836
|
+
await gwClient.request("cron.update", {
|
|
11837
|
+
jobId: cmd.payload?.jobId,
|
|
11838
|
+
patch: { enabled: true }
|
|
11839
|
+
});
|
|
11840
|
+
break;
|
|
11841
|
+
case "cronDisable":
|
|
11842
|
+
await gwClient.request("cron.update", {
|
|
11843
|
+
jobId: cmd.payload?.jobId,
|
|
11844
|
+
patch: { enabled: false }
|
|
11845
|
+
});
|
|
11846
|
+
break;
|
|
11847
|
+
case "cronDelete":
|
|
11848
|
+
await gwClient.request("cron.remove", {
|
|
11849
|
+
jobId: cmd.payload?.jobId
|
|
11850
|
+
});
|
|
11851
|
+
break;
|
|
11852
|
+
case "cronRunNow": {
|
|
11853
|
+
const runResult = await gwClient.request(
|
|
11854
|
+
"cron.run",
|
|
11855
|
+
{ jobId: cmd.payload?.jobId }
|
|
11856
|
+
);
|
|
11857
|
+
if (runResult?.ok && runResult?.ran) {
|
|
11858
|
+
const jobId = cmd.payload?.jobId;
|
|
11859
|
+
let polls = 0;
|
|
11860
|
+
const hs = getHotState();
|
|
11861
|
+
if (hs.cronRunNowPoll) clearInterval(hs.cronRunNowPoll);
|
|
11862
|
+
const pollInterval = setInterval(async () => {
|
|
11863
|
+
polls++;
|
|
11864
|
+
if (polls >= 15) {
|
|
11865
|
+
clearInterval(pollInterval);
|
|
11866
|
+
hs.cronRunNowPoll = null;
|
|
11867
|
+
return;
|
|
11868
|
+
}
|
|
11869
|
+
try {
|
|
11870
|
+
const pollClient = getHotState().gatewayProtocolClient;
|
|
11871
|
+
if (!pollClient || !pollClient.isAlive()) {
|
|
11872
|
+
clearInterval(pollInterval);
|
|
11873
|
+
hs.cronRunNowPoll = null;
|
|
11874
|
+
return;
|
|
11875
|
+
}
|
|
11876
|
+
const pollResult = await pollClient.request("cron.list");
|
|
11877
|
+
const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
|
|
11878
|
+
const job = freshJobs.find((j) => j.id === jobId);
|
|
11879
|
+
if (job && !job.state?.runningAtMs) {
|
|
11880
|
+
clearInterval(pollInterval);
|
|
11881
|
+
hs.cronRunNowPoll = null;
|
|
11882
|
+
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11883
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11884
|
+
}
|
|
11885
|
+
} catch {
|
|
11886
|
+
}
|
|
11887
|
+
}, 2e3);
|
|
11888
|
+
hs.cronRunNowPoll = pollInterval;
|
|
11941
11889
|
}
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11890
|
+
break;
|
|
11891
|
+
}
|
|
11892
|
+
case "cronCreate": {
|
|
11893
|
+
const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
|
|
11894
|
+
await gwClient.request("cron.add", {
|
|
11895
|
+
job: {
|
|
11896
|
+
agentId,
|
|
11897
|
+
name: cmd.payload?.name,
|
|
11898
|
+
enabled: true,
|
|
11899
|
+
schedule: cmd.payload?.schedule,
|
|
11900
|
+
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11901
|
+
sessionTarget: "isolated",
|
|
11902
|
+
wakeMode: "now"
|
|
11954
11903
|
}
|
|
11955
|
-
}
|
|
11956
|
-
|
|
11957
|
-
}
|
|
11904
|
+
});
|
|
11905
|
+
break;
|
|
11906
|
+
}
|
|
11907
|
+
case "cronUpdate": {
|
|
11908
|
+
const patch = {};
|
|
11909
|
+
if (cmd.payload?.name) patch.name = cmd.payload.name;
|
|
11910
|
+
if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
|
|
11911
|
+
if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
|
|
11912
|
+
if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
|
|
11913
|
+
await gwClient.request("cron.update", {
|
|
11914
|
+
jobId: cmd.payload?.jobId,
|
|
11915
|
+
patch
|
|
11916
|
+
});
|
|
11917
|
+
break;
|
|
11918
|
+
}
|
|
11919
|
+
default:
|
|
11920
|
+
logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
|
|
11958
11921
|
}
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11968
|
-
sessionTarget: "isolated",
|
|
11969
|
-
wakeMode: "now"
|
|
11922
|
+
if (gwClient.isAlive()) {
|
|
11923
|
+
try {
|
|
11924
|
+
const snapResult = await gwClient.request("cron.list");
|
|
11925
|
+
const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
|
|
11926
|
+
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11927
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11928
|
+
} catch (snapErr) {
|
|
11929
|
+
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11970
11930
|
}
|
|
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
11931
|
}
|
|
11932
|
+
} finally {
|
|
11933
|
+
gwClient.close();
|
|
11993
11934
|
}
|
|
11994
11935
|
} else {
|
|
11995
11936
|
logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
|
|
11996
11937
|
}
|
|
11997
11938
|
} catch (err) {
|
|
11998
11939
|
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
11940
|
+
try {
|
|
11941
|
+
await c.mutation(failCommandRef, {
|
|
11942
|
+
commandId: cmd._id,
|
|
11943
|
+
apiKey: cfg.apiKey,
|
|
11944
|
+
reason: String(err).slice(0, 500)
|
|
11945
|
+
});
|
|
11946
|
+
} catch (failErr) {
|
|
11947
|
+
logger.error(`cohort-sync: failed to mark command ${cmd._id} as failed: ${String(failErr)}`);
|
|
11948
|
+
}
|
|
11999
11949
|
}
|
|
12000
11950
|
}
|
|
12001
11951
|
} finally {
|
|
@@ -12044,6 +11994,13 @@ function closeSubscription() {
|
|
|
12044
11994
|
}
|
|
12045
11995
|
state.commandSubscription = null;
|
|
12046
11996
|
}
|
|
11997
|
+
if (state.gatewayProtocolClient) {
|
|
11998
|
+
try {
|
|
11999
|
+
state.gatewayProtocolClient.close();
|
|
12000
|
+
} catch {
|
|
12001
|
+
}
|
|
12002
|
+
state.gatewayProtocolClient = null;
|
|
12003
|
+
}
|
|
12047
12004
|
client?.close();
|
|
12048
12005
|
client = null;
|
|
12049
12006
|
clearHotState();
|
|
@@ -12093,7 +12050,9 @@ function clearIntervalsFromHot() {
|
|
|
12093
12050
|
const state = getHotState();
|
|
12094
12051
|
if (state.intervals.heartbeat) clearInterval(state.intervals.heartbeat);
|
|
12095
12052
|
if (state.intervals.activityFlush) clearInterval(state.intervals.activityFlush);
|
|
12053
|
+
if (state.cronRunNowPoll) clearInterval(state.cronRunNowPoll);
|
|
12096
12054
|
state.intervals = { heartbeat: null, activityFlush: null };
|
|
12055
|
+
state.cronRunNowPoll = null;
|
|
12097
12056
|
}
|
|
12098
12057
|
function addActivityToHot(entry) {
|
|
12099
12058
|
const state = getHotState();
|
|
@@ -12152,31 +12111,31 @@ var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
|
12152
12111
|
function normalizeStatus(status) {
|
|
12153
12112
|
return VALID_STATUSES.has(status) ? status : "idle";
|
|
12154
12113
|
}
|
|
12155
|
-
async function v1Get(apiUrl, apiKey,
|
|
12156
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${
|
|
12114
|
+
async function v1Get(apiUrl, apiKey, path4) {
|
|
12115
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12157
12116
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
12158
12117
|
signal: AbortSignal.timeout(1e4)
|
|
12159
12118
|
});
|
|
12160
|
-
if (!res.ok) throw new Error(`GET ${
|
|
12119
|
+
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
12161
12120
|
return res.json();
|
|
12162
12121
|
}
|
|
12163
|
-
async function v1Patch(apiUrl, apiKey,
|
|
12164
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${
|
|
12122
|
+
async function v1Patch(apiUrl, apiKey, path4, body) {
|
|
12123
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12165
12124
|
method: "PATCH",
|
|
12166
12125
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
12167
12126
|
body: JSON.stringify(body),
|
|
12168
12127
|
signal: AbortSignal.timeout(1e4)
|
|
12169
12128
|
});
|
|
12170
|
-
if (!res.ok) throw new Error(`PATCH ${
|
|
12129
|
+
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
12171
12130
|
}
|
|
12172
|
-
async function v1Post(apiUrl, apiKey,
|
|
12173
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${
|
|
12131
|
+
async function v1Post(apiUrl, apiKey, path4, body) {
|
|
12132
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12174
12133
|
method: "POST",
|
|
12175
12134
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
12176
12135
|
body: JSON.stringify(body),
|
|
12177
12136
|
signal: AbortSignal.timeout(1e4)
|
|
12178
12137
|
});
|
|
12179
|
-
if (!res.ok) throw new Error(`POST ${
|
|
12138
|
+
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
12180
12139
|
}
|
|
12181
12140
|
async function checkForUpdate(currentVersion, logger) {
|
|
12182
12141
|
try {
|
|
@@ -12344,64 +12303,542 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
|
|
|
12344
12303
|
logger.info("cohort-sync: full sync complete");
|
|
12345
12304
|
}
|
|
12346
12305
|
|
|
12347
|
-
// src/gateway-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12306
|
+
// src/gateway-client.ts
|
|
12307
|
+
import crypto2 from "node:crypto";
|
|
12308
|
+
|
|
12309
|
+
// src/diag.ts
|
|
12310
|
+
import fs from "node:fs";
|
|
12311
|
+
import path from "node:path";
|
|
12312
|
+
import os from "node:os";
|
|
12313
|
+
var LOG_DIR = path.join(os.homedir(), ".openclaw", "logs", "cohort-sync");
|
|
12314
|
+
var LOG_PATH = path.join(LOG_DIR, "diag.log");
|
|
12315
|
+
var LOG_PATH_ROTATED = path.join(LOG_DIR, "diag.log.1");
|
|
12316
|
+
var MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
12317
|
+
var isDebug = process.env.COHORT_SYNC_DEBUG === "1";
|
|
12318
|
+
try {
|
|
12319
|
+
fs.mkdirSync(LOG_DIR, { recursive: true, mode: 448 });
|
|
12320
|
+
} catch {
|
|
12321
|
+
}
|
|
12322
|
+
var writesSinceCheck = 0;
|
|
12323
|
+
function diag(label, data) {
|
|
12324
|
+
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")) {
|
|
12325
|
+
return;
|
|
12326
|
+
}
|
|
12327
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
12328
|
+
const sanitized = data ? " " + JSON.stringify(data, (_k, v2) => {
|
|
12329
|
+
if (typeof v2 === "string" && v2.length > 200) return v2.slice(0, 200) + "\u2026";
|
|
12330
|
+
return v2;
|
|
12331
|
+
}) : "";
|
|
12332
|
+
const line = `[${ts}] ${label}${sanitized}
|
|
12333
|
+
`;
|
|
12334
|
+
try {
|
|
12335
|
+
fs.appendFileSync(LOG_PATH, line, { mode: 384 });
|
|
12336
|
+
if (++writesSinceCheck >= 100) {
|
|
12337
|
+
writesSinceCheck = 0;
|
|
12338
|
+
const stat = fs.statSync(LOG_PATH);
|
|
12339
|
+
if (stat.size > MAX_LOG_SIZE) {
|
|
12340
|
+
try {
|
|
12341
|
+
fs.unlinkSync(LOG_PATH_ROTATED);
|
|
12342
|
+
} catch {
|
|
12343
|
+
}
|
|
12344
|
+
fs.renameSync(LOG_PATH, LOG_PATH_ROTATED);
|
|
12345
|
+
}
|
|
12358
12346
|
}
|
|
12359
|
-
}
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12347
|
+
} catch {
|
|
12348
|
+
}
|
|
12349
|
+
}
|
|
12350
|
+
|
|
12351
|
+
// src/device-identity-crypto.ts
|
|
12352
|
+
import crypto from "node:crypto";
|
|
12353
|
+
import fs2 from "node:fs";
|
|
12354
|
+
import path2 from "node:path";
|
|
12355
|
+
import os2 from "node:os";
|
|
12356
|
+
var IDENTITY_PATH = path2.join(os2.homedir(), ".openclaw", "extensions", "cohort-sync", ".device-identity.json");
|
|
12357
|
+
function loadOrCreateDeviceIdentity() {
|
|
12358
|
+
try {
|
|
12359
|
+
const data = JSON.parse(fs2.readFileSync(IDENTITY_PATH, "utf-8"));
|
|
12360
|
+
if (data.deviceId && data.publicKeyPem && data.privateKeyPem) {
|
|
12361
|
+
diag("GW_CLIENT_DEVICE_IDENTITY_LOADED", { deviceId: data.deviceId });
|
|
12362
|
+
return data;
|
|
12369
12363
|
}
|
|
12364
|
+
} catch {
|
|
12365
|
+
}
|
|
12366
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
12367
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
12368
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
12369
|
+
const publicKeyDer = publicKey.export({ type: "spki", format: "der" });
|
|
12370
|
+
const rawPublicKey = publicKeyDer.subarray(publicKeyDer.length - 32);
|
|
12371
|
+
const deviceId = crypto.createHash("sha256").update(rawPublicKey).digest("hex");
|
|
12372
|
+
const identity = { deviceId, publicKeyPem, privateKeyPem };
|
|
12373
|
+
try {
|
|
12374
|
+
fs2.writeFileSync(IDENTITY_PATH, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
12375
|
+
diag("GW_CLIENT_DEVICE_IDENTITY_CREATED", { deviceId });
|
|
12376
|
+
} catch (err) {
|
|
12377
|
+
diag("GW_CLIENT_DEVICE_IDENTITY_WRITE_FAILED", { error: String(err) });
|
|
12378
|
+
}
|
|
12379
|
+
return identity;
|
|
12380
|
+
}
|
|
12381
|
+
function normalizeMetadata(value) {
|
|
12382
|
+
if (typeof value !== "string") return "";
|
|
12383
|
+
const trimmed = value.trim();
|
|
12384
|
+
if (!trimmed) return "";
|
|
12385
|
+
return trimmed.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
|
|
12386
|
+
}
|
|
12387
|
+
function buildDeviceAuthPayloadV3(params) {
|
|
12388
|
+
return [
|
|
12389
|
+
"v3",
|
|
12390
|
+
params.deviceId,
|
|
12391
|
+
params.clientId,
|
|
12392
|
+
params.clientMode,
|
|
12393
|
+
params.role,
|
|
12394
|
+
params.scopes.join(","),
|
|
12395
|
+
String(params.signedAtMs),
|
|
12396
|
+
params.token ?? "",
|
|
12397
|
+
params.nonce,
|
|
12398
|
+
normalizeMetadata(params.platform),
|
|
12399
|
+
normalizeMetadata(params.deviceFamily)
|
|
12400
|
+
].join("|");
|
|
12401
|
+
}
|
|
12402
|
+
function signPayload(privateKeyPem, payload) {
|
|
12403
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
12404
|
+
const signature = crypto.sign(null, Buffer.from(payload, "utf-8"), key);
|
|
12405
|
+
return signature.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
12406
|
+
}
|
|
12407
|
+
|
|
12408
|
+
// src/gateway-client.ts
|
|
12409
|
+
var ALLOWED_METHODS = /* @__PURE__ */ new Set([
|
|
12410
|
+
"cron.list",
|
|
12411
|
+
"cron.add",
|
|
12412
|
+
"cron.update",
|
|
12413
|
+
"cron.remove",
|
|
12414
|
+
"cron.run",
|
|
12415
|
+
"cron.runs",
|
|
12416
|
+
"cron.status",
|
|
12417
|
+
"channels.status",
|
|
12418
|
+
"sessions.list",
|
|
12419
|
+
"sessions.preview",
|
|
12420
|
+
"agent",
|
|
12421
|
+
"snapshot",
|
|
12422
|
+
"system.presence"
|
|
12423
|
+
]);
|
|
12424
|
+
function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
12425
|
+
const signedAtMs = Date.now();
|
|
12426
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
12427
|
+
deviceId: identity.deviceId,
|
|
12428
|
+
clientId: "gateway-client",
|
|
12429
|
+
clientMode: "backend",
|
|
12430
|
+
role: "operator",
|
|
12431
|
+
scopes: ["operator.read", "operator.write"],
|
|
12432
|
+
signedAtMs,
|
|
12433
|
+
token,
|
|
12434
|
+
nonce,
|
|
12435
|
+
platform: process.platform,
|
|
12436
|
+
deviceFamily: null
|
|
12370
12437
|
});
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12438
|
+
const signature = signPayload(identity.privateKeyPem, payload);
|
|
12439
|
+
return {
|
|
12440
|
+
type: "req",
|
|
12441
|
+
id,
|
|
12442
|
+
method: "connect",
|
|
12443
|
+
params: {
|
|
12444
|
+
minProtocol: 3,
|
|
12445
|
+
maxProtocol: 3,
|
|
12446
|
+
client: {
|
|
12447
|
+
id: "gateway-client",
|
|
12448
|
+
version: pluginVersion,
|
|
12449
|
+
platform: process.platform,
|
|
12450
|
+
mode: "backend"
|
|
12451
|
+
},
|
|
12452
|
+
role: "operator",
|
|
12453
|
+
scopes: ["operator.read", "operator.write"],
|
|
12454
|
+
auth: { token },
|
|
12455
|
+
device: {
|
|
12456
|
+
id: identity.deviceId,
|
|
12457
|
+
publicKey: identity.publicKeyPem,
|
|
12458
|
+
signature,
|
|
12459
|
+
signedAt: signedAtMs,
|
|
12460
|
+
nonce
|
|
12461
|
+
},
|
|
12462
|
+
locale: "en-US",
|
|
12463
|
+
userAgent: `cohort-sync/${pluginVersion}`
|
|
12464
|
+
}
|
|
12465
|
+
};
|
|
12466
|
+
}
|
|
12467
|
+
function parseHelloOk(response) {
|
|
12468
|
+
if (!response.ok) {
|
|
12469
|
+
const msg = response.error?.message ?? "unknown error";
|
|
12470
|
+
throw new Error(`Gateway connect error: ${msg}`);
|
|
12471
|
+
}
|
|
12472
|
+
const payload = response.payload;
|
|
12473
|
+
if (payload?.type === "hello-error") {
|
|
12474
|
+
const msg = payload.message ?? "unknown error";
|
|
12475
|
+
throw new Error(`hello-error: ${msg}`);
|
|
12476
|
+
}
|
|
12477
|
+
if (payload?.type !== "hello-ok") {
|
|
12478
|
+
throw new Error(`Unexpected payload type: ${String(payload?.type ?? "missing")}`);
|
|
12479
|
+
}
|
|
12480
|
+
const policy = payload.policy ?? {};
|
|
12481
|
+
const methods = payload.methods ?? [];
|
|
12482
|
+
const events = payload.events ?? [];
|
|
12483
|
+
return {
|
|
12484
|
+
methods: new Set(methods),
|
|
12485
|
+
events: new Set(events),
|
|
12486
|
+
tickIntervalMs: policy.tickIntervalMs ?? 15e3,
|
|
12487
|
+
snapshot: payload.snapshot
|
|
12488
|
+
};
|
|
12489
|
+
}
|
|
12490
|
+
function getPendingRequests() {
|
|
12491
|
+
const g = globalThis;
|
|
12492
|
+
const hot = g.__cohort_sync__ ?? (g.__cohort_sync__ = {});
|
|
12493
|
+
if (!hot.pendingGatewayRequests) hot.pendingGatewayRequests = /* @__PURE__ */ new Map();
|
|
12494
|
+
return hot.pendingGatewayRequests;
|
|
12495
|
+
}
|
|
12496
|
+
var GatewayClient2 = class {
|
|
12497
|
+
port;
|
|
12498
|
+
logger;
|
|
12499
|
+
ws = null;
|
|
12500
|
+
alive = false;
|
|
12501
|
+
tickWatchdog = null;
|
|
12502
|
+
reconnectTimer = null;
|
|
12503
|
+
reconnectAttempts = 0;
|
|
12504
|
+
closed = false;
|
|
12505
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
12506
|
+
pluginVersion;
|
|
12507
|
+
tickIntervalMs = 15e3;
|
|
12508
|
+
// default; overwritten by hello-ok response
|
|
12509
|
+
deviceIdentity;
|
|
12510
|
+
/** Public Sets populated from hello-ok — consumers can inspect gateway capabilities */
|
|
12511
|
+
availableMethods = /* @__PURE__ */ new Set();
|
|
12512
|
+
availableEvents = /* @__PURE__ */ new Set();
|
|
12513
|
+
/**
|
|
12514
|
+
* @param port - Gateway WebSocket port
|
|
12515
|
+
* @param token - Auth token (stored in closure via constructor param, NOT on this or globalThis)
|
|
12516
|
+
* @param logger - RuntimeLogger from plugin SDK
|
|
12517
|
+
* @param pluginVersion - Version string for the connect frame userAgent
|
|
12518
|
+
*/
|
|
12519
|
+
constructor(port, token, logger, pluginVersion = "0.5.0") {
|
|
12520
|
+
this.port = port;
|
|
12521
|
+
this.logger = logger;
|
|
12522
|
+
this.pluginVersion = pluginVersion;
|
|
12523
|
+
this.deviceIdentity = loadOrCreateDeviceIdentity();
|
|
12524
|
+
this._getToken = () => token;
|
|
12525
|
+
}
|
|
12526
|
+
/** Token accessor — closure over constructor param */
|
|
12527
|
+
_getToken;
|
|
12528
|
+
/**
|
|
12529
|
+
* Connect to the gateway, perform the protocol v3 handshake, and start
|
|
12530
|
+
* the tick watchdog.
|
|
12531
|
+
*
|
|
12532
|
+
* Flow:
|
|
12533
|
+
* 1. Open WebSocket to ws://127.0.0.1:{port}
|
|
12534
|
+
* 2. Wait up to 500ms for connect.challenge event (optional)
|
|
12535
|
+
* 3. Send connect request frame
|
|
12536
|
+
* 4. Wait for hello-ok response
|
|
12537
|
+
* 5. Start tick watchdog at 2.5x tickIntervalMs
|
|
12538
|
+
*/
|
|
12539
|
+
async connect() {
|
|
12540
|
+
if (this.closed) throw new Error("GatewayClient has been closed");
|
|
12541
|
+
diag("GW_CLIENT_CONNECTING", { port: this.port });
|
|
12542
|
+
return new Promise((resolve, reject) => {
|
|
12543
|
+
const ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
|
|
12544
|
+
this.ws = ws;
|
|
12545
|
+
let settled = false;
|
|
12546
|
+
const settle = (err) => {
|
|
12547
|
+
if (settled) return;
|
|
12548
|
+
settled = true;
|
|
12549
|
+
if (err) {
|
|
12550
|
+
this.alive = false;
|
|
12551
|
+
reject(err);
|
|
12552
|
+
} else {
|
|
12553
|
+
resolve();
|
|
12554
|
+
}
|
|
12555
|
+
};
|
|
12556
|
+
const handshakeTimeout = setTimeout(() => {
|
|
12557
|
+
settle(new Error("Gateway handshake timed out after 10000ms"));
|
|
12558
|
+
ws.close();
|
|
12559
|
+
}, 1e4);
|
|
12560
|
+
ws.addEventListener("open", () => {
|
|
12561
|
+
diag("GW_CLIENT_WS_OPEN", { port: this.port });
|
|
12562
|
+
let challengeReceived = false;
|
|
12563
|
+
let challengeNonce = "";
|
|
12564
|
+
const challengeTimer = setTimeout(() => {
|
|
12565
|
+
if (!challengeReceived) {
|
|
12566
|
+
diag("GW_CLIENT_NO_CHALLENGE", { waited: 500 });
|
|
12567
|
+
sendConnect();
|
|
12568
|
+
}
|
|
12569
|
+
}, 500);
|
|
12570
|
+
const onChallengeMessage = (event) => {
|
|
12571
|
+
try {
|
|
12572
|
+
const data = JSON.parse(String(event.data));
|
|
12573
|
+
if (data.type === "event" && data.event === "connect.challenge") {
|
|
12574
|
+
challengeReceived = true;
|
|
12575
|
+
clearTimeout(challengeTimer);
|
|
12576
|
+
challengeNonce = data.payload?.nonce ?? "";
|
|
12577
|
+
diag("GW_CLIENT_CHALLENGE_RECEIVED", { hasNonce: !!challengeNonce });
|
|
12578
|
+
sendConnect();
|
|
12579
|
+
}
|
|
12580
|
+
} catch {
|
|
12581
|
+
}
|
|
12582
|
+
};
|
|
12583
|
+
ws.addEventListener("message", onChallengeMessage);
|
|
12584
|
+
const sendConnect = () => {
|
|
12585
|
+
ws.removeEventListener("message", onChallengeMessage);
|
|
12586
|
+
const id = crypto2.randomUUID();
|
|
12587
|
+
const frame = buildConnectFrame(
|
|
12588
|
+
id,
|
|
12589
|
+
this._getToken(),
|
|
12590
|
+
this.pluginVersion,
|
|
12591
|
+
this.deviceIdentity,
|
|
12592
|
+
challengeNonce
|
|
12593
|
+
);
|
|
12594
|
+
diag("GW_CLIENT_SENDING_CONNECT", { id, protocol: 3 });
|
|
12595
|
+
ws.send(JSON.stringify(frame));
|
|
12596
|
+
const onHelloMessage = (event) => {
|
|
12597
|
+
try {
|
|
12598
|
+
const data = JSON.parse(String(event.data));
|
|
12599
|
+
if (data.type === "res" && data.id === id) {
|
|
12600
|
+
ws.removeEventListener("message", onHelloMessage);
|
|
12601
|
+
clearTimeout(handshakeTimeout);
|
|
12602
|
+
try {
|
|
12603
|
+
const result = parseHelloOk(data);
|
|
12604
|
+
this.availableMethods = result.methods;
|
|
12605
|
+
this.availableEvents = result.events;
|
|
12606
|
+
this.tickIntervalMs = result.tickIntervalMs;
|
|
12607
|
+
this.alive = true;
|
|
12608
|
+
this.reconnectAttempts = 0;
|
|
12609
|
+
diag("GW_CLIENT_HELLO_OK", {
|
|
12610
|
+
methods: result.methods.size,
|
|
12611
|
+
events: result.events.size,
|
|
12612
|
+
tickIntervalMs: result.tickIntervalMs
|
|
12613
|
+
});
|
|
12614
|
+
this.startTickWatchdog(result.tickIntervalMs);
|
|
12615
|
+
ws.addEventListener("message", (ev) => this.handleMessage(ev));
|
|
12616
|
+
if (result.snapshot) {
|
|
12617
|
+
this.emit("snapshot", result.snapshot);
|
|
12618
|
+
}
|
|
12619
|
+
this.logger.info("cohort-sync: gateway client connected (protocol v3)");
|
|
12620
|
+
settle();
|
|
12621
|
+
} catch (err) {
|
|
12622
|
+
diag("GW_CLIENT_HELLO_FAILED", { error: String(err) });
|
|
12623
|
+
settle(err instanceof Error ? err : new Error(String(err)));
|
|
12624
|
+
ws.close();
|
|
12625
|
+
}
|
|
12626
|
+
}
|
|
12627
|
+
} catch {
|
|
12628
|
+
}
|
|
12629
|
+
};
|
|
12630
|
+
ws.addEventListener("message", onHelloMessage);
|
|
12631
|
+
};
|
|
12632
|
+
});
|
|
12633
|
+
ws.addEventListener("close", () => {
|
|
12634
|
+
clearTimeout(handshakeTimeout);
|
|
12635
|
+
this.alive = false;
|
|
12636
|
+
this.stopTickWatchdog();
|
|
12637
|
+
diag("GW_CLIENT_WS_CLOSED", { port: this.port });
|
|
12638
|
+
this.logger.warn("cohort-sync: gateway client WebSocket closed");
|
|
12639
|
+
const pending = getPendingRequests();
|
|
12640
|
+
for (const [, entry] of pending) {
|
|
12641
|
+
clearTimeout(entry.timer);
|
|
12642
|
+
entry.reject(new Error("Gateway WebSocket closed"));
|
|
12643
|
+
}
|
|
12644
|
+
pending.clear();
|
|
12645
|
+
if (!settled) {
|
|
12646
|
+
settle(new Error("Gateway WebSocket closed during handshake"));
|
|
12647
|
+
}
|
|
12648
|
+
if (!this.closed) {
|
|
12649
|
+
this.scheduleReconnect();
|
|
12650
|
+
}
|
|
12379
12651
|
});
|
|
12652
|
+
ws.addEventListener("error", (err) => {
|
|
12653
|
+
diag("GW_CLIENT_WS_ERROR", { error: String(err) });
|
|
12654
|
+
this.logger.error(`cohort-sync: gateway client WS error: ${String(err)}`);
|
|
12655
|
+
});
|
|
12656
|
+
});
|
|
12657
|
+
}
|
|
12658
|
+
/**
|
|
12659
|
+
* Whether the WebSocket is open and the handshake completed successfully.
|
|
12660
|
+
*/
|
|
12661
|
+
isAlive() {
|
|
12662
|
+
return this.alive && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
12663
|
+
}
|
|
12664
|
+
/**
|
|
12665
|
+
* Register an event handler for gateway events (tick, cron.changed, etc.).
|
|
12666
|
+
*/
|
|
12667
|
+
on(event, handler) {
|
|
12668
|
+
let handlers = this.eventHandlers.get(event);
|
|
12669
|
+
if (!handlers) {
|
|
12670
|
+
handlers = /* @__PURE__ */ new Set();
|
|
12671
|
+
this.eventHandlers.set(event, handlers);
|
|
12380
12672
|
}
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12673
|
+
handlers.add(handler);
|
|
12674
|
+
}
|
|
12675
|
+
/**
|
|
12676
|
+
* Send a request to the gateway and wait for the response.
|
|
12677
|
+
*
|
|
12678
|
+
* Validates the method against ALLOWED_METHODS to prevent accidentally
|
|
12679
|
+
* calling admin or unauthorized methods.
|
|
12680
|
+
*/
|
|
12681
|
+
async request(method, params, timeoutMs = 1e4) {
|
|
12682
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
12683
|
+
throw new Error(`Method "${method}" is not in ALLOWED_METHODS`);
|
|
12684
|
+
}
|
|
12685
|
+
if (!this.isAlive()) {
|
|
12686
|
+
throw new Error("Gateway client is not connected");
|
|
12687
|
+
}
|
|
12688
|
+
const id = crypto2.randomUUID();
|
|
12689
|
+
const frame = {
|
|
12690
|
+
type: "req",
|
|
12691
|
+
id,
|
|
12692
|
+
method,
|
|
12693
|
+
params
|
|
12694
|
+
};
|
|
12695
|
+
const pending = getPendingRequests();
|
|
12696
|
+
return new Promise((resolve, reject) => {
|
|
12697
|
+
const timer = setTimeout(() => {
|
|
12698
|
+
pending.delete(id);
|
|
12699
|
+
reject(new Error(`Gateway method "${method}" timed out after ${timeoutMs}ms`));
|
|
12700
|
+
}, timeoutMs);
|
|
12701
|
+
pending.set(id, {
|
|
12702
|
+
resolve,
|
|
12703
|
+
reject,
|
|
12704
|
+
timer
|
|
12390
12705
|
});
|
|
12706
|
+
this.ws.send(JSON.stringify(frame));
|
|
12707
|
+
});
|
|
12708
|
+
}
|
|
12709
|
+
/**
|
|
12710
|
+
* Clean shutdown — close WebSocket, clear timers, reject pending requests.
|
|
12711
|
+
*/
|
|
12712
|
+
close() {
|
|
12713
|
+
this.closed = true;
|
|
12714
|
+
this.alive = false;
|
|
12715
|
+
this.stopTickWatchdog();
|
|
12716
|
+
if (this.reconnectTimer) {
|
|
12717
|
+
clearTimeout(this.reconnectTimer);
|
|
12718
|
+
this.reconnectTimer = null;
|
|
12391
12719
|
}
|
|
12392
|
-
|
|
12393
|
-
|
|
12720
|
+
const pending = getPendingRequests();
|
|
12721
|
+
for (const [, entry] of pending) {
|
|
12722
|
+
clearTimeout(entry.timer);
|
|
12723
|
+
entry.reject(new Error("Gateway client closed"));
|
|
12724
|
+
}
|
|
12725
|
+
pending.clear();
|
|
12726
|
+
if (this.ws) {
|
|
12727
|
+
this.ws.close();
|
|
12728
|
+
this.ws = null;
|
|
12729
|
+
}
|
|
12730
|
+
diag("GW_CLIENT_CLOSED", { port: this.port });
|
|
12731
|
+
this.logger.info("cohort-sync: gateway client closed");
|
|
12732
|
+
}
|
|
12733
|
+
// -------------------------------------------------------------------------
|
|
12734
|
+
// Private methods
|
|
12735
|
+
// -------------------------------------------------------------------------
|
|
12736
|
+
/**
|
|
12737
|
+
* Route incoming WebSocket messages to the appropriate handler.
|
|
12738
|
+
*
|
|
12739
|
+
* Frame types:
|
|
12740
|
+
* - "res" → resolve/reject a pending request
|
|
12741
|
+
* - "event" → dispatch to registered event handlers; reset tick watchdog on tick
|
|
12742
|
+
*/
|
|
12743
|
+
handleMessage(event) {
|
|
12394
12744
|
try {
|
|
12395
|
-
const
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12745
|
+
const data = JSON.parse(String(event.data));
|
|
12746
|
+
if (data.type === "res") {
|
|
12747
|
+
this.handleResponse(data);
|
|
12748
|
+
} else if (data.type === "event") {
|
|
12749
|
+
this.handleEvent(data);
|
|
12750
|
+
}
|
|
12751
|
+
} catch {
|
|
12402
12752
|
}
|
|
12403
|
-
}
|
|
12404
|
-
|
|
12753
|
+
}
|
|
12754
|
+
handleResponse(frame) {
|
|
12755
|
+
const pending = getPendingRequests();
|
|
12756
|
+
const entry = pending.get(frame.id);
|
|
12757
|
+
if (!entry) return;
|
|
12758
|
+
pending.delete(frame.id);
|
|
12759
|
+
clearTimeout(entry.timer);
|
|
12760
|
+
if (frame.ok) {
|
|
12761
|
+
entry.resolve(frame.payload);
|
|
12762
|
+
} else {
|
|
12763
|
+
entry.reject(new Error(`Gateway method failed: ${frame.error?.message ?? "unknown error"}`));
|
|
12764
|
+
}
|
|
12765
|
+
}
|
|
12766
|
+
handleEvent(frame) {
|
|
12767
|
+
if (frame.event === "tick") {
|
|
12768
|
+
this.resetTickWatchdog();
|
|
12769
|
+
}
|
|
12770
|
+
this.emit(frame.event, frame.payload);
|
|
12771
|
+
}
|
|
12772
|
+
emit(event, payload) {
|
|
12773
|
+
const handlers = this.eventHandlers.get(event);
|
|
12774
|
+
if (handlers) {
|
|
12775
|
+
for (const handler of handlers) {
|
|
12776
|
+
try {
|
|
12777
|
+
handler(payload);
|
|
12778
|
+
} catch (err) {
|
|
12779
|
+
this.logger.error(`cohort-sync: event handler error for "${event}": ${String(err)}`);
|
|
12780
|
+
}
|
|
12781
|
+
}
|
|
12782
|
+
}
|
|
12783
|
+
}
|
|
12784
|
+
/**
|
|
12785
|
+
* Start the tick watchdog timer.
|
|
12786
|
+
*
|
|
12787
|
+
* The gateway sends tick events at tickIntervalMs. If we don't receive
|
|
12788
|
+
* one within 2.5x that interval, the connection is considered dead.
|
|
12789
|
+
*/
|
|
12790
|
+
startTickWatchdog(tickIntervalMs) {
|
|
12791
|
+
this.stopTickWatchdog();
|
|
12792
|
+
const watchdogMs = Math.round(tickIntervalMs * 2.5);
|
|
12793
|
+
this.tickWatchdog = setTimeout(() => {
|
|
12794
|
+
diag("GW_CLIENT_TICK_TIMEOUT", { watchdogMs });
|
|
12795
|
+
this.logger.warn(`cohort-sync: tick watchdog expired after ${watchdogMs}ms \u2014 closing connection`);
|
|
12796
|
+
this.alive = false;
|
|
12797
|
+
this.ws?.close();
|
|
12798
|
+
}, watchdogMs);
|
|
12799
|
+
}
|
|
12800
|
+
resetTickWatchdog() {
|
|
12801
|
+
if (this.tickWatchdog) {
|
|
12802
|
+
this.startTickWatchdog(this.tickIntervalMs);
|
|
12803
|
+
}
|
|
12804
|
+
}
|
|
12805
|
+
stopTickWatchdog() {
|
|
12806
|
+
if (this.tickWatchdog) {
|
|
12807
|
+
clearTimeout(this.tickWatchdog);
|
|
12808
|
+
this.tickWatchdog = null;
|
|
12809
|
+
}
|
|
12810
|
+
}
|
|
12811
|
+
/**
|
|
12812
|
+
* Schedule a reconnection attempt with exponential backoff and jitter.
|
|
12813
|
+
*
|
|
12814
|
+
* Backoff: 1s base, doubles each attempt, capped at 30s.
|
|
12815
|
+
* Jitter: +/- 25% randomization to avoid thundering herd.
|
|
12816
|
+
*/
|
|
12817
|
+
scheduleReconnect() {
|
|
12818
|
+
if (this.closed) return;
|
|
12819
|
+
const baseMs = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
12820
|
+
const jitter = baseMs * 0.25 * (Math.random() * 2 - 1);
|
|
12821
|
+
const delayMs = Math.round(Math.max(baseMs + jitter, 1e3));
|
|
12822
|
+
this.reconnectAttempts++;
|
|
12823
|
+
diag("GW_CLIENT_RECONNECT_SCHEDULED", {
|
|
12824
|
+
attempt: this.reconnectAttempts,
|
|
12825
|
+
delayMs
|
|
12826
|
+
});
|
|
12827
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
12828
|
+
this.reconnectTimer = null;
|
|
12829
|
+
try {
|
|
12830
|
+
diag("GW_CLIENT_RECONNECTING", { attempt: this.reconnectAttempts });
|
|
12831
|
+
await this.connect();
|
|
12832
|
+
diag("GW_CLIENT_RECONNECTED", { attempt: this.reconnectAttempts });
|
|
12833
|
+
} catch (err) {
|
|
12834
|
+
diag("GW_CLIENT_RECONNECT_FAILED", {
|
|
12835
|
+
attempt: this.reconnectAttempts,
|
|
12836
|
+
error: String(err)
|
|
12837
|
+
});
|
|
12838
|
+
}
|
|
12839
|
+
}, delayMs);
|
|
12840
|
+
}
|
|
12841
|
+
};
|
|
12405
12842
|
|
|
12406
12843
|
// src/agent-state.ts
|
|
12407
12844
|
import { basename } from "node:path";
|
|
@@ -12902,20 +13339,6 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
|
|
|
12902
13339
|
|
|
12903
13340
|
// src/hooks.ts
|
|
12904
13341
|
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
13342
|
function dumpCtx(ctx) {
|
|
12920
13343
|
if (!ctx || typeof ctx !== "object") return { _raw: String(ctx) };
|
|
12921
13344
|
const out = {};
|
|
@@ -12940,16 +13363,64 @@ function dumpEvent(event) {
|
|
|
12940
13363
|
}
|
|
12941
13364
|
var PLUGIN_VERSION = "unknown";
|
|
12942
13365
|
try {
|
|
12943
|
-
const pkgPath =
|
|
12944
|
-
const pkgJson = JSON.parse(
|
|
13366
|
+
const pkgPath = path3.join(path3.dirname(new URL(import.meta.url).pathname), "package.json");
|
|
13367
|
+
const pkgJson = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
|
|
12945
13368
|
PLUGIN_VERSION = pkgJson.version ?? "unknown";
|
|
12946
13369
|
} catch {
|
|
12947
13370
|
}
|
|
12948
13371
|
diag("MODULE_LOADED", { BUILD_ID, PLUGIN_VERSION });
|
|
13372
|
+
var lastCronSnapshotJson = "";
|
|
13373
|
+
function resolveGatewayToken(api) {
|
|
13374
|
+
const rawToken = api.config?.gateway?.auth?.token;
|
|
13375
|
+
if (typeof rawToken === "string") return rawToken;
|
|
13376
|
+
if (rawToken && typeof rawToken === "object" && rawToken.source === "env") {
|
|
13377
|
+
return process.env[rawToken.id] ?? null;
|
|
13378
|
+
}
|
|
13379
|
+
return null;
|
|
13380
|
+
}
|
|
13381
|
+
async function quickCronSync(port, token, cfg, resolveAgentName, logger) {
|
|
13382
|
+
const client2 = new GatewayClient2(port, token, logger, PLUGIN_VERSION);
|
|
13383
|
+
try {
|
|
13384
|
+
await client2.connect();
|
|
13385
|
+
const result = await client2.request("cron.list", { includeDisabled: true });
|
|
13386
|
+
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 });
|
|
13387
|
+
const jobs = Array.isArray(result) ? result : result?.jobs ?? [];
|
|
13388
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13389
|
+
const serialized = JSON.stringify(mapped);
|
|
13390
|
+
if (serialized !== lastCronSnapshotJson) {
|
|
13391
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13392
|
+
lastCronSnapshotJson = serialized;
|
|
13393
|
+
diag("HEARTBEAT_CRON_PUSHED", { count: mapped.length });
|
|
13394
|
+
} else {
|
|
13395
|
+
diag("HEARTBEAT_CRON_UNCHANGED", {});
|
|
13396
|
+
}
|
|
13397
|
+
} finally {
|
|
13398
|
+
client2.close();
|
|
13399
|
+
}
|
|
13400
|
+
}
|
|
13401
|
+
function registerCronEventHandlers(client2, cfg, resolveAgentName) {
|
|
13402
|
+
if (client2.availableEvents.has("cron")) {
|
|
13403
|
+
let debounceTimer = null;
|
|
13404
|
+
client2.on("cron", () => {
|
|
13405
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
13406
|
+
debounceTimer = setTimeout(async () => {
|
|
13407
|
+
try {
|
|
13408
|
+
const cronResult = await client2.request("cron.list", { includeDisabled: true });
|
|
13409
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
13410
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13411
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13412
|
+
diag("CRON_EVENT_PUSHED", { count: mapped.length });
|
|
13413
|
+
} catch (err) {
|
|
13414
|
+
diag("CRON_EVENT_PUSH_FAILED", { error: String(err) });
|
|
13415
|
+
}
|
|
13416
|
+
}, 2e3);
|
|
13417
|
+
});
|
|
13418
|
+
}
|
|
13419
|
+
}
|
|
12949
13420
|
function parseIdentityFile(workspaceDir) {
|
|
12950
13421
|
try {
|
|
12951
|
-
const filePath =
|
|
12952
|
-
const content =
|
|
13422
|
+
const filePath = path3.join(workspaceDir, "IDENTITY.md");
|
|
13423
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
12953
13424
|
const identity = {};
|
|
12954
13425
|
for (const line of content.split(/\r?\n/)) {
|
|
12955
13426
|
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
|
@@ -12987,10 +13458,7 @@ function getOrCreateTracker() {
|
|
|
12987
13458
|
state.tracker = fresh;
|
|
12988
13459
|
return fresh;
|
|
12989
13460
|
}
|
|
12990
|
-
var STATE_FILE_PATH =
|
|
12991
|
-
path.dirname(new URL(import.meta.url).pathname),
|
|
12992
|
-
".session-state.json"
|
|
12993
|
-
);
|
|
13461
|
+
var STATE_FILE_PATH = "";
|
|
12994
13462
|
function saveSessionsToDisk(tracker) {
|
|
12995
13463
|
try {
|
|
12996
13464
|
const state = tracker.exportState();
|
|
@@ -13005,14 +13473,14 @@ function saveSessionsToDisk(tracker) {
|
|
|
13005
13473
|
data.sessions.push({ agentName: name, key });
|
|
13006
13474
|
}
|
|
13007
13475
|
}
|
|
13008
|
-
|
|
13476
|
+
fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
|
|
13009
13477
|
} catch {
|
|
13010
13478
|
}
|
|
13011
13479
|
}
|
|
13012
13480
|
function loadSessionsFromDisk(tracker, logger) {
|
|
13013
13481
|
try {
|
|
13014
|
-
if (!
|
|
13015
|
-
const data = JSON.parse(
|
|
13482
|
+
if (!fs3.existsSync(STATE_FILE_PATH)) return;
|
|
13483
|
+
const data = JSON.parse(fs3.readFileSync(STATE_FILE_PATH, "utf8"));
|
|
13016
13484
|
if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
|
|
13017
13485
|
logger.info("cohort-sync: disk session state too old (>24h), skipping");
|
|
13018
13486
|
return;
|
|
@@ -13054,12 +13522,12 @@ async function fetchAgentContext(apiKey, apiUrl, logger) {
|
|
|
13054
13522
|
}
|
|
13055
13523
|
}
|
|
13056
13524
|
function registerHooks(api, cfg) {
|
|
13525
|
+
STATE_FILE_PATH = path3.join(cfg.stateDir, "session-state.json");
|
|
13057
13526
|
const { logger, config } = api;
|
|
13058
13527
|
const nameMap = cfg.agentNameMap;
|
|
13059
13528
|
const tracker = getOrCreateTracker();
|
|
13060
13529
|
let heartbeatInterval = null;
|
|
13061
13530
|
let activityFlushInterval = null;
|
|
13062
|
-
registerCronGatewayMethods(api);
|
|
13063
13531
|
logger.info(`cohort-sync: registerHooks [${BUILD_ID}]`);
|
|
13064
13532
|
diag("REGISTER_HOOKS", {
|
|
13065
13533
|
BUILD_ID,
|
|
@@ -13179,18 +13647,19 @@ function registerHooks(api, cfg) {
|
|
|
13179
13647
|
logger.info(`cohort-sync: flushed ${pendingActivity.length} pending activity entries before hot-reload`);
|
|
13180
13648
|
}
|
|
13181
13649
|
clearIntervalsFromHot();
|
|
13650
|
+
const heartbeatMs = cfg.syncIntervalMs ?? 12e4;
|
|
13182
13651
|
heartbeatInterval = setInterval(() => {
|
|
13183
13652
|
pushHeartbeat().catch((err) => {
|
|
13184
13653
|
logger.warn(`cohort-sync: heartbeat tick failed: ${String(err)}`);
|
|
13185
13654
|
});
|
|
13186
|
-
},
|
|
13655
|
+
}, heartbeatMs);
|
|
13187
13656
|
activityFlushInterval = setInterval(() => {
|
|
13188
13657
|
flushActivityBuffer().catch((err) => {
|
|
13189
13658
|
logger.warn(`cohort-sync: activity flush tick failed: ${String(err)}`);
|
|
13190
13659
|
});
|
|
13191
13660
|
}, 3e3);
|
|
13192
13661
|
saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
|
|
13193
|
-
logger.info(
|
|
13662
|
+
logger.info(`cohort-sync: intervals created (heartbeat=${heartbeatMs / 1e3}s, activityFlush=3s)`);
|
|
13194
13663
|
{
|
|
13195
13664
|
const hotState = getHotState();
|
|
13196
13665
|
if (hotState.commandSubscription) {
|
|
@@ -13200,6 +13669,19 @@ function registerHooks(api, cfg) {
|
|
|
13200
13669
|
const unsub = initCommandSubscription(cfg, logger, resolveAgentName);
|
|
13201
13670
|
hotState.commandSubscription = unsub;
|
|
13202
13671
|
}
|
|
13672
|
+
{
|
|
13673
|
+
const port = api.config?.gateway?.port;
|
|
13674
|
+
const token = resolveGatewayToken(api);
|
|
13675
|
+
if (port && token) {
|
|
13676
|
+
const hotState = getHotState();
|
|
13677
|
+
hotState.gatewayPort = port;
|
|
13678
|
+
hotState.gatewayToken = token;
|
|
13679
|
+
diag("REGISTER_HOOKS_CRON_SYNC", { port });
|
|
13680
|
+
quickCronSync(port, token, cfg, resolveAgentName, logger).catch((err) => {
|
|
13681
|
+
diag("REGISTER_HOOKS_CRON_SYNC_FAILED", { error: String(err) });
|
|
13682
|
+
});
|
|
13683
|
+
}
|
|
13684
|
+
}
|
|
13203
13685
|
api.registerTool((toolCtx) => {
|
|
13204
13686
|
const agentId = toolCtx.agentId ?? "main";
|
|
13205
13687
|
const agentName = resolveAgentName(agentId);
|
|
@@ -13349,18 +13831,6 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13349
13831
|
}
|
|
13350
13832
|
}
|
|
13351
13833
|
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
13834
|
logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
|
|
13365
13835
|
}
|
|
13366
13836
|
async function flushActivityBuffer() {
|
|
@@ -13400,20 +13870,28 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13400
13870
|
diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: { ...getHotState().channelAgentBridge } });
|
|
13401
13871
|
const hotState = getHotState();
|
|
13402
13872
|
hotState.gatewayPort = event.port;
|
|
13403
|
-
const
|
|
13404
|
-
if (
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13873
|
+
const token = resolveGatewayToken(api);
|
|
13874
|
+
if (token) {
|
|
13875
|
+
diag("GW_CLIENT_CONNECTING", { port: event.port, hasToken: true });
|
|
13876
|
+
try {
|
|
13877
|
+
const client2 = new GatewayClient2(event.port, token, logger, PLUGIN_VERSION);
|
|
13878
|
+
await client2.connect();
|
|
13879
|
+
hotState.gatewayProtocolClient = client2;
|
|
13880
|
+
registerCronEventHandlers(client2, cfg, resolveAgentName);
|
|
13881
|
+
diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
|
|
13882
|
+
const cronResult = await client2.request("cron.list", { includeDisabled: true });
|
|
13883
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
13884
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13885
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13886
|
+
lastCronSnapshotJson = JSON.stringify(mapped);
|
|
13887
|
+
diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
|
|
13888
|
+
} catch (err) {
|
|
13889
|
+
diag("GW_CLIENT_CONNECT_FAILED", { error: String(err) });
|
|
13890
|
+
logger.error(`cohort-sync: gateway client connect failed: ${String(err)}`);
|
|
13416
13891
|
}
|
|
13892
|
+
} else {
|
|
13893
|
+
diag("GW_CLIENT_NO_TOKEN", {});
|
|
13894
|
+
logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
|
|
13417
13895
|
}
|
|
13418
13896
|
await initSubscription(
|
|
13419
13897
|
event.port,
|
|
@@ -13857,7 +14335,7 @@ function registerCohortCli(ctx, cfg) {
|
|
|
13857
14335
|
agents.push({ id, name });
|
|
13858
14336
|
}
|
|
13859
14337
|
} else {
|
|
13860
|
-
agents.push({ id: "main", name: "
|
|
14338
|
+
agents.push({ id: "main", name: "main" });
|
|
13861
14339
|
}
|
|
13862
14340
|
const manifest = { agents };
|
|
13863
14341
|
logger.info("cohort: Starting device authorization...");
|
|
@@ -13917,40 +14395,10 @@ function registerCohortCli(ctx, cfg) {
|
|
|
13917
14395
|
// index.ts
|
|
13918
14396
|
init_keychain();
|
|
13919
14397
|
var DEFAULT_API_URL = "https://fortunate-chipmunk-286.convex.site";
|
|
13920
|
-
async function doActivate(api) {
|
|
13921
|
-
const cfg = api.pluginConfig;
|
|
13922
|
-
const apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
|
|
13923
|
-
api.logger.info(`cohort-sync: activating (api: ${apiUrl})`);
|
|
13924
|
-
let apiKey = cfg?.apiKey;
|
|
13925
|
-
if (!apiKey) {
|
|
13926
|
-
try {
|
|
13927
|
-
apiKey = await getCredential(apiUrl) ?? void 0;
|
|
13928
|
-
} catch (err) {
|
|
13929
|
-
api.logger.error(
|
|
13930
|
-
`cohort-sync: keychain lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
13931
|
-
);
|
|
13932
|
-
}
|
|
13933
|
-
}
|
|
13934
|
-
if (!apiKey) {
|
|
13935
|
-
api.logger.warn(
|
|
13936
|
-
"cohort-sync: no API key found \u2014 run 'openclaw cohort auth' to authenticate"
|
|
13937
|
-
);
|
|
13938
|
-
return;
|
|
13939
|
-
}
|
|
13940
|
-
api.logger.info(`cohort-sync: activated (api: ${apiUrl})`);
|
|
13941
|
-
registerHooks(api, {
|
|
13942
|
-
apiUrl,
|
|
13943
|
-
apiKey,
|
|
13944
|
-
agentNameMap: cfg?.agentNameMap
|
|
13945
|
-
});
|
|
13946
|
-
}
|
|
13947
14398
|
var plugin = {
|
|
13948
14399
|
id: "cohort-sync",
|
|
13949
14400
|
name: "Cohort Sync",
|
|
13950
14401
|
description: "Syncs agent status and skills to Cohort dashboard",
|
|
13951
|
-
// register() is synchronous — the SDK does not await it.
|
|
13952
|
-
// We register CLI here, then self-activate async (the gateway does not
|
|
13953
|
-
// call activate() for extension-directory plugins).
|
|
13954
14402
|
register(api) {
|
|
13955
14403
|
const cfg = api.pluginConfig;
|
|
13956
14404
|
const apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
|
|
@@ -13962,15 +14410,40 @@ var plugin = {
|
|
|
13962
14410
|
}),
|
|
13963
14411
|
{ commands: ["cohort"] }
|
|
13964
14412
|
);
|
|
13965
|
-
|
|
13966
|
-
|
|
13967
|
-
|
|
13968
|
-
|
|
13969
|
-
|
|
13970
|
-
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
14413
|
+
api.registerService({
|
|
14414
|
+
id: "cohort-sync-core",
|
|
14415
|
+
async start(svcCtx) {
|
|
14416
|
+
api.logger.info(`cohort-sync: service starting (api: ${apiUrl})`);
|
|
14417
|
+
let apiKey = cfg?.apiKey;
|
|
14418
|
+
if (!apiKey) {
|
|
14419
|
+
try {
|
|
14420
|
+
apiKey = await getCredential(apiUrl) ?? void 0;
|
|
14421
|
+
} catch (err) {
|
|
14422
|
+
api.logger.error(
|
|
14423
|
+
`cohort-sync: keychain lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
14424
|
+
);
|
|
14425
|
+
}
|
|
14426
|
+
}
|
|
14427
|
+
if (!apiKey) {
|
|
14428
|
+
api.logger.warn(
|
|
14429
|
+
"cohort-sync: no API key found \u2014 run 'openclaw cohort auth' to authenticate"
|
|
14430
|
+
);
|
|
14431
|
+
return;
|
|
14432
|
+
}
|
|
14433
|
+
api.logger.info(`cohort-sync: activated (api: ${apiUrl})`);
|
|
14434
|
+
registerHooks(api, {
|
|
14435
|
+
apiUrl,
|
|
14436
|
+
apiKey,
|
|
14437
|
+
stateDir: svcCtx.stateDir,
|
|
14438
|
+
agentNameMap: cfg?.agentNameMap,
|
|
14439
|
+
syncIntervalMs: cfg?.syncIntervalMs
|
|
14440
|
+
});
|
|
14441
|
+
},
|
|
14442
|
+
async stop() {
|
|
14443
|
+
closeSubscription();
|
|
14444
|
+
api.logger.info("cohort-sync: service stopped");
|
|
14445
|
+
}
|
|
14446
|
+
});
|
|
13974
14447
|
}
|
|
13975
14448
|
};
|
|
13976
14449
|
var index_default = plugin;
|