clawdos-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/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # clawdos-cli
2
+
3
+ **ClawdOS 2.0** — the agent OS that watches Base, from your terminal.
4
+
5
+ ```
6
+ ██████╗ ██╗ █████╗ ██╗ ██╗██████╗ ██████╗ ███████╗
7
+ ██╔════╝ ██║ ██╔══██╗██║ ██║██╔══██╗██╔═══██╗██╔════╝
8
+ ██║ ██║ ███████║██║ █╗ ██║██║ ██║██║ ██║███████╗
9
+ ██║ ██║ ██╔══██║██║███╗██║██║ ██║██║ ██║╚════██║
10
+ ╚██████╗ ███████╗██║ ██║╚███╔███╔╝██████╔╝╚██████╔╝███████║
11
+ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═════╝ ╚═════╝ ╚══════╝
12
+ 2.0
13
+ ```
14
+
15
+ Token prices, wallet stats, trending launches, gas, TVL — natural
16
+ language or slash commands, no auth needed for basic use.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install -g clawdos-cli
22
+ ```
23
+
24
+ Then:
25
+
26
+ ```bash
27
+ clawd
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ | Command | What it does |
33
+ | ----------------- | ------------------------------------------------- |
34
+ | `/price <token>` | Token price + 24h change + mcap + liquidity (Dexscreener) |
35
+ | `/wallet <addr>` | ETH balance + EOA/contract status on Base |
36
+ | `/gas` | Base gas price + cost estimates in USD |
37
+ | `/trending` | Boosted / trending Base tokens |
38
+ | `/new` | Newest Base token launches (last 24h) |
39
+ | `/tvl` | Base chain TVL + top protocols (DefiLlama) |
40
+ | `/scan <addr>` | Quick on-chain summary (wallet or token) |
41
+ | `/login <fal-key>`| Set fal.ai key to enable natural-language mode |
42
+ | `/whoami` | Show your session config |
43
+ | `/clear` | Clear terminal |
44
+ | `/help` | All commands |
45
+ | `/exit` | Quit (or Ctrl+D) |
46
+
47
+ ## Natural language (optional)
48
+
49
+ Set a [fal.ai](https://fal.ai) API key to ask questions in plain English:
50
+
51
+ ```
52
+ /login <your-fal-key>
53
+ ```
54
+
55
+ Then:
56
+
57
+ ```
58
+ ▸ what is the price of degen on base
59
+ ▸ show me wallet 0xabc...
60
+ ▸ is gas expensive right now
61
+ ▸ what just launched on base in the last day
62
+ ▸ tvl
63
+ ```
64
+
65
+ Routes through claude-sonnet-4.5 to classify intent and dispatch to
66
+ the right slash command.
67
+
68
+ ## Privacy + cost
69
+
70
+ - All data fetched from public APIs (Dexscreener, DeFi Llama, Base RPC,
71
+ CoinGecko). No auth or sign-in for the core commands.
72
+ - The optional natural-language mode calls fal.ai with your own key —
73
+ ClawdOS doesn't see or store your key anywhere besides
74
+ `~/.config/clawdos/config.json` on your machine.
75
+ - Free except for fal.ai's own per-token billing if you enable NL mode.
76
+
77
+ ## License
78
+
79
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,696 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config.ts
4
+ import { homedir } from "os";
5
+ import { join, dirname } from "path";
6
+ import { mkdir, readFile, writeFile, chmod } from "fs/promises";
7
+ var CONFIG_DIR = process.platform === "win32" ? join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "clawdos") : join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "clawdos");
8
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
9
+ async function loadConfig() {
10
+ try {
11
+ const raw = await readFile(CONFIG_PATH, "utf-8");
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ async function saveConfig(cfg) {
18
+ await mkdir(dirname(CONFIG_PATH), { recursive: true });
19
+ await writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf-8");
20
+ if (process.platform !== "win32") {
21
+ try {
22
+ await chmod(CONFIG_PATH, 384);
23
+ } catch {
24
+ }
25
+ }
26
+ }
27
+
28
+ // src/repl.ts
29
+ import { createInterface } from "readline/promises";
30
+
31
+ // src/commands.ts
32
+ import { createPublicClient, http, formatEther } from "viem";
33
+ import { base } from "viem/chains";
34
+
35
+ // src/theme.ts
36
+ import chalk from "chalk";
37
+ var cyan = chalk.hex("#22d3ee");
38
+ var cyanSoft = chalk.hex("#67e8f9");
39
+ var cyanDeep = chalk.hex("#0891b2");
40
+ var violet = chalk.hex("#a78bfa");
41
+ var violetSoft = chalk.hex("#c4b5fd");
42
+ var ink = chalk.hex("#71717a");
43
+ var inkDim = chalk.hex("#52525b");
44
+ var marble = chalk.hex("#e4e4e7");
45
+ var ok = chalk.hex("#34d399");
46
+ var warn = chalk.hex("#fbbf24");
47
+ var err = chalk.hex("#f87171");
48
+ var up = chalk.hex("#22c55e");
49
+ var down = chalk.hex("#ef4444");
50
+ function clawdGradient(text) {
51
+ const lines = text.split("\n");
52
+ const shades = [
53
+ "#22d3ee",
54
+ "#22d3ee",
55
+ "#06b6d4",
56
+ "#0ea5e9",
57
+ "#3b82f6",
58
+ "#6366f1",
59
+ "#8b5cf6",
60
+ "#a78bfa"
61
+ ];
62
+ return lines.map((line, i) => chalk.hex(shades[Math.min(i, shades.length - 1)])(line)).join("\n");
63
+ }
64
+ var dim = chalk.dim;
65
+ var bold = chalk.bold;
66
+
67
+ // src/render.ts
68
+ var MAX_CELL = 36;
69
+ function renderTable(rows, columns) {
70
+ if (rows.length === 0) return inkDim(" (no rows)");
71
+ const cols = columns ?? Object.keys(rows[0]);
72
+ const widths = {};
73
+ for (const c of cols) {
74
+ widths[c] = Math.min(MAX_CELL, Math.max(c.length, ...rows.map((r) => fmtCell(r[c]).length)));
75
+ }
76
+ const top = "\u250C" + cols.map((c) => "\u2500".repeat(widths[c] + 2)).join("\u252C") + "\u2510";
77
+ const mid = "\u251C" + cols.map((c) => "\u2500".repeat(widths[c] + 2)).join("\u253C") + "\u2524";
78
+ const bot = "\u2514" + cols.map((c) => "\u2500".repeat(widths[c] + 2)).join("\u2534") + "\u2518";
79
+ const head = "\u2502" + cols.map((c) => ` ${cyanSoft(c.padEnd(widths[c]))} `).join("\u2502") + "\u2502";
80
+ const body = rows.map(
81
+ (r) => "\u2502" + cols.map((c) => {
82
+ const raw = r[c];
83
+ const v = fmtCell(raw);
84
+ const truncated = v.length > MAX_CELL;
85
+ const padded = (truncated ? v.slice(0, MAX_CELL - 1) + "\u2026" : v).padEnd(widths[c]);
86
+ return ` ${colorize(c, raw, padded)} `;
87
+ }).join("\u2502") + "\u2502"
88
+ ).join("\n");
89
+ return `${ink(top)}
90
+ ${head}
91
+ ${ink(mid)}
92
+ ${body}
93
+ ${ink(bot)}`;
94
+ }
95
+ function fmtCell(v) {
96
+ if (v === null || v === void 0) return "\u2014";
97
+ if (typeof v === "object") return JSON.stringify(v);
98
+ return String(v);
99
+ }
100
+ function colorize(col, raw, padded) {
101
+ if (typeof raw === "number" && /change|24h|delta|pct|%/i.test(col)) {
102
+ return raw > 0 ? up(padded) : raw < 0 ? down(padded) : marble(padded);
103
+ }
104
+ return marble(padded);
105
+ }
106
+ function fmtUsd(n) {
107
+ if (n === 0) return "$0";
108
+ const abs = Math.abs(n);
109
+ if (abs < 1e-4) {
110
+ const exp = Math.floor(Math.log10(abs));
111
+ const mantissa = abs / Math.pow(10, exp);
112
+ return (n < 0 ? "-$" : "$") + mantissa.toFixed(3) + "e" + exp;
113
+ }
114
+ if (abs < 1) return (n < 0 ? "-$" : "$") + abs.toFixed(6);
115
+ if (abs < 100) return (n < 0 ? "-$" : "$") + abs.toFixed(4);
116
+ if (abs < 1e3) return (n < 0 ? "-$" : "$") + abs.toFixed(2);
117
+ if (abs < 1e6) return (n < 0 ? "-$" : "$") + (abs / 1e3).toFixed(2) + "K";
118
+ if (abs < 1e9) return (n < 0 ? "-$" : "$") + (abs / 1e6).toFixed(2) + "M";
119
+ return (n < 0 ? "-$" : "$") + (abs / 1e9).toFixed(2) + "B";
120
+ }
121
+ function fmtPct(n) {
122
+ const sign = n > 0 ? "+" : "";
123
+ return `${sign}${n.toFixed(2)}%`;
124
+ }
125
+ function fmtPctColored(n) {
126
+ const s = fmtPct(n);
127
+ return n > 0 ? up(s) : n < 0 ? down(s) : marble(s);
128
+ }
129
+ function fmtAddr(a) {
130
+ if (!a) return "\u2014";
131
+ return a.slice(0, 6) + "\u2026" + a.slice(-4);
132
+ }
133
+ var SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
134
+ function sparkline(values) {
135
+ if (values.length === 0) return "";
136
+ const min = Math.min(...values);
137
+ const max = Math.max(...values);
138
+ const range = max - min || 1;
139
+ return values.map((v) => SPARK[Math.min(7, Math.floor((v - min) / range * 8))]).join("");
140
+ }
141
+ function sectionTitle(title) {
142
+ return ` ${cyan("\u25B8")} ${bold(marble(title))}`;
143
+ }
144
+ function renderHelp() {
145
+ const tip = (k, v) => ` ${cyan(k.padEnd(20))} ${ink(v)}`;
146
+ return [
147
+ "",
148
+ ` ${cyanSoft("commands")}`,
149
+ tip("/price <token>", "token price + 24h + mcap + liquidity"),
150
+ tip("/wallet <addr>", "ETH balance + top token holdings on Base"),
151
+ tip("/gas", "current Base gas price + congestion"),
152
+ tip("/trending", "top trending tokens on Base (volume 24h)"),
153
+ tip("/new", "newest Base token launches (last 24h)"),
154
+ tip("/tvl", "Base chain TVL + top protocols"),
155
+ tip("/scan <addr>", "quick on-chain summary of an address"),
156
+ tip("/clear", "clear terminal"),
157
+ tip("/help", "this list"),
158
+ tip("/exit", "quit (Ctrl+D)"),
159
+ "",
160
+ ` ${cyanSoft("natural language")}`,
161
+ ` ${ink("\u2192 if you set a fal.ai key with /login, anything else is treated")}`,
162
+ ` ${ink(" as a question and routed to the right command")}`,
163
+ ` ${dim(' examples: "DEGEN price?", "what is base gas right now"')}`,
164
+ ""
165
+ ].join("\n");
166
+ }
167
+
168
+ // src/commands.ts
169
+ var basePublic = createPublicClient({ chain: base, transport: http() });
170
+ async function cmdPrice(query) {
171
+ if (!query.trim()) return ` ${err("\u2717")} usage: /price <token-symbol-or-address>`;
172
+ const r = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(query.trim())}`);
173
+ if (!r.ok) return ` ${err("\u2717")} dexscreener error: ${r.status}`;
174
+ const data = await r.json();
175
+ const pairs = (data.pairs ?? []).filter((p) => p.chainId === "base").sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0)).slice(0, 5);
176
+ if (pairs.length === 0) {
177
+ const any = (data.pairs ?? []).slice(0, 3);
178
+ if (any.length === 0) return ` ${err("\u2717")} no token found matching "${query}"`;
179
+ return sectionTitle(`No match on Base \u2014 top results across all chains:`) + "\n\n" + renderTable(any.map((p) => pairRow(p)), ["symbol", "chain", "price", "24h", "mcap", "liq", "vol_24h"]);
180
+ }
181
+ return sectionTitle(`Base \xB7 "${query}" \u2192 top by liquidity`) + "\n\n" + renderTable(pairs.map((p) => pairRow(p)), ["symbol", "name", "price", "24h", "mcap", "liq", "vol_24h", "address"]);
182
+ }
183
+ function pairRow(p) {
184
+ const sym = p.baseToken?.symbol ?? "?";
185
+ const name = p.baseToken?.name?.slice(0, 18) ?? "?";
186
+ const price = p.priceUsd ? parseFloat(p.priceUsd) : 0;
187
+ const ch24 = p.priceChange?.h24 ?? 0;
188
+ const mcap = p.marketCap ?? p.fdv ?? 0;
189
+ const liq = p.liquidity?.usd ?? 0;
190
+ const v24 = p.volume?.h24 ?? 0;
191
+ return {
192
+ symbol: sym,
193
+ name,
194
+ chain: p.chainId,
195
+ price: fmtUsd(price),
196
+ "24h": ch24,
197
+ // raw number → colorized by render
198
+ mcap: fmtUsd(mcap),
199
+ liq: fmtUsd(liq),
200
+ vol_24h: fmtUsd(v24),
201
+ address: fmtAddr(p.baseToken?.address ?? "")
202
+ };
203
+ }
204
+ async function cmdWallet(addr) {
205
+ if (!/^0x[a-fA-F0-9]{40}$/.test(addr.trim())) {
206
+ return ` ${err("\u2717")} usage: /wallet <0x...>`;
207
+ }
208
+ const a = addr.trim();
209
+ const [bal, code] = await Promise.all([
210
+ basePublic.getBalance({ address: a }),
211
+ basePublic.getBytecode({ address: a }).catch(() => void 0)
212
+ ]);
213
+ const isContract = !!code && code !== "0x";
214
+ const out = [
215
+ "",
216
+ sectionTitle(`Wallet \xB7 ${fmtAddr(a)}`),
217
+ ` ${ink("address ")} ${marble(a)}`,
218
+ ` ${ink("type ")} ${isContract ? violet("smart contract") : cyanSoft("EOA")}`,
219
+ ` ${ink("eth balance ")} ${marble(formatEther(bal))} ${cyan("ETH")} ${inkDim(`(~${fmtUsd(parseFloat(formatEther(bal)) * 3500)} at ~$3500/ETH)`)}`
220
+ ];
221
+ out.push("");
222
+ out.push(` ${inkDim("(token holdings coming in v0.2 \u2014 needs Basescan API key)")}`);
223
+ return out.join("\n");
224
+ }
225
+ async function cmdGas() {
226
+ const [gasPrice, blockNumber, ethPrice] = await Promise.all([
227
+ basePublic.getGasPrice(),
228
+ basePublic.getBlockNumber(),
229
+ fetchEthPriceUsd()
230
+ ]);
231
+ const gwei = Number(gasPrice) / 1e9;
232
+ const verdict = gwei < 0.05 ? `${ok("calm")} \u2014 basically free` : gwei < 0.5 ? `${ok("normal")} \u2014 comfortable` : gwei < 2 ? `${cyanSoft("busy")} \u2014 pay attention` : `${err("congested")} \u2014 wait a few minutes`;
233
+ const transferEth = Number(gasPrice) * 21e3 / 1e18;
234
+ const swapEth = Number(gasPrice) * 15e4 / 1e18;
235
+ const deployEth = Number(gasPrice) * 15e5 / 1e18;
236
+ return [
237
+ "",
238
+ sectionTitle("Base \xB7 gas"),
239
+ ` ${ink("gas price ")} ${marble(gwei.toFixed(6))} ${cyan("gwei")}`,
240
+ ` ${ink("verdict ")} ${verdict}`,
241
+ ` ${ink("block ")} ${marble("#" + blockNumber.toString())}`,
242
+ ` ${ink("eth price ")} ${marble(fmtUsd(ethPrice))}`,
243
+ "",
244
+ ` ${ink("cost estimates")}`,
245
+ ` ${inkDim("ETH transfer (21k gas) ")} ${marble(transferEth.toFixed(8) + " ETH")} ${inkDim("\u2248")} ${cyanSoft(fmtUsd(transferEth * ethPrice))}`,
246
+ ` ${inkDim("Uniswap swap (150k gas) ")} ${marble(swapEth.toFixed(8) + " ETH")} ${inkDim("\u2248")} ${cyanSoft(fmtUsd(swapEth * ethPrice))}`,
247
+ ` ${inkDim("Contract deploy (1.5M gas) ")} ${marble(deployEth.toFixed(8) + " ETH")} ${inkDim("\u2248")} ${cyanSoft(fmtUsd(deployEth * ethPrice))}`,
248
+ ""
249
+ ].join("\n");
250
+ }
251
+ async function fetchEthPriceUsd() {
252
+ try {
253
+ const r = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd");
254
+ const d = await r.json();
255
+ return d.ethereum?.usd ?? 3500;
256
+ } catch {
257
+ return 3500;
258
+ }
259
+ }
260
+ async function cmdTrending() {
261
+ const r = await fetch(`https://api.dexscreener.com/token-boosts/top/v1`);
262
+ if (!r.ok) return ` ${err("\u2717")} dexscreener boosts error: ${r.status}`;
263
+ const data = await r.json();
264
+ const baseBoosts = data.filter((b) => b.chainId === "base").slice(0, 10);
265
+ if (baseBoosts.length === 0) return ` ${err("\u2717")} no boosted Base tokens right now`;
266
+ const enriched = await Promise.all(baseBoosts.map(async (b) => {
267
+ try {
268
+ const r2 = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${b.tokenAddress}`);
269
+ const d2 = await r2.json();
270
+ const p = (d2.pairs ?? []).filter((p2) => p2.chainId === "base").sort((a, b2) => (b2.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];
271
+ if (!p) return null;
272
+ return pairRow(p);
273
+ } catch {
274
+ return null;
275
+ }
276
+ }));
277
+ const rows = enriched.filter((x) => !!x);
278
+ if (rows.length === 0) return ` ${err("\u2717")} no enriched data`;
279
+ return sectionTitle("Base \xB7 trending (boosted on dexscreener)") + "\n\n" + renderTable(rows, ["symbol", "name", "price", "24h", "mcap", "liq", "vol_24h"]);
280
+ }
281
+ async function cmdNew() {
282
+ const r = await fetch(`https://api.dexscreener.com/token-profiles/latest/v1`);
283
+ if (!r.ok) return ` ${err("\u2717")} dexscreener latest error: ${r.status}`;
284
+ const data = await r.json();
285
+ const baseLatest = data.filter((t) => t.chainId === "base").slice(0, 10);
286
+ if (baseLatest.length === 0) return ` ${err("\u2717")} no recent Base profiles`;
287
+ const rows = await Promise.all(baseLatest.map(async (t) => {
288
+ try {
289
+ const r2 = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${t.tokenAddress}`);
290
+ const d2 = await r2.json();
291
+ const p = (d2.pairs ?? []).filter((p2) => p2.chainId === "base").sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];
292
+ if (!p) return null;
293
+ return pairRow(p);
294
+ } catch {
295
+ return null;
296
+ }
297
+ }));
298
+ const filtered = rows.filter((x) => !!x);
299
+ return sectionTitle("Base \xB7 newest token profiles") + "\n\n" + renderTable(filtered, ["symbol", "name", "price", "24h", "mcap", "liq", "vol_24h"]);
300
+ }
301
+ async function cmdTvl() {
302
+ const [hist, protos] = await Promise.all([
303
+ fetch("https://api.llama.fi/v2/historicalChainTvl/Base").then((r) => r.json()),
304
+ fetch("https://api.llama.fi/protocols").then((r) => r.json())
305
+ ]);
306
+ const recent = hist.slice(-30);
307
+ const current = recent[recent.length - 1].tvl;
308
+ const wkAgo = recent[recent.length - 8]?.tvl ?? current;
309
+ const monAgo = recent[0]?.tvl ?? current;
310
+ const wkPct = (current - wkAgo) / wkAgo * 100;
311
+ const monPct = (current - monAgo) / monAgo * 100;
312
+ const spark = sparkline(recent.map((d) => d.tvl));
313
+ const baseProtos = protos.filter((p) => p.chainTvls && p.chainTvls["Base"]).sort((a, b) => (b.chainTvls["Base"] ?? 0) - (a.chainTvls["Base"] ?? 0)).slice(0, 10);
314
+ const out = [
315
+ "",
316
+ sectionTitle("Base \xB7 TVL"),
317
+ ` ${ink("now ")} ${cyanSoft(fmtUsd(current))}`,
318
+ ` ${ink("7d change ")} ${fmtPctColored(wkPct)}`,
319
+ ` ${ink("30d change ")} ${fmtPctColored(monPct)}`,
320
+ ` ${ink("30d trend ")} ${cyan(spark)}`,
321
+ "",
322
+ sectionTitle("Top protocols by Base TVL"),
323
+ "",
324
+ renderTable(
325
+ baseProtos.map((p) => ({
326
+ protocol: p.name,
327
+ tvl: fmtUsd(p.chainTvls["Base"])
328
+ })),
329
+ ["protocol", "tvl"]
330
+ )
331
+ ];
332
+ return out.join("\n");
333
+ }
334
+ async function cmdScan(addr) {
335
+ if (!/^0x[a-fA-F0-9]{40}$/.test(addr.trim())) {
336
+ return ` ${err("\u2717")} usage: /scan <0x...>`;
337
+ }
338
+ const a = addr.trim();
339
+ const [bal, code, txCount] = await Promise.all([
340
+ basePublic.getBalance({ address: a }),
341
+ basePublic.getBytecode({ address: a }).catch(() => void 0),
342
+ basePublic.getTransactionCount({ address: a })
343
+ ]);
344
+ const isContract = !!code && code !== "0x";
345
+ let tokenInfo = null;
346
+ try {
347
+ const r = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${a}`);
348
+ const d = await r.json();
349
+ const p = (d.pairs ?? []).filter((p2) => p2.chainId === "base").sort((x, y) => (y.liquidity?.usd ?? 0) - (x.liquidity?.usd ?? 0))[0];
350
+ if (p) {
351
+ const ch24 = p.priceChange?.h24 ?? 0;
352
+ tokenInfo = `
353
+ ${ink("\u2500\u2500\u2500\u2500 token data \u2500\u2500\u2500\u2500")}
354
+ ${ink("symbol ")} ${cyan(p.baseToken?.symbol ?? "?")}
355
+ ${ink("name ")} ${marble(p.baseToken?.name ?? "?")}
356
+ ${ink("price ")} ${cyanSoft(fmtUsd(parseFloat(p.priceUsd ?? "0")))}
357
+ ${ink("24h ")} ${fmtPctColored(ch24)}
358
+ ${ink("mcap ")} ${marble(fmtUsd(p.marketCap ?? p.fdv ?? 0))}
359
+ ${ink("liquidity ")} ${marble(fmtUsd(p.liquidity?.usd ?? 0))}`;
360
+ }
361
+ } catch {
362
+ }
363
+ return [
364
+ "",
365
+ sectionTitle(`Scan \xB7 ${fmtAddr(a)}`),
366
+ ` ${ink("address ")} ${marble(a)}`,
367
+ ` ${ink("type ")} ${isContract ? violet("smart contract") : cyanSoft("EOA")}`,
368
+ ` ${ink("eth balance ")} ${marble(formatEther(bal))} ${cyan("ETH")}`,
369
+ ` ${ink("tx count ")} ${marble(txCount.toString())}`,
370
+ tokenInfo ?? "",
371
+ "",
372
+ ` ${inkDim("basescan: https://basescan.org/address/" + a)}`,
373
+ ""
374
+ ].join("\n");
375
+ }
376
+
377
+ // src/llm.ts
378
+ var FAL_URL = "https://fal.run/openrouter/router";
379
+ var MODEL = "anthropic/claude-sonnet-4.5";
380
+ var SYSTEM = `You are an intent classifier for ClawdOS \u2014 a terminal CLI that answers Base blockchain ecosystem questions. Map the user's natural-language request to ONE of these commands:
381
+
382
+ price \u2014 token price / market cap / 24h change lookup
383
+ wallet \u2014 show ETH balance and info for a wallet address
384
+ scan \u2014 quick on-chain summary of any address (wallet or contract)
385
+ gas \u2014 current Base gas price + cost estimates
386
+ trending \u2014 top trending Base tokens by volume / boosts
387
+ new \u2014 newest token launches on Base in the last 24h
388
+ tvl \u2014 Base chain total value locked + top protocols
389
+ unknown \u2014 none of the above
390
+
391
+ Reply with VALID JSON ONLY, no markdown, no commentary:
392
+
393
+ {"command": "<one of the above>", "args": "<single argument string, or empty>"}
394
+
395
+ Argument extraction:
396
+ - For 'price': the token symbol or address (e.g. "DEGEN", "0xabc...")
397
+ - For 'wallet' or 'scan': the 0x... address
398
+ - For 'gas', 'trending', 'new', 'tvl': empty string
399
+ - For 'unknown': empty string
400
+
401
+ Examples:
402
+ user: "what is the price of $DEGEN" \u2192 {"command":"price","args":"DEGEN"}
403
+ user: "show me wallet 0xabc..." \u2192 {"command":"wallet","args":"0xabc..."}
404
+ user: "is gas expensive right now" \u2192 {"command":"gas","args":""}
405
+ user: "what just launched on base" \u2192 {"command":"new","args":""}
406
+ user: "tvl" \u2192 {"command":"tvl","args":""}
407
+ user: "tell me a joke" \u2192 {"command":"unknown","args":""}
408
+ `;
409
+ async function classifyIntent(prompt, falKey) {
410
+ try {
411
+ const controller = new AbortController();
412
+ const t = setTimeout(() => controller.abort(), 8e3);
413
+ const r = await fetch(FAL_URL, {
414
+ method: "POST",
415
+ headers: {
416
+ "content-type": "application/json",
417
+ "authorization": `Key ${falKey}`
418
+ },
419
+ body: JSON.stringify({
420
+ prompt: `${SYSTEM}
421
+
422
+ User request: ${prompt}
423
+
424
+ JSON:`,
425
+ model: MODEL,
426
+ temperature: 0.1
427
+ }),
428
+ signal: controller.signal
429
+ });
430
+ clearTimeout(t);
431
+ if (!r.ok) return { command: "unknown", args: "" };
432
+ const j = await r.json();
433
+ const raw = (j.output ?? j.text ?? j.choices?.[0]?.message?.content ?? "").trim();
434
+ const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
435
+ const parsed = JSON.parse(cleaned);
436
+ return {
437
+ command: parsed.command ?? "unknown",
438
+ args: String(parsed.args ?? "")
439
+ };
440
+ } catch {
441
+ return { command: "unknown", args: "" };
442
+ }
443
+ }
444
+
445
+ // src/banner.ts
446
+ var CLAWDOS_ASCII = String.raw`
447
+ ██████╗ ██╗ █████╗ ██╗ ██╗██████╗ ██████╗ ███████╗
448
+ ██╔════╝ ██║ ██╔══██╗██║ ██║██╔══██╗██╔═══██╗██╔════╝
449
+ ██║ ██║ ███████║██║ █╗ ██║██║ ██║██║ ██║███████╗
450
+ ██║ ██║ ██╔══██║██║███╗██║██║ ██║██║ ██║╚════██║
451
+ ╚██████╗ ███████╗██║ ██║╚███╔███╔╝██████╔╝╚██████╔╝███████║
452
+ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═════╝ ╚═════╝ ╚══════╝
453
+ ╔═╗
454
+ ╚═╝ 2.0
455
+ `;
456
+ var CLAW = String.raw`
457
+ ▄▄▄▄▄
458
+ ▄█▀░░░▀█▄
459
+ █▀░░▄▄▄░░▀█
460
+ █░░██████░░█
461
+ █░░██▀▀██░░█
462
+ ▀█▄░░░░▄█▀
463
+ ▀█████▀
464
+ ▀▀▀
465
+ `;
466
+ function renderBanner(handle) {
467
+ const w = 64;
468
+ const sep = ink("\u2500".repeat(w));
469
+ const ascii = clawdGradient(CLAWDOS_ASCII);
470
+ const tag = cyanSoft(" the agent OS that watches Base.");
471
+ const sub = inkDim(" prices \xB7 wallets \xB7 trends \xB7 gas \xB7 tvl \u2014 in your terminal");
472
+ const status = handle ? ` ${ink("session \xB7")} ${cyan(handle)}` : ` ${ink("session \xB7")} ${dim("guest mode")}`;
473
+ return `
474
+ ${ascii}
475
+ ${tag}
476
+ ${sub}
477
+
478
+ ${sep}
479
+ ${status}
480
+ ${sep}
481
+ `;
482
+ }
483
+ function renderMiniHeader(handle, lastMs) {
484
+ const left = `${cyan("\u25B8 clawd")} ${ink("\xB7")} ${cyanSoft(handle)}`;
485
+ const right = lastMs !== void 0 ? inkDim(`(${lastMs}ms)`) : "";
486
+ return `${left} ${right}`;
487
+ }
488
+
489
+ // src/repl.ts
490
+ async function runRepl(cfg) {
491
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
492
+ const handle = cfg.handle ?? (cfg.wallet ? cfg.wallet.slice(0, 8) + "\u2026" : "guest");
493
+ rl.setPrompt(` ${cyan("\u25B8")} `);
494
+ let lastMs;
495
+ const printHeader = () => {
496
+ console.log("");
497
+ console.log(renderMiniHeader(handle, lastMs));
498
+ };
499
+ printHeader();
500
+ console.log(` ${inkDim("type")} ${dim("/help")} ${inkDim("for commands, or ask anything in plain english")}`);
501
+ if (!cfg.fal_key) {
502
+ console.log(` ${inkDim("(natural language disabled \u2014 set fal.ai key with")} ${dim("/login <fal-key>")} ${inkDim("to enable)")}`);
503
+ }
504
+ rl.prompt();
505
+ rl.on("line", async (raw) => {
506
+ const input = raw.trim();
507
+ if (!input) {
508
+ rl.prompt();
509
+ return;
510
+ }
511
+ const t0 = Date.now();
512
+ try {
513
+ if (input.startsWith("/")) {
514
+ const handled = await handleSlash(input, cfg);
515
+ if (handled === "exit") {
516
+ rl.close();
517
+ return;
518
+ }
519
+ lastMs = Date.now() - t0;
520
+ } else {
521
+ if (!cfg.fal_key) {
522
+ console.log(` ${err("\u2717")} natural-language disabled \u2014 set fal.ai key first:`);
523
+ console.log(` ${dim("/login <your-fal-key>")}`);
524
+ console.log(` ${inkDim("get one at https://fal.ai \u2192 Dashboard \u2192 API keys")}`);
525
+ } else {
526
+ process.stdout.write(` ${dim("thinking\u2026")}`);
527
+ const intent = await classifyIntent(input, cfg.fal_key);
528
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
529
+ if (intent.command === "unknown") {
530
+ console.log(` ${err("\u2717")} couldn't map that to a command. try /help.`);
531
+ } else {
532
+ console.log(` ${inkDim("\u2192 /")} ${cyanSoft(intent.command)}${intent.args ? " " + cyan(intent.args) : ""}`);
533
+ await runCommand(intent.command, intent.args);
534
+ lastMs = Date.now() - t0;
535
+ }
536
+ }
537
+ }
538
+ } catch (e) {
539
+ console.log(` ${err("\u2717")} ${e.message}`);
540
+ }
541
+ printHeader();
542
+ rl.prompt();
543
+ });
544
+ rl.on("close", () => {
545
+ console.log("");
546
+ console.log(` ${cyanSoft("ClawdOS goes back to sleep.")}`);
547
+ process.exit(0);
548
+ });
549
+ }
550
+ async function handleSlash(input, cfg) {
551
+ const [rawCmd, ...rest] = input.slice(1).split(" ");
552
+ const cmd = (rawCmd ?? "").toLowerCase();
553
+ const arg = rest.join(" ").trim();
554
+ switch (cmd) {
555
+ case "help":
556
+ console.log(renderHelp());
557
+ return "ok";
558
+ case "exit":
559
+ case "quit":
560
+ return "exit";
561
+ case "clear":
562
+ process.stdout.write("\x1Bc");
563
+ return "ok";
564
+ case "login": {
565
+ if (!arg) {
566
+ console.log(` ${err("\u2717")} usage: /login <fal-api-key>`);
567
+ return "ok";
568
+ }
569
+ cfg.fal_key = arg;
570
+ await saveConfig(cfg);
571
+ console.log(` ${ok("\u2713")} fal.ai key saved \u2014 natural-language now enabled`);
572
+ return "ok";
573
+ }
574
+ case "logout": {
575
+ cfg.fal_key = void 0;
576
+ await saveConfig(cfg);
577
+ console.log(` ${ok("\u2713")} fal.ai key removed`);
578
+ return "ok";
579
+ }
580
+ case "whoami": {
581
+ console.log("");
582
+ console.log(` ${ink("session ")} ${cfg.handle ? cyan(cfg.handle) : dim("guest")}`);
583
+ console.log(` ${ink("wallet ")} ${cfg.wallet ? marble(cfg.wallet) : dim("none set")}`);
584
+ console.log(` ${ink("fal key ")} ${cfg.fal_key ? ok("configured") : dim("not set")}`);
585
+ console.log("");
586
+ return "ok";
587
+ }
588
+ // The actual commands
589
+ case "price":
590
+ await runCommand("price", arg);
591
+ return "ok";
592
+ case "wallet":
593
+ await runCommand("wallet", arg);
594
+ return "ok";
595
+ case "scan":
596
+ await runCommand("scan", arg);
597
+ return "ok";
598
+ case "gas":
599
+ await runCommand("gas", "");
600
+ return "ok";
601
+ case "trending":
602
+ await runCommand("trending", "");
603
+ return "ok";
604
+ case "new":
605
+ await runCommand("new", "");
606
+ return "ok";
607
+ case "tvl":
608
+ await runCommand("tvl", "");
609
+ return "ok";
610
+ default:
611
+ console.log(` ${err("\u2717")} unknown command: /${cmd}`);
612
+ console.log(` ${inkDim("type /help to see all commands")}`);
613
+ return "ok";
614
+ }
615
+ }
616
+ async function runCommand(command, arg) {
617
+ let out = "";
618
+ process.stdout.write(` ${dim("fetching\u2026")}`);
619
+ try {
620
+ switch (command) {
621
+ case "price":
622
+ out = await cmdPrice(arg);
623
+ break;
624
+ case "wallet":
625
+ out = await cmdWallet(arg);
626
+ break;
627
+ case "scan":
628
+ out = await cmdScan(arg);
629
+ break;
630
+ case "gas":
631
+ out = await cmdGas();
632
+ break;
633
+ case "trending":
634
+ out = await cmdTrending();
635
+ break;
636
+ case "new":
637
+ out = await cmdNew();
638
+ break;
639
+ case "tvl":
640
+ out = await cmdTvl();
641
+ break;
642
+ default:
643
+ out = ` ${err("\u2717")} unknown command: ${command}`;
644
+ }
645
+ } catch (e) {
646
+ out = ` ${err("\u2717")} ${e.message}`;
647
+ }
648
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
649
+ console.log(out);
650
+ }
651
+
652
+ // src/index.ts
653
+ async function main() {
654
+ const args = process.argv.slice(2);
655
+ if (args[0] === "--version" || args[0] === "-v") {
656
+ const pkg = await import("./package-ZJP322TQ.js");
657
+ console.log(pkg.default.version);
658
+ return;
659
+ }
660
+ if (args[0] === "--help" || args[0] === "-h") {
661
+ console.log(`
662
+ clawd \u2014 ClawdOS 2.0 \u2014 the agent OS that watches Base
663
+
664
+ USAGE
665
+ clawd Start the REPL
666
+ clawd --version Print version
667
+ clawd --help This help
668
+
669
+ INSIDE THE REPL
670
+ /price <token> Token price / mcap / 24h
671
+ /wallet <addr> Wallet balance + info
672
+ /gas Base gas price
673
+ /trending Trending Base tokens
674
+ /new Newest Base launches
675
+ /tvl Base TVL + top protocols
676
+ /scan <addr> Quick on-chain summary
677
+ /login <fal-key> Enable natural-language
678
+ /help Full command list
679
+
680
+ DOCS
681
+ https://clawdos.xyz
682
+ `);
683
+ return;
684
+ }
685
+ let cfg = await loadConfig() ?? {};
686
+ if (process.env.CLAWDOS_FAL_KEY && !cfg.fal_key) {
687
+ cfg.fal_key = process.env.CLAWDOS_FAL_KEY;
688
+ await saveConfig(cfg);
689
+ }
690
+ console.log(renderBanner(cfg.handle ?? (cfg.wallet ? cfg.wallet.slice(0, 8) + "\u2026" : void 0)));
691
+ await runRepl(cfg);
692
+ }
693
+ main().catch((e) => {
694
+ console.error("\n fatal:", e.message);
695
+ process.exit(1);
696
+ });
@@ -0,0 +1,46 @@
1
+ // package.json
2
+ var package_default = {
3
+ name: "clawdos-cli",
4
+ version: "0.1.0",
5
+ description: "ClawdOS 2.0 \u2014 terminal oracle for the Base ecosystem. Token prices, wallet stats, trending launches, gas, TVL \u2014 natural language or slash commands.",
6
+ type: "module",
7
+ bin: {
8
+ clawd: "./dist/index.js"
9
+ },
10
+ files: ["dist", "README.md"],
11
+ scripts: {
12
+ build: "tsup src/index.ts --format esm --clean --target node20",
13
+ dev: "bun run src/index.ts",
14
+ typecheck: "tsc --noEmit"
15
+ },
16
+ keywords: [
17
+ "clawdos",
18
+ "cli",
19
+ "base",
20
+ "ecosystem",
21
+ "dex",
22
+ "defi",
23
+ "token",
24
+ "wallet",
25
+ "agent"
26
+ ],
27
+ license: "MIT",
28
+ homepage: "https://clawdos.xyz",
29
+ repository: {
30
+ type: "git",
31
+ url: "https://github.com/clawdos/clawdos-cli.git"
32
+ },
33
+ author: "clawdos",
34
+ dependencies: {
35
+ chalk: "^5.3.0",
36
+ viem: "^2.21.0"
37
+ },
38
+ devDependencies: {
39
+ tsup: "^8.0.0",
40
+ typescript: "^5.6.0",
41
+ "@types/node": "^22.0.0"
42
+ }
43
+ };
44
+ export {
45
+ package_default as default
46
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "clawdos-cli",
3
+ "version": "0.1.0",
4
+ "description": "ClawdOS 2.0 — terminal oracle for the Base ecosystem. Token prices, wallet stats, trending launches, gas, TVL — natural language or slash commands.",
5
+ "type": "module",
6
+ "bin": {
7
+ "clawd": "dist/index.js"
8
+ },
9
+ "files": ["dist", "README.md"],
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --clean --target node20",
12
+ "dev": "bun run src/index.ts",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "keywords": [
16
+ "clawdos", "cli", "base", "ecosystem", "dex", "defi", "token", "wallet", "agent"
17
+ ],
18
+ "license": "MIT",
19
+ "homepage": "https://clawdos.xyz",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/clawdos/clawdos-cli.git"
23
+ },
24
+ "author": "clawdos",
25
+ "dependencies": {
26
+ "chalk": "^5.3.0",
27
+ "viem": "^2.21.0"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.6.0",
32
+ "@types/node": "^22.0.0"
33
+ }
34
+ }