clawzempic 1.0.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/clawzempic.mjs +899 -0
- package/package.json +35 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clawzempic CLI
|
|
5
|
+
*
|
|
6
|
+
* One-command setup for the Clawzempic intelligent LLM proxy.
|
|
7
|
+
* Zero dependencies — uses Node.js built-ins only (requires Node 18+).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx clawzempic Interactive setup
|
|
11
|
+
* npx clawzempic init Set up Clawzempic in this project
|
|
12
|
+
* npx clawzempic test Verify your connection works
|
|
13
|
+
* npx clawzempic status Check usage and savings
|
|
14
|
+
* npx clawzempic doctor Diagnose common issues
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createInterface } from 'node:readline/promises';
|
|
18
|
+
import { stdin, stdout, stderr } from 'node:process';
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs';
|
|
20
|
+
import { join, basename } from 'node:path';
|
|
21
|
+
|
|
22
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const API_BASE = 'https://api.clawzempic.ai';
|
|
25
|
+
const VERSION = '1.0.0';
|
|
26
|
+
|
|
27
|
+
// ── ANSI Colors (no dependencies) ─────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const c = {
|
|
30
|
+
reset: '\x1b[0m',
|
|
31
|
+
bold: '\x1b[1m',
|
|
32
|
+
dim: '\x1b[2m',
|
|
33
|
+
green: '\x1b[32m',
|
|
34
|
+
yellow: '\x1b[33m',
|
|
35
|
+
blue: '\x1b[34m',
|
|
36
|
+
magenta: '\x1b[35m',
|
|
37
|
+
cyan: '\x1b[36m',
|
|
38
|
+
red: '\x1b[31m',
|
|
39
|
+
gray: '\x1b[90m',
|
|
40
|
+
bgGreen: '\x1b[42m',
|
|
41
|
+
bgRed: '\x1b[41m',
|
|
42
|
+
white: '\x1b[37m',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ok = `${c.green}✓${c.reset}`;
|
|
46
|
+
const fail = `${c.red}✗${c.reset}`;
|
|
47
|
+
const warn = `${c.yellow}!${c.reset}`;
|
|
48
|
+
const info = `${c.blue}i${c.reset}`;
|
|
49
|
+
const arrow = `${c.cyan}→${c.reset}`;
|
|
50
|
+
|
|
51
|
+
// ── UI Helpers ────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function banner() {
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(` ${c.bold}${c.cyan}⚡ Clawzempic${c.reset} ${c.dim}v${VERSION}${c.reset}`);
|
|
56
|
+
console.log(` ${c.dim}Intelligent LLM API Proxy — prompt caching, smart routing, memory${c.reset}`);
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function box(lines) {
|
|
61
|
+
const maxLen = Math.max(...lines.map(l => stripAnsi(l).length));
|
|
62
|
+
const hr = '─'.repeat(maxLen + 4);
|
|
63
|
+
console.log(` ${c.dim}┌${hr}┐${c.reset}`);
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const pad = maxLen - stripAnsi(line).length;
|
|
66
|
+
console.log(` ${c.dim}│${c.reset} ${line}${' '.repeat(pad)} ${c.dim}│${c.reset}`);
|
|
67
|
+
}
|
|
68
|
+
console.log(` ${c.dim}└${hr}┘${c.reset}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stripAnsi(str) {
|
|
72
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function heading(text) {
|
|
76
|
+
console.log(`\n ${c.bold}${text}${c.reset}\n`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function bullet(icon, text) {
|
|
80
|
+
console.log(` ${icon} ${text}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function blank() {
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class Spinner {
|
|
88
|
+
constructor(text) {
|
|
89
|
+
this.text = text;
|
|
90
|
+
this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
91
|
+
this.i = 0;
|
|
92
|
+
this.interval = null;
|
|
93
|
+
}
|
|
94
|
+
start() {
|
|
95
|
+
this.interval = setInterval(() => {
|
|
96
|
+
stderr.write(`\r ${c.cyan}${this.frames[this.i++ % this.frames.length]}${c.reset} ${this.text}`);
|
|
97
|
+
}, 80);
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
succeed(text) {
|
|
101
|
+
this.stop();
|
|
102
|
+
console.log(`\r ${ok} ${text || this.text}`);
|
|
103
|
+
}
|
|
104
|
+
fail(text) {
|
|
105
|
+
this.stop();
|
|
106
|
+
console.log(`\r ${fail} ${text || this.text}`);
|
|
107
|
+
}
|
|
108
|
+
stop() {
|
|
109
|
+
if (this.interval) {
|
|
110
|
+
clearInterval(this.interval);
|
|
111
|
+
this.interval = null;
|
|
112
|
+
stderr.write('\r' + ' '.repeat(stripAnsi(this.text).length + 10) + '\r');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Prompts ───────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
let rl;
|
|
120
|
+
|
|
121
|
+
function getRL() {
|
|
122
|
+
if (!rl) {
|
|
123
|
+
rl = createInterface({ input: stdin, output: stdout });
|
|
124
|
+
}
|
|
125
|
+
return rl;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function ask(question, opts = {}) {
|
|
129
|
+
const { required = true, secret = false, validate, defaultVal } = opts;
|
|
130
|
+
const suffix = defaultVal ? ` ${c.dim}(${defaultVal})${c.reset}` : '';
|
|
131
|
+
const prompt = ` ${c.cyan}?${c.reset} ${question}${suffix}: `;
|
|
132
|
+
|
|
133
|
+
while (true) {
|
|
134
|
+
if (secret) {
|
|
135
|
+
// Hide input for API keys
|
|
136
|
+
stdout.write(prompt);
|
|
137
|
+
const answer = await new Promise((resolve) => {
|
|
138
|
+
let buf = '';
|
|
139
|
+
stdin.setRawMode(true);
|
|
140
|
+
stdin.resume();
|
|
141
|
+
stdin.setEncoding('utf8');
|
|
142
|
+
const onData = (ch) => {
|
|
143
|
+
if (ch === '\n' || ch === '\r') {
|
|
144
|
+
stdin.setRawMode(false);
|
|
145
|
+
stdin.removeListener('data', onData);
|
|
146
|
+
stdout.write('\n');
|
|
147
|
+
resolve(buf);
|
|
148
|
+
} else if (ch === '\x03') {
|
|
149
|
+
// Ctrl+C
|
|
150
|
+
stdin.setRawMode(false);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
} else if (ch === '\x7f' || ch === '\b') {
|
|
153
|
+
if (buf.length > 0) {
|
|
154
|
+
buf = buf.slice(0, -1);
|
|
155
|
+
stdout.write('\b \b');
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
buf += ch;
|
|
159
|
+
stdout.write('*');
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
stdin.on('data', onData);
|
|
163
|
+
});
|
|
164
|
+
const trimmed = answer.trim();
|
|
165
|
+
if (!trimmed && defaultVal) return defaultVal;
|
|
166
|
+
if (!trimmed && required) {
|
|
167
|
+
bullet(warn, 'This field is required.');
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (validate) {
|
|
171
|
+
const err = validate(trimmed);
|
|
172
|
+
if (err) { bullet(warn, err); continue; }
|
|
173
|
+
}
|
|
174
|
+
return trimmed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const answer = await getRL().question(prompt);
|
|
178
|
+
const trimmed = answer.trim();
|
|
179
|
+
if (!trimmed && defaultVal) return defaultVal;
|
|
180
|
+
if (!trimmed && required) {
|
|
181
|
+
bullet(warn, 'This field is required.');
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!trimmed && !required) return '';
|
|
185
|
+
if (validate) {
|
|
186
|
+
const err = validate(trimmed);
|
|
187
|
+
if (err) { bullet(warn, err); continue; }
|
|
188
|
+
}
|
|
189
|
+
return trimmed;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function confirm(question, defaultYes = true) {
|
|
194
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
195
|
+
const answer = await ask(`${question} ${c.dim}(${hint})${c.reset}`, { required: false });
|
|
196
|
+
if (!answer) return defaultYes;
|
|
197
|
+
return answer.toLowerCase().startsWith('y');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function choose(question, options) {
|
|
201
|
+
console.log(`\n ${c.cyan}?${c.reset} ${question}\n`);
|
|
202
|
+
options.forEach((opt, i) => {
|
|
203
|
+
console.log(` ${c.cyan}${i + 1}${c.reset}) ${opt.label}${opt.hint ? ` ${c.dim}${opt.hint}${c.reset}` : ''}`);
|
|
204
|
+
});
|
|
205
|
+
blank();
|
|
206
|
+
while (true) {
|
|
207
|
+
const answer = await ask('Choice', { validate: (v) => {
|
|
208
|
+
const n = parseInt(v, 10);
|
|
209
|
+
if (isNaN(n) || n < 1 || n > options.length) return `Enter 1-${options.length}`;
|
|
210
|
+
return null;
|
|
211
|
+
}});
|
|
212
|
+
return options[parseInt(answer, 10) - 1].value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── API Client ────────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
async function apiPost(path, body) {
|
|
219
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'content-type': 'application/json' },
|
|
222
|
+
body: JSON.stringify(body),
|
|
223
|
+
});
|
|
224
|
+
const data = await res.json();
|
|
225
|
+
return { ok: res.ok, status: res.status, data };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function apiGet(path, apiKey) {
|
|
229
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
230
|
+
headers: { 'x-api-key': apiKey },
|
|
231
|
+
});
|
|
232
|
+
const data = await res.json();
|
|
233
|
+
return { ok: res.ok, status: res.status, data };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function testConnection(apiKey) {
|
|
237
|
+
const start = Date.now();
|
|
238
|
+
const res = await fetch(`${API_BASE}/v1/chat`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: {
|
|
241
|
+
'x-api-key': apiKey,
|
|
242
|
+
'content-type': 'application/json',
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
246
|
+
max_tokens: 32,
|
|
247
|
+
messages: [{ role: 'user', content: 'Say "connected" and nothing else.' }],
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
const latency = Date.now() - start;
|
|
251
|
+
const data = await res.json();
|
|
252
|
+
const model = res.headers.get('x-router-model') || 'unknown';
|
|
253
|
+
return { ok: res.ok, status: res.status, data, latency, model };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Framework Detection ───────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
const FRAMEWORKS = [
|
|
259
|
+
{
|
|
260
|
+
id: 'openclaw',
|
|
261
|
+
name: 'OpenClaw',
|
|
262
|
+
detect: (dir) => {
|
|
263
|
+
// OpenClaw: docker-compose with openclaw image, or .openclaw/ dir
|
|
264
|
+
if (existsSync(join(dir, '.openclaw'))) return true;
|
|
265
|
+
for (const f of ['docker-compose.yml', 'docker-compose.yaml']) {
|
|
266
|
+
if (existsSync(join(dir, f))) {
|
|
267
|
+
try {
|
|
268
|
+
const content = readFileSync(join(dir, f), 'utf8');
|
|
269
|
+
if (content.includes('openclaw') || content.includes('open-webui') || content.includes('ghcr.io/open-webui')) return true;
|
|
270
|
+
} catch {}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
},
|
|
275
|
+
envFile: '.env',
|
|
276
|
+
envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'nanobot',
|
|
280
|
+
name: 'Nanobot',
|
|
281
|
+
detect: (dir) => {
|
|
282
|
+
if (existsSync(join(dir, 'nanobot.yaml')) || existsSync(join(dir, 'nanobot.yml'))) return true;
|
|
283
|
+
if (existsSync(join(dir, 'CLAUDE.md'))) {
|
|
284
|
+
try {
|
|
285
|
+
const content = readFileSync(join(dir, 'CLAUDE.md'), 'utf8');
|
|
286
|
+
if (content.includes('nanobot')) return true;
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
},
|
|
291
|
+
envFile: '.env',
|
|
292
|
+
envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'python',
|
|
296
|
+
name: 'Python (Anthropic SDK)',
|
|
297
|
+
detect: (dir) => {
|
|
298
|
+
for (const f of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
|
|
299
|
+
if (existsSync(join(dir, f))) {
|
|
300
|
+
try {
|
|
301
|
+
const content = readFileSync(join(dir, f), 'utf8');
|
|
302
|
+
if (content.includes('anthropic')) return true;
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
},
|
|
308
|
+
envFile: '.env',
|
|
309
|
+
envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'langchain',
|
|
313
|
+
name: 'LangChain',
|
|
314
|
+
detect: (dir) => {
|
|
315
|
+
for (const f of ['requirements.txt', 'pyproject.toml', 'package.json']) {
|
|
316
|
+
if (existsSync(join(dir, f))) {
|
|
317
|
+
try {
|
|
318
|
+
const content = readFileSync(join(dir, f), 'utf8');
|
|
319
|
+
if (content.includes('langchain-anthropic') || content.includes('langchain_anthropic')) return true;
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
},
|
|
325
|
+
envFile: '.env',
|
|
326
|
+
envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: 'node',
|
|
330
|
+
name: 'Node.js (Anthropic SDK)',
|
|
331
|
+
detect: (dir) => {
|
|
332
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
333
|
+
try {
|
|
334
|
+
const content = readFileSync(join(dir, 'package.json'), 'utf8');
|
|
335
|
+
if (content.includes('@anthropic-ai/sdk') || content.includes('anthropic')) return true;
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
},
|
|
340
|
+
envFile: '.env',
|
|
341
|
+
envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
function detectFramework(dir) {
|
|
346
|
+
for (const fw of FRAMEWORKS) {
|
|
347
|
+
if (fw.detect(dir)) return fw;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function detectExistingConfig(dir) {
|
|
353
|
+
// Check if already configured
|
|
354
|
+
const envPath = join(dir, '.env');
|
|
355
|
+
if (existsSync(envPath)) {
|
|
356
|
+
try {
|
|
357
|
+
const content = readFileSync(envPath, 'utf8');
|
|
358
|
+
if (content.includes('clawzempic.ai') || content.includes('sk-clwz-')) {
|
|
359
|
+
return { configured: true, envPath };
|
|
360
|
+
}
|
|
361
|
+
} catch {}
|
|
362
|
+
}
|
|
363
|
+
return { configured: false, envPath };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Config Patching ───────────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
function patchEnvFile(envPath, vars) {
|
|
369
|
+
let content = '';
|
|
370
|
+
let existingKeys = new Set();
|
|
371
|
+
|
|
372
|
+
if (existsSync(envPath)) {
|
|
373
|
+
// Back up first
|
|
374
|
+
const backupPath = envPath + '.backup';
|
|
375
|
+
if (!existsSync(backupPath)) {
|
|
376
|
+
copyFileSync(envPath, backupPath);
|
|
377
|
+
bullet(info, `Backed up ${c.dim}${basename(envPath)}${c.reset} ${arrow} ${c.dim}${basename(backupPath)}${c.reset}`);
|
|
378
|
+
}
|
|
379
|
+
content = readFileSync(envPath, 'utf8');
|
|
380
|
+
// Parse existing keys
|
|
381
|
+
for (const line of content.split('\n')) {
|
|
382
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)\s*=/);
|
|
383
|
+
if (match) existingKeys.add(match[1]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const changes = [];
|
|
388
|
+
|
|
389
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
390
|
+
if (existingKeys.has(key)) {
|
|
391
|
+
// Replace existing line
|
|
392
|
+
const regex = new RegExp(`^${key}\\s*=.*$`, 'm');
|
|
393
|
+
const oldLine = content.match(regex)?.[0] || '';
|
|
394
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
395
|
+
changes.push({ key, action: 'updated', old: oldLine, new: `${key}=${value}` });
|
|
396
|
+
} else {
|
|
397
|
+
// Append
|
|
398
|
+
if (content.length > 0 && !content.endsWith('\n')) content += '\n';
|
|
399
|
+
content += `${key}=${value}\n`;
|
|
400
|
+
changes.push({ key, action: 'added', new: `${key}=${value}` });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
writeFileSync(envPath, content, 'utf8');
|
|
405
|
+
return changes;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
async function cmdInit(args) {
|
|
411
|
+
banner();
|
|
412
|
+
const dir = process.cwd();
|
|
413
|
+
|
|
414
|
+
// Check existing config
|
|
415
|
+
const existing = detectExistingConfig(dir);
|
|
416
|
+
if (existing.configured) {
|
|
417
|
+
bullet(warn, `Clawzempic is already configured in ${c.dim}${basename(existing.envPath)}${c.reset}`);
|
|
418
|
+
const proceed = await confirm('Overwrite existing configuration?', false);
|
|
419
|
+
if (!proceed) {
|
|
420
|
+
bullet(info, 'No changes made. Run `npx clawzempic test` to verify your connection.');
|
|
421
|
+
return cleanup();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Determine mode
|
|
426
|
+
const mode = args.key
|
|
427
|
+
? 'connect'
|
|
428
|
+
: await choose('How would you like to set up?', [
|
|
429
|
+
{ value: 'signup', label: 'Create a new account', hint: '(recommended)' },
|
|
430
|
+
{ value: 'connect', label: 'I already have a Clawzempic API key' },
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
let clawzempicKey;
|
|
434
|
+
let provider = null;
|
|
435
|
+
|
|
436
|
+
if (mode === 'signup') {
|
|
437
|
+
// ── Signup Flow ──────────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
const name = await ask('Your name or bot name');
|
|
440
|
+
const email = await ask('Email', { required: false });
|
|
441
|
+
|
|
442
|
+
const providerKey = await ask('Your LLM provider API key', {
|
|
443
|
+
secret: true,
|
|
444
|
+
validate: (v) => {
|
|
445
|
+
if (v.length < 10) return 'Key seems too short. Check your API key.';
|
|
446
|
+
return null;
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Detect provider
|
|
451
|
+
if (providerKey.startsWith('sk-ant-')) {
|
|
452
|
+
provider = 'anthropic';
|
|
453
|
+
bullet(ok, `Detected provider: ${c.bold}Anthropic${c.reset}`);
|
|
454
|
+
} else if (providerKey.startsWith('sk-or-')) {
|
|
455
|
+
provider = 'openrouter';
|
|
456
|
+
bullet(ok, `Detected provider: ${c.bold}OpenRouter${c.reset}`);
|
|
457
|
+
} else {
|
|
458
|
+
bullet(warn, 'Could not auto-detect provider from key prefix. Defaulting to Anthropic.');
|
|
459
|
+
provider = 'anthropic';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Create account
|
|
463
|
+
const spinner = new Spinner('Creating account...').start();
|
|
464
|
+
try {
|
|
465
|
+
const res = await apiPost('/v1/signup', {
|
|
466
|
+
agent_name: name,
|
|
467
|
+
email: email || undefined,
|
|
468
|
+
framework: detectFramework(dir)?.id || 'custom',
|
|
469
|
+
upstream_key: providerKey,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (!res.ok) {
|
|
473
|
+
spinner.fail(`Signup failed: ${res.data.error || `HTTP ${res.status}`}`);
|
|
474
|
+
if (res.status === 429) {
|
|
475
|
+
bullet(info, 'Rate limited. Try again in a few minutes, or use an existing key.');
|
|
476
|
+
}
|
|
477
|
+
return cleanup(1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
clawzempicKey = res.data.api_key;
|
|
481
|
+
spinner.succeed(`Account created ${c.dim}(${res.data.client_id})${c.reset}`);
|
|
482
|
+
|
|
483
|
+
if (res.data.key_stored) {
|
|
484
|
+
bullet(ok, `Provider key stored securely — no extra headers needed`);
|
|
485
|
+
}
|
|
486
|
+
} catch (err) {
|
|
487
|
+
spinner.fail(`Could not reach ${API_BASE}`);
|
|
488
|
+
bullet(info, `Check your internet connection and try again.`);
|
|
489
|
+
bullet(info, `Error: ${err.message}`);
|
|
490
|
+
return cleanup(1);
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
// ── Connect Flow ─────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
clawzempicKey = args.key || await ask('Clawzempic API key', {
|
|
496
|
+
secret: true,
|
|
497
|
+
validate: (v) => {
|
|
498
|
+
if (!v.startsWith('sk-clwz-')) return 'Key should start with sk-clwz-';
|
|
499
|
+
return null;
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── Framework Detection ──────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
heading('Framework Detection');
|
|
507
|
+
|
|
508
|
+
const framework = detectFramework(dir);
|
|
509
|
+
if (framework) {
|
|
510
|
+
bullet(ok, `Detected: ${c.bold}${framework.name}${c.reset}`);
|
|
511
|
+
} else {
|
|
512
|
+
bullet(info, 'No specific framework detected. Using generic .env config.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── Config Changes ───────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
heading('Configuration');
|
|
518
|
+
|
|
519
|
+
const envFile = framework?.envFile || '.env';
|
|
520
|
+
const envPath = join(dir, envFile);
|
|
521
|
+
const envVars = { ...(framework?.envVars || { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE }) };
|
|
522
|
+
|
|
523
|
+
// Set the API key value
|
|
524
|
+
for (const key of Object.keys(envVars)) {
|
|
525
|
+
if (envVars[key] === null) envVars[key] = clawzempicKey;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Show what we'll do
|
|
529
|
+
bullet(info, `Will update ${c.bold}${envFile}${c.reset}:`);
|
|
530
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
531
|
+
const display = key.includes('KEY') ? value.substring(0, 12) + '...' : value;
|
|
532
|
+
console.log(` ${c.green}+${c.reset} ${key}=${display}`);
|
|
533
|
+
}
|
|
534
|
+
blank();
|
|
535
|
+
|
|
536
|
+
const applyChanges = await confirm('Apply these changes?');
|
|
537
|
+
if (!applyChanges) {
|
|
538
|
+
bullet(info, 'No changes made. You can set these manually:');
|
|
539
|
+
blank();
|
|
540
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
541
|
+
console.log(` ${key}=${value}`);
|
|
542
|
+
}
|
|
543
|
+
blank();
|
|
544
|
+
return cleanup();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Apply
|
|
548
|
+
const changes = patchEnvFile(envPath, envVars);
|
|
549
|
+
for (const change of changes) {
|
|
550
|
+
if (change.action === 'updated') {
|
|
551
|
+
bullet(ok, `Updated ${c.bold}${change.key}${c.reset}`);
|
|
552
|
+
} else {
|
|
553
|
+
bullet(ok, `Added ${c.bold}${change.key}${c.reset}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── Test Connection ──────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
blank();
|
|
560
|
+
const runTest = await confirm('Test the connection?');
|
|
561
|
+
if (runTest) {
|
|
562
|
+
await runConnectionTest(clawzempicKey);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Done ─────────────────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
blank();
|
|
568
|
+
box([
|
|
569
|
+
`${c.green}${c.bold}Setup complete!${c.reset}`,
|
|
570
|
+
'',
|
|
571
|
+
`${c.dim}Dashboard:${c.reset} ${API_BASE}/v1/auth/portal`,
|
|
572
|
+
`${c.dim}Insights:${c.reset} ${API_BASE}/v1/insights`,
|
|
573
|
+
`${c.dim}Docs:${c.reset} ${API_BASE}/docs`,
|
|
574
|
+
'',
|
|
575
|
+
`${c.yellow}Restart your app to start saving.${c.reset}`,
|
|
576
|
+
]);
|
|
577
|
+
blank();
|
|
578
|
+
|
|
579
|
+
return cleanup();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function runConnectionTest(apiKey) {
|
|
583
|
+
const spinner = new Spinner('Testing connection...').start();
|
|
584
|
+
try {
|
|
585
|
+
const result = await testConnection(apiKey);
|
|
586
|
+
if (result.ok) {
|
|
587
|
+
spinner.succeed(`Connected! Routed to ${c.bold}${result.model}${c.reset} ${c.dim}(${result.latency}ms)${c.reset}`);
|
|
588
|
+
return true;
|
|
589
|
+
} else {
|
|
590
|
+
const errMsg = result.data?.error?.message || result.data?.error || `HTTP ${result.status}`;
|
|
591
|
+
spinner.fail(`Connection failed: ${errMsg}`);
|
|
592
|
+
|
|
593
|
+
// Helpful diagnostics
|
|
594
|
+
if (result.status === 400 && errMsg.includes('MISSING_UPSTREAM_KEY')) {
|
|
595
|
+
bullet(info, 'Your provider key was not stored during signup.');
|
|
596
|
+
bullet(info, 'Either re-run setup with your provider key, or pass x-upstream-key header.');
|
|
597
|
+
} else if (result.status === 401) {
|
|
598
|
+
bullet(info, 'Invalid Clawzempic API key. Check your key and try again.');
|
|
599
|
+
} else if (result.status === 429) {
|
|
600
|
+
bullet(info, 'Rate limited. Wait a moment and try again.');
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
} catch (err) {
|
|
605
|
+
spinner.fail(`Could not reach ${API_BASE}`);
|
|
606
|
+
bullet(info, `Error: ${err.message}`);
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function cmdTest(args) {
|
|
612
|
+
banner();
|
|
613
|
+
|
|
614
|
+
// Find API key: arg > env > .env file
|
|
615
|
+
let apiKey = args.key;
|
|
616
|
+
|
|
617
|
+
if (!apiKey) {
|
|
618
|
+
apiKey = process.env.CLAWZEMPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!apiKey) {
|
|
622
|
+
const envPath = join(process.cwd(), '.env');
|
|
623
|
+
if (existsSync(envPath)) {
|
|
624
|
+
const content = readFileSync(envPath, 'utf8');
|
|
625
|
+
const match = content.match(/^(?:CLAWZEMPIC_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
|
|
626
|
+
if (match) apiKey = match[1].trim();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!apiKey) {
|
|
631
|
+
bullet(fail, 'No API key found. Run `npx clawzempic` to set up first.');
|
|
632
|
+
return cleanup(1);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!apiKey.startsWith('sk-clwz-')) {
|
|
636
|
+
bullet(warn, 'Key does not look like a Clawzempic key (expected sk-clwz-...).');
|
|
637
|
+
bullet(info, 'If you haven\'t set up yet, run `npx clawzempic` first.');
|
|
638
|
+
return cleanup(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const success = await runConnectionTest(apiKey);
|
|
642
|
+
blank();
|
|
643
|
+
return cleanup(success ? 0 : 1);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function cmdStatus(args) {
|
|
647
|
+
banner();
|
|
648
|
+
|
|
649
|
+
// Find API key
|
|
650
|
+
let apiKey = args.key;
|
|
651
|
+
if (!apiKey) {
|
|
652
|
+
apiKey = process.env.CLAWZEMPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
653
|
+
}
|
|
654
|
+
if (!apiKey) {
|
|
655
|
+
const envPath = join(process.cwd(), '.env');
|
|
656
|
+
if (existsSync(envPath)) {
|
|
657
|
+
const content = readFileSync(envPath, 'utf8');
|
|
658
|
+
const match = content.match(/^(?:CLAWZEMPIC_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
|
|
659
|
+
if (match) apiKey = match[1].trim();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (!apiKey || !apiKey.startsWith('sk-clwz-')) {
|
|
664
|
+
bullet(fail, 'No Clawzempic API key found. Run `npx clawzempic` to set up first.');
|
|
665
|
+
return cleanup(1);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const spinner = new Spinner('Fetching usage data...').start();
|
|
669
|
+
try {
|
|
670
|
+
const res = await apiGet('/v1/insights', apiKey);
|
|
671
|
+
if (!res.ok) {
|
|
672
|
+
spinner.fail(`Failed to fetch insights: ${res.data?.error || `HTTP ${res.status}`}`);
|
|
673
|
+
return cleanup(1);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
spinner.succeed('Usage data loaded');
|
|
677
|
+
blank();
|
|
678
|
+
|
|
679
|
+
const d = res.data;
|
|
680
|
+
const period = d.period || {};
|
|
681
|
+
const usage = d.usage || {};
|
|
682
|
+
const savings = d.savings || {};
|
|
683
|
+
const limits = d.limits || {};
|
|
684
|
+
|
|
685
|
+
heading('Account');
|
|
686
|
+
if (d.client_id) bullet(info, `Client: ${c.bold}${d.client_id}${c.reset}`);
|
|
687
|
+
if (d.plan) bullet(info, `Plan: ${c.bold}${d.plan}${c.reset}`);
|
|
688
|
+
if (period.start) bullet(info, `Period: ${period.start} — ${period.end || 'now'}`);
|
|
689
|
+
|
|
690
|
+
heading('Usage');
|
|
691
|
+
bullet(info, `Requests: ${c.bold}${usage.total_requests || 0}${c.reset}`);
|
|
692
|
+
bullet(info, `Input tokens: ${c.bold}${(usage.total_input_tokens || 0).toLocaleString()}${c.reset}`);
|
|
693
|
+
bullet(info, `Output tokens: ${c.bold}${(usage.total_output_tokens || 0).toLocaleString()}${c.reset}`);
|
|
694
|
+
bullet(info, `Cost: ${c.bold}$${(usage.total_cost_usd || 0).toFixed(4)}${c.reset}`);
|
|
695
|
+
|
|
696
|
+
if (savings.total_savings_usd) {
|
|
697
|
+
heading('Savings');
|
|
698
|
+
bullet(ok, `Total saved: ${c.bold}${c.green}$${savings.total_savings_usd.toFixed(4)}${c.reset}`);
|
|
699
|
+
if (savings.cache_savings_usd) bullet(ok, `From caching: $${savings.cache_savings_usd.toFixed(4)}`);
|
|
700
|
+
if (savings.routing_savings_usd) bullet(ok, `From routing: $${savings.routing_savings_usd.toFixed(4)}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (limits.daily_limit_usd !== undefined) {
|
|
704
|
+
heading('Limits');
|
|
705
|
+
bullet(info, `Daily: ${limits.daily_limit_usd === null ? 'unlimited' : '$' + limits.daily_limit_usd}`);
|
|
706
|
+
bullet(info, `Monthly: ${limits.monthly_limit_usd === null ? 'unlimited' : '$' + limits.monthly_limit_usd}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
blank();
|
|
710
|
+
} catch (err) {
|
|
711
|
+
spinner.fail(`Could not reach ${API_BASE}: ${err.message}`);
|
|
712
|
+
return cleanup(1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return cleanup();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function cmdDoctor() {
|
|
719
|
+
banner();
|
|
720
|
+
heading('Diagnostics');
|
|
721
|
+
|
|
722
|
+
const dir = process.cwd();
|
|
723
|
+
|
|
724
|
+
// 1. Check .env exists
|
|
725
|
+
const envPath = join(dir, '.env');
|
|
726
|
+
if (existsSync(envPath)) {
|
|
727
|
+
bullet(ok, `.env file found`);
|
|
728
|
+
} else {
|
|
729
|
+
bullet(fail, `No .env file in current directory`);
|
|
730
|
+
bullet(info, 'Run `npx clawzempic` to set up.');
|
|
731
|
+
return cleanup(1);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// 2. Check for Clawzempic config
|
|
735
|
+
const content = readFileSync(envPath, 'utf8');
|
|
736
|
+
const keyMatch = content.match(/^(?:CLAWZEMPIC_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
|
|
737
|
+
if (keyMatch && keyMatch[1].startsWith('sk-clwz-')) {
|
|
738
|
+
bullet(ok, `Clawzempic API key found`);
|
|
739
|
+
} else if (keyMatch) {
|
|
740
|
+
bullet(warn, `API key found but does not look like a Clawzempic key (expected sk-clwz-...)`);
|
|
741
|
+
bullet(info, 'You may be using a direct Anthropic key. Run `npx clawzempic` to set up proxy routing.');
|
|
742
|
+
} else {
|
|
743
|
+
bullet(fail, 'No API key found in .env');
|
|
744
|
+
return cleanup(1);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 3. Check base URL
|
|
748
|
+
const baseUrlMatch = content.match(/^ANTHROPIC_BASE_URL\s*=\s*(.+)$/m);
|
|
749
|
+
if (baseUrlMatch && baseUrlMatch[1].includes('clawzempic.ai')) {
|
|
750
|
+
bullet(ok, `Base URL pointing to Clawzempic`);
|
|
751
|
+
} else if (baseUrlMatch) {
|
|
752
|
+
bullet(warn, `Base URL set to ${baseUrlMatch[1]} — not pointing to Clawzempic`);
|
|
753
|
+
} else {
|
|
754
|
+
bullet(fail, `ANTHROPIC_BASE_URL not set — requests go directly to Anthropic, bypassing Clawzempic`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// 4. Check framework
|
|
758
|
+
const fw = detectFramework(dir);
|
|
759
|
+
if (fw) {
|
|
760
|
+
bullet(ok, `Framework detected: ${fw.name}`);
|
|
761
|
+
} else {
|
|
762
|
+
bullet(info, 'No specific framework detected (generic setup)');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// 5. Check docker-compose for OpenClaw
|
|
766
|
+
for (const f of ['docker-compose.yml', 'docker-compose.yaml']) {
|
|
767
|
+
if (existsSync(join(dir, f))) {
|
|
768
|
+
const dcContent = readFileSync(join(dir, f), 'utf8');
|
|
769
|
+
if (dcContent.includes('env_file') && dcContent.includes('.env')) {
|
|
770
|
+
bullet(ok, `docker-compose uses .env file`);
|
|
771
|
+
} else if (dcContent.includes('ANTHROPIC_API_KEY')) {
|
|
772
|
+
bullet(warn, `docker-compose has hardcoded ANTHROPIC_API_KEY — should use env_file instead`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// 6. Test connection
|
|
778
|
+
blank();
|
|
779
|
+
if (keyMatch && keyMatch[1].startsWith('sk-clwz-')) {
|
|
780
|
+
const success = await runConnectionTest(keyMatch[1]);
|
|
781
|
+
if (success) {
|
|
782
|
+
blank();
|
|
783
|
+
bullet(ok, `${c.green}Everything looks good!${c.reset}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
blank();
|
|
788
|
+
return cleanup();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function cmdHelp() {
|
|
792
|
+
banner();
|
|
793
|
+
console.log(` ${c.bold}Usage:${c.reset}
|
|
794
|
+
|
|
795
|
+
${c.cyan}npx clawzempic${c.reset} Interactive setup (recommended)
|
|
796
|
+
${c.cyan}npx clawzempic init${c.reset} Set up Clawzempic in this project
|
|
797
|
+
${c.cyan}npx clawzempic test${c.reset} Verify your connection works
|
|
798
|
+
${c.cyan}npx clawzempic status${c.reset} Check usage and savings
|
|
799
|
+
${c.cyan}npx clawzempic doctor${c.reset} Diagnose common issues
|
|
800
|
+
|
|
801
|
+
${c.bold}Options:${c.reset}
|
|
802
|
+
|
|
803
|
+
${c.cyan}--key${c.reset} <key> Clawzempic API key (skip prompt)
|
|
804
|
+
${c.cyan}--help${c.reset} Show this help
|
|
805
|
+
${c.cyan}--version${c.reset} Show version
|
|
806
|
+
|
|
807
|
+
${c.bold}What is Clawzempic?${c.reset}
|
|
808
|
+
|
|
809
|
+
Clawzempic is a drop-in proxy for Claude API calls. Point your SDK at
|
|
810
|
+
our base URL and get automatic prompt caching, intelligent model routing
|
|
811
|
+
(Haiku for simple tasks, Opus for complex ones), persistent memory, and
|
|
812
|
+
70-95% cost savings. Works with Anthropic and OpenRouter keys.
|
|
813
|
+
|
|
814
|
+
${c.bold}Quick start:${c.reset}
|
|
815
|
+
|
|
816
|
+
${c.dim}# In your project directory:${c.reset}
|
|
817
|
+
npx clawzempic
|
|
818
|
+
|
|
819
|
+
${c.dim}# That's it. Restart your app and requests are proxied.${c.reset}
|
|
820
|
+
|
|
821
|
+
${c.bold}Links:${c.reset}
|
|
822
|
+
|
|
823
|
+
Docs: ${c.cyan}https://api.clawzempic.ai/docs${c.reset}
|
|
824
|
+
Dashboard: ${c.cyan}https://api.clawzempic.ai/v1/auth/portal${c.reset}
|
|
825
|
+
GitHub: ${c.cyan}https://github.com/naveenspark/sam-router${c.reset}
|
|
826
|
+
`);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ── Arg Parsing ───────────────────────────────────────────────────────────────
|
|
830
|
+
|
|
831
|
+
function parseArgs(argv) {
|
|
832
|
+
const args = { command: null, key: null, help: false, version: false };
|
|
833
|
+
const positional = [];
|
|
834
|
+
|
|
835
|
+
for (let i = 2; i < argv.length; i++) {
|
|
836
|
+
const arg = argv[i];
|
|
837
|
+
if (arg === '--help' || arg === '-h') args.help = true;
|
|
838
|
+
else if (arg === '--version' || arg === '-v') args.version = true;
|
|
839
|
+
else if (arg === '--key' && argv[i + 1]) { args.key = argv[++i]; }
|
|
840
|
+
else if (!arg.startsWith('-')) positional.push(arg);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
args.command = positional[0] || null;
|
|
844
|
+
return args;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────────
|
|
848
|
+
|
|
849
|
+
function cleanup(exitCode = 0) {
|
|
850
|
+
if (rl) {
|
|
851
|
+
rl.close();
|
|
852
|
+
rl = null;
|
|
853
|
+
}
|
|
854
|
+
process.exit(exitCode);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Handle Ctrl+C gracefully
|
|
858
|
+
process.on('SIGINT', () => {
|
|
859
|
+
console.log(`\n\n ${c.dim}Cancelled.${c.reset}\n`);
|
|
860
|
+
cleanup(1);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
864
|
+
|
|
865
|
+
const args = parseArgs(process.argv);
|
|
866
|
+
|
|
867
|
+
if (args.version) {
|
|
868
|
+
console.log(`clawzempic ${VERSION}`);
|
|
869
|
+
process.exit(0);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (args.help && !args.command) {
|
|
873
|
+
cmdHelp();
|
|
874
|
+
process.exit(0);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const command = args.command || 'init';
|
|
878
|
+
|
|
879
|
+
switch (command) {
|
|
880
|
+
case 'init':
|
|
881
|
+
await cmdInit(args);
|
|
882
|
+
break;
|
|
883
|
+
case 'test':
|
|
884
|
+
await cmdTest(args);
|
|
885
|
+
break;
|
|
886
|
+
case 'status':
|
|
887
|
+
await cmdStatus(args);
|
|
888
|
+
break;
|
|
889
|
+
case 'doctor':
|
|
890
|
+
await cmdDoctor();
|
|
891
|
+
break;
|
|
892
|
+
case 'help':
|
|
893
|
+
cmdHelp();
|
|
894
|
+
process.exit(0);
|
|
895
|
+
break;
|
|
896
|
+
default:
|
|
897
|
+
console.error(`Unknown command: ${command}. Run 'npx clawzempic --help' for usage.`);
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawzempic",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Intelligent LLM API proxy — prompt caching, smart routing, memory. Drop-in replacement that cuts your Claude API costs 70-95%.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"clawzempic": "./bin/clawzempic.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"anthropic",
|
|
18
|
+
"claude",
|
|
19
|
+
"openrouter",
|
|
20
|
+
"llm",
|
|
21
|
+
"proxy",
|
|
22
|
+
"api",
|
|
23
|
+
"caching",
|
|
24
|
+
"routing",
|
|
25
|
+
"cost-optimization",
|
|
26
|
+
"ai",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/naveenspark/sam-router"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://clawzempic.ai",
|
|
34
|
+
"author": "Clawzempic"
|
|
35
|
+
}
|