@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 +21 -0
- package/README.md +72 -0
- package/dist/index.js +245 -208
- package/dist/package.json +1 -1
- package/package.json +15 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfio/cohort-sync",
|
|
3
|
-
"version": "0.1.
|
|
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",
|