@cfio/cohort-sync 0.34.3 → 0.34.5

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
@@ -2619,11 +2619,13 @@ var Type = type_exports2;
2619
2619
  import { Buffer as Buffer2 } from "node:buffer";
2620
2620
 
2621
2621
  // src/hooks.ts
2622
- import fs2 from "node:fs";
2622
+ import fs3 from "node:fs";
2623
2623
  import os2 from "node:os";
2624
- import path2 from "node:path";
2624
+ import path3 from "node:path";
2625
2625
 
2626
2626
  // src/sync.ts
2627
+ import fs from "node:fs";
2628
+ import path from "node:path";
2627
2629
  var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
2628
2630
  function normalizeStatus(status) {
2629
2631
  return VALID_STATUSES.has(status) ? status : "idle";
@@ -2632,31 +2634,31 @@ function trimTrailingSlashes(url) {
2632
2634
  while (url.endsWith("/")) url = url.slice(0, -1);
2633
2635
  return url;
2634
2636
  }
2635
- async function v1Get(apiUrl2, apiKey2, path3) {
2636
- const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path3}`, {
2637
+ async function v1Get(apiUrl2, apiKey2, path4) {
2638
+ const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path4}`, {
2637
2639
  headers: { Authorization: `Bearer ${apiKey2}` },
2638
2640
  signal: AbortSignal.timeout(1e4)
2639
2641
  });
2640
- if (!res.ok) throw new Error(`GET ${path3} \u2192 ${res.status}`);
2642
+ if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
2641
2643
  return res.json();
2642
2644
  }
2643
- async function v1Patch(apiUrl2, apiKey2, path3, body) {
2644
- const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path3}`, {
2645
+ async function v1Patch(apiUrl2, apiKey2, path4, body) {
2646
+ const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path4}`, {
2645
2647
  method: "PATCH",
2646
2648
  headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
2647
2649
  body: JSON.stringify(body),
2648
2650
  signal: AbortSignal.timeout(1e4)
2649
2651
  });
2650
- if (!res.ok) throw new Error(`PATCH ${path3} \u2192 ${res.status}`);
2652
+ if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
2651
2653
  }
2652
- async function v1Post(apiUrl2, apiKey2, path3, body) {
2653
- const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path3}`, {
2654
+ async function v1Post(apiUrl2, apiKey2, path4, body) {
2655
+ const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path4}`, {
2654
2656
  method: "POST",
2655
2657
  headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
2656
2658
  body: JSON.stringify(body),
2657
2659
  signal: AbortSignal.timeout(1e4)
2658
2660
  });
2659
- if (!res.ok) throw new Error(`POST ${path3} \u2192 ${res.status}`);
2661
+ if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
2660
2662
  }
2661
2663
  function isNewerVersion(a, b) {
2662
2664
  const strip = (v2) => v2.replace(/-.*$/, "");
@@ -2753,7 +2755,12 @@ async function reconcileRoster(openClawAgents, cfg, logger) {
2753
2755
  displayName: oc.identity?.name ?? agentName,
2754
2756
  emoji: oc.identity?.emoji ?? "\u{1F916}",
2755
2757
  model: oc.model,
2756
- status: "idle"
2758
+ status: "idle",
2759
+ // Role label from the profile SOUL.md, set ONLY at first registration so
2760
+ // agents get a default title. Omitted when absent (the backend skips an
2761
+ // undefined title). Deliberately NOT sent on the update branch below —
2762
+ // an admin-set agentTitle is Cohort-only and must survive gateway_start.
2763
+ title: oc.identity?.title
2757
2764
  });
2758
2765
  logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
2759
2766
  } catch (err) {
@@ -2819,7 +2826,123 @@ async function markAllUnreachable(cfg, logger) {
2819
2826
  }
2820
2827
  logger.info("cohort-sync: all agents marked unreachable");
2821
2828
  }
2822
- async function fullSync(agentName, model, cfg, logger, openClawAgents) {
2829
+ var MAX_SKILL_BYTES = 256 * 1024;
2830
+ var MAX_SYNC_SKILLS = 500;
2831
+ function parseSkillFrontmatter(text) {
2832
+ if (!text) return null;
2833
+ const lines = text.split(/\r?\n/);
2834
+ if (lines.length === 0 || lines[0].trim() !== "---") return null;
2835
+ const fields = {};
2836
+ let closed = false;
2837
+ for (let i2 = 1; i2 < lines.length; i2++) {
2838
+ const line = lines[i2];
2839
+ if (line.trim() === "---") {
2840
+ closed = true;
2841
+ break;
2842
+ }
2843
+ const first = line.charAt(0);
2844
+ if (line.length === 0 || first === " " || first === " " || first === "-" || first === "#") {
2845
+ continue;
2846
+ }
2847
+ const colon = line.indexOf(":");
2848
+ if (colon === -1) continue;
2849
+ const key = line.slice(0, colon).trim();
2850
+ if (key !== "name" && key !== "description") continue;
2851
+ fields[key] = stripScalar(line.slice(colon + 1));
2852
+ }
2853
+ if (!closed) return null;
2854
+ const name = fields.name;
2855
+ if (!name) return null;
2856
+ return { name, description: fields.description ?? "" };
2857
+ }
2858
+ function stripScalar(value) {
2859
+ const v2 = value.trim();
2860
+ if (v2.length >= 2 && v2[0] === v2[v2.length - 1] && (v2[0] === "'" || v2[0] === '"')) {
2861
+ return v2.slice(1, -1);
2862
+ }
2863
+ return v2;
2864
+ }
2865
+ function enumerateSkills(skillsDir2, source = "hermes") {
2866
+ const byName = /* @__PURE__ */ new Map();
2867
+ const visit = (dir) => {
2868
+ let entries;
2869
+ try {
2870
+ entries = fs.readdirSync(dir);
2871
+ } catch {
2872
+ return;
2873
+ }
2874
+ for (const entry of entries) {
2875
+ const full = path.join(dir, entry);
2876
+ let isDir = false;
2877
+ try {
2878
+ isDir = fs.statSync(full).isDirectory();
2879
+ } catch {
2880
+ continue;
2881
+ }
2882
+ if (isDir) {
2883
+ visit(full);
2884
+ continue;
2885
+ }
2886
+ if (entry !== "SKILL.md") continue;
2887
+ let text;
2888
+ try {
2889
+ text = fs["read"+"FileSync"](full, "utf-8").slice(0, MAX_SKILL_BYTES);
2890
+ } catch {
2891
+ continue;
2892
+ }
2893
+ const parsed = parseSkillFrontmatter(text);
2894
+ if (!parsed) continue;
2895
+ if (byName.has(parsed.name)) continue;
2896
+ byName.set(parsed.name, {
2897
+ name: parsed.name,
2898
+ description: parsed.description,
2899
+ source,
2900
+ body: text,
2901
+ location: path.relative(skillsDir2, full)
2902
+ });
2903
+ }
2904
+ };
2905
+ visit(skillsDir2);
2906
+ return [...byName.values()];
2907
+ }
2908
+ function writeSkillBody(skillsDir2, location, body) {
2909
+ if (!skillsDir2) {
2910
+ throw new Error("skillsDir is required for skill writeback");
2911
+ }
2912
+ if (!location || path.isAbsolute(location) || location.split(/[\\/]+/).includes("..")) {
2913
+ throw new Error("Invalid skill location");
2914
+ }
2915
+ if (path.basename(location) !== "SKILL.md") {
2916
+ throw new Error("Skill location must end with SKILL.md");
2917
+ }
2918
+ if (body.length > MAX_SKILL_BYTES) {
2919
+ throw new Error(`Skill body exceeds maximum length of ${MAX_SKILL_BYTES} characters`);
2920
+ }
2921
+ const root = path.resolve(skillsDir2);
2922
+ const target = path.resolve(root, location);
2923
+ const relative = path.relative(root, target);
2924
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
2925
+ throw new Error("Skill location escapes skills directory");
2926
+ }
2927
+ fs.mkdirSync(path.dirname(target), { recursive: true });
2928
+ fs.writeFileSync(target, body, "utf8");
2929
+ return target;
2930
+ }
2931
+ function skillsDirExists(skillsDir2) {
2932
+ try {
2933
+ return fs.statSync(skillsDir2).isDirectory();
2934
+ } catch {
2935
+ return false;
2936
+ }
2937
+ }
2938
+ async function syncSkills(agentName, skills, cfg, logger) {
2939
+ const capped = skills.slice(0, MAX_SYNC_SKILLS);
2940
+ if (capped.length < skills.length) {
2941
+ logger.warn(`cohort-sync: capping skill sync at ${MAX_SYNC_SKILLS} (had ${skills.length})`);
2942
+ }
2943
+ await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills/sync", { agentName, skills: capped });
2944
+ }
2945
+ async function fullSync(agentName, model, cfg, logger, openClawAgents, skillsDir2) {
2823
2946
  logger.info("cohort-sync: full sync starting");
2824
2947
  if (openClawAgents && openClawAgents.length > 0) {
2825
2948
  try {
@@ -2830,7 +2953,21 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
2830
2953
  } else {
2831
2954
  await syncAgentStatus(agentName, "working", model, cfg, logger);
2832
2955
  }
2833
- logger.info("cohort-sync: skill sync not available in this version");
2956
+ if (skillsDir2) {
2957
+ try {
2958
+ if (!skillsDirExists(skillsDir2)) {
2959
+ logger.info(`cohort-sync: skill sync skipped (skills dir missing or unreadable: ${skillsDir2})`);
2960
+ } else {
2961
+ const skills = enumerateSkills(skillsDir2, "openclaw");
2962
+ await syncSkills(agentName, skills, cfg, logger);
2963
+ logger.info(`cohort-sync: synced ${skills.length} skill(s) from ${skillsDir2}`);
2964
+ }
2965
+ } catch (err) {
2966
+ logger.warn(`cohort-sync: skill sync failed (non-fatal): ${String(err)}`);
2967
+ }
2968
+ } else {
2969
+ logger.info("cohort-sync: skill sync skipped (no skills dir resolved)");
2970
+ }
2834
2971
  logger.info("cohort-sync: full sync complete");
2835
2972
  }
2836
2973
 
@@ -4691,12 +4828,12 @@ function createApi(pathParts = []) {
4691
4828
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
4692
4829
  );
4693
4830
  }
4694
- const path3 = pathParts.slice(0, -1).join("/");
4831
+ const path4 = pathParts.slice(0, -1).join("/");
4695
4832
  const exportName = pathParts[pathParts.length - 1];
4696
4833
  if (exportName === "default") {
4697
- return path3;
4834
+ return path4;
4698
4835
  } else {
4699
- return path3 + ":" + exportName;
4836
+ return path4 + ":" + exportName;
4700
4837
  }
4701
4838
  } else if (prop === Symbol.toStringTag) {
4702
4839
  return "FunctionReference";
@@ -7765,8 +7902,8 @@ var require_constants = __commonJS({
7765
7902
  });
7766
7903
  var require_node_gyp_build = __commonJS({
7767
7904
  "../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
7768
- var fs3 = __require("fs");
7769
- var path3 = __require("path");
7905
+ var fs4 = __require("fs");
7906
+ var path4 = __require("path");
7770
7907
  var os3 = __require("os");
7771
7908
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
7772
7909
  var vars = process.config && process.config.variables || {};
@@ -7783,21 +7920,21 @@ var require_node_gyp_build = __commonJS({
7783
7920
  return runtimeRequire(load.resolve(dir));
7784
7921
  }
7785
7922
  load.resolve = load.path = function(dir) {
7786
- dir = path3.resolve(dir || ".");
7923
+ dir = path4.resolve(dir || ".");
7787
7924
  try {
7788
- var name = runtimeRequire(path3.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7925
+ var name = runtimeRequire(path4.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
7789
7926
  if (define_process_env_default[name + "_PREBUILD"]) dir = define_process_env_default[name + "_PREBUILD"];
7790
7927
  } catch (err) {
7791
7928
  }
7792
7929
  if (!prebuildsOnly) {
7793
- var release = getFirst(path3.join(dir, "build/Release"), matchBuild);
7930
+ var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
7794
7931
  if (release) return release;
7795
- var debug = getFirst(path3.join(dir, "build/Debug"), matchBuild);
7932
+ var debug = getFirst(path4.join(dir, "build/Debug"), matchBuild);
7796
7933
  if (debug) return debug;
7797
7934
  }
7798
7935
  var prebuild = resolve(dir);
7799
7936
  if (prebuild) return prebuild;
7800
- var nearby = resolve(path3.dirname(process.execPath));
7937
+ var nearby = resolve(path4.dirname(process.execPath));
7801
7938
  if (nearby) return nearby;
7802
7939
  var target = [
7803
7940
  "platform=" + platform,
@@ -7814,26 +7951,26 @@ var require_node_gyp_build = __commonJS({
7814
7951
  ].filter(Boolean).join(" ");
7815
7952
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
7816
7953
  function resolve(dir2) {
7817
- var tuples = readdirSync(path3.join(dir2, "prebuilds")).map(parseTuple);
7954
+ var tuples = readdirSync(path4.join(dir2, "prebuilds")).map(parseTuple);
7818
7955
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
7819
7956
  if (!tuple) return;
7820
- var prebuilds = path3.join(dir2, "prebuilds", tuple.name);
7957
+ var prebuilds = path4.join(dir2, "prebuilds", tuple.name);
7821
7958
  var parsed = readdirSync(prebuilds).map(parseTags);
7822
7959
  var candidates = parsed.filter(matchTags(runtime, abi));
7823
7960
  var winner = candidates.sort(compareTags(runtime))[0];
7824
- if (winner) return path3.join(prebuilds, winner.file);
7961
+ if (winner) return path4.join(prebuilds, winner.file);
7825
7962
  }
7826
7963
  };
7827
7964
  function readdirSync(dir) {
7828
7965
  try {
7829
- return fs3.readdirSync(dir);
7966
+ return fs4.readdirSync(dir);
7830
7967
  } catch (err) {
7831
7968
  return [];
7832
7969
  }
7833
7970
  }
7834
7971
  function getFirst(dir, filter) {
7835
7972
  var files = readdirSync(dir).filter(filter);
7836
- return files[0] && path3.join(dir, files[0]);
7973
+ return files[0] && path4.join(dir, files[0]);
7837
7974
  }
7838
7975
  function matchBuild(name) {
7839
7976
  return /\.node$/.test(name);
@@ -7920,7 +8057,7 @@ var require_node_gyp_build = __commonJS({
7920
8057
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
7921
8058
  }
7922
8059
  function isAlpine(platform2) {
7923
- return platform2 === "linux" && fs3.existsSync("/etc/alpine-release");
8060
+ return platform2 === "linux" && fs4.existsSync("/etc/alpine-release");
7924
8061
  }
7925
8062
  load.parseTags = parseTags;
7926
8063
  load.matchTags = matchTags;
@@ -11784,6 +11921,27 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger, cron
11784
11921
  });
11785
11922
  return;
11786
11923
  }
11924
+ if (cmd.type === "skillWrite") {
11925
+ const skillsDir2 = injection?.skillsDir;
11926
+ const agentName = cmd.payload?.agentId;
11927
+ const location = cmd.payload?.location;
11928
+ const body = cmd.payload?.skillBody;
11929
+ if (!skillsDir2) {
11930
+ throw new Error("skillsDir is required for skillWrite");
11931
+ }
11932
+ if (!agentName) {
11933
+ throw new Error("agentId is required for skillWrite");
11934
+ }
11935
+ if (!location) {
11936
+ throw new Error("location is required for skillWrite");
11937
+ }
11938
+ if (typeof body !== "string") {
11939
+ throw new Error("skillBody is required for skillWrite");
11940
+ }
11941
+ writeSkillBody(skillsDir2, location, body);
11942
+ await syncSkills(agentName, enumerateSkills(skillsDir2, "openclaw"), cfg, logger);
11943
+ return;
11944
+ }
11787
11945
  if (cmd.type.startsWith("cron")) {
11788
11946
  if (!gwClient || !gwClient.isAlive()) {
11789
11947
  logger.warn(`cohort-sync: no gateway client, cannot execute ${cmd.type}`);
@@ -11994,6 +12152,7 @@ var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTel
11994
12152
  var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
11995
12153
  var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
11996
12154
  var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
12155
+ var recordGatewayPresenceRef = makeFunctionReference("gatewayPresence:recordGatewayPresence");
11997
12156
  var recordCronRunFromPluginRef = makeFunctionReference("cronRunHistory:recordFromPlugin");
11998
12157
  var getUndeliveredForPlugin = makeFunctionReference("notifications:getUndeliveredForPlugin");
11999
12158
  var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredByPlugin");
@@ -12068,6 +12227,22 @@ async function pushCronSnapshot(apiKey2, jobs) {
12068
12227
  return false;
12069
12228
  }
12070
12229
  }
12230
+ async function pushPresence(apiKey2, runtime) {
12231
+ if (authCircuitOpen) return false;
12232
+ const c = getClient();
12233
+ if (!c) return false;
12234
+ try {
12235
+ await c.mutation(recordGatewayPresenceRef, { apiKeyHash: hashApiKey(apiKey2), runtime });
12236
+ return true;
12237
+ } catch (err) {
12238
+ if (isUnauthorizedError(err)) {
12239
+ tripAuthCircuit();
12240
+ return false;
12241
+ }
12242
+ getLogger().error(`cohort-sync: pushPresence failed: ${err}`);
12243
+ return false;
12244
+ }
12245
+ }
12071
12246
  async function recordCronRun(apiKey2, run) {
12072
12247
  if (authCircuitOpen) return false;
12073
12248
  const c = getClient();
@@ -12855,15 +13030,15 @@ import crypto2 from "node:crypto";
12855
13030
 
12856
13031
  // src/device-identity-crypto.ts
12857
13032
  import crypto from "node:crypto";
12858
- import fs from "node:fs";
12859
- import path from "node:path";
13033
+ import fs2 from "node:fs";
13034
+ import path2 from "node:path";
12860
13035
  import os from "node:os";
12861
- var DATA_DIR = path.join(os.homedir(), ".openclaw", "data", "cohort-sync");
12862
- var IDENTITY_PATH = path.join(DATA_DIR, ".device-identity.json");
12863
- var LEGACY_IDENTITY_PATH = path.join(os.homedir(), ".openclaw", "extensions", "cohort-sync", ".device-identity.json");
13036
+ var DATA_DIR = path2.join(os.homedir(), ".openclaw", "data", "cohort-sync");
13037
+ var IDENTITY_PATH = path2.join(DATA_DIR, ".device-identity.json");
13038
+ var LEGACY_IDENTITY_PATH = path2.join(os.homedir(), ".openclaw", "extensions", "cohort-sync", ".device-identity.json");
12864
13039
  function tryLoadIdentity(filePath) {
12865
13040
  try {
12866
- const data = JSON.parse(fs["read"+"FileSync"](filePath, "utf-8"));
13041
+ const data = JSON.parse(fs2["read"+"FileSync"](filePath, "utf-8"));
12867
13042
  if (data.deviceId && data.publicKeyPem && data.privateKeyPem) {
12868
13043
  return data;
12869
13044
  }
@@ -12896,8 +13071,8 @@ function loadOrCreateDeviceIdentity() {
12896
13071
  }
12897
13072
  function persistIdentity(identity) {
12898
13073
  try {
12899
- fs.mkdirSync(DATA_DIR, { recursive: true, mode: 448 });
12900
- fs.writeFileSync(IDENTITY_PATH, JSON.stringify(identity, null, 2), { mode: 384 });
13074
+ fs2.mkdirSync(DATA_DIR, { recursive: true, mode: 448 });
13075
+ fs2.writeFileSync(IDENTITY_PATH, JSON.stringify(identity, null, 2), { mode: 384 });
12901
13076
  } catch (err) {
12902
13077
  console.debug("cohort-sync: device identity write failed", { error: String(err) });
12903
13078
  }
@@ -14009,6 +14184,28 @@ var AgentStateTracker = class {
14009
14184
 
14010
14185
  // src/types.ts
14011
14186
  var DEFAULT_API_URL = "https://api.cohort.bot";
14187
+ function normalizeTriageBrief(raw) {
14188
+ if (raw === null || typeof raw !== "object") {
14189
+ return { error: "brief must be an object with problem, solution (and optional risks)." };
14190
+ }
14191
+ const obj = raw;
14192
+ const requireField = (name) => {
14193
+ const value = obj[name];
14194
+ if (typeof value !== "string" || value.trim().length === 0) {
14195
+ return { error: `brief.${name} is required and must be a non-empty string.` };
14196
+ }
14197
+ return value.trim();
14198
+ };
14199
+ const problem = requireField("problem");
14200
+ if (typeof problem !== "string") return problem;
14201
+ const solution = requireField("solution");
14202
+ if (typeof solution !== "string") return solution;
14203
+ const brief = { problem, solution };
14204
+ if (typeof obj.risks === "string" && obj.risks.trim().length > 0) {
14205
+ brief.risks = obj.risks.trim();
14206
+ }
14207
+ return { brief };
14208
+ }
14012
14209
 
14013
14210
  // src/tool-runtime.ts
14014
14211
  var apiKey = null;
@@ -14119,7 +14316,8 @@ function dumpEvent(event) {
14119
14316
  function positiveNumber(value) {
14120
14317
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
14121
14318
  }
14122
- var PLUGIN_VERSION = true ? "0.34.3" : "unknown";
14319
+ var PLUGIN_VERSION = true ? "0.34.5" : "unknown";
14320
+ var PRESENCE_PING_INTERVAL_MS = 12e4;
14123
14321
  function resolveGatewayToken(api) {
14124
14322
  const token2 = api.config?.gateway?.auth?.token;
14125
14323
  return typeof token2 === "string" ? token2 : null;
@@ -14149,8 +14347,8 @@ function registerCronEventHandlers(client2, cfg, resolveAgentName, cronTimestamp
14149
14347
  }
14150
14348
  function parseIdentityFile(workspaceDir) {
14151
14349
  try {
14152
- const filePath = path2.join(workspaceDir, "IDENTITY.md");
14153
- const content = fs2["read"+"FileSync"](filePath, "utf-8");
14350
+ const filePath = path3.join(workspaceDir, "IDENTITY.md");
14351
+ const content = fs3["read"+"FileSync"](filePath, "utf-8");
14154
14352
  const identity = {};
14155
14353
  for (const line of content.split(/\r?\n/)) {
14156
14354
  const cleaned = line.trim().replace(/^\s*-\s*/, "");
@@ -14169,14 +14367,41 @@ function parseIdentityFile(workspaceDir) {
14169
14367
  return null;
14170
14368
  }
14171
14369
  }
14370
+ function parseRoleFromSoul(content) {
14371
+ const MAX_LEN = 80;
14372
+ for (const line of content.split(/\r?\n/)) {
14373
+ const cleaned = line.trim().replace(/^\s*-\s*/, "");
14374
+ const colonIndex = cleaned.indexOf(":");
14375
+ if (colonIndex === -1) continue;
14376
+ const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, "").trim().toLowerCase();
14377
+ if (label !== "role" && label !== "current role") continue;
14378
+ let value = cleaned.slice(colonIndex + 1).replace(/^[*_]+|[*_]+$/g, "").trim();
14379
+ if (!value) continue;
14380
+ const firstSentence = value.split(/\.\s/)[0].trim();
14381
+ if (firstSentence) value = firstSentence;
14382
+ if (value.length > MAX_LEN) value = value.slice(0, MAX_LEN).trim();
14383
+ return value;
14384
+ }
14385
+ return void 0;
14386
+ }
14387
+ function parseSoulRole(workspaceDir) {
14388
+ try {
14389
+ const content = fs3["read"+"FileSync"](path3.join(workspaceDir, "SOUL.md"), "utf-8");
14390
+ return parseRoleFromSoul(content);
14391
+ } catch {
14392
+ return void 0;
14393
+ }
14394
+ }
14172
14395
  function resolveIdentity(configIdentity, workspaceDir) {
14173
14396
  const fileIdentity = workspaceDir ? parseIdentityFile(workspaceDir) : null;
14174
- if (!configIdentity && !fileIdentity) return void 0;
14397
+ const soulRole = workspaceDir ? parseSoulRole(workspaceDir) : void 0;
14398
+ if (!configIdentity && !fileIdentity && !soulRole) return void 0;
14175
14399
  return {
14176
14400
  name: configIdentity?.name ?? fileIdentity?.name,
14177
14401
  emoji: configIdentity?.emoji ?? fileIdentity?.emoji,
14178
14402
  theme: configIdentity?.theme ?? fileIdentity?.theme,
14179
- avatar: configIdentity?.avatar ?? fileIdentity?.avatar
14403
+ avatar: configIdentity?.avatar ?? fileIdentity?.avatar,
14404
+ title: configIdentity?.title ?? soulRole
14180
14405
  };
14181
14406
  }
14182
14407
  function saveSessionsToDisk(tracker, stateFilePath) {
@@ -14193,14 +14418,14 @@ function saveSessionsToDisk(tracker, stateFilePath) {
14193
14418
  data.sessions.push({ agentName: name, key });
14194
14419
  }
14195
14420
  }
14196
- fs2.writeFileSync(stateFilePath, JSON.stringify(data), { mode: 384 });
14421
+ fs3.writeFileSync(stateFilePath, JSON.stringify(data), { mode: 384 });
14197
14422
  } catch {
14198
14423
  }
14199
14424
  }
14200
14425
  function loadSessionsFromDisk(tracker, stateFilePath, logger) {
14201
14426
  try {
14202
- if (!fs2.existsSync(stateFilePath)) return;
14203
- const data = JSON.parse(fs2["read"+"FileSync"](stateFilePath, "utf8"));
14427
+ if (!fs3.existsSync(stateFilePath)) return;
14428
+ const data = JSON.parse(fs3["read"+"FileSync"](stateFilePath, "utf8"));
14204
14429
  if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
14205
14430
  logger.info("cohort-sync: disk session state too old (>24h), skipping");
14206
14431
  return;
@@ -14269,7 +14494,7 @@ async function handleGatewayStart(event, state) {
14269
14494
  if (!state) {
14270
14495
  return;
14271
14496
  }
14272
- const { cfg, tracker, logger, config, api } = state;
14497
+ const { cfg, tracker, logger, config, api, skillsDir: skillsDir2 } = state;
14273
14498
  try {
14274
14499
  const latestVersion = await checkForUpdate(PLUGIN_VERSION, logger);
14275
14500
  if (latestVersion) {
@@ -14296,7 +14521,14 @@ async function handleGatewayStart(event, state) {
14296
14521
  model: state.resolveModel(a.id),
14297
14522
  identity: resolveIdentity(a.identity, a.workspace)
14298
14523
  }));
14299
- await fullSync(state.resolveAgentName("main"), state.resolveModel("main"), cfg, logger, agentList);
14524
+ await fullSync(
14525
+ state.resolveAgentName("main"),
14526
+ state.resolveModel("main"),
14527
+ cfg,
14528
+ logger,
14529
+ agentList,
14530
+ skillsDir2
14531
+ );
14300
14532
  } catch (err) {
14301
14533
  logger.error(`cohort-sync: gateway_start sync failed: ${String(err)}`);
14302
14534
  }
@@ -14329,7 +14561,7 @@ async function handleGatewayStart(event, state) {
14329
14561
  state.resolveAgentName,
14330
14562
  state.persistentGwClient,
14331
14563
  state.cronTimestampTracker,
14332
- { port: state.gatewayPort, hooksToken }
14564
+ { port: state.gatewayPort, hooksToken, skillsDir: skillsDir2 }
14333
14565
  );
14334
14566
  state.commandUnsubscriber = unsub;
14335
14567
  } catch (err) {
@@ -14421,6 +14653,17 @@ async function handleGatewayStart(event, state) {
14421
14653
  saveSessionsToDisk(tracker, state.stateFilePath);
14422
14654
  }, 15e4);
14423
14655
  logger.info("cohort-sync: keepalive interval started (150s)");
14656
+ void pushPresence(cfg.apiKey, "openclaw").catch(() => {
14657
+ });
14658
+ if (state.presenceInterval) clearInterval(state.presenceInterval);
14659
+ state.presenceInterval = setInterval(() => {
14660
+ void pushPresence(cfg.apiKey, "openclaw").catch(() => {
14661
+ });
14662
+ }, PRESENCE_PING_INTERVAL_MS);
14663
+ if (typeof state.presenceInterval.unref === "function") {
14664
+ state.presenceInterval.unref();
14665
+ }
14666
+ logger.info(`cohort-sync: liveness ping interval started (${PRESENCE_PING_INTERVAL_MS / 1e3}s)`);
14424
14667
  }
14425
14668
  function registerHookHandlers(api, logger, getState) {
14426
14669
  function resolveAgentFromContext(state, ctx) {
@@ -14497,7 +14740,7 @@ function registerHookHandlers(api, logger, getState) {
14497
14740
  const parsed = parseSessionKey(sessionKey);
14498
14741
  const routineId = parsed.kind === "cron" ? parsed.identifier : void 0;
14499
14742
  try {
14500
- const raw = fs2["read"+"FileSync"](state.cronStorePath, "utf8");
14743
+ const raw = fs3["read"+"FileSync"](state.cronStorePath, "utf8");
14501
14744
  const store = JSON.parse(raw);
14502
14745
  const jobs = store.jobs ?? [];
14503
14746
  const mapped = jobs.map((j) => mapCronJob(j, state.resolveAgentName, state.cronTimestampTracker));
@@ -14666,7 +14909,7 @@ function registerHookHandlers(api, logger, getState) {
14666
14909
  state.resolveAgentName,
14667
14910
  state.persistentGwClient,
14668
14911
  state.cronTimestampTracker,
14669
- { port: state.gatewayPort, hooksToken: state.gatewayToken }
14912
+ { port: state.gatewayPort, hooksToken: state.gatewayToken, skillsDir }
14670
14913
  );
14671
14914
  state.commandUnsubscriber = unsub;
14672
14915
  } catch (err) {
@@ -14881,6 +15124,10 @@ function registerHookHandlers(api, logger, getState) {
14881
15124
  clearInterval(state.keepaliveInterval);
14882
15125
  state.keepaliveInterval = null;
14883
15126
  }
15127
+ if (state.presenceInterval) {
15128
+ clearInterval(state.presenceInterval);
15129
+ state.presenceInterval = null;
15130
+ }
14884
15131
  if (state.updateCheckInterval) {
14885
15132
  clearInterval(state.updateCheckInterval);
14886
15133
  state.updateCheckInterval = null;
@@ -14931,7 +15178,7 @@ function registerHookHandlers(api, logger, getState) {
14931
15178
  }
14932
15179
  function initializeHookState(api, cfg) {
14933
15180
  const { logger, config } = api;
14934
- const stateFilePath = path2.join(cfg.stateDir, "session-state.json");
15181
+ const stateFilePath = path3.join(cfg.stateDir, "session-state.json");
14935
15182
  const nameMap = cfg.agentNameMap;
14936
15183
  const tracker = new AgentStateTracker();
14937
15184
  const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
@@ -14965,7 +15212,7 @@ function initializeHookState(api, cfg) {
14965
15212
  function getModelContextLimit2(model) {
14966
15213
  return getModelContextLimit(model);
14967
15214
  }
14968
- const cronStorePath = api.config?.cron?.store ?? path2.join(os2.homedir(), ".openclaw", "cron", "jobs.json");
15215
+ const cronStorePath = api.config?.cron?.store ?? path3.join(os2.homedir(), ".openclaw", "cron", "jobs.json");
14969
15216
  const activityBatch = new MicroBatch({
14970
15217
  maxSize: 10,
14971
15218
  maxDelayMs: 1e3,
@@ -14975,6 +15222,7 @@ function initializeHookState(api, cfg) {
14975
15222
  const gatewayPort = api.config?.gateway?.port ?? null;
14976
15223
  const gatewayToken = resolveGatewayToken(api);
14977
15224
  const hooksToken = resolveHooksToken(api);
15225
+ const skillsDir2 = api.config?.skills?.dir ?? path3.join(os2.homedir(), ".openclaw", "skills");
14978
15226
  const cronTimestampTracker = new CronTimestampTracker();
14979
15227
  const cronRunStarts = /* @__PURE__ */ new Map();
14980
15228
  let persistentGwClient = null;
@@ -14990,7 +15238,7 @@ function initializeHookState(api, cfg) {
14990
15238
  resolveAgentName,
14991
15239
  persistentGwClient,
14992
15240
  cronTimestampTracker,
14993
- { port: gatewayPort, hooksToken }
15241
+ { port: gatewayPort, hooksToken, skillsDir: skillsDir2 }
14994
15242
  );
14995
15243
  setToolRuntime({
14996
15244
  apiKey: cfg.apiKey,
@@ -15035,12 +15283,14 @@ function initializeHookState(api, cfg) {
15035
15283
  getModelContextLimit: getModelContextLimit2,
15036
15284
  activityBatch,
15037
15285
  cronStorePath,
15286
+ skillsDir: skillsDir2,
15038
15287
  stateFilePath,
15039
15288
  gatewayPort,
15040
15289
  gatewayToken,
15041
15290
  persistentGwClient,
15042
15291
  gwClientInitialized,
15043
15292
  keepaliveInterval: null,
15293
+ presenceInterval: null,
15044
15294
  commandUnsubscriber: commandUnsub,
15045
15295
  channelsUnsubscriber: null,
15046
15296
  api,
@@ -15084,6 +15334,65 @@ function registerGatewayMethods(api, getGatewayClient) {
15084
15334
  );
15085
15335
  }
15086
15336
 
15337
+ // src/triage-pr-files.ts
15338
+ import { execFile } from "node:child_process";
15339
+ import { readFile } from "node:fs/promises";
15340
+ import { join } from "node:path";
15341
+ import { promisify } from "node:util";
15342
+ var execFileAsync = promisify(execFile);
15343
+ async function collectChangedFiles(args) {
15344
+ const cwd = args.repoDir || process.cwd();
15345
+ const git = async (gitArgs) => {
15346
+ const { stdout } = await execFileAsync("git", gitArgs, {
15347
+ cwd,
15348
+ maxBuffer: 64 * 1024 * 1024
15349
+ });
15350
+ return stdout;
15351
+ };
15352
+ const files = [];
15353
+ const seen = /* @__PURE__ */ new Set();
15354
+ const nameStatus = await git(["diff", "--name-status", "-z", args.baseSha]);
15355
+ const tokens = nameStatus.split("\0");
15356
+ let i2 = 0;
15357
+ while (i2 < tokens.length) {
15358
+ const status = tokens[i2];
15359
+ if (!status) {
15360
+ i2 += 1;
15361
+ continue;
15362
+ }
15363
+ let path4;
15364
+ if (status[0] === "R" || status[0] === "C") {
15365
+ const oldPath = tokens[i2 + 1] ?? "";
15366
+ path4 = tokens[i2 + 2] ?? "";
15367
+ i2 += 3;
15368
+ if (oldPath && !seen.has(oldPath)) {
15369
+ seen.add(oldPath);
15370
+ files.push({ path: oldPath, deleted: true });
15371
+ }
15372
+ } else {
15373
+ path4 = tokens[i2 + 1] ?? "";
15374
+ i2 += 2;
15375
+ }
15376
+ if (!path4 || seen.has(path4)) continue;
15377
+ seen.add(path4);
15378
+ if (status[0] === "D") {
15379
+ files.push({ path: path4, deleted: true });
15380
+ } else {
15381
+ files.push({ path: path4, content: await readRepoFile(cwd, path4) });
15382
+ }
15383
+ }
15384
+ const others = await git(["ls-files", "--others", "--exclude-standard", "-z"]);
15385
+ for (const path4 of others.split("\0")) {
15386
+ if (!path4 || seen.has(path4)) continue;
15387
+ seen.add(path4);
15388
+ files.push({ path: path4, content: await readRepoFile(cwd, path4) });
15389
+ }
15390
+ return files;
15391
+ }
15392
+ async function readRepoFile(cwd, path4) {
15393
+ return readFile(join(cwd, path4), "utf8");
15394
+ }
15395
+
15087
15396
  // src/pocket-guide.ts
15088
15397
  var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
15089
15398
 
@@ -15313,6 +15622,31 @@ async function safeHttpError(response) {
15313
15622
  }
15314
15623
  return "";
15315
15624
  }
15625
+ var PR_MAX_FILES = 50;
15626
+ var PR_MAX_FILE_BYTES = 1e6;
15627
+ var PR_MAX_TOTAL_BYTES = 5e6;
15628
+ var PR_BASE_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
15629
+ function checkPrEnvelope(files) {
15630
+ if (files.length === 0) {
15631
+ return "No changed files to report \u2014 make a change first, then call cohort_triage_pr.";
15632
+ }
15633
+ if (files.length > PR_MAX_FILES) {
15634
+ return `Change too large: ${files.length} files (max ${PR_MAX_FILES}).`;
15635
+ }
15636
+ let total = 0;
15637
+ for (const entry of files) {
15638
+ if ("deleted" in entry) continue;
15639
+ const size = Buffer2.byteLength(entry.content, "utf8");
15640
+ if (size > PR_MAX_FILE_BYTES) {
15641
+ return `File too large: ${entry.path} (max ${PR_MAX_FILE_BYTES} bytes).`;
15642
+ }
15643
+ total += size;
15644
+ }
15645
+ if (total > PR_MAX_TOTAL_BYTES) {
15646
+ return "Change too large (total content size exceeds the limit).";
15647
+ }
15648
+ return null;
15649
+ }
15316
15650
  function renderTaskContext(context) {
15317
15651
  if (!context) return "";
15318
15652
  const lines = [];
@@ -16067,7 +16401,7 @@ ${renderGoal(goal)}`, goal);
16067
16401
  return {
16068
16402
  name: "cohort_triage",
16069
16403
  label: "cohort_triage",
16070
- description: "Report your triage decision on a Cohort triage item back to Cohort. Pass the Triage item ID from your wake task as triage_item_id, the tier (trivial | judgment | feature | not_actionable), whether it is actionable, and your reasoning. SHADOW MODE: do NOT write code or open PRs.",
16404
+ description: "Report your triage decision on a Cohort triage item back to Cohort. Pass the Triage item ID from your wake task as triage_item_id, the tier (trivial | judgment | feature | not_actionable), whether it is actionable, and your reasoning. Also pass a short `title` \u2014 a brief (\u226480 char) issue summary in your own words (a task-name headline of what you read), NOT a paste of the feedback; Cohort uses it as the Build-it task title. SHADOW MODE: do NOT write code or open PRs.",
16071
16405
  parameters: Type.Object({
16072
16406
  triage_item_id: Type.String({ description: "Triage item ID from your wake task (e.g. triageItems:abc123)." }),
16073
16407
  tier: Type.Union([
@@ -16078,7 +16412,8 @@ ${renderGoal(goal)}`, goal);
16078
16412
  ], { description: "Triage tier for this feedback." }),
16079
16413
  actionable: Type.Boolean({ description: "Whether this feedback is actionable." }),
16080
16414
  reasoning: Type.String({ description: "Why you reached this decision." }),
16081
- confidence: Type.Optional(Type.Number({ description: "Optional confidence in the decision, 0 to 1." }))
16415
+ confidence: Type.Optional(Type.Number({ description: "Optional confidence in the decision, 0 to 1." })),
16416
+ title: Type.Optional(Type.String({ description: "Optional short (\u226480 char) issue summary in your own words \u2014 a task-name headline of the problem, NOT a paste of the feedback. Used as the Build-it task title." }))
16082
16417
  }),
16083
16418
  async execute(_toolCallId, params) {
16084
16419
  const rt = getToolRuntime();
@@ -16098,7 +16433,8 @@ ${renderGoal(goal)}`, goal);
16098
16433
  tier: params.tier,
16099
16434
  actionable: params.actionable,
16100
16435
  reasoning: params.reasoning,
16101
- ...params.confidence !== void 0 ? { confidence: params.confidence } : {}
16436
+ ...params.confidence !== void 0 ? { confidence: params.confidence } : {},
16437
+ ...params.title !== void 0 ? { title: params.title } : {}
16102
16438
  }),
16103
16439
  signal: AbortSignal.timeout(1e4)
16104
16440
  }
@@ -16116,6 +16452,72 @@ ${renderGoal(goal)}`, goal);
16116
16452
  }
16117
16453
  };
16118
16454
  });
16455
+ api.registerTool(() => {
16456
+ return {
16457
+ name: "cohort_triage_pr",
16458
+ label: "cohort_triage_pr",
16459
+ description: "Report a Build-it fix you produced for a Cohort triage item. Pass the Triage item ID, the base commit SHA you branched from (base_sha), and a structured brief (problem, solution, optional risks). The tool collects your changed files from the current worktree and sends them to Cohort; the Cohort spine opens the PR and it auto-merges once CI is green. Do NOT open a PR or merge yourself \u2014 you hold no merge credential.",
16460
+ parameters: Type.Object({
16461
+ triage_item_id: Type.String({ description: "Triage item ID from your wake task (e.g. triageItems:abc123)." }),
16462
+ base_sha: Type.String({ description: "The 40- or 64-char hex commit SHA you branched from." }),
16463
+ brief: Type.Object({
16464
+ problem: Type.String({ description: "What's wrong (the issue being fixed)." }),
16465
+ solution: Type.String({ description: "How the PR fixes it (rendered under the 'Fix' label)." }),
16466
+ risks: Type.Optional(Type.String({ description: "Optional risks/blast radius of the change." }))
16467
+ }),
16468
+ repo_dir: Type.Optional(Type.String({ description: "Optional worktree path to diff (defaults to the current directory)." }))
16469
+ }),
16470
+ async execute(_toolCallId, params) {
16471
+ const rt = getToolRuntime();
16472
+ if (!rt.isReady) {
16473
+ return textResult("cohort_triage_pr is not ready yet \u2014 the plugin is still starting up.");
16474
+ }
16475
+ if (!PR_BASE_SHA_RE.test(params.base_sha)) {
16476
+ return textResult("Invalid base_sha: must be a 40- or 64-char hex commit SHA.");
16477
+ }
16478
+ const normalized = normalizeTriageBrief(params.brief);
16479
+ if ("error" in normalized) {
16480
+ return textResult(`Invalid brief: ${normalized.error}`);
16481
+ }
16482
+ let files;
16483
+ try {
16484
+ files = await collectChangedFiles({ baseSha: params.base_sha, repoDir: params.repo_dir });
16485
+ } catch (err) {
16486
+ return textResult(`Failed to collect changed files for ${params.triage_item_id}: ${err instanceof Error ? redactSecrets(err.message) : "Unknown error"}`);
16487
+ }
16488
+ const envelopeError = checkPrEnvelope(files);
16489
+ if (envelopeError) {
16490
+ return textResult(envelopeError);
16491
+ }
16492
+ try {
16493
+ const response = await fetch(
16494
+ `${rt.apiUrl}/api/v1/plugin/triage/${encodeURIComponent(params.triage_item_id)}/pr`,
16495
+ {
16496
+ method: "POST",
16497
+ headers: {
16498
+ "Authorization": `Bearer ${rt.apiKey}`,
16499
+ "Content-Type": "application/json"
16500
+ },
16501
+ body: JSON.stringify({
16502
+ brief: normalized.brief,
16503
+ baseSha: params.base_sha,
16504
+ files
16505
+ }),
16506
+ signal: AbortSignal.timeout(3e4)
16507
+ }
16508
+ );
16509
+ if (!response.ok) {
16510
+ const message = await safeHttpError(response);
16511
+ return textResult(`Failed to report PR for ${params.triage_item_id}: ${response.status}${message}`);
16512
+ }
16513
+ const result = await response.json();
16514
+ return textResult(`Reported ${files.length} changed file(s) for ${params.triage_item_id}; the spine will open the PR.`, result);
16515
+ } catch (err) {
16516
+ return textResult(`Failed to report PR for ${params.triage_item_id}: ${err instanceof Error ? redactSecrets(err.message) : "Unknown error"}`);
16517
+ }
16518
+ }
16519
+ };
16520
+ });
16119
16521
  api.registerTool(() => {
16120
16522
  return {
16121
16523
  name: "cohort_relate",
@@ -25,6 +25,7 @@
25
25
  "cohort_goal",
26
26
  "cohort_transition",
27
27
  "cohort_triage",
28
+ "cohort_triage_pr",
28
29
  "cohort_relate",
29
30
  "cohort_assign"
30
31
  ]
@@ -84,5 +85,5 @@
84
85
  }
85
86
  }
86
87
  },
87
- "version": "0.34.3"
88
+ "version": "0.34.5"
88
89
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.34.3",
3
+ "version": "0.34.5",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -23,6 +23,7 @@
23
23
  "cohort_goal",
24
24
  "cohort_transition",
25
25
  "cohort_triage",
26
+ "cohort_triage_pr",
26
27
  "cohort_relate",
27
28
  "cohort_assign"
28
29
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.34.3",
3
+ "version": "0.34.5",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",