clinkz-cli 0.0.0-stage → 0.2.3

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/anim.js ADDED
@@ -0,0 +1,46 @@
1
+ // Terminal motion for clinkz: the logo sweep on start and the working spinner. Only runs on a real, colour-capable
2
+ // terminal; piped output stays plain. NO_COLOR or CLINKZ_NO_ANIM turns it off.
3
+ const live = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && !process.env.CLINKZ_NO_ANIM;
4
+ const LOGO = [
5
+ " ██████╗██╗ ██╗███╗ ██╗██╗ ██╗███████╗",
6
+ "██╔════╝██║ ██║████╗ ██║██║ ██╔╝╚══███╔╝",
7
+ "██║ ██║ ██║██╔██╗ ██║█████╔╝ ███╔╝ ",
8
+ "██║ ██║ ██║██║╚██╗██║██╔═██╗ ███╔╝ ",
9
+ "╚██████╗███████╗██║██║ ╚████║██║ ██╗███████╗",
10
+ " ╚═════╝╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝",
11
+ ];
12
+ const WIDTH = LOGO[0].length, STOPS = [[122, 85, 255], [182, 162, 255], [255, 255, 255]];
13
+ const mix = (a, b, t) => Math.round(a + (b - a) * t);
14
+ function shade(t) { const p = Math.min(0.999, Math.max(0, t)) * (STOPS.length - 1), i = Math.floor(p), k = p - i; return STOPS[i].map((v, n) => mix(v, STOPS[i + 1][n], k)); }
15
+ const ink = (ch, t) => { const [r, g, b] = shade(t); return `\x1b[38;2;${r};${g};${b}m${ch}`; };
16
+ const RESET = "\x1b[0m", HIDE = "\x1b[?25l", SHOW = "\x1b[?25h", CLEAR = "\r\x1b[2K";
17
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
18
+ const put = (s) => process.stdout.write(s);
19
+ process.on("exit", () => { if (live()) put(SHOW); });
20
+
21
+ function rows(upTo) {
22
+ return LOGO.map((line) => { let out = ""; for (let i = 0; i < WIDTH; i++) { const ch = i < upTo ? line[i] : " "; if (ch === " ") { out += " "; continue; } const glow = i > upTo - 5; out += glow ? `\x1b[38;2;255;255;255m${ch}` : ink(ch, i / (WIDTH - 1)); } return out + RESET; }).join("\n");
23
+ }
24
+ /** The logo wipes in from the left behind a white edge. Skipped when the terminal is too narrow for it. */
25
+ export async function banner() {
26
+ if (!live() || (process.stdout.columns || 80) < WIDTH + 2) return false;
27
+ put(HIDE + "\n");
28
+ for (let col = 0; col <= WIDTH + 4; col += 3) { put(rows(col)); await wait(22); put(`\r\x1b[${LOGO.length - 1}A`); }
29
+ put(rows(WIDTH + 8) + "\n" + SHOW); return true;
30
+ }
31
+
32
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
33
+ const WORDS = ["Thinking", "Nocking", "Drawing the bow", "Taking aim", "Reading the wind", "Fletching", "Tracing the arrow"];
34
+ /** One line that spins while nothing is printing. start() may be called again to change the label. */
35
+ export function spinner() {
36
+ let timer = null, label = null, t0 = 0, n = 0;
37
+ const draw = () => {
38
+ const secs = Math.floor((Date.now() - t0) / 1000), word = label || WORDS[Math.floor(secs / 4) % WORDS.length];
39
+ const [r, g, b] = shade(0.5 + 0.5 * Math.sin(n / 5)); n++;
40
+ put(`${CLEAR}\x1b[38;2;${r};${g};${b}m${FRAMES[n % FRAMES.length]} ${word}…${RESET} \x1b[2m(${secs}s)${RESET}`);
41
+ };
42
+ return {
43
+ start(text = null) { if (!live()) return; label = text; if (timer) return; t0 = Date.now(); put(HIDE); draw(); timer = setInterval(draw, 90); },
44
+ stop() { if (!timer) return; clearInterval(timer); timer = null; put(CLEAR + SHOW); },
45
+ };
46
+ }
package/build.js ADDED
@@ -0,0 +1,97 @@
1
+ // clinkz build: vibe coding in the terminal. The model runs on the server; its tools run here, in the current folder.
2
+ // Nothing escapes that folder, secret files stay unreadable, and every write and command waits for the user's yes.
3
+ import { spawn } from "node:child_process";
4
+ import { lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import path from "node:path";
7
+ import readline from "node:readline";
8
+ import { banner, spinner } from "./anim.js";
9
+
10
+ const ROOT_LIMIT = 200_000, OUTPUT_LIMIT = 20_000, COMMAND_MS = 120_000, ROUNDS = 30;
11
+ const SECRET = /(^|[\\/])(\.env[^\\/]*|\.ssh|\.aws|\.npmrc|id_rsa[^\\/]*|id_ed25519[^\\/]*|[^\\/]*\.pem|[^\\/]*\.key|[^\\/]*wallet[^\\/]*\.json|config\.json)($|[\\/])/i;
12
+ const SKIP = new Set(["node_modules", ".git", "dist", ".next"]);
13
+ const SAFE_ENV = () => Object.fromEntries(Object.entries(process.env).filter(([k]) => !/KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|MNEMONIC/i.test(k)));
14
+
15
+ /** Resolve a model-supplied path inside the workspace, or throw. Links are followed so they cannot lead outside. */
16
+ export function inside(root, given, { write = false } = {}) {
17
+ const raw = String(given ?? ""); if (!raw || raw.includes("\0")) throw new Error("Bad path.");
18
+ const full = path.resolve(root, raw), rel = path.relative(root, full);
19
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error("That path is outside the project folder.");
20
+ if (SECRET.test(rel)) throw new Error("Secret files are off limits.");
21
+ if (write && /(^|[\\/])\.git($|[\\/])/.test(rel)) throw new Error("The .git folder is off limits.");
22
+ let probe = full; for (;;) { try { lstatSync(probe); break; } catch { const up = path.dirname(probe); if (up === probe) break; probe = up; } }
23
+ const real = realpathSync(probe), back = path.relative(realpathSync(root), real);
24
+ if (back.startsWith("..") || path.isAbsolute(back)) throw new Error("That path leads outside the project folder.");
25
+ return full;
26
+ }
27
+ function listing(root, dir, depth, out) {
28
+ for (const e of readdirSync(dir, { withFileTypes: true })) { if (SKIP.has(e.name)) continue; const p = path.join(dir, e.name), rel = path.relative(root, p); if (SECRET.test(rel)) continue; out.push(rel + (e.isDirectory() ? "/" : "")); if (e.isDirectory() && depth > 0 && out.length < 400) listing(root, p, depth - 1, out); }
29
+ return out;
30
+ }
31
+ function runCommand(command, cwd) {
32
+ return new Promise((resolve) => {
33
+ const child = spawn(command, { cwd, shell: true, env: SAFE_ENV(), stdio: ["ignore", "pipe", "pipe"] }); let out = "";
34
+ const take = (d) => { process.stdout.write(d); out = (out + d).slice(-OUTPUT_LIMIT); };
35
+ child.stdout.on("data", take); child.stderr.on("data", take);
36
+ const timer = setTimeout(() => { child.kill(); out += `\n(Stopped after ${COMMAND_MS / 1000} s.)`; }, COMMAND_MS);
37
+ child.on("close", (code) => { clearTimeout(timer); resolve(`exit code ${code}\n${out}`); });
38
+ child.on("error", (e) => { clearTimeout(timer); resolve(`Could not start: ${e.message}`); });
39
+ });
40
+ }
41
+
42
+ export async function runBuild({ api, stream, clean, ui, task }) {
43
+ const root = process.cwd(), { dim, bold, green, red, violet, yellow } = ui;
44
+ if (path.resolve(root) === path.resolve(homedir()) || path.dirname(root) === root) throw new Error("Run this inside a project folder, not your home or drive root.");
45
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout }), lines = rl[Symbol.asyncIterator]();
46
+ const ask = async (q) => { process.stdout.write(q); const r = await lines.next(); return r.done ? null : r.value.trim(); };
47
+ let writesOk = false; const spin = spinner();
48
+ const STEP = { list_files: "Looking around", read_file: "Reading the code", write_file: "Writing code", run_command: "Preparing a command" };
49
+ const tools = {
50
+ list_files: (i) => listing(root, inside(root, i.path || "."), 3, []).join("\n") || "(empty folder)",
51
+ read_file: (i) => { const f = inside(root, i.path); if (statSync(f).size > ROOT_LIMIT) throw new Error("File is too large to read."); return readFileSync(f, "utf8"); },
52
+ async write_file(i) {
53
+ const f = inside(root, i.path, { write: true }); if (typeof i.content !== "string") throw new Error("Content is missing.");
54
+ console.log(` ${yellow("write")} ${clean(i.path)} ${dim(`(${i.content.split("\n").length} lines)`)}`);
55
+ if (!writesOk) { const a = (await ask(` ${bold("Allow?")} [y] yes [a] all writes [n] no: `))?.toLowerCase(); if (a === "a") writesOk = true; else if (a !== "y") throw new Error("The user declined this write."); }
56
+ mkdirSync(path.dirname(f), { recursive: true }); writeFileSync(f, i.content); return "Written.";
57
+ },
58
+ async run_command(i) {
59
+ if (typeof i.command !== "string" || !i.command.trim()) throw new Error("Command is missing.");
60
+ console.log(` ${yellow("run")} ${clean(i.command)}\n ${dim(clean(i.why || ""))}`);
61
+ if ((await ask(` ${bold("Run it?")} [y] yes [n] no: `))?.toLowerCase() !== "y") throw new Error("The user declined this command.");
62
+ return runCommand(i.command, root);
63
+ },
64
+ };
65
+ const messages = [];
66
+ async function turn(text) {
67
+ messages.push({ role: "user", content: text });
68
+ for (let round = 0; round < ROUNDS; round++) {
69
+ let final = null, midLine = false; spin.start();
70
+ try {
71
+ await stream("/api/build", { method: "POST", body: JSON.stringify({ messages }) }, (ev, d) => {
72
+ if (ev === "delta") { spin.stop(); const t = clean(d); process.stdout.write(t); midLine = !t.endsWith("\n"); }
73
+ else if (ev === "trial") { spin.stop(); console.log(dim(` Free trial: ${d.left} build${d.left === 1 ? "" : "s"} left after this one.`)); spin.start(); }
74
+ else if (ev === "working") { if (midLine) { process.stdout.write("\n"); midLine = false; } spin.start(STEP[d.name] || "Working"); }
75
+ else if (ev === "message") final = d; else if (ev === "error") throw new Error(d.message);
76
+ });
77
+ } finally { spin.stop(); }
78
+ if (midLine) process.stdout.write("\n");
79
+ if (!final) throw new Error("The server ended the step early.");
80
+ if (final.stop_reason !== "tool_use") { messages.push({ role: "assistant", content: final.content }); if (final.stop_reason === "max_tokens") console.log(yellow("That answer hit the length limit. Ask for a smaller step.")); return; }
81
+ messages.push({ role: "assistant", content: final.content }); const results = [];
82
+ for (const b of final.content) {
83
+ if (b.type !== "tool_use") continue;
84
+ try { const h = Object.hasOwn(tools, b.name) ? tools[b.name] : null; if (!h) throw new Error("Unknown tool."); if (b.name === "list_files" || b.name === "read_file") console.log(dim(` › ${b.name === "read_file" ? "read " + clean(b.input?.path) : "looked through the folder"}`)); const out = await h(b.input || {}); results.push({ type: "tool_result", tool_use_id: b.id, content: String(out).slice(0, OUTPUT_LIMIT * 10) }); }
85
+ catch (e) { console.log(` ${red("✕")} ${clean(e.message)}`); results.push({ type: "tool_result", tool_use_id: b.id, content: String(e.message), is_error: true }); }
86
+ }
87
+ messages.push({ role: "user", content: results });
88
+ }
89
+ console.log(yellow("Stopped after too many steps."));
90
+ }
91
+ const drawn = await banner();
92
+ console.log(`${drawn ? "" : violet(bold("CLINKZ build")) + " "}${dim(`${drawn ? "build " : ""}· ${clean(root)}`)}\n${dim("Describe what you want. It asks before every write and command. /exit to leave.")}`);
93
+ let next = task || (await ask(violet("› ")));
94
+ try {
95
+ while (next && next !== "/exit" && next !== "/quit") { await turn(`Project folder: ${path.basename(root)}. CLINKZ API base: ${api}.\n\n${next}`); console.log(green("Done.")); next = task ? null : await ask(violet("› ")); while (next === "") next = await ask(violet("› ")); }
96
+ } finally { rl.close(); }
97
+ }
package/clinkz.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ // clinkz: the CLINKZ terminal client. Plain language goes to the agent on the server; commands read the public API.
3
+ // It keeps no wallet and no private key, and it never sends a transaction. The only secret is the access code.
4
+ import { randomBytes } from "node:crypto";
5
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import path from "node:path";
8
+ import readline from "node:readline/promises";
9
+ import { banner, spinner } from "./anim.js";
10
+ import { runBuild } from "./build.js";
11
+
12
+ const DIR = path.join(homedir(), ".clinkz"), FILE = path.join(DIR, "config.json");
13
+ const DEFAULT_API = "https://clinkzai.xyz";
14
+ const color = process.stdout.isTTY && !process.env.NO_COLOR;
15
+ const paint = (code) => (s) => (color ? `\x1b[${code}m${s}\x1b[0m` : String(s));
16
+ const dim = paint(2), bold = paint(1), green = paint(32), red = paint(31), violet = paint(35), yellow = paint(33);
17
+
18
+ // Token names and alert text come from strangers. Control characters are stripped so they cannot drive the terminal.
19
+ const clean = (s) => String(s ?? "").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
20
+ const num = (v, d = 0) => Number(v || 0).toLocaleString("en-US", { maximumFractionDigits: d });
21
+ const usd = (v) => (v == null ? "—" : v >= 1e6 ? `$${(v / 1e6).toFixed(2)}M` : v >= 1e3 ? `$${(v / 1e3).toFixed(1)}k` : v >= 1 ? `$${v.toFixed(2)}` : `$${Number(v).toPrecision(3)}`);
22
+ const pct = (v) => (v == null ? "—" : `${v > 0 ? "+" : ""}${Number(v).toFixed(Math.abs(v) >= 100 ? 0 : 1)}%`);
23
+ const tint = (v) => (v == null ? dim : v >= 0 ? green : red);
24
+ const pad = (s, n) => { const t = clean(s); return t.length > n ? t.slice(0, n - 1) + "…" : t.padEnd(n); };
25
+ const die = (m) => { console.error(red(clean(m))); process.exitCode = 1; };
26
+
27
+ async function load() { try { return JSON.parse(await readFile(FILE, "utf8")); } catch { return {}; } }
28
+ async function save(c) { await mkdir(DIR, { recursive: true }); await writeFile(FILE, JSON.stringify(c, null, 1), { mode: 0o600 }); }
29
+ const cfg = await load();
30
+ const server = (url) => url.replace(/\/$/, "");
31
+ let API = server(process.env.CLINKZ_API || cfg.api || DEFAULT_API);
32
+ const headers = () => ({ "content-type": "application/json", ...(cfg.code ? { "x-clinkz-code": cfg.code, "x-clinkz-device": cfg.device } : {}) });
33
+
34
+ async function failure(r) { let m = r.statusText; try { const j = await r.json(); m = j.message || j.error || m; } catch {} return new Error(m); }
35
+ async function get(p) { const r = await fetch(API + p, { headers: headers(), signal: AbortSignal.timeout(30_000) }); if (!r.ok) throw await failure(r); return r.json(); }
36
+ async function stream(p, init, on) {
37
+ const r = await fetch(API + p, { ...init, headers: headers() }); if (!r.ok) throw await failure(r);
38
+ const dec = new TextDecoder(); let buf = "";
39
+ for await (const chunk of r.body) { buf += dec.decode(chunk, { stream: true }); let i; while ((i = buf.indexOf("\n\n")) >= 0) { const block = buf.slice(0, i); buf = buf.slice(i + 2); let ev = "message", data = null; for (const line of block.split("\n")) { if (line.startsWith("event: ")) ev = line.slice(7); else if (line.startsWith("data: ")) { try { data = JSON.parse(line.slice(6)); } catch {} } } if (data !== null) on(ev, data); } }
40
+ }
41
+
42
+ const LABEL = { discover: "reading the feed", search: "searching", get_pair: "reading the pair", alerts: "reading signals", track_record: "reading the record", scam_check: "running the scam check", forecast: "writing a forecast" };
43
+ const STATUS = { ok: green("●"), warn: yellow("●"), bad: red("●"), unknown: dim("●") };
44
+ const printStep = (s) => console.log(` ${STATUS[s.status] || dim("●")} ${bold(clean(s.title))} ${dim(clean(s.detail))}`);
45
+
46
+ async function ask(history) {
47
+ let text = "", midLine = false; const spin = spinner(); spin.start();
48
+ const line = (s) => { spin.stop(); if (midLine) { process.stdout.write("\n"); midLine = false; } console.log(s); };
49
+ await stream("/api/agent", { method: "POST", body: JSON.stringify({ messages: history.slice(-12) }) }, (ev, d) => {
50
+ if (ev === "delta") { spin.stop(); const t = clean(d); text += t; process.stdout.write(t); midLine = !t.endsWith("\n"); }
51
+ else if (ev === "tool") { line(dim(` › ${LABEL[d.name] || "working"}…`)); spin.start(); }
52
+ else if (ev === "step") { spin.stop(); if (midLine) line(""); printStep(d); spin.start(); }
53
+ else if (ev === "error") line(red(clean(d.message)));
54
+ }).finally(() => spin.stop());
55
+ if (midLine) process.stdout.write("\n");
56
+ return text;
57
+ }
58
+ async function repl() {
59
+ if (!cfg.code) return die("Not logged in. Get a code on the CLINKZ dashboard, then run: clinkz login <code>");
60
+ const drawn = await banner();
61
+ console.log(`${drawn ? "" : violet(bold("CLINKZ")) + " "}${dim("terminal · type a question, /exit to leave. CLINKZ does not give financial advice.")}`);
62
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const history = [];
63
+ for (;;) {
64
+ let q; try { q = (await rl.question(violet("› "))).trim(); } catch { break; }
65
+ if (!q) continue; if (q === "/exit" || q === "/quit") break; if (q === "/clear") { history.length = 0; console.log(dim("Cleared.")); continue; }
66
+ history.push({ role: "user", content: q });
67
+ try { const a = await ask(history); if (a) history.push({ role: "assistant", content: a }); else history.pop(); } catch (e) { history.pop(); die(e.message); }
68
+ }
69
+ rl.close();
70
+ }
71
+
72
+ const chainOf = (f) => (f.chain === "all" ? "" : f.chain || "robinhood");
73
+ const flags = (args) => { const f = {}, rest = []; for (let i = 0; i < args.length; i++) { const m = args[i].match(/^--(\w+)$/); if (m) f[m[1]] = args[++i]; else rest.push(args[i]); } return { f, rest }; };
74
+ const row = (p) => `${pad(p.base.symbol, 12)} ${dim(p.chain === "solana" ? "SOL" : "RH ")} ${usd(p.priceUsd).padStart(10)} ${tint(p.change.h1)(pct(p.change.h1).padStart(8))} ${usd(p.vol.h1).padStart(9)} ${usd(p.liq).padStart(9)} ${dim(clean(p.pair))}`;
75
+ const head = () => console.log(dim(`${"TOKEN".padEnd(12)} CH ${"PRICE".padStart(10)} ${"1H".padStart(8)} ${"VOL 1H".padStart(9)} ${"LIQ".padStart(9)} PAIR`));
76
+ const alert = (a) => console.log(`${(a.severity === "high" ? red : a.severity === "mid" ? yellow : dim)("●")} ${bold(clean(a.title))} ${clean(a.symbol || "")} ${dim(clean(a.detail))}`);
77
+ const need = (cond, usage) => { if (!cond) throw new Error("Usage: " + usage); };
78
+
79
+ const COMMANDS = {
80
+ async login(args) {
81
+ const { f, rest } = flags(args); need(rest[0], "clinkz login <code> [--api <url>]");
82
+ const next = { ...cfg, code: rest[0].trim(), device: cfg.device || randomBytes(16).toString("hex"), ...(f.api ? { api: f.api } : {}) }; Object.assign(cfg, next); if (f.api) API = server(f.api);
83
+ await save(next); const me = await get("/api/access/me"); if (!me.wallet) { delete next.code; await save(next); throw new Error("That code was not accepted."); }
84
+ console.log(green(`Logged in as ${clean(me.short || me.wallet)} · ${clean(me.tier)} · ${me.quota - me.used} analyst calls left today.`));
85
+ },
86
+ async logout() { await rm(FILE, { force: true }); console.log("Logged out."); },
87
+ async whoami() { const me = await get("/api/access/me"); console.log(me.wallet ? `${clean(me.short || me.wallet)} · ${clean(me.tier)} · ${me.used}/${me.quota} analyst calls today · ${API}` : `Not logged in · ${API}`); },
88
+ async discover(args) { const { f } = flags(args); const q = new URLSearchParams({ tab: f.tab || "trending", limit: f.limit || "15", minLiq: f.minliq || "5000", chain: chainOf(f) }); const r = await get(`/api/discover?${q}`); head(); r.pairs.forEach((p) => console.log(row(p))); },
89
+ async search(args) { need(args[0], "clinkz search <text>"); const r = await get(`/api/search?q=${encodeURIComponent(args.join(" "))}`); head(); r.pairs.forEach((p) => console.log(row(p))); },
90
+ async pair([chain, pair]) { need(chain && pair, "clinkz pair <chain> <pair>"); const r = await get(`/api/pair/${encodeURIComponent(chain)}/${encodeURIComponent(pair)}`); head(); console.log(row(r.pair)); if (r.verdict) console.log(`Verdict: ${bold(clean(r.verdict.verdict))} ${r.verdict.score}/100 ${dim(clean(r.verdict.summary))}`); r.signals.slice(0, 5).forEach(alert); },
91
+ async alerts(args) { const { f } = flags(args); const r = await get(`/api/alerts?limit=${encodeURIComponent(f.limit || 15)}&chain=${chainOf(f)}`); r.alerts.reverse().forEach(alert); },
92
+ async watch(args) { const { f } = flags(args); console.log(dim("Watching signals. Ctrl+C to stop.")); await stream(`/api/stream?chain=${chainOf(f)}`, {}, (ev, d) => { if (ev === "alert") alert(d); }); },
93
+ async build(args) { if (!cfg.code) throw new Error("Not logged in. Get a code on the CLINKZ dashboard, then run: clinkz login <code>"); await runBuild({ api: API, stream, clean, ui: { dim, bold, green, red, violet, yellow }, task: args.join(" ") }); },
94
+ async record() { const { data: r } = await get("/api/v1/record"); console.log(`Forecasts ${num(r.forecasts)} · resolved ${num(r.resolved)} · direction hit ${r.directionHitRate ?? "—"}% · range hit ${r.rangeHitRate ?? "—"}% · verdicts ${num(r.verdicts)}`); },
95
+ async scan([chain, pair]) {
96
+ need(chain && pair, "clinkz scan <chain> <pair>");
97
+ await stream(`/api/scan/${encodeURIComponent(chain)}/${encodeURIComponent(pair)}`, {}, (ev, d) => {
98
+ if (ev === "step") printStep(d); else if (ev === "weighing") console.log(dim(" › weighing the evidence…")); else if (ev === "error") die(d.message);
99
+ else if (ev === "verdict") { console.log(`\n${bold(clean(d.verdict).toUpperCase())} ${d.score}/100 · ${clean(d.confidence)} confidence\n${clean(d.summary)}`); (d.flags || []).forEach((x) => console.log(` ${x.severity === "high" ? red("!") : yellow("!")} ${clean(x.label)}: ${dim(clean(x.evidence))}`)); if (d.missing?.length) console.log(dim(`Missing: ${d.missing.map(clean).join(" · ")}`)); }
100
+ });
101
+ },
102
+ async forecast([chain, pair, horizon]) { need(chain && pair, "clinkz forecast <chain> <pair> [1h|4h|24h]"); const r = await fetch(`${API}/api/forecast/${encodeURIComponent(chain)}/${encodeURIComponent(pair)}`, { method: "POST", headers: headers(), body: JSON.stringify({ horizon }) }); if (!r.ok) throw await failure(r); const { forecast: f } = await r.json(); console.log(`${bold(clean(f.direction).toUpperCase())} ${clean(f.horizon)} · ${f.rangePct[0]}% to ${f.rangePct[1]}% · ${clean(f.confidence)} confidence\n${clean(f.summary)}\n${dim("Invalidation: " + clean(f.invalidation))}`); },
103
+ help() {
104
+ console.log(`${violet(bold("clinkz"))} ${dim("· CLINKZ in your terminal")}
105
+
106
+ clinkz chat with CLINKZ
107
+ clinkz "new Solana tokens?" ask one question
108
+ clinkz login <code> log in with a code from the dashboard
109
+ clinkz build ["what to make"] build a tool in this folder, no coding needed
110
+ clinkz discover live feed ${dim("--chain solana|all --tab --limit --minliq")}
111
+ clinkz search <text> find a token
112
+ clinkz pair <chain> <pair> read one pair
113
+ clinkz scan <chain> <pair> scam check
114
+ clinkz forecast <chain> <pair> [1h|4h|24h]
115
+ clinkz alerts latest signals ${dim("--chain solana|all --limit")}
116
+ clinkz watch stream signals live
117
+ clinkz record public track record
118
+ clinkz whoami | logout
119
+
120
+ ${dim(`Server: ${API} (set CLINKZ_API to change it). CLINKZ never sees your keys and never sends a transaction.`)}`);
121
+ },
122
+ };
123
+
124
+ const [cmd, ...args] = process.argv.slice(2);
125
+ try {
126
+ if (!cmd) await repl();
127
+ else if (cmd === "--help" || cmd === "-h") COMMANDS.help();
128
+ else if (Object.hasOwn(COMMANDS, cmd)) await COMMANDS[cmd](args);
129
+ else { if (!cfg.code) throw new Error("Not logged in. Get a code on the CLINKZ dashboard, then run: clinkz login <code>"); await ask([{ role: "user", content: [cmd, ...args].join(" ") }]); }
130
+ } catch (e) { die(e.message); }
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "clinkz-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.3",
4
+ "description": "CLINKZ in your terminal: live signals, scam checks and forecasts for Solana and Robinhood Chain, in plain language.",
5
+ "type": "module",
6
+ "bin": { "clinkz": "clinkz.js" },
7
+ "files": ["clinkz.js", "build.js", "anim.js"],
8
+ "engines": { "node": ">=20" },
9
+ "license": "MIT"
10
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.