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 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
+ }
@@ -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
+ }