clawzempic 1.0.5 → 1.0.7

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 (2) hide show
  1. package/bin/clawzempic.js +506 -160
  2. package/package.json +1 -1
package/bin/clawzempic.js CHANGED
@@ -12,6 +12,11 @@
12
12
  * npx clawzempic test Verify your connection works
13
13
  * npx clawzempic status Check usage and savings
14
14
  * npx clawzempic doctor Diagnose common issues
15
+ *
16
+ * Flags:
17
+ * --key <key> Provide API key (skip interactive prompt)
18
+ * --yes, -y Auto-confirm all prompts (non-interactive)
19
+ * --no-test Skip connection test after init
15
20
  */
16
21
 
17
22
  import { createInterface } from 'node:readline/promises';
@@ -22,7 +27,8 @@ import { join, basename } from 'node:path';
22
27
  // ── Constants ─────────────────────────────────────────────────────────────────
23
28
 
24
29
  const API_BASE = 'https://api.clawzempic.ai';
25
- const VERSION = '1.0.5';
30
+ const VERSION = '1.0.7';
31
+ const OPENCLAW_DIR_NAME = '.openclaw';
26
32
 
27
33
  // ── ANSI Colors (no dependencies) ─────────────────────────────────────────────
28
34
 
@@ -191,6 +197,8 @@ async function ask(question, opts = {}) {
191
197
  }
192
198
 
193
199
  async function confirm(question, defaultYes = true) {
200
+ // --yes flag auto-confirms everything
201
+ if (globalArgs?.yes) return true;
194
202
  const hint = defaultYes ? 'Y/n' : 'y/N';
195
203
  const answer = await ask(`${question} ${c.dim}(${hint})${c.reset}`, { required: false });
196
204
  if (!answer) return defaultYes;
@@ -280,7 +288,7 @@ const FRAMEWORKS = [
280
288
  return false;
281
289
  },
282
290
  envFile: '.env',
283
- envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
291
+ envVars: { LLM_API_KEY: null, LLM_BASE_URL: API_BASE },
284
292
  },
285
293
  {
286
294
  id: 'nanobot',
@@ -296,11 +304,11 @@ const FRAMEWORKS = [
296
304
  return false;
297
305
  },
298
306
  envFile: '.env',
299
- envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
307
+ envVars: { LLM_API_KEY: null, LLM_BASE_URL: API_BASE },
300
308
  },
301
309
  {
302
310
  id: 'python',
303
- name: 'Python (Anthropic SDK)',
311
+ name: 'Python (Anthropic/OpenRouter SDK)',
304
312
  detect: (dir) => {
305
313
  for (const f of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
306
314
  if (existsSync(join(dir, f))) {
@@ -313,7 +321,7 @@ const FRAMEWORKS = [
313
321
  return false;
314
322
  },
315
323
  envFile: '.env',
316
- envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
324
+ envVars: { LLM_API_KEY: null, LLM_BASE_URL: API_BASE },
317
325
  },
318
326
  {
319
327
  id: 'langchain',
@@ -330,11 +338,11 @@ const FRAMEWORKS = [
330
338
  return false;
331
339
  },
332
340
  envFile: '.env',
333
- envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
341
+ envVars: { LLM_API_KEY: null, LLM_BASE_URL: API_BASE },
334
342
  },
335
343
  {
336
344
  id: 'node',
337
- name: 'Node.js (Anthropic SDK)',
345
+ name: 'Node.js (Anthropic/OpenRouter SDK)',
338
346
  detect: (dir) => {
339
347
  if (existsSync(join(dir, 'package.json'))) {
340
348
  try {
@@ -345,7 +353,7 @@ const FRAMEWORKS = [
345
353
  return false;
346
354
  },
347
355
  envFile: '.env',
348
- envVars: { ANTHROPIC_API_KEY: null, ANTHROPIC_BASE_URL: API_BASE },
356
+ envVars: { LLM_API_KEY: null, LLM_BASE_URL: API_BASE },
349
357
  },
350
358
  ];
351
359
 
@@ -357,17 +365,26 @@ function detectFramework(dir) {
357
365
  }
358
366
 
359
367
  function detectExistingConfig(dir) {
360
- // Check if already configured
368
+ // Check OpenClaw auth-profiles.json first
369
+ const openclawDir = findOpenClawDir(dir);
370
+ if (openclawDir) {
371
+ const key = findKeyInAuthProfiles(openclawDir);
372
+ if (key) {
373
+ return { configured: true, envPath: join(openclawDir, 'auth-profiles.json'), isOpenClaw: true };
374
+ }
375
+ }
376
+
377
+ // Check .env file
361
378
  const envPath = join(dir, '.env');
362
379
  if (existsSync(envPath)) {
363
380
  try {
364
381
  const content = readFileSync(envPath, 'utf8');
365
382
  if (content.includes('clawzempic.ai') || content.includes('sk-clwz-') || content.includes('CLAWZEMPIC_API_KEY')) {
366
- return { configured: true, envPath };
383
+ return { configured: true, envPath, isOpenClaw: false };
367
384
  }
368
385
  } catch {}
369
386
  }
370
- return { configured: false, envPath };
387
+ return { configured: false, envPath, isOpenClaw: false };
371
388
  }
372
389
 
373
390
  // ── Config Patching ───────────────────────────────────────────────────────────
@@ -412,6 +429,175 @@ function patchEnvFile(envPath, vars) {
412
429
  return changes;
413
430
  }
414
431
 
432
+ // ── OpenClaw v2026.2+ Helpers ─────────────────────────────────────────────────
433
+
434
+ /**
435
+ * Find the .openclaw directory. Checks cwd first, then home dir.
436
+ */
437
+ function findOpenClawDir(dir) {
438
+ const local = join(dir, OPENCLAW_DIR_NAME);
439
+ if (existsSync(local)) return local;
440
+ const home = join(process.env.HOME || process.env.USERPROFILE || '', OPENCLAW_DIR_NAME);
441
+ if (existsSync(home)) return home;
442
+ return null;
443
+ }
444
+
445
+ /**
446
+ * Check if this is OpenClaw v2026.2+ (has auth-profiles.json).
447
+ */
448
+ function isModernOpenClaw(openclawDir) {
449
+ return existsSync(join(openclawDir, 'auth-profiles.json'));
450
+ }
451
+
452
+ /**
453
+ * Read and patch auth-profiles.json to add/update a clawzempic provider profile.
454
+ */
455
+ function patchAuthProfiles(openclawDir, apiKey) {
456
+ const profilesPath = join(openclawDir, 'auth-profiles.json');
457
+ let profiles = { version: 1, profiles: {} };
458
+
459
+ if (existsSync(profilesPath)) {
460
+ // Back up
461
+ const backupPath = profilesPath + '.bak.clawzempic';
462
+ if (!existsSync(backupPath)) {
463
+ copyFileSync(profilesPath, backupPath);
464
+ bullet(info, `Backed up ${c.dim}auth-profiles.json${c.reset} ${arrow} ${c.dim}auth-profiles.json.bak.clawzempic${c.reset}`);
465
+ }
466
+ try {
467
+ profiles = JSON.parse(readFileSync(profilesPath, 'utf8'));
468
+ } catch {
469
+ bullet(warn, 'Could not parse auth-profiles.json, creating fresh one.');
470
+ }
471
+ }
472
+
473
+ const hadClawzempic = !!profiles.profiles?.clawzempic;
474
+
475
+ if (!profiles.profiles) profiles.profiles = {};
476
+ profiles.profiles.clawzempic = {
477
+ type: 'api_key',
478
+ provider: 'anthropic',
479
+ key: apiKey,
480
+ baseUrl: API_BASE,
481
+ createdAt: new Date().toISOString(),
482
+ };
483
+
484
+ writeFileSync(profilesPath, JSON.stringify(profiles, null, 2) + '\n', 'utf8');
485
+ return { action: hadClawzempic ? 'updated' : 'added', profilesPath };
486
+ }
487
+
488
+ /**
489
+ * Read and patch openclaw.json to set model to use clawzempic provider.
490
+ */
491
+ function patchOpenClawConfig(openclawDir) {
492
+ const configPath = join(openclawDir, 'openclaw.json');
493
+ if (!existsSync(configPath)) return { patched: false, reason: 'no openclaw.json' };
494
+
495
+ let config;
496
+ try {
497
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
498
+ } catch {
499
+ return { patched: false, reason: 'could not parse openclaw.json' };
500
+ }
501
+
502
+ // Back up
503
+ const backupPath = configPath + '.bak.clawzempic';
504
+ if (!existsSync(backupPath)) {
505
+ copyFileSync(configPath, backupPath);
506
+ }
507
+
508
+ // Update model to use clawzempic provider
509
+ if (!config.agents) config.agents = {};
510
+ if (!config.agents.defaults) config.agents.defaults = {};
511
+ if (!config.agents.defaults.model) config.agents.defaults.model = {};
512
+
513
+ const oldModel = config.agents.defaults.model.primary || 'none';
514
+ config.agents.defaults.model.primary = 'clawzempic/claude-sonnet-4-5-20250929';
515
+
516
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
517
+ return { patched: true, oldModel, configPath };
518
+ }
519
+
520
+ /**
521
+ * Check OpenClaw gateway auth status.
522
+ */
523
+ function checkGatewayAuth(openclawDir) {
524
+ // Check openclaw.json for gateway.auth
525
+ const configPath = join(openclawDir, 'openclaw.json');
526
+ if (!existsSync(configPath)) return { hasAuth: false, mode: 'unknown' };
527
+
528
+ try {
529
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
530
+ const auth = config.gateway?.auth;
531
+ if (!auth || !auth.mode || auth.mode === 'none') {
532
+ return { hasAuth: false, mode: auth?.mode || 'none' };
533
+ }
534
+ return { hasAuth: true, mode: auth.mode };
535
+ } catch {
536
+ return { hasAuth: false, mode: 'unknown' };
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Find a Clawzempic API key from OpenClaw auth-profiles.json.
542
+ */
543
+ function findKeyInAuthProfiles(openclawDir) {
544
+ if (!openclawDir) return null;
545
+ const profilesPath = join(openclawDir, 'auth-profiles.json');
546
+ if (!existsSync(profilesPath)) return null;
547
+
548
+ try {
549
+ const profiles = JSON.parse(readFileSync(profilesPath, 'utf8'));
550
+ // Check clawzempic profile first
551
+ if (profiles.profiles?.clawzempic?.key) {
552
+ return profiles.profiles.clawzempic.key;
553
+ }
554
+ // Check any profile that points to clawzempic.ai
555
+ for (const [, profile] of Object.entries(profiles.profiles || {})) {
556
+ if (profile.baseUrl?.includes('clawzempic.ai') && profile.key) {
557
+ return profile.key;
558
+ }
559
+ }
560
+ } catch {}
561
+ return null;
562
+ }
563
+
564
+ /**
565
+ * Find API key from all possible locations.
566
+ */
567
+ function findApiKey(dir) {
568
+ // 1. Environment variables (check new names first, fall back to old)
569
+ const envKey = process.env.CLAWZEMPIC_API_KEY || process.env.LLM_API_KEY || process.env.ANTHROPIC_API_KEY;
570
+ if (envKey && (envKey.startsWith('sk-clwz-') || envKey.startsWith('sr_'))) {
571
+ return { key: envKey, source: 'environment variable' };
572
+ }
573
+
574
+ // 2. OpenClaw auth-profiles.json
575
+ const openclawDir = findOpenClawDir(dir);
576
+ if (openclawDir) {
577
+ const authKey = findKeyInAuthProfiles(openclawDir);
578
+ if (authKey && (authKey.startsWith('sk-clwz-') || authKey.startsWith('sr_'))) {
579
+ return { key: authKey, source: 'OpenClaw auth-profiles.json' };
580
+ }
581
+ }
582
+
583
+ // 3. .env file in cwd
584
+ const envPath = join(dir, '.env');
585
+ if (existsSync(envPath)) {
586
+ try {
587
+ const content = readFileSync(envPath, 'utf8');
588
+ const match = content.match(/^(?:CLAWZEMPIC_API_KEY|LLM_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
589
+ if (match) {
590
+ const val = match[1].trim();
591
+ if (val.startsWith('sk-clwz-') || val.startsWith('sr_')) {
592
+ return { key: val, source: '.env file' };
593
+ }
594
+ }
595
+ } catch {}
596
+ }
597
+
598
+ return null;
599
+ }
600
+
415
601
  // ── Commands ──────────────────────────────────────────────────────────────────
416
602
 
417
603
  async function cmdInit(args) {
@@ -536,86 +722,160 @@ async function cmdInit(args) {
536
722
 
537
723
  heading('Configuration');
538
724
 
539
- const envFile = framework?.envFile || '.env';
540
- const envPath = join(dir, envFile);
541
-
542
- // Build env vars: proxy key + base URL. Keep provider key separate.
543
- const envVars = {
544
- CLAWZEMPIC_API_KEY: clawzempicKey,
545
- ANTHROPIC_BASE_URL: API_BASE,
546
- };
725
+ // OpenClaw v2026.2+ gets special treatment: patch auth-profiles.json + openclaw.json
726
+ const openclawDir = findOpenClawDir(dir);
727
+ const useOpenClawPath = framework?.id === 'openclaw' && openclawDir && isModernOpenClaw(openclawDir);
547
728
 
548
- // Show what we'll do
549
- bullet(info, `Will update ${c.bold}${envFile}${c.reset}:`);
550
- for (const [key, value] of Object.entries(envVars)) {
551
- const display = key.includes('KEY') ? value.substring(0, 12) + '...' : value;
552
- console.log(` ${c.green}+${c.reset} ${key}=${display}`);
553
- }
554
- blank();
729
+ if (useOpenClawPath) {
730
+ bullet(ok, `OpenClaw v2026.2+ detected at ${c.dim}${openclawDir}${c.reset}`);
731
+ blank();
555
732
 
556
- const applyChanges = await confirm('Apply these changes?');
557
- if (!applyChanges) {
558
- bullet(info, 'No changes made. You can set these manually:');
733
+ // Show what we'll do
734
+ bullet(info, `Will update ${c.bold}auth-profiles.json${c.reset}:`);
735
+ console.log(` ${c.green}+${c.reset} Add "clawzempic" provider (baseUrl: ${API_BASE})`);
736
+ console.log(` ${c.green}+${c.reset} API key: ${clawzempicKey.substring(0, 12)}...`);
737
+ blank();
738
+ bullet(info, `Will update ${c.bold}openclaw.json${c.reset}:`);
739
+ console.log(` ${c.green}+${c.reset} Set default model to clawzempic/claude-sonnet-4-5-20250929`);
559
740
  blank();
741
+
742
+ const applyChanges = await confirm('Apply these changes?');
743
+ if (!applyChanges) {
744
+ bullet(info, 'No changes made. You can configure manually:');
745
+ blank();
746
+ bullet(info, `Add to ${c.bold}auth-profiles.json${c.reset} → profiles.clawzempic:`);
747
+ console.log(` { "type": "api_key", "provider": "anthropic", "key": "${clawzempicKey}", "baseUrl": "${API_BASE}" }`);
748
+ blank();
749
+ bullet(info, `Set in ${c.bold}openclaw.json${c.reset} → agents.defaults.model.primary:`);
750
+ console.log(` "clawzempic/claude-sonnet-4-5-20250929"`);
751
+ blank();
752
+ return cleanup();
753
+ }
754
+
755
+ // Patch auth profiles
756
+ const profileResult = patchAuthProfiles(openclawDir, clawzempicKey);
757
+ bullet(ok, `${profileResult.action === 'updated' ? 'Updated' : 'Added'} clawzempic profile in ${c.bold}auth-profiles.json${c.reset}`);
758
+
759
+ // Patch openclaw.json model config
760
+ const configResult = patchOpenClawConfig(openclawDir);
761
+ if (configResult.patched) {
762
+ bullet(ok, `Set default model to ${c.bold}clawzempic/claude-sonnet-4-5-20250929${c.reset}`);
763
+ if (configResult.oldModel !== 'none') {
764
+ bullet(info, `Previous model: ${c.dim}${configResult.oldModel}${c.reset}`);
765
+ }
766
+ } else {
767
+ bullet(warn, `Could not update openclaw.json: ${configResult.reason}`);
768
+ }
769
+
770
+ // Check gateway auth
771
+ const gwAuth = checkGatewayAuth(openclawDir);
772
+ if (!gwAuth.hasAuth) {
773
+ blank();
774
+ bullet(warn, `Gateway auth is ${c.bold}${gwAuth.mode}${c.reset} — v2026.2+ requires a password or token.`);
775
+ bullet(info, `Set gateway.auth in openclaw.json to "password" or "token" mode.`);
776
+ bullet(info, `Example: ${c.dim}{ "gateway": { "auth": { "mode": "password" } } }${c.reset}`);
777
+ } else {
778
+ bullet(ok, `Gateway auth: ${c.bold}${gwAuth.mode}${c.reset}`);
779
+ }
780
+ } else {
781
+ // Generic .env path (non-OpenClaw frameworks)
782
+ const envFile = framework?.envFile || '.env';
783
+ const envPath = join(dir, envFile);
784
+
785
+ // Build env vars: proxy key + base URL. Keep provider key separate.
786
+ const envVars = {
787
+ CLAWZEMPIC_API_KEY: clawzempicKey,
788
+ LLM_BASE_URL: API_BASE,
789
+ };
790
+
791
+ // Show what we'll do
792
+ bullet(info, `Will update ${c.bold}${envFile}${c.reset}:`);
560
793
  for (const [key, value] of Object.entries(envVars)) {
561
- console.log(` ${key}=${value}`);
794
+ const display = key.includes('KEY') ? value.substring(0, 12) + '...' : value;
795
+ console.log(` ${c.green}+${c.reset} ${key}=${display}`);
562
796
  }
563
797
  blank();
564
- return cleanup();
565
- }
566
798
 
567
- // Clean up v1.0.3 bug: it wrote Clawzempic keys into ANTHROPIC_API_KEY.
568
- // Only remove if the value is clearly not an Anthropic key (sr_ or sk-clwz- prefix).
569
- if (existsSync(envPath)) {
570
- const existing = readFileSync(envPath, 'utf8');
571
- const badMatch = existing.match(/^ANTHROPIC_API_KEY\s*=\s*(.+)$/m);
572
- if (badMatch) {
573
- const badVal = badMatch[1].trim();
574
- if (badVal.startsWith('sr_') || badVal.startsWith('sk-clwz-')) {
575
- const cleaned = existing.replace(/^ANTHROPIC_API_KEY\s*=\s*(?:sr_|sk-clwz-).+\n?/m, '');
576
- writeFileSync(envPath, cleaned, 'utf8');
577
- bullet(warn, `Removed stale ANTHROPIC_API_KEY (had a Clawzempic key from v1.0.3 bug)`);
799
+ const applyChanges = await confirm('Apply these changes?');
800
+ if (!applyChanges) {
801
+ bullet(info, 'No changes made. You can set these manually:');
802
+ blank();
803
+ for (const [key, value] of Object.entries(envVars)) {
804
+ console.log(` ${key}=${value}`);
578
805
  }
806
+ blank();
807
+ return cleanup();
579
808
  }
580
- }
581
809
 
582
- // Apply
583
- const changes = patchEnvFile(envPath, envVars);
584
- for (const change of changes) {
585
- if (change.action === 'updated') {
586
- bullet(ok, `Updated ${c.bold}${change.key}${c.reset}`);
587
- } else {
588
- bullet(ok, `Added ${c.bold}${change.key}${c.reset}`);
810
+ // Clean up legacy: remove Clawzempic keys from ANTHROPIC_API_KEY (v1.0.3 bug + old naming)
811
+ if (existsSync(envPath)) {
812
+ const existing = readFileSync(envPath, 'utf8');
813
+ const badMatch = existing.match(/^ANTHROPIC_API_KEY\s*=\s*(.+)$/m);
814
+ if (badMatch) {
815
+ const badVal = badMatch[1].trim();
816
+ if (badVal.startsWith('sr_') || badVal.startsWith('sk-clwz-')) {
817
+ const cleaned = existing.replace(/^ANTHROPIC_API_KEY\s*=\s*(?:sr_|sk-clwz-).+\n?/m, '');
818
+ writeFileSync(envPath, cleaned, 'utf8');
819
+ bullet(warn, `Removed stale ANTHROPIC_API_KEY (had a Clawzempic key — use CLAWZEMPIC_API_KEY instead)`);
820
+ }
821
+ }
822
+ // Also clean up old ANTHROPIC_BASE_URL pointing to clawzempic
823
+ const oldBaseMatch = existing.match(/^ANTHROPIC_BASE_URL\s*=\s*(.+)$/m);
824
+ if (oldBaseMatch && oldBaseMatch[1].includes('clawzempic.ai')) {
825
+ const cleaned2 = existing.replace(/^ANTHROPIC_BASE_URL\s*=\s*.+clawzempic\.ai.+\n?/m, '');
826
+ writeFileSync(envPath, cleaned2, 'utf8');
827
+ bullet(warn, `Migrated ANTHROPIC_BASE_URL → LLM_BASE_URL`);
828
+ }
829
+ }
830
+
831
+ // Apply
832
+ const changes = patchEnvFile(envPath, envVars);
833
+ for (const change of changes) {
834
+ if (change.action === 'updated') {
835
+ bullet(ok, `Updated ${c.bold}${change.key}${c.reset}`);
836
+ } else {
837
+ bullet(ok, `Added ${c.bold}${change.key}${c.reset}`);
838
+ }
589
839
  }
590
840
  }
591
841
 
592
842
  // ── Test Connection ──────────────────────────────────────────────────────
593
843
 
594
844
  blank();
595
- const runTest = await confirm('Test the connection?');
596
- let testPassed = !runTest; // skip = assume ok
597
- if (runTest) {
598
- testPassed = await runConnectionTest(clawzempicKey);
845
+ let testPassed = true;
846
+ if (args.noTest) {
847
+ bullet(info, 'Skipping connection test (--no-test)');
848
+ } else {
849
+ const runTest = await confirm('Test the connection?');
850
+ testPassed = !runTest; // skip = assume ok
851
+ if (runTest) {
852
+ testPassed = await runConnectionTest(clawzempicKey);
853
+ }
599
854
  }
600
855
 
601
856
  // ── Done ─────────────────────────────────────────────────────────────────
602
857
 
603
858
  blank();
604
859
  if (testPassed) {
605
- box([
860
+ const doneLines = [
606
861
  `${c.green}${c.bold}Setup complete!${c.reset}`,
607
862
  '',
608
863
  `${c.dim}Dashboard:${c.reset} ${API_BASE}/v1/auth/portal`,
609
864
  `${c.dim}Insights:${c.reset} ${API_BASE}/v1/insights`,
610
865
  `${c.dim}Docs:${c.reset} ${API_BASE}/docs`,
611
866
  '',
612
- `${c.yellow}Restart your app to start saving.${c.reset}`,
613
- ]);
867
+ ];
868
+ if (useOpenClawPath) {
869
+ doneLines.push(`${c.yellow}Restart your gateway:${c.reset} ${c.cyan}openclaw gateway restart${c.reset}`);
870
+ } else {
871
+ doneLines.push(`${c.yellow}Restart your app to start saving.${c.reset}`);
872
+ }
873
+ box(doneLines);
614
874
  } else {
615
875
  box([
616
876
  `${c.red}${c.bold}Setup incomplete${c.reset} — connection test failed.`,
617
877
  '',
618
- `${c.dim}Config was written to ${envFile}, but your key didn't authenticate.${c.reset}`,
878
+ `${c.dim}Config was written, but your key didn't authenticate.${c.reset}`,
619
879
  `${c.dim}Check your key and run:${c.reset} ${c.cyan}npx clawzempic test${c.reset}`,
620
880
  ]);
621
881
  }
@@ -656,19 +916,14 @@ async function runConnectionTest(apiKey) {
656
916
  async function cmdTest(args) {
657
917
  banner();
658
918
 
659
- // Find API key: arg > env > .env file
919
+ // Find API key: arg > env > auth-profiles > .env
660
920
  let apiKey = args.key;
661
921
 
662
922
  if (!apiKey) {
663
- apiKey = process.env.CLAWZEMPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
664
- }
665
-
666
- if (!apiKey) {
667
- const envPath = join(process.cwd(), '.env');
668
- if (existsSync(envPath)) {
669
- const content = readFileSync(envPath, 'utf8');
670
- const match = content.match(/^(?:CLAWZEMPIC_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
671
- if (match) apiKey = match[1].trim();
923
+ const found = findApiKey(process.cwd());
924
+ if (found) {
925
+ apiKey = found.key;
926
+ bullet(info, `Found key in ${c.dim}${found.source}${c.reset}`);
672
927
  }
673
928
  }
674
929
 
@@ -694,15 +949,8 @@ async function cmdStatus(args) {
694
949
  // Find API key
695
950
  let apiKey = args.key;
696
951
  if (!apiKey) {
697
- apiKey = process.env.CLAWZEMPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
698
- }
699
- if (!apiKey) {
700
- const envPath = join(process.cwd(), '.env');
701
- if (existsSync(envPath)) {
702
- const content = readFileSync(envPath, 'utf8');
703
- const match = content.match(/^(?:CLAWZEMPIC_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
704
- if (match) apiKey = match[1].trim();
705
- }
952
+ const found = findApiKey(process.cwd());
953
+ if (found) apiKey = found.key;
706
954
  }
707
955
 
708
956
  if (!apiKey || (!apiKey.startsWith('sk-clwz-') && !apiKey.startsWith('sr_'))) {
@@ -766,66 +1014,152 @@ async function cmdDoctor() {
766
1014
 
767
1015
  const dir = process.cwd();
768
1016
 
769
- // 1. Check .env exists
770
- const envPath = join(dir, '.env');
771
- if (existsSync(envPath)) {
772
- bullet(ok, `.env file found`);
773
- } else {
774
- bullet(fail, `No .env file in current directory`);
775
- bullet(info, 'Run `npx clawzempic` to set up.');
776
- return cleanup(1);
777
- }
1017
+ // Check if OpenClaw v2026.2+
1018
+ const openclawDir = findOpenClawDir(dir);
1019
+ const fw = detectFramework(dir);
778
1020
 
779
- // 2. Check for Clawzempic config
780
- const content = readFileSync(envPath, 'utf8');
781
- const keyMatch = content.match(/^(?:CLAWZEMPIC_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
782
- if (keyMatch && (keyMatch[1].startsWith('sk-clwz-') || keyMatch[1].startsWith('sr_'))) {
783
- bullet(ok, `Clawzempic API key found`);
784
- } else if (keyMatch) {
785
- bullet(warn, `API key found but does not look like a Clawzempic key (expected sk-clwz-... or sr_...)`);
786
- bullet(info, 'You may be using a direct provider key. Run `npx clawzempic` to set up proxy routing.');
787
- } else {
788
- bullet(fail, 'No API key found in .env');
789
- return cleanup(1);
790
- }
1021
+ if (openclawDir && isModernOpenClaw(openclawDir)) {
1022
+ // ── OpenClaw v2026.2+ Doctor ──────────────────────────────────────────
1023
+ bullet(ok, `OpenClaw v2026.2+ detected at ${c.dim}${openclawDir}${c.reset}`);
791
1024
 
792
- // 3. Check base URL
793
- const baseUrlMatch = content.match(/^ANTHROPIC_BASE_URL\s*=\s*(.+)$/m);
794
- if (baseUrlMatch && baseUrlMatch[1].includes('clawzempic.ai')) {
795
- bullet(ok, `Base URL pointing to Clawzempic`);
796
- } else if (baseUrlMatch) {
797
- bullet(warn, `Base URL set to ${baseUrlMatch[1]} — not pointing to Clawzempic`);
798
- } else {
799
- bullet(fail, `ANTHROPIC_BASE_URL not set — requests go directly to Anthropic, bypassing Clawzempic`);
800
- }
1025
+ // Check auth-profiles.json for clawzempic profile
1026
+ const profilesPath = join(openclawDir, 'auth-profiles.json');
1027
+ let profiles;
1028
+ try {
1029
+ profiles = JSON.parse(readFileSync(profilesPath, 'utf8'));
1030
+ } catch {
1031
+ bullet(fail, 'Could not parse auth-profiles.json');
1032
+ return cleanup(1);
1033
+ }
801
1034
 
802
- // 4. Check framework
803
- const fw = detectFramework(dir);
804
- if (fw) {
805
- bullet(ok, `Framework detected: ${fw.name}`);
806
- } else {
807
- bullet(info, 'No specific framework detected (generic setup)');
808
- }
1035
+ const clawzempicProfile = profiles.profiles?.clawzempic;
1036
+ if (clawzempicProfile) {
1037
+ bullet(ok, `Clawzempic provider profile found`);
1038
+ if (clawzempicProfile.baseUrl?.includes('clawzempic.ai')) {
1039
+ bullet(ok, `Base URL: ${c.dim}${clawzempicProfile.baseUrl}${c.reset}`);
1040
+ } else {
1041
+ bullet(fail, `Base URL is ${c.dim}${clawzempicProfile.baseUrl}${c.reset} — should be ${API_BASE}`);
1042
+ }
1043
+ } else {
1044
+ // Check if any profile points to clawzempic
1045
+ let foundClawzempic = false;
1046
+ for (const [name, profile] of Object.entries(profiles.profiles || {})) {
1047
+ if (profile.baseUrl?.includes('clawzempic.ai')) {
1048
+ bullet(ok, `Clawzempic configured in "${name}" profile (baseUrl: ${c.dim}${profile.baseUrl}${c.reset})`);
1049
+ foundClawzempic = true;
1050
+ break;
1051
+ }
1052
+ }
1053
+ if (!foundClawzempic) {
1054
+ bullet(fail, 'No Clawzempic provider in auth-profiles.json');
1055
+ bullet(info, 'Run `npx clawzempic` to add it.');
1056
+ }
1057
+ }
809
1058
 
810
- // 5. Check docker-compose for OpenClaw
811
- for (const f of ['docker-compose.yml', 'docker-compose.yaml']) {
812
- if (existsSync(join(dir, f))) {
813
- const dcContent = readFileSync(join(dir, f), 'utf8');
814
- if (dcContent.includes('env_file') && dcContent.includes('.env')) {
815
- bullet(ok, `docker-compose uses .env file`);
816
- } else if (dcContent.includes('ANTHROPIC_API_KEY')) {
817
- bullet(warn, `docker-compose has hardcoded ANTHROPIC_API_KEY — should use env_file instead`);
1059
+ // Check openclaw.json model config
1060
+ const configPath = join(openclawDir, 'openclaw.json');
1061
+ if (existsSync(configPath)) {
1062
+ try {
1063
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
1064
+ const model = config.agents?.defaults?.model?.primary || 'not set';
1065
+ if (model.startsWith('clawzempic/')) {
1066
+ bullet(ok, `Default model: ${c.bold}${model}${c.reset}`);
1067
+ } else {
1068
+ bullet(info, `Default model: ${c.dim}${model}${c.reset} (not using clawzempic provider)`);
1069
+ }
1070
+ } catch {
1071
+ bullet(warn, 'Could not parse openclaw.json');
818
1072
  }
819
1073
  }
820
- }
821
1074
 
822
- // 6. Test connection
823
- blank();
824
- if (keyMatch && (keyMatch[1].startsWith('sk-clwz-') || keyMatch[1].startsWith('sr_'))) {
825
- const success = await runConnectionTest(keyMatch[1]);
826
- if (success) {
1075
+ // Check gateway auth
1076
+ const gwAuth = checkGatewayAuth(openclawDir);
1077
+ if (gwAuth.hasAuth) {
1078
+ bullet(ok, `Gateway auth: ${c.bold}${gwAuth.mode}${c.reset}`);
1079
+ } else {
1080
+ bullet(warn, `Gateway auth is ${c.bold}${gwAuth.mode}${c.reset} — v2026.2+ requires a password or token`);
1081
+ }
1082
+
1083
+ // Test connection
1084
+ const apiKeyResult = findApiKey(dir);
1085
+ if (apiKeyResult) {
1086
+ blank();
1087
+ const success = await runConnectionTest(apiKeyResult.key);
1088
+ if (success) {
1089
+ blank();
1090
+ bullet(ok, `${c.green}Everything looks good!${c.reset}`);
1091
+ }
1092
+ } else {
827
1093
  blank();
828
- bullet(ok, `${c.green}Everything looks good!${c.reset}`);
1094
+ bullet(warn, 'No Clawzempic API key found to test connection.');
1095
+ }
1096
+
1097
+ } else {
1098
+ // ── Generic Doctor ────────────────────────────────────────────────────
1099
+
1100
+ // 1. Check .env exists
1101
+ const envPath = join(dir, '.env');
1102
+ if (existsSync(envPath)) {
1103
+ bullet(ok, `.env file found`);
1104
+ } else {
1105
+ bullet(fail, `No .env file in current directory`);
1106
+ if (openclawDir) {
1107
+ bullet(info, `OpenClaw detected but no auth-profiles.json — may be an older version.`);
1108
+ }
1109
+ bullet(info, 'Run `npx clawzempic` to set up.');
1110
+ return cleanup(1);
1111
+ }
1112
+
1113
+ // 2. Check for Clawzempic config
1114
+ const content = readFileSync(envPath, 'utf8');
1115
+ const keyMatch = content.match(/^(?:CLAWZEMPIC_API_KEY|LLM_API_KEY|ANTHROPIC_API_KEY)\s*=\s*(.+)$/m);
1116
+ if (keyMatch && (keyMatch[1].startsWith('sk-clwz-') || keyMatch[1].startsWith('sr_'))) {
1117
+ bullet(ok, `Clawzempic API key found`);
1118
+ } else if (keyMatch) {
1119
+ bullet(warn, `API key found but does not look like a Clawzempic key (expected sk-clwz-... or sr_...)`);
1120
+ bullet(info, 'You may be using a direct provider key. Run `npx clawzempic` to set up proxy routing.');
1121
+ } else {
1122
+ bullet(fail, 'No API key found in .env');
1123
+ return cleanup(1);
1124
+ }
1125
+
1126
+ // 3. Check base URL (check LLM_BASE_URL first, fall back to ANTHROPIC_BASE_URL)
1127
+ const baseUrlMatch = content.match(/^(?:LLM_BASE_URL|ANTHROPIC_BASE_URL)\s*=\s*(.+)$/m);
1128
+ if (baseUrlMatch && baseUrlMatch[1].includes('clawzempic.ai')) {
1129
+ bullet(ok, `Base URL pointing to Clawzempic`);
1130
+ } else if (baseUrlMatch) {
1131
+ bullet(warn, `Base URL set to ${baseUrlMatch[1]} — not pointing to Clawzempic`);
1132
+ } else {
1133
+ bullet(fail, `LLM_BASE_URL not set — requests go directly to your provider, bypassing Clawzempic`);
1134
+ }
1135
+
1136
+ // 4. Check framework
1137
+ if (fw) {
1138
+ bullet(ok, `Framework detected: ${fw.name}`);
1139
+ } else {
1140
+ bullet(info, 'No specific framework detected (generic setup)');
1141
+ }
1142
+
1143
+ // 5. Check docker-compose for OpenClaw
1144
+ for (const f of ['docker-compose.yml', 'docker-compose.yaml']) {
1145
+ if (existsSync(join(dir, f))) {
1146
+ const dcContent = readFileSync(join(dir, f), 'utf8');
1147
+ if (dcContent.includes('env_file') && dcContent.includes('.env')) {
1148
+ bullet(ok, `docker-compose uses .env file`);
1149
+ } else if (dcContent.includes('ANTHROPIC_API_KEY')) {
1150
+ bullet(warn, `docker-compose has hardcoded ANTHROPIC_API_KEY — should use env_file instead`);
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ // 6. Test connection
1156
+ blank();
1157
+ if (keyMatch && (keyMatch[1].startsWith('sk-clwz-') || keyMatch[1].startsWith('sr_'))) {
1158
+ const success = await runConnectionTest(keyMatch[1]);
1159
+ if (success) {
1160
+ blank();
1161
+ bullet(ok, `${c.green}Everything looks good!${c.reset}`);
1162
+ }
829
1163
  }
830
1164
  }
831
1165
 
@@ -874,13 +1208,15 @@ function cmdHelp() {
874
1208
  // ── Arg Parsing ───────────────────────────────────────────────────────────────
875
1209
 
876
1210
  function parseArgs(argv) {
877
- const args = { command: null, key: null, help: false, version: false };
1211
+ const args = { command: null, key: null, help: false, version: false, yes: false, noTest: false };
878
1212
  const positional = [];
879
1213
 
880
1214
  for (let i = 2; i < argv.length; i++) {
881
1215
  const arg = argv[i];
882
1216
  if (arg === '--help' || arg === '-h') args.help = true;
883
1217
  else if (arg === '--version' || arg === '-v') args.version = true;
1218
+ else if (arg === '--yes' || arg === '-y') args.yes = true;
1219
+ else if (arg === '--no-test') args.noTest = true;
884
1220
  else if (arg === '--key' && argv[i + 1]) { args.key = argv[++i]; }
885
1221
  else if (!arg.startsWith('-')) positional.push(arg);
886
1222
  }
@@ -907,38 +1243,48 @@ process.on('SIGINT', () => {
907
1243
 
908
1244
  // ── Main ──────────────────────────────────────────────────────────────────────
909
1245
 
910
- const args = parseArgs(process.argv);
911
-
912
- if (args.version) {
913
- console.log(`clawzempic ${VERSION}`);
914
- process.exit(0);
915
- }
1246
+ let globalArgs = null;
916
1247
 
917
- if (args.help && !args.command) {
918
- cmdHelp();
919
- process.exit(0);
920
- }
1248
+ async function main() {
1249
+ const args = parseArgs(process.argv);
1250
+ globalArgs = args;
921
1251
 
922
- const command = args.command || 'init';
1252
+ if (args.version) {
1253
+ console.log(`clawzempic ${VERSION}`);
1254
+ process.exit(0);
1255
+ }
923
1256
 
924
- switch (command) {
925
- case 'init':
926
- await cmdInit(args);
927
- break;
928
- case 'test':
929
- await cmdTest(args);
930
- break;
931
- case 'status':
932
- await cmdStatus(args);
933
- break;
934
- case 'doctor':
935
- await cmdDoctor();
936
- break;
937
- case 'help':
1257
+ if (args.help && !args.command) {
938
1258
  cmdHelp();
939
1259
  process.exit(0);
940
- break;
941
- default:
942
- console.error(`Unknown command: ${command}. Run 'npx clawzempic --help' for usage.`);
943
- process.exit(1);
1260
+ }
1261
+
1262
+ const command = args.command || 'init';
1263
+
1264
+ switch (command) {
1265
+ case 'init':
1266
+ await cmdInit(args);
1267
+ break;
1268
+ case 'test':
1269
+ await cmdTest(args);
1270
+ break;
1271
+ case 'status':
1272
+ await cmdStatus(args);
1273
+ break;
1274
+ case 'doctor':
1275
+ await cmdDoctor();
1276
+ break;
1277
+ case 'help':
1278
+ cmdHelp();
1279
+ process.exit(0);
1280
+ break;
1281
+ default:
1282
+ console.error(`Unknown command: ${command}. Run 'npx clawzempic --help' for usage.`);
1283
+ process.exit(1);
1284
+ }
944
1285
  }
1286
+
1287
+ main().catch(err => {
1288
+ console.error(`\n ${c.red}Fatal:${c.reset} ${err.message}\n`);
1289
+ process.exit(1);
1290
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawzempic",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Intelligent LLM API proxy — prompt caching, smart routing, memory. Drop-in replacement that cuts your Claude API costs 70-95%.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",