@neosmithai/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.
Files changed (3) hide show
  1. package/README.md +61 -0
  2. package/bin/neosmith.js +226 -0
  3. package/package.json +22 -0
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @neosmithai/cli
2
+
3
+ Drop-in router for Claude Code. Same experience, ~60% lower inference cost.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx @neosmithai/cli init sk-plus-alice-xxxxxx
9
+ ```
10
+
11
+ That's it. Open a new Claude Code session and your next prompt routes through
12
+ NeoSmith.
13
+
14
+ No NeoSmith key yet? Email **contact-us@neosmith.ai** for a trial — 3 weeks,
15
+ 25M tokens per developer, no credit card.
16
+
17
+ ## Commands
18
+
19
+ | Command | What it does |
20
+ |---|---|
21
+ | `neosmith init <key>` | Points Claude Code at `router.neosmith.ai` and stores your key in `~/.claude/settings.json`. Runs a live verify against `/whoami`. |
22
+ | `neosmith verify` | Checks the key currently installed. Prints your dev slug, org, tier, and 30-day cap usage. |
23
+ | `neosmith uninstall` | Restores Claude Code to talk to Anthropic directly. If you had a prior Anthropic config before running `init`, it's restored from backup. |
24
+ | `neosmith help` | Usage. |
25
+
26
+ ## How it works
27
+
28
+ `init` writes three env keys into `~/.claude/settings.json`:
29
+
30
+ ```json
31
+ {
32
+ "env": {
33
+ "ANTHROPIC_BASE_URL": "https://router.neosmith.ai",
34
+ "ANTHROPIC_API_KEY": "sk-plus-alice-xxxxxx",
35
+ "ANTHROPIC_MODEL": "claude-opus-4"
36
+ }
37
+ }
38
+ ```
39
+
40
+ Claude Code picks those up on next launch. Every prompt hits the NeoSmith
41
+ router, which routes cheap traffic to a distilled SLM and escalates to Claude
42
+ Opus 4.7 when the task actually needs it. Verifier catches regressions so
43
+ output quality stays Opus-class.
44
+
45
+ ## Portal
46
+
47
+ Manage your key, rotate it, and see cap usage at:
48
+
49
+ **https://router.neosmith.ai/me/login**
50
+
51
+ ## Uninstall
52
+
53
+ ```bash
54
+ npx @neosmithai/cli uninstall
55
+ ```
56
+
57
+ Claude Code goes back to Anthropic direct on its next launch.
58
+
59
+ ## License
60
+
61
+ MIT. Source: https://github.com/Neosmith-ai/cli
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ // NeoSmith CLI — wires Claude Code (or any other Anthropic-compatible
3
+ // client) to route through https://router.neosmith.ai. Same UX, ~60%
4
+ // lower inference cost.
5
+ //
6
+ // Usage:
7
+ // npx @neosmithai/cli init <api-key>
8
+ // npx @neosmithai/cli verify
9
+ // npx @neosmithai/cli uninstall
10
+ //
11
+ // Zero runtime deps on purpose — only Node stdlib.
12
+
13
+ "use strict";
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const os = require("os");
18
+ const https = require("https");
19
+ const readline = require("readline");
20
+
21
+ const ROUTER = process.env.NEOSMITH_BASE_URL || "https://router.neosmith.ai";
22
+ const MODEL = "claude-opus-4";
23
+ const CLAUDE_SETTINGS = path.join(os.homedir(), ".claude", "settings.json");
24
+ const BACKUP_SUFFIX = ".neosmith-backup";
25
+
26
+ const COLORS = {
27
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
28
+ cyan: "\x1b[36m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m",
29
+ };
30
+ function c(color, s) { return process.stdout.isTTY ? COLORS[color] + s + COLORS.reset : s; }
31
+
32
+ // ── commands ────────────────────────────────────────────────────────────
33
+
34
+ function cmdInit(args) {
35
+ let key = args[0];
36
+ if (!key) {
37
+ // Interactive prompt
38
+ key = promptSync("Paste your NeoSmith API key (sk-plus-*, sk-slm-*, sk-std-*): ").trim();
39
+ if (!key) die("No key provided. Aborting.");
40
+ }
41
+ if (!/^sk-(plus|slm|std)-/.test(key) && !/^eyJ/.test(key)) {
42
+ warn("That doesn't look like a NeoSmith API key (expected sk-plus-*, sk-slm-*, sk-std-*, or a Cognito JWT).");
43
+ if (!confirm("Proceed anyway?")) die("Aborted.");
44
+ }
45
+
46
+ ensureDir(path.dirname(CLAUDE_SETTINGS));
47
+ const existing = readJSON(CLAUDE_SETTINGS);
48
+ if (hasNeoSmithConfig(existing)) {
49
+ if (!confirm("Claude Code already configured for NeoSmith. Overwrite?")) die("Aborted.");
50
+ } else if (hasAnthropicConfig(existing)) {
51
+ // Back up direct-Anthropic config so uninstall can restore it
52
+ const backup = CLAUDE_SETTINGS + BACKUP_SUFFIX;
53
+ writeJSON(backup, existing);
54
+ log(c("dim", `Backed up prior config → ${backup}`));
55
+ }
56
+
57
+ const next = { ...existing, env: { ...(existing.env || {}) } };
58
+ next.env.ANTHROPIC_BASE_URL = ROUTER;
59
+ next.env.ANTHROPIC_API_KEY = key;
60
+ next.env.ANTHROPIC_MODEL = MODEL;
61
+ writeJSON(CLAUDE_SETTINGS, next);
62
+
63
+ log(c("green", `✓ Wrote ${CLAUDE_SETTINGS}`));
64
+ log("");
65
+ log(c("bold", "Next:") + " open a new Claude Code session. Your next prompt goes through NeoSmith.");
66
+ log("");
67
+
68
+ // Verify inline
69
+ log(c("dim", "Verifying key against " + ROUTER + " …"));
70
+ return doVerify(key);
71
+ }
72
+
73
+ function cmdVerify(args) {
74
+ let key = args[0];
75
+ if (!key) {
76
+ const settings = readJSON(CLAUDE_SETTINGS);
77
+ key = settings && settings.env && settings.env.ANTHROPIC_API_KEY;
78
+ }
79
+ if (!key) die("No key found. Run `neosmith init <key>` first or pass --key.");
80
+ return doVerify(key);
81
+ }
82
+
83
+ function doVerify(key) {
84
+ return get(`${ROUTER}/whoami`, { Authorization: `Bearer ${key}` }).then((resp) => {
85
+ if (resp.status !== 200) {
86
+ warn(`NeoSmith returned ${resp.status}: ${resp.body.slice(0, 200)}`);
87
+ if (resp.status === 401) {
88
+ log("Key rejected. Ask your admin to check it, or run `neosmith init <key>` with the latest one.");
89
+ }
90
+ process.exit(1);
91
+ }
92
+ let data;
93
+ try { data = JSON.parse(resp.body); } catch { data = {}; }
94
+ log(c("green", "✓ NeoSmith active"));
95
+ if (data.dev_slug) {
96
+ log(` dev: ${c("bold", data.dev_slug)} org: ${data.org_id} tier: ${data.tier}`);
97
+ if (data.cap) {
98
+ const used = (data.cap.consumed_30d || 0).toLocaleString();
99
+ const cap = (data.cap.effective_cap_tokens || 0).toLocaleString();
100
+ const rem = (data.cap.remaining ?? 0).toLocaleString();
101
+ log(` cap: ${used} / ${cap} tokens used in last 30 days (${rem} remaining)`);
102
+ }
103
+ }
104
+ log("");
105
+ log(c("dim", `See your portal: ${ROUTER}/me/login`));
106
+ });
107
+ }
108
+
109
+ function cmdUninstall() {
110
+ const existing = readJSON(CLAUDE_SETTINGS);
111
+ if (!existing || !existing.env) die("Nothing to uninstall — Claude Code config not found.");
112
+ if (!hasNeoSmithConfig(existing)) {
113
+ warn("Claude Code isn't pointed at NeoSmith. Nothing to do.");
114
+ return Promise.resolve();
115
+ }
116
+
117
+ const backup = CLAUDE_SETTINGS + BACKUP_SUFFIX;
118
+ let next;
119
+ if (fileExists(backup)) {
120
+ next = readJSON(backup);
121
+ log(c("dim", `Restored pre-NeoSmith config from ${backup}`));
122
+ fs.unlinkSync(backup);
123
+ } else {
124
+ next = { ...existing };
125
+ next.env = { ...(existing.env || {}) };
126
+ delete next.env.ANTHROPIC_BASE_URL;
127
+ delete next.env.ANTHROPIC_API_KEY;
128
+ delete next.env.ANTHROPIC_MODEL;
129
+ if (Object.keys(next.env).length === 0) delete next.env;
130
+ }
131
+ writeJSON(CLAUDE_SETTINGS, next);
132
+ log(c("green", "✓ NeoSmith removed from Claude Code config."));
133
+ log(" Claude Code will talk to Anthropic directly on its next launch.");
134
+ return Promise.resolve();
135
+ }
136
+
137
+ function cmdHelp() {
138
+ log(`${c("bold", "NeoSmith CLI")} — drop-in router for Claude Code.`);
139
+ log("");
140
+ log(c("bold", "Commands:"));
141
+ log(" " + c("cyan", "neosmith init <key>") + " Point Claude Code at NeoSmith.");
142
+ log(" " + c("cyan", "neosmith verify") + " Check that your key is active.");
143
+ log(" " + c("cyan", "neosmith uninstall") + " Restore Claude Code to direct Anthropic.");
144
+ log("");
145
+ log(c("bold", "Example:"));
146
+ log(` ${c("dim", "$")} npx @neosmithai/cli init sk-plus-alice-xxxxxx`);
147
+ log("");
148
+ log(c("dim", "No NeoSmith account yet? Email contact-us@neosmith.ai for a trial key."));
149
+ log(c("dim", `Docs: ${ROUTER}/me/login · Status: github.com/Neosmith-ai/issues`));
150
+ }
151
+
152
+ // ── helpers ─────────────────────────────────────────────────────────────
153
+
154
+ function hasNeoSmithConfig(s) {
155
+ return s && s.env && typeof s.env.ANTHROPIC_BASE_URL === "string" &&
156
+ s.env.ANTHROPIC_BASE_URL.includes("neosmith.ai");
157
+ }
158
+ function hasAnthropicConfig(s) {
159
+ return s && s.env && (s.env.ANTHROPIC_API_KEY || s.env.ANTHROPIC_BASE_URL);
160
+ }
161
+ function ensureDir(d) { fs.mkdirSync(d, { recursive: true }); }
162
+ function fileExists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
163
+ function readJSON(p) {
164
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); }
165
+ catch { return {}; }
166
+ }
167
+ function writeJSON(p, obj) {
168
+ ensureDir(path.dirname(p));
169
+ fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n", { mode: 0o600 });
170
+ }
171
+ function log(s) { console.log(s); }
172
+ function warn(s) { console.error(c("yellow", "! ") + s); }
173
+ function die(s) { console.error(c("red", "✗ ") + s); process.exit(1); }
174
+
175
+ function promptSync(q) {
176
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
177
+ // readline.question is async — use a sync hack via execSync for a tiny helper
178
+ return new Promise((resolve) => {
179
+ rl.question(q, (ans) => { rl.close(); resolve(ans); });
180
+ });
181
+ }
182
+ function confirm(q) {
183
+ if (!process.stdout.isTTY) return true; // default yes in non-interactive
184
+ const ans = require("child_process")
185
+ .execSync(`printf "%s" "${q} [Y/n] "; read -r r; echo $r`, { stdio: ["inherit", "pipe", "inherit"], shell: "/bin/bash" })
186
+ .toString().trim().toLowerCase();
187
+ return ans === "" || ans === "y" || ans === "yes";
188
+ }
189
+
190
+ function get(url, headers = {}) {
191
+ return new Promise((resolve, reject) => {
192
+ const req = https.get(url, { headers }, (res) => {
193
+ let body = "";
194
+ res.on("data", (chunk) => body += chunk);
195
+ res.on("end", () => resolve({ status: res.statusCode, body }));
196
+ });
197
+ req.on("error", reject);
198
+ req.setTimeout(10000, () => { req.destroy(new Error("timeout")); });
199
+ });
200
+ }
201
+
202
+ // ── entry ───────────────────────────────────────────────────────────────
203
+
204
+ async function main() {
205
+ const args = process.argv.slice(2);
206
+ const cmd = (args.shift() || "").toLowerCase();
207
+ try {
208
+ switch (cmd) {
209
+ case "init": await cmdInit(args); break;
210
+ case "verify": await cmdVerify(args); break;
211
+ case "uninstall": await cmdUninstall(); break;
212
+ case "":
213
+ case "-h":
214
+ case "--help":
215
+ case "help": cmdHelp(); break;
216
+ default:
217
+ warn(`Unknown command: ${cmd}`);
218
+ cmdHelp();
219
+ process.exit(1);
220
+ }
221
+ } catch (e) {
222
+ die(e && e.message ? e.message : String(e));
223
+ }
224
+ }
225
+
226
+ main();
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@neosmithai/cli",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in router for Claude Code: same experience, ~60% lower inference cost.",
5
+ "keywords": ["claude", "claude-code", "anthropic", "router", "llm", "neosmith"],
6
+ "homepage": "https://neosmith.ai",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Neosmith-ai/cli.git"
10
+ },
11
+ "bugs": "https://github.com/Neosmith-ai/issues/issues",
12
+ "license": "MIT",
13
+ "author": "NeoSmith AI <contact-us@neosmith.ai>",
14
+ "bin": {
15
+ "neosmith": "bin/neosmith.js"
16
+ },
17
+ "main": "bin/neosmith.js",
18
+ "files": ["bin/", "README.md"],
19
+ "engines": {
20
+ "node": ">=16"
21
+ }
22
+ }