coherence-cli 0.7.0 → 0.8.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/README.md CHANGED
@@ -159,6 +159,35 @@ Every part of the network links to every other. Jump in wherever makes sense for
159
159
 
160
160
  ---
161
161
 
162
+ ## API key — what it's for and how to get one
163
+
164
+ **No key needed for:** Browsing ideas, searching specs, viewing lineage, checking ledgers, reading node status, exploring the network.
165
+
166
+ **Key needed for:** Updating idea status, recording contributions, linking identities, voting on governance, creating specs, managing federation.
167
+
168
+ Most people start without a key. You only need one when you're ready to contribute.
169
+
170
+ ### Get a key (interactive)
171
+
172
+ ```bash
173
+ cc setup
174
+ ```
175
+
176
+ This walks you through choosing a name, linking an identity (GitHub, Discord, Ethereum, etc.), and generates a key tied to your identity. Stored in `~/.coherence-network/config.json`.
177
+
178
+ ### Manual setup
179
+
180
+ ```bash
181
+ export COHERENCE_API_KEY=your-key
182
+ ```
183
+
184
+ Or set it permanently:
185
+
186
+ ```bash
187
+ cc identity set myname
188
+ # Key is auto-generated and stored in ~/.coherence-network/config.json
189
+ ```
190
+
162
191
  ## Configuration
163
192
 
164
193
  By default, `cc` talks to the public API at `https://api.coherencycoin.com`. Override with environment variables:
@@ -167,7 +196,7 @@ By default, `cc` talks to the public API at `https://api.coherencycoin.com`. Ove
167
196
  # Point to a local node
168
197
  export COHERENCE_API_URL=http://localhost:8000
169
198
 
170
- # Enable write operations
199
+ # Set API key for write operations
171
200
  export COHERENCE_API_KEY=your-key
172
201
  ```
173
202
 
@@ -175,6 +204,82 @@ Config is stored in `~/.coherence-network/config.json`.
175
204
 
176
205
  ---
177
206
 
207
+ ## Search, filter, and CRUD
208
+
209
+ Every resource in the network is accessible from the CLI. Here's how to find what you need:
210
+
211
+ ### Ideas — the core unit
212
+
213
+ ```bash
214
+ cc ideas # Top 20 by free-energy score (ROI × confidence)
215
+ cc ideas 50 # Top 50
216
+ cc idea <id> # Full detail: scores, open questions, children, tasks
217
+ cc idea create <id> <name> # Create a new idea
218
+ --desc "..." # Description
219
+ --value 50 --cost 5 # Potential value and estimated cost
220
+ --parent <parent-id> # Parent idea (fractal hierarchy)
221
+ ```
222
+
223
+ ### Specs — blueprints linked to ideas
224
+
225
+ ```bash
226
+ cc specs # List specs with ROI and value gap
227
+ cc specs 50 # More results
228
+ cc spec <id> # Full spec: summary, pseudocode, implementation status
229
+ ```
230
+
231
+ ### Tasks — the work queue
232
+
233
+ ```bash
234
+ cc tasks # All pending tasks
235
+ cc tasks running # In-progress tasks
236
+ cc tasks completed 20 # Last 20 completed
237
+ cc task <id> # Full task: direction, idea link, output, provider
238
+ cc task next # Claim the highest-priority task
239
+ cc task seed <idea-id> spec # Create a task for an idea
240
+ ```
241
+
242
+ ### Contributors and identity
243
+
244
+ ```bash
245
+ cc identity # Your linked accounts and CC balance
246
+ cc identity lookup github <handle> # Find any contributor by provider identity
247
+ ```
248
+
249
+ ### Nodes and federation
250
+
251
+ ```bash
252
+ cc nodes # All nodes: status, SHA, providers, last seen
253
+ cc inbox # Messages from other nodes
254
+ cc msg <node> <text> # Send a message
255
+ cc cmd <node> status # Remote diagnostics
256
+ ```
257
+
258
+ ### Searching and filtering
259
+
260
+ Most list commands accept a limit. For deeper filtering, use the API directly:
261
+
262
+ ```bash
263
+ # Search ideas by keyword
264
+ curl -s "$CN_API/api/ideas/cards?search=treasury&limit=10" | jq '.items[] | {id, name}'
265
+
266
+ # Search specs by keyword
267
+ curl -s "$CN_API/api/spec-registry/cards?search=auth&limit=10" | jq '.items[] | {spec_id, title}'
268
+
269
+ # Filter tasks by type and status
270
+ curl -s "$CN_API/api/agent/tasks?status=completed&task_type=review&limit=10" | jq '.tasks[] | {id, task_type}'
271
+
272
+ # Contributor ledger
273
+ curl -s "$CN_API/api/contributions/ledger/seeker71" | jq '.balance'
274
+
275
+ # Ideas by parent (fractal drill-down)
276
+ curl -s "$CN_API/api/ideas/<parent-id>" | jq '.child_idea_ids'
277
+ ```
278
+
279
+ The full API has 215 endpoints across 20 resource groups — see [api.coherencycoin.com/docs](https://api.coherencycoin.com/docs) for the complete reference. The CLI covers the most common operations; the API covers everything.
280
+
281
+ ---
282
+
178
283
  ## All commands
179
284
 
180
285
  ```
package/bin/cc.mjs CHANGED
@@ -7,73 +7,57 @@
7
7
  * Zero dependencies. Node 18+ required.
8
8
  */
9
9
 
10
- import { createRequire } from "node:module";
11
10
  import { listIdeas, showIdea, shareIdea, stakeOnIdea, forkIdea, createIdea } from "../lib/commands/ideas.mjs";
12
11
  import { listSpecs, showSpec } from "../lib/commands/specs.mjs";
13
12
  import { contribute } from "../lib/commands/contribute.mjs";
14
13
  import { showStatus, showResonance } from "../lib/commands/status.mjs";
15
14
  import { showIdentity, linkIdentity, unlinkIdentity, lookupIdentity, setupIdentity, setIdentity } from "../lib/commands/identity.mjs";
16
- import { listNodes, sendMessage, readMessages, sendCommand } from "../lib/commands/nodes.mjs";
17
- import { listTasks, showTask, claimTask, claimNext, reportTask, seedTask } from "../lib/commands/tasks.mjs";
18
- import { setup } from "../lib/commands/setup.mjs";
19
-
20
- // Version check — non-blocking, runs in background
21
- const require = createRequire(import.meta.url);
22
- const pkg = require("../package.json");
23
- const LOCAL_VERSION = pkg.version;
24
-
25
- async function checkForUpdate() {
26
- try {
27
- const resp = await fetch("https://registry.npmjs.org/coherence-cli/latest", {
28
- signal: AbortSignal.timeout(3000),
29
- });
30
- if (!resp.ok) return;
31
- const data = await resp.json();
32
- const latest = data.version;
33
- if (latest && latest !== LOCAL_VERSION && latest > LOCAL_VERSION) {
34
- console.log(
35
- `\n\x1b[33m Update available: ${LOCAL_VERSION} → ${latest} — auto-updating...\x1b[0m`,
36
- );
37
- const { execSync } = await import("node:child_process");
38
- try {
39
- execSync(`npm i -g coherence-cli@${latest}`, { stdio: "pipe" });
40
- console.log(`\x1b[32m ✓ Updated to v${latest}\x1b[0m\n`);
41
- } catch {
42
- console.log(`\x1b[31m ✗ Auto-update failed. Run: npm i -g coherence-cli@${latest}\x1b[0m\n`);
43
- }
44
- }
45
- } catch {
46
- // Silent — don't block the CLI for a version check
47
- }
48
- }
49
-
50
- const updateCheck = checkForUpdate();
15
+ import { listNodes, sendMessage, readMessages } from "../lib/commands/nodes.mjs";
16
+ import { listContributors, showContributor, showContributions } from "../lib/commands/contributors.mjs";
17
+ import { listAssets, showAsset, createAsset } from "../lib/commands/assets.mjs";
18
+ import { showNewsFeed, showTrending, showSources, addSource, showNewsResonance } from "../lib/commands/news.mjs";
19
+ import { showTreasury, showDeposits, makeDeposit } from "../lib/commands/treasury.mjs";
20
+ import { listLinks, showLink, showValuation, payoutPreview } from "../lib/commands/lineage.mjs";
21
+ import { listChangeRequests, showChangeRequest, vote, propose } from "../lib/commands/governance.mjs";
22
+ import { listServices, showService, showServicesHealth, showServicesDeps } from "../lib/commands/services.mjs";
23
+ import { showFrictionReport, listFrictionEvents, showFrictionCategories } from "../lib/commands/friction.mjs";
24
+ import { listProviders, showProviderStats } from "../lib/commands/providers.mjs";
25
+ import { showTraceability, showCoverage, traceIdea, traceSpec } from "../lib/commands/traceability.mjs";
26
+ import { showDiag, showDiagHealth, showDiagIssues, showDiagRunners, showDiagVisibility } from "../lib/commands/diagnostics.mjs";
51
27
 
52
28
  const [command, ...args] = process.argv.slice(2);
53
29
 
54
30
  const COMMANDS = {
55
- ideas: () => listIdeas(args),
56
- idea: () => handleIdea(args),
57
- share: () => shareIdea(),
58
- stake: () => stakeOnIdea(args),
59
- fork: () => forkIdea(args),
60
- specs: () => listSpecs(args),
61
- spec: () => showSpec(args),
62
- contribute: () => contribute(args),
63
- status: () => showStatus(),
64
- resonance: () => showResonance(),
65
- identity: () => handleIdentity(args),
66
- nodes: () => listNodes(),
67
- msg: () => sendMessage(args),
68
- cmd: () => sendCommand(args),
69
- messages: () => readMessages(args),
70
- inbox: () => readMessages(args),
71
- tasks: () => listTasks(args),
72
- task: () => handleTask(args),
73
- setup: () => setup(args),
74
- update: () => selfUpdate(),
75
- version: () => console.log(`cc v${LOCAL_VERSION}`),
76
- help: () => showHelp(),
31
+ ideas: () => listIdeas(args),
32
+ idea: () => handleIdea(args),
33
+ share: () => shareIdea(),
34
+ stake: () => stakeOnIdea(args),
35
+ fork: () => forkIdea(args),
36
+ specs: () => listSpecs(args),
37
+ spec: () => showSpec(args),
38
+ contribute: () => contribute(args),
39
+ status: () => showStatus(),
40
+ resonance: () => showResonance(),
41
+ identity: () => handleIdentity(args),
42
+ nodes: () => listNodes(),
43
+ msg: () => sendMessage(args),
44
+ messages: () => readMessages(args),
45
+ inbox: () => readMessages(args),
46
+ contributors: () => listContributors(args),
47
+ contributor: () => handleContributor(args),
48
+ assets: () => listAssets(args),
49
+ asset: () => handleAsset(args),
50
+ news: () => handleNews(args),
51
+ treasury: () => handleTreasury(args),
52
+ lineage: () => handleLineage(args),
53
+ governance: () => handleGovernance(args),
54
+ services: () => handleServices(args),
55
+ service: () => showService(args),
56
+ friction: () => handleFriction(args),
57
+ providers: () => handleProviders(args),
58
+ trace: () => handleTrace(args),
59
+ diag: () => handleDiag(args),
60
+ help: () => showHelp(),
77
61
  };
78
62
 
79
63
  async function handleIdea(args) {
@@ -81,17 +65,6 @@ async function handleIdea(args) {
81
65
  return showIdea(args);
82
66
  }
83
67
 
84
- async function handleTask(args) {
85
- const sub = args[0];
86
- switch (sub) {
87
- case "next": return claimNext();
88
- case "claim": return claimTask(args.slice(1));
89
- case "report": return reportTask(args.slice(1));
90
- case "seed": return seedTask(args.slice(1));
91
- default: return showTask(args);
92
- }
93
- }
94
-
95
68
  async function handleIdentity(args) {
96
69
  const sub = args[0];
97
70
  const subArgs = args.slice(1);
@@ -105,26 +78,101 @@ async function handleIdentity(args) {
105
78
  }
106
79
  }
107
80
 
108
- async function selfUpdate() {
109
- const { execSync } = await import("node:child_process");
110
- console.log(`Current: v${LOCAL_VERSION}`);
111
- console.log("Checking npm for latest version...");
112
- try {
113
- const resp = await fetch("https://registry.npmjs.org/coherence-cli/latest", {
114
- signal: AbortSignal.timeout(5000),
115
- });
116
- const data = await resp.json();
117
- const latest = data.version;
118
- if (latest === LOCAL_VERSION) {
119
- console.log(`\x1b[32m✓\x1b[0m Already on latest version (${LOCAL_VERSION})`);
120
- return;
121
- }
122
- console.log(`Updating ${LOCAL_VERSION} → ${latest}...`);
123
- execSync(`npm i -g coherence-cli@${latest}`, { stdio: "inherit" });
124
- console.log(`\x1b[32m✓\x1b[0m Updated to v${latest}`);
125
- } catch (e) {
126
- console.error(`\x1b[31m✗\x1b[0m Update failed: ${e.message}`);
127
- console.log(" Manual: npm i -g coherence-cli@latest");
81
+ async function handleContributor(args) {
82
+ if (args[1] === "contributions") return showContributions(args);
83
+ return showContributor(args);
84
+ }
85
+
86
+ async function handleAsset(args) {
87
+ if (args[0] === "create") return createAsset(args.slice(1));
88
+ return showAsset(args);
89
+ }
90
+
91
+ async function handleNews(args) {
92
+ const sub = args[0];
93
+ switch (sub) {
94
+ case "trending": return showTrending();
95
+ case "sources": return showSources();
96
+ case "source":
97
+ if (args[1] === "add") return addSource(args.slice(2));
98
+ return showSources();
99
+ case "resonance": return showNewsResonance(args.slice(1));
100
+ default: return showNewsFeed();
101
+ }
102
+ }
103
+
104
+ async function handleTreasury(args) {
105
+ const sub = args[0];
106
+ switch (sub) {
107
+ case "deposits": return showDeposits(args.slice(1));
108
+ case "deposit": return makeDeposit(args.slice(1));
109
+ default: return showTreasury();
110
+ }
111
+ }
112
+
113
+ async function handleLineage(args) {
114
+ if (!args[0]) return listLinks([]);
115
+ if (args[1] === "valuation") return showValuation(args);
116
+ if (args[1] === "payout") return payoutPreview([args[0], args[2]]);
117
+ // If first arg is a number, treat as limit
118
+ if (/^\d+$/.test(args[0])) return listLinks(args);
119
+ return showLink(args);
120
+ }
121
+
122
+ async function handleGovernance(args) {
123
+ const sub = args[0];
124
+ switch (sub) {
125
+ case "vote": return vote(args.slice(1));
126
+ case "propose": return propose(args.slice(1));
127
+ case undefined: return listChangeRequests();
128
+ default: return showChangeRequest(args);
129
+ }
130
+ }
131
+
132
+ async function handleServices(args) {
133
+ const sub = args[0];
134
+ switch (sub) {
135
+ case "health": return showServicesHealth();
136
+ case "deps": return showServicesDeps();
137
+ default: return listServices();
138
+ }
139
+ }
140
+
141
+ async function handleFriction(args) {
142
+ const sub = args[0];
143
+ switch (sub) {
144
+ case "events": return listFrictionEvents(args.slice(1));
145
+ case "categories": return showFrictionCategories();
146
+ default: return showFrictionReport();
147
+ }
148
+ }
149
+
150
+ async function handleProviders(args) {
151
+ const sub = args[0];
152
+ switch (sub) {
153
+ case "stats": return showProviderStats();
154
+ default: return listProviders();
155
+ }
156
+ }
157
+
158
+ async function handleTrace(args) {
159
+ const sub = args[0];
160
+ switch (sub) {
161
+ case "coverage": return showCoverage();
162
+ case "idea": return traceIdea(args.slice(1));
163
+ case "spec": return traceSpec(args.slice(1));
164
+ default: return showTraceability();
165
+ }
166
+ }
167
+
168
+ async function handleDiag(args) {
169
+ const sub = args[0];
170
+ switch (sub) {
171
+ case "health": return showDiagHealth();
172
+ case "issues": return showDiagIssues();
173
+ case "runners": return showDiagRunners();
174
+ case "visibility": return showDiagVisibility();
175
+ default: return showDiag();
128
176
  }
129
177
  }
130
178
 
@@ -158,31 +206,72 @@ function showHelp() {
158
206
  identity unlink <p> Unlink a provider
159
207
  identity lookup <p> <id> Find contributor by identity
160
208
 
161
- \x1b[1mTasks (agent-to-agent):\x1b[0m
162
- tasks [status] [limit] List tasks (pending|running|completed)
163
- task <id> View task detail
164
- task next Claim next pending task (for AI agents)
165
- task claim <id> Claim a specific task
166
- task report <id> <status> [output] Report result (completed|failed)
167
- task seed <idea> [type] Create task from idea (spec|test|impl|review)
168
-
169
209
  \x1b[1mFederation:\x1b[0m
170
210
  nodes List federation nodes
171
211
  msg <node|broadcast> <text> Send message to a node
172
- cmd <node> <command> Remote command (update|status|diagnose|restart|ping)
173
212
  inbox Read your messages
174
213
 
175
- \x1b[1mSystem:\x1b[0m
176
- update Self-update to latest npm version
177
- version Show current version
178
- help Show this help
214
+ \x1b[1mContributors:\x1b[0m
215
+ contributors [limit] List contributors
216
+ contributor <id> View contributor detail
217
+ contributor <id> contributions View contributions
218
+
219
+ \x1b[1mAssets:\x1b[0m
220
+ assets [limit] List assets
221
+ asset <id> View asset detail
222
+ asset create <type> <desc> Create an asset
223
+
224
+ \x1b[1mNews:\x1b[0m
225
+ news News feed
226
+ news trending Trending news
227
+ news sources List news sources
228
+ news source add <url> <name> Add a news source
229
+ news resonance [contributor] News resonance
230
+
231
+ \x1b[1mTreasury:\x1b[0m
232
+ treasury Treasury overview
233
+ treasury deposits <id> Deposits for contributor
234
+ treasury deposit <amt> <asset> Make a deposit
235
+
236
+ \x1b[1mLineage:\x1b[0m
237
+ lineage [limit] Value lineage links
238
+ lineage <id> View lineage link
239
+ lineage <id> valuation Link valuation
240
+ lineage <id> payout <amt> Payout preview
241
+
242
+ \x1b[1mGovernance:\x1b[0m
243
+ governance List change requests
244
+ governance <id> View change request
245
+ governance vote <id> <yes|no> Vote on change request
246
+ governance propose <title> <desc> Create proposal
247
+
248
+ \x1b[1mServices:\x1b[0m
249
+ services List services
250
+ service <id> View service detail
251
+ services health Services health check
252
+ services deps Service dependencies
253
+
254
+ \x1b[1mFriction:\x1b[0m
255
+ friction Friction report
256
+ friction events [limit] Friction events
257
+ friction categories Friction categories
179
258
 
180
259
  \x1b[1mProviders:\x1b[0m
181
- github, x, discord, telegram, mastodon, bluesky, linkedin, reddit,
182
- youtube, twitch, instagram, tiktok, gitlab, bitbucket, npm, crates,
183
- pypi, hackernews, stackoverflow, ethereum, bitcoin, solana, cosmos,
184
- nostr, ens, lens, email, google, apple, microsoft, orcid, did,
185
- keybase, pgp, fediverse, openclaw
260
+ providers List providers
261
+ providers stats Provider statistics
262
+
263
+ \x1b[1mTraceability:\x1b[0m
264
+ trace Traceability overview
265
+ trace coverage Traceability coverage
266
+ trace idea <id> Trace an idea
267
+ trace spec <id> Trace a spec
268
+
269
+ \x1b[1mDiagnostics:\x1b[0m
270
+ diag Agent effectiveness + pipeline
271
+ diag health Collective health
272
+ diag issues Fatal + monitor issues
273
+ diag runners Agent runners
274
+ diag visibility Agent visibility
186
275
 
187
276
  \x1b[2mHub: https://api.coherencycoin.com\x1b[0m
188
277
  `);
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Assets commands: assets, asset
3
+ */
4
+
5
+ import { get, post } from "../api.mjs";
6
+
7
+ function truncate(str, len) {
8
+ if (!str) return "";
9
+ return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ }
11
+
12
+ export async function listAssets(args) {
13
+ const limit = parseInt(args[0]) || 20;
14
+ const raw = await get("/api/assets", { limit });
15
+ const data = Array.isArray(raw) ? raw : raw?.assets;
16
+ if (!data || !Array.isArray(data)) {
17
+ console.log("Could not fetch assets.");
18
+ return;
19
+ }
20
+ if (data.length === 0) {
21
+ console.log("No assets found.");
22
+ return;
23
+ }
24
+
25
+ console.log();
26
+ console.log(`\x1b[1m ASSETS\x1b[0m (${data.length})`);
27
+ console.log(` ${"─".repeat(60)}`);
28
+ 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}`);
32
+ }
33
+ console.log();
34
+ }
35
+
36
+ export async function showAsset(args) {
37
+ const id = args[0];
38
+ if (!id) {
39
+ console.log("Usage: cc asset <id>");
40
+ return;
41
+ }
42
+ const data = await get(`/api/assets/${encodeURIComponent(id)}`);
43
+ if (!data) {
44
+ console.log(`Asset '${id}' not found.`);
45
+ return;
46
+ }
47
+ console.log();
48
+ console.log(`\x1b[1m ${data.name || data.id}\x1b[0m`);
49
+ console.log(` ${"─".repeat(50)}`);
50
+ if (data.type || data.asset_type) console.log(` Type: ${data.type || data.asset_type}`);
51
+ if (data.description) console.log(` Description: ${truncate(data.description, 60)}`);
52
+ if (data.value != null) console.log(` Value: ${data.value}`);
53
+ if (data.created_at) console.log(` Created: ${data.created_at}`);
54
+ console.log();
55
+ }
56
+
57
+ export async function createAsset(args) {
58
+ const type = args[0];
59
+ const desc = args.slice(1).join(" ");
60
+ if (!type || !desc) {
61
+ console.log("Usage: cc asset create <type> <description>");
62
+ return;
63
+ }
64
+ const result = await post("/api/assets", {
65
+ type,
66
+ description: desc,
67
+ });
68
+ if (result) {
69
+ console.log(`\x1b[32m✓\x1b[0m Asset created: ${result.id || "(new)"}`);
70
+ } else {
71
+ console.log("Failed to create asset.");
72
+ }
73
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Contributors commands: contributors, contributor
3
+ */
4
+
5
+ import { get } from "../api.mjs";
6
+
7
+ function truncate(str, len) {
8
+ if (!str) return "";
9
+ return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ }
11
+
12
+ export async function listContributors(args) {
13
+ const limit = parseInt(args[0]) || 20;
14
+ const raw = await get("/api/contributors", { limit });
15
+ const data = Array.isArray(raw) ? raw : raw?.contributors;
16
+ if (!data || !Array.isArray(data)) {
17
+ console.log("Could not fetch contributors.");
18
+ return;
19
+ }
20
+ if (data.length === 0) {
21
+ console.log("No contributors found.");
22
+ return;
23
+ }
24
+
25
+ console.log();
26
+ console.log(`\x1b[1m CONTRIBUTORS\x1b[0m (${data.length})`);
27
+ console.log(` ${"─".repeat(60)}`);
28
+ 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}`);
32
+ }
33
+ console.log();
34
+ }
35
+
36
+ export async function showContributor(args) {
37
+ const id = args[0];
38
+ if (!id) {
39
+ console.log("Usage: cc contributor <id>");
40
+ return;
41
+ }
42
+ const data = await get(`/api/contributors/${encodeURIComponent(id)}`);
43
+ if (!data) {
44
+ console.log(`Contributor '${id}' not found.`);
45
+ return;
46
+ }
47
+ console.log();
48
+ console.log(`\x1b[1m ${data.name || data.display_name || data.id}\x1b[0m`);
49
+ console.log(` ${"─".repeat(50)}`);
50
+ if (data.id) console.log(` ID: ${data.id}`);
51
+ if (data.total_cc != null) console.log(` Total CC: ${data.total_cc}`);
52
+ if (data.role) console.log(` Role: ${data.role}`);
53
+ if (data.joined_at) console.log(` Joined: ${data.joined_at}`);
54
+ console.log();
55
+ }
56
+
57
+ export async function showContributions(args) {
58
+ const id = args[0];
59
+ if (!id) {
60
+ console.log("Usage: cc contributor <id> contributions");
61
+ return;
62
+ }
63
+ const data = await get(`/api/contributors/${encodeURIComponent(id)}/contributions`);
64
+ const list = Array.isArray(data) ? data : data?.contributions;
65
+ if (!list || !Array.isArray(list)) {
66
+ console.log(`No contributions found for '${id}'.`);
67
+ return;
68
+ }
69
+
70
+ console.log();
71
+ console.log(`\x1b[1m CONTRIBUTIONS\x1b[0m for ${id} (${list.length})`);
72
+ console.log(` ${"─".repeat(60)}`);
73
+ for (const c of list) {
74
+ const desc = truncate(c.description || c.type || "?", 45);
75
+ const cc = c.cc_amount != null ? `${c.cc_amount} CC` : "";
76
+ console.log(` ${desc.padEnd(47)} ${cc}`);
77
+ }
78
+ console.log();
79
+ }