coherence-cli 0.1.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 +96 -0
- package/lib/api.mjs +64 -0
- package/lib/commands/contribute.mjs +38 -0
- package/lib/commands/ideas.mjs +138 -0
- package/lib/commands/identity.mjs +92 -0
- package/lib/commands/specs.mjs +64 -0
- package/lib/commands/status.mjs +60 -0
- package/lib/config.mjs +49 -0
- package/lib/identity.mjs +85 -0
- package/package.json +28 -0
package/bin/cc.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cc — Coherence Network CLI
|
|
5
|
+
*
|
|
6
|
+
* Identity-first contributions, idea staking, and value traceability.
|
|
7
|
+
* Zero dependencies. Node 18+ required.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { listIdeas, showIdea, shareIdea, stakeOnIdea, forkIdea } from "../lib/commands/ideas.mjs";
|
|
11
|
+
import { listSpecs, showSpec } from "../lib/commands/specs.mjs";
|
|
12
|
+
import { contribute } from "../lib/commands/contribute.mjs";
|
|
13
|
+
import { showStatus, showResonance } from "../lib/commands/status.mjs";
|
|
14
|
+
import { showIdentity, linkIdentity, unlinkIdentity, lookupIdentity, setupIdentity } from "../lib/commands/identity.mjs";
|
|
15
|
+
|
|
16
|
+
const [command, ...args] = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
const COMMANDS = {
|
|
19
|
+
ideas: () => listIdeas(args),
|
|
20
|
+
idea: () => showIdea(args),
|
|
21
|
+
share: () => shareIdea(),
|
|
22
|
+
stake: () => stakeOnIdea(args),
|
|
23
|
+
fork: () => forkIdea(args),
|
|
24
|
+
specs: () => listSpecs(args),
|
|
25
|
+
spec: () => showSpec(args),
|
|
26
|
+
contribute: () => contribute(),
|
|
27
|
+
status: () => showStatus(),
|
|
28
|
+
resonance: () => showResonance(),
|
|
29
|
+
identity: () => handleIdentity(args),
|
|
30
|
+
help: () => showHelp(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function handleIdentity(args) {
|
|
34
|
+
const sub = args[0];
|
|
35
|
+
const subArgs = args.slice(1);
|
|
36
|
+
switch (sub) {
|
|
37
|
+
case "link": return linkIdentity(subArgs);
|
|
38
|
+
case "unlink": return unlinkIdentity(subArgs);
|
|
39
|
+
case "lookup": return lookupIdentity(subArgs);
|
|
40
|
+
case "setup": return setupIdentity();
|
|
41
|
+
default: return showIdentity();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function showHelp() {
|
|
46
|
+
console.log(`
|
|
47
|
+
\x1b[1mcc\x1b[0m — Coherence Network CLI
|
|
48
|
+
|
|
49
|
+
\x1b[1mUsage:\x1b[0m cc <command> [args]
|
|
50
|
+
|
|
51
|
+
\x1b[1mExplore:\x1b[0m
|
|
52
|
+
ideas [limit] Browse ideas by ROI
|
|
53
|
+
idea <id> View idea detail
|
|
54
|
+
specs [limit] List feature specs
|
|
55
|
+
spec <id> View spec detail
|
|
56
|
+
resonance What's alive right now
|
|
57
|
+
status Network health + node info
|
|
58
|
+
|
|
59
|
+
\x1b[1mContribute:\x1b[0m
|
|
60
|
+
share Submit a new idea
|
|
61
|
+
stake <id> <cc> Stake CC on an idea
|
|
62
|
+
fork <id> Fork an idea
|
|
63
|
+
contribute Record any contribution
|
|
64
|
+
|
|
65
|
+
\x1b[1mIdentity:\x1b[0m
|
|
66
|
+
identity Show your linked accounts
|
|
67
|
+
identity setup Guided onboarding
|
|
68
|
+
identity link <p> <id> Link a provider (github, discord, ethereum, ...)
|
|
69
|
+
identity unlink <p> Unlink a provider
|
|
70
|
+
identity lookup <p> <id> Find contributor by identity
|
|
71
|
+
|
|
72
|
+
\x1b[1mProviders:\x1b[0m
|
|
73
|
+
github, x, discord, telegram, mastodon, bluesky, linkedin, reddit,
|
|
74
|
+
youtube, twitch, instagram, tiktok, gitlab, bitbucket, npm, crates,
|
|
75
|
+
pypi, hackernews, stackoverflow, ethereum, bitcoin, solana, cosmos,
|
|
76
|
+
nostr, ens, lens, email, google, apple, microsoft, orcid, did,
|
|
77
|
+
keybase, pgp, fediverse, openclaw
|
|
78
|
+
|
|
79
|
+
\x1b[2mHub: https://api.coherencycoin.com\x1b[0m
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Run
|
|
84
|
+
const handler = COMMANDS[command];
|
|
85
|
+
if (handler) {
|
|
86
|
+
Promise.resolve(handler()).catch((err) => {
|
|
87
|
+
console.error(err.message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
90
|
+
} else if (!command) {
|
|
91
|
+
showHelp();
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`Unknown command: ${command}`);
|
|
94
|
+
console.log("Run 'cc help' for available commands.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
package/lib/api.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Coherence Network API.
|
|
3
|
+
* Zero deps — uses native fetch (Node 18+).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getHubUrl } from "./config.mjs";
|
|
7
|
+
|
|
8
|
+
const TIMEOUT_MS = 12_000;
|
|
9
|
+
|
|
10
|
+
function buildUrl(path, params) {
|
|
11
|
+
const base = getHubUrl().replace(/\/$/, "");
|
|
12
|
+
const url = new URL(path, base);
|
|
13
|
+
if (params) {
|
|
14
|
+
for (const [k, v] of Object.entries(params)) {
|
|
15
|
+
if (v != null) url.searchParams.set(k, String(v));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return url.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function get(path, params) {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(buildUrl(path, params), {
|
|
24
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) return null;
|
|
27
|
+
return await res.json();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(`\x1b[2m network error: ${err.message}\x1b[0m`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function post(path, body) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(buildUrl(path), {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const err = await res.json().catch(() => ({}));
|
|
44
|
+
console.error(`\x1b[2m ${res.status}: ${err.detail || "request failed"}\x1b[0m`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return await res.json();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`\x1b[2m network error: ${err.message}\x1b[0m`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function del(path) {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(buildUrl(path), {
|
|
57
|
+
method: "DELETE",
|
|
58
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
59
|
+
});
|
|
60
|
+
return res.ok;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribute command — record any contribution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { post } from "../api.mjs";
|
|
6
|
+
import { ensureIdentity } from "../identity.mjs";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
|
|
10
|
+
export async function contribute() {
|
|
11
|
+
const contributor = await ensureIdentity();
|
|
12
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
13
|
+
|
|
14
|
+
console.log();
|
|
15
|
+
console.log("Record a contribution — anything you did that created value.");
|
|
16
|
+
console.log();
|
|
17
|
+
|
|
18
|
+
const type = (await rl.question("Type (code/docs/review/design/community/other): > ")).trim() || "other";
|
|
19
|
+
const amount = parseFloat((await rl.question("CC value (default 1): > ")).trim()) || 1.0;
|
|
20
|
+
const ideaId = (await rl.question("Idea ID (optional, press enter to skip): > ")).trim() || undefined;
|
|
21
|
+
const description = (await rl.question("Brief description: > ")).trim();
|
|
22
|
+
|
|
23
|
+
rl.close();
|
|
24
|
+
|
|
25
|
+
const result = await post("/api/contributions/record", {
|
|
26
|
+
contributor_id: contributor,
|
|
27
|
+
type,
|
|
28
|
+
amount_cc: amount,
|
|
29
|
+
idea_id: ideaId,
|
|
30
|
+
metadata: { description },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (result) {
|
|
34
|
+
console.log(`\x1b[32m✓\x1b[0m Contribution recorded: ${type} (${amount} CC)`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log("Failed to record contribution.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ideas commands: ideas, idea, share, stake, fork
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { get, post } from "../api.mjs";
|
|
6
|
+
import { ensureIdentity } from "../identity.mjs";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
|
|
10
|
+
function truncate(str, len) {
|
|
11
|
+
if (!str) return "";
|
|
12
|
+
return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function listIdeas(args) {
|
|
16
|
+
const limit = parseInt(args[0]) || 20;
|
|
17
|
+
const raw = await get("/api/ideas", { limit });
|
|
18
|
+
// API may return { ideas: [...] } or a raw array
|
|
19
|
+
const data = Array.isArray(raw) ? raw : raw?.ideas;
|
|
20
|
+
if (!data || !Array.isArray(data)) {
|
|
21
|
+
console.log("Could not fetch ideas.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (data.length === 0) {
|
|
25
|
+
console.log("No ideas in the portfolio yet.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(`\x1b[1m IDEAS\x1b[0m (${data.length})`);
|
|
31
|
+
console.log(` ${"─".repeat(72)}`);
|
|
32
|
+
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}`);
|
|
38
|
+
}
|
|
39
|
+
console.log();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function showIdea(args) {
|
|
43
|
+
const id = args[0];
|
|
44
|
+
if (!id) {
|
|
45
|
+
console.log("Usage: cc idea <id>");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const data = await get(`/api/ideas/${encodeURIComponent(id)}`);
|
|
49
|
+
if (!data) {
|
|
50
|
+
console.log(`Idea '${id}' not found.`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(`\x1b[1m ${data.name || data.id}\x1b[0m`);
|
|
55
|
+
if (data.description) console.log(` ${truncate(data.description, 72)}`);
|
|
56
|
+
console.log(` ${"─".repeat(50)}`);
|
|
57
|
+
console.log(` Status: ${data.manifestation_status || "NONE"}`);
|
|
58
|
+
console.log(` Potential: ${data.potential_value ?? "?"}`);
|
|
59
|
+
console.log(` Actual: ${data.actual_value ?? 0}`);
|
|
60
|
+
console.log(` Est. Cost: ${data.estimated_cost ?? "?"}`);
|
|
61
|
+
console.log(` Confidence: ${data.confidence ?? "?"}`);
|
|
62
|
+
if (data.free_energy_score != null) console.log(` Free Energy: ${data.free_energy_score.toFixed(3)}`);
|
|
63
|
+
if (data.roi_cc != null) console.log(` ROI (CC): ${data.roi_cc.toFixed(2)}`);
|
|
64
|
+
if (data.open_questions?.length) {
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(" \x1b[1mOpen Questions:\x1b[0m");
|
|
67
|
+
for (const q of data.open_questions) {
|
|
68
|
+
const qText = typeof q === "string" ? q : q.question || q.text || JSON.stringify(q);
|
|
69
|
+
console.log(` ? ${truncate(qText, 68)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function shareIdea() {
|
|
76
|
+
const contributor = await ensureIdentity();
|
|
77
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
78
|
+
|
|
79
|
+
console.log();
|
|
80
|
+
const name = (await rl.question("Idea name: > ")).trim();
|
|
81
|
+
if (!name) { rl.close(); return; }
|
|
82
|
+
const description = (await rl.question("Description: > ")).trim();
|
|
83
|
+
const potentialValue = parseFloat((await rl.question("Potential value (CC): > ")).trim()) || 100;
|
|
84
|
+
const estimatedCost = parseFloat((await rl.question("Estimated cost (CC): > ")).trim()) || 50;
|
|
85
|
+
|
|
86
|
+
rl.close();
|
|
87
|
+
|
|
88
|
+
const result = await post("/api/ideas", {
|
|
89
|
+
name,
|
|
90
|
+
description,
|
|
91
|
+
potential_value: potentialValue,
|
|
92
|
+
estimated_cost: estimatedCost,
|
|
93
|
+
metadata: { shared_by: contributor },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (result) {
|
|
97
|
+
console.log(`\x1b[32m✓\x1b[0m Idea shared: ${result.id || result.name || name}`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log("Failed to share idea.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function stakeOnIdea(args) {
|
|
104
|
+
const ideaId = args[0];
|
|
105
|
+
const amount = parseFloat(args[1]);
|
|
106
|
+
if (!ideaId || isNaN(amount)) {
|
|
107
|
+
console.log("Usage: cc stake <idea-id> <amount-cc>");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const contributor = await ensureIdentity();
|
|
111
|
+
const result = await post(`/api/ideas/${encodeURIComponent(ideaId)}/stake`, {
|
|
112
|
+
contributor_id: contributor,
|
|
113
|
+
amount_cc: amount,
|
|
114
|
+
});
|
|
115
|
+
if (result) {
|
|
116
|
+
console.log(`\x1b[32m✓\x1b[0m Staked ${amount} CC on '${ideaId}'`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log("Stake failed.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function forkIdea(args) {
|
|
123
|
+
const ideaId = args[0];
|
|
124
|
+
if (!ideaId) {
|
|
125
|
+
console.log("Usage: cc fork <idea-id>");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const contributor = await ensureIdentity();
|
|
129
|
+
const result = await post(
|
|
130
|
+
`/api/ideas/${encodeURIComponent(ideaId)}/fork?forker_id=${encodeURIComponent(contributor)}`,
|
|
131
|
+
{},
|
|
132
|
+
);
|
|
133
|
+
if (result) {
|
|
134
|
+
console.log(`\x1b[32m✓\x1b[0m Forked idea '${ideaId}' → ${result.id || "(new)"}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log("Fork failed.");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity commands: show, link, unlink, lookup, setup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { get, post, del } from "../api.mjs";
|
|
6
|
+
import { getContributorId } from "../config.mjs";
|
|
7
|
+
import { ensureIdentity } from "../identity.mjs";
|
|
8
|
+
|
|
9
|
+
export async function showIdentity() {
|
|
10
|
+
const id = getContributorId();
|
|
11
|
+
if (!id) {
|
|
12
|
+
console.log("No identity configured. Run: cc identity setup");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const data = await get(`/api/identity/${encodeURIComponent(id)}`);
|
|
16
|
+
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(`\x1b[1m ${id}\x1b[0m`);
|
|
19
|
+
console.log(` ${"─".repeat(40)}`);
|
|
20
|
+
|
|
21
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
22
|
+
console.log(" No linked accounts.");
|
|
23
|
+
} else {
|
|
24
|
+
for (const rec of data) {
|
|
25
|
+
const dot = rec.verified ? "\x1b[32m●\x1b[0m" : "\x1b[33m○\x1b[0m";
|
|
26
|
+
console.log(` ${dot} ${rec.provider.padEnd(14)} ${rec.provider_id}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(" Link more: cc identity link <provider> <id>");
|
|
31
|
+
console.log();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function linkIdentity(args) {
|
|
35
|
+
const [provider, ...rest] = args;
|
|
36
|
+
const providerId = rest.join(" ");
|
|
37
|
+
if (!provider || !providerId) {
|
|
38
|
+
console.log("Usage: cc identity link <provider> <id>");
|
|
39
|
+
console.log("Providers: github, x, discord, telegram, ethereum, bitcoin, solana, email, gitlab, linkedin, ...");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const contributor = await ensureIdentity();
|
|
43
|
+
const result = await post("/api/identity/link", {
|
|
44
|
+
contributor_id: contributor,
|
|
45
|
+
provider,
|
|
46
|
+
provider_id: providerId,
|
|
47
|
+
display_name: providerId,
|
|
48
|
+
});
|
|
49
|
+
if (result) {
|
|
50
|
+
console.log(`\x1b[32m✓\x1b[0m Linked ${provider}:${providerId}`);
|
|
51
|
+
} else {
|
|
52
|
+
console.log("Link failed.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function unlinkIdentity(args) {
|
|
57
|
+
const provider = args[0];
|
|
58
|
+
if (!provider) {
|
|
59
|
+
console.log("Usage: cc identity unlink <provider>");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const contributor = getContributorId();
|
|
63
|
+
if (!contributor) {
|
|
64
|
+
console.log("No identity configured.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const ok = await del(`/api/identity/${encodeURIComponent(contributor)}/${provider}`);
|
|
68
|
+
if (ok) {
|
|
69
|
+
console.log(`\x1b[32m✓\x1b[0m Unlinked ${provider}`);
|
|
70
|
+
} else {
|
|
71
|
+
console.log("Unlink failed or not found.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function lookupIdentity(args) {
|
|
76
|
+
const [provider, providerId] = args;
|
|
77
|
+
if (!provider || !providerId) {
|
|
78
|
+
console.log("Usage: cc identity lookup <provider> <id>");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const data = await get(`/api/identity/lookup/${encodeURIComponent(provider)}/${encodeURIComponent(providerId)}`);
|
|
82
|
+
if (data) {
|
|
83
|
+
console.log(`\x1b[32m✓\x1b[0m ${provider}:${providerId} → contributor: ${data.contributor_id}`);
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`No contributor found for ${provider}:${providerId}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function setupIdentity() {
|
|
90
|
+
// Force re-run onboarding
|
|
91
|
+
await ensureIdentity();
|
|
92
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specs commands: specs, spec
|
|
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 listSpecs(args) {
|
|
13
|
+
const limit = parseInt(args[0]) || 20;
|
|
14
|
+
const data = await get("/api/spec-registry", { limit });
|
|
15
|
+
if (!data || !Array.isArray(data)) {
|
|
16
|
+
console.log("Could not fetch specs.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (data.length === 0) {
|
|
20
|
+
console.log("No specs registered.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(`\x1b[1m SPECS\x1b[0m (${data.length})`);
|
|
26
|
+
console.log(` ${"─".repeat(72)}`);
|
|
27
|
+
for (const s of data) {
|
|
28
|
+
const roi = s.estimated_roi != null ? `ROI ${s.estimated_roi.toFixed(1)}` : "";
|
|
29
|
+
const gap = s.value_gap != null ? `Gap ${s.value_gap.toFixed(0)}` : "";
|
|
30
|
+
console.log(` ${(s.spec_id || "").padEnd(8)} ${truncate(s.title || "", 38).padEnd(40)} ${roi.padEnd(12)} ${gap}`);
|
|
31
|
+
}
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function showSpec(args) {
|
|
36
|
+
const id = args[0];
|
|
37
|
+
if (!id) {
|
|
38
|
+
console.log("Usage: cc spec <spec-id>");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const data = await get(`/api/spec-registry/${encodeURIComponent(id)}`);
|
|
42
|
+
if (!data) {
|
|
43
|
+
console.log(`Spec '${id}' not found.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(`\x1b[1m ${data.title || data.spec_id}\x1b[0m`);
|
|
48
|
+
if (data.summary) console.log(` ${truncate(data.summary, 72)}`);
|
|
49
|
+
console.log(` ${"─".repeat(50)}`);
|
|
50
|
+
if (data.estimated_roi != null) console.log(` Est. ROI: ${data.estimated_roi.toFixed(2)}`);
|
|
51
|
+
if (data.actual_roi != null) console.log(` Actual ROI: ${data.actual_roi.toFixed(2)}`);
|
|
52
|
+
if (data.value_gap != null) console.log(` Value Gap: ${data.value_gap.toFixed(0)}`);
|
|
53
|
+
if (data.implementation_summary) {
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(" \x1b[1mImplementation:\x1b[0m");
|
|
56
|
+
console.log(` ${truncate(data.implementation_summary, 72)}`);
|
|
57
|
+
}
|
|
58
|
+
if (data.pseudocode_summary) {
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(" \x1b[1mPseudocode:\x1b[0m");
|
|
61
|
+
console.log(` ${truncate(data.pseudocode_summary, 72)}`);
|
|
62
|
+
}
|
|
63
|
+
console.log();
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status commands: status, resonance
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { get } from "../api.mjs";
|
|
6
|
+
import { getContributorId, getHubUrl } from "../config.mjs";
|
|
7
|
+
import { hostname } from "node:os";
|
|
8
|
+
|
|
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");
|
|
13
|
+
|
|
14
|
+
console.log();
|
|
15
|
+
console.log("\x1b[1m COHERENCE NETWORK STATUS\x1b[0m");
|
|
16
|
+
console.log(` ${"─".repeat(40)}`);
|
|
17
|
+
|
|
18
|
+
if (health) {
|
|
19
|
+
console.log(` API: \x1b[32m${health.status}\x1b[0m (${health.version || "?"})`);
|
|
20
|
+
console.log(` Uptime: ${health.uptime_human || "?"}`);
|
|
21
|
+
} else {
|
|
22
|
+
console.log(` API: \x1b[31moffline\x1b[0m`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(` Hub: ${getHubUrl()}`);
|
|
26
|
+
console.log(` Node: ${hostname()}`);
|
|
27
|
+
console.log(` Identity: ${getContributorId() || "(not set — run: cc identity setup)"}`);
|
|
28
|
+
|
|
29
|
+
if (ideas?.count != null) {
|
|
30
|
+
console.log(` Ideas: ${ideas.count}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (Array.isArray(nodes) && nodes.length > 0) {
|
|
34
|
+
console.log(` Fed. Nodes: ${nodes.length}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function showResonance() {
|
|
41
|
+
const data = await get("/api/ideas/resonance");
|
|
42
|
+
if (!data) {
|
|
43
|
+
console.log("Could not fetch resonance.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log();
|
|
47
|
+
console.log("\x1b[1m RESONANCE\x1b[0m — what's alive right now");
|
|
48
|
+
console.log(` ${"─".repeat(40)}`);
|
|
49
|
+
if (Array.isArray(data)) {
|
|
50
|
+
for (const item of data.slice(0, 10)) {
|
|
51
|
+
const name = item.name || item.idea_id || item.id || "?";
|
|
52
|
+
console.log(` \x1b[33m~\x1b[0m ${name}`);
|
|
53
|
+
}
|
|
54
|
+
} else if (typeof data === "object") {
|
|
55
|
+
for (const [key, val] of Object.entries(data)) {
|
|
56
|
+
console.log(` ${key}: ${JSON.stringify(val)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader — reads/writes ~/.coherence-network/config.json
|
|
3
|
+
* Same file as the Python CLI and API use.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
export const CONFIG_DIR = join(homedir(), ".coherence-network");
|
|
11
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
|
+
|
|
13
|
+
const DEFAULT_HUB_URL = "https://api.coherencycoin.com";
|
|
14
|
+
|
|
15
|
+
export function loadConfig() {
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveConfig(updates) {
|
|
25
|
+
const config = { ...loadConfig(), ...updates };
|
|
26
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
27
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getContributorId() {
|
|
34
|
+
return (
|
|
35
|
+
loadConfig().contributor_id ||
|
|
36
|
+
process.env.COHERENCE_CONTRIBUTOR ||
|
|
37
|
+
null
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getHubUrl() {
|
|
42
|
+
// Env vars override config file (allows easy local dev testing)
|
|
43
|
+
return (
|
|
44
|
+
process.env.COHERENCE_HUB_URL ||
|
|
45
|
+
process.env.COHERENCE_API_URL ||
|
|
46
|
+
loadConfig().hub_url ||
|
|
47
|
+
DEFAULT_HUB_URL
|
|
48
|
+
);
|
|
49
|
+
}
|
package/lib/identity.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-first onboarding — runs on first use if no contributor_id in config.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { stdin, stdout } from "node:process";
|
|
7
|
+
import { getContributorId, saveConfig } from "./config.mjs";
|
|
8
|
+
import { post } from "./api.mjs";
|
|
9
|
+
|
|
10
|
+
const ONBOARD_PROVIDERS = ["github", "ethereum", "x", "email"];
|
|
11
|
+
|
|
12
|
+
async function prompt(rl, question) {
|
|
13
|
+
const answer = await rl.question(question);
|
|
14
|
+
return answer.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure the user has a contributor identity.
|
|
19
|
+
* If not, run guided onboarding. Returns the contributor_id.
|
|
20
|
+
*/
|
|
21
|
+
export async function ensureIdentity() {
|
|
22
|
+
let id = getContributorId();
|
|
23
|
+
if (id) return id;
|
|
24
|
+
|
|
25
|
+
// Non-interactive environment — can't prompt
|
|
26
|
+
if (!stdin.isTTY) {
|
|
27
|
+
console.error("No contributor identity configured.");
|
|
28
|
+
console.error("Set COHERENCE_CONTRIBUTOR or run: cc identity setup");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
33
|
+
|
|
34
|
+
console.log();
|
|
35
|
+
console.log("\x1b[1mWelcome to the Coherence Network.\x1b[0m");
|
|
36
|
+
console.log();
|
|
37
|
+
|
|
38
|
+
const name = await prompt(rl, "What's your name? > ");
|
|
39
|
+
if (!name) {
|
|
40
|
+
console.log("A name is needed to attribute your contributions.");
|
|
41
|
+
rl.close();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
saveConfig({ contributor_id: name });
|
|
46
|
+
id = name;
|
|
47
|
+
|
|
48
|
+
// Link name identity
|
|
49
|
+
await post("/api/identity/link", {
|
|
50
|
+
contributor_id: name,
|
|
51
|
+
provider: "name",
|
|
52
|
+
provider_id: name,
|
|
53
|
+
display_name: name,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log();
|
|
57
|
+
|
|
58
|
+
for (const provider of ONBOARD_PROVIDERS) {
|
|
59
|
+
const yn = await prompt(rl, `Link your ${provider}? (y/n) > `);
|
|
60
|
+
if (yn.toLowerCase() === "y" || yn.toLowerCase() === "yes") {
|
|
61
|
+
const value = await prompt(rl, ` ${provider} handle/address: > `);
|
|
62
|
+
if (value) {
|
|
63
|
+
const result = await post("/api/identity/link", {
|
|
64
|
+
contributor_id: name,
|
|
65
|
+
provider,
|
|
66
|
+
provider_id: value,
|
|
67
|
+
display_name: value,
|
|
68
|
+
});
|
|
69
|
+
if (result) {
|
|
70
|
+
console.log(` \x1b[32m✓\x1b[0m Linked ${provider}:${value}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(` \x1b[33m!\x1b[0m Could not link (network issue). Try later: cc identity link ${provider} ${value}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(`Saved to ~/.coherence-network/config.json`);
|
|
80
|
+
console.log(`Link more accounts anytime: cc identity link <provider> <id>`);
|
|
81
|
+
console.log();
|
|
82
|
+
|
|
83
|
+
rl.close();
|
|
84
|
+
return id;
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coherence-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Coherence Network CLI — identity-first contributions, idea staking, and value traceability",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc": "bin/cc.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"lib/"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"coherence",
|
|
18
|
+
"contribution",
|
|
19
|
+
"attribution",
|
|
20
|
+
"identity",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/seeker71/Coherence-Network.git"
|
|
27
|
+
}
|
|
28
|
+
}
|