coherence-cli 0.8.3 → 0.9.0

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
@@ -24,6 +24,9 @@ import { showFrictionReport, listFrictionEvents, showFrictionCategories } from "
24
24
  import { listProviders, showProviderStats } from "../lib/commands/providers.mjs";
25
25
  import { showTraceability, showCoverage, traceIdea, traceSpec } from "../lib/commands/traceability.mjs";
26
26
  import { showDiag, showDiagHealth, showDiagIssues, showDiagRunners, showDiagVisibility } from "../lib/commands/diagnostics.mjs";
27
+ import { deploy } from "../lib/commands/deploy.mjs";
28
+ import { listen } from "../lib/commands/listen.mjs";
29
+ import { listTasks, showTask, claimTask, claimNext, reportTask, seedTask } from "../lib/commands/tasks.mjs";
27
30
 
28
31
  const [command, ...args] = process.argv.slice(2);
29
32
 
@@ -57,6 +60,10 @@ const COMMANDS = {
57
60
  providers: () => handleProviders(args),
58
61
  trace: () => handleTrace(args),
59
62
  diag: () => handleDiag(args),
63
+ tasks: () => listTasks(args),
64
+ task: () => handleTask(args),
65
+ deploy: () => deploy(args),
66
+ listen: () => listen(args),
60
67
  help: () => showHelp(),
61
68
  };
62
69
 
@@ -83,6 +90,17 @@ async function handleContributor(args) {
83
90
  return showContributor(args);
84
91
  }
85
92
 
93
+ async function handleTask(args) {
94
+ const sub = args[0];
95
+ switch (sub) {
96
+ case "next": return claimNext();
97
+ case "claim": return claimTask(args.slice(1));
98
+ case "report": return reportTask(args.slice(1));
99
+ case "seed": return seedTask(args.slice(1));
100
+ default: return showTask(args);
101
+ }
102
+ }
103
+
86
104
  async function handleAsset(args) {
87
105
  if (args[0] === "create") return createAsset(args.slice(1));
88
106
  return showAsset(args);
@@ -216,6 +234,14 @@ function showHelp() {
216
234
  contributor <id> View contributor detail
217
235
  contributor <id> contributions View contributions
218
236
 
237
+ \x1b[1mTasks (agent work protocol):\x1b[0m
238
+ tasks [status] [limit] List tasks (pending, running, completed)
239
+ task <id> View task detail
240
+ task next Claim highest-priority pending task
241
+ task claim <id> Claim a specific task
242
+ task report <id> <completed|failed> [output] Report result
243
+ task seed <idea> [type] Create task from idea (spec|test|impl|review)
244
+
219
245
  \x1b[1mAssets:\x1b[0m
220
246
  assets [limit] List assets
221
247
  asset <id> View asset detail
@@ -274,6 +300,7 @@ function showHelp() {
274
300
  diag visibility Agent visibility
275
301
 
276
302
  \x1b[2mHub: https://api.coherencycoin.com\x1b[0m
303
+ \x1b[2mDocs: https://coherencycoin.com\x1b[0m
277
304
  `);
278
305
  }
279
306
 
@@ -4,9 +4,21 @@
4
4
 
5
5
  import { get, post } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
14
+ }
15
+
16
+ /** Colored type badge */
17
+ function assetBadge(type) {
18
+ if (!type) return "\x1b[2m[?]\x1b[0m".padEnd(16);
19
+ const colors = { spec: "\x1b[36m", code: "\x1b[32m", doc: "\x1b[33m", test: "\x1b[35m", data: "\x1b[34m" };
20
+ const color = colors[type.toLowerCase()] || "\x1b[2m";
21
+ return `${color}[${type}]\x1b[0m`.padEnd(16);
10
22
  }
11
23
 
12
24
  export async function listAssets(args) {
@@ -24,11 +36,12 @@ export async function listAssets(args) {
24
36
 
25
37
  console.log();
26
38
  console.log(`\x1b[1m ASSETS\x1b[0m (${data.length})`);
27
- console.log(` ${"─".repeat(60)}`);
39
+ console.log(` ${"─".repeat(72)}`);
28
40
  for (const a of data) {
29
- const name = truncate(a.name || a.description || a.id, 35);
30
- const type = (a.type || a.asset_type || "").padEnd(12);
31
- console.log(` ${type} ${name}`);
41
+ const badge = assetBadge(a.type || a.asset_type);
42
+ const desc = truncate(a.name || a.description || a.id, 42).padEnd(44);
43
+ const cost = a.value != null ? String(a.value).padStart(8) : a.cost != null ? String(a.cost).padStart(8) : "";
44
+ console.log(` ${badge} ${desc} ${cost}`);
32
45
  }
33
46
  console.log();
34
47
  }
@@ -4,9 +4,22 @@
4
4
 
5
5
  import { get } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
14
+ }
15
+
16
+ /** Colored type badge */
17
+ function typeBadge(type) {
18
+ if (!type) return "\x1b[2m[?]\x1b[0m".padEnd(17);
19
+ const upper = type.toUpperCase();
20
+ if (upper === "HUMAN") return "\x1b[36m[HUMAN]\x1b[0m".padEnd(18);
21
+ if (upper === "AGENT") return "\x1b[33m[AGENT]\x1b[0m".padEnd(18);
22
+ return `\x1b[2m[${upper}]\x1b[0m`.padEnd(18);
10
23
  }
11
24
 
12
25
  export async function listContributors(args) {
@@ -24,11 +37,12 @@ export async function listContributors(args) {
24
37
 
25
38
  console.log();
26
39
  console.log(`\x1b[1m CONTRIBUTORS\x1b[0m (${data.length})`);
27
- console.log(` ${"─".repeat(60)}`);
40
+ console.log(` ${"─".repeat(68)}`);
28
41
  for (const c of data) {
29
- const name = truncate(c.name || c.display_name || c.id, 30);
30
- const cc = c.total_cc != null ? `${c.total_cc} CC` : "";
31
- console.log(` ${name.padEnd(32)} ${cc}`);
42
+ const name = truncate(c.name || c.display_name || c.id, 30).padEnd(32);
43
+ const badge = typeBadge(c.type || c.role);
44
+ const cc = c.total_cc != null ? String(c.total_cc.toFixed ? c.total_cc.toFixed(1) : c.total_cc).padStart(8) + " CC" : "";
45
+ console.log(` ${name} ${badge} ${cc}`);
32
46
  }
33
47
  console.log();
34
48
  }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Deploy command: deploy latest main to VPS (coherencycoin.com)
3
+ *
4
+ * cc deploy — deploy now (directly if SSH key available, otherwise via node message)
5
+ * cc deploy status — check what SHA is deployed vs origin/main
6
+ */
7
+
8
+ import { get, post } from "../api.mjs";
9
+ import { existsSync } from "node:fs";
10
+ import { execSync } from "node:child_process";
11
+ import { homedir, hostname as getHostname } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ const SSH_KEY = join(homedir(), ".ssh", "hostinger-openclaw");
15
+ const VPS_HOST = "root@187.77.152.42";
16
+ const REPO_DIR = "/docker/coherence-network/repo";
17
+ const COMPOSE_DIR = "/docker/coherence-network";
18
+
19
+ function ssh(cmd, timeout = 120000) {
20
+ return execSync(
21
+ `ssh -i "${SSH_KEY}" -o LogLevel=QUIET -o StrictHostKeyChecking=no ${VPS_HOST} '${cmd}'`,
22
+ { encoding: "utf-8", timeout },
23
+ ).trim();
24
+ }
25
+
26
+ export async function deploy(args) {
27
+ const sub = args[0];
28
+ if (sub === "status") return deployStatus();
29
+
30
+ console.log("\x1b[1m DEPLOYING TO VPS\x1b[0m");
31
+ console.log(` ${"─".repeat(50)}`);
32
+
33
+ // If this machine has SSH key, deploy directly — don't send a message to yourself
34
+ if (existsSync(SSH_KEY)) {
35
+ return deployDirect();
36
+ }
37
+
38
+ // No SSH key — ask a node that has one to deploy
39
+ return deployViaMessage();
40
+ }
41
+
42
+ async function deployDirect() {
43
+ console.log(" Deploying directly (SSH key found)...");
44
+ console.log();
45
+
46
+ try {
47
+ // 1. Capture current SHA
48
+ const prevSha = ssh(`cd ${REPO_DIR} && git rev-parse --short HEAD`, 15000);
49
+ console.log(` Current VPS: ${prevSha}`);
50
+
51
+ // 2. Git pull
52
+ const pullOutput = ssh(`cd ${REPO_DIR} && git pull origin main --ff-only`, 30000);
53
+ const newSha = ssh(`cd ${REPO_DIR} && git rev-parse --short HEAD`, 15000);
54
+
55
+ if (newSha === prevSha) {
56
+ console.log(` \x1b[32m✓\x1b[0m VPS already up to date at ${newSha}`);
57
+ await broadcast(`Deploy: VPS already at ${newSha}, no changes needed.`);
58
+ return;
59
+ }
60
+
61
+ console.log(` Pulled: ${prevSha} → ${newSha}`);
62
+
63
+ // 3. Build + restart
64
+ console.log(" Building containers (this may take a minute)...");
65
+ ssh(`cd ${COMPOSE_DIR} && docker compose build --no-cache api web && docker compose up -d api web`, 300000);
66
+ console.log(" Containers restarted, waiting 30s for health check...");
67
+
68
+ // 4. Wait
69
+ await new Promise((r) => setTimeout(r, 30000));
70
+
71
+ // 5. Health check
72
+ const health = await get("/api/health");
73
+ if (health?.status === "ok" && health?.schema_ok) {
74
+ console.log(` \x1b[32m✓\x1b[0m Deploy successful: ${prevSha} → ${newSha}`);
75
+ console.log(` Health: OK | Schema: OK | Uptime: ${health.uptime_human || "?"}`);
76
+ await broadcast(`Deploy successful: ${prevSha} → ${newSha}. Health OK, schema OK.`);
77
+ } else {
78
+ // Rollback
79
+ console.log(` \x1b[31m✗\x1b[0m Health check failed — rolling back to ${prevSha}`);
80
+ ssh(`cd ${REPO_DIR} && git checkout ${prevSha} && cd ${COMPOSE_DIR} && docker compose build --no-cache api web && docker compose up -d api web`, 300000);
81
+ console.log(` Rolled back to ${prevSha}`);
82
+ await broadcast(`Deploy FAILED health check. Rolled back ${newSha} → ${prevSha}.`);
83
+ }
84
+ } catch (e) {
85
+ console.log(` \x1b[31m✗\x1b[0m Deploy error: ${e.message}`);
86
+ }
87
+ }
88
+
89
+ async function deployViaMessage() {
90
+ console.log(" No SSH key on this machine — sending deploy command to a node that has one...");
91
+ console.log();
92
+
93
+ const nodes = await get("/api/federation/nodes");
94
+ if (!nodes || !Array.isArray(nodes)) {
95
+ console.log(" \x1b[31m✗\x1b[0m Could not fetch nodes");
96
+ return;
97
+ }
98
+
99
+ // Find any node that might have SSH (prefer macOS)
100
+ const target = nodes.find((n) => n.os_type === "macos") || nodes[0];
101
+ if (!target) {
102
+ console.log(" \x1b[31m✗\x1b[0m No nodes available");
103
+ return;
104
+ }
105
+
106
+ const myNodeId = nodes.find((n) => n.hostname === getHostname())?.node_id || "cli-user";
107
+
108
+ const result = await post(`/api/federation/nodes/${myNodeId}/messages`, {
109
+ from_node: myNodeId,
110
+ to_node: target.node_id,
111
+ type: "command",
112
+ text: "Deploy latest main to VPS",
113
+ payload: { command: "deploy" },
114
+ });
115
+
116
+ if (result?.id) {
117
+ console.log(` \x1b[32m✓\x1b[0m Deploy command sent to ${target.hostname}`);
118
+ console.log(" Node will deploy on next poll (~2 min). Check: cc deploy status");
119
+ } else {
120
+ console.log(" \x1b[31m✗\x1b[0m Failed to send deploy command");
121
+ }
122
+ }
123
+
124
+ async function broadcast(text) {
125
+ try {
126
+ const nodes = await get("/api/federation/nodes");
127
+ const myNodeId = nodes?.find((n) => n.hostname === getHostname())?.node_id || "deployer";
128
+ await post(`/api/federation/broadcast`, {
129
+ from_node: myNodeId,
130
+ type: "deploy",
131
+ text,
132
+ });
133
+ } catch { /* best effort */ }
134
+ }
135
+
136
+ async function deployStatus() {
137
+ console.log("\x1b[1m DEPLOY STATUS\x1b[0m");
138
+ console.log(` ${"─".repeat(50)}`);
139
+
140
+ const health = await get("/api/health");
141
+ if (!health) {
142
+ console.log(" \x1b[31m✗\x1b[0m API unreachable");
143
+ return;
144
+ }
145
+
146
+ const deployedSha = health.deployed_sha || "unknown";
147
+ console.log(` API: ${health.status === "ok" ? "\x1b[32mok\x1b[0m" : "\x1b[31m" + health.status + "\x1b[0m"}`);
148
+ console.log(` Deployed: ${deployedSha.slice(0, 10)}`);
149
+ console.log(` Schema: ${health.schema_ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"}`);
150
+ console.log(` Uptime: ${health.uptime_human || "?"}`);
151
+
152
+ const nodes = await get("/api/federation/nodes");
153
+ if (nodes && Array.isArray(nodes)) {
154
+ console.log();
155
+ console.log(" Node SHAs:");
156
+ for (const n of nodes) {
157
+ const git = n.capabilities?.git || {};
158
+ const sha = (git.local_sha || "?").slice(0, 10);
159
+ const match = sha === deployedSha.slice(0, 10) ? "\x1b[32m✓\x1b[0m" : "\x1b[33m≠\x1b[0m";
160
+ console.log(` ${match} ${(n.hostname || "?").padEnd(25)} ${sha}`);
161
+ }
162
+ }
163
+ }
@@ -4,9 +4,13 @@
4
4
 
5
5
  import { get } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
10
14
  }
11
15
 
12
16
  export async function showFrictionReport() {
@@ -16,23 +20,53 @@ export async function showFrictionReport() {
16
20
  return;
17
21
  }
18
22
 
23
+ const total = data.total_friction ?? data.total_events ?? data.event_count ?? 0;
24
+ const days = data.period_days ?? data.days ?? 7;
25
+
19
26
  console.log();
20
27
  console.log(`\x1b[1m FRICTION REPORT\x1b[0m`);
21
28
  console.log(` ${"─".repeat(50)}`);
22
- if (data.total_friction != null) console.log(` Total: ${data.total_friction}`);
23
- if (data.top_categories && Array.isArray(data.top_categories)) {
24
- console.log(` Top categories:`);
25
- for (const c of data.top_categories) {
26
- const name = c.name || c.category || "?";
27
- const count = c.count != null ? c.count : "?";
28
- console.log(` ${name.padEnd(25)} ${count}`);
29
+ console.log(` \x1b[1m${total}\x1b[0m events in \x1b[1m${days}\x1b[0m days`);
30
+ console.log();
31
+
32
+ // Top block types as bar chart
33
+ const blockTypes = data.top_block_types || data.top_types || [];
34
+ if (Array.isArray(blockTypes) && blockTypes.length > 0) {
35
+ console.log(` \x1b[1mTop Block Types\x1b[0m`);
36
+ const maxCount = Math.max(...blockTypes.map(b => b.count || 0), 1);
37
+ for (const b of blockTypes.slice(0, 8)) {
38
+ const name = (b.name || b.type || "?").padEnd(20);
39
+ const count = b.count || 0;
40
+ const barLen = Math.round((count / maxCount) * 20);
41
+ const bar = "\x1b[33m" + "\u2593".repeat(barLen) + "\u2591".repeat(20 - barLen) + "\x1b[0m";
42
+ console.log(` ${name} ${bar} ${String(count).padStart(5)}`);
29
43
  }
44
+ console.log();
30
45
  }
31
- // Fallback: print all keys
32
- const shown = new Set(["total_friction", "top_categories"]);
46
+
47
+ // Top categories/stages as colored list
48
+ const categories = data.top_categories || data.top_stages || [];
49
+ if (Array.isArray(categories) && categories.length > 0) {
50
+ console.log(` \x1b[1mTop Stages\x1b[0m`);
51
+ const stageColors = ["\x1b[31m", "\x1b[33m", "\x1b[36m", "\x1b[32m", "\x1b[35m"];
52
+ categories.forEach((c, i) => {
53
+ const name = c.name || c.category || c.stage || "?";
54
+ const count = c.count != null ? c.count : "?";
55
+ const color = stageColors[i % stageColors.length];
56
+ console.log(` ${color}●\x1b[0m ${name.padEnd(25)} ${String(count).padStart(5)}`);
57
+ });
58
+ console.log();
59
+ }
60
+
61
+ // Remaining keys (excluding already shown)
62
+ const shown = new Set(["total_friction", "total_events", "event_count", "period_days", "days", "top_categories", "top_stages", "top_block_types", "top_types"]);
33
63
  for (const [key, val] of Object.entries(data)) {
34
64
  if (!shown.has(key) && val != null) {
35
- console.log(` ${key}: ${JSON.stringify(val)}`);
65
+ if (typeof val === "object") {
66
+ console.log(` \x1b[2m${key}:\x1b[0m ${JSON.stringify(val)}`);
67
+ } else {
68
+ console.log(` \x1b[2m${key}:\x1b[0m ${val}`);
69
+ }
36
70
  }
37
71
  }
38
72
  console.log();
@@ -4,9 +4,22 @@
4
4
 
5
5
  import { get, post } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
14
+ }
15
+
16
+ /** Colored status label */
17
+ function statusLabel(status) {
18
+ const s = (status || "?").toLowerCase();
19
+ if (s === "approved") return "\x1b[32mapproved\x1b[0m";
20
+ if (s === "rejected") return "\x1b[31mrejected\x1b[0m";
21
+ if (s === "pending") return "\x1b[33mpending\x1b[0m";
22
+ return `\x1b[2m${s}\x1b[0m`;
10
23
  }
11
24
 
12
25
  export async function listChangeRequests() {
@@ -23,12 +36,19 @@ export async function listChangeRequests() {
23
36
 
24
37
  console.log();
25
38
  console.log(`\x1b[1m GOVERNANCE\x1b[0m (${data.length} change requests)`);
26
- console.log(` ${"─".repeat(60)}`);
39
+ console.log(` ${"─".repeat(74)}`);
27
40
  for (const cr of data) {
28
41
  const status = cr.status || "?";
29
- const dot = status === "approved" ? "\x1b[32m●\x1b[0m" : status === "rejected" ? "\x1b[31m●\x1b[0m" : "\x1b[33m●\x1b[0m";
30
- const title = truncate(cr.title || cr.id, 45);
31
- console.log(` ${dot} ${title.padEnd(47)} ${status}`);
42
+ const dot = status === "approved" ? "\x1b[32m●\x1b[0m"
43
+ : status === "rejected" ? "\x1b[31m●\x1b[0m"
44
+ : "\x1b[33m●\x1b[0m";
45
+ const title = truncate(cr.title || cr.id, 35).padEnd(37);
46
+ const proposer = cr.proposer || cr.proposed_by || "";
47
+ const proposerStr = proposer ? truncate(proposer, 14).padEnd(16) : "".padEnd(16);
48
+ const votesFor = cr.votes_for != null ? cr.votes_for : 0;
49
+ const votesAgainst = cr.votes_against != null ? cr.votes_against : 0;
50
+ const votes = `\x1b[32m+${votesFor}\x1b[0m/\x1b[31m-${votesAgainst}\x1b[0m`;
51
+ console.log(` ${dot} ${title} ${proposerStr} ${votes} ${statusLabel(status)}`);
32
52
  }
33
53
  console.log();
34
54
  }
@@ -7,9 +7,19 @@ import { ensureIdentity } from "../identity.mjs";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { stdin, stdout } from "node:process";
9
9
 
10
+ /** Truncate at word boundary, append "..." if needed */
10
11
  function truncate(str, len) {
11
12
  if (!str) return "";
12
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
13
+ if (str.length <= len) return str;
14
+ const trimmed = str.slice(0, len - 3);
15
+ const lastSpace = trimmed.lastIndexOf(" ");
16
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
17
+ }
18
+
19
+ /** Mini bar: filled vs empty blocks for a score out of max */
20
+ function miniBar(value, max, width = 5) {
21
+ const filled = Math.round((value / max) * width);
22
+ return "\u2593".repeat(Math.min(filled, width)) + "\u2591".repeat(width - Math.min(filled, width));
13
23
  }
14
24
 
15
25
  export async function listIdeas(args) {
@@ -28,14 +38,20 @@ export async function listIdeas(args) {
28
38
 
29
39
  console.log();
30
40
  console.log(`\x1b[1m IDEAS\x1b[0m (${data.length})`);
31
- console.log(` ${"─".repeat(72)}`);
41
+ console.log(` ${"─".repeat(74)}`);
32
42
  for (const idea of data) {
33
- const status = idea.manifestation_status || "NONE";
34
- const dot = status === "VALIDATED" ? "\x1b[32m●\x1b[0m" : status === "PARTIAL" ? "\x1b[33m●\x1b[0m" : "\x1b[2m○\x1b[0m";
35
- const roi = idea.roi_cc != null ? `ROI ${idea.roi_cc.toFixed(1)}` : "";
36
- const fe = idea.free_energy_score != null ? `FE ${idea.free_energy_score.toFixed(2)}` : "";
37
- console.log(` ${dot} ${truncate(idea.name || idea.id, 40).padEnd(42)} ${roi.padEnd(12)} ${fe}`);
43
+ const status = (idea.manifestation_status || "NONE").toUpperCase();
44
+ const dot = status === "VALIDATED" ? "\x1b[32m●\x1b[0m"
45
+ : status === "PARTIAL" ? "\x1b[33m●\x1b[0m"
46
+ : "\x1b[2m○\x1b[0m";
47
+ const name = truncate(idea.name || idea.id, 45).padEnd(47);
48
+ const roi = idea.roi_cc != null ? String(idea.roi_cc.toFixed(1)).padStart(6) : " -";
49
+ const fe = idea.free_energy_score != null ? idea.free_energy_score.toFixed(2) : null;
50
+ const feStr = fe != null ? `${String(fe).padStart(5)} ${miniBar(idea.free_energy_score, 20)}` : "";
51
+ console.log(` ${dot} ${name} ${roi} ${feStr}`);
38
52
  }
53
+ console.log(` ${"─".repeat(74)}`);
54
+ console.log(`\x1b[2m ${"Name".padEnd(49)} ${"ROI".padStart(6)} ${"FE".padStart(5)}\x1b[0m`);
39
55
  console.log();
40
56
  }
41
57
 
@@ -52,7 +68,7 @@ export async function showIdea(args) {
52
68
  }
53
69
  console.log();
54
70
  console.log(`\x1b[1m ${data.name || data.id}\x1b[0m`);
55
- if (data.description) console.log(` ${truncate(data.description, 72)}`);
71
+ if (data.description) console.log(` \x1b[2m${truncate(data.description, 72)}\x1b[0m`);
56
72
  console.log(` ${"─".repeat(50)}`);
57
73
  console.log(` Status: ${data.manifestation_status || "NONE"}`);
58
74
  console.log(` Potential: ${data.potential_value ?? "?"}`);
@@ -4,9 +4,13 @@
4
4
 
5
5
  import { get, post } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
10
14
  }
11
15
 
12
16
  export async function listLinks(args) {
@@ -24,12 +28,14 @@ export async function listLinks(args) {
24
28
 
25
29
  console.log();
26
30
  console.log(`\x1b[1m VALUE LINEAGE\x1b[0m (${data.length})`);
27
- console.log(` ${"─".repeat(60)}`);
31
+ console.log(` ${"─".repeat(74)}`);
28
32
  for (const link of data) {
29
- const from = truncate(link.from_id || link.source || "?", 20);
30
- const to = truncate(link.to_id || link.target || "?", 20);
31
- const weight = link.weight != null ? `w=${link.weight.toFixed(2)}` : "";
32
- console.log(` ${from.padEnd(22)} \x1b[2m→\x1b[0m ${to.padEnd(22)} ${weight}`);
33
+ const ideaId = truncate(link.idea_id || link.from_id || link.source || "?", 18).padEnd(20);
34
+ const specId = truncate(link.spec_id || link.to_id || link.target || "?", 18).padEnd(20);
35
+ const contributor = truncate(link.contributor || link.contributor_id || "", 14).padEnd(16);
36
+ const cost = link.estimated_cost != null ? String(link.estimated_cost.toFixed ? link.estimated_cost.toFixed(1) : link.estimated_cost).padStart(7)
37
+ : link.weight != null ? String(link.weight.toFixed(2)).padStart(7) : "";
38
+ console.log(` ${ideaId} \x1b[2m->\x1b[0m ${specId} \x1b[2m${contributor}\x1b[0m ${cost}`);
33
39
  }
34
40
  console.log();
35
41
  }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * cc listen — real-time event stream from the network
3
+ *
4
+ * Connects to the SSE endpoint and prints events as they arrive.
5
+ * Messages, deploys, task completions, status changes — all pushed in real-time.
6
+ *
7
+ * Usage:
8
+ * cc listen # Listen for events for this node
9
+ * cc listen --all # Listen for all events (broadcast)
10
+ */
11
+
12
+ import { hostname } from "node:os";
13
+ import { get } from "../api.mjs";
14
+
15
+ const API_BASE = process.env.COHERENCE_API_URL || "https://api.coherencycoin.com";
16
+
17
+ export async function listen(args) {
18
+ // Resolve our node ID
19
+ const nodes = await get("/api/federation/nodes");
20
+ const myNode = nodes?.find((n) => n.hostname === hostname());
21
+ const nodeId = myNode?.node_id || "unknown";
22
+
23
+ if (nodeId === "unknown") {
24
+ console.log("\x1b[31m✗\x1b[0m Could not resolve node ID. Is the runner registered?");
25
+ return;
26
+ }
27
+
28
+ const url = `${API_BASE}/api/federation/nodes/${nodeId}/stream`;
29
+
30
+ console.log("\x1b[1m LISTENING\x1b[0m");
31
+ console.log(` ${"─".repeat(50)}`);
32
+ console.log(` Node: ${myNode.hostname} (${nodeId.slice(0, 12)})`);
33
+ console.log(` Stream: ${url}`);
34
+ console.log(` Press Ctrl+C to stop`);
35
+ console.log();
36
+
37
+ try {
38
+ const response = await fetch(url, {
39
+ headers: { Accept: "text/event-stream" },
40
+ signal: AbortSignal.timeout(3600000), // 1 hour max
41
+ });
42
+
43
+ if (!response.ok) {
44
+ console.log(`\x1b[31m✗\x1b[0m Stream error: HTTP ${response.status}`);
45
+ return;
46
+ }
47
+
48
+ const reader = response.body.getReader();
49
+ const decoder = new TextDecoder();
50
+ let buffer = "";
51
+
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+
56
+ buffer += decoder.decode(value, { stream: true });
57
+ const lines = buffer.split("\n");
58
+ buffer = lines.pop() || "";
59
+
60
+ for (const line of lines) {
61
+ if (line.startsWith("data: ")) {
62
+ try {
63
+ const event = JSON.parse(line.slice(6));
64
+ formatEvent(event);
65
+ } catch {
66
+ // Not JSON — skip
67
+ }
68
+ }
69
+ }
70
+ }
71
+ } catch (e) {
72
+ if (e.name === "AbortError") {
73
+ console.log("\n Stream timed out after 1 hour.");
74
+ } else {
75
+ console.log(`\n\x1b[31m✗\x1b[0m Stream error: ${e.message}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ function formatEvent(event) {
81
+ const ts = new Date().toLocaleTimeString();
82
+ const type = event.event_type || event.type || "?";
83
+
84
+ switch (type) {
85
+ case "connected":
86
+ console.log(` \x1b[32m●\x1b[0m \x1b[2m${ts}\x1b[0m Connected to stream`);
87
+ break;
88
+ case "text":
89
+ case "command":
90
+ case "command_response":
91
+ console.log(` \x1b[33m◆\x1b[0m \x1b[2m${ts}\x1b[0m [${type}] from ${(event.from_node || "?").slice(0, 12)}`);
92
+ console.log(` ${event.text || "(no text)"}`);
93
+ break;
94
+ case "deploy":
95
+ console.log(` \x1b[36m▲\x1b[0m \x1b[2m${ts}\x1b[0m DEPLOY: ${event.text || "?"}`);
96
+ break;
97
+ case "task_completed":
98
+ console.log(` \x1b[32m✓\x1b[0m \x1b[2m${ts}\x1b[0m Task completed: ${event.task_type || "?"} — ${event.idea_name || "?"}`);
99
+ break;
100
+ case "task_failed":
101
+ console.log(` \x1b[31m✗\x1b[0m \x1b[2m${ts}\x1b[0m Task failed: ${event.task_type || "?"} — ${event.error || "?"}`);
102
+ break;
103
+ default:
104
+ console.log(` \x1b[2m${ts}\x1b[0m [${type}] ${JSON.stringify(event).slice(0, 100)}`);
105
+ }
106
+ }