@cfio/cohort-sync 0.1.0 → 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Creative Foresight, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @cfio/cohort-sync
2
+
3
+ OpenClaw plugin that syncs your gateway to the [Cohort](https://my.cohort.bot) dashboard. Keeps agent status and skills visible in real time — no polling, no manual config.
4
+
5
+ ## What it does
6
+
7
+ - Reports agent status (`working` / `idle`) on gateway start and session end
8
+ - Syncs installed skills to the dashboard on startup
9
+ - Enables @mention delivery from Cohort to your agent
10
+
11
+ No VPN, Tailscale, or port forwarding needed. The plugin connects outbound to Cohort and works behind any firewall.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g @cfio/cohort-sync
17
+ ```
18
+
19
+ The postinstall script copies the plugin to `~/.openclaw/extensions/cohort-sync/` automatically.
20
+
21
+ ## Configure
22
+
23
+ Map your agent's internal name to its Cohort name:
24
+
25
+ ```bash
26
+ openclaw config set "plugins.entries.cohort-sync.config.agentNameMap.main" "YOUR_AGENT_NAME"
27
+ ```
28
+
29
+ Authenticate with Cohort:
30
+
31
+ ```bash
32
+ openclaw cohort auth
33
+ ```
34
+
35
+ This opens a browser-based device auth flow. The API key is stored in your system keychain — no manual key management needed.
36
+
37
+ ## Verify
38
+
39
+ ```bash
40
+ openclaw doctor
41
+ ```
42
+
43
+ Look for `✓ cohort-sync loaded`. Then restart your gateway:
44
+
45
+ ```bash
46
+ openclaw gateway restart
47
+ ```
48
+
49
+ Your agent should appear in the Cohort dashboard within seconds.
50
+
51
+ ## Update
52
+
53
+ ```bash
54
+ npm install -g @cfio/cohort-sync
55
+ ```
56
+
57
+ Running the install command again updates to the latest version and overwrites the extension files.
58
+
59
+ ## Requirements
60
+
61
+ - [OpenClaw](https://openclaw.dev) gateway installed and running
62
+ - A Cohort account ([my.cohort.bot](https://my.cohort.bot))
63
+
64
+ ## Documentation
65
+
66
+ Full setup guide, configuration reference, and troubleshooting:
67
+
68
+ **[docs.cohort.bot/gateway](https://docs.cohort.bot/gateway)**
69
+
70
+ ## License
71
+
72
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.js CHANGED
@@ -2699,209 +2699,6 @@ var Type = type_exports2;
2699
2699
 
2700
2700
  // src/sync.ts
2701
2701
  import { execSync } from "node:child_process";
2702
- function extractJson(raw) {
2703
- const jsonStart = raw.search(/[\[{]/);
2704
- const jsonEndBracket = raw.lastIndexOf("]");
2705
- const jsonEndBrace = raw.lastIndexOf("}");
2706
- const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
2707
- if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
2708
- throw new Error("No JSON found in output");
2709
- }
2710
- return raw.slice(jsonStart, jsonEnd + 1);
2711
- }
2712
- function fetchSkills(logger) {
2713
- try {
2714
- const raw = execSync("openclaw skills list --json", {
2715
- encoding: "utf8",
2716
- timeout: 1e4,
2717
- stdio: ["ignore", "pipe", "ignore"],
2718
- env: { ...process.env, NO_COLOR: "1" }
2719
- });
2720
- const parsed = JSON.parse(extractJson(raw));
2721
- const list = Array.isArray(parsed) ? parsed : parsed?.skills ?? [];
2722
- return list.map((s) => ({
2723
- name: String(s.name ?? s.id ?? "unknown"),
2724
- description: String(s.description ?? ""),
2725
- source: String(s.source ?? s.origin ?? "unknown"),
2726
- ...s.emoji ? { emoji: String(s.emoji) } : {}
2727
- }));
2728
- } catch (err) {
2729
- logger.warn(`cohort-sync: failed to fetch skills: ${String(err)}`);
2730
- return [];
2731
- }
2732
- }
2733
- var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
2734
- function normalizeStatus(status) {
2735
- return VALID_STATUSES.has(status) ? status : "idle";
2736
- }
2737
- async function v1Get(apiUrl, apiKey, path2) {
2738
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
2739
- headers: { Authorization: `Bearer ${apiKey}` },
2740
- signal: AbortSignal.timeout(1e4)
2741
- });
2742
- if (!res.ok) throw new Error(`GET ${path2} \u2192 ${res.status}`);
2743
- return res.json();
2744
- }
2745
- async function v1Patch(apiUrl, apiKey, path2, body) {
2746
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
2747
- method: "PATCH",
2748
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
2749
- body: JSON.stringify(body),
2750
- signal: AbortSignal.timeout(1e4)
2751
- });
2752
- if (!res.ok) throw new Error(`PATCH ${path2} \u2192 ${res.status}`);
2753
- }
2754
- async function v1Post(apiUrl, apiKey, path2, body) {
2755
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
2756
- method: "POST",
2757
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
2758
- body: JSON.stringify(body),
2759
- signal: AbortSignal.timeout(1e4)
2760
- });
2761
- if (!res.ok) throw new Error(`POST ${path2} \u2192 ${res.status}`);
2762
- }
2763
- async function syncAgentStatus(agentName, status, model, cfg, logger) {
2764
- try {
2765
- const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
2766
- const agents = data?.data ?? [];
2767
- const agent = agents.find(
2768
- (a) => a.name.toLowerCase() === agentName.toLowerCase()
2769
- );
2770
- if (!agent) {
2771
- const available = agents.map((a) => a.name).join(", ") || "(none)";
2772
- logger.warn(
2773
- `cohort-sync: agent "${agentName}" not found in workspace \u2014 skipping status sync. Available agents: [${available}]. Configure agentNameMap in your plugin config to map OpenClaw agent IDs to Cohort names.`
2774
- );
2775
- return;
2776
- }
2777
- await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
2778
- status: normalizeStatus(status),
2779
- model
2780
- });
2781
- logger.info(`cohort-sync: agent "${agentName}" \u2192 ${normalizeStatus(status)}`);
2782
- } catch (err) {
2783
- logger.warn(`cohort-sync: syncAgentStatus failed: ${String(err)}`);
2784
- }
2785
- }
2786
- async function syncSkillsToV1(skills, cfg, logger) {
2787
- for (const skill of skills) {
2788
- try {
2789
- await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills", {
2790
- name: skill.name,
2791
- description: skill.description
2792
- });
2793
- } catch (err) {
2794
- logger.warn(`cohort-sync: failed to sync skill "${skill.name}": ${String(err)}`);
2795
- }
2796
- }
2797
- }
2798
- var lastKnownRoster = [];
2799
- function getLastKnownRoster() {
2800
- return lastKnownRoster;
2801
- }
2802
- async function reconcileRoster(openClawAgents, cfg, logger) {
2803
- const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
2804
- const cohortAgents = data?.data ?? [];
2805
- const cohortByName = new Map(cohortAgents.map((a) => [a.name.toLowerCase(), a]));
2806
- const openClawNames = new Set(
2807
- openClawAgents.map((a) => {
2808
- const nameMap = cfg.agentNameMap;
2809
- return (nameMap?.[a.id] ?? a.id).toLowerCase();
2810
- })
2811
- );
2812
- for (const oc of openClawAgents) {
2813
- const agentName = (cfg.agentNameMap?.[oc.id] ?? oc.id).toLowerCase();
2814
- const existing = cohortByName.get(agentName);
2815
- if (!existing) {
2816
- try {
2817
- await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/agents", {
2818
- name: agentName,
2819
- displayName: oc.identity?.name ?? agentName,
2820
- emoji: oc.identity?.emoji ?? "\u{1F916}",
2821
- model: oc.model,
2822
- status: "idle"
2823
- });
2824
- logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
2825
- } catch (err) {
2826
- logger.warn(`cohort-sync: failed to provision agent "${agentName}": ${String(err)}`);
2827
- }
2828
- } else {
2829
- const updates = {
2830
- model: oc.model,
2831
- status: "idle"
2832
- };
2833
- if (oc.identity?.name) {
2834
- updates.displayName = oc.identity.name;
2835
- }
2836
- if (oc.identity?.emoji) {
2837
- updates.emoji = oc.identity.emoji;
2838
- }
2839
- try {
2840
- await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${existing.id}`, updates);
2841
- } catch (err) {
2842
- logger.warn(`cohort-sync: failed to update agent "${agentName}": ${String(err)}`);
2843
- }
2844
- }
2845
- }
2846
- for (const cohort of cohortAgents) {
2847
- if (!openClawNames.has(cohort.name.toLowerCase())) {
2848
- if (cohort.status === "archived" || cohort.status === "deleted" || cohort.status === "unreachable") {
2849
- continue;
2850
- }
2851
- try {
2852
- await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${cohort.id}`, {
2853
- status: "unreachable"
2854
- });
2855
- logger.info(`cohort-sync: marked agent "${cohort.name}" as unreachable`);
2856
- } catch (err) {
2857
- logger.warn(
2858
- `cohort-sync: failed to mark agent "${cohort.name}" as unreachable: ${String(err)}`
2859
- );
2860
- }
2861
- }
2862
- }
2863
- const updatedData = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
2864
- const finalRoster = updatedData?.data ?? cohortAgents;
2865
- lastKnownRoster = finalRoster;
2866
- return finalRoster;
2867
- }
2868
- async function markAllUnreachable(cfg, logger) {
2869
- const roster = getLastKnownRoster();
2870
- if (roster.length === 0) {
2871
- logger.warn("cohort-sync: no cached roster \u2014 skipping markAllUnreachable");
2872
- return;
2873
- }
2874
- for (const agent of roster) {
2875
- if (agent.status === "unreachable" || agent.status === "archived" || agent.status === "deleted") {
2876
- continue;
2877
- }
2878
- try {
2879
- await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
2880
- status: "unreachable"
2881
- });
2882
- } catch (err) {
2883
- logger.warn(`cohort-sync: failed to mark "${agent.name}" as unreachable: ${String(err)}`);
2884
- }
2885
- }
2886
- logger.info("cohort-sync: all agents marked unreachable");
2887
- }
2888
- async function fullSync(agentName, model, cfg, logger, openClawAgents) {
2889
- logger.info("cohort-sync: full sync starting");
2890
- if (openClawAgents && openClawAgents.length > 0) {
2891
- try {
2892
- await reconcileRoster(openClawAgents, cfg, logger);
2893
- } catch (err) {
2894
- logger.warn(`cohort-sync: roster reconciliation failed: ${String(err)}`);
2895
- }
2896
- } else {
2897
- await syncAgentStatus(agentName, "working", model, cfg, logger);
2898
- }
2899
- const skills = fetchSkills(logger);
2900
- if (skills.length > 0) {
2901
- await syncSkillsToV1(skills, cfg, logger);
2902
- }
2903
- logger.info("cohort-sync: full sync complete");
2904
- }
2905
2702
 
2906
2703
  // ../../node_modules/.pnpm/convex@1.32.0_react@19.2.1/node_modules/convex/dist/esm/index.js
2907
2704
  var version = "1.32.0";
@@ -11778,6 +11575,27 @@ function setHotState(state) {
11778
11575
  function clearHotState() {
11779
11576
  delete globalThis[HOT_KEY];
11780
11577
  }
11578
+ function setRosterHotState(roster) {
11579
+ const hot = getHotState();
11580
+ if (hot) {
11581
+ hot.lastKnownRoster = roster;
11582
+ }
11583
+ }
11584
+ function getRosterHotState() {
11585
+ const hot = getHotState();
11586
+ return hot?.lastKnownRoster ?? null;
11587
+ }
11588
+ var savedLogger = null;
11589
+ function setLogger(logger) {
11590
+ savedLogger = logger;
11591
+ }
11592
+ function getLogger() {
11593
+ return savedLogger ?? {
11594
+ info: console.log,
11595
+ warn: console.warn,
11596
+ error: console.error
11597
+ };
11598
+ }
11781
11599
  var client = null;
11782
11600
  var savedConvexUrl = null;
11783
11601
  var unsubscribers = [];
@@ -11802,11 +11620,13 @@ function getOrCreateClient() {
11802
11620
  const hot = getHotState();
11803
11621
  if (hot?.client) {
11804
11622
  client = hot.client;
11623
+ getLogger().info("cohort-sync: recovered ConvexClient from globalThis");
11805
11624
  return client;
11806
11625
  }
11807
11626
  if (!savedConvexUrl) return null;
11808
11627
  client = new ConvexClient(savedConvexUrl);
11809
- setHotState({ client, convexUrl: savedConvexUrl, unsubscribers: [] });
11628
+ getLogger().info(`cohort-sync: created fresh ConvexClient (${savedConvexUrl})`);
11629
+ setHotState({ client, convexUrl: savedConvexUrl, unsubscribers: [], lastKnownRoster: [] });
11810
11630
  return client;
11811
11631
  }
11812
11632
  async function initSubscription(port, cfg, hooksToken, logger) {
@@ -11815,14 +11635,16 @@ async function initSubscription(port, cfg, hooksToken, logger) {
11815
11635
  const hot = getHotState();
11816
11636
  if (hot?.client) {
11817
11637
  client = hot.client;
11638
+ logger.info("cohort-sync: reusing hot-reload ConvexClient for subscription");
11818
11639
  } else {
11819
11640
  client = new ConvexClient(convexUrl);
11641
+ logger.info(`cohort-sync: created new ConvexClient for subscription (${convexUrl})`);
11820
11642
  }
11821
11643
  if (!hooksToken) {
11822
11644
  logger.warn(
11823
11645
  `cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
11824
11646
  );
11825
- setHotState({ client, convexUrl, unsubscribers: [] });
11647
+ setHotState({ client, convexUrl, unsubscribers: [], lastKnownRoster: [] });
11826
11648
  return;
11827
11649
  }
11828
11650
  const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
@@ -11865,7 +11687,7 @@ async function initSubscription(port, cfg, hooksToken, logger) {
11865
11687
  );
11866
11688
  unsubscribers.push(unsubscribe);
11867
11689
  }
11868
- setHotState({ client, convexUrl, unsubscribers: [...unsubscribers] });
11690
+ setHotState({ client, convexUrl, unsubscribers: [...unsubscribers], lastKnownRoster: [] });
11869
11691
  }
11870
11692
  function closeSubscription() {
11871
11693
  for (const unsub of unsubscribers) {
@@ -11894,7 +11716,7 @@ async function pushTelemetry(apiKey, data) {
11894
11716
  try {
11895
11717
  await c.mutation(upsertTelemetryFromPlugin, { apiKey, ...data });
11896
11718
  } catch (err) {
11897
- console.error(`cohort-sync: pushTelemetry failed: ${err}`);
11719
+ getLogger().error(`cohort-sync: pushTelemetry failed: ${err}`);
11898
11720
  }
11899
11721
  }
11900
11722
  async function pushSessions(apiKey, agentName, sessions) {
@@ -11903,9 +11725,221 @@ async function pushSessions(apiKey, agentName, sessions) {
11903
11725
  try {
11904
11726
  await c.mutation(upsertSessionsFromPlugin, { apiKey, agentName, sessions });
11905
11727
  } catch (err) {
11906
- console.error(`cohort-sync: pushSessions failed: ${err}`);
11728
+ getLogger().error(`cohort-sync: pushSessions failed: ${err}`);
11729
+ }
11730
+ }
11731
+
11732
+ // src/sync.ts
11733
+ function extractJson(raw) {
11734
+ const jsonStart = raw.search(/[\[{]/);
11735
+ const jsonEndBracket = raw.lastIndexOf("]");
11736
+ const jsonEndBrace = raw.lastIndexOf("}");
11737
+ const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
11738
+ if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
11739
+ throw new Error("No JSON found in output");
11740
+ }
11741
+ return raw.slice(jsonStart, jsonEnd + 1);
11742
+ }
11743
+ function fetchSkills(logger) {
11744
+ try {
11745
+ const raw = execSync("openclaw skills list --json", {
11746
+ encoding: "utf8",
11747
+ timeout: 1e4,
11748
+ stdio: ["ignore", "pipe", "ignore"],
11749
+ env: { ...process.env, NO_COLOR: "1" }
11750
+ });
11751
+ const parsed = JSON.parse(extractJson(raw));
11752
+ const list = Array.isArray(parsed) ? parsed : parsed?.skills ?? [];
11753
+ return list.map((s) => ({
11754
+ name: String(s.name ?? s.id ?? "unknown"),
11755
+ description: String(s.description ?? ""),
11756
+ source: String(s.source ?? s.origin ?? "unknown"),
11757
+ ...s.emoji ? { emoji: String(s.emoji) } : {}
11758
+ }));
11759
+ } catch (err) {
11760
+ logger.warn(`cohort-sync: failed to fetch skills: ${String(err)}`);
11761
+ return [];
11762
+ }
11763
+ }
11764
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
11765
+ function normalizeStatus(status) {
11766
+ return VALID_STATUSES.has(status) ? status : "idle";
11767
+ }
11768
+ async function v1Get(apiUrl, apiKey, path2) {
11769
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
11770
+ headers: { Authorization: `Bearer ${apiKey}` },
11771
+ signal: AbortSignal.timeout(1e4)
11772
+ });
11773
+ if (!res.ok) throw new Error(`GET ${path2} \u2192 ${res.status}`);
11774
+ return res.json();
11775
+ }
11776
+ async function v1Patch(apiUrl, apiKey, path2, body) {
11777
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
11778
+ method: "PATCH",
11779
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
11780
+ body: JSON.stringify(body),
11781
+ signal: AbortSignal.timeout(1e4)
11782
+ });
11783
+ if (!res.ok) throw new Error(`PATCH ${path2} \u2192 ${res.status}`);
11784
+ }
11785
+ async function v1Post(apiUrl, apiKey, path2, body) {
11786
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path2}`, {
11787
+ method: "POST",
11788
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
11789
+ body: JSON.stringify(body),
11790
+ signal: AbortSignal.timeout(1e4)
11791
+ });
11792
+ if (!res.ok) throw new Error(`POST ${path2} \u2192 ${res.status}`);
11793
+ }
11794
+ async function syncAgentStatus(agentName, status, model, cfg, logger) {
11795
+ try {
11796
+ const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
11797
+ const agents = data?.data ?? [];
11798
+ const agent = agents.find(
11799
+ (a) => a.name.toLowerCase() === agentName.toLowerCase()
11800
+ );
11801
+ if (!agent) {
11802
+ const available = agents.map((a) => a.name).join(", ") || "(none)";
11803
+ logger.warn(
11804
+ `cohort-sync: agent "${agentName}" not found in workspace \u2014 skipping status sync. Available agents: [${available}]. Configure agentNameMap in your plugin config to map OpenClaw agent IDs to Cohort names.`
11805
+ );
11806
+ return;
11807
+ }
11808
+ await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
11809
+ status: normalizeStatus(status),
11810
+ model
11811
+ });
11812
+ logger.info(`cohort-sync: agent "${agentName}" \u2192 ${normalizeStatus(status)}`);
11813
+ } catch (err) {
11814
+ logger.warn(`cohort-sync: syncAgentStatus failed: ${String(err)}`);
11815
+ }
11816
+ }
11817
+ async function syncSkillsToV1(skills, cfg, logger) {
11818
+ for (const skill of skills) {
11819
+ try {
11820
+ await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills", {
11821
+ name: skill.name,
11822
+ description: skill.description
11823
+ });
11824
+ } catch (err) {
11825
+ logger.warn(`cohort-sync: failed to sync skill "${skill.name}": ${String(err)}`);
11826
+ }
11907
11827
  }
11908
11828
  }
11829
+ var lastKnownRoster = [];
11830
+ function getLastKnownRoster() {
11831
+ return lastKnownRoster;
11832
+ }
11833
+ function restoreRosterFromHotReload(hotRoster, logger) {
11834
+ if (hotRoster && hotRoster.length > 0 && lastKnownRoster.length === 0) {
11835
+ lastKnownRoster = hotRoster;
11836
+ logger.info(`cohort-sync: recovered roster (${hotRoster.length} agents) after hot-reload`);
11837
+ }
11838
+ }
11839
+ async function reconcileRoster(openClawAgents, cfg, logger) {
11840
+ const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
11841
+ const cohortAgents = data?.data ?? [];
11842
+ const cohortByName = new Map(cohortAgents.map((a) => [a.name.toLowerCase(), a]));
11843
+ const openClawNames = new Set(
11844
+ openClawAgents.map((a) => {
11845
+ const nameMap = cfg.agentNameMap;
11846
+ return (nameMap?.[a.id] ?? a.identity?.name ?? a.id).toLowerCase();
11847
+ })
11848
+ );
11849
+ for (const oc of openClawAgents) {
11850
+ const agentName = (cfg.agentNameMap?.[oc.id] ?? oc.identity?.name ?? oc.id).toLowerCase();
11851
+ const existing = cohortByName.get(agentName);
11852
+ if (!existing) {
11853
+ try {
11854
+ await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/agents", {
11855
+ name: agentName,
11856
+ displayName: oc.identity?.name ?? agentName,
11857
+ emoji: oc.identity?.emoji ?? "\u{1F916}",
11858
+ model: oc.model,
11859
+ status: "idle"
11860
+ });
11861
+ logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
11862
+ } catch (err) {
11863
+ logger.warn(`cohort-sync: failed to provision agent "${agentName}": ${String(err)}`);
11864
+ }
11865
+ } else {
11866
+ const updates = {
11867
+ model: oc.model,
11868
+ status: "idle"
11869
+ };
11870
+ if (oc.identity?.name) {
11871
+ updates.displayName = oc.identity.name;
11872
+ }
11873
+ if (oc.identity?.emoji) {
11874
+ updates.emoji = oc.identity.emoji;
11875
+ }
11876
+ try {
11877
+ await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${existing.id}`, updates);
11878
+ } catch (err) {
11879
+ logger.warn(`cohort-sync: failed to update agent "${agentName}": ${String(err)}`);
11880
+ }
11881
+ }
11882
+ }
11883
+ for (const cohort of cohortAgents) {
11884
+ if (!openClawNames.has(cohort.name.toLowerCase())) {
11885
+ if (cohort.status === "archived" || cohort.status === "deleted" || cohort.status === "unreachable") {
11886
+ continue;
11887
+ }
11888
+ try {
11889
+ await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${cohort.id}`, {
11890
+ status: "unreachable"
11891
+ });
11892
+ logger.info(`cohort-sync: marked agent "${cohort.name}" as unreachable`);
11893
+ } catch (err) {
11894
+ logger.warn(
11895
+ `cohort-sync: failed to mark agent "${cohort.name}" as unreachable: ${String(err)}`
11896
+ );
11897
+ }
11898
+ }
11899
+ }
11900
+ const updatedData = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
11901
+ const finalRoster = updatedData?.data ?? cohortAgents;
11902
+ lastKnownRoster = finalRoster;
11903
+ setRosterHotState(finalRoster);
11904
+ return finalRoster;
11905
+ }
11906
+ async function markAllUnreachable(cfg, logger) {
11907
+ const roster = getLastKnownRoster();
11908
+ if (roster.length === 0) {
11909
+ logger.warn("cohort-sync: no cached roster \u2014 skipping markAllUnreachable");
11910
+ return;
11911
+ }
11912
+ for (const agent of roster) {
11913
+ if (agent.status === "unreachable" || agent.status === "archived" || agent.status === "deleted") {
11914
+ continue;
11915
+ }
11916
+ try {
11917
+ await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
11918
+ status: "unreachable"
11919
+ });
11920
+ } catch (err) {
11921
+ logger.warn(`cohort-sync: failed to mark "${agent.name}" as unreachable: ${String(err)}`);
11922
+ }
11923
+ }
11924
+ logger.info("cohort-sync: all agents marked unreachable");
11925
+ }
11926
+ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
11927
+ logger.info("cohort-sync: full sync starting");
11928
+ if (openClawAgents && openClawAgents.length > 0) {
11929
+ try {
11930
+ await reconcileRoster(openClawAgents, cfg, logger);
11931
+ } catch (err) {
11932
+ logger.warn(`cohort-sync: roster reconciliation failed: ${String(err)}`);
11933
+ }
11934
+ } else {
11935
+ await syncAgentStatus(agentName, "working", model, cfg, logger);
11936
+ }
11937
+ const skills = fetchSkills(logger);
11938
+ if (skills.length > 0) {
11939
+ await syncSkillsToV1(skills, cfg, logger);
11940
+ }
11941
+ logger.info("cohort-sync: full sync complete");
11942
+ }
11909
11943
 
11910
11944
  // src/telemetry.ts
11911
11945
  var TelemetryTracker = class {
@@ -12018,7 +12052,9 @@ function registerHooks(api, cfg) {
12018
12052
  const sessions = new SessionTracker();
12019
12053
  let heartbeatInterval = null;
12020
12054
  setConvexUrl(cfg);
12055
+ setLogger(logger);
12021
12056
  restoreFromHotReload(logger);
12057
+ restoreRosterFromHotReload(getRosterHotState(), logger);
12022
12058
  function resolveAgentName(agentId) {
12023
12059
  return (nameMap?.[agentId] ?? agentId).toLowerCase();
12024
12060
  }
@@ -12081,6 +12117,7 @@ function registerHooks(api, cfg) {
12081
12117
  logger.warn(`cohort-sync: heartbeat push failed for ${agentName}: ${String(err)}`);
12082
12118
  }
12083
12119
  }
12120
+ logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
12084
12121
  }
12085
12122
  api.on("gateway_start", async (event) => {
12086
12123
  try {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/package.json CHANGED
@@ -1,7 +1,21 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
+ "license": "MIT",
6
+ "homepage": "https://docs.cohort.bot/gateway",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/davebirnbaum/creativeforesight-io.git",
10
+ "directory": "packages/cohort-sync"
11
+ },
12
+ "keywords": [
13
+ "openclaw",
14
+ "cohort",
15
+ "ai-agent",
16
+ "plugin",
17
+ "sync"
18
+ ],
5
19
  "type": "module",
6
20
  "main": "dist/index.js",
7
21
  "types": "index.ts",