@caliber-ai/cli 0.2.0 → 0.3.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/dist/bin.js +1304 -317
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +5 -2
package/dist/bin.js CHANGED
@@ -32,8 +32,8 @@ __export(constants_exports, {
32
32
  FRONTEND_URL: () => FRONTEND_URL,
33
33
  MANIFEST_FILE: () => MANIFEST_FILE
34
34
  });
35
- import path from "path";
36
- import os from "os";
35
+ import path2 from "path";
36
+ import os2 from "os";
37
37
  var API_URL, FRONTEND_URL, CLI_CALLBACK_PORT, AUTH_DIR, AUTH_FILE, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR;
38
38
  var init_constants = __esm({
39
39
  "src/constants.ts"() {
@@ -41,39 +41,65 @@ var init_constants = __esm({
41
41
  API_URL = process.env.CALIBER_API_URL || "https://caliber-backend.up.railway.app";
42
42
  FRONTEND_URL = process.env.CALIBER_FRONTEND_URL || "https://caliber-app.up.railway.app";
43
43
  CLI_CALLBACK_PORT = 19284;
44
- AUTH_DIR = path.join(os.homedir(), ".caliber");
45
- AUTH_FILE = path.join(AUTH_DIR, "auth.json");
44
+ AUTH_DIR = path2.join(os2.homedir(), ".caliber");
45
+ AUTH_FILE = path2.join(AUTH_DIR, "auth.json");
46
46
  CALIBER_DIR = ".caliber";
47
- MANIFEST_FILE = path.join(CALIBER_DIR, "manifest.json");
48
- BACKUPS_DIR = path.join(CALIBER_DIR, "backups");
47
+ MANIFEST_FILE = path2.join(CALIBER_DIR, "manifest.json");
48
+ BACKUPS_DIR = path2.join(CALIBER_DIR, "backups");
49
49
  }
50
50
  });
51
51
 
52
+ // src/lib/sentry.ts
53
+ import * as Sentry from "@sentry/node";
54
+ import os from "os";
55
+ import fs from "fs";
56
+ import path from "path";
57
+ import { fileURLToPath } from "url";
58
+ var dsn = process.env.CALIBER_SENTRY_DSN || "https://e988132485fced78d7d60202f97942b3@o4510975175622656.ingest.us.sentry.io/4510975226347520";
59
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
60
+ var pkg = JSON.parse(
61
+ fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8")
62
+ );
63
+ if (dsn) {
64
+ Sentry.init({
65
+ dsn,
66
+ release: `caliber-cli@${pkg.version}`,
67
+ environment: process.env.NODE_ENV || "production",
68
+ tracesSampleRate: 0,
69
+ beforeSend(event) {
70
+ event.tags = { ...event.tags, source: "cli", os: os.platform() };
71
+ return event;
72
+ }
73
+ });
74
+ }
75
+
52
76
  // src/cli.ts
53
77
  import { Command } from "commander";
54
- import fs12 from "fs";
78
+ import fs16 from "fs";
79
+ import path13 from "path";
80
+ import { fileURLToPath as fileURLToPath3 } from "url";
55
81
 
56
82
  // src/commands/init.ts
57
83
  import chalk2 from "chalk";
58
84
  import ora2 from "ora";
59
85
  import readline from "readline";
60
- import path10 from "path";
86
+ import fs13 from "fs";
61
87
 
62
88
  // src/auth/token-store.ts
63
89
  init_constants();
64
- import fs from "fs";
90
+ import fs2 from "fs";
65
91
  function getStoredAuth() {
66
92
  try {
67
- if (!fs.existsSync(AUTH_FILE)) return null;
68
- const data = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
93
+ if (!fs2.existsSync(AUTH_FILE)) return null;
94
+ const data = JSON.parse(fs2.readFileSync(AUTH_FILE, "utf-8"));
69
95
  return data;
70
96
  } catch {
71
97
  return null;
72
98
  }
73
99
  }
74
100
  function storeAuth(auth2) {
75
- if (!fs.existsSync(AUTH_DIR)) {
76
- fs.mkdirSync(AUTH_DIR, { recursive: true });
101
+ if (!fs2.existsSync(AUTH_DIR)) {
102
+ fs2.mkdirSync(AUTH_DIR, { recursive: true });
77
103
  }
78
104
  const data = {
79
105
  accessToken: auth2.accessToken,
@@ -82,11 +108,11 @@ function storeAuth(auth2) {
82
108
  userId: auth2.user.id,
83
109
  email: auth2.user.email
84
110
  };
85
- fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
111
+ fs2.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
86
112
  }
87
113
  function clearAuth() {
88
114
  try {
89
- if (fs.existsSync(AUTH_FILE)) fs.unlinkSync(AUTH_FILE);
115
+ if (fs2.existsSync(AUTH_FILE)) fs2.unlinkSync(AUTH_FILE);
90
116
  } catch {
91
117
  }
92
118
  }
@@ -113,16 +139,16 @@ function generatePKCE() {
113
139
  // src/auth/callback-server.ts
114
140
  init_constants();
115
141
  import http from "http";
116
- import { URL as URL2 } from "url";
142
+ import { URL } from "url";
117
143
  function startCallbackServer(expectedState) {
118
- return new Promise((resolve, reject) => {
144
+ return new Promise((resolve2, reject) => {
119
145
  const server = http.createServer((req, res) => {
120
146
  if (!req.url?.startsWith("/callback")) {
121
147
  res.writeHead(404);
122
148
  res.end();
123
149
  return;
124
150
  }
125
- const url = new URL2(req.url, `http://127.0.0.1:${CLI_CALLBACK_PORT}`);
151
+ const url = new URL(req.url, `http://127.0.0.1:${CLI_CALLBACK_PORT}`);
126
152
  const error = url.searchParams.get("error");
127
153
  if (error) {
128
154
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -152,7 +178,7 @@ function startCallbackServer(expectedState) {
152
178
  </div>
153
179
  </body></html>`);
154
180
  server.close();
155
- resolve({
181
+ resolve2({
156
182
  accessToken,
157
183
  refreshToken,
158
184
  expiresIn: parseInt(expiresIn || "3600", 10),
@@ -177,12 +203,13 @@ init_constants();
177
203
  // src/telemetry.ts
178
204
  init_constants();
179
205
  import { PostHog } from "posthog-node";
180
- import fs2 from "fs";
181
- import path2 from "path";
182
- import os2 from "os";
206
+ import fs3 from "fs";
207
+ import path3 from "path";
208
+ import os3 from "os";
183
209
  import { randomUUID } from "crypto";
210
+ import { fileURLToPath as fileURLToPath2 } from "url";
184
211
  var POSTHOG_API_KEY = process.env.CALIBER_POSTHOG_KEY || "phc_XXrV0pSX4s2QVxVoOaeuyXDvtlRwPAjovt1ttMGVMPp";
185
- var DEVICE_ID_FILE = path2.join(AUTH_DIR, "device-id");
212
+ var DEVICE_ID_FILE = path3.join(AUTH_DIR, "device-id");
186
213
  var client = null;
187
214
  function getClient() {
188
215
  if (!client) {
@@ -192,36 +219,37 @@ function getClient() {
192
219
  }
193
220
  function getDeviceId() {
194
221
  try {
195
- if (fs2.existsSync(DEVICE_ID_FILE)) {
196
- return fs2.readFileSync(DEVICE_ID_FILE, "utf-8").trim();
222
+ if (fs3.existsSync(DEVICE_ID_FILE)) {
223
+ return fs3.readFileSync(DEVICE_ID_FILE, "utf-8").trim();
197
224
  }
198
225
  } catch {
199
226
  }
200
227
  const id = randomUUID();
201
228
  try {
202
- if (!fs2.existsSync(AUTH_DIR)) {
203
- fs2.mkdirSync(AUTH_DIR, { recursive: true });
229
+ if (!fs3.existsSync(AUTH_DIR)) {
230
+ fs3.mkdirSync(AUTH_DIR, { recursive: true });
204
231
  }
205
- fs2.writeFileSync(DEVICE_ID_FILE, id, { mode: 384 });
232
+ fs3.writeFileSync(DEVICE_ID_FILE, id, { mode: 384 });
206
233
  } catch {
207
234
  }
208
235
  return id;
209
236
  }
210
237
  function isFirstRun() {
211
- return !fs2.existsSync(DEVICE_ID_FILE);
238
+ return !fs3.existsSync(DEVICE_ID_FILE);
212
239
  }
213
240
  function getDistinctId() {
214
241
  const auth2 = getStoredAuth();
215
242
  return auth2?.userId || getDeviceId();
216
243
  }
217
- var pkg = JSON.parse(
218
- fs2.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
244
+ var __dirname_telemetry = path3.dirname(fileURLToPath2(import.meta.url));
245
+ var pkg2 = JSON.parse(
246
+ fs3.readFileSync(path3.resolve(__dirname_telemetry, "..", "package.json"), "utf-8")
219
247
  );
220
248
  function baseProperties() {
221
249
  return {
222
250
  source: "cli",
223
- cli_version: pkg.version,
224
- os: os2.platform()
251
+ cli_version: pkg2.version,
252
+ os: os3.platform()
225
253
  };
226
254
  }
227
255
  function trackEvent(event, properties = {}) {
@@ -245,29 +273,11 @@ function identifyUser(userId, email) {
245
273
  } catch {
246
274
  }
247
275
  }
248
- function countSuggestedFiles(setup) {
249
- let count = 0;
250
- const claude = setup.claude;
251
- const cursor = setup.cursor;
252
- if (claude) {
253
- if (claude.claudeMd) count++;
254
- if (claude.settings) count++;
255
- if (claude.settingsLocal) count++;
256
- if (Array.isArray(claude.skills)) count += claude.skills.length;
257
- if (claude.mcpServers && Object.keys(claude.mcpServers).length > 0) count++;
258
- }
259
- if (cursor) {
260
- if (cursor.cursorrules) count++;
261
- if (Array.isArray(cursor.rules)) count += cursor.rules.length;
262
- if (cursor.mcpServers && Object.keys(cursor.mcpServers).length > 0) count++;
263
- }
264
- return count;
265
- }
266
276
  async function shutdownTelemetry() {
267
277
  try {
268
278
  await Promise.race([
269
279
  client?.shutdown(),
270
- new Promise((resolve) => setTimeout(resolve, 2e3))
280
+ new Promise((resolve2) => setTimeout(resolve2, 2e3))
271
281
  ]);
272
282
  } catch {
273
283
  }
@@ -303,8 +313,9 @@ ${authUrl}
303
313
  trackEvent("signin", { method: "cli_pkce" });
304
314
  spinner.succeed(chalk.green(`Logged in as ${result.user.email}`));
305
315
  } catch (err) {
316
+ trackEvent("error_occurred", { error_type: "auth_failed", error_message: err instanceof Error ? err.message : "Unknown error", command: "login" });
306
317
  spinner.fail(chalk.red(`Authentication failed: ${err instanceof Error ? err.message : "Unknown error"}`));
307
- process.exit(1);
318
+ throw new Error("__exit__");
308
319
  }
309
320
  }
310
321
 
@@ -313,7 +324,19 @@ import crypto2 from "crypto";
313
324
 
314
325
  // src/fingerprint/git.ts
315
326
  import { execSync } from "child_process";
327
+ import { existsSync } from "fs";
328
+ import { resolve, dirname } from "path";
329
+ function isInsideGitRepo() {
330
+ let dir = process.cwd();
331
+ while (true) {
332
+ if (existsSync(resolve(dir, ".git"))) return true;
333
+ const parent = dirname(dir);
334
+ if (parent === dir) return false;
335
+ dir = parent;
336
+ }
337
+ }
316
338
  function getGitRemoteUrl() {
339
+ if (!isInsideGitRepo()) return void 0;
317
340
  try {
318
341
  return execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
319
342
  } catch {
@@ -322,8 +345,8 @@ function getGitRemoteUrl() {
322
345
  }
323
346
 
324
347
  // src/fingerprint/package-json.ts
325
- import fs3 from "fs";
326
- import path3 from "path";
348
+ import fs4 from "fs";
349
+ import path4 from "path";
327
350
  import { globSync } from "glob";
328
351
  var NODE_FRAMEWORK_DEPS = {
329
352
  react: "React",
@@ -374,8 +397,8 @@ var WORKSPACE_GLOBS = [
374
397
  ];
375
398
  function detectNodeFrameworks(pkgPath) {
376
399
  try {
377
- const pkg4 = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
378
- const allDeps = { ...pkg4.dependencies, ...pkg4.devDependencies };
400
+ const pkg5 = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
401
+ const allDeps = { ...pkg5.dependencies, ...pkg5.devDependencies };
379
402
  const frameworks = [];
380
403
  for (const [dep, framework] of Object.entries(NODE_FRAMEWORK_DEPS)) {
381
404
  if (allDeps[dep]) frameworks.push(framework);
@@ -388,16 +411,16 @@ function detectNodeFrameworks(pkgPath) {
388
411
  function detectPythonFrameworks(dir) {
389
412
  const frameworks = [];
390
413
  const candidates = [
391
- path3.join(dir, "pyproject.toml"),
392
- path3.join(dir, "requirements.txt"),
414
+ path4.join(dir, "pyproject.toml"),
415
+ path4.join(dir, "requirements.txt"),
393
416
  ...globSync("apps/*/pyproject.toml", { cwd: dir, absolute: true }),
394
417
  ...globSync("apps/*/requirements.txt", { cwd: dir, absolute: true }),
395
418
  ...globSync("services/*/pyproject.toml", { cwd: dir, absolute: true })
396
419
  ];
397
420
  for (const filePath of candidates) {
398
- if (!fs3.existsSync(filePath)) continue;
421
+ if (!fs4.existsSync(filePath)) continue;
399
422
  try {
400
- const content = fs3.readFileSync(filePath, "utf-8").toLowerCase();
423
+ const content = fs4.readFileSync(filePath, "utf-8").toLowerCase();
401
424
  for (const [dep, framework] of Object.entries(PYTHON_FRAMEWORK_DEPS)) {
402
425
  if (content.includes(dep)) frameworks.push(framework);
403
426
  }
@@ -407,15 +430,15 @@ function detectPythonFrameworks(dir) {
407
430
  return frameworks;
408
431
  }
409
432
  function analyzePackageJson(dir) {
410
- const rootPkgPath = path3.join(dir, "package.json");
433
+ const rootPkgPath = path4.join(dir, "package.json");
411
434
  let name;
412
435
  const allFrameworks = [];
413
436
  const languages = [];
414
- if (fs3.existsSync(rootPkgPath)) {
437
+ if (fs4.existsSync(rootPkgPath)) {
415
438
  try {
416
- const pkg4 = JSON.parse(fs3.readFileSync(rootPkgPath, "utf-8"));
417
- name = pkg4.name;
418
- const allDeps = { ...pkg4.dependencies, ...pkg4.devDependencies };
439
+ const pkg5 = JSON.parse(fs4.readFileSync(rootPkgPath, "utf-8"));
440
+ name = pkg5.name;
441
+ const allDeps = { ...pkg5.dependencies, ...pkg5.devDependencies };
419
442
  allFrameworks.push(...detectNodeFrameworks(rootPkgPath));
420
443
  if (allDeps.typescript || allDeps["@types/node"]) {
421
444
  languages.push("TypeScript");
@@ -429,8 +452,8 @@ function analyzePackageJson(dir) {
429
452
  for (const pkgPath of matches) {
430
453
  allFrameworks.push(...detectNodeFrameworks(pkgPath));
431
454
  try {
432
- const pkg4 = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
433
- const deps = { ...pkg4.dependencies, ...pkg4.devDependencies };
455
+ const pkg5 = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
456
+ const deps = { ...pkg5.dependencies, ...pkg5.devDependencies };
434
457
  if (deps.typescript || deps["@types/node"]) {
435
458
  languages.push("TypeScript");
436
459
  }
@@ -447,8 +470,8 @@ function analyzePackageJson(dir) {
447
470
  }
448
471
 
449
472
  // src/fingerprint/file-tree.ts
450
- import fs4 from "fs";
451
- import path4 from "path";
473
+ import fs5 from "fs";
474
+ import path5 from "path";
452
475
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
453
476
  "node_modules",
454
477
  ".git",
@@ -471,10 +494,10 @@ function getFileTree(dir, maxDepth = 3) {
471
494
  }
472
495
  function scan(base, rel, depth, maxDepth, result) {
473
496
  if (depth > maxDepth) return;
474
- const fullPath = path4.join(base, rel);
497
+ const fullPath = path5.join(base, rel);
475
498
  let entries;
476
499
  try {
477
- entries = fs4.readdirSync(fullPath, { withFileTypes: true });
500
+ entries = fs5.readdirSync(fullPath, { withFileTypes: true });
478
501
  } catch {
479
502
  return;
480
503
  }
@@ -492,7 +515,7 @@ function scan(base, rel, depth, maxDepth, result) {
492
515
  }
493
516
 
494
517
  // src/fingerprint/languages.ts
495
- import path5 from "path";
518
+ import path6 from "path";
496
519
  var EXT_TO_LANG = {
497
520
  ".ts": "TypeScript",
498
521
  ".tsx": "TypeScript",
@@ -518,7 +541,7 @@ var EXT_TO_LANG = {
518
541
  function detectLanguages(fileTree) {
519
542
  const langs = /* @__PURE__ */ new Set();
520
543
  for (const file of fileTree) {
521
- const ext = path5.extname(file).toLowerCase();
544
+ const ext = path6.extname(file).toLowerCase();
522
545
  if (EXT_TO_LANG[ext]) {
523
546
  langs.add(EXT_TO_LANG[ext]);
524
547
  }
@@ -527,43 +550,43 @@ function detectLanguages(fileTree) {
527
550
  }
528
551
 
529
552
  // src/fingerprint/existing-config.ts
530
- import fs5 from "fs";
531
- import path6 from "path";
553
+ import fs6 from "fs";
554
+ import path7 from "path";
532
555
  function readExistingConfigs(dir) {
533
556
  const configs = {};
534
- const claudeMdPath = path6.join(dir, "CLAUDE.md");
535
- if (fs5.existsSync(claudeMdPath)) {
536
- configs.claudeMd = fs5.readFileSync(claudeMdPath, "utf-8");
557
+ const claudeMdPath = path7.join(dir, "CLAUDE.md");
558
+ if (fs6.existsSync(claudeMdPath)) {
559
+ configs.claudeMd = fs6.readFileSync(claudeMdPath, "utf-8");
537
560
  }
538
- const claudeSettingsPath = path6.join(dir, ".claude", "settings.json");
539
- if (fs5.existsSync(claudeSettingsPath)) {
561
+ const claudeSettingsPath = path7.join(dir, ".claude", "settings.json");
562
+ if (fs6.existsSync(claudeSettingsPath)) {
540
563
  try {
541
- configs.claudeSettings = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
564
+ configs.claudeSettings = JSON.parse(fs6.readFileSync(claudeSettingsPath, "utf-8"));
542
565
  } catch {
543
566
  }
544
567
  }
545
- const skillsDir = path6.join(dir, ".claude", "skills");
546
- if (fs5.existsSync(skillsDir)) {
568
+ const skillsDir = path7.join(dir, ".claude", "skills");
569
+ if (fs6.existsSync(skillsDir)) {
547
570
  try {
548
- const files = fs5.readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
571
+ const files = fs6.readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
549
572
  configs.claudeSkills = files.map((f) => ({
550
573
  filename: f,
551
- content: fs5.readFileSync(path6.join(skillsDir, f), "utf-8")
574
+ content: fs6.readFileSync(path7.join(skillsDir, f), "utf-8")
552
575
  }));
553
576
  } catch {
554
577
  }
555
578
  }
556
- const cursorrulesPath = path6.join(dir, ".cursorrules");
557
- if (fs5.existsSync(cursorrulesPath)) {
558
- configs.cursorrules = fs5.readFileSync(cursorrulesPath, "utf-8");
579
+ const cursorrulesPath = path7.join(dir, ".cursorrules");
580
+ if (fs6.existsSync(cursorrulesPath)) {
581
+ configs.cursorrules = fs6.readFileSync(cursorrulesPath, "utf-8");
559
582
  }
560
- const cursorRulesDir = path6.join(dir, ".cursor", "rules");
561
- if (fs5.existsSync(cursorRulesDir)) {
583
+ const cursorRulesDir = path7.join(dir, ".cursor", "rules");
584
+ if (fs6.existsSync(cursorRulesDir)) {
562
585
  try {
563
- const files = fs5.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"));
586
+ const files = fs6.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"));
564
587
  configs.cursorRules = files.map((f) => ({
565
588
  filename: f,
566
- content: fs5.readFileSync(path6.join(cursorRulesDir, f), "utf-8")
589
+ content: fs6.readFileSync(path7.join(cursorRulesDir, f), "utf-8")
567
590
  }));
568
591
  } catch {
569
592
  }
@@ -571,6 +594,277 @@ function readExistingConfigs(dir) {
571
594
  return configs;
572
595
  }
573
596
 
597
+ // src/fingerprint/code-analysis.ts
598
+ import fs7 from "fs";
599
+ import path8 from "path";
600
+ var IGNORE_DIRS2 = /* @__PURE__ */ new Set([
601
+ "node_modules",
602
+ ".git",
603
+ ".next",
604
+ "dist",
605
+ "build",
606
+ ".cache",
607
+ ".turbo",
608
+ "coverage",
609
+ ".caliber",
610
+ "__pycache__",
611
+ ".venv",
612
+ "vendor",
613
+ "target"
614
+ ]);
615
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py"]);
616
+ var CONFIG_FILE_NAMES = /* @__PURE__ */ new Set([
617
+ "Dockerfile",
618
+ "docker-compose.yml",
619
+ "docker-compose.yaml",
620
+ "Makefile",
621
+ "tsconfig.json",
622
+ "pyproject.toml",
623
+ "turbo.json",
624
+ "next.config.js",
625
+ "next.config.mjs",
626
+ "next.config.ts",
627
+ "vite.config.ts",
628
+ "vite.config.js",
629
+ "vite.config.mjs",
630
+ "drizzle.config.ts",
631
+ "drizzle.config.js",
632
+ "jest.config.ts",
633
+ "jest.config.js",
634
+ "jest.config.mjs",
635
+ "vitest.config.ts",
636
+ "vitest.config.js",
637
+ "vitest.config.mts",
638
+ "alembic.ini",
639
+ "setup.cfg",
640
+ "tox.ini"
641
+ ]);
642
+ var CONFIG_GLOBS_DIRS = [
643
+ { dir: ".github/workflows", pattern: /\.ya?ml$/ }
644
+ ];
645
+ var TOTAL_BUDGET = 5e4;
646
+ var CONFIG_BUDGET = Math.floor(TOTAL_BUDGET * 0.3);
647
+ var SOURCE_BUDGET = Math.floor(TOTAL_BUDGET * 0.7);
648
+ function analyzeCode(dir) {
649
+ const sourceFiles = [];
650
+ const configFiles = [];
651
+ walkDir(dir, "", 0, 10, sourceFiles, configFiles, dir);
652
+ sortByPriority(sourceFiles);
653
+ let configChars = 0;
654
+ const trimmedConfigs = [];
655
+ for (const cfg of configFiles) {
656
+ const size = cfg.path.length + cfg.content.length;
657
+ if (configChars + size > CONFIG_BUDGET) break;
658
+ trimmedConfigs.push(cfg);
659
+ configChars += size;
660
+ }
661
+ let sourceChars = 0;
662
+ let truncated = false;
663
+ const fileSummaries = [];
664
+ for (const relPath of sourceFiles) {
665
+ const fullPath = path8.join(dir, relPath);
666
+ let content;
667
+ try {
668
+ content = fs7.readFileSync(fullPath, "utf-8");
669
+ } catch {
670
+ continue;
671
+ }
672
+ const lineCount = content.split("\n").length;
673
+ if (lineCount > 500) continue;
674
+ const ext = path8.extname(relPath);
675
+ const language = resolveLanguage(ext);
676
+ if (!language) continue;
677
+ const summary = language === "py" ? extractPython(relPath, content) : extractTypeScriptJavaScript(relPath, content, language);
678
+ const summarySize = estimateSummarySize(summary);
679
+ if (sourceChars + summarySize > SOURCE_BUDGET) {
680
+ truncated = true;
681
+ break;
682
+ }
683
+ fileSummaries.push(summary);
684
+ sourceChars += summarySize;
685
+ }
686
+ return { fileSummaries, configFiles: trimmedConfigs, truncated };
687
+ }
688
+ function walkDir(base, rel, depth, maxDepth, sourceFiles, configFiles, rootDir) {
689
+ if (depth > maxDepth) return;
690
+ const fullPath = path8.join(base, rel);
691
+ let entries;
692
+ try {
693
+ entries = fs7.readdirSync(fullPath, { withFileTypes: true });
694
+ } catch {
695
+ return;
696
+ }
697
+ for (const entry of entries) {
698
+ if (IGNORE_DIRS2.has(entry.name)) continue;
699
+ if (entry.name.startsWith(".") && depth === 0 && entry.isDirectory()) continue;
700
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
701
+ if (entry.isDirectory()) {
702
+ const matchedGlobDir = CONFIG_GLOBS_DIRS.find(
703
+ (g) => relPath === g.dir || relPath.endsWith(`/${g.dir}`)
704
+ );
705
+ if (matchedGlobDir) {
706
+ collectConfigsFromDir(base, relPath, matchedGlobDir.pattern, configFiles);
707
+ }
708
+ walkDir(base, relPath, depth + 1, maxDepth, sourceFiles, configFiles, rootDir);
709
+ } else {
710
+ if (CONFIG_FILE_NAMES.has(entry.name)) {
711
+ try {
712
+ const content = fs7.readFileSync(path8.join(base, relPath), "utf-8");
713
+ configFiles.push({ path: relPath, content });
714
+ } catch {
715
+ }
716
+ }
717
+ const ext = path8.extname(entry.name);
718
+ if (SOURCE_EXTENSIONS.has(ext) && !entry.name.endsWith(".d.ts")) {
719
+ sourceFiles.push(relPath);
720
+ }
721
+ }
722
+ }
723
+ }
724
+ function collectConfigsFromDir(base, relDir, pattern, configFiles) {
725
+ const fullDir = path8.join(base, relDir);
726
+ let entries;
727
+ try {
728
+ entries = fs7.readdirSync(fullDir, { withFileTypes: true });
729
+ } catch {
730
+ return;
731
+ }
732
+ for (const entry of entries) {
733
+ if (entry.isFile() && pattern.test(entry.name)) {
734
+ const relPath = `${relDir}/${entry.name}`;
735
+ try {
736
+ const content = fs7.readFileSync(path8.join(base, relPath), "utf-8");
737
+ configFiles.push({ path: relPath, content });
738
+ } catch {
739
+ }
740
+ }
741
+ }
742
+ }
743
+ function sortByPriority(files) {
744
+ const entryPointNames = /* @__PURE__ */ new Set(["index.ts", "index.js", "main.py", "app.ts", "app.js", "server.ts", "server.js"]);
745
+ const routePattern = /(route|api|controller)/i;
746
+ const schemaPattern = /(types|schema|models)/i;
747
+ const servicePattern = /(service|lib|utils)/i;
748
+ const testPattern = /(test|spec|__tests__)/i;
749
+ function priority(filePath) {
750
+ const base = path8.basename(filePath);
751
+ if (entryPointNames.has(base)) return 0;
752
+ if (routePattern.test(filePath)) return 1;
753
+ if (schemaPattern.test(filePath)) return 2;
754
+ if (servicePattern.test(filePath)) return 3;
755
+ if (testPattern.test(filePath)) return 5;
756
+ return 4;
757
+ }
758
+ files.sort((a, b) => priority(a) - priority(b));
759
+ }
760
+ function resolveLanguage(ext) {
761
+ if (ext === ".ts" || ext === ".tsx") return "ts";
762
+ if (ext === ".js" || ext === ".jsx") return "js";
763
+ if (ext === ".py") return "py";
764
+ return null;
765
+ }
766
+ function extractTypeScriptJavaScript(filePath, content, language) {
767
+ const lines = content.split("\n");
768
+ const imports = [];
769
+ const exports = [];
770
+ const functions = [];
771
+ const classes = [];
772
+ const types = [];
773
+ const routes = [];
774
+ for (const line of lines) {
775
+ const trimmed = line.trim();
776
+ if (/^import\s+/.test(trimmed)) {
777
+ imports.push(trimmed);
778
+ continue;
779
+ }
780
+ const exportMatch = trimmed.match(
781
+ /^export\s+(?:default\s+)?(?:async\s+)?(function|const|class|interface|type|enum)\s+(\w+)/
782
+ );
783
+ if (exportMatch) {
784
+ exports.push(`${exportMatch[1]} ${exportMatch[2]}`);
785
+ if (exportMatch[1] === "class") classes.push(exportMatch[2]);
786
+ if (exportMatch[1] === "interface" || exportMatch[1] === "type") types.push(exportMatch[2]);
787
+ if (exportMatch[1] === "function") functions.push(exportMatch[2]);
788
+ continue;
789
+ }
790
+ const fnMatch = trimmed.match(/^(?:async\s+)?function\s+(\w+)/);
791
+ if (fnMatch && !trimmed.startsWith("export")) {
792
+ functions.push(fnMatch[1]);
793
+ continue;
794
+ }
795
+ const arrowMatch = trimmed.match(/^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\(/);
796
+ if (arrowMatch && !exports.some((e) => e.endsWith(arrowMatch[1]))) {
797
+ functions.push(arrowMatch[1]);
798
+ }
799
+ const classMatch = trimmed.match(/^class\s+(\w+)/);
800
+ if (classMatch && !trimmed.startsWith("export")) {
801
+ classes.push(classMatch[1]);
802
+ continue;
803
+ }
804
+ const ifaceMatch = trimmed.match(/^(?:interface|type)\s+(\w+)/);
805
+ if (ifaceMatch && !trimmed.startsWith("export")) {
806
+ types.push(ifaceMatch[1]);
807
+ continue;
808
+ }
809
+ const routeMatch = trimmed.match(
810
+ /(?:router|app|server)\.(get|post|put|patch|delete|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/
811
+ );
812
+ if (routeMatch) {
813
+ routes.push(`${routeMatch[1].toUpperCase()} ${routeMatch[2]}`);
814
+ }
815
+ }
816
+ return { path: filePath, language, imports, exports, functions, classes, types, routes };
817
+ }
818
+ function extractPython(filePath, content) {
819
+ const lines = content.split("\n");
820
+ const imports = [];
821
+ const functions = [];
822
+ const classes = [];
823
+ const routes = [];
824
+ let prevLine = "";
825
+ for (const line of lines) {
826
+ const trimmed = line.trim();
827
+ if (/^import\s+/.test(trimmed) || /^from\s+/.test(trimmed)) {
828
+ imports.push(trimmed);
829
+ continue;
830
+ }
831
+ const defMatch = trimmed.match(/^def\s+(\w+)\s*\(([^)]*)\)/);
832
+ if (defMatch) {
833
+ functions.push(`${defMatch[1]}(${defMatch[2]})`);
834
+ }
835
+ const classMatch = trimmed.match(/^class\s+(\w+)/);
836
+ if (classMatch) {
837
+ classes.push(classMatch[1]);
838
+ }
839
+ const decoratorRoute = prevLine.match(
840
+ /@(?:app|router)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"]([^'"]+)['"]/
841
+ );
842
+ if (decoratorRoute && defMatch) {
843
+ routes.push(`${decoratorRoute[1].toUpperCase()} ${decoratorRoute[2]}`);
844
+ }
845
+ const includeRouter = trimmed.match(
846
+ /include_router\s*\([^,]+,\s*prefix\s*=\s*['"]([^'"]+)['"]/
847
+ );
848
+ if (includeRouter) {
849
+ routes.push(`ROUTER ${includeRouter[1]}`);
850
+ }
851
+ prevLine = trimmed;
852
+ }
853
+ return {
854
+ path: filePath,
855
+ language: "py",
856
+ imports,
857
+ exports: [],
858
+ functions,
859
+ classes,
860
+ types: [],
861
+ routes
862
+ };
863
+ }
864
+ function estimateSummarySize(summary) {
865
+ return summary.path.length + summary.imports.reduce((s, i) => s + i.length, 0) + summary.exports.reduce((s, e) => s + e.length, 0) + summary.functions.reduce((s, f) => s + f.length, 0) + summary.classes.reduce((s, c) => s + c.length, 0) + summary.types.reduce((s, t) => s + t.length, 0) + summary.routes.reduce((s, r) => s + r.length, 0);
866
+ }
867
+
574
868
  // src/fingerprint/index.ts
575
869
  function collectFingerprint(dir) {
576
870
  const gitRemoteUrl = getGitRemoteUrl();
@@ -578,6 +872,7 @@ function collectFingerprint(dir) {
578
872
  const fileTree = getFileTree(dir);
579
873
  const fileLangs = detectLanguages(fileTree);
580
874
  const existingConfigs = readExistingConfigs(dir);
875
+ const codeAnalysis = analyzeCode(dir);
581
876
  const languages = [.../* @__PURE__ */ new Set([...pkgInfo.languages, ...fileLangs])];
582
877
  return {
583
878
  gitRemoteUrl,
@@ -585,7 +880,8 @@ function collectFingerprint(dir) {
585
880
  languages,
586
881
  frameworks: pkgInfo.frameworks,
587
882
  fileTree,
588
- existingConfigs
883
+ existingConfigs,
884
+ codeAnalysis
589
885
  };
590
886
  }
591
887
  function computeFingerprintHash(fingerprint) {
@@ -598,10 +894,9 @@ function computeFingerprintHash(fingerprint) {
598
894
 
599
895
  // src/api/client.ts
600
896
  init_constants();
601
- async function refreshTokenIfNeeded() {
897
+ async function forceRefreshToken() {
602
898
  const auth2 = getStoredAuth();
603
899
  if (!auth2) return null;
604
- if (!isTokenExpired()) return auth2.accessToken;
605
900
  try {
606
901
  const resp = await fetch(`${API_URL}/api/auth/refresh`, {
607
902
  method: "POST",
@@ -621,12 +916,21 @@ async function refreshTokenIfNeeded() {
621
916
  return null;
622
917
  }
623
918
  }
624
- async function apiRequest(path11, options = {}) {
625
- const token = await refreshTokenIfNeeded();
626
- if (!token) {
919
+ async function getValidToken() {
920
+ const auth2 = getStoredAuth();
921
+ if (!auth2) {
627
922
  throw new Error("Not authenticated. Run `caliber login` first.");
628
923
  }
629
- const resp = await fetch(`${API_URL}${path11}`, {
924
+ if (!isTokenExpired()) return auth2.accessToken;
925
+ const refreshed = await forceRefreshToken();
926
+ if (!refreshed) {
927
+ throw new Error("Session expired. Run `caliber login` to re-authenticate.");
928
+ }
929
+ return refreshed;
930
+ }
931
+ async function apiRequest(path15, options = {}) {
932
+ let token = await getValidToken();
933
+ let resp = await fetch(`${API_URL}${path15}`, {
630
934
  method: options.method || "GET",
631
935
  headers: {
632
936
  "Content-Type": "application/json",
@@ -634,6 +938,21 @@ async function apiRequest(path11, options = {}) {
634
938
  },
635
939
  body: options.body ? JSON.stringify(options.body) : void 0
636
940
  });
941
+ if (resp.status === 401) {
942
+ const refreshed = await forceRefreshToken();
943
+ if (!refreshed) {
944
+ throw new Error("Session expired. Run `caliber login` to re-authenticate.");
945
+ }
946
+ token = refreshed;
947
+ resp = await fetch(`${API_URL}${path15}`, {
948
+ method: options.method || "GET",
949
+ headers: {
950
+ "Content-Type": "application/json",
951
+ Authorization: `Bearer ${token}`
952
+ },
953
+ body: options.body ? JSON.stringify(options.body) : void 0
954
+ });
955
+ }
637
956
  if (!resp.ok) {
638
957
  const error = await resp.json().catch(() => ({ error: resp.statusText }));
639
958
  throw new Error(error.error || `API error: ${resp.status}`);
@@ -641,12 +960,9 @@ async function apiRequest(path11, options = {}) {
641
960
  const json = await resp.json();
642
961
  return json.data;
643
962
  }
644
- async function apiStream(path11, body, onChunk, onComplete, onError, onStatus) {
645
- const token = await refreshTokenIfNeeded();
646
- if (!token) {
647
- throw new Error("Not authenticated. Run `caliber login` first.");
648
- }
649
- const resp = await fetch(`${API_URL}${path11}`, {
963
+ async function apiStream(path15, body, onChunk, onComplete, onError, onStatus) {
964
+ let token = await getValidToken();
965
+ let resp = await fetch(`${API_URL}${path15}`, {
650
966
  method: "POST",
651
967
  headers: {
652
968
  "Content-Type": "application/json",
@@ -654,6 +970,21 @@ async function apiStream(path11, body, onChunk, onComplete, onError, onStatus) {
654
970
  },
655
971
  body: JSON.stringify(body)
656
972
  });
973
+ if (resp.status === 401) {
974
+ const refreshed = await forceRefreshToken();
975
+ if (!refreshed) {
976
+ throw new Error("Session expired. Run `caliber login` to re-authenticate.");
977
+ }
978
+ token = refreshed;
979
+ resp = await fetch(`${API_URL}${path15}`, {
980
+ method: "POST",
981
+ headers: {
982
+ "Content-Type": "application/json",
983
+ Authorization: `Bearer ${token}`
984
+ },
985
+ body: JSON.stringify(body)
986
+ });
987
+ }
657
988
  if (!resp.ok || !resp.body) {
658
989
  throw new Error(`API error: ${resp.status}`);
659
990
  }
@@ -674,7 +1005,7 @@ async function apiStream(path11, body, onChunk, onComplete, onError, onStatus) {
674
1005
  const parsed = JSON.parse(data);
675
1006
  if (parsed.type === "chunk") onChunk(parsed.content);
676
1007
  else if (parsed.type === "status" && onStatus) onStatus(parsed.message);
677
- else if (parsed.type === "complete") onComplete({ setup: parsed.setup, explanation: parsed.explanation });
1008
+ else if (parsed.type === "complete") onComplete({ setup: parsed.setup, explanation: parsed.explanation, raw: parsed.raw });
678
1009
  else if (parsed.type === "error") onError(parsed.message);
679
1010
  } catch {
680
1011
  }
@@ -683,132 +1014,132 @@ async function apiStream(path11, body, onChunk, onComplete, onError, onStatus) {
683
1014
  }
684
1015
 
685
1016
  // src/writers/index.ts
686
- import fs10 from "fs";
1017
+ import fs12 from "fs";
687
1018
 
688
1019
  // src/writers/claude/index.ts
689
- import fs6 from "fs";
690
- import path7 from "path";
1020
+ import fs8 from "fs";
1021
+ import path9 from "path";
691
1022
  function writeClaudeConfig(config) {
692
1023
  const written = [];
693
- fs6.writeFileSync("CLAUDE.md", config.claudeMd);
1024
+ fs8.writeFileSync("CLAUDE.md", config.claudeMd);
694
1025
  written.push("CLAUDE.md");
695
1026
  const claudeDir = ".claude";
696
- if (!fs6.existsSync(claudeDir)) fs6.mkdirSync(claudeDir, { recursive: true });
697
- fs6.writeFileSync(
698
- path7.join(claudeDir, "settings.json"),
1027
+ if (!fs8.existsSync(claudeDir)) fs8.mkdirSync(claudeDir, { recursive: true });
1028
+ fs8.writeFileSync(
1029
+ path9.join(claudeDir, "settings.json"),
699
1030
  JSON.stringify(config.settings, null, 2)
700
1031
  );
701
- written.push(path7.join(claudeDir, "settings.json"));
702
- fs6.writeFileSync(
703
- path7.join(claudeDir, "settings.local.json"),
1032
+ written.push(path9.join(claudeDir, "settings.json"));
1033
+ fs8.writeFileSync(
1034
+ path9.join(claudeDir, "settings.local.json"),
704
1035
  JSON.stringify(config.settingsLocal, null, 2)
705
1036
  );
706
- written.push(path7.join(claudeDir, "settings.local.json"));
1037
+ written.push(path9.join(claudeDir, "settings.local.json"));
707
1038
  if (config.skills?.length) {
708
- const skillsDir = path7.join(claudeDir, "skills");
709
- if (!fs6.existsSync(skillsDir)) fs6.mkdirSync(skillsDir, { recursive: true });
1039
+ const skillsDir = path9.join(claudeDir, "skills");
1040
+ if (!fs8.existsSync(skillsDir)) fs8.mkdirSync(skillsDir, { recursive: true });
710
1041
  for (const skill of config.skills) {
711
1042
  const filename = `${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
712
- const skillPath = path7.join(skillsDir, filename);
713
- fs6.writeFileSync(skillPath, skill.content);
1043
+ const skillPath = path9.join(skillsDir, filename);
1044
+ fs8.writeFileSync(skillPath, skill.content);
714
1045
  written.push(skillPath);
715
1046
  }
716
1047
  }
717
1048
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
718
1049
  const mcpConfig = { mcpServers: config.mcpServers };
719
- fs6.writeFileSync(".mcp.json", JSON.stringify(mcpConfig, null, 2));
1050
+ fs8.writeFileSync(".mcp.json", JSON.stringify(mcpConfig, null, 2));
720
1051
  written.push(".mcp.json");
721
1052
  }
722
1053
  return written;
723
1054
  }
724
1055
 
725
1056
  // src/writers/cursor/index.ts
726
- import fs7 from "fs";
727
- import path8 from "path";
1057
+ import fs9 from "fs";
1058
+ import path10 from "path";
728
1059
  function writeCursorConfig(config) {
729
1060
  const written = [];
730
1061
  if (config.cursorrules) {
731
- fs7.writeFileSync(".cursorrules", config.cursorrules);
1062
+ fs9.writeFileSync(".cursorrules", config.cursorrules);
732
1063
  written.push(".cursorrules");
733
1064
  }
734
1065
  if (config.rules?.length) {
735
- const rulesDir = path8.join(".cursor", "rules");
736
- if (!fs7.existsSync(rulesDir)) fs7.mkdirSync(rulesDir, { recursive: true });
1066
+ const rulesDir = path10.join(".cursor", "rules");
1067
+ if (!fs9.existsSync(rulesDir)) fs9.mkdirSync(rulesDir, { recursive: true });
737
1068
  for (const rule of config.rules) {
738
- const rulePath = path8.join(rulesDir, rule.filename);
739
- fs7.writeFileSync(rulePath, rule.content);
1069
+ const rulePath = path10.join(rulesDir, rule.filename);
1070
+ fs9.writeFileSync(rulePath, rule.content);
740
1071
  written.push(rulePath);
741
1072
  }
742
1073
  }
743
1074
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
744
1075
  const cursorDir = ".cursor";
745
- if (!fs7.existsSync(cursorDir)) fs7.mkdirSync(cursorDir, { recursive: true });
1076
+ if (!fs9.existsSync(cursorDir)) fs9.mkdirSync(cursorDir, { recursive: true });
746
1077
  const mcpConfig = { mcpServers: config.mcpServers };
747
- fs7.writeFileSync(
748
- path8.join(cursorDir, "mcp.json"),
1078
+ fs9.writeFileSync(
1079
+ path10.join(cursorDir, "mcp.json"),
749
1080
  JSON.stringify(mcpConfig, null, 2)
750
1081
  );
751
- written.push(path8.join(cursorDir, "mcp.json"));
1082
+ written.push(path10.join(cursorDir, "mcp.json"));
752
1083
  }
753
1084
  return written;
754
1085
  }
755
1086
 
756
1087
  // src/writers/backup.ts
757
1088
  init_constants();
758
- import fs8 from "fs";
759
- import path9 from "path";
1089
+ import fs10 from "fs";
1090
+ import path11 from "path";
760
1091
  function createBackup(files) {
761
1092
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
762
- const backupDir = path9.join(BACKUPS_DIR, timestamp);
1093
+ const backupDir = path11.join(BACKUPS_DIR, timestamp);
763
1094
  for (const file of files) {
764
- if (!fs8.existsSync(file)) continue;
765
- const dest = path9.join(backupDir, file);
766
- const destDir = path9.dirname(dest);
767
- if (!fs8.existsSync(destDir)) {
768
- fs8.mkdirSync(destDir, { recursive: true });
1095
+ if (!fs10.existsSync(file)) continue;
1096
+ const dest = path11.join(backupDir, file);
1097
+ const destDir = path11.dirname(dest);
1098
+ if (!fs10.existsSync(destDir)) {
1099
+ fs10.mkdirSync(destDir, { recursive: true });
769
1100
  }
770
- fs8.copyFileSync(file, dest);
1101
+ fs10.copyFileSync(file, dest);
771
1102
  }
772
1103
  return backupDir;
773
1104
  }
774
1105
  function restoreBackup(backupDir, file) {
775
- const backupFile = path9.join(backupDir, file);
776
- if (!fs8.existsSync(backupFile)) return false;
777
- const destDir = path9.dirname(file);
778
- if (!fs8.existsSync(destDir)) {
779
- fs8.mkdirSync(destDir, { recursive: true });
1106
+ const backupFile = path11.join(backupDir, file);
1107
+ if (!fs10.existsSync(backupFile)) return false;
1108
+ const destDir = path11.dirname(file);
1109
+ if (!fs10.existsSync(destDir)) {
1110
+ fs10.mkdirSync(destDir, { recursive: true });
780
1111
  }
781
- fs8.copyFileSync(backupFile, file);
1112
+ fs10.copyFileSync(backupFile, file);
782
1113
  return true;
783
1114
  }
784
1115
 
785
1116
  // src/writers/manifest.ts
786
1117
  init_constants();
787
- import fs9 from "fs";
1118
+ import fs11 from "fs";
788
1119
  import crypto3 from "crypto";
789
1120
  function readManifest() {
790
1121
  try {
791
- if (!fs9.existsSync(MANIFEST_FILE)) return null;
792
- return JSON.parse(fs9.readFileSync(MANIFEST_FILE, "utf-8"));
1122
+ if (!fs11.existsSync(MANIFEST_FILE)) return null;
1123
+ return JSON.parse(fs11.readFileSync(MANIFEST_FILE, "utf-8"));
793
1124
  } catch {
794
1125
  return null;
795
1126
  }
796
1127
  }
797
1128
  function writeManifest(manifest) {
798
- if (!fs9.existsSync(CALIBER_DIR)) {
799
- fs9.mkdirSync(CALIBER_DIR, { recursive: true });
1129
+ if (!fs11.existsSync(CALIBER_DIR)) {
1130
+ fs11.mkdirSync(CALIBER_DIR, { recursive: true });
800
1131
  }
801
- fs9.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
1132
+ fs11.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
802
1133
  }
803
1134
  function fileChecksum(filePath) {
804
- const content = fs9.readFileSync(filePath);
1135
+ const content = fs11.readFileSync(filePath);
805
1136
  return crypto3.createHash("sha256").update(content).digest("hex");
806
1137
  }
807
1138
 
808
1139
  // src/writers/index.ts
809
1140
  function writeSetup(setup) {
810
1141
  const filesToWrite = getFilesToWrite(setup);
811
- const existingFiles = filesToWrite.filter((f) => fs10.existsSync(f));
1142
+ const existingFiles = filesToWrite.filter((f) => fs12.existsSync(f));
812
1143
  const backupDir = existingFiles.length > 0 ? createBackup(existingFiles) : void 0;
813
1144
  const written = [];
814
1145
  if ((setup.targetAgent === "claude" || setup.targetAgent === "both") && setup.claude) {
@@ -836,8 +1167,8 @@ function undoSetup() {
836
1167
  const removed = [];
837
1168
  for (const entry of manifest.entries) {
838
1169
  if (entry.action === "created") {
839
- if (fs10.existsSync(entry.path)) {
840
- fs10.unlinkSync(entry.path);
1170
+ if (fs12.existsSync(entry.path)) {
1171
+ fs12.unlinkSync(entry.path);
841
1172
  removed.push(entry.path);
842
1173
  }
843
1174
  } else if (entry.action === "modified" && manifest.backupDir) {
@@ -847,8 +1178,8 @@ function undoSetup() {
847
1178
  }
848
1179
  }
849
1180
  const { MANIFEST_FILE: MANIFEST_FILE2 } = (init_constants(), __toCommonJS(constants_exports));
850
- if (fs10.existsSync(MANIFEST_FILE2)) {
851
- fs10.unlinkSync(MANIFEST_FILE2);
1181
+ if (fs12.existsSync(MANIFEST_FILE2)) {
1182
+ fs12.unlinkSync(MANIFEST_FILE2);
852
1183
  }
853
1184
  return { restored, removed };
854
1185
  }
@@ -874,13 +1205,13 @@ function getFilesToWrite(setup) {
874
1205
  }
875
1206
  function ensureGitignore() {
876
1207
  const gitignorePath = ".gitignore";
877
- if (fs10.existsSync(gitignorePath)) {
878
- const content = fs10.readFileSync(gitignorePath, "utf-8");
1208
+ if (fs12.existsSync(gitignorePath)) {
1209
+ const content = fs12.readFileSync(gitignorePath, "utf-8");
879
1210
  if (!content.includes(".caliber/")) {
880
- fs10.appendFileSync(gitignorePath, "\n# Caliber local state\n.caliber/\n");
1211
+ fs12.appendFileSync(gitignorePath, "\n# Caliber local state\n.caliber/\n");
881
1212
  }
882
1213
  } else {
883
- fs10.writeFileSync(gitignorePath, "# Caliber local state\n.caliber/\n");
1214
+ fs12.writeFileSync(gitignorePath, "# Caliber local state\n.caliber/\n");
884
1215
  }
885
1216
  }
886
1217
 
@@ -958,7 +1289,7 @@ async function initCommand(options) {
958
1289
  auth2 = getStoredAuth();
959
1290
  if (!auth2) {
960
1291
  console.log(chalk2.red("Authentication required. Exiting."));
961
- process.exit(1);
1292
+ throw new Error("__exit__");
962
1293
  }
963
1294
  }
964
1295
  console.log(chalk2.dim(`Authenticated as ${auth2.email}
@@ -984,14 +1315,23 @@ async function initCommand(options) {
984
1315
  `));
985
1316
  const matchSpinner = ora2("Checking for existing setup...").start();
986
1317
  let existingSetup = null;
1318
+ let existingProjectId = null;
987
1319
  try {
988
1320
  const match = await apiRequest("/api/projects/match", {
989
1321
  method: "POST",
990
1322
  body: { fingerprintHash: hash }
991
1323
  });
1324
+ if (match.project) {
1325
+ existingProjectId = match.project.id;
1326
+ }
992
1327
  if (match.setup) {
993
1328
  existingSetup = match.setup;
994
- trackEvent("existing_config_detected");
1329
+ trackEvent("existing_config_detected", {
1330
+ has_claude_md: !!fingerprint.existingConfigs.claudeMd,
1331
+ has_cursorrules: !!fingerprint.existingConfigs.cursorrules,
1332
+ has_cursor_rules: (fingerprint.existingConfigs.cursorRules?.length ?? 0) > 0,
1333
+ has_skills: (fingerprint.existingConfigs.claudeSkills?.length ?? 0) > 0
1334
+ });
995
1335
  matchSpinner.succeed("Found existing setup");
996
1336
  } else {
997
1337
  matchSpinner.info("No existing setup found");
@@ -1007,55 +1347,63 @@ async function initCommand(options) {
1007
1347
  }
1008
1348
  let generatedSetup = null;
1009
1349
  let setupExplanation;
1350
+ let rawOutput;
1010
1351
  trackEvent("generation_started", { target_agent: targetAgent });
1011
- const generationStart = Date.now();
1012
1352
  const genSpinner = ora2("Generating setup...").start();
1013
1353
  const genMessages = new SpinnerMessages(genSpinner, GENERATION_MESSAGES);
1014
1354
  genMessages.start();
1015
- await apiStream(
1016
- "/api/setups/generate",
1017
- {
1018
- fingerprint,
1019
- targetAgent,
1020
- prompt: fingerprint.description
1021
- },
1022
- () => {
1023
- },
1024
- (payload) => {
1025
- generatedSetup = payload.setup;
1026
- setupExplanation = payload.explanation;
1027
- },
1028
- (error) => {
1029
- genMessages.stop();
1030
- trackEvent("error_occurred", { error_type: "generation_failed", error_message: error });
1031
- genSpinner.fail(`Generation error: ${error}`);
1032
- },
1033
- (status) => {
1034
- genMessages.handleServerStatus(status);
1035
- }
1036
- );
1355
+ try {
1356
+ await apiStream(
1357
+ "/api/setups/generate",
1358
+ {
1359
+ fingerprint,
1360
+ targetAgent,
1361
+ prompt: fingerprint.description
1362
+ },
1363
+ () => {
1364
+ },
1365
+ (payload) => {
1366
+ generatedSetup = payload.setup;
1367
+ setupExplanation = payload.explanation;
1368
+ rawOutput = payload.raw;
1369
+ },
1370
+ (error) => {
1371
+ genMessages.stop();
1372
+ trackEvent("error_occurred", { error_type: "generation_failed", error_message: error, command: "init" });
1373
+ genSpinner.fail(`Generation error: ${error}`);
1374
+ },
1375
+ (status) => {
1376
+ genMessages.handleServerStatus(status);
1377
+ }
1378
+ );
1379
+ } catch (err) {
1380
+ genMessages.stop();
1381
+ const msg = err instanceof Error ? err.message : "Unknown error";
1382
+ trackEvent("error_occurred", { error_type: "generation_request_failed", error_message: msg, command: "init" });
1383
+ genSpinner.fail(`Generation failed: ${msg}`);
1384
+ throw new Error("__exit__");
1385
+ }
1037
1386
  genMessages.stop();
1038
1387
  if (!generatedSetup) {
1039
1388
  genSpinner.fail("Failed to generate setup.");
1040
- process.exit(1);
1389
+ if (rawOutput) {
1390
+ console.log(chalk2.dim("\nRaw LLM output (JSON parse failed):"));
1391
+ console.log(chalk2.dim(rawOutput.slice(0, 500)));
1392
+ }
1393
+ throw new Error("__exit__");
1041
1394
  }
1042
- trackEvent("generation_completed", {
1043
- target_agent: targetAgent,
1044
- duration_ms: Date.now() - generationStart,
1045
- files_suggested: countSuggestedFiles(generatedSetup)
1046
- });
1047
1395
  genSpinner.succeed("Setup generated");
1048
1396
  printSetupSummary(generatedSetup);
1049
- let action = await promptAction();
1397
+ let explained = false;
1398
+ let action = await promptAction(explained);
1050
1399
  while (action === "explain") {
1051
1400
  if (setupExplanation) {
1052
- console.log(chalk2.bold("\nWhy this setup?\n"));
1053
- console.log(chalk2.dim(setupExplanation));
1054
- console.log("");
1401
+ printExplanation(setupExplanation);
1055
1402
  } else {
1056
1403
  console.log(chalk2.dim("\nNo explanation available for this setup.\n"));
1057
1404
  }
1058
- action = await promptAction();
1405
+ explained = true;
1406
+ action = await promptAction(explained);
1059
1407
  }
1060
1408
  if (action === "decline") {
1061
1409
  trackEvent("setup_declined");
@@ -1065,7 +1413,7 @@ async function initCommand(options) {
1065
1413
  if (action === "refine") {
1066
1414
  generatedSetup = await refineLoop(generatedSetup, targetAgent);
1067
1415
  if (!generatedSetup) {
1068
- trackEvent("generation_cancelled");
1416
+ trackEvent("refinement_cancelled");
1069
1417
  console.log(chalk2.dim("Refinement cancelled. No files were modified."));
1070
1418
  return;
1071
1419
  }
@@ -1080,9 +1428,6 @@ async function initCommand(options) {
1080
1428
  const result = writeSetup(generatedSetup);
1081
1429
  writeSpinner.succeed("Config files written");
1082
1430
  trackEvent("setup_applied", { files_written: result.written.length, target_agent: targetAgent });
1083
- for (const file of result.written) {
1084
- trackEvent("config_file_written", { file_type: path10.extname(file) || path10.basename(file) });
1085
- }
1086
1431
  console.log(chalk2.bold("\nFiles created/updated:"));
1087
1432
  for (const file of result.written) {
1088
1433
  console.log(` ${chalk2.green("\u2713")} ${file}`);
@@ -1094,20 +1439,23 @@ async function initCommand(options) {
1094
1439
  } catch (err) {
1095
1440
  writeSpinner.fail("Failed to write files");
1096
1441
  console.error(chalk2.red(err instanceof Error ? err.message : "Unknown error"));
1097
- process.exit(1);
1442
+ throw new Error("__exit__");
1098
1443
  }
1099
1444
  try {
1100
- const project = await apiRequest("/api/projects", {
1101
- method: "POST",
1102
- body: {
1103
- fingerprintHash: hash,
1104
- name: fingerprint.packageName || "untitled",
1105
- gitRemoteUrl: fingerprint.gitRemoteUrl,
1106
- description: fingerprint.description
1107
- }
1108
- });
1109
- trackEvent("project_created", { project_name: fingerprint.packageName || "untitled" });
1110
- await apiRequest(`/api/setups/project/${project.id}`, {
1445
+ let projectId = existingProjectId;
1446
+ if (!projectId) {
1447
+ const project = await apiRequest("/api/projects", {
1448
+ method: "POST",
1449
+ body: {
1450
+ fingerprintHash: hash,
1451
+ name: fingerprint.packageName || "untitled",
1452
+ gitRemoteUrl: fingerprint.gitRemoteUrl,
1453
+ description: fingerprint.description
1454
+ }
1455
+ });
1456
+ projectId = project.id;
1457
+ }
1458
+ await apiRequest(`/api/setups/project/${projectId}`, {
1111
1459
  method: "POST",
1112
1460
  body: {
1113
1461
  targetAgent,
@@ -1171,15 +1519,15 @@ async function refineLoop(currentSetup, _targetAgent) {
1171
1519
  }
1172
1520
  function promptInput(question) {
1173
1521
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1174
- return new Promise((resolve) => {
1522
+ return new Promise((resolve2) => {
1175
1523
  rl.question(chalk2.cyan(`${question} `), (answer) => {
1176
1524
  rl.close();
1177
- resolve(answer.trim());
1525
+ resolve2(answer.trim());
1178
1526
  });
1179
1527
  });
1180
1528
  }
1181
1529
  function promptAgent() {
1182
- return new Promise((resolve) => {
1530
+ return new Promise((resolve2) => {
1183
1531
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1184
1532
  console.log(chalk2.bold("Which coding agent are you using?"));
1185
1533
  console.log(" 1. Claude Code");
@@ -1188,81 +1536,123 @@ function promptAgent() {
1188
1536
  rl.question(chalk2.cyan("\nChoose (1-3): "), (answer) => {
1189
1537
  rl.close();
1190
1538
  const map = { "1": "claude", "2": "cursor", "3": "both" };
1191
- resolve(map[answer.trim()] || "claude");
1539
+ resolve2(map[answer.trim()] || "claude");
1192
1540
  });
1193
1541
  });
1194
1542
  }
1195
- function promptAction() {
1196
- return new Promise((resolve) => {
1543
+ function promptAction(explained) {
1544
+ return new Promise((resolve2) => {
1197
1545
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1198
1546
  console.log(chalk2.bold("What would you like to do?"));
1199
- console.log(" 1. Accept and apply");
1200
- console.log(" 2. Refine via chat");
1201
- console.log(" 3. Explain recommendations");
1202
- console.log(" 4. Decline");
1203
- rl.question(chalk2.cyan("\nChoose (1-4): "), (answer) => {
1204
- rl.close();
1205
- const map = {
1206
- "1": "accept",
1207
- "2": "refine",
1208
- "3": "explain",
1209
- "4": "decline"
1210
- };
1211
- resolve(map[answer.trim()] || "accept");
1212
- });
1547
+ if (explained) {
1548
+ console.log(" 1. Accept and apply");
1549
+ console.log(" 2. Refine via chat");
1550
+ console.log(" 3. Decline");
1551
+ rl.question(chalk2.cyan("\nChoose (1-3): "), (answer) => {
1552
+ rl.close();
1553
+ const map = {
1554
+ "1": "accept",
1555
+ "2": "refine",
1556
+ "3": "decline"
1557
+ };
1558
+ resolve2(map[answer.trim()] || "accept");
1559
+ });
1560
+ } else {
1561
+ console.log(" 1. Accept and apply");
1562
+ console.log(" 2. Refine via chat");
1563
+ console.log(" 3. Explain recommendations");
1564
+ console.log(" 4. Decline");
1565
+ rl.question(chalk2.cyan("\nChoose (1-4): "), (answer) => {
1566
+ rl.close();
1567
+ const map = {
1568
+ "1": "accept",
1569
+ "2": "refine",
1570
+ "3": "explain",
1571
+ "4": "decline"
1572
+ };
1573
+ resolve2(map[answer.trim()] || "accept");
1574
+ });
1575
+ }
1213
1576
  });
1214
1577
  }
1578
+ function fileEntry(filePath, desc) {
1579
+ const icon = fs13.existsSync(filePath) ? chalk2.yellow("~") : chalk2.green("+");
1580
+ const description = desc ? chalk2.dim(`\u2014 ${desc}`) : "";
1581
+ return ` ${icon} ${filePath} ${description}`;
1582
+ }
1215
1583
  function printSetupSummary(setup) {
1216
1584
  const claude = setup.claude;
1217
1585
  const cursor = setup.cursor;
1586
+ const descriptions = setup.fileDescriptions || {};
1218
1587
  console.log("");
1219
1588
  if (claude) {
1220
1589
  if (claude.claudeMd) {
1221
- console.log(` ${chalk2.green("+")} CLAUDE.md ${chalk2.dim("\u2014 project guidelines, commands, conventions")}`);
1590
+ console.log(fileEntry("CLAUDE.md", descriptions["CLAUDE.md"]));
1222
1591
  }
1223
1592
  if (claude.settings) {
1224
- console.log(` ${chalk2.green("+")} .claude/settings.json ${chalk2.dim("\u2014 tool permissions & hooks")}`);
1593
+ console.log(fileEntry(".claude/settings.json", descriptions[".claude/settings.json"]));
1225
1594
  }
1226
1595
  if (claude.settingsLocal) {
1227
- console.log(` ${chalk2.green("+")} .claude/settings.local.json ${chalk2.dim("\u2014 local dev permissions")}`);
1596
+ console.log(fileEntry(".claude/settings.local.json", descriptions[".claude/settings.local.json"]));
1228
1597
  }
1229
1598
  const skills = claude.skills;
1230
1599
  if (Array.isArray(skills) && skills.length > 0) {
1231
- console.log(` ${chalk2.green("+")} ${skills.length} skill${skills.length > 1 ? "s" : ""}:`);
1232
1600
  for (const skill of skills) {
1233
- console.log(` ${chalk2.dim("\u2022")} ${skill.name}`);
1601
+ const skillPath = `.claude/skills/${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
1602
+ console.log(fileEntry(skillPath, descriptions[skillPath] || skill.name));
1234
1603
  }
1235
1604
  }
1236
1605
  const mcpServers = claude.mcpServers;
1237
1606
  if (mcpServers && Object.keys(mcpServers).length > 0) {
1238
- const names = Object.keys(mcpServers);
1239
- console.log(` ${chalk2.green("+")} ${names.length} MCP server${names.length > 1 ? "s" : ""}:`);
1240
- for (const name of names) {
1241
- console.log(` ${chalk2.dim("\u2022")} ${name}`);
1242
- }
1607
+ console.log(fileEntry(".mcp.json", descriptions[".mcp.json"] || Object.keys(mcpServers).join(", ")));
1243
1608
  }
1244
1609
  }
1245
1610
  if (cursor) {
1246
1611
  if (cursor.cursorrules) {
1247
- console.log(` ${chalk2.green("+")} .cursorrules ${chalk2.dim("\u2014 coding rules for Cursor")}`);
1612
+ console.log(fileEntry(".cursorrules", descriptions[".cursorrules"]));
1248
1613
  }
1249
1614
  const rules = cursor.rules;
1250
1615
  if (Array.isArray(rules) && rules.length > 0) {
1251
- console.log(` ${chalk2.green("+")} ${rules.length} Cursor rule${rules.length > 1 ? "s" : ""}:`);
1252
1616
  for (const rule of rules) {
1253
- console.log(` ${chalk2.dim("\u2022")} ${rule.filename}`);
1617
+ const rulePath = `.cursor/rules/${rule.filename}`;
1618
+ console.log(fileEntry(rulePath, descriptions[rulePath]));
1254
1619
  }
1255
1620
  }
1256
1621
  const mcpServers = cursor.mcpServers;
1257
1622
  if (mcpServers && Object.keys(mcpServers).length > 0) {
1258
- const names = Object.keys(mcpServers);
1259
- console.log(` ${chalk2.green("+")} ${names.length} MCP server${names.length > 1 ? "s" : ""}:`);
1260
- for (const name of names) {
1261
- console.log(` ${chalk2.dim("\u2022")} ${name}`);
1262
- }
1623
+ console.log(fileEntry(".cursor/mcp.json", descriptions[".cursor/mcp.json"] || Object.keys(mcpServers).join(", ")));
1263
1624
  }
1264
1625
  }
1265
1626
  console.log("");
1627
+ console.log(` ${chalk2.green("+")} ${chalk2.dim("new")} ${chalk2.yellow("~")} ${chalk2.dim("modified")}`);
1628
+ console.log("");
1629
+ }
1630
+ function printExplanation(explanation) {
1631
+ console.log(chalk2.bold("\n Why this setup?\n"));
1632
+ const lines = explanation.split("\n");
1633
+ for (const line of lines) {
1634
+ const trimmed = line.trim();
1635
+ if (!trimmed) continue;
1636
+ const headerMatch = trimmed.match(/^\[(.+)\]$/);
1637
+ if (headerMatch) {
1638
+ console.log(` ${chalk2.bold.hex("#6366f1")(headerMatch[1])}`);
1639
+ continue;
1640
+ }
1641
+ const itemMatch = trimmed.match(/^-\s+\*\*(.+?)\*\*[:\s]*(.*)$/);
1642
+ if (itemMatch) {
1643
+ const name = itemMatch[1];
1644
+ const desc = itemMatch[2].replace(/^\s*[-—:]\s*/, "");
1645
+ console.log(` ${chalk2.dim("\u25B8")} ${chalk2.white(name)} ${chalk2.dim(desc)}`);
1646
+ continue;
1647
+ }
1648
+ const plainMatch = trimmed.match(/^-\s+(.*)$/);
1649
+ if (plainMatch) {
1650
+ console.log(` ${chalk2.dim("\u25B8")} ${chalk2.dim(plainMatch[1])}`);
1651
+ continue;
1652
+ }
1653
+ console.log(` ${chalk2.dim(trimmed)}`);
1654
+ }
1655
+ console.log("");
1266
1656
  }
1267
1657
 
1268
1658
  // src/commands/undo.ts
@@ -1293,14 +1683,150 @@ function undoCommand() {
1293
1683
  console.log("");
1294
1684
  } catch (err) {
1295
1685
  spinner.fail(chalk3.red(err instanceof Error ? err.message : "Undo failed"));
1296
- process.exit(1);
1686
+ throw new Error("__exit__");
1297
1687
  }
1298
1688
  }
1299
1689
 
1300
1690
  // src/commands/status.ts
1301
1691
  import chalk4 from "chalk";
1302
- import fs11 from "fs";
1303
- function statusCommand(options) {
1692
+ import fs15 from "fs";
1693
+
1694
+ // src/scanner/index.ts
1695
+ import fs14 from "fs";
1696
+ import path12 from "path";
1697
+ import crypto4 from "crypto";
1698
+ function scanLocalState(dir) {
1699
+ const items = [];
1700
+ const claudeMdPath = path12.join(dir, "CLAUDE.md");
1701
+ if (fs14.existsSync(claudeMdPath)) {
1702
+ items.push({
1703
+ type: "rule",
1704
+ platform: "claude",
1705
+ name: "CLAUDE.md",
1706
+ contentHash: hashFile(claudeMdPath),
1707
+ path: claudeMdPath
1708
+ });
1709
+ }
1710
+ const settingsPath = path12.join(dir, ".claude", "settings.json");
1711
+ if (fs14.existsSync(settingsPath)) {
1712
+ items.push({
1713
+ type: "config",
1714
+ platform: "claude",
1715
+ name: "settings.json",
1716
+ contentHash: hashFile(settingsPath),
1717
+ path: settingsPath
1718
+ });
1719
+ }
1720
+ const skillsDir = path12.join(dir, ".claude", "skills");
1721
+ if (fs14.existsSync(skillsDir)) {
1722
+ for (const file of fs14.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
1723
+ const filePath = path12.join(skillsDir, file);
1724
+ items.push({
1725
+ type: "skill",
1726
+ platform: "claude",
1727
+ name: file,
1728
+ contentHash: hashFile(filePath),
1729
+ path: filePath
1730
+ });
1731
+ }
1732
+ }
1733
+ const mcpJsonPath = path12.join(dir, ".mcp.json");
1734
+ if (fs14.existsSync(mcpJsonPath)) {
1735
+ try {
1736
+ const mcpJson = JSON.parse(fs14.readFileSync(mcpJsonPath, "utf-8"));
1737
+ if (mcpJson.mcpServers) {
1738
+ for (const name of Object.keys(mcpJson.mcpServers)) {
1739
+ items.push({
1740
+ type: "mcp",
1741
+ platform: "claude",
1742
+ name,
1743
+ contentHash: hashContent(JSON.stringify(mcpJson.mcpServers[name])),
1744
+ path: mcpJsonPath
1745
+ });
1746
+ }
1747
+ }
1748
+ } catch {
1749
+ }
1750
+ }
1751
+ const cursorrulesPath = path12.join(dir, ".cursorrules");
1752
+ if (fs14.existsSync(cursorrulesPath)) {
1753
+ items.push({
1754
+ type: "rule",
1755
+ platform: "cursor",
1756
+ name: ".cursorrules",
1757
+ contentHash: hashFile(cursorrulesPath),
1758
+ path: cursorrulesPath
1759
+ });
1760
+ }
1761
+ const cursorRulesDir = path12.join(dir, ".cursor", "rules");
1762
+ if (fs14.existsSync(cursorRulesDir)) {
1763
+ for (const file of fs14.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
1764
+ const filePath = path12.join(cursorRulesDir, file);
1765
+ items.push({
1766
+ type: "rule",
1767
+ platform: "cursor",
1768
+ name: file,
1769
+ contentHash: hashFile(filePath),
1770
+ path: filePath
1771
+ });
1772
+ }
1773
+ }
1774
+ const cursorMcpPath = path12.join(dir, ".cursor", "mcp.json");
1775
+ if (fs14.existsSync(cursorMcpPath)) {
1776
+ try {
1777
+ const mcpJson = JSON.parse(fs14.readFileSync(cursorMcpPath, "utf-8"));
1778
+ if (mcpJson.mcpServers) {
1779
+ for (const name of Object.keys(mcpJson.mcpServers)) {
1780
+ items.push({
1781
+ type: "mcp",
1782
+ platform: "cursor",
1783
+ name,
1784
+ contentHash: hashContent(JSON.stringify(mcpJson.mcpServers[name])),
1785
+ path: cursorMcpPath
1786
+ });
1787
+ }
1788
+ }
1789
+ } catch {
1790
+ }
1791
+ }
1792
+ return items;
1793
+ }
1794
+ function compareState(serverItems, localItems) {
1795
+ const installed = [];
1796
+ const missing = [];
1797
+ const outdated = [];
1798
+ const extra = [];
1799
+ const localMap = /* @__PURE__ */ new Map();
1800
+ for (const item of localItems) {
1801
+ localMap.set(`${item.type}:${item.platform}:${item.name}`, item);
1802
+ }
1803
+ for (const server of serverItems) {
1804
+ const key = `${server.type}:${server.platform}:${server.name}`;
1805
+ const local = localMap.get(key);
1806
+ localMap.delete(key);
1807
+ if (!local) {
1808
+ missing.push(server);
1809
+ } else if (local.contentHash !== server.content_hash) {
1810
+ outdated.push({ server, local });
1811
+ } else {
1812
+ installed.push({ server, local });
1813
+ }
1814
+ }
1815
+ for (const local of localMap.values()) {
1816
+ extra.push(local);
1817
+ }
1818
+ return { installed, missing, outdated, extra };
1819
+ }
1820
+ function hashFile(filePath) {
1821
+ const content = fs14.readFileSync(filePath);
1822
+ return crypto4.createHash("sha256").update(content).digest("hex");
1823
+ }
1824
+ function hashContent(content) {
1825
+ return crypto4.createHash("sha256").update(JSON.stringify({ text: content })).digest("hex");
1826
+ }
1827
+
1828
+ // src/commands/status.ts
1829
+ async function statusCommand(options) {
1304
1830
  const auth2 = getStoredAuth();
1305
1831
  const manifest = readManifest();
1306
1832
  if (options.json) {
@@ -1324,10 +1850,34 @@ function statusCommand(options) {
1324
1850
  }
1325
1851
  console.log(` Files managed: ${chalk4.cyan(manifest.entries.length.toString())}`);
1326
1852
  for (const entry of manifest.entries) {
1327
- const exists = fs11.existsSync(entry.path);
1853
+ const exists = fs15.existsSync(entry.path);
1328
1854
  const icon = exists ? chalk4.green("\u2713") : chalk4.red("\u2717");
1329
1855
  console.log(` ${icon} ${entry.path} (${entry.action})`);
1330
1856
  }
1857
+ if (auth2) {
1858
+ try {
1859
+ const fingerprint = collectFingerprint(process.cwd());
1860
+ const hash = computeFingerprintHash(fingerprint);
1861
+ const match = await apiRequest(
1862
+ "/api/projects/match",
1863
+ { method: "POST", body: { fingerprintHash: hash } }
1864
+ );
1865
+ if (match?.project) {
1866
+ const serverItems = await apiRequest(
1867
+ `/api/sync/project/${match.project.id}/items`
1868
+ );
1869
+ if (serverItems?.length) {
1870
+ const localItems = scanLocalState(process.cwd());
1871
+ const diff = compareState(serverItems, localItems);
1872
+ console.log(chalk4.bold("\n Sync"));
1873
+ console.log(` ${chalk4.green("\u2713")} Installed: ${diff.installed.length}`);
1874
+ if (diff.missing.length) console.log(` ${chalk4.red("\u2717")} Missing: ${diff.missing.length}`);
1875
+ if (diff.outdated.length) console.log(` ${chalk4.yellow("~")} Outdated: ${diff.outdated.length}`);
1876
+ }
1877
+ }
1878
+ } catch {
1879
+ }
1880
+ }
1331
1881
  console.log("");
1332
1882
  }
1333
1883
 
@@ -1339,12 +1889,12 @@ async function updateCommand(options) {
1339
1889
  const auth2 = getStoredAuth();
1340
1890
  if (!auth2) {
1341
1891
  console.log(chalk5.red("Not logged in. Run `caliber login` first."));
1342
- process.exit(1);
1892
+ throw new Error("__exit__");
1343
1893
  }
1344
1894
  const manifest = readManifest();
1345
1895
  if (!manifest) {
1346
1896
  console.log(chalk5.yellow("No existing setup found. Run `caliber init` first."));
1347
- process.exit(1);
1897
+ throw new Error("__exit__");
1348
1898
  }
1349
1899
  const spinner = ora4("Re-analyzing project...").start();
1350
1900
  const fingerprint = collectFingerprint(process.cwd());
@@ -1362,53 +1912,55 @@ async function updateCommand(options) {
1362
1912
  });
1363
1913
  let generatedSetup = null;
1364
1914
  trackEvent("generation_started", { target_agent: "both" });
1365
- const generationStart = Date.now();
1366
1915
  const genSpinner = ora4("Regenerating setup...").start();
1367
1916
  const genMessages = new SpinnerMessages(genSpinner, GENERATION_MESSAGES);
1368
1917
  genMessages.start();
1369
- await apiStream(
1370
- "/api/setups/generate",
1371
- {
1372
- fingerprint,
1373
- targetAgent: "both"
1374
- },
1375
- () => {
1376
- },
1377
- (payload) => {
1378
- generatedSetup = payload.setup;
1379
- },
1380
- (error) => {
1381
- genMessages.stop();
1382
- trackEvent("error_occurred", { error_type: "generation_failed", error_message: error });
1383
- genSpinner.fail(`Generation error: ${error}`);
1384
- },
1385
- (status) => {
1386
- genMessages.handleServerStatus(status);
1387
- }
1388
- );
1918
+ try {
1919
+ await apiStream(
1920
+ "/api/setups/generate",
1921
+ {
1922
+ fingerprint,
1923
+ targetAgent: "both"
1924
+ },
1925
+ () => {
1926
+ },
1927
+ (payload) => {
1928
+ generatedSetup = payload.setup;
1929
+ },
1930
+ (error) => {
1931
+ genMessages.stop();
1932
+ trackEvent("error_occurred", { error_type: "generation_failed", error_message: error, command: "update" });
1933
+ genSpinner.fail(`Generation error: ${error}`);
1934
+ },
1935
+ (status) => {
1936
+ genMessages.handleServerStatus(status);
1937
+ }
1938
+ );
1939
+ } catch (err) {
1940
+ genMessages.stop();
1941
+ const msg = err instanceof Error ? err.message : "Unknown error";
1942
+ trackEvent("error_occurred", { error_type: "generation_request_failed", error_message: msg, command: "update" });
1943
+ genSpinner.fail(`Regeneration failed: ${msg}`);
1944
+ throw new Error("__exit__");
1945
+ }
1389
1946
  genMessages.stop();
1390
1947
  if (!generatedSetup) {
1391
1948
  genSpinner.fail("Failed to regenerate setup.");
1392
- process.exit(1);
1949
+ throw new Error("__exit__");
1393
1950
  }
1394
1951
  genSpinner.succeed("Setup regenerated");
1395
- trackEvent("generation_completed", {
1396
- target_agent: "both",
1397
- duration_ms: Date.now() - generationStart,
1398
- files_suggested: countSuggestedFiles(generatedSetup)
1399
- });
1400
1952
  if (options.dryRun) {
1401
1953
  console.log(chalk5.yellow("\n[Dry run] Would write:"));
1402
1954
  console.log(JSON.stringify(generatedSetup, null, 2));
1403
1955
  return;
1404
1956
  }
1405
1957
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1406
- const answer = await new Promise((resolve) => {
1407
- rl.question(chalk5.cyan("\n\nApply updated setup? (y/n): "), resolve);
1958
+ const answer = await new Promise((resolve2) => {
1959
+ rl.question(chalk5.cyan("\n\nApply updated setup? (y/n): "), resolve2);
1408
1960
  });
1409
1961
  rl.close();
1410
1962
  if (answer.trim().toLowerCase() !== "y") {
1411
- trackEvent("generation_cancelled");
1963
+ trackEvent("update_declined");
1412
1964
  console.log(chalk5.dim("Update cancelled."));
1413
1965
  return;
1414
1966
  }
@@ -1435,35 +1987,457 @@ function logoutCommand() {
1435
1987
  console.log(chalk6.green("Logged out successfully."));
1436
1988
  }
1437
1989
 
1990
+ // src/commands/recommend.ts
1991
+ import chalk7 from "chalk";
1992
+ import ora5 from "ora";
1993
+ async function recommendCommand(options) {
1994
+ const auth2 = getStoredAuth();
1995
+ if (!auth2) {
1996
+ console.log(chalk7.red("Not authenticated. Run `caliber login` first."));
1997
+ throw new Error("__exit__");
1998
+ }
1999
+ const fingerprint = collectFingerprint(process.cwd());
2000
+ const hash = computeFingerprintHash(fingerprint);
2001
+ const match = await apiRequest(
2002
+ "/api/projects/match",
2003
+ { method: "POST", body: { fingerprintHash: hash } }
2004
+ );
2005
+ if (!match?.project) {
2006
+ console.log(chalk7.yellow("No project found. Run `caliber init` first."));
2007
+ throw new Error("__exit__");
2008
+ }
2009
+ const projectId = match.project.id;
2010
+ if (options.generate) {
2011
+ const spinner = ora5("Detecting technologies and searching for skills...").start();
2012
+ try {
2013
+ const recs2 = await apiRequest(
2014
+ `/api/recommendations/project/${projectId}/generate`,
2015
+ { method: "POST" }
2016
+ );
2017
+ spinner.succeed(`Found ${recs2?.length || 0} recommendations`);
2018
+ if (recs2?.length) {
2019
+ console.log("");
2020
+ printRecommendations(recs2);
2021
+ }
2022
+ } catch (err) {
2023
+ spinner.fail("Failed to generate recommendations");
2024
+ throw err;
2025
+ }
2026
+ return;
2027
+ }
2028
+ const statusFilter = options.status || "pending";
2029
+ const recs = await apiRequest(
2030
+ `/api/recommendations/project/${projectId}?status=${statusFilter}`
2031
+ );
2032
+ if (!recs?.length) {
2033
+ console.log(chalk7.dim(`
2034
+ No ${statusFilter} recommendations. Run \`caliber recommend --generate\` to discover skills.
2035
+ `));
2036
+ return;
2037
+ }
2038
+ printRecommendations(recs);
2039
+ }
2040
+ function printRecommendations(recs) {
2041
+ console.log(chalk7.bold("\n Skill Recommendations\n"));
2042
+ console.log(
2043
+ ` ${chalk7.dim("Name".padEnd(30))} ${chalk7.dim("Score".padEnd(8))} ${chalk7.dim("Technology".padEnd(15))} ${chalk7.dim("Status")}`
2044
+ );
2045
+ console.log(chalk7.dim(" " + "\u2500".repeat(70)));
2046
+ for (const rec of recs) {
2047
+ const scoreColor = rec.score >= 90 ? chalk7.green : rec.score >= 70 ? chalk7.blue : chalk7.yellow;
2048
+ console.log(
2049
+ ` ${rec.skill_name.padEnd(30)} ${scoreColor(`${rec.score}%`.padEnd(8))} ${rec.detected_technology.padEnd(15)} ${rec.status}`
2050
+ );
2051
+ if (rec.reason) {
2052
+ console.log(` ${chalk7.dim(rec.reason)}`);
2053
+ }
2054
+ }
2055
+ console.log("");
2056
+ }
2057
+
2058
+ // src/commands/health.ts
2059
+ import chalk8 from "chalk";
2060
+ import ora6 from "ora";
2061
+ async function healthCommand(options) {
2062
+ const auth2 = getStoredAuth();
2063
+ if (!auth2) {
2064
+ console.log(chalk8.red("Not authenticated. Run `caliber login` first."));
2065
+ throw new Error("__exit__");
2066
+ }
2067
+ const fingerprint = collectFingerprint(process.cwd());
2068
+ const hash = computeFingerprintHash(fingerprint);
2069
+ const match = await apiRequest(
2070
+ "/api/projects/match",
2071
+ { method: "POST", body: { fingerprintHash: hash } }
2072
+ );
2073
+ if (!match?.project) {
2074
+ console.log(chalk8.yellow("No project found. Run `caliber init` first."));
2075
+ throw new Error("__exit__");
2076
+ }
2077
+ const projectId = match.project.id;
2078
+ const spinner = ora6("Analyzing context health...").start();
2079
+ let report;
2080
+ try {
2081
+ report = await apiRequest(
2082
+ `/api/context/project/${projectId}/analyze`,
2083
+ { method: "POST" }
2084
+ );
2085
+ spinner.succeed("Analysis complete");
2086
+ } catch (err) {
2087
+ spinner.fail("Analysis failed");
2088
+ throw err;
2089
+ }
2090
+ if (options.json) {
2091
+ console.log(JSON.stringify(report, null, 2));
2092
+ return;
2093
+ }
2094
+ printReport(report);
2095
+ if (options.fix) {
2096
+ console.log(chalk8.bold("\nGenerating fix plan..."));
2097
+ const plan = await apiRequest(
2098
+ `/api/context/reports/${report.id}/fix-plan`,
2099
+ { method: "POST", body: { projectId } }
2100
+ );
2101
+ if (!plan?.actions.length) {
2102
+ console.log(chalk8.dim("No fixes needed."));
2103
+ return;
2104
+ }
2105
+ console.log(chalk8.bold("\nProposed actions:"));
2106
+ for (const action of plan.actions) {
2107
+ const icon = action.type === "remove" ? chalk8.red("- ") : action.type === "add" ? chalk8.green("+ ") : chalk8.yellow("~ ");
2108
+ console.log(` ${icon}${action.type}: ${action.items.join(", ")}`);
2109
+ }
2110
+ console.log(chalk8.dim(`
2111
+ Estimated improvement: +${plan.estimatedScoreImprovement} points`));
2112
+ const fixSpinner = ora6("Executing fix plan...").start();
2113
+ try {
2114
+ const result = await apiRequest(
2115
+ `/api/context/reports/${report.id}/fix-execute`,
2116
+ { method: "POST", body: { projectId } }
2117
+ );
2118
+ fixSpinner.succeed("Fix applied");
2119
+ console.log("");
2120
+ console.log(` Score: ${result.scoreBefore} \u2192 ${chalk8.green(String(result.scoreAfter))} (${chalk8.green(`+${result.improvement}`)})`);
2121
+ if (result.itemsRemoved) console.log(` Removed: ${result.itemsRemoved} items`);
2122
+ if (result.itemsConsolidated) console.log(` Consolidated: ${result.itemsConsolidated} items`);
2123
+ if (result.itemsAdded) console.log(` Added: ${result.itemsAdded} items`);
2124
+ console.log("");
2125
+ } catch (err) {
2126
+ fixSpinner.fail("Fix execution failed");
2127
+ throw err;
2128
+ }
2129
+ }
2130
+ }
2131
+ function printReport(report) {
2132
+ const gradeColors = {
2133
+ A: chalk8.green,
2134
+ B: chalk8.blue,
2135
+ C: chalk8.yellow,
2136
+ D: chalk8.hex("#FFA500"),
2137
+ F: chalk8.red
2138
+ };
2139
+ const gradeColor = gradeColors[report.grade] || chalk8.white;
2140
+ console.log(chalk8.bold("\n Context Health Report\n"));
2141
+ console.log(` Grade: ${gradeColor(report.grade)} Score: ${gradeColor(String(report.score))}/100
2142
+ `);
2143
+ if (Object.keys(report.category_breakdown).length) {
2144
+ console.log(chalk8.dim(" Category Breakdown:"));
2145
+ const categories = Object.entries(report.category_breakdown);
2146
+ const maxCount = Math.max(...categories.map(([, v]) => v.count));
2147
+ for (const [name, data] of categories) {
2148
+ const barLen = maxCount > 0 ? Math.round(data.count / maxCount * 20) : 0;
2149
+ const bar = "\u2588".repeat(barLen) + "\u2591".repeat(20 - barLen);
2150
+ console.log(` ${name.padEnd(12)} ${bar} ${data.count} items (${data.tokens} tokens)`);
2151
+ }
2152
+ console.log("");
2153
+ }
2154
+ if (report.recommendations.length) {
2155
+ console.log(chalk8.dim(" Recommendations:"));
2156
+ for (const rec of report.recommendations) {
2157
+ const icon = rec.priority === "high" ? chalk8.red("!") : rec.priority === "medium" ? chalk8.yellow("~") : chalk8.dim("-");
2158
+ console.log(` ${icon} ${rec.description}`);
2159
+ }
2160
+ console.log("");
2161
+ }
2162
+ }
2163
+
2164
+ // src/commands/sync.ts
2165
+ import chalk9 from "chalk";
2166
+ import ora7 from "ora";
2167
+ async function syncCommand(options) {
2168
+ const auth2 = getStoredAuth();
2169
+ if (!auth2) {
2170
+ console.log(chalk9.red("Not authenticated. Run `caliber login` first."));
2171
+ throw new Error("__exit__");
2172
+ }
2173
+ const fingerprint = collectFingerprint(process.cwd());
2174
+ const hash = computeFingerprintHash(fingerprint);
2175
+ const match = await apiRequest(
2176
+ "/api/projects/match",
2177
+ { method: "POST", body: { fingerprintHash: hash } }
2178
+ );
2179
+ if (!match?.project) {
2180
+ console.log(chalk9.yellow("No project found. Run `caliber init` first."));
2181
+ throw new Error("__exit__");
2182
+ }
2183
+ const projectId = match.project.id;
2184
+ const spinner = ora7("Scanning local state...").start();
2185
+ const localItems = scanLocalState(process.cwd());
2186
+ spinner.text = "Fetching server items...";
2187
+ const serverItems = await apiRequest(
2188
+ `/api/sync/project/${projectId}/items`
2189
+ );
2190
+ spinner.succeed(`Found ${localItems.length} local items, ${serverItems?.length || 0} server items`);
2191
+ if (!serverItems?.length) {
2192
+ console.log(chalk9.dim("\nNo items configured on server. Run `caliber init` to set up your project.\n"));
2193
+ return;
2194
+ }
2195
+ const platformFilter = options.platform;
2196
+ const filteredServer = platformFilter ? serverItems.filter((i) => i.platform === platformFilter || i.platform === "both") : serverItems;
2197
+ const filteredLocal = platformFilter ? localItems.filter((i) => i.platform === platformFilter) : localItems;
2198
+ const diff = compareState(filteredServer, filteredLocal);
2199
+ printDiff(diff);
2200
+ if (diff.missing.length === 0 && diff.outdated.length === 0) {
2201
+ console.log(chalk9.green("\nAll items synced.\n"));
2202
+ await reportToServer(projectId, filteredServer, diff);
2203
+ return;
2204
+ }
2205
+ if (options.dryRun) {
2206
+ console.log(chalk9.dim("\nDry run \u2014 no changes made.\n"));
2207
+ return;
2208
+ }
2209
+ const installSpinner = ora7("Installing missing and outdated items...").start();
2210
+ try {
2211
+ const setup = buildSetupFromItems([...diff.missing, ...diff.outdated.map((o) => o.server)]);
2212
+ if (setup) {
2213
+ writeSetup(setup);
2214
+ }
2215
+ installSpinner.succeed("Items synced successfully");
2216
+ } catch (err) {
2217
+ installSpinner.fail("Sync failed");
2218
+ throw err;
2219
+ }
2220
+ await reportToServer(projectId, filteredServer, diff);
2221
+ }
2222
+ async function reportToServer(projectId, serverItems, diff) {
2223
+ const items = [
2224
+ ...diff.installed.map((i) => ({
2225
+ itemId: i.server.id,
2226
+ state: "installed",
2227
+ localHash: i.local.contentHash
2228
+ })),
2229
+ ...diff.missing.map((i) => ({
2230
+ itemId: i.id,
2231
+ state: "missing"
2232
+ })),
2233
+ ...diff.outdated.map((i) => ({
2234
+ itemId: i.server.id,
2235
+ state: "outdated",
2236
+ localHash: i.local.contentHash
2237
+ })),
2238
+ ...diff.extra.map((i) => ({
2239
+ itemId: i.name,
2240
+ state: "extra",
2241
+ localHash: i.contentHash
2242
+ }))
2243
+ ];
2244
+ const platforms = new Set(serverItems.map((i) => i.platform));
2245
+ for (const platform of platforms) {
2246
+ try {
2247
+ await apiRequest(`/api/sync/project/${projectId}/report`, {
2248
+ method: "POST",
2249
+ body: {
2250
+ platform,
2251
+ items: items.filter((i) => {
2252
+ const match = serverItems.find((s) => s.id === i.itemId);
2253
+ return match?.platform === platform;
2254
+ })
2255
+ }
2256
+ });
2257
+ } catch {
2258
+ }
2259
+ }
2260
+ }
2261
+ function printDiff(diff) {
2262
+ console.log(chalk9.bold("\n Sync Status\n"));
2263
+ if (diff.installed.length) {
2264
+ console.log(` ${chalk9.green("\u2713")} Installed: ${diff.installed.length}`);
2265
+ }
2266
+ if (diff.missing.length) {
2267
+ console.log(` ${chalk9.red("\u2717")} Missing: ${diff.missing.length}`);
2268
+ for (const item of diff.missing) {
2269
+ console.log(` ${chalk9.red("-")} ${item.name} (${item.type}/${item.platform})`);
2270
+ }
2271
+ }
2272
+ if (diff.outdated.length) {
2273
+ console.log(` ${chalk9.yellow("~")} Outdated: ${diff.outdated.length}`);
2274
+ for (const item of diff.outdated) {
2275
+ console.log(` ${chalk9.yellow("~")} ${item.server.name} (${item.server.type}/${item.server.platform})`);
2276
+ }
2277
+ }
2278
+ if (diff.extra.length) {
2279
+ console.log(` ${chalk9.dim("+")} Extra (local only): ${diff.extra.length}`);
2280
+ for (const item of diff.extra) {
2281
+ console.log(` ${chalk9.dim("+")} ${item.name} (${item.type}/${item.platform})`);
2282
+ }
2283
+ }
2284
+ console.log("");
2285
+ }
2286
+ function buildSetupFromItems(items) {
2287
+ if (!items.length) return null;
2288
+ const claude = {};
2289
+ const cursor = {};
2290
+ let hasAny = false;
2291
+ for (const item of items) {
2292
+ const content = item.content;
2293
+ if (item.platform === "claude" || item.platform === "both") {
2294
+ if (item.type === "rule" && item.name === "CLAUDE.md") {
2295
+ claude.claudeMd = content.text;
2296
+ hasAny = true;
2297
+ } else if (item.type === "config" && item.name === "settings.json") {
2298
+ claude.settings = content;
2299
+ hasAny = true;
2300
+ } else if (item.type === "skill") {
2301
+ if (!claude.skills) claude.skills = [];
2302
+ claude.skills.push({
2303
+ name: item.name,
2304
+ content: content.text || ""
2305
+ });
2306
+ hasAny = true;
2307
+ } else if (item.type === "mcp") {
2308
+ if (!claude.mcpServers) claude.mcpServers = {};
2309
+ claude.mcpServers[item.name] = content;
2310
+ hasAny = true;
2311
+ }
2312
+ }
2313
+ if (item.platform === "cursor" || item.platform === "both") {
2314
+ if (item.type === "rule" && item.name === ".cursorrules") {
2315
+ cursor.cursorrules = content.text;
2316
+ hasAny = true;
2317
+ } else if (item.type === "rule") {
2318
+ if (!cursor.rules) cursor.rules = [];
2319
+ cursor.rules.push({
2320
+ filename: item.name,
2321
+ content: content.text || ""
2322
+ });
2323
+ hasAny = true;
2324
+ } else if (item.type === "mcp") {
2325
+ if (!cursor.mcpServers) cursor.mcpServers = {};
2326
+ cursor.mcpServers[item.name] = content;
2327
+ hasAny = true;
2328
+ }
2329
+ }
2330
+ }
2331
+ if (!hasAny) return null;
2332
+ const hasClaude = Object.keys(claude).length > 0;
2333
+ const hasCursor = Object.keys(cursor).length > 0;
2334
+ return {
2335
+ targetAgent: hasClaude && hasCursor ? "both" : hasClaude ? "claude" : "cursor",
2336
+ ...hasClaude ? { claude } : {},
2337
+ ...hasCursor ? { cursor } : {}
2338
+ };
2339
+ }
2340
+
2341
+ // src/commands/diff.ts
2342
+ import chalk10 from "chalk";
2343
+ import ora8 from "ora";
2344
+ async function diffCommand(options) {
2345
+ const auth2 = getStoredAuth();
2346
+ if (!auth2) {
2347
+ console.log(chalk10.red("Not authenticated. Run `caliber login` first."));
2348
+ throw new Error("__exit__");
2349
+ }
2350
+ const fingerprint = collectFingerprint(process.cwd());
2351
+ const hash = computeFingerprintHash(fingerprint);
2352
+ const match = await apiRequest(
2353
+ "/api/projects/match",
2354
+ { method: "POST", body: { fingerprintHash: hash } }
2355
+ );
2356
+ if (!match?.project) {
2357
+ console.log(chalk10.yellow("No project found. Run `caliber init` first."));
2358
+ throw new Error("__exit__");
2359
+ }
2360
+ const projectId = match.project.id;
2361
+ const spinner = ora8("Comparing local and server state...").start();
2362
+ const localItems = scanLocalState(process.cwd());
2363
+ const serverItems = await apiRequest(
2364
+ `/api/sync/project/${projectId}/items`
2365
+ );
2366
+ spinner.stop();
2367
+ if (!serverItems?.length) {
2368
+ console.log(chalk10.dim("\nNo items configured on server.\n"));
2369
+ return;
2370
+ }
2371
+ const platformFilter = options.platform;
2372
+ const filteredServer = platformFilter ? serverItems.filter((i) => i.platform === platformFilter || i.platform === "both") : serverItems;
2373
+ const filteredLocal = platformFilter ? localItems.filter((i) => i.platform === platformFilter) : localItems;
2374
+ const diff = compareState(filteredServer, filteredLocal);
2375
+ console.log(chalk10.bold("\n Config Diff\n"));
2376
+ if (diff.installed.length) {
2377
+ console.log(` ${chalk10.green("\u2713")} In sync: ${diff.installed.length}`);
2378
+ }
2379
+ if (diff.missing.length) {
2380
+ console.log(` ${chalk10.red("\u2717")} Missing locally: ${diff.missing.length}`);
2381
+ for (const item of diff.missing) {
2382
+ console.log(` ${chalk10.red("-")} ${item.name} (${item.type}/${item.platform})`);
2383
+ }
2384
+ }
2385
+ if (diff.outdated.length) {
2386
+ console.log(` ${chalk10.yellow("~")} Outdated: ${diff.outdated.length}`);
2387
+ for (const item of diff.outdated) {
2388
+ console.log(` ${chalk10.yellow("~")} ${item.server.name} (${item.server.type}/${item.server.platform})`);
2389
+ }
2390
+ }
2391
+ if (diff.extra.length) {
2392
+ console.log(` ${chalk10.dim("+")} Local only: ${diff.extra.length}`);
2393
+ for (const item of diff.extra) {
2394
+ console.log(` ${chalk10.dim("+")} ${item.name} (${item.type}/${item.platform})`);
2395
+ }
2396
+ }
2397
+ if (diff.missing.length === 0 && diff.outdated.length === 0) {
2398
+ console.log(chalk10.green("\n Everything is in sync.\n"));
2399
+ } else {
2400
+ console.log(chalk10.dim("\n Run `caliber sync` to apply changes.\n"));
2401
+ }
2402
+ }
2403
+
1438
2404
  // src/cli.ts
1439
- var pkg2 = JSON.parse(
1440
- fs12.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
2405
+ var __dirname2 = path13.dirname(fileURLToPath3(import.meta.url));
2406
+ var pkg3 = JSON.parse(
2407
+ fs16.readFileSync(path13.resolve(__dirname2, "..", "package.json"), "utf-8")
1441
2408
  );
1442
2409
  var program = new Command();
1443
- program.name("caliber").description("Configure your coding agent environment").version(pkg2.version);
2410
+ program.name("caliber").description("Configure your coding agent environment").version(pkg3.version);
1444
2411
  program.command("init").description("Initialize coding agent setup for this project").option("--agent <type>", "Target agent: claude, cursor, or both").option("--dry-run", "Preview changes without writing files").option("--force", "Overwrite existing setup without prompting").action(initCommand);
1445
2412
  program.command("undo").description("Revert all config changes made by Caliber").action(undoCommand);
1446
2413
  program.command("status").description("Show current Caliber setup status").option("--json", "Output as JSON").action(statusCommand);
1447
2414
  program.command("update").description("Re-analyze project and update setup").option("--dry-run", "Preview changes without writing files").action(updateCommand);
1448
2415
  program.command("login").description("Authenticate with Caliber").action(loginCommand);
1449
2416
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
2417
+ program.command("recommend").description("Discover and manage skill recommendations").option("--generate", "Generate new recommendations").option("--status <status>", "Filter by status: pending, accepted, dismissed").action(recommendCommand);
2418
+ program.command("health").description("Analyze context health and quality").option("--fix", "Generate and execute a fix plan").option("--json", "Output as JSON").action(healthCommand);
2419
+ program.command("sync").description("Sync local config with server state").option("--platform <platform>", "Target platform: claude, cursor, or both").option("--dry-run", "Preview changes without writing files").action(syncCommand);
2420
+ program.command("diff").description("Compare local config with server state").option("--platform <platform>", "Target platform: claude, cursor, or both").action(diffCommand);
1450
2421
 
1451
2422
  // src/utils/version-check.ts
1452
- import fs13 from "fs";
2423
+ import fs17 from "fs";
2424
+ import path14 from "path";
2425
+ import { fileURLToPath as fileURLToPath4 } from "url";
1453
2426
  import readline3 from "readline";
1454
2427
  import { execSync as execSync2 } from "child_process";
1455
- import chalk7 from "chalk";
1456
- import ora5 from "ora";
1457
- var pkg3 = JSON.parse(
1458
- fs13.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
2428
+ import chalk11 from "chalk";
2429
+ import ora9 from "ora";
2430
+ var __dirname_vc = path14.dirname(fileURLToPath4(import.meta.url));
2431
+ var pkg4 = JSON.parse(
2432
+ fs17.readFileSync(path14.resolve(__dirname_vc, "..", "package.json"), "utf-8")
1459
2433
  );
1460
2434
  function promptYesNo(question) {
1461
2435
  const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
1462
- return new Promise((resolve) => {
1463
- rl.question(chalk7.cyan(`${question} `), (answer) => {
2436
+ return new Promise((resolve2) => {
2437
+ rl.question(chalk11.cyan(`${question} `), (answer) => {
1464
2438
  rl.close();
1465
2439
  const normalized = answer.trim().toLowerCase();
1466
- resolve(normalized === "" || normalized === "y" || normalized === "yes");
2440
+ resolve2(normalized === "" || normalized === "y" || normalized === "yes");
1467
2441
  });
1468
2442
  });
1469
2443
  }
@@ -1479,22 +2453,22 @@ async function checkForUpdates() {
1479
2453
  const data = await res.json();
1480
2454
  const latest = data.version;
1481
2455
  if (!latest) return;
1482
- const current = pkg3.version;
2456
+ const current = pkg4.version;
1483
2457
  if (current === latest) return;
1484
2458
  const isInteractive = process.stdin.isTTY === true;
1485
2459
  if (!isInteractive) {
1486
2460
  console.log(
1487
- chalk7.yellow(
2461
+ chalk11.yellow(
1488
2462
  `
1489
2463
  Update available: ${current} -> ${latest}
1490
- Run ${chalk7.bold("npm install -g @caliber-ai/cli")} to upgrade.
2464
+ Run ${chalk11.bold("npm install -g @caliber-ai/cli")} to upgrade.
1491
2465
  `
1492
2466
  )
1493
2467
  );
1494
2468
  return;
1495
2469
  }
1496
2470
  console.log(
1497
- chalk7.yellow(`
2471
+ chalk11.yellow(`
1498
2472
  Update available: ${current} -> ${latest}`)
1499
2473
  );
1500
2474
  const shouldUpdate = await promptYesNo("Would you like to update now? (Y/n)");
@@ -1502,12 +2476,12 @@ Update available: ${current} -> ${latest}`)
1502
2476
  console.log();
1503
2477
  return;
1504
2478
  }
1505
- const spinner = ora5("Updating @caliber-ai/cli...").start();
2479
+ const spinner = ora9("Updating @caliber-ai/cli...").start();
1506
2480
  try {
1507
2481
  execSync2("npm install -g @caliber-ai/cli", { stdio: "pipe" });
1508
- spinner.succeed(chalk7.green(`Updated to ${latest}`));
2482
+ spinner.succeed(chalk11.green(`Updated to ${latest}`));
1509
2483
  const args = process.argv.slice(2);
1510
- console.log(chalk7.dim(`
2484
+ console.log(chalk11.dim(`
1511
2485
  Restarting: caliber ${args.join(" ")}
1512
2486
  `));
1513
2487
  execSync2(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
@@ -1517,8 +2491,8 @@ Restarting: caliber ${args.join(" ")}
1517
2491
  } catch {
1518
2492
  spinner.fail("Update failed");
1519
2493
  console.log(
1520
- chalk7.yellow(
1521
- `Run ${chalk7.bold("npm install -g @caliber-ai/cli")} manually to upgrade.
2494
+ chalk11.yellow(
2495
+ `Run ${chalk11.bold("npm install -g @caliber-ai/cli")} manually to upgrade.
1522
2496
  `
1523
2497
  )
1524
2498
  );
@@ -1536,12 +2510,25 @@ if (firstRun) {
1536
2510
  var auth = getStoredAuth();
1537
2511
  if (auth) {
1538
2512
  identifyUser(auth.userId, auth.email);
2513
+ Sentry.setUser({ id: auth.userId, email: auth.email });
1539
2514
  }
1540
2515
  await checkForUpdates();
1541
- process.on("SIGINT", () => process.exit(130));
1542
- process.on("SIGTERM", () => process.exit(143));
1543
- program.parseAsync().finally(async () => {
1544
- await shutdownTelemetry();
1545
- process.exit(0);
1546
- });
2516
+ async function gracefulExit(code) {
2517
+ await Promise.all([
2518
+ shutdownTelemetry(),
2519
+ Sentry.flush(2e3)
2520
+ ]);
2521
+ process.exit(code);
2522
+ }
2523
+ process.on("SIGINT", () => gracefulExit(130));
2524
+ process.on("SIGTERM", () => gracefulExit(143));
2525
+ program.parseAsync().catch((err) => {
2526
+ const msg = err instanceof Error ? err.message : "Unexpected error";
2527
+ if (msg !== "__exit__") {
2528
+ console.error(msg);
2529
+ Sentry.captureException(err, { tags: { command: process.argv[2] || "unknown" } });
2530
+ }
2531
+ trackEvent("error_occurred", { error_type: "cli_crash", error_message: msg, command: process.argv[2] || "unknown" });
2532
+ process.exitCode = 1;
2533
+ }).finally(() => gracefulExit(Number(process.exitCode ?? 0)));
1547
2534
  //# sourceMappingURL=bin.js.map