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 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
+ }
@@ -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
- const health = await get("/api/health");
11
- const ideas = await get("/api/ideas/count");
12
- const nodes = await get("/api/federation/nodes");
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(40)}`);
49
+ console.log(` ${"─".repeat(50)}`);
17
50
 
51
+ // API health
18
52
  if (health) {
19
- console.log(` API: \x1b[32m${health.status}\x1b[0m (${health.version || "?"})`);
20
- console.log(` Uptime: ${health.uptime_human || "?"}`);
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: \x1b[31moffline\x1b[0m`);
57
+ console.log(` API: \x1b[31moffline\x1b[0m`);
23
58
  }
24
59
 
25
- console.log(` Hub: ${getHubUrl()}`);
26
- console.log(` Node: ${hostname()}`);
27
- console.log(` Identity: ${getContributorId() || "(not set — run: cc identity setup)"}`);
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
- if (ideas?.count != null) {
30
- console.log(` Ideas: ${ideas.count}`);
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
- console.log(` Fed. Nodes: ${nodes.length}`);
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.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": {