@cfio/cohort-sync 0.8.1 → 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 +572 -204
- package/dist/openclaw.plugin.json +3 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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,11 +98,6 @@ var init_keychain = __esm({
|
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
// src/hooks.ts
|
|
102
|
-
import fs3 from "node:fs";
|
|
103
|
-
import os3 from "node:os";
|
|
104
|
-
import path3 from "node:path";
|
|
105
|
-
|
|
106
101
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
107
102
|
var value_exports = {};
|
|
108
103
|
__export(value_exports, {
|
|
@@ -2709,17 +2704,43 @@ __export(type_exports2, {
|
|
|
2709
2704
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/type/index.mjs
|
|
2710
2705
|
var Type = type_exports2;
|
|
2711
2706
|
|
|
2707
|
+
// src/hooks.ts
|
|
2708
|
+
import fs3 from "node:fs";
|
|
2709
|
+
import os3 from "node:os";
|
|
2710
|
+
import path3 from "node:path";
|
|
2711
|
+
|
|
2712
2712
|
// src/sync.ts
|
|
2713
2713
|
import { execSync } from "node:child_process";
|
|
2714
2714
|
function extractJson(raw) {
|
|
2715
2715
|
const jsonStart = raw.search(/[\[{]/);
|
|
2716
|
-
|
|
2717
|
-
const
|
|
2718
|
-
const
|
|
2719
|
-
|
|
2720
|
-
|
|
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
|
+
}
|
|
2721
2742
|
}
|
|
2722
|
-
|
|
2743
|
+
throw new Error("No complete JSON found in output");
|
|
2723
2744
|
}
|
|
2724
2745
|
function fetchSkills(logger) {
|
|
2725
2746
|
try {
|
|
@@ -2746,32 +2767,45 @@ var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
|
2746
2767
|
function normalizeStatus(status) {
|
|
2747
2768
|
return VALID_STATUSES.has(status) ? status : "idle";
|
|
2748
2769
|
}
|
|
2749
|
-
async function v1Get(
|
|
2750
|
-
const res = await fetch(`${
|
|
2751
|
-
headers: { Authorization: `Bearer ${
|
|
2770
|
+
async function v1Get(apiUrl2, apiKey2, path4) {
|
|
2771
|
+
const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
|
|
2772
|
+
headers: { Authorization: `Bearer ${apiKey2}` },
|
|
2752
2773
|
signal: AbortSignal.timeout(1e4)
|
|
2753
2774
|
});
|
|
2754
2775
|
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
2755
2776
|
return res.json();
|
|
2756
2777
|
}
|
|
2757
|
-
async function v1Patch(
|
|
2758
|
-
const res = await fetch(`${
|
|
2778
|
+
async function v1Patch(apiUrl2, apiKey2, path4, body) {
|
|
2779
|
+
const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
|
|
2759
2780
|
method: "PATCH",
|
|
2760
|
-
headers: { Authorization: `Bearer ${
|
|
2781
|
+
headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
|
|
2761
2782
|
body: JSON.stringify(body),
|
|
2762
2783
|
signal: AbortSignal.timeout(1e4)
|
|
2763
2784
|
});
|
|
2764
2785
|
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
2765
2786
|
}
|
|
2766
|
-
async function v1Post(
|
|
2767
|
-
const res = await fetch(`${
|
|
2787
|
+
async function v1Post(apiUrl2, apiKey2, path4, body) {
|
|
2788
|
+
const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
|
|
2768
2789
|
method: "POST",
|
|
2769
|
-
headers: { Authorization: `Bearer ${
|
|
2790
|
+
headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
|
|
2770
2791
|
body: JSON.stringify(body),
|
|
2771
2792
|
signal: AbortSignal.timeout(1e4)
|
|
2772
2793
|
});
|
|
2773
2794
|
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
2774
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
|
+
}
|
|
2775
2809
|
async function checkForUpdate(currentVersion, logger) {
|
|
2776
2810
|
try {
|
|
2777
2811
|
const res = await fetch("https://registry.npmjs.org/@cfio/cohort-sync/latest", {
|
|
@@ -2780,9 +2814,9 @@ async function checkForUpdate(currentVersion, logger) {
|
|
|
2780
2814
|
if (!res.ok) return;
|
|
2781
2815
|
const data = await res.json();
|
|
2782
2816
|
const latest = data.version;
|
|
2783
|
-
if (latest && latest !== currentVersion) {
|
|
2817
|
+
if (latest && latest !== currentVersion && isNewerVersion(latest, currentVersion)) {
|
|
2784
2818
|
logger.warn(
|
|
2785
|
-
`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`
|
|
2786
2820
|
);
|
|
2787
2821
|
}
|
|
2788
2822
|
} catch {
|
|
@@ -2812,16 +2846,20 @@ async function syncAgentStatus(agentName, status, model, cfg, logger) {
|
|
|
2812
2846
|
}
|
|
2813
2847
|
}
|
|
2814
2848
|
async function syncSkillsToV1(skills, cfg, logger) {
|
|
2849
|
+
let synced = 0;
|
|
2815
2850
|
for (const skill of skills) {
|
|
2816
2851
|
try {
|
|
2817
2852
|
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills", {
|
|
2818
2853
|
name: skill.name,
|
|
2819
2854
|
description: skill.description
|
|
2820
2855
|
});
|
|
2856
|
+
synced++;
|
|
2857
|
+
if (synced % 5 === 0) await new Promise((r) => setTimeout(r, 500));
|
|
2821
2858
|
} catch (err) {
|
|
2822
2859
|
logger.warn(`cohort-sync: failed to sync skill "${skill.name}": ${String(err)}`);
|
|
2823
2860
|
}
|
|
2824
2861
|
}
|
|
2862
|
+
if (synced > 0) logger.info(`cohort-sync: synced ${synced}/${skills.length} skills`);
|
|
2825
2863
|
}
|
|
2826
2864
|
var lastKnownRoster = [];
|
|
2827
2865
|
function getLastKnownRoster() {
|
|
@@ -11777,11 +11815,40 @@ function reverseResolveAgentName(cohortName, forwardMap) {
|
|
|
11777
11815
|
}
|
|
11778
11816
|
|
|
11779
11817
|
// src/commands.ts
|
|
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;
|
|
11780
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
|
+
}
|
|
11781
11843
|
if (cmd.type === "restart") {
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
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
|
+
}
|
|
11785
11852
|
return;
|
|
11786
11853
|
}
|
|
11787
11854
|
if (cmd.type.startsWith("cron")) {
|
|
@@ -11861,12 +11928,12 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
|
|
|
11861
11928
|
function hashApiKey(key) {
|
|
11862
11929
|
return createHash("sha256").update(key).digest("hex");
|
|
11863
11930
|
}
|
|
11864
|
-
function deriveConvexUrl(
|
|
11865
|
-
const normalized =
|
|
11931
|
+
function deriveConvexUrl(apiUrl2) {
|
|
11932
|
+
const normalized = apiUrl2.replace(/\/+$/, "");
|
|
11866
11933
|
if (/^https?:\/\/api\.cohort\.bot$/i.test(normalized)) {
|
|
11867
11934
|
return normalized.replace(/api\.cohort\.bot$/i, "ws.cohort.bot");
|
|
11868
11935
|
}
|
|
11869
|
-
return
|
|
11936
|
+
return apiUrl2.replace(/\.convex\.site\/?$/, ".convex.cloud");
|
|
11870
11937
|
}
|
|
11871
11938
|
var savedLogger = null;
|
|
11872
11939
|
function setLogger(logger) {
|
|
@@ -11887,6 +11954,7 @@ function createClient(convexUrl) {
|
|
|
11887
11954
|
client.close();
|
|
11888
11955
|
}
|
|
11889
11956
|
savedConvexUrl = convexUrl;
|
|
11957
|
+
authCircuitOpen = false;
|
|
11890
11958
|
client = new ConvexClient(convexUrl);
|
|
11891
11959
|
return client;
|
|
11892
11960
|
}
|
|
@@ -11914,6 +11982,17 @@ function closeBridge() {
|
|
|
11914
11982
|
}
|
|
11915
11983
|
savedConvexUrl = null;
|
|
11916
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
|
+
}
|
|
11917
11996
|
var commandUnsubscriber = null;
|
|
11918
11997
|
var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTelemetryFromPlugin");
|
|
11919
11998
|
var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
|
|
@@ -11925,62 +12004,101 @@ var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPend
|
|
|
11925
12004
|
var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
|
|
11926
12005
|
var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
|
|
11927
12006
|
var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
|
|
11928
|
-
async function pushTelemetry(
|
|
12007
|
+
async function pushTelemetry(apiKey2, data) {
|
|
12008
|
+
if (authCircuitOpen) return;
|
|
11929
12009
|
const c = getClient();
|
|
11930
12010
|
if (!c) return;
|
|
11931
12011
|
try {
|
|
11932
|
-
await c.mutation(upsertTelemetryFromPlugin, { apiKeyHash: hashApiKey(
|
|
12012
|
+
await c.mutation(upsertTelemetryFromPlugin, { apiKeyHash: hashApiKey(apiKey2), ...data });
|
|
11933
12013
|
} catch (err) {
|
|
12014
|
+
if (isUnauthorizedError(err)) {
|
|
12015
|
+
tripAuthCircuit();
|
|
12016
|
+
return;
|
|
12017
|
+
}
|
|
11934
12018
|
getLogger().error(`cohort-sync: pushTelemetry failed: ${err}`);
|
|
11935
12019
|
}
|
|
11936
12020
|
}
|
|
11937
|
-
async function pushSessions(
|
|
12021
|
+
async function pushSessions(apiKey2, agentName, sessions) {
|
|
12022
|
+
if (authCircuitOpen) return;
|
|
11938
12023
|
const c = getClient();
|
|
11939
12024
|
if (!c) return;
|
|
11940
12025
|
try {
|
|
11941
|
-
await c.mutation(upsertSessionsFromPlugin, { apiKeyHash: hashApiKey(
|
|
12026
|
+
await c.mutation(upsertSessionsFromPlugin, { apiKeyHash: hashApiKey(apiKey2), agentName, sessions });
|
|
11942
12027
|
} catch (err) {
|
|
12028
|
+
if (isUnauthorizedError(err)) {
|
|
12029
|
+
tripAuthCircuit();
|
|
12030
|
+
return;
|
|
12031
|
+
}
|
|
11943
12032
|
getLogger().error(`cohort-sync: pushSessions failed: ${err}`);
|
|
11944
12033
|
}
|
|
11945
12034
|
}
|
|
11946
|
-
async function pushActivity(
|
|
11947
|
-
if (entries.length === 0) return;
|
|
12035
|
+
async function pushActivity(apiKey2, entries) {
|
|
12036
|
+
if (authCircuitOpen || entries.length === 0) return;
|
|
11948
12037
|
const c = getClient();
|
|
11949
12038
|
if (!c) return;
|
|
11950
12039
|
try {
|
|
11951
|
-
await c.mutation(pushActivityFromPluginRef, { apiKeyHash: hashApiKey(
|
|
12040
|
+
await c.mutation(pushActivityFromPluginRef, { apiKeyHash: hashApiKey(apiKey2), entries });
|
|
11952
12041
|
} catch (err) {
|
|
12042
|
+
if (isUnauthorizedError(err)) {
|
|
12043
|
+
tripAuthCircuit();
|
|
12044
|
+
return;
|
|
12045
|
+
}
|
|
11953
12046
|
getLogger().error(`cohort-sync: pushActivity failed: ${err}`);
|
|
11954
12047
|
}
|
|
11955
12048
|
}
|
|
11956
|
-
async function pushCronSnapshot(
|
|
12049
|
+
async function pushCronSnapshot(apiKey2, jobs) {
|
|
12050
|
+
if (authCircuitOpen) return false;
|
|
11957
12051
|
const c = getClient();
|
|
11958
12052
|
if (!c) return false;
|
|
11959
12053
|
try {
|
|
11960
|
-
await c.mutation(upsertCronSnapshotFromPluginRef, { apiKeyHash: hashApiKey(
|
|
12054
|
+
await c.mutation(upsertCronSnapshotFromPluginRef, { apiKeyHash: hashApiKey(apiKey2), jobs });
|
|
11961
12055
|
return true;
|
|
11962
12056
|
} catch (err) {
|
|
12057
|
+
if (isUnauthorizedError(err)) {
|
|
12058
|
+
tripAuthCircuit();
|
|
12059
|
+
return false;
|
|
12060
|
+
}
|
|
11963
12061
|
getLogger().error(`cohort-sync: pushCronSnapshot failed: ${err}`);
|
|
11964
12062
|
return false;
|
|
11965
12063
|
}
|
|
11966
12064
|
}
|
|
11967
|
-
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
|
+
}
|
|
11968
12069
|
const c = getClient();
|
|
11969
12070
|
if (!c) {
|
|
11970
12071
|
throw new Error("Convex client not initialized \u2014 subscription may not be active");
|
|
11971
12072
|
}
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
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
|
+
}
|
|
11979
12087
|
}
|
|
11980
12088
|
var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
|
|
11981
12089
|
- Does your planned response address the task's stated scope? If not, do not comment.
|
|
11982
12090
|
- Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
|
|
11983
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
|
+
}
|
|
11984
12102
|
function buildNotificationMessage(n) {
|
|
11985
12103
|
let header;
|
|
11986
12104
|
let cta;
|
|
@@ -11989,11 +12107,11 @@ function buildNotificationMessage(n) {
|
|
|
11989
12107
|
if (n.isMentioned) {
|
|
11990
12108
|
header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
|
|
11991
12109
|
By: ${n.actorName}`;
|
|
11992
|
-
cta =
|
|
12110
|
+
cta = `You were directly mentioned. Read the comment and respond using the cohort_comment tool (taskNumber: ${n.taskNumber}).`;
|
|
11993
12111
|
} else {
|
|
11994
12112
|
header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
|
|
11995
12113
|
From: ${n.actorName}`;
|
|
11996
|
-
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.`;
|
|
11997
12115
|
}
|
|
11998
12116
|
break;
|
|
11999
12117
|
case "assignment":
|
|
@@ -12011,8 +12129,19 @@ By: ${n.actorName}`;
|
|
|
12011
12129
|
From: ${n.actorName}`;
|
|
12012
12130
|
cta = "Check the task and respond if needed.";
|
|
12013
12131
|
}
|
|
12014
|
-
|
|
12015
|
-
|
|
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
|
+
}
|
|
12016
12145
|
let scope = "";
|
|
12017
12146
|
if (n.taskDescription && n.type === "comment") {
|
|
12018
12147
|
const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
|
|
@@ -12029,7 +12158,7 @@ ${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
|
|
|
12029
12158
|
${prompt}` : "";
|
|
12030
12159
|
return `${header}${scope}${body}
|
|
12031
12160
|
|
|
12032
|
-
${cta}${promptBlock}`;
|
|
12161
|
+
${cta}${promptBlock}${TOOLS_REFERENCE}`;
|
|
12033
12162
|
}
|
|
12034
12163
|
async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
12035
12164
|
const response = await fetch(`http://localhost:${port}/hooks/agent`, {
|
|
@@ -12044,13 +12173,16 @@ async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
|
12044
12173
|
agentId,
|
|
12045
12174
|
deliver: false,
|
|
12046
12175
|
sessionKey: `hook:cohort:task-${n.taskNumber}`
|
|
12047
|
-
})
|
|
12176
|
+
}),
|
|
12177
|
+
signal: AbortSignal.timeout(1e4)
|
|
12048
12178
|
});
|
|
12049
12179
|
if (!response.ok) {
|
|
12050
12180
|
throw new Error(`/hooks/agent returned ${response.status} ${response.statusText}`);
|
|
12051
12181
|
}
|
|
12052
12182
|
}
|
|
12053
|
-
|
|
12183
|
+
var deliveryFailures = /* @__PURE__ */ new Map();
|
|
12184
|
+
var MAX_DELIVERY_ATTEMPTS = 3;
|
|
12185
|
+
async function startNotificationSubscription(port, cfg, hooksToken, logger, gwClient) {
|
|
12054
12186
|
const c = getClient();
|
|
12055
12187
|
if (!c) {
|
|
12056
12188
|
logger.warn("cohort-sync: no ConvexClient \u2014 notification subscription skipped");
|
|
@@ -12060,7 +12192,6 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
|
12060
12192
|
logger.warn(
|
|
12061
12193
|
`cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
|
|
12062
12194
|
);
|
|
12063
|
-
return;
|
|
12064
12195
|
}
|
|
12065
12196
|
const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
|
|
12066
12197
|
const reverseNameMap = {};
|
|
@@ -12078,19 +12209,49 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
|
12078
12209
|
getUndeliveredForPlugin,
|
|
12079
12210
|
{ agent: agentName, apiKeyHash },
|
|
12080
12211
|
async (notifications) => {
|
|
12081
|
-
if (processing) return;
|
|
12212
|
+
if (authCircuitOpen || processing) return;
|
|
12082
12213
|
processing = true;
|
|
12083
12214
|
try {
|
|
12084
12215
|
for (const n of notifications) {
|
|
12216
|
+
const failCount = deliveryFailures.get(n._id) ?? 0;
|
|
12217
|
+
if (failCount >= MAX_DELIVERY_ATTEMPTS) {
|
|
12218
|
+
continue;
|
|
12219
|
+
}
|
|
12085
12220
|
try {
|
|
12086
|
-
|
|
12087
|
-
|
|
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
|
+
}
|
|
12088
12229
|
await c.mutation(markDeliveredByPlugin, {
|
|
12089
12230
|
notificationId: n._id,
|
|
12090
12231
|
apiKeyHash
|
|
12091
12232
|
});
|
|
12233
|
+
deliveryFailures.delete(n._id);
|
|
12092
12234
|
} catch (err) {
|
|
12093
|
-
|
|
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
|
+
}
|
|
12094
12255
|
}
|
|
12095
12256
|
}
|
|
12096
12257
|
} finally {
|
|
@@ -12098,6 +12259,10 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
|
12098
12259
|
}
|
|
12099
12260
|
},
|
|
12100
12261
|
(err) => {
|
|
12262
|
+
if (isUnauthorizedError(err)) {
|
|
12263
|
+
tripAuthCircuit();
|
|
12264
|
+
return;
|
|
12265
|
+
}
|
|
12101
12266
|
logger.error(`cohort-sync: subscription error for "${agentName}": ${String(err)}`);
|
|
12102
12267
|
}
|
|
12103
12268
|
);
|
|
@@ -12116,7 +12281,7 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
|
12116
12281
|
getPendingCommandsForPlugin,
|
|
12117
12282
|
{ apiKeyHash },
|
|
12118
12283
|
async (commands) => {
|
|
12119
|
-
if (processing) return;
|
|
12284
|
+
if (authCircuitOpen || processing) return;
|
|
12120
12285
|
if (commands.length === 0) return;
|
|
12121
12286
|
processing = true;
|
|
12122
12287
|
try {
|
|
@@ -12130,6 +12295,10 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
|
12130
12295
|
await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger);
|
|
12131
12296
|
if (cmd.type === "restart") return;
|
|
12132
12297
|
} catch (err) {
|
|
12298
|
+
if (isUnauthorizedError(err)) {
|
|
12299
|
+
tripAuthCircuit();
|
|
12300
|
+
return;
|
|
12301
|
+
}
|
|
12133
12302
|
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
12134
12303
|
try {
|
|
12135
12304
|
await c.mutation(failCommandRef, {
|
|
@@ -12138,6 +12307,10 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
|
12138
12307
|
reason: String(err).slice(0, 500)
|
|
12139
12308
|
});
|
|
12140
12309
|
} catch (failErr) {
|
|
12310
|
+
if (isUnauthorizedError(failErr)) {
|
|
12311
|
+
tripAuthCircuit();
|
|
12312
|
+
return;
|
|
12313
|
+
}
|
|
12141
12314
|
logger.error(`cohort-sync: failed to mark command ${cmd._id} as failed: ${String(failErr)}`);
|
|
12142
12315
|
}
|
|
12143
12316
|
}
|
|
@@ -12147,6 +12320,10 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
|
12147
12320
|
}
|
|
12148
12321
|
},
|
|
12149
12322
|
(err) => {
|
|
12323
|
+
if (isUnauthorizedError(err)) {
|
|
12324
|
+
tripAuthCircuit();
|
|
12325
|
+
return;
|
|
12326
|
+
}
|
|
12150
12327
|
logger.error(`cohort-sync: command subscription error: ${String(err)}`);
|
|
12151
12328
|
}
|
|
12152
12329
|
);
|
|
@@ -12271,7 +12448,8 @@ var ALLOWED_METHODS = /* @__PURE__ */ new Set([
|
|
|
12271
12448
|
"sessions.preview",
|
|
12272
12449
|
"agent",
|
|
12273
12450
|
"snapshot",
|
|
12274
|
-
"system.presence"
|
|
12451
|
+
"system.presence",
|
|
12452
|
+
"gateway.restart"
|
|
12275
12453
|
]);
|
|
12276
12454
|
function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
12277
12455
|
const signedAtMs = Date.now();
|
|
@@ -12330,8 +12508,9 @@ function parseHelloOk(response) {
|
|
|
12330
12508
|
throw new Error(`Unexpected payload type: ${String(payload?.type ?? "missing")}`);
|
|
12331
12509
|
}
|
|
12332
12510
|
const policy = payload.policy ?? {};
|
|
12333
|
-
const
|
|
12334
|
-
const
|
|
12511
|
+
const features = payload.features;
|
|
12512
|
+
const methods = features?.methods ?? payload.methods ?? [];
|
|
12513
|
+
const events = features?.events ?? payload.events ?? [];
|
|
12335
12514
|
return {
|
|
12336
12515
|
methods: new Set(methods),
|
|
12337
12516
|
events: new Set(events),
|
|
@@ -12803,6 +12982,15 @@ function parseSessionKey(key) {
|
|
|
12803
12982
|
if (rest[0] === "main") {
|
|
12804
12983
|
return { kind: "direct", channel: "signal" };
|
|
12805
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
|
+
}
|
|
12806
12994
|
if (rest[0] === "signal") {
|
|
12807
12995
|
if (rest[1] === "group") {
|
|
12808
12996
|
return { kind: "group", channel: "signal", identifier: rest.slice(2).join(":") };
|
|
@@ -12816,6 +13004,14 @@ function parseSessionKey(key) {
|
|
|
12816
13004
|
if (rest[0] === "slack" && rest[1] === "channel") {
|
|
12817
13005
|
return { kind: "group", channel: "slack", identifier: rest[2] };
|
|
12818
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 };
|
|
12819
13015
|
}
|
|
12820
13016
|
return { kind: "cli", channel: "cli" };
|
|
12821
13017
|
}
|
|
@@ -13293,6 +13489,30 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
|
|
|
13293
13489
|
- Don't ignore workspace-specific overrides from your context response.
|
|
13294
13490
|
`;
|
|
13295
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
|
+
|
|
13296
13516
|
// src/hooks.ts
|
|
13297
13517
|
var REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
13298
13518
|
"token",
|
|
@@ -13340,6 +13560,12 @@ try {
|
|
|
13340
13560
|
} catch {
|
|
13341
13561
|
}
|
|
13342
13562
|
diag("MODULE_LOADED", { PLUGIN_VERSION });
|
|
13563
|
+
var _gatewayStartHandler = null;
|
|
13564
|
+
async function handleGatewayStart(event) {
|
|
13565
|
+
if (_gatewayStartHandler) {
|
|
13566
|
+
await _gatewayStartHandler(event);
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
13343
13569
|
function resolveGatewayToken(api) {
|
|
13344
13570
|
const rawToken = api.config?.gateway?.auth?.token;
|
|
13345
13571
|
if (typeof rawToken === "string") return rawToken;
|
|
@@ -13425,7 +13651,7 @@ function saveSessionsToDisk(tracker2) {
|
|
|
13425
13651
|
data.sessions.push({ agentName: name, key });
|
|
13426
13652
|
}
|
|
13427
13653
|
}
|
|
13428
|
-
fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
|
|
13654
|
+
fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data), { mode: 384 });
|
|
13429
13655
|
} catch {
|
|
13430
13656
|
}
|
|
13431
13657
|
}
|
|
@@ -13456,23 +13682,6 @@ function loadSessionsFromDisk(tracker2, logger) {
|
|
|
13456
13682
|
} catch {
|
|
13457
13683
|
}
|
|
13458
13684
|
}
|
|
13459
|
-
async function fetchAgentContext(apiKey, apiUrl, logger) {
|
|
13460
|
-
try {
|
|
13461
|
-
const response = await fetch(`${apiUrl}/api/v1/context`, {
|
|
13462
|
-
method: "GET",
|
|
13463
|
-
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
13464
|
-
});
|
|
13465
|
-
if (!response.ok) {
|
|
13466
|
-
logger.warn(`cohort-sync: /context returned ${response.status}, using pocket guide`);
|
|
13467
|
-
return POCKET_GUIDE;
|
|
13468
|
-
}
|
|
13469
|
-
const data = await response.json();
|
|
13470
|
-
return data.briefing || POCKET_GUIDE;
|
|
13471
|
-
} catch (err) {
|
|
13472
|
-
logger.warn(`cohort-sync: /context fetch failed: ${String(err)}, using pocket guide`);
|
|
13473
|
-
return POCKET_GUIDE;
|
|
13474
|
-
}
|
|
13475
|
-
}
|
|
13476
13685
|
function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
|
|
13477
13686
|
const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
|
|
13478
13687
|
persistentGwClient = client2;
|
|
@@ -13557,6 +13766,12 @@ function registerHooks(api, cfg) {
|
|
|
13557
13766
|
function resolveAgentName(agentId) {
|
|
13558
13767
|
return (nameMap?.[agentId] ?? identityNameMap[agentId] ?? agentId).toLowerCase();
|
|
13559
13768
|
}
|
|
13769
|
+
setToolRuntime({
|
|
13770
|
+
apiKey: cfg.apiKey,
|
|
13771
|
+
apiUrl: cfg.apiUrl,
|
|
13772
|
+
resolveAgentName,
|
|
13773
|
+
logger
|
|
13774
|
+
});
|
|
13560
13775
|
function resolveAgentFromContext(ctx) {
|
|
13561
13776
|
const allCtxKeys = Object.keys(ctx);
|
|
13562
13777
|
diag("RESOLVE_AGENT_FROM_CTX_START", {
|
|
@@ -13638,101 +13853,6 @@ function registerHooks(api, cfg) {
|
|
|
13638
13853
|
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13639
13854
|
commandUnsubscriber2 = unsub;
|
|
13640
13855
|
}
|
|
13641
|
-
api.registerTool((toolCtx) => {
|
|
13642
|
-
const agentId = toolCtx.agentId ?? "main";
|
|
13643
|
-
const agentName = resolveAgentName(agentId);
|
|
13644
|
-
return {
|
|
13645
|
-
name: "cohort_comment",
|
|
13646
|
-
label: "cohort_comment",
|
|
13647
|
-
description: "Post a comment on a Cohort task. Use this to respond to @mentions or collaborate on tasks.",
|
|
13648
|
-
parameters: Type.Object({
|
|
13649
|
-
task_number: Type.Number({ description: "Task number (e.g. 312)" }),
|
|
13650
|
-
comment: Type.String({ description: "Comment text to post" }),
|
|
13651
|
-
no_reply: Type.Optional(Type.Boolean({
|
|
13652
|
-
description: "If true, no notifications will be sent for this comment. Use for final/closing comments."
|
|
13653
|
-
}))
|
|
13654
|
-
}),
|
|
13655
|
-
async execute(_toolCallId, params) {
|
|
13656
|
-
try {
|
|
13657
|
-
const result = await callAddCommentFromPlugin(cfg.apiKey, {
|
|
13658
|
-
taskNumber: params.task_number,
|
|
13659
|
-
agentName,
|
|
13660
|
-
content: params.comment,
|
|
13661
|
-
noReply: params.no_reply ?? false
|
|
13662
|
-
});
|
|
13663
|
-
const lines = [`Comment posted on task #${params.task_number}.`];
|
|
13664
|
-
if (result.stats) {
|
|
13665
|
-
lines.push("");
|
|
13666
|
-
lines.push(`This task has ${result.stats.totalComments} comments. ${result.stats.myRecentCount}/${result.stats.threshold} hourly limit used on this task.`);
|
|
13667
|
-
}
|
|
13668
|
-
if (result.budget) {
|
|
13669
|
-
lines.push(`Daily budget: ${result.budget.used}/${result.budget.limit}`);
|
|
13670
|
-
}
|
|
13671
|
-
return {
|
|
13672
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
13673
|
-
details: result
|
|
13674
|
-
};
|
|
13675
|
-
} catch (err) {
|
|
13676
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
13677
|
-
if (msg.includes("AGENT_COMMENTS_LOCKED")) {
|
|
13678
|
-
return {
|
|
13679
|
-
content: [{
|
|
13680
|
-
type: "text",
|
|
13681
|
-
text: `Cannot comment on task #${params.task_number}.
|
|
13682
|
-
Reason: Agent comments are locked on this task.
|
|
13683
|
-
Do not re-attempt to comment on this task.`
|
|
13684
|
-
}],
|
|
13685
|
-
details: { error: "AGENT_COMMENTS_LOCKED", taskNumber: params.task_number }
|
|
13686
|
-
};
|
|
13687
|
-
}
|
|
13688
|
-
if (msg.includes("TASK_HOUR_LIMIT_REACHED")) {
|
|
13689
|
-
const parts = msg.split("|");
|
|
13690
|
-
const count = parts[1] ?? "?";
|
|
13691
|
-
const limit = parts[2] ?? "?";
|
|
13692
|
-
return {
|
|
13693
|
-
content: [{
|
|
13694
|
-
type: "text",
|
|
13695
|
-
text: `Cannot comment on task #${params.task_number}.
|
|
13696
|
-
Reason: You have posted ${count} comments on this task in the last hour (limit: ${limit}).
|
|
13697
|
-
Step back from this task. Do not comment again until the next hour.`
|
|
13698
|
-
}],
|
|
13699
|
-
details: { error: "TASK_HOUR_LIMIT_REACHED", count, limit, taskNumber: params.task_number }
|
|
13700
|
-
};
|
|
13701
|
-
}
|
|
13702
|
-
if (msg.includes("DAILY_LIMIT_REACHED")) {
|
|
13703
|
-
const parts = msg.split("|");
|
|
13704
|
-
const used = parts[1] ?? "?";
|
|
13705
|
-
const max = parts[2] ?? "?";
|
|
13706
|
-
const resetAt = parts[3] ?? "tomorrow";
|
|
13707
|
-
return {
|
|
13708
|
-
content: [{
|
|
13709
|
-
type: "text",
|
|
13710
|
-
text: `Cannot comment on task #${params.task_number}.
|
|
13711
|
-
Reason: Daily comment limit reached (${used}/${max}).
|
|
13712
|
-
Do not attempt to make more comments until ${resetAt}.`
|
|
13713
|
-
}],
|
|
13714
|
-
details: { error: "DAILY_LIMIT_REACHED", used, max, resetAt, taskNumber: params.task_number }
|
|
13715
|
-
};
|
|
13716
|
-
}
|
|
13717
|
-
throw err;
|
|
13718
|
-
}
|
|
13719
|
-
}
|
|
13720
|
-
};
|
|
13721
|
-
});
|
|
13722
|
-
api.registerTool(() => {
|
|
13723
|
-
return {
|
|
13724
|
-
name: "cohort_context",
|
|
13725
|
-
label: "cohort_context",
|
|
13726
|
-
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.",
|
|
13727
|
-
parameters: Type.Object({}),
|
|
13728
|
-
async execute() {
|
|
13729
|
-
const briefing = await fetchAgentContext(cfg.apiKey, cfg.apiUrl, logger);
|
|
13730
|
-
return {
|
|
13731
|
-
content: [{ type: "text", text: briefing }]
|
|
13732
|
-
};
|
|
13733
|
-
}
|
|
13734
|
-
};
|
|
13735
|
-
});
|
|
13736
13856
|
function resolveModel(agentId) {
|
|
13737
13857
|
const agent = config?.agents?.list?.find((a) => a.id === agentId);
|
|
13738
13858
|
const m = agent?.model;
|
|
@@ -13752,7 +13872,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13752
13872
|
if (m.includes("deepseek")) return 128e3;
|
|
13753
13873
|
return 2e5;
|
|
13754
13874
|
}
|
|
13755
|
-
|
|
13875
|
+
_gatewayStartHandler = async (event) => {
|
|
13756
13876
|
diag("HOOK_gateway_start", { port: event.port, eventKeys: Object.keys(event) });
|
|
13757
13877
|
try {
|
|
13758
13878
|
checkForUpdate(PLUGIN_VERSION, logger).catch(() => {
|
|
@@ -13803,7 +13923,8 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13803
13923
|
event.port,
|
|
13804
13924
|
cfg,
|
|
13805
13925
|
api.config.hooks?.token,
|
|
13806
|
-
logger
|
|
13926
|
+
logger,
|
|
13927
|
+
persistentGwClient
|
|
13807
13928
|
).catch((err) => {
|
|
13808
13929
|
logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
|
|
13809
13930
|
});
|
|
@@ -13840,7 +13961,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13840
13961
|
saveSessionsToDisk(tracker2);
|
|
13841
13962
|
}, KEEPALIVE_INTERVAL_MS);
|
|
13842
13963
|
logger.info(`cohort-sync: keepalive interval started (${KEEPALIVE_INTERVAL_MS / 1e3}s)`);
|
|
13843
|
-
}
|
|
13964
|
+
};
|
|
13844
13965
|
api.on("agent_end", async (event, ctx) => {
|
|
13845
13966
|
diag("HOOK_agent_end", { ctx: dumpCtx(ctx), success: event.success, error: event.error, durationMs: event.durationMs });
|
|
13846
13967
|
const agentId = ctx.agentId ?? "main";
|
|
@@ -14190,11 +14311,11 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14190
14311
|
import { execFile as execFile2 } from "node:child_process";
|
|
14191
14312
|
|
|
14192
14313
|
// src/device-auth.ts
|
|
14193
|
-
function baseUrl(
|
|
14194
|
-
return
|
|
14314
|
+
function baseUrl(apiUrl2) {
|
|
14315
|
+
return apiUrl2.replace(/\/+$/, "");
|
|
14195
14316
|
}
|
|
14196
|
-
async function startDeviceAuth(
|
|
14197
|
-
const url = `${baseUrl(
|
|
14317
|
+
async function startDeviceAuth(apiUrl2, manifest) {
|
|
14318
|
+
const url = `${baseUrl(apiUrl2)}/api/v1/device-auth/start`;
|
|
14198
14319
|
const res = await fetch(url, {
|
|
14199
14320
|
method: "POST",
|
|
14200
14321
|
headers: { "Content-Type": "application/json" },
|
|
@@ -14209,8 +14330,8 @@ async function startDeviceAuth(apiUrl, manifest) {
|
|
|
14209
14330
|
}
|
|
14210
14331
|
return res.json();
|
|
14211
14332
|
}
|
|
14212
|
-
async function pollDeviceAuth(
|
|
14213
|
-
const url = `${baseUrl(
|
|
14333
|
+
async function pollDeviceAuth(apiUrl2, deviceCode) {
|
|
14334
|
+
const url = `${baseUrl(apiUrl2)}/api/v1/device-auth/poll`;
|
|
14214
14335
|
const res = await fetch(url, {
|
|
14215
14336
|
method: "POST",
|
|
14216
14337
|
headers: { "Content-Type": "application/json" },
|
|
@@ -14225,7 +14346,7 @@ async function pollDeviceAuth(apiUrl, deviceCode) {
|
|
|
14225
14346
|
}
|
|
14226
14347
|
return res.json();
|
|
14227
14348
|
}
|
|
14228
|
-
async function waitForApproval(
|
|
14349
|
+
async function waitForApproval(apiUrl2, deviceCode, opts) {
|
|
14229
14350
|
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
14230
14351
|
const timeoutMs = opts?.timeoutMs ?? 9e5;
|
|
14231
14352
|
const onPoll = opts?.onPoll;
|
|
@@ -14236,7 +14357,7 @@ async function waitForApproval(apiUrl, deviceCode, opts) {
|
|
|
14236
14357
|
return { status: "timeout" };
|
|
14237
14358
|
}
|
|
14238
14359
|
try {
|
|
14239
|
-
const result = await pollDeviceAuth(
|
|
14360
|
+
const result = await pollDeviceAuth(apiUrl2, deviceCode);
|
|
14240
14361
|
onPoll?.(result.status);
|
|
14241
14362
|
if (result.status === "approved") {
|
|
14242
14363
|
return { status: "approved", apiKey: result.apiKey };
|
|
@@ -14353,45 +14474,292 @@ var plugin = {
|
|
|
14353
14474
|
description: "Syncs agent status and skills to Cohort dashboard",
|
|
14354
14475
|
register(api) {
|
|
14355
14476
|
const cfg = api.pluginConfig;
|
|
14356
|
-
const
|
|
14357
|
-
if (!
|
|
14477
|
+
const apiUrl2 = cfg?.apiUrl || DEFAULT_API_URL;
|
|
14478
|
+
if (!apiUrl2.startsWith("https://") && !apiUrl2.startsWith("http://localhost") && !apiUrl2.startsWith("http://127.0.0.1")) {
|
|
14358
14479
|
api.logger.error(
|
|
14359
|
-
"cohort-sync: apiUrl
|
|
14480
|
+
"cohort-sync: apiUrl must use HTTPS for security. Got: " + apiUrl2.replace(/\/\/.*@/, "//***@")
|
|
14360
14481
|
);
|
|
14361
14482
|
return;
|
|
14362
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
|
});
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"additionalProperties": false,
|
|
36
36
|
"properties": {
|
|
37
37
|
"apiUrl": {
|
|
38
|
-
"type": "string"
|
|
38
|
+
"type": "string",
|
|
39
|
+
"default": "https://api.cohort.bot"
|
|
39
40
|
},
|
|
40
41
|
"apiKey": {
|
|
41
42
|
"type": "string"
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
|
-
"version": "0.
|
|
58
|
+
"version": "0.9.1"
|
|
58
59
|
}
|
package/dist/package.json
CHANGED