@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.
- package/dist/bin.js +1304 -317
- package/dist/bin.js.map +1 -1
- 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
|
|
36
|
-
import
|
|
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 =
|
|
45
|
-
AUTH_FILE =
|
|
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 =
|
|
48
|
-
BACKUPS_DIR =
|
|
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
|
|
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
|
|
86
|
+
import fs13 from "fs";
|
|
61
87
|
|
|
62
88
|
// src/auth/token-store.ts
|
|
63
89
|
init_constants();
|
|
64
|
-
import
|
|
90
|
+
import fs2 from "fs";
|
|
65
91
|
function getStoredAuth() {
|
|
66
92
|
try {
|
|
67
|
-
if (!
|
|
68
|
-
const data = JSON.parse(
|
|
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 (!
|
|
76
|
-
|
|
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
|
-
|
|
111
|
+
fs2.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
86
112
|
}
|
|
87
113
|
function clearAuth() {
|
|
88
114
|
try {
|
|
89
|
-
if (
|
|
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
|
|
142
|
+
import { URL } from "url";
|
|
117
143
|
function startCallbackServer(expectedState) {
|
|
118
|
-
return new Promise((
|
|
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
|
|
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
|
-
|
|
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
|
|
181
|
-
import
|
|
182
|
-
import
|
|
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 =
|
|
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 (
|
|
196
|
-
return
|
|
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 (!
|
|
203
|
-
|
|
229
|
+
if (!fs3.existsSync(AUTH_DIR)) {
|
|
230
|
+
fs3.mkdirSync(AUTH_DIR, { recursive: true });
|
|
204
231
|
}
|
|
205
|
-
|
|
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 !
|
|
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
|
|
218
|
-
|
|
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:
|
|
224
|
-
os:
|
|
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((
|
|
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
|
-
|
|
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
|
|
326
|
-
import
|
|
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
|
|
378
|
-
const allDeps = { ...
|
|
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
|
-
|
|
392
|
-
|
|
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 (!
|
|
421
|
+
if (!fs4.existsSync(filePath)) continue;
|
|
399
422
|
try {
|
|
400
|
-
const content =
|
|
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 =
|
|
433
|
+
const rootPkgPath = path4.join(dir, "package.json");
|
|
411
434
|
let name;
|
|
412
435
|
const allFrameworks = [];
|
|
413
436
|
const languages = [];
|
|
414
|
-
if (
|
|
437
|
+
if (fs4.existsSync(rootPkgPath)) {
|
|
415
438
|
try {
|
|
416
|
-
const
|
|
417
|
-
name =
|
|
418
|
-
const allDeps = { ...
|
|
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
|
|
433
|
-
const deps = { ...
|
|
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
|
|
451
|
-
import
|
|
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 =
|
|
497
|
+
const fullPath = path5.join(base, rel);
|
|
475
498
|
let entries;
|
|
476
499
|
try {
|
|
477
|
-
entries =
|
|
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
|
|
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 =
|
|
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
|
|
531
|
-
import
|
|
553
|
+
import fs6 from "fs";
|
|
554
|
+
import path7 from "path";
|
|
532
555
|
function readExistingConfigs(dir) {
|
|
533
556
|
const configs = {};
|
|
534
|
-
const claudeMdPath =
|
|
535
|
-
if (
|
|
536
|
-
configs.claudeMd =
|
|
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 =
|
|
539
|
-
if (
|
|
561
|
+
const claudeSettingsPath = path7.join(dir, ".claude", "settings.json");
|
|
562
|
+
if (fs6.existsSync(claudeSettingsPath)) {
|
|
540
563
|
try {
|
|
541
|
-
configs.claudeSettings = JSON.parse(
|
|
564
|
+
configs.claudeSettings = JSON.parse(fs6.readFileSync(claudeSettingsPath, "utf-8"));
|
|
542
565
|
} catch {
|
|
543
566
|
}
|
|
544
567
|
}
|
|
545
|
-
const skillsDir =
|
|
546
|
-
if (
|
|
568
|
+
const skillsDir = path7.join(dir, ".claude", "skills");
|
|
569
|
+
if (fs6.existsSync(skillsDir)) {
|
|
547
570
|
try {
|
|
548
|
-
const files =
|
|
571
|
+
const files = fs6.readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
|
|
549
572
|
configs.claudeSkills = files.map((f) => ({
|
|
550
573
|
filename: f,
|
|
551
|
-
content:
|
|
574
|
+
content: fs6.readFileSync(path7.join(skillsDir, f), "utf-8")
|
|
552
575
|
}));
|
|
553
576
|
} catch {
|
|
554
577
|
}
|
|
555
578
|
}
|
|
556
|
-
const cursorrulesPath =
|
|
557
|
-
if (
|
|
558
|
-
configs.cursorrules =
|
|
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 =
|
|
561
|
-
if (
|
|
583
|
+
const cursorRulesDir = path7.join(dir, ".cursor", "rules");
|
|
584
|
+
if (fs6.existsSync(cursorRulesDir)) {
|
|
562
585
|
try {
|
|
563
|
-
const files =
|
|
586
|
+
const files = fs6.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"));
|
|
564
587
|
configs.cursorRules = files.map((f) => ({
|
|
565
588
|
filename: f,
|
|
566
|
-
content:
|
|
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
|
|
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
|
|
625
|
-
const
|
|
626
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
645
|
-
|
|
646
|
-
|
|
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
|
|
1017
|
+
import fs12 from "fs";
|
|
687
1018
|
|
|
688
1019
|
// src/writers/claude/index.ts
|
|
689
|
-
import
|
|
690
|
-
import
|
|
1020
|
+
import fs8 from "fs";
|
|
1021
|
+
import path9 from "path";
|
|
691
1022
|
function writeClaudeConfig(config) {
|
|
692
1023
|
const written = [];
|
|
693
|
-
|
|
1024
|
+
fs8.writeFileSync("CLAUDE.md", config.claudeMd);
|
|
694
1025
|
written.push("CLAUDE.md");
|
|
695
1026
|
const claudeDir = ".claude";
|
|
696
|
-
if (!
|
|
697
|
-
|
|
698
|
-
|
|
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(
|
|
702
|
-
|
|
703
|
-
|
|
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(
|
|
1037
|
+
written.push(path9.join(claudeDir, "settings.local.json"));
|
|
707
1038
|
if (config.skills?.length) {
|
|
708
|
-
const skillsDir =
|
|
709
|
-
if (!
|
|
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 =
|
|
713
|
-
|
|
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
|
-
|
|
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
|
|
727
|
-
import
|
|
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
|
-
|
|
1062
|
+
fs9.writeFileSync(".cursorrules", config.cursorrules);
|
|
732
1063
|
written.push(".cursorrules");
|
|
733
1064
|
}
|
|
734
1065
|
if (config.rules?.length) {
|
|
735
|
-
const rulesDir =
|
|
736
|
-
if (!
|
|
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 =
|
|
739
|
-
|
|
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 (!
|
|
1076
|
+
if (!fs9.existsSync(cursorDir)) fs9.mkdirSync(cursorDir, { recursive: true });
|
|
746
1077
|
const mcpConfig = { mcpServers: config.mcpServers };
|
|
747
|
-
|
|
748
|
-
|
|
1078
|
+
fs9.writeFileSync(
|
|
1079
|
+
path10.join(cursorDir, "mcp.json"),
|
|
749
1080
|
JSON.stringify(mcpConfig, null, 2)
|
|
750
1081
|
);
|
|
751
|
-
written.push(
|
|
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
|
|
759
|
-
import
|
|
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 =
|
|
1093
|
+
const backupDir = path11.join(BACKUPS_DIR, timestamp);
|
|
763
1094
|
for (const file of files) {
|
|
764
|
-
if (!
|
|
765
|
-
const dest =
|
|
766
|
-
const destDir =
|
|
767
|
-
if (!
|
|
768
|
-
|
|
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
|
-
|
|
1101
|
+
fs10.copyFileSync(file, dest);
|
|
771
1102
|
}
|
|
772
1103
|
return backupDir;
|
|
773
1104
|
}
|
|
774
1105
|
function restoreBackup(backupDir, file) {
|
|
775
|
-
const backupFile =
|
|
776
|
-
if (!
|
|
777
|
-
const destDir =
|
|
778
|
-
if (!
|
|
779
|
-
|
|
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
|
-
|
|
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
|
|
1118
|
+
import fs11 from "fs";
|
|
788
1119
|
import crypto3 from "crypto";
|
|
789
1120
|
function readManifest() {
|
|
790
1121
|
try {
|
|
791
|
-
if (!
|
|
792
|
-
return JSON.parse(
|
|
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 (!
|
|
799
|
-
|
|
1129
|
+
if (!fs11.existsSync(CALIBER_DIR)) {
|
|
1130
|
+
fs11.mkdirSync(CALIBER_DIR, { recursive: true });
|
|
800
1131
|
}
|
|
801
|
-
|
|
1132
|
+
fs11.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
|
|
802
1133
|
}
|
|
803
1134
|
function fileChecksum(filePath) {
|
|
804
|
-
const content =
|
|
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) =>
|
|
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 (
|
|
840
|
-
|
|
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 (
|
|
851
|
-
|
|
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 (
|
|
878
|
-
const content =
|
|
1208
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
1209
|
+
const content = fs12.readFileSync(gitignorePath, "utf-8");
|
|
879
1210
|
if (!content.includes(".caliber/")) {
|
|
880
|
-
|
|
1211
|
+
fs12.appendFileSync(gitignorePath, "\n# Caliber local state\n.caliber/\n");
|
|
881
1212
|
}
|
|
882
1213
|
} else {
|
|
883
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
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
|
|
1397
|
+
let explained = false;
|
|
1398
|
+
let action = await promptAction(explained);
|
|
1050
1399
|
while (action === "explain") {
|
|
1051
1400
|
if (setupExplanation) {
|
|
1052
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
1442
|
+
throw new Error("__exit__");
|
|
1098
1443
|
}
|
|
1099
1444
|
try {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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((
|
|
1522
|
+
return new Promise((resolve2) => {
|
|
1175
1523
|
rl.question(chalk2.cyan(`${question} `), (answer) => {
|
|
1176
1524
|
rl.close();
|
|
1177
|
-
|
|
1525
|
+
resolve2(answer.trim());
|
|
1178
1526
|
});
|
|
1179
1527
|
});
|
|
1180
1528
|
}
|
|
1181
1529
|
function promptAgent() {
|
|
1182
|
-
return new Promise((
|
|
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
|
-
|
|
1539
|
+
resolve2(map[answer.trim()] || "claude");
|
|
1192
1540
|
});
|
|
1193
1541
|
});
|
|
1194
1542
|
}
|
|
1195
|
-
function promptAction() {
|
|
1196
|
-
return new Promise((
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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(
|
|
1590
|
+
console.log(fileEntry("CLAUDE.md", descriptions["CLAUDE.md"]));
|
|
1222
1591
|
}
|
|
1223
1592
|
if (claude.settings) {
|
|
1224
|
-
console.log(
|
|
1593
|
+
console.log(fileEntry(".claude/settings.json", descriptions[".claude/settings.json"]));
|
|
1225
1594
|
}
|
|
1226
1595
|
if (claude.settingsLocal) {
|
|
1227
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1303
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
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((
|
|
1407
|
-
rl.question(chalk5.cyan("\n\nApply updated setup? (y/n): "),
|
|
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("
|
|
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
|
|
1440
|
-
|
|
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(
|
|
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
|
|
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
|
|
1456
|
-
import
|
|
1457
|
-
var
|
|
1458
|
-
|
|
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((
|
|
1463
|
-
rl.question(
|
|
2436
|
+
return new Promise((resolve2) => {
|
|
2437
|
+
rl.question(chalk11.cyan(`${question} `), (answer) => {
|
|
1464
2438
|
rl.close();
|
|
1465
2439
|
const normalized = answer.trim().toLowerCase();
|
|
1466
|
-
|
|
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 =
|
|
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
|
-
|
|
2461
|
+
chalk11.yellow(
|
|
1488
2462
|
`
|
|
1489
2463
|
Update available: ${current} -> ${latest}
|
|
1490
|
-
Run ${
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
2482
|
+
spinner.succeed(chalk11.green(`Updated to ${latest}`));
|
|
1509
2483
|
const args = process.argv.slice(2);
|
|
1510
|
-
console.log(
|
|
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
|
-
|
|
1521
|
-
`Run ${
|
|
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
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
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
|