coherence-cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cc.mjs +10 -0
- package/lib/commands/nodes.mjs +141 -0
- package/lib/commands/status.mjs +119 -13
- package/package.json +1 -1
package/bin/cc.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { listSpecs, showSpec } from "../lib/commands/specs.mjs";
|
|
|
12
12
|
import { contribute } from "../lib/commands/contribute.mjs";
|
|
13
13
|
import { showStatus, showResonance } from "../lib/commands/status.mjs";
|
|
14
14
|
import { showIdentity, linkIdentity, unlinkIdentity, lookupIdentity, setupIdentity, setIdentity } from "../lib/commands/identity.mjs";
|
|
15
|
+
import { listNodes, sendMessage, readMessages } from "../lib/commands/nodes.mjs";
|
|
15
16
|
|
|
16
17
|
const [command, ...args] = process.argv.slice(2);
|
|
17
18
|
|
|
@@ -27,6 +28,10 @@ const COMMANDS = {
|
|
|
27
28
|
status: () => showStatus(),
|
|
28
29
|
resonance: () => showResonance(),
|
|
29
30
|
identity: () => handleIdentity(args),
|
|
31
|
+
nodes: () => listNodes(),
|
|
32
|
+
msg: () => sendMessage(args),
|
|
33
|
+
messages: () => readMessages(args),
|
|
34
|
+
inbox: () => readMessages(args),
|
|
30
35
|
help: () => showHelp(),
|
|
31
36
|
};
|
|
32
37
|
|
|
@@ -78,6 +83,11 @@ function showHelp() {
|
|
|
78
83
|
identity unlink <p> Unlink a provider
|
|
79
84
|
identity lookup <p> <id> Find contributor by identity
|
|
80
85
|
|
|
86
|
+
\x1b[1mFederation:\x1b[0m
|
|
87
|
+
nodes List federation nodes
|
|
88
|
+
msg <node|broadcast> <text> Send message to a node
|
|
89
|
+
inbox Read your messages
|
|
90
|
+
|
|
81
91
|
\x1b[1mProviders:\x1b[0m
|
|
82
92
|
github, x, discord, telegram, mastodon, bluesky, linkedin, reddit,
|
|
83
93
|
youtube, twitch, instagram, tiktok, gitlab, bitbucket, npm, crates,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation node commands: nodes, msg, broadcast
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { get, post } from "../api.mjs";
|
|
6
|
+
import { hostname } from "node:os";
|
|
7
|
+
|
|
8
|
+
export async function listNodes() {
|
|
9
|
+
const nodes = await get("/api/federation/nodes");
|
|
10
|
+
if (!nodes || !Array.isArray(nodes)) {
|
|
11
|
+
console.log("Could not fetch federation nodes.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log();
|
|
16
|
+
console.log("\x1b[1m FEDERATION NODES\x1b[0m");
|
|
17
|
+
console.log(` ${"─".repeat(50)}`);
|
|
18
|
+
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
const lastSeen = node.last_seen_at ? new Date(node.last_seen_at) : null;
|
|
22
|
+
const ageMs = lastSeen ? now - lastSeen.getTime() : Infinity;
|
|
23
|
+
const ageMin = Math.floor(ageMs / 60000);
|
|
24
|
+
|
|
25
|
+
// Status dot
|
|
26
|
+
let dot = "\x1b[31m●\x1b[0m"; // red
|
|
27
|
+
if (ageMin < 5) dot = "\x1b[32m●\x1b[0m"; // green
|
|
28
|
+
else if (ageMin < 60) dot = "\x1b[33m●\x1b[0m"; // yellow
|
|
29
|
+
|
|
30
|
+
// OS icon
|
|
31
|
+
const os = node.os_type || "?";
|
|
32
|
+
const icon = os === "macos" ? "🍎" : os === "windows" ? "🪟" : os === "linux" ? "🐧" : "🖥️";
|
|
33
|
+
|
|
34
|
+
// Providers
|
|
35
|
+
let providers = [];
|
|
36
|
+
try {
|
|
37
|
+
providers = typeof node.providers_json === "string"
|
|
38
|
+
? JSON.parse(node.providers_json)
|
|
39
|
+
: (node.providers || []);
|
|
40
|
+
} catch { providers = []; }
|
|
41
|
+
|
|
42
|
+
const ago = ageMin < 1 ? "now" : ageMin < 60 ? `${ageMin}m ago` : `${Math.floor(ageMin / 60)}h ago`;
|
|
43
|
+
|
|
44
|
+
console.log(` ${dot} ${icon} \x1b[1m${node.hostname || "?"}\x1b[0m ${ago}`);
|
|
45
|
+
console.log(` ${providers.join(", ")}`);
|
|
46
|
+
console.log(` id: ${node.node_id}`);
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function sendMessage(args) {
|
|
52
|
+
const [targetOrBroadcast, ...textParts] = args;
|
|
53
|
+
const text = textParts.join(" ");
|
|
54
|
+
|
|
55
|
+
if (!targetOrBroadcast || !text) {
|
|
56
|
+
console.log("Usage: cc msg <node_id|broadcast> <message text>");
|
|
57
|
+
console.log(" cc msg broadcast Hello all nodes!");
|
|
58
|
+
console.log(" cc msg e66ff3d35dd4ccb1 Hello Mac node!");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Determine our node ID from hostname
|
|
63
|
+
const myHost = hostname();
|
|
64
|
+
const nodes = await get("/api/federation/nodes");
|
|
65
|
+
let myNodeId = null;
|
|
66
|
+
if (Array.isArray(nodes)) {
|
|
67
|
+
const mine = nodes.find(n => n.hostname === myHost);
|
|
68
|
+
if (mine) myNodeId = mine.node_id;
|
|
69
|
+
}
|
|
70
|
+
if (!myNodeId) {
|
|
71
|
+
// Fallback: use first 16 chars of hostname hash
|
|
72
|
+
myNodeId = myHost;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (targetOrBroadcast === "broadcast" || targetOrBroadcast === "all") {
|
|
76
|
+
const result = await post("/api/federation/broadcast", {
|
|
77
|
+
from_node: myNodeId,
|
|
78
|
+
to_node: null,
|
|
79
|
+
type: "text",
|
|
80
|
+
text,
|
|
81
|
+
payload: {},
|
|
82
|
+
});
|
|
83
|
+
if (result) {
|
|
84
|
+
console.log(`\x1b[32m✓\x1b[0m Broadcast sent: ${text.slice(0, 60)}`);
|
|
85
|
+
} else {
|
|
86
|
+
console.log("\x1b[31m✗\x1b[0m Failed to broadcast");
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const result = await post(`/api/federation/nodes/${myNodeId}/messages`, {
|
|
90
|
+
from_node: myNodeId,
|
|
91
|
+
to_node: targetOrBroadcast,
|
|
92
|
+
type: "text",
|
|
93
|
+
text,
|
|
94
|
+
payload: {},
|
|
95
|
+
});
|
|
96
|
+
if (result) {
|
|
97
|
+
console.log(`\x1b[32m✓\x1b[0m Message sent to ${targetOrBroadcast.slice(0, 12)}: ${text.slice(0, 60)}`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log("\x1b[31m✗\x1b[0m Failed to send message");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function readMessages(args) {
|
|
105
|
+
const myHost = hostname();
|
|
106
|
+
const nodes = await get("/api/federation/nodes");
|
|
107
|
+
let myNodeId = null;
|
|
108
|
+
if (Array.isArray(nodes)) {
|
|
109
|
+
const mine = nodes.find(n => n.hostname === myHost);
|
|
110
|
+
if (mine) myNodeId = mine.node_id;
|
|
111
|
+
}
|
|
112
|
+
if (!myNodeId) {
|
|
113
|
+
console.log("Could not determine your node ID. Register first.");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = await get(`/api/federation/nodes/${myNodeId}/messages?unread_only=false&limit=20`);
|
|
118
|
+
if (!data || !data.messages) {
|
|
119
|
+
console.log("No messages.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log();
|
|
124
|
+
console.log("\x1b[1m MESSAGES\x1b[0m");
|
|
125
|
+
console.log(` ${"─".repeat(50)}`);
|
|
126
|
+
|
|
127
|
+
if (data.messages.length === 0) {
|
|
128
|
+
console.log(" No messages yet.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const msg of data.messages) {
|
|
132
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toLocaleString() : "?";
|
|
133
|
+
const from = msg.from_node ? msg.from_node.slice(0, 12) : "?";
|
|
134
|
+
const type = msg.type || "text";
|
|
135
|
+
const unread = !msg.read_by?.includes(myNodeId) ? " \x1b[33m(new)\x1b[0m" : "";
|
|
136
|
+
|
|
137
|
+
console.log(` \x1b[2m${ts}\x1b[0m [${type}] from ${from}${unread}`);
|
|
138
|
+
console.log(` ${msg.text || "(no text)"}`);
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
}
|
package/lib/commands/status.mjs
CHANGED
|
@@ -7,31 +7,137 @@ import { getContributorId, getHubUrl } from "../config.mjs";
|
|
|
7
7
|
import { hostname } from "node:os";
|
|
8
8
|
|
|
9
9
|
export async function showStatus() {
|
|
10
|
-
|
|
11
|
-
const ideas
|
|
12
|
-
|
|
10
|
+
// Fetch everything in parallel
|
|
11
|
+
const [health, ideas, nodes, pendingData, runningData, completedData, coherence, ledger, messages] =
|
|
12
|
+
await Promise.all([
|
|
13
|
+
get("/api/health"),
|
|
14
|
+
get("/api/ideas/count"),
|
|
15
|
+
get("/api/federation/nodes"),
|
|
16
|
+
get("/api/agent/tasks", { status: "pending", limit: 100 }),
|
|
17
|
+
get("/api/agent/tasks", { status: "running", limit: 100 }),
|
|
18
|
+
get("/api/agent/tasks", { status: "completed", limit: 5 }),
|
|
19
|
+
get("/api/coherence/score"),
|
|
20
|
+
getContributorId() ? get(`/api/contributions/ledger/${encodeURIComponent(getContributorId())}`) : null,
|
|
21
|
+
getContributorId() ? get(`/api/federation/nodes/${encodeURIComponent(getContributorId())}/messages`, { unread_only: true, limit: 10 }) : null,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const taskList = (d) => {
|
|
25
|
+
if (!d) return [];
|
|
26
|
+
if (Array.isArray(d)) return d;
|
|
27
|
+
return d.tasks || [];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const pending = taskList(pendingData);
|
|
31
|
+
const running = taskList(runningData);
|
|
32
|
+
const completed = taskList(completedData);
|
|
33
|
+
|
|
34
|
+
/** Extract a human-readable name from a task */
|
|
35
|
+
function _taskName(t) {
|
|
36
|
+
const ctx = t.context || {};
|
|
37
|
+
if (ctx.idea_name) return ctx.idea_name.slice(0, 45);
|
|
38
|
+
if (t.direction) {
|
|
39
|
+
// Extract the idea name from "Write a spec for: <name>." pattern
|
|
40
|
+
const match = t.direction.match(/for:\s*(.+?)[\.\n]/);
|
|
41
|
+
if (match) return match[1].slice(0, 45);
|
|
42
|
+
return t.direction.slice(0, 45);
|
|
43
|
+
}
|
|
44
|
+
return t.id?.slice(0, 20) || "?";
|
|
45
|
+
}
|
|
13
46
|
|
|
14
47
|
console.log();
|
|
15
48
|
console.log("\x1b[1m COHERENCE NETWORK STATUS\x1b[0m");
|
|
16
|
-
console.log(` ${"─".repeat(
|
|
49
|
+
console.log(` ${"─".repeat(50)}`);
|
|
17
50
|
|
|
51
|
+
// API health
|
|
18
52
|
if (health) {
|
|
19
|
-
|
|
20
|
-
console.log(`
|
|
53
|
+
const schemaIcon = health.schema_ok === false ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
|
|
54
|
+
console.log(` API: \x1b[32m${health.status}\x1b[0m (${health.version || "?"}) ${schemaIcon} schema`);
|
|
55
|
+
console.log(` Uptime: ${health.uptime_human || "?"}`);
|
|
21
56
|
} else {
|
|
22
|
-
console.log(` API:
|
|
57
|
+
console.log(` API: \x1b[31moffline\x1b[0m`);
|
|
23
58
|
}
|
|
24
59
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
// Coherence score
|
|
61
|
+
if (coherence && coherence.score != null) {
|
|
62
|
+
const score = coherence.score.toFixed(2);
|
|
63
|
+
const color = coherence.score >= 0.7 ? "\x1b[32m" : coherence.score >= 0.4 ? "\x1b[33m" : "\x1b[31m";
|
|
64
|
+
console.log(` Coherence: ${color}${score}\x1b[0m (${coherence.signals_with_data || 0}/${coherence.total_signals || 0} signals)`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(` Hub: ${getHubUrl()}`);
|
|
68
|
+
console.log(` Node: ${hostname()}`);
|
|
69
|
+
console.log(` Identity: ${getContributorId() || "\x1b[33m(not set — run: cc identity set <id>)\x1b[0m"}`);
|
|
28
70
|
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
// Ideas
|
|
72
|
+
if (ideas) {
|
|
73
|
+
const byStatus = ideas.by_status || {};
|
|
74
|
+
console.log(` Ideas: ${ideas.total || 0} (${byStatus.validated || 0} validated, ${byStatus.none || 0} open)`);
|
|
31
75
|
}
|
|
32
76
|
|
|
77
|
+
// Nodes
|
|
33
78
|
if (Array.isArray(nodes) && nodes.length > 0) {
|
|
34
|
-
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const alive = nodes.filter((n) => {
|
|
81
|
+
const last = n.last_seen_at || n.last_heartbeat || n.registered_at || "";
|
|
82
|
+
if (!last) return false;
|
|
83
|
+
return now - new Date(last).getTime() < 600_000; // 10 min
|
|
84
|
+
});
|
|
85
|
+
console.log(` Nodes: ${nodes.length} registered (${alive.length} live)`);
|
|
86
|
+
for (const n of nodes) {
|
|
87
|
+
const last = n.last_seen_at || n.last_heartbeat || n.registered_at || "";
|
|
88
|
+
const ago = last ? Math.round((now - new Date(last).getTime()) / 60000) : 9999;
|
|
89
|
+
const icon = ago < 10 ? "\x1b[32m●\x1b[0m" : ago < 60 ? "\x1b[33m●\x1b[0m" : "\x1b[31m○\x1b[0m";
|
|
90
|
+
const agoStr = ago < 60 ? `${ago}m` : `${Math.round(ago / 60)}h`;
|
|
91
|
+
console.log(` ${icon} ${(n.hostname || n.node_id || "?").slice(0, 20).padEnd(22)} ${(n.os_type || "?").padEnd(8)} ${agoStr} ago`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Pipeline
|
|
96
|
+
console.log();
|
|
97
|
+
console.log("\x1b[1m PIPELINE\x1b[0m");
|
|
98
|
+
console.log(` ${"─".repeat(50)}`);
|
|
99
|
+
console.log(` Pending: ${pending.length}`);
|
|
100
|
+
console.log(` Running: ${running.length}`);
|
|
101
|
+
|
|
102
|
+
if (running.length > 0) {
|
|
103
|
+
for (const t of running.slice(0, 3)) {
|
|
104
|
+
const name = _taskName(t);
|
|
105
|
+
const age = t.created_at ? Math.round((Date.now() - new Date(t.created_at).getTime()) / 60000) : 0;
|
|
106
|
+
const ageColor = age > 15 ? "\x1b[31m" : age > 5 ? "\x1b[33m" : "\x1b[32m";
|
|
107
|
+
console.log(` \x1b[33m▸\x1b[0m ${(t.task_type || "?").padEnd(6)} ${ageColor}${age}m\x1b[0m ${name}`);
|
|
108
|
+
}
|
|
109
|
+
if (running.length > 3) console.log(` ... and ${running.length - 3} more`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Recent completions
|
|
113
|
+
if (completed.length > 0) {
|
|
114
|
+
console.log(` Recent: last ${completed.length} completed`);
|
|
115
|
+
for (const t of completed.slice(0, 3)) {
|
|
116
|
+
const name = _taskName(t);
|
|
117
|
+
console.log(` \x1b[32m✓\x1b[0m ${t.task_type || "?"} ${name}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// My ledger
|
|
122
|
+
if (ledger && ledger.balance) {
|
|
123
|
+
console.log();
|
|
124
|
+
console.log("\x1b[1m MY LEDGER\x1b[0m");
|
|
125
|
+
console.log(` ${"─".repeat(50)}`);
|
|
126
|
+
console.log(` Total: ${ledger.balance.grand_total || 0} CC`);
|
|
127
|
+
const types = ledger.balance.totals_by_type || {};
|
|
128
|
+
const sorted = Object.entries(types).sort((a, b) => b[1] - a[1]);
|
|
129
|
+
for (const [type, amount] of sorted) {
|
|
130
|
+
console.log(` ${type.padEnd(14)} ${amount} CC`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Messages
|
|
135
|
+
if (messages && messages.count > 0) {
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(`\x1b[33m 📬 ${messages.count} unread message(s)\x1b[0m`);
|
|
138
|
+
for (const m of (messages.messages || []).slice(0, 3)) {
|
|
139
|
+
console.log(` from ${(m.from_node || "?").slice(0, 12)}: ${(m.text || "").slice(0, 60)}`);
|
|
140
|
+
}
|
|
35
141
|
}
|
|
36
142
|
|
|
37
143
|
console.log();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coherence-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Coherence Network CLI — trace ideas from inception to payout, with fair attribution, coherence scoring, and 37 identity providers. No signup needed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|