@cfio/cohort-sync 0.8.0 → 0.9.1
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 +645 -277
- package/dist/openclaw.plugin.json +5 -4
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,9 +16,9 @@ __export(keychain_exports, {
|
|
|
16
16
|
setCredential: () => setCredential
|
|
17
17
|
});
|
|
18
18
|
import { execFile } from "node:child_process";
|
|
19
|
-
import
|
|
19
|
+
import os4 from "node:os";
|
|
20
20
|
function assertMacOS(operation) {
|
|
21
|
-
if (
|
|
21
|
+
if (os4.platform() !== "darwin") {
|
|
22
22
|
throw new Error(
|
|
23
23
|
`cohort-sync: ${operation} requires macOS Keychain. On Linux/Windows, set your API key in OpenClaw config: plugins.entries.cohort-sync.config.apiKey`
|
|
24
24
|
);
|
|
@@ -44,20 +44,20 @@ function isNotFoundError(err) {
|
|
|
44
44
|
}
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
|
-
async function setCredential(
|
|
47
|
+
async function setCredential(apiUrl2, apiKey2) {
|
|
48
48
|
assertMacOS("storing credentials");
|
|
49
49
|
await securityCmd([
|
|
50
50
|
"add-generic-password",
|
|
51
51
|
"-s",
|
|
52
52
|
SERVICE,
|
|
53
53
|
"-a",
|
|
54
|
-
|
|
54
|
+
apiUrl2,
|
|
55
55
|
"-w",
|
|
56
|
-
|
|
56
|
+
apiKey2,
|
|
57
57
|
"-U"
|
|
58
58
|
]);
|
|
59
59
|
}
|
|
60
|
-
async function getCredential(
|
|
60
|
+
async function getCredential(apiUrl2) {
|
|
61
61
|
assertMacOS("reading credentials");
|
|
62
62
|
try {
|
|
63
63
|
const { stdout } = await securityCmd([
|
|
@@ -65,7 +65,7 @@ async function getCredential(apiUrl) {
|
|
|
65
65
|
"-s",
|
|
66
66
|
SERVICE,
|
|
67
67
|
"-a",
|
|
68
|
-
|
|
68
|
+
apiUrl2,
|
|
69
69
|
"-w"
|
|
70
70
|
]);
|
|
71
71
|
return stdout.trim();
|
|
@@ -74,7 +74,7 @@ async function getCredential(apiUrl) {
|
|
|
74
74
|
throw err;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
async function deleteCredential(
|
|
77
|
+
async function deleteCredential(apiUrl2) {
|
|
78
78
|
assertMacOS("deleting credentials");
|
|
79
79
|
try {
|
|
80
80
|
await securityCmd([
|
|
@@ -82,7 +82,7 @@ async function deleteCredential(apiUrl) {
|
|
|
82
82
|
"-s",
|
|
83
83
|
SERVICE,
|
|
84
84
|
"-a",
|
|
85
|
-
|
|
85
|
+
apiUrl2
|
|
86
86
|
]);
|
|
87
87
|
return true;
|
|
88
88
|
} catch (err) {
|
|
@@ -98,10 +98,6 @@ var init_keychain = __esm({
|
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
// src/hooks.ts
|
|
102
|
-
import fs3 from "node:fs";
|
|
103
|
-
import path3 from "node:path";
|
|
104
|
-
|
|
105
101
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
106
102
|
var value_exports = {};
|
|
107
103
|
__export(value_exports, {
|
|
@@ -2708,17 +2704,43 @@ __export(type_exports2, {
|
|
|
2708
2704
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/type/index.mjs
|
|
2709
2705
|
var Type = type_exports2;
|
|
2710
2706
|
|
|
2707
|
+
// src/hooks.ts
|
|
2708
|
+
import fs3 from "node:fs";
|
|
2709
|
+
import os3 from "node:os";
|
|
2710
|
+
import path3 from "node:path";
|
|
2711
|
+
|
|
2711
2712
|
// src/sync.ts
|
|
2712
2713
|
import { execSync } from "node:child_process";
|
|
2713
2714
|
function extractJson(raw) {
|
|
2714
2715
|
const jsonStart = raw.search(/[\[{]/);
|
|
2715
|
-
|
|
2716
|
-
const
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
2716
|
+
if (jsonStart === -1) throw new Error("No JSON found in output");
|
|
2717
|
+
const openChar = raw[jsonStart];
|
|
2718
|
+
const closeChar = openChar === "[" ? "]" : "}";
|
|
2719
|
+
let depth = 0;
|
|
2720
|
+
let inString = false;
|
|
2721
|
+
let escape = false;
|
|
2722
|
+
for (let i = jsonStart; i < raw.length; i++) {
|
|
2723
|
+
const ch = raw[i];
|
|
2724
|
+
if (escape) {
|
|
2725
|
+
escape = false;
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
if (ch === "\\") {
|
|
2729
|
+
escape = true;
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
if (ch === '"') {
|
|
2733
|
+
inString = !inString;
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
if (inString) continue;
|
|
2737
|
+
if (ch === openChar) depth++;
|
|
2738
|
+
else if (ch === closeChar) {
|
|
2739
|
+
depth--;
|
|
2740
|
+
if (depth === 0) return raw.slice(jsonStart, i + 1);
|
|
2741
|
+
}
|
|
2720
2742
|
}
|
|
2721
|
-
|
|
2743
|
+
throw new Error("No complete JSON found in output");
|
|
2722
2744
|
}
|
|
2723
2745
|
function fetchSkills(logger) {
|
|
2724
2746
|
try {
|
|
@@ -2745,32 +2767,45 @@ var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
|
2745
2767
|
function normalizeStatus(status) {
|
|
2746
2768
|
return VALID_STATUSES.has(status) ? status : "idle";
|
|
2747
2769
|
}
|
|
2748
|
-
async function v1Get(
|
|
2749
|
-
const res = await fetch(`${
|
|
2750
|
-
headers: { Authorization: `Bearer ${
|
|
2770
|
+
async function v1Get(apiUrl2, apiKey2, path4) {
|
|
2771
|
+
const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
|
|
2772
|
+
headers: { Authorization: `Bearer ${apiKey2}` },
|
|
2751
2773
|
signal: AbortSignal.timeout(1e4)
|
|
2752
2774
|
});
|
|
2753
2775
|
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
2754
2776
|
return res.json();
|
|
2755
2777
|
}
|
|
2756
|
-
async function v1Patch(
|
|
2757
|
-
const res = await fetch(`${
|
|
2778
|
+
async function v1Patch(apiUrl2, apiKey2, path4, body) {
|
|
2779
|
+
const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
|
|
2758
2780
|
method: "PATCH",
|
|
2759
|
-
headers: { Authorization: `Bearer ${
|
|
2781
|
+
headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
|
|
2760
2782
|
body: JSON.stringify(body),
|
|
2761
2783
|
signal: AbortSignal.timeout(1e4)
|
|
2762
2784
|
});
|
|
2763
2785
|
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
2764
2786
|
}
|
|
2765
|
-
async function v1Post(
|
|
2766
|
-
const res = await fetch(`${
|
|
2787
|
+
async function v1Post(apiUrl2, apiKey2, path4, body) {
|
|
2788
|
+
const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
|
|
2767
2789
|
method: "POST",
|
|
2768
|
-
headers: { Authorization: `Bearer ${
|
|
2790
|
+
headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
|
|
2769
2791
|
body: JSON.stringify(body),
|
|
2770
2792
|
signal: AbortSignal.timeout(1e4)
|
|
2771
2793
|
});
|
|
2772
2794
|
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
2773
2795
|
}
|
|
2796
|
+
function isNewerVersion(a, b) {
|
|
2797
|
+
const strip = (v2) => v2.replace(/-.*$/, "");
|
|
2798
|
+
const pa = strip(a).split(".").map(Number);
|
|
2799
|
+
const pb = strip(b).split(".").map(Number);
|
|
2800
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
2801
|
+
const na = pa[i] ?? 0;
|
|
2802
|
+
const nb = pb[i] ?? 0;
|
|
2803
|
+
if (isNaN(na) || isNaN(nb)) return false;
|
|
2804
|
+
if (na > nb) return true;
|
|
2805
|
+
if (na < nb) return false;
|
|
2806
|
+
}
|
|
2807
|
+
return false;
|
|
2808
|
+
}
|
|
2774
2809
|
async function checkForUpdate(currentVersion, logger) {
|
|
2775
2810
|
try {
|
|
2776
2811
|
const res = await fetch("https://registry.npmjs.org/@cfio/cohort-sync/latest", {
|
|
@@ -2779,9 +2814,9 @@ async function checkForUpdate(currentVersion, logger) {
|
|
|
2779
2814
|
if (!res.ok) return;
|
|
2780
2815
|
const data = await res.json();
|
|
2781
2816
|
const latest = data.version;
|
|
2782
|
-
if (latest && latest !== currentVersion) {
|
|
2817
|
+
if (latest && latest !== currentVersion && isNewerVersion(latest, currentVersion)) {
|
|
2783
2818
|
logger.warn(
|
|
2784
|
-
`cohort-sync: update available (${currentVersion} \u2192 ${latest}) \u2014 run "
|
|
2819
|
+
`cohort-sync: update available (${currentVersion} \u2192 ${latest}) \u2014 run "openclaw plugins install @cfio/cohort-sync" to update`
|
|
2785
2820
|
);
|
|
2786
2821
|
}
|
|
2787
2822
|
} catch {
|
|
@@ -2811,16 +2846,20 @@ async function syncAgentStatus(agentName, status, model, cfg, logger) {
|
|
|
2811
2846
|
}
|
|
2812
2847
|
}
|
|
2813
2848
|
async function syncSkillsToV1(skills, cfg, logger) {
|
|
2849
|
+
let synced = 0;
|
|
2814
2850
|
for (const skill of skills) {
|
|
2815
2851
|
try {
|
|
2816
2852
|
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills", {
|
|
2817
2853
|
name: skill.name,
|
|
2818
2854
|
description: skill.description
|
|
2819
2855
|
});
|
|
2856
|
+
synced++;
|
|
2857
|
+
if (synced % 5 === 0) await new Promise((r) => setTimeout(r, 500));
|
|
2820
2858
|
} catch (err) {
|
|
2821
2859
|
logger.warn(`cohort-sync: failed to sync skill "${skill.name}": ${String(err)}`);
|
|
2822
2860
|
}
|
|
2823
2861
|
}
|
|
2862
|
+
if (synced > 0) logger.info(`cohort-sync: synced ${synced}/${skills.length} skills`);
|
|
2824
2863
|
}
|
|
2825
2864
|
var lastKnownRoster = [];
|
|
2826
2865
|
function getLastKnownRoster() {
|
|
@@ -2930,6 +2969,9 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
|
|
|
2930
2969
|
logger.info("cohort-sync: full sync complete");
|
|
2931
2970
|
}
|
|
2932
2971
|
|
|
2972
|
+
// src/convex-bridge.ts
|
|
2973
|
+
import { createHash } from "crypto";
|
|
2974
|
+
|
|
2933
2975
|
// ../../node_modules/.pnpm/convex@1.33.0_patch_hash=l43bztwr6e2lbmpd6ao6hmcg24_react@19.2.1/node_modules/convex/dist/esm/index.js
|
|
2934
2976
|
var version = "1.33.0";
|
|
2935
2977
|
|
|
@@ -7860,14 +7902,14 @@ var require_node_gyp_build = __commonJS({
|
|
|
7860
7902
|
"../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
7861
7903
|
var fs4 = __require("fs");
|
|
7862
7904
|
var path4 = __require("path");
|
|
7863
|
-
var
|
|
7905
|
+
var os5 = __require("os");
|
|
7864
7906
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
7865
7907
|
var vars = process.config && process.config.variables || {};
|
|
7866
7908
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
7867
7909
|
var abi = process.versions.modules;
|
|
7868
7910
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
7869
|
-
var arch = process.env.npm_config_arch ||
|
|
7870
|
-
var platform = process.env.npm_config_platform ||
|
|
7911
|
+
var arch = process.env.npm_config_arch || os5.arch();
|
|
7912
|
+
var platform = process.env.npm_config_platform || os5.platform();
|
|
7871
7913
|
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
|
|
7872
7914
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
7873
7915
|
var uv = (process.versions.uv || "").split(".")[0];
|
|
@@ -10219,7 +10261,7 @@ var require_websocket = __commonJS({
|
|
|
10219
10261
|
var http = __require("http");
|
|
10220
10262
|
var net = __require("net");
|
|
10221
10263
|
var tls = __require("tls");
|
|
10222
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
10264
|
+
var { randomBytes, createHash: createHash2 } = __require("crypto");
|
|
10223
10265
|
var { Duplex, Readable } = __require("stream");
|
|
10224
10266
|
var { URL: URL2 } = __require("url");
|
|
10225
10267
|
var PerMessageDeflate = require_permessage_deflate();
|
|
@@ -10876,7 +10918,7 @@ var require_websocket = __commonJS({
|
|
|
10876
10918
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
10877
10919
|
return;
|
|
10878
10920
|
}
|
|
10879
|
-
const digest =
|
|
10921
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
10880
10922
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
10881
10923
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
10882
10924
|
return;
|
|
@@ -11141,7 +11183,7 @@ var require_websocket_server = __commonJS({
|
|
|
11141
11183
|
var EventEmitter = __require("events");
|
|
11142
11184
|
var http = __require("http");
|
|
11143
11185
|
var { Duplex } = __require("stream");
|
|
11144
|
-
var { createHash } = __require("crypto");
|
|
11186
|
+
var { createHash: createHash2 } = __require("crypto");
|
|
11145
11187
|
var extension = require_extension();
|
|
11146
11188
|
var PerMessageDeflate = require_permessage_deflate();
|
|
11147
11189
|
var subprotocol = require_subprotocol();
|
|
@@ -11436,7 +11478,7 @@ var require_websocket_server = __commonJS({
|
|
|
11436
11478
|
);
|
|
11437
11479
|
}
|
|
11438
11480
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
11439
|
-
const digest =
|
|
11481
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
11440
11482
|
const headers = [
|
|
11441
11483
|
"HTTP/1.1 101 Switching Protocols",
|
|
11442
11484
|
"Upgrade: websocket",
|
|
@@ -11773,12 +11815,40 @@ function reverseResolveAgentName(cohortName, forwardMap) {
|
|
|
11773
11815
|
}
|
|
11774
11816
|
|
|
11775
11817
|
// src/commands.ts
|
|
11776
|
-
var
|
|
11818
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
11819
|
+
var RATE_LIMIT_MAX = 10;
|
|
11820
|
+
var commandTimestamps = [];
|
|
11821
|
+
function checkRateLimit() {
|
|
11822
|
+
const now = Date.now();
|
|
11823
|
+
while (commandTimestamps.length > 0 && commandTimestamps[0] < now - RATE_LIMIT_WINDOW_MS) {
|
|
11824
|
+
commandTimestamps.shift();
|
|
11825
|
+
}
|
|
11826
|
+
if (commandTimestamps.length >= RATE_LIMIT_MAX) {
|
|
11827
|
+
return false;
|
|
11828
|
+
}
|
|
11829
|
+
commandTimestamps.push(now);
|
|
11830
|
+
return true;
|
|
11831
|
+
}
|
|
11832
|
+
var MAX_CRON_MESSAGE_LENGTH = 1e3;
|
|
11777
11833
|
async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
|
|
11834
|
+
logger.info(`cohort-sync: executing command type=${cmd.type} id=${cmd._id}`);
|
|
11835
|
+
if (!checkRateLimit()) {
|
|
11836
|
+
logger.warn(`cohort-sync: rate limit exceeded (>${RATE_LIMIT_MAX} commands/min), rejecting command ${cmd._id}`);
|
|
11837
|
+
throw new Error(`Rate limit exceeded: more than ${RATE_LIMIT_MAX} commands per minute`);
|
|
11838
|
+
}
|
|
11839
|
+
if (cmd.payload?.message && cmd.payload.message.length > MAX_CRON_MESSAGE_LENGTH) {
|
|
11840
|
+
logger.warn(`cohort-sync: cron message too long (${cmd.payload.message.length} chars, max ${MAX_CRON_MESSAGE_LENGTH}), rejecting command ${cmd._id}`);
|
|
11841
|
+
throw new Error(`Cron message exceeds maximum length of ${MAX_CRON_MESSAGE_LENGTH} characters`);
|
|
11842
|
+
}
|
|
11778
11843
|
if (cmd.type === "restart") {
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11844
|
+
if (gwClient && gwClient.isAlive()) {
|
|
11845
|
+
logger.warn(`cohort-sync: RESTART command received (id=${cmd._id}), issuing graceful gateway restart`);
|
|
11846
|
+
await gwClient.request("gateway.restart", { reason: "Cohort restart command" });
|
|
11847
|
+
} else {
|
|
11848
|
+
logger.warn(`cohort-sync: RESTART command received (id=${cmd._id}), no gateway client \u2014 falling back to SIGTERM`);
|
|
11849
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
11850
|
+
process.kill(process.pid, "SIGTERM");
|
|
11851
|
+
}
|
|
11782
11852
|
return;
|
|
11783
11853
|
}
|
|
11784
11854
|
if (cmd.type.startsWith("cron")) {
|
|
@@ -11806,41 +11876,7 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
|
|
|
11806
11876
|
});
|
|
11807
11877
|
break;
|
|
11808
11878
|
case "cronRunNow": {
|
|
11809
|
-
|
|
11810
|
-
"cron.run",
|
|
11811
|
-
{ jobId: cmd.payload?.jobId }
|
|
11812
|
-
);
|
|
11813
|
-
if (runResult?.ok && runResult?.ran) {
|
|
11814
|
-
const jobId = cmd.payload?.jobId;
|
|
11815
|
-
let polls = 0;
|
|
11816
|
-
if (cronRunNowPoll) clearInterval(cronRunNowPoll);
|
|
11817
|
-
const pollInterval = setInterval(async () => {
|
|
11818
|
-
polls++;
|
|
11819
|
-
if (polls >= 15) {
|
|
11820
|
-
clearInterval(pollInterval);
|
|
11821
|
-
cronRunNowPoll = null;
|
|
11822
|
-
return;
|
|
11823
|
-
}
|
|
11824
|
-
try {
|
|
11825
|
-
if (!gwClient || !gwClient.isAlive()) {
|
|
11826
|
-
clearInterval(pollInterval);
|
|
11827
|
-
cronRunNowPoll = null;
|
|
11828
|
-
return;
|
|
11829
|
-
}
|
|
11830
|
-
const pollResult = await gwClient.request("cron.list");
|
|
11831
|
-
const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
|
|
11832
|
-
const job = freshJobs.find((j) => j.id === jobId);
|
|
11833
|
-
if (job && !job.state?.runningAtMs) {
|
|
11834
|
-
clearInterval(pollInterval);
|
|
11835
|
-
cronRunNowPoll = null;
|
|
11836
|
-
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11837
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11838
|
-
}
|
|
11839
|
-
} catch {
|
|
11840
|
-
}
|
|
11841
|
-
}, 2e3);
|
|
11842
|
-
cronRunNowPoll = pollInterval;
|
|
11843
|
-
}
|
|
11879
|
+
await gwClient.request("cron.run", { jobId: cmd.payload?.jobId });
|
|
11844
11880
|
break;
|
|
11845
11881
|
}
|
|
11846
11882
|
case "cronCreate": {
|
|
@@ -11875,7 +11911,7 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
|
|
|
11875
11911
|
}
|
|
11876
11912
|
if (gwClient.isAlive()) {
|
|
11877
11913
|
try {
|
|
11878
|
-
const snapResult = await gwClient.request("cron.list");
|
|
11914
|
+
const snapResult = await gwClient.request("cron.list", { includeDisabled: true });
|
|
11879
11915
|
const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
|
|
11880
11916
|
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11881
11917
|
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
@@ -11889,8 +11925,15 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
|
|
|
11889
11925
|
}
|
|
11890
11926
|
|
|
11891
11927
|
// src/convex-bridge.ts
|
|
11892
|
-
function
|
|
11893
|
-
return
|
|
11928
|
+
function hashApiKey(key) {
|
|
11929
|
+
return createHash("sha256").update(key).digest("hex");
|
|
11930
|
+
}
|
|
11931
|
+
function deriveConvexUrl(apiUrl2) {
|
|
11932
|
+
const normalized = apiUrl2.replace(/\/+$/, "");
|
|
11933
|
+
if (/^https?:\/\/api\.cohort\.bot$/i.test(normalized)) {
|
|
11934
|
+
return normalized.replace(/api\.cohort\.bot$/i, "ws.cohort.bot");
|
|
11935
|
+
}
|
|
11936
|
+
return apiUrl2.replace(/\.convex\.site\/?$/, ".convex.cloud");
|
|
11894
11937
|
}
|
|
11895
11938
|
var savedLogger = null;
|
|
11896
11939
|
function setLogger(logger) {
|
|
@@ -11911,6 +11954,7 @@ function createClient(convexUrl) {
|
|
|
11911
11954
|
client.close();
|
|
11912
11955
|
}
|
|
11913
11956
|
savedConvexUrl = convexUrl;
|
|
11957
|
+
authCircuitOpen = false;
|
|
11914
11958
|
client = new ConvexClient(convexUrl);
|
|
11915
11959
|
return client;
|
|
11916
11960
|
}
|
|
@@ -11938,6 +11982,17 @@ function closeBridge() {
|
|
|
11938
11982
|
}
|
|
11939
11983
|
savedConvexUrl = null;
|
|
11940
11984
|
}
|
|
11985
|
+
var authCircuitOpen = false;
|
|
11986
|
+
function isUnauthorizedError(err) {
|
|
11987
|
+
return String(err).includes("Unauthorized");
|
|
11988
|
+
}
|
|
11989
|
+
function tripAuthCircuit() {
|
|
11990
|
+
if (authCircuitOpen) return;
|
|
11991
|
+
authCircuitOpen = true;
|
|
11992
|
+
getLogger().error(
|
|
11993
|
+
"cohort-sync: API key rejected \u2014 all outbound mutations disabled until gateway restart. Re-run `openclaw cohort auth` to issue a new key, then restart the gateway."
|
|
11994
|
+
);
|
|
11995
|
+
}
|
|
11941
11996
|
var commandUnsubscriber = null;
|
|
11942
11997
|
var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTelemetryFromPlugin");
|
|
11943
11998
|
var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
|
|
@@ -11949,62 +12004,101 @@ var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPend
|
|
|
11949
12004
|
var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
|
|
11950
12005
|
var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
|
|
11951
12006
|
var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
|
|
11952
|
-
async function pushTelemetry(
|
|
12007
|
+
async function pushTelemetry(apiKey2, data) {
|
|
12008
|
+
if (authCircuitOpen) return;
|
|
11953
12009
|
const c = getClient();
|
|
11954
12010
|
if (!c) return;
|
|
11955
12011
|
try {
|
|
11956
|
-
await c.mutation(upsertTelemetryFromPlugin, {
|
|
12012
|
+
await c.mutation(upsertTelemetryFromPlugin, { apiKeyHash: hashApiKey(apiKey2), ...data });
|
|
11957
12013
|
} catch (err) {
|
|
12014
|
+
if (isUnauthorizedError(err)) {
|
|
12015
|
+
tripAuthCircuit();
|
|
12016
|
+
return;
|
|
12017
|
+
}
|
|
11958
12018
|
getLogger().error(`cohort-sync: pushTelemetry failed: ${err}`);
|
|
11959
12019
|
}
|
|
11960
12020
|
}
|
|
11961
|
-
async function pushSessions(
|
|
12021
|
+
async function pushSessions(apiKey2, agentName, sessions) {
|
|
12022
|
+
if (authCircuitOpen) return;
|
|
11962
12023
|
const c = getClient();
|
|
11963
12024
|
if (!c) return;
|
|
11964
12025
|
try {
|
|
11965
|
-
await c.mutation(upsertSessionsFromPlugin, {
|
|
12026
|
+
await c.mutation(upsertSessionsFromPlugin, { apiKeyHash: hashApiKey(apiKey2), agentName, sessions });
|
|
11966
12027
|
} catch (err) {
|
|
12028
|
+
if (isUnauthorizedError(err)) {
|
|
12029
|
+
tripAuthCircuit();
|
|
12030
|
+
return;
|
|
12031
|
+
}
|
|
11967
12032
|
getLogger().error(`cohort-sync: pushSessions failed: ${err}`);
|
|
11968
12033
|
}
|
|
11969
12034
|
}
|
|
11970
|
-
async function pushActivity(
|
|
11971
|
-
if (entries.length === 0) return;
|
|
12035
|
+
async function pushActivity(apiKey2, entries) {
|
|
12036
|
+
if (authCircuitOpen || entries.length === 0) return;
|
|
11972
12037
|
const c = getClient();
|
|
11973
12038
|
if (!c) return;
|
|
11974
12039
|
try {
|
|
11975
|
-
await c.mutation(pushActivityFromPluginRef, {
|
|
12040
|
+
await c.mutation(pushActivityFromPluginRef, { apiKeyHash: hashApiKey(apiKey2), entries });
|
|
11976
12041
|
} catch (err) {
|
|
12042
|
+
if (isUnauthorizedError(err)) {
|
|
12043
|
+
tripAuthCircuit();
|
|
12044
|
+
return;
|
|
12045
|
+
}
|
|
11977
12046
|
getLogger().error(`cohort-sync: pushActivity failed: ${err}`);
|
|
11978
12047
|
}
|
|
11979
12048
|
}
|
|
11980
|
-
async function pushCronSnapshot(
|
|
12049
|
+
async function pushCronSnapshot(apiKey2, jobs) {
|
|
12050
|
+
if (authCircuitOpen) return false;
|
|
11981
12051
|
const c = getClient();
|
|
11982
12052
|
if (!c) return false;
|
|
11983
12053
|
try {
|
|
11984
|
-
await c.mutation(upsertCronSnapshotFromPluginRef, {
|
|
12054
|
+
await c.mutation(upsertCronSnapshotFromPluginRef, { apiKeyHash: hashApiKey(apiKey2), jobs });
|
|
11985
12055
|
return true;
|
|
11986
12056
|
} catch (err) {
|
|
12057
|
+
if (isUnauthorizedError(err)) {
|
|
12058
|
+
tripAuthCircuit();
|
|
12059
|
+
return false;
|
|
12060
|
+
}
|
|
11987
12061
|
getLogger().error(`cohort-sync: pushCronSnapshot failed: ${err}`);
|
|
11988
12062
|
return false;
|
|
11989
12063
|
}
|
|
11990
12064
|
}
|
|
11991
|
-
async function callAddCommentFromPlugin(
|
|
12065
|
+
async function callAddCommentFromPlugin(apiKey2, args) {
|
|
12066
|
+
if (authCircuitOpen) {
|
|
12067
|
+
throw new Error("API key rejected \u2014 re-run `openclaw cohort auth` and restart the gateway");
|
|
12068
|
+
}
|
|
11992
12069
|
const c = getClient();
|
|
11993
12070
|
if (!c) {
|
|
11994
12071
|
throw new Error("Convex client not initialized \u2014 subscription may not be active");
|
|
11995
12072
|
}
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12073
|
+
try {
|
|
12074
|
+
return await c.mutation(addCommentFromPluginRef, {
|
|
12075
|
+
apiKeyHash: hashApiKey(apiKey2),
|
|
12076
|
+
taskNumber: args.taskNumber,
|
|
12077
|
+
agentName: args.agentName,
|
|
12078
|
+
content: args.content,
|
|
12079
|
+
noReply: args.noReply
|
|
12080
|
+
});
|
|
12081
|
+
} catch (err) {
|
|
12082
|
+
if (isUnauthorizedError(err)) {
|
|
12083
|
+
tripAuthCircuit();
|
|
12084
|
+
}
|
|
12085
|
+
throw err;
|
|
12086
|
+
}
|
|
12003
12087
|
}
|
|
12004
12088
|
var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
|
|
12005
12089
|
- Does your planned response address the task's stated scope? If not, do not comment.
|
|
12006
12090
|
- Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
|
|
12007
12091
|
- If the work is complete, transition the task to "waiting" and set noReply=true on your final comment, then stop working on this task.`;
|
|
12092
|
+
var TOOLS_REFERENCE = `
|
|
12093
|
+
TOOLS: Use these \u2014 do NOT call the REST API directly.
|
|
12094
|
+
- cohort_comment(task_number, comment) \u2014 post a comment
|
|
12095
|
+
- cohort_task(task_number) \u2014 fetch full task details + comments
|
|
12096
|
+
- cohort_transition(task_number, status) \u2014 change status
|
|
12097
|
+
- cohort_assign(task_number, assignee) \u2014 assign/unassign
|
|
12098
|
+
- cohort_context() \u2014 get your session briefing`;
|
|
12099
|
+
function sanitizePreview(raw) {
|
|
12100
|
+
return raw.replace(/<\/?user_comment>/gi, "");
|
|
12101
|
+
}
|
|
12008
12102
|
function buildNotificationMessage(n) {
|
|
12009
12103
|
let header;
|
|
12010
12104
|
let cta;
|
|
@@ -12013,11 +12107,11 @@ function buildNotificationMessage(n) {
|
|
|
12013
12107
|
if (n.isMentioned) {
|
|
12014
12108
|
header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
|
|
12015
12109
|
By: ${n.actorName}`;
|
|
12016
|
-
cta =
|
|
12110
|
+
cta = `You were directly mentioned. Read the comment and respond using the cohort_comment tool (taskNumber: ${n.taskNumber}).`;
|
|
12017
12111
|
} else {
|
|
12018
12112
|
header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
|
|
12019
12113
|
From: ${n.actorName}`;
|
|
12020
|
-
cta =
|
|
12114
|
+
cta = `You were NOT @mentioned \u2014 you are receiving this because you are subscribed to this task. Do not respond unless you see something incorrect, urgent, or directly relevant to your expertise that others have missed.`;
|
|
12021
12115
|
}
|
|
12022
12116
|
break;
|
|
12023
12117
|
case "assignment":
|
|
@@ -12035,8 +12129,19 @@ By: ${n.actorName}`;
|
|
|
12035
12129
|
From: ${n.actorName}`;
|
|
12036
12130
|
cta = "Check the task and respond if needed.";
|
|
12037
12131
|
}
|
|
12038
|
-
|
|
12039
|
-
|
|
12132
|
+
let body = "";
|
|
12133
|
+
if (n.preview) {
|
|
12134
|
+
if (n.type === "comment") {
|
|
12135
|
+
const safe = sanitizePreview(n.preview);
|
|
12136
|
+
body = `
|
|
12137
|
+
<user_comment>
|
|
12138
|
+
${safe}
|
|
12139
|
+
</user_comment>`;
|
|
12140
|
+
} else {
|
|
12141
|
+
body = `
|
|
12142
|
+
Comment: "${n.preview}"`;
|
|
12143
|
+
}
|
|
12144
|
+
}
|
|
12040
12145
|
let scope = "";
|
|
12041
12146
|
if (n.taskDescription && n.type === "comment") {
|
|
12042
12147
|
const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
|
|
@@ -12053,7 +12158,7 @@ ${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
|
|
|
12053
12158
|
${prompt}` : "";
|
|
12054
12159
|
return `${header}${scope}${body}
|
|
12055
12160
|
|
|
12056
|
-
${cta}${promptBlock}`;
|
|
12161
|
+
${cta}${promptBlock}${TOOLS_REFERENCE}`;
|
|
12057
12162
|
}
|
|
12058
12163
|
async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
12059
12164
|
const response = await fetch(`http://localhost:${port}/hooks/agent`, {
|
|
@@ -12068,13 +12173,16 @@ async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
|
12068
12173
|
agentId,
|
|
12069
12174
|
deliver: false,
|
|
12070
12175
|
sessionKey: `hook:cohort:task-${n.taskNumber}`
|
|
12071
|
-
})
|
|
12176
|
+
}),
|
|
12177
|
+
signal: AbortSignal.timeout(1e4)
|
|
12072
12178
|
});
|
|
12073
12179
|
if (!response.ok) {
|
|
12074
12180
|
throw new Error(`/hooks/agent returned ${response.status} ${response.statusText}`);
|
|
12075
12181
|
}
|
|
12076
12182
|
}
|
|
12077
|
-
|
|
12183
|
+
var deliveryFailures = /* @__PURE__ */ new Map();
|
|
12184
|
+
var MAX_DELIVERY_ATTEMPTS = 3;
|
|
12185
|
+
async function startNotificationSubscription(port, cfg, hooksToken, logger, gwClient) {
|
|
12078
12186
|
const c = getClient();
|
|
12079
12187
|
if (!c) {
|
|
12080
12188
|
logger.warn("cohort-sync: no ConvexClient \u2014 notification subscription skipped");
|
|
@@ -12084,7 +12192,6 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
|
12084
12192
|
logger.warn(
|
|
12085
12193
|
`cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
|
|
12086
12194
|
);
|
|
12087
|
-
return;
|
|
12088
12195
|
}
|
|
12089
12196
|
const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
|
|
12090
12197
|
const reverseNameMap = {};
|
|
@@ -12097,23 +12204,54 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
|
12097
12204
|
const openclawAgentId = reverseNameMap[agentName] ?? agentName;
|
|
12098
12205
|
logger.info(`cohort-sync: subscribing to notifications for agent "${agentName}" (openclawId: "${openclawAgentId}")`);
|
|
12099
12206
|
let processing = false;
|
|
12207
|
+
const apiKeyHash = hashApiKey(cfg.apiKey);
|
|
12100
12208
|
const unsubscribe = c.onUpdate(
|
|
12101
12209
|
getUndeliveredForPlugin,
|
|
12102
|
-
{ agent: agentName,
|
|
12210
|
+
{ agent: agentName, apiKeyHash },
|
|
12103
12211
|
async (notifications) => {
|
|
12104
|
-
if (processing) return;
|
|
12212
|
+
if (authCircuitOpen || processing) return;
|
|
12105
12213
|
processing = true;
|
|
12106
12214
|
try {
|
|
12107
12215
|
for (const n of notifications) {
|
|
12216
|
+
const failCount = deliveryFailures.get(n._id) ?? 0;
|
|
12217
|
+
if (failCount >= MAX_DELIVERY_ATTEMPTS) {
|
|
12218
|
+
continue;
|
|
12219
|
+
}
|
|
12108
12220
|
try {
|
|
12109
|
-
|
|
12110
|
-
|
|
12221
|
+
if (hooksToken) {
|
|
12222
|
+
await injectNotification(port, hooksToken, n, openclawAgentId);
|
|
12223
|
+
logger.info(`cohort-sync: injected notification for task #${n.taskNumber} (${agentName})`);
|
|
12224
|
+
} else {
|
|
12225
|
+
throw new Error(
|
|
12226
|
+
`no transport available for notification ${n._id} (gwClient alive: ${gwClient?.isAlive() ?? "null"}, hooksToken: ${!!hooksToken})`
|
|
12227
|
+
);
|
|
12228
|
+
}
|
|
12111
12229
|
await c.mutation(markDeliveredByPlugin, {
|
|
12112
12230
|
notificationId: n._id,
|
|
12113
|
-
|
|
12231
|
+
apiKeyHash
|
|
12114
12232
|
});
|
|
12233
|
+
deliveryFailures.delete(n._id);
|
|
12115
12234
|
} catch (err) {
|
|
12116
|
-
|
|
12235
|
+
const newFailCount = failCount + 1;
|
|
12236
|
+
deliveryFailures.set(n._id, newFailCount);
|
|
12237
|
+
if (newFailCount >= MAX_DELIVERY_ATTEMPTS) {
|
|
12238
|
+
logger.error(
|
|
12239
|
+
`cohort-sync: dead-letter notification ${n._id} for task #${n.taskNumber} (${n.type} from ${n.actorName}) after ${MAX_DELIVERY_ATTEMPTS} failed delivery attempts: ${String(err)}`
|
|
12240
|
+
);
|
|
12241
|
+
try {
|
|
12242
|
+
await c.mutation(markDeliveredByPlugin, {
|
|
12243
|
+
notificationId: n._id,
|
|
12244
|
+
apiKeyHash
|
|
12245
|
+
});
|
|
12246
|
+
deliveryFailures.delete(n._id);
|
|
12247
|
+
} catch (markErr) {
|
|
12248
|
+
logger.error(`cohort-sync: failed to dead-letter ${n._id}: ${String(markErr)}`);
|
|
12249
|
+
}
|
|
12250
|
+
} else {
|
|
12251
|
+
logger.warn(
|
|
12252
|
+
`cohort-sync: failed to inject notification ${n._id} (attempt ${newFailCount}/${MAX_DELIVERY_ATTEMPTS}): ${String(err)}`
|
|
12253
|
+
);
|
|
12254
|
+
}
|
|
12117
12255
|
}
|
|
12118
12256
|
}
|
|
12119
12257
|
} finally {
|
|
@@ -12121,6 +12259,10 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
|
12121
12259
|
}
|
|
12122
12260
|
},
|
|
12123
12261
|
(err) => {
|
|
12262
|
+
if (isUnauthorizedError(err)) {
|
|
12263
|
+
tripAuthCircuit();
|
|
12264
|
+
return;
|
|
12265
|
+
}
|
|
12124
12266
|
logger.error(`cohort-sync: subscription error for "${agentName}": ${String(err)}`);
|
|
12125
12267
|
}
|
|
12126
12268
|
);
|
|
@@ -12134,11 +12276,12 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
|
12134
12276
|
return null;
|
|
12135
12277
|
}
|
|
12136
12278
|
let processing = false;
|
|
12279
|
+
const apiKeyHash = hashApiKey(cfg.apiKey);
|
|
12137
12280
|
const unsubscribe = c.onUpdate(
|
|
12138
12281
|
getPendingCommandsForPlugin,
|
|
12139
|
-
{
|
|
12282
|
+
{ apiKeyHash },
|
|
12140
12283
|
async (commands) => {
|
|
12141
|
-
if (processing) return;
|
|
12284
|
+
if (authCircuitOpen || processing) return;
|
|
12142
12285
|
if (commands.length === 0) return;
|
|
12143
12286
|
processing = true;
|
|
12144
12287
|
try {
|
|
@@ -12147,19 +12290,27 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
|
12147
12290
|
try {
|
|
12148
12291
|
await c.mutation(acknowledgeCommandRef, {
|
|
12149
12292
|
commandId: cmd._id,
|
|
12150
|
-
|
|
12293
|
+
apiKeyHash
|
|
12151
12294
|
});
|
|
12152
12295
|
await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger);
|
|
12153
12296
|
if (cmd.type === "restart") return;
|
|
12154
12297
|
} catch (err) {
|
|
12298
|
+
if (isUnauthorizedError(err)) {
|
|
12299
|
+
tripAuthCircuit();
|
|
12300
|
+
return;
|
|
12301
|
+
}
|
|
12155
12302
|
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
12156
12303
|
try {
|
|
12157
12304
|
await c.mutation(failCommandRef, {
|
|
12158
12305
|
commandId: cmd._id,
|
|
12159
|
-
|
|
12306
|
+
apiKeyHash,
|
|
12160
12307
|
reason: String(err).slice(0, 500)
|
|
12161
12308
|
});
|
|
12162
12309
|
} catch (failErr) {
|
|
12310
|
+
if (isUnauthorizedError(failErr)) {
|
|
12311
|
+
tripAuthCircuit();
|
|
12312
|
+
return;
|
|
12313
|
+
}
|
|
12163
12314
|
logger.error(`cohort-sync: failed to mark command ${cmd._id} as failed: ${String(failErr)}`);
|
|
12164
12315
|
}
|
|
12165
12316
|
}
|
|
@@ -12169,6 +12320,10 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
|
12169
12320
|
}
|
|
12170
12321
|
},
|
|
12171
12322
|
(err) => {
|
|
12323
|
+
if (isUnauthorizedError(err)) {
|
|
12324
|
+
tripAuthCircuit();
|
|
12325
|
+
return;
|
|
12326
|
+
}
|
|
12172
12327
|
logger.error(`cohort-sync: command subscription error: ${String(err)}`);
|
|
12173
12328
|
}
|
|
12174
12329
|
);
|
|
@@ -12293,7 +12448,8 @@ var ALLOWED_METHODS = /* @__PURE__ */ new Set([
|
|
|
12293
12448
|
"sessions.preview",
|
|
12294
12449
|
"agent",
|
|
12295
12450
|
"snapshot",
|
|
12296
|
-
"system.presence"
|
|
12451
|
+
"system.presence",
|
|
12452
|
+
"gateway.restart"
|
|
12297
12453
|
]);
|
|
12298
12454
|
function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
12299
12455
|
const signedAtMs = Date.now();
|
|
@@ -12302,7 +12458,7 @@ function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
|
12302
12458
|
clientId: "gateway-client",
|
|
12303
12459
|
clientMode: "backend",
|
|
12304
12460
|
role: "operator",
|
|
12305
|
-
scopes: ["operator.read", "operator.write"],
|
|
12461
|
+
scopes: ["operator.read", "operator.write", "operator.admin"],
|
|
12306
12462
|
signedAtMs,
|
|
12307
12463
|
token,
|
|
12308
12464
|
nonce,
|
|
@@ -12324,7 +12480,7 @@ function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
|
12324
12480
|
mode: "backend"
|
|
12325
12481
|
},
|
|
12326
12482
|
role: "operator",
|
|
12327
|
-
scopes: ["operator.read", "operator.write"],
|
|
12483
|
+
scopes: ["operator.read", "operator.write", "operator.admin"],
|
|
12328
12484
|
auth: { token },
|
|
12329
12485
|
device: {
|
|
12330
12486
|
id: identity.deviceId,
|
|
@@ -12352,8 +12508,9 @@ function parseHelloOk(response) {
|
|
|
12352
12508
|
throw new Error(`Unexpected payload type: ${String(payload?.type ?? "missing")}`);
|
|
12353
12509
|
}
|
|
12354
12510
|
const policy = payload.policy ?? {};
|
|
12355
|
-
const
|
|
12356
|
-
const
|
|
12511
|
+
const features = payload.features;
|
|
12512
|
+
const methods = features?.methods ?? payload.methods ?? [];
|
|
12513
|
+
const events = features?.events ?? payload.events ?? [];
|
|
12357
12514
|
return {
|
|
12358
12515
|
methods: new Set(methods),
|
|
12359
12516
|
events: new Set(events),
|
|
@@ -12825,6 +12982,15 @@ function parseSessionKey(key) {
|
|
|
12825
12982
|
if (rest[0] === "main") {
|
|
12826
12983
|
return { kind: "direct", channel: "signal" };
|
|
12827
12984
|
}
|
|
12985
|
+
if (rest[0] === "cron") {
|
|
12986
|
+
return { kind: "cron", channel: "cron", identifier: rest.slice(1).join(":") };
|
|
12987
|
+
}
|
|
12988
|
+
if (rest[0] === "subagent") {
|
|
12989
|
+
return { kind: "subagent", channel: "subagent", identifier: rest.slice(1).join(":") };
|
|
12990
|
+
}
|
|
12991
|
+
if (rest[0] === "acp") {
|
|
12992
|
+
return { kind: "acp", channel: "acp", identifier: rest.slice(1).join(":") };
|
|
12993
|
+
}
|
|
12828
12994
|
if (rest[0] === "signal") {
|
|
12829
12995
|
if (rest[1] === "group") {
|
|
12830
12996
|
return { kind: "group", channel: "signal", identifier: rest.slice(2).join(":") };
|
|
@@ -12838,6 +13004,14 @@ function parseSessionKey(key) {
|
|
|
12838
13004
|
if (rest[0] === "slack" && rest[1] === "channel") {
|
|
12839
13005
|
return { kind: "group", channel: "slack", identifier: rest[2] };
|
|
12840
13006
|
}
|
|
13007
|
+
const channel = rest[0];
|
|
13008
|
+
if (rest.includes("group") || rest.includes("channel")) {
|
|
13009
|
+
return { kind: "group", channel, identifier: rest.slice(1).join(":") };
|
|
13010
|
+
}
|
|
13011
|
+
if (rest.includes("dm") || rest.includes("direct")) {
|
|
13012
|
+
return { kind: "direct", channel, identifier: rest.slice(1).join(":") };
|
|
13013
|
+
}
|
|
13014
|
+
return { kind: "direct", channel, identifier: rest.slice(1).join(":") || void 0 };
|
|
12841
13015
|
}
|
|
12842
13016
|
return { kind: "cli", channel: "cli" };
|
|
12843
13017
|
}
|
|
@@ -13315,6 +13489,30 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
|
|
|
13315
13489
|
- Don't ignore workspace-specific overrides from your context response.
|
|
13316
13490
|
`;
|
|
13317
13491
|
|
|
13492
|
+
// src/types.ts
|
|
13493
|
+
var DEFAULT_API_URL = "https://api.cohort.bot";
|
|
13494
|
+
|
|
13495
|
+
// src/tool-runtime.ts
|
|
13496
|
+
var apiKey = null;
|
|
13497
|
+
var apiUrl = DEFAULT_API_URL;
|
|
13498
|
+
var resolveAgentNameFn = null;
|
|
13499
|
+
var loggerRef = null;
|
|
13500
|
+
function setToolRuntime(state) {
|
|
13501
|
+
apiKey = state.apiKey;
|
|
13502
|
+
apiUrl = state.apiUrl;
|
|
13503
|
+
resolveAgentNameFn = state.resolveAgentName;
|
|
13504
|
+
loggerRef = state.logger;
|
|
13505
|
+
}
|
|
13506
|
+
function getToolRuntime() {
|
|
13507
|
+
return {
|
|
13508
|
+
apiKey,
|
|
13509
|
+
apiUrl,
|
|
13510
|
+
resolveAgentName: resolveAgentNameFn,
|
|
13511
|
+
logger: loggerRef,
|
|
13512
|
+
isReady: apiKey !== null && resolveAgentNameFn !== null
|
|
13513
|
+
};
|
|
13514
|
+
}
|
|
13515
|
+
|
|
13318
13516
|
// src/hooks.ts
|
|
13319
13517
|
var REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
13320
13518
|
"token",
|
|
@@ -13362,6 +13560,12 @@ try {
|
|
|
13362
13560
|
} catch {
|
|
13363
13561
|
}
|
|
13364
13562
|
diag("MODULE_LOADED", { PLUGIN_VERSION });
|
|
13563
|
+
var _gatewayStartHandler = null;
|
|
13564
|
+
async function handleGatewayStart(event) {
|
|
13565
|
+
if (_gatewayStartHandler) {
|
|
13566
|
+
await _gatewayStartHandler(event);
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
13365
13569
|
function resolveGatewayToken(api) {
|
|
13366
13570
|
const rawToken = api.config?.gateway?.auth?.token;
|
|
13367
13571
|
if (typeof rawToken === "string") return rawToken;
|
|
@@ -13447,7 +13651,7 @@ function saveSessionsToDisk(tracker2) {
|
|
|
13447
13651
|
data.sessions.push({ agentName: name, key });
|
|
13448
13652
|
}
|
|
13449
13653
|
}
|
|
13450
|
-
fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
|
|
13654
|
+
fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data), { mode: 384 });
|
|
13451
13655
|
} catch {
|
|
13452
13656
|
}
|
|
13453
13657
|
}
|
|
@@ -13478,54 +13682,35 @@ function loadSessionsFromDisk(tracker2, logger) {
|
|
|
13478
13682
|
} catch {
|
|
13479
13683
|
}
|
|
13480
13684
|
}
|
|
13481
|
-
|
|
13482
|
-
try {
|
|
13483
|
-
const response = await fetch(`${apiUrl}/api/v1/context`, {
|
|
13484
|
-
method: "GET",
|
|
13485
|
-
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
13486
|
-
});
|
|
13487
|
-
if (!response.ok) {
|
|
13488
|
-
logger.warn(`cohort-sync: /context returned ${response.status}, using pocket guide`);
|
|
13489
|
-
return POCKET_GUIDE;
|
|
13490
|
-
}
|
|
13491
|
-
const data = await response.json();
|
|
13492
|
-
return data.briefing || POCKET_GUIDE;
|
|
13493
|
-
} catch (err) {
|
|
13494
|
-
logger.warn(`cohort-sync: /context fetch failed: ${String(err)}, using pocket guide`);
|
|
13495
|
-
return POCKET_GUIDE;
|
|
13496
|
-
}
|
|
13497
|
-
}
|
|
13498
|
-
async function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
|
|
13685
|
+
function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
|
|
13499
13686
|
const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
|
|
13500
|
-
await client2.connect();
|
|
13501
13687
|
persistentGwClient = client2;
|
|
13502
13688
|
gwClientInitialized = true;
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
diag("GW_CLIENT_SHUTDOWN_EVENT", {});
|
|
13507
|
-
logger.info("cohort-sync: gateway shutdown event received");
|
|
13508
|
-
});
|
|
13509
|
-
}
|
|
13510
|
-
client2.onReconnect = async () => {
|
|
13511
|
-
diag("GW_CLIENT_RECONNECTED_RESUBSCRIBE", {});
|
|
13689
|
+
const onConnected = async () => {
|
|
13690
|
+
diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
|
|
13691
|
+
logger.info(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
|
|
13512
13692
|
registerCronEventHandlers(client2, cfg, resolveAgentName);
|
|
13693
|
+
if (client2.availableEvents.has("shutdown")) {
|
|
13694
|
+
client2.on("shutdown", () => {
|
|
13695
|
+
diag("GW_CLIENT_SHUTDOWN_EVENT", {});
|
|
13696
|
+
logger.info("cohort-sync: gateway shutdown event received");
|
|
13697
|
+
});
|
|
13698
|
+
}
|
|
13513
13699
|
try {
|
|
13514
|
-
const
|
|
13515
|
-
const
|
|
13516
|
-
const
|
|
13517
|
-
await pushCronSnapshot(cfg.apiKey,
|
|
13518
|
-
diag("
|
|
13700
|
+
const cronResult = await client2.request("cron.list", { includeDisabled: true });
|
|
13701
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
13702
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13703
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13704
|
+
diag("GW_CLIENT_CRON_PUSH", { count: mapped.length });
|
|
13519
13705
|
} catch (err) {
|
|
13520
|
-
diag("
|
|
13706
|
+
diag("GW_CLIENT_CRON_PUSH_FAILED", { error: String(err) });
|
|
13521
13707
|
}
|
|
13522
13708
|
};
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
|
|
13709
|
+
client2.onReconnect = onConnected;
|
|
13710
|
+
client2.connect().then(() => onConnected()).catch((err) => {
|
|
13711
|
+
diag("GW_CLIENT_INITIAL_CONNECT_DEFERRED", { error: String(err) });
|
|
13712
|
+
logger.warn(`cohort-sync: GW connect will retry: ${String(err)}`);
|
|
13713
|
+
});
|
|
13529
13714
|
}
|
|
13530
13715
|
function registerHooks(api, cfg) {
|
|
13531
13716
|
STATE_FILE_PATH = path3.join(cfg.stateDir, "session-state.json");
|
|
@@ -13535,6 +13720,12 @@ function registerHooks(api, cfg) {
|
|
|
13535
13720
|
const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
|
|
13536
13721
|
createClient(convexUrl);
|
|
13537
13722
|
setLogger(logger);
|
|
13723
|
+
gatewayPort = api.config?.gateway?.port ?? null;
|
|
13724
|
+
gatewayToken = resolveGatewayToken(api);
|
|
13725
|
+
if (gatewayPort && gatewayToken) {
|
|
13726
|
+
initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
|
|
13727
|
+
}
|
|
13728
|
+
const cronStorePath = api.config?.cron?.store ?? path3.join(os3.homedir(), ".openclaw", "cron", "jobs.json");
|
|
13538
13729
|
logger.info(`cohort-sync: registerHooks v${PLUGIN_VERSION}`);
|
|
13539
13730
|
diag("REGISTER_HOOKS", {
|
|
13540
13731
|
PLUGIN_VERSION,
|
|
@@ -13575,6 +13766,12 @@ function registerHooks(api, cfg) {
|
|
|
13575
13766
|
function resolveAgentName(agentId) {
|
|
13576
13767
|
return (nameMap?.[agentId] ?? identityNameMap[agentId] ?? agentId).toLowerCase();
|
|
13577
13768
|
}
|
|
13769
|
+
setToolRuntime({
|
|
13770
|
+
apiKey: cfg.apiKey,
|
|
13771
|
+
apiUrl: cfg.apiUrl,
|
|
13772
|
+
resolveAgentName,
|
|
13773
|
+
logger
|
|
13774
|
+
});
|
|
13578
13775
|
function resolveAgentFromContext(ctx) {
|
|
13579
13776
|
const allCtxKeys = Object.keys(ctx);
|
|
13580
13777
|
diag("RESOLVE_AGENT_FROM_CTX_START", {
|
|
@@ -13656,101 +13853,6 @@ function registerHooks(api, cfg) {
|
|
|
13656
13853
|
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13657
13854
|
commandUnsubscriber2 = unsub;
|
|
13658
13855
|
}
|
|
13659
|
-
api.registerTool((toolCtx) => {
|
|
13660
|
-
const agentId = toolCtx.agentId ?? "main";
|
|
13661
|
-
const agentName = resolveAgentName(agentId);
|
|
13662
|
-
return {
|
|
13663
|
-
name: "cohort_comment",
|
|
13664
|
-
label: "cohort_comment",
|
|
13665
|
-
description: "Post a comment on a Cohort task. Use this to respond to @mentions or collaborate on tasks.",
|
|
13666
|
-
parameters: Type.Object({
|
|
13667
|
-
task_number: Type.Number({ description: "Task number (e.g. 312)" }),
|
|
13668
|
-
comment: Type.String({ description: "Comment text to post" }),
|
|
13669
|
-
no_reply: Type.Optional(Type.Boolean({
|
|
13670
|
-
description: "If true, no notifications will be sent for this comment. Use for final/closing comments."
|
|
13671
|
-
}))
|
|
13672
|
-
}),
|
|
13673
|
-
async execute(_toolCallId, params) {
|
|
13674
|
-
try {
|
|
13675
|
-
const result = await callAddCommentFromPlugin(cfg.apiKey, {
|
|
13676
|
-
taskNumber: params.task_number,
|
|
13677
|
-
agentName,
|
|
13678
|
-
content: params.comment,
|
|
13679
|
-
noReply: params.no_reply ?? false
|
|
13680
|
-
});
|
|
13681
|
-
const lines = [`Comment posted on task #${params.task_number}.`];
|
|
13682
|
-
if (result.stats) {
|
|
13683
|
-
lines.push("");
|
|
13684
|
-
lines.push(`This task has ${result.stats.totalComments} comments. ${result.stats.myRecentCount}/${result.stats.threshold} hourly limit used on this task.`);
|
|
13685
|
-
}
|
|
13686
|
-
if (result.budget) {
|
|
13687
|
-
lines.push(`Daily budget: ${result.budget.used}/${result.budget.limit}`);
|
|
13688
|
-
}
|
|
13689
|
-
return {
|
|
13690
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
13691
|
-
details: result
|
|
13692
|
-
};
|
|
13693
|
-
} catch (err) {
|
|
13694
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
13695
|
-
if (msg.includes("AGENT_COMMENTS_LOCKED")) {
|
|
13696
|
-
return {
|
|
13697
|
-
content: [{
|
|
13698
|
-
type: "text",
|
|
13699
|
-
text: `Cannot comment on task #${params.task_number}.
|
|
13700
|
-
Reason: Agent comments are locked on this task.
|
|
13701
|
-
Do not re-attempt to comment on this task.`
|
|
13702
|
-
}],
|
|
13703
|
-
details: { error: "AGENT_COMMENTS_LOCKED", taskNumber: params.task_number }
|
|
13704
|
-
};
|
|
13705
|
-
}
|
|
13706
|
-
if (msg.includes("TASK_HOUR_LIMIT_REACHED")) {
|
|
13707
|
-
const parts = msg.split("|");
|
|
13708
|
-
const count = parts[1] ?? "?";
|
|
13709
|
-
const limit = parts[2] ?? "?";
|
|
13710
|
-
return {
|
|
13711
|
-
content: [{
|
|
13712
|
-
type: "text",
|
|
13713
|
-
text: `Cannot comment on task #${params.task_number}.
|
|
13714
|
-
Reason: You have posted ${count} comments on this task in the last hour (limit: ${limit}).
|
|
13715
|
-
Step back from this task. Do not comment again until the next hour.`
|
|
13716
|
-
}],
|
|
13717
|
-
details: { error: "TASK_HOUR_LIMIT_REACHED", count, limit, taskNumber: params.task_number }
|
|
13718
|
-
};
|
|
13719
|
-
}
|
|
13720
|
-
if (msg.includes("DAILY_LIMIT_REACHED")) {
|
|
13721
|
-
const parts = msg.split("|");
|
|
13722
|
-
const used = parts[1] ?? "?";
|
|
13723
|
-
const max = parts[2] ?? "?";
|
|
13724
|
-
const resetAt = parts[3] ?? "tomorrow";
|
|
13725
|
-
return {
|
|
13726
|
-
content: [{
|
|
13727
|
-
type: "text",
|
|
13728
|
-
text: `Cannot comment on task #${params.task_number}.
|
|
13729
|
-
Reason: Daily comment limit reached (${used}/${max}).
|
|
13730
|
-
Do not attempt to make more comments until ${resetAt}.`
|
|
13731
|
-
}],
|
|
13732
|
-
details: { error: "DAILY_LIMIT_REACHED", used, max, resetAt, taskNumber: params.task_number }
|
|
13733
|
-
};
|
|
13734
|
-
}
|
|
13735
|
-
throw err;
|
|
13736
|
-
}
|
|
13737
|
-
}
|
|
13738
|
-
};
|
|
13739
|
-
});
|
|
13740
|
-
api.registerTool(() => {
|
|
13741
|
-
return {
|
|
13742
|
-
name: "cohort_context",
|
|
13743
|
-
label: "cohort_context",
|
|
13744
|
-
description: "Get your Cohort session briefing. Call this at the start of every work session to receive your guidelines, current assignments, active projects, and recent team activity.",
|
|
13745
|
-
parameters: Type.Object({}),
|
|
13746
|
-
async execute() {
|
|
13747
|
-
const briefing = await fetchAgentContext(cfg.apiKey, cfg.apiUrl, logger);
|
|
13748
|
-
return {
|
|
13749
|
-
content: [{ type: "text", text: briefing }]
|
|
13750
|
-
};
|
|
13751
|
-
}
|
|
13752
|
-
};
|
|
13753
|
-
});
|
|
13754
13856
|
function resolveModel(agentId) {
|
|
13755
13857
|
const agent = config?.agents?.list?.find((a) => a.id === agentId);
|
|
13756
13858
|
const m = agent?.model;
|
|
@@ -13770,7 +13872,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13770
13872
|
if (m.includes("deepseek")) return 128e3;
|
|
13771
13873
|
return 2e5;
|
|
13772
13874
|
}
|
|
13773
|
-
|
|
13875
|
+
_gatewayStartHandler = async (event) => {
|
|
13774
13876
|
diag("HOOK_gateway_start", { port: event.port, eventKeys: Object.keys(event) });
|
|
13775
13877
|
try {
|
|
13776
13878
|
checkForUpdate(PLUGIN_VERSION, logger).catch(() => {
|
|
@@ -13821,7 +13923,8 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13821
13923
|
event.port,
|
|
13822
13924
|
cfg,
|
|
13823
13925
|
api.config.hooks?.token,
|
|
13824
|
-
logger
|
|
13926
|
+
logger,
|
|
13927
|
+
persistentGwClient
|
|
13825
13928
|
).catch((err) => {
|
|
13826
13929
|
logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
|
|
13827
13930
|
});
|
|
@@ -13858,7 +13961,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13858
13961
|
saveSessionsToDisk(tracker2);
|
|
13859
13962
|
}, KEEPALIVE_INTERVAL_MS);
|
|
13860
13963
|
logger.info(`cohort-sync: keepalive interval started (${KEEPALIVE_INTERVAL_MS / 1e3}s)`);
|
|
13861
|
-
}
|
|
13964
|
+
};
|
|
13862
13965
|
api.on("agent_end", async (event, ctx) => {
|
|
13863
13966
|
diag("HOOK_agent_end", { ctx: dumpCtx(ctx), success: event.success, error: event.error, durationMs: event.durationMs });
|
|
13864
13967
|
const agentId = ctx.agentId ?? "main";
|
|
@@ -13873,6 +13976,19 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13873
13976
|
tracker2.markTelemetryPushed(agentName);
|
|
13874
13977
|
}
|
|
13875
13978
|
}
|
|
13979
|
+
const sessionKey = ctx.sessionKey;
|
|
13980
|
+
if (sessionKey && sessionKey.includes(":cron:")) {
|
|
13981
|
+
try {
|
|
13982
|
+
const raw = fs3.readFileSync(cronStorePath, "utf8");
|
|
13983
|
+
const store = JSON.parse(raw);
|
|
13984
|
+
const jobs = store.jobs ?? [];
|
|
13985
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13986
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13987
|
+
diag("CRON_AGENT_END_PUSH", { count: mapped.length, sessionKey });
|
|
13988
|
+
} catch (err) {
|
|
13989
|
+
diag("CRON_AGENT_END_PUSH_FAILED", { error: String(err) });
|
|
13990
|
+
}
|
|
13991
|
+
}
|
|
13876
13992
|
if (event.success === false) {
|
|
13877
13993
|
const entry = buildActivityEntry(agentName, "agent_end", {
|
|
13878
13994
|
success: false,
|
|
@@ -14195,11 +14311,11 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14195
14311
|
import { execFile as execFile2 } from "node:child_process";
|
|
14196
14312
|
|
|
14197
14313
|
// src/device-auth.ts
|
|
14198
|
-
function baseUrl(
|
|
14199
|
-
return
|
|
14314
|
+
function baseUrl(apiUrl2) {
|
|
14315
|
+
return apiUrl2.replace(/\/+$/, "");
|
|
14200
14316
|
}
|
|
14201
|
-
async function startDeviceAuth(
|
|
14202
|
-
const url = `${baseUrl(
|
|
14317
|
+
async function startDeviceAuth(apiUrl2, manifest) {
|
|
14318
|
+
const url = `${baseUrl(apiUrl2)}/api/v1/device-auth/start`;
|
|
14203
14319
|
const res = await fetch(url, {
|
|
14204
14320
|
method: "POST",
|
|
14205
14321
|
headers: { "Content-Type": "application/json" },
|
|
@@ -14214,8 +14330,8 @@ async function startDeviceAuth(apiUrl, manifest) {
|
|
|
14214
14330
|
}
|
|
14215
14331
|
return res.json();
|
|
14216
14332
|
}
|
|
14217
|
-
async function pollDeviceAuth(
|
|
14218
|
-
const url = `${baseUrl(
|
|
14333
|
+
async function pollDeviceAuth(apiUrl2, deviceCode) {
|
|
14334
|
+
const url = `${baseUrl(apiUrl2)}/api/v1/device-auth/poll`;
|
|
14219
14335
|
const res = await fetch(url, {
|
|
14220
14336
|
method: "POST",
|
|
14221
14337
|
headers: { "Content-Type": "application/json" },
|
|
@@ -14230,7 +14346,7 @@ async function pollDeviceAuth(apiUrl, deviceCode) {
|
|
|
14230
14346
|
}
|
|
14231
14347
|
return res.json();
|
|
14232
14348
|
}
|
|
14233
|
-
async function waitForApproval(
|
|
14349
|
+
async function waitForApproval(apiUrl2, deviceCode, opts) {
|
|
14234
14350
|
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
14235
14351
|
const timeoutMs = opts?.timeoutMs ?? 9e5;
|
|
14236
14352
|
const onPoll = opts?.onPoll;
|
|
@@ -14241,7 +14357,7 @@ async function waitForApproval(apiUrl, deviceCode, opts) {
|
|
|
14241
14357
|
return { status: "timeout" };
|
|
14242
14358
|
}
|
|
14243
14359
|
try {
|
|
14244
|
-
const result = await pollDeviceAuth(
|
|
14360
|
+
const result = await pollDeviceAuth(apiUrl2, deviceCode);
|
|
14245
14361
|
onPoll?.(result.status);
|
|
14246
14362
|
if (result.status === "approved") {
|
|
14247
14363
|
return { status: "approved", apiKey: result.apiKey };
|
|
@@ -14352,46 +14468,298 @@ function registerCohortCli(ctx, cfg) {
|
|
|
14352
14468
|
|
|
14353
14469
|
// index.ts
|
|
14354
14470
|
init_keychain();
|
|
14355
|
-
var DEFAULT_API_URL = "https://fortunate-chipmunk-286.convex.site";
|
|
14356
14471
|
var plugin = {
|
|
14357
14472
|
id: "cohort-sync",
|
|
14358
14473
|
name: "Cohort Sync",
|
|
14359
14474
|
description: "Syncs agent status and skills to Cohort dashboard",
|
|
14360
14475
|
register(api) {
|
|
14361
14476
|
const cfg = api.pluginConfig;
|
|
14362
|
-
const
|
|
14477
|
+
const apiUrl2 = cfg?.apiUrl || DEFAULT_API_URL;
|
|
14478
|
+
if (!apiUrl2.startsWith("https://") && !apiUrl2.startsWith("http://localhost") && !apiUrl2.startsWith("http://127.0.0.1")) {
|
|
14479
|
+
api.logger.error(
|
|
14480
|
+
"cohort-sync: apiUrl must use HTTPS for security. Got: " + apiUrl2.replace(/\/\/.*@/, "//***@")
|
|
14481
|
+
);
|
|
14482
|
+
return;
|
|
14483
|
+
}
|
|
14363
14484
|
api.registerCli(
|
|
14364
14485
|
(ctx) => registerCohortCli(ctx, {
|
|
14365
|
-
apiUrl,
|
|
14486
|
+
apiUrl: apiUrl2,
|
|
14366
14487
|
apiKey: cfg?.apiKey,
|
|
14367
14488
|
agentNameMap: cfg?.agentNameMap
|
|
14368
14489
|
}),
|
|
14369
14490
|
{ commands: ["cohort"] }
|
|
14370
14491
|
);
|
|
14492
|
+
const gatewayPort2 = api.config?.gateway?.port ?? 18789;
|
|
14493
|
+
api.registerHook(
|
|
14494
|
+
"gateway:startup",
|
|
14495
|
+
async (...args) => {
|
|
14496
|
+
const event = args[0] ?? {};
|
|
14497
|
+
const port = event?.port ?? gatewayPort2;
|
|
14498
|
+
api.logger.info(`cohort-sync: gateway:startup hook fired (port=${port})`);
|
|
14499
|
+
await handleGatewayStart({ ...event, port });
|
|
14500
|
+
},
|
|
14501
|
+
{
|
|
14502
|
+
name: "cohort-sync.gateway-startup",
|
|
14503
|
+
description: "Sync agents and start notification subscription on gateway startup"
|
|
14504
|
+
}
|
|
14505
|
+
);
|
|
14506
|
+
api.registerTool((toolCtx) => {
|
|
14507
|
+
const agentId = toolCtx.agentId ?? "main";
|
|
14508
|
+
return {
|
|
14509
|
+
name: "cohort_comment",
|
|
14510
|
+
label: "cohort_comment",
|
|
14511
|
+
description: "Post a comment on a Cohort task. Use this to respond to @mentions or collaborate on tasks.",
|
|
14512
|
+
parameters: Type.Object({
|
|
14513
|
+
task_number: Type.Number({ description: "Task number (e.g. 312)" }),
|
|
14514
|
+
comment: Type.String({ description: "Comment text to post" }),
|
|
14515
|
+
no_reply: Type.Optional(Type.Boolean({
|
|
14516
|
+
description: "If true, no notifications will be sent for this comment. Use for final/closing comments."
|
|
14517
|
+
}))
|
|
14518
|
+
}),
|
|
14519
|
+
async execute(_toolCallId, params) {
|
|
14520
|
+
const rt = getToolRuntime();
|
|
14521
|
+
if (!rt.isReady) {
|
|
14522
|
+
return {
|
|
14523
|
+
content: [{ type: "text", text: "cohort_comment is not ready yet \u2014 the plugin is still starting up. Try again in a few seconds." }]
|
|
14524
|
+
};
|
|
14525
|
+
}
|
|
14526
|
+
const agentName = rt.resolveAgentName(agentId);
|
|
14527
|
+
try {
|
|
14528
|
+
const result = await callAddCommentFromPlugin(rt.apiKey, {
|
|
14529
|
+
taskNumber: params.task_number,
|
|
14530
|
+
agentName,
|
|
14531
|
+
content: params.comment,
|
|
14532
|
+
noReply: params.no_reply ?? false
|
|
14533
|
+
});
|
|
14534
|
+
const lines = [`Comment posted on task #${params.task_number}.`];
|
|
14535
|
+
if (result.stats) {
|
|
14536
|
+
lines.push("");
|
|
14537
|
+
lines.push(`This task has ${result.stats.totalComments} comments. ${result.stats.myRecentCount}/${result.stats.threshold} hourly limit used on this task.`);
|
|
14538
|
+
}
|
|
14539
|
+
if (result.budget) {
|
|
14540
|
+
lines.push(`Daily budget: ${result.budget.used}/${result.budget.limit}`);
|
|
14541
|
+
}
|
|
14542
|
+
return {
|
|
14543
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
14544
|
+
details: result
|
|
14545
|
+
};
|
|
14546
|
+
} catch (err) {
|
|
14547
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14548
|
+
if (msg.includes("AGENT_COMMENTS_LOCKED")) {
|
|
14549
|
+
return { content: [{ type: "text", text: `Cannot comment on task #${params.task_number}.
|
|
14550
|
+
Reason: Agent comments are locked on this task.
|
|
14551
|
+
Do not re-attempt.` }] };
|
|
14552
|
+
}
|
|
14553
|
+
if (msg.includes("TASK_HOUR_LIMIT_REACHED")) {
|
|
14554
|
+
return { content: [{ type: "text", text: `Cannot comment on task #${params.task_number}.
|
|
14555
|
+
Reason: Per-task hourly limit reached.
|
|
14556
|
+
Step back from this task.` }] };
|
|
14557
|
+
}
|
|
14558
|
+
if (msg.includes("DAILY_LIMIT_REACHED")) {
|
|
14559
|
+
return { content: [{ type: "text", text: `Cannot comment on task #${params.task_number}.
|
|
14560
|
+
Reason: Daily comment limit reached.
|
|
14561
|
+
Do not attempt more comments until tomorrow.` }] };
|
|
14562
|
+
}
|
|
14563
|
+
throw err;
|
|
14564
|
+
}
|
|
14565
|
+
}
|
|
14566
|
+
};
|
|
14567
|
+
});
|
|
14568
|
+
api.registerTool(() => {
|
|
14569
|
+
return {
|
|
14570
|
+
name: "cohort_context",
|
|
14571
|
+
label: "cohort_context",
|
|
14572
|
+
description: "Get your Cohort session briefing. Call this at the start of every work session to receive your guidelines, current assignments, active projects, and recent team activity.",
|
|
14573
|
+
parameters: Type.Object({}),
|
|
14574
|
+
async execute() {
|
|
14575
|
+
const rt = getToolRuntime();
|
|
14576
|
+
if (!rt.isReady) {
|
|
14577
|
+
return { content: [{ type: "text", text: POCKET_GUIDE }] };
|
|
14578
|
+
}
|
|
14579
|
+
try {
|
|
14580
|
+
const response = await fetch(`${rt.apiUrl}/api/v1/context`, {
|
|
14581
|
+
method: "GET",
|
|
14582
|
+
headers: { "Authorization": `Bearer ${rt.apiKey}` },
|
|
14583
|
+
signal: AbortSignal.timeout(1e4)
|
|
14584
|
+
});
|
|
14585
|
+
if (!response.ok) return { content: [{ type: "text", text: POCKET_GUIDE }] };
|
|
14586
|
+
const data = await response.json();
|
|
14587
|
+
return { content: [{ type: "text", text: data.briefing || POCKET_GUIDE }] };
|
|
14588
|
+
} catch {
|
|
14589
|
+
return { content: [{ type: "text", text: POCKET_GUIDE }] };
|
|
14590
|
+
}
|
|
14591
|
+
}
|
|
14592
|
+
};
|
|
14593
|
+
});
|
|
14594
|
+
api.registerTool(() => {
|
|
14595
|
+
return {
|
|
14596
|
+
name: "cohort_task",
|
|
14597
|
+
label: "cohort_task",
|
|
14598
|
+
description: "Fetch full details for a Cohort task by number, including description and recent comments. Use this before responding to a notification if you need more context about the task.",
|
|
14599
|
+
parameters: Type.Object({
|
|
14600
|
+
task_number: Type.Number({ description: "Task number (e.g. 370)" }),
|
|
14601
|
+
include_comments: Type.Optional(Type.Boolean({ description: "Include recent comments (default: true)" })),
|
|
14602
|
+
comment_limit: Type.Optional(Type.Number({ description: "Max comments to return (default: 10)" }))
|
|
14603
|
+
}),
|
|
14604
|
+
async execute(_toolCallId, params) {
|
|
14605
|
+
const rt = getToolRuntime();
|
|
14606
|
+
if (!rt.isReady) {
|
|
14607
|
+
return { content: [{ type: "text", text: "cohort_task is not ready yet \u2014 the plugin is still starting up." }] };
|
|
14608
|
+
}
|
|
14609
|
+
try {
|
|
14610
|
+
const taskRes = await fetch(`${rt.apiUrl}/api/v1/tasks/${params.task_number}`, {
|
|
14611
|
+
headers: { "Authorization": `Bearer ${rt.apiKey}` },
|
|
14612
|
+
signal: AbortSignal.timeout(1e4)
|
|
14613
|
+
});
|
|
14614
|
+
if (!taskRes.ok) {
|
|
14615
|
+
return { content: [{ type: "text", text: `Task #${params.task_number} not found (${taskRes.status}).` }] };
|
|
14616
|
+
}
|
|
14617
|
+
const task = await taskRes.json();
|
|
14618
|
+
const lines = [
|
|
14619
|
+
`# Task #${task.taskNumber}: ${task.title}`,
|
|
14620
|
+
`**Status:** ${task.status} | **Priority:** ${task.priority ?? "none"} | **Effort:** ${task.effort ?? "none"}`,
|
|
14621
|
+
`**Assigned to:** ${task.assignedTo ?? "unassigned"}`,
|
|
14622
|
+
`**Created:** ${task.createdAt}`,
|
|
14623
|
+
"",
|
|
14624
|
+
"## Description",
|
|
14625
|
+
task.description || "(no description)"
|
|
14626
|
+
];
|
|
14627
|
+
if (params.include_comments !== false) {
|
|
14628
|
+
const limit = params.comment_limit ?? 10;
|
|
14629
|
+
const commentsRes = await fetch(
|
|
14630
|
+
`${rt.apiUrl}/api/v1/tasks/${params.task_number}/comments?limit=${limit}`,
|
|
14631
|
+
{
|
|
14632
|
+
headers: { "Authorization": `Bearer ${rt.apiKey}` },
|
|
14633
|
+
signal: AbortSignal.timeout(1e4)
|
|
14634
|
+
}
|
|
14635
|
+
);
|
|
14636
|
+
if (commentsRes.ok) {
|
|
14637
|
+
const commentsData = await commentsRes.json();
|
|
14638
|
+
const comments = commentsData.data ?? [];
|
|
14639
|
+
if (comments.length > 0) {
|
|
14640
|
+
lines.push("", "## Recent Comments");
|
|
14641
|
+
for (const c of comments) {
|
|
14642
|
+
lines.push(`**${c.authorName}** (${c.authorType}, ${c.createdAt.slice(0, 16)}): ${c.body.slice(0, 300)}`);
|
|
14643
|
+
}
|
|
14644
|
+
if (comments.length >= limit) {
|
|
14645
|
+
lines.push(`(showing ${limit} most recent \u2014 there may be more)`);
|
|
14646
|
+
}
|
|
14647
|
+
}
|
|
14648
|
+
}
|
|
14649
|
+
}
|
|
14650
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
14651
|
+
} catch (err) {
|
|
14652
|
+
return { content: [{ type: "text", text: `Failed to fetch task #${params.task_number}: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
14653
|
+
}
|
|
14654
|
+
}
|
|
14655
|
+
};
|
|
14656
|
+
});
|
|
14657
|
+
api.registerTool(() => {
|
|
14658
|
+
return {
|
|
14659
|
+
name: "cohort_transition",
|
|
14660
|
+
label: "cohort_transition",
|
|
14661
|
+
description: "Change the status of a Cohort task. Valid statuses: backlog, todo, in_progress, waiting, done. Always leave a comment explaining the transition before calling this.",
|
|
14662
|
+
parameters: Type.Object({
|
|
14663
|
+
task_number: Type.Number({ description: "Task number (e.g. 370)" }),
|
|
14664
|
+
status: Type.String({ description: "Target status: backlog, todo, in_progress, waiting, or done" })
|
|
14665
|
+
}),
|
|
14666
|
+
async execute(_toolCallId, params) {
|
|
14667
|
+
const rt = getToolRuntime();
|
|
14668
|
+
if (!rt.isReady) {
|
|
14669
|
+
return { content: [{ type: "text", text: "cohort_transition is not ready yet \u2014 the plugin is still starting up." }] };
|
|
14670
|
+
}
|
|
14671
|
+
const validStatuses = ["backlog", "todo", "in_progress", "waiting", "done"];
|
|
14672
|
+
if (!validStatuses.includes(params.status)) {
|
|
14673
|
+
return { content: [{ type: "text", text: `Invalid status "${params.status}". Valid statuses: ${validStatuses.join(", ")}` }] };
|
|
14674
|
+
}
|
|
14675
|
+
try {
|
|
14676
|
+
const res = await fetch(`${rt.apiUrl}/api/v1/tasks/${params.task_number}/transition`, {
|
|
14677
|
+
method: "POST",
|
|
14678
|
+
headers: {
|
|
14679
|
+
"Authorization": `Bearer ${rt.apiKey}`,
|
|
14680
|
+
"Content-Type": "application/json"
|
|
14681
|
+
},
|
|
14682
|
+
body: JSON.stringify({ to: params.status }),
|
|
14683
|
+
signal: AbortSignal.timeout(1e4)
|
|
14684
|
+
});
|
|
14685
|
+
if (!res.ok) {
|
|
14686
|
+
const body = await res.text();
|
|
14687
|
+
return { content: [{ type: "text", text: `Failed to transition task #${params.task_number} to "${params.status}": ${res.status} ${body.slice(0, 200)}` }] };
|
|
14688
|
+
}
|
|
14689
|
+
const task = await res.json();
|
|
14690
|
+
return { content: [{ type: "text", text: `Task #${params.task_number} transitioned to "${params.status}".` }] };
|
|
14691
|
+
} catch (err) {
|
|
14692
|
+
return { content: [{ type: "text", text: `Failed to transition task #${params.task_number}: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
14693
|
+
}
|
|
14694
|
+
}
|
|
14695
|
+
};
|
|
14696
|
+
});
|
|
14697
|
+
api.registerTool((toolCtx) => {
|
|
14698
|
+
const agentId = toolCtx.agentId ?? "main";
|
|
14699
|
+
return {
|
|
14700
|
+
name: "cohort_assign",
|
|
14701
|
+
label: "cohort_assign",
|
|
14702
|
+
description: "Assign a Cohort task to a team member (agent or human) by name. Use your own name to self-assign. Set assignee to null or empty string to unassign.",
|
|
14703
|
+
parameters: Type.Object({
|
|
14704
|
+
task_number: Type.Number({ description: "Task number (e.g. 370)" }),
|
|
14705
|
+
assignee: Type.Union([
|
|
14706
|
+
Type.String({ description: "Name of the assignee (e.g. 'tosh', 'dave')" }),
|
|
14707
|
+
Type.Null({ description: "Set to null to unassign" })
|
|
14708
|
+
])
|
|
14709
|
+
}),
|
|
14710
|
+
async execute(_toolCallId, params) {
|
|
14711
|
+
const rt = getToolRuntime();
|
|
14712
|
+
if (!rt.isReady) {
|
|
14713
|
+
return { content: [{ type: "text", text: "cohort_assign is not ready yet \u2014 the plugin is still starting up." }] };
|
|
14714
|
+
}
|
|
14715
|
+
try {
|
|
14716
|
+
const assignee = params.assignee?.trim() || null;
|
|
14717
|
+
const res = await fetch(`${rt.apiUrl}/api/v1/tasks/${params.task_number}`, {
|
|
14718
|
+
method: "PATCH",
|
|
14719
|
+
headers: {
|
|
14720
|
+
"Authorization": `Bearer ${rt.apiKey}`,
|
|
14721
|
+
"Content-Type": "application/json"
|
|
14722
|
+
},
|
|
14723
|
+
body: JSON.stringify({ assignedTo: assignee }),
|
|
14724
|
+
signal: AbortSignal.timeout(1e4)
|
|
14725
|
+
});
|
|
14726
|
+
if (!res.ok) {
|
|
14727
|
+
const body = await res.text();
|
|
14728
|
+
return { content: [{ type: "text", text: `Failed to assign task #${params.task_number}: ${res.status} ${body.slice(0, 200)}` }] };
|
|
14729
|
+
}
|
|
14730
|
+
const task = await res.json();
|
|
14731
|
+
const msg = assignee ? `Task #${params.task_number} assigned to ${assignee}.` : `Task #${params.task_number} unassigned.`;
|
|
14732
|
+
return { content: [{ type: "text", text: msg }] };
|
|
14733
|
+
} catch (err) {
|
|
14734
|
+
return { content: [{ type: "text", text: `Failed to assign task #${params.task_number}: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
14735
|
+
}
|
|
14736
|
+
}
|
|
14737
|
+
};
|
|
14738
|
+
});
|
|
14371
14739
|
api.registerService({
|
|
14372
14740
|
id: "cohort-sync-core",
|
|
14373
14741
|
async start(svcCtx) {
|
|
14374
|
-
api.logger.info(`cohort-sync: service starting (api: ${
|
|
14375
|
-
let
|
|
14376
|
-
if (!
|
|
14742
|
+
api.logger.info(`cohort-sync: service starting (api: ${apiUrl2})`);
|
|
14743
|
+
let apiKey2 = cfg?.apiKey;
|
|
14744
|
+
if (!apiKey2) {
|
|
14377
14745
|
try {
|
|
14378
|
-
|
|
14746
|
+
apiKey2 = await getCredential(apiUrl2) ?? void 0;
|
|
14379
14747
|
} catch (err) {
|
|
14380
14748
|
api.logger.error(
|
|
14381
14749
|
`cohort-sync: keychain lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
14382
14750
|
);
|
|
14383
14751
|
}
|
|
14384
14752
|
}
|
|
14385
|
-
if (!
|
|
14753
|
+
if (!apiKey2) {
|
|
14386
14754
|
api.logger.warn(
|
|
14387
14755
|
"cohort-sync: no API key found \u2014 run 'openclaw cohort auth' to authenticate"
|
|
14388
14756
|
);
|
|
14389
14757
|
return;
|
|
14390
14758
|
}
|
|
14391
|
-
api.logger.info(`cohort-sync: activated (api: ${
|
|
14759
|
+
api.logger.info(`cohort-sync: activated (api: ${apiUrl2})`);
|
|
14392
14760
|
registerHooks(api, {
|
|
14393
|
-
apiUrl,
|
|
14394
|
-
apiKey,
|
|
14761
|
+
apiUrl: apiUrl2,
|
|
14762
|
+
apiKey: apiKey2,
|
|
14395
14763
|
stateDir: svcCtx.stateDir,
|
|
14396
14764
|
agentNameMap: cfg?.agentNameMap
|
|
14397
14765
|
});
|