@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 CHANGED
@@ -16,9 +16,9 @@ __export(keychain_exports, {
16
16
  setCredential: () => setCredential
17
17
  });
18
18
  import { execFile } from "node:child_process";
19
- import os3 from "node:os";
19
+ import os4 from "node:os";
20
20
  function assertMacOS(operation) {
21
- if (os3.platform() !== "darwin") {
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(apiUrl, apiKey) {
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
- apiUrl,
54
+ apiUrl2,
55
55
  "-w",
56
- apiKey,
56
+ apiKey2,
57
57
  "-U"
58
58
  ]);
59
59
  }
60
- async function getCredential(apiUrl) {
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
- apiUrl,
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(apiUrl) {
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
- apiUrl
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
- const jsonEndBracket = raw.lastIndexOf("]");
2716
- const jsonEndBrace = raw.lastIndexOf("}");
2717
- const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
2718
- if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
2719
- throw new Error("No JSON found in output");
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
- return raw.slice(jsonStart, jsonEnd + 1);
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(apiUrl, apiKey, path4) {
2749
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
2750
- headers: { Authorization: `Bearer ${apiKey}` },
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(apiUrl, apiKey, path4, body) {
2757
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
2778
+ async function v1Patch(apiUrl2, apiKey2, path4, body) {
2779
+ const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
2758
2780
  method: "PATCH",
2759
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
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(apiUrl, apiKey, path4, body) {
2766
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
2787
+ async function v1Post(apiUrl2, apiKey2, path4, body) {
2788
+ const res = await fetch(`${apiUrl2.replace(/\/+$/, "")}${path4}`, {
2767
2789
  method: "POST",
2768
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
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 "npm install -g @cfio/cohort-sync" to update`
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 os4 = __require("os");
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 || os4.arch();
7870
- var platform = process.env.npm_config_platform || os4.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 = createHash("sha1").update(key + GUID).digest("base64");
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 = createHash("sha1").update(key + GUID).digest("base64");
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 cronRunNowPoll = null;
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
- logger.info("cohort-sync: restart command, terminating in 500ms");
11780
- await new Promise((r) => setTimeout(r, 500));
11781
- process.kill(process.pid, "SIGTERM");
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
- const runResult = await gwClient.request(
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 deriveConvexUrl(apiUrl) {
11893
- return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
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(apiKey, data) {
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, { apiKey, ...data });
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(apiKey, agentName, sessions) {
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, { apiKey, agentName, sessions });
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(apiKey, entries) {
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, { apiKey, entries });
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(apiKey, jobs) {
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, { apiKey, jobs });
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(apiKey, args) {
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
- return await c.mutation(addCommentFromPluginRef, {
11997
- apiKey,
11998
- taskNumber: args.taskNumber,
11999
- agentName: args.agentName,
12000
- content: args.content,
12001
- noReply: args.noReply
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 = "You were directly mentioned. Read the comment and respond using the cohort_comment tool.";
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 = "Read the comment thread and respond using the cohort_comment tool if appropriate.";
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
- const body = n.preview ? `
12039
- Comment: "${n.preview}"` : "";
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
- async function startNotificationSubscription(port, cfg, hooksToken, logger) {
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, apiKey: cfg.apiKey },
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
- await injectNotification(port, hooksToken, n, openclawAgentId);
12110
- logger.info(`cohort-sync: injected notification for task #${n.taskNumber} (${agentName})`);
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
- apiKey: cfg.apiKey
12231
+ apiKeyHash
12114
12232
  });
12233
+ deliveryFailures.delete(n._id);
12115
12234
  } catch (err) {
12116
- logger.warn(`cohort-sync: failed to inject notification ${n._id}: ${String(err)}`);
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
- { apiKey: cfg.apiKey },
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
- apiKey: cfg.apiKey
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
- apiKey: cfg.apiKey,
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 methods = payload.methods ?? [];
12356
- const events = payload.events ?? [];
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
- async function fetchAgentContext(apiKey, apiUrl, logger) {
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
- registerCronEventHandlers(client2, cfg, resolveAgentName);
13504
- if (client2.availableEvents.has("shutdown")) {
13505
- client2.on("shutdown", () => {
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 cronResult2 = await client2.request("cron.list", { includeDisabled: true });
13515
- const jobs2 = Array.isArray(cronResult2) ? cronResult2 : cronResult2?.jobs ?? [];
13516
- const mapped2 = jobs2.map((j) => mapCronJob(j, resolveAgentName));
13517
- await pushCronSnapshot(cfg.apiKey, mapped2);
13518
- diag("GW_CLIENT_RECONNECT_CRON_PUSH", { count: mapped2.length });
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("GW_CLIENT_RECONNECT_CRON_FAILED", { error: String(err) });
13706
+ diag("GW_CLIENT_CRON_PUSH_FAILED", { error: String(err) });
13521
13707
  }
13522
13708
  };
13523
- diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
13524
- const cronResult = await client2.request("cron.list", { includeDisabled: true });
13525
- const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13526
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13527
- await pushCronSnapshot(cfg.apiKey, mapped);
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
- api.on("gateway_start", async (event) => {
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(apiUrl) {
14199
- return apiUrl.replace(/\/+$/, "");
14314
+ function baseUrl(apiUrl2) {
14315
+ return apiUrl2.replace(/\/+$/, "");
14200
14316
  }
14201
- async function startDeviceAuth(apiUrl, manifest) {
14202
- const url = `${baseUrl(apiUrl)}/api/v1/device-auth/start`;
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(apiUrl, deviceCode) {
14218
- const url = `${baseUrl(apiUrl)}/api/v1/device-auth/poll`;
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(apiUrl, deviceCode, opts) {
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(apiUrl, deviceCode);
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 apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
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: ${apiUrl})`);
14375
- let apiKey = cfg?.apiKey;
14376
- if (!apiKey) {
14742
+ api.logger.info(`cohort-sync: service starting (api: ${apiUrl2})`);
14743
+ let apiKey2 = cfg?.apiKey;
14744
+ if (!apiKey2) {
14377
14745
  try {
14378
- apiKey = await getCredential(apiUrl) ?? void 0;
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 (!apiKey) {
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: ${apiUrl})`);
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
  });