lens-engine 0.1.18 → 0.1.20

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/cli.js CHANGED
@@ -3347,36 +3347,108 @@ var {
3347
3347
  Help
3348
3348
  } = import_index.default;
3349
3349
 
3350
- // packages/cli/src/util/repo.ts
3351
- import { readFile } from "fs/promises";
3352
- import { existsSync } from "fs";
3353
- import { resolve, dirname, basename } from "path";
3354
- function findGitRoot(from) {
3355
- let dir = resolve(from);
3356
- while (true) {
3357
- if (existsSync(resolve(dir, ".git"))) return dir;
3358
- const parent = dirname(dir);
3359
- if (parent === dir) return null;
3360
- dir = parent;
3350
+ // packages/cli/src/util/config.ts
3351
+ import { randomUUID } from "crypto";
3352
+ import fsSync from "fs";
3353
+ import fs from "fs/promises";
3354
+ import os from "os";
3355
+ import path from "path";
3356
+ var CONFIG_DIR = path.join(os.homedir(), ".lens");
3357
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
3358
+ var DEFAULTS = {
3359
+ inject_behavior: "always",
3360
+ show_progress: true
3361
+ };
3362
+ async function readConfig() {
3363
+ try {
3364
+ const raw = await fs.readFile(CONFIG_FILE, "utf-8");
3365
+ return { ...DEFAULTS, ...JSON.parse(raw) };
3366
+ } catch {
3367
+ return DEFAULTS;
3361
3368
  }
3362
3369
  }
3363
- async function parseRemoteUrl(gitRoot) {
3370
+ async function writeConfig(config) {
3371
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
3372
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
3373
+ }
3374
+ async function configGet(key) {
3375
+ const config = await readConfig();
3376
+ if (key in config) {
3377
+ return JSON.stringify(config[key], null, 2);
3378
+ }
3379
+ return "null";
3380
+ }
3381
+ async function configSet(key, value) {
3382
+ const config = await readConfig();
3383
+ if (key === "inject_behavior" && ["always", "skip", "once"].includes(value)) {
3384
+ config.inject_behavior = value;
3385
+ } else if (key === "show_progress") {
3386
+ config.show_progress = value === "true";
3387
+ } else if (key === "cloud_url") {
3388
+ config.cloud_url = value;
3389
+ } else if (key === "telemetry") {
3390
+ config.telemetry = value === "true";
3391
+ } else {
3392
+ throw new Error(`Invalid config: ${key}=${value}`);
3393
+ }
3394
+ await writeConfig(config);
3395
+ }
3396
+ function getCloudUrl() {
3397
+ if (process.env.LENS_CLOUD_URL) return process.env.LENS_CLOUD_URL;
3364
3398
  try {
3365
- const config = await readFile(resolve(gitRoot, ".git/config"), "utf-8");
3366
- const match = config.match(/\[remote "origin"\][^[]*url\s*=\s*(.+)/m);
3367
- return match?.[1]?.trim();
3399
+ const cfg2 = JSON.parse(fsSync.readFileSync(CONFIG_FILE, "utf-8"));
3400
+ if (cfg2.cloud_url) return cfg2.cloud_url;
3368
3401
  } catch {
3369
- return void 0;
3402
+ }
3403
+ return "https://cloud.lens-engine.com";
3404
+ }
3405
+ function readConfigSync() {
3406
+ try {
3407
+ return { ...DEFAULTS, ...JSON.parse(fsSync.readFileSync(CONFIG_FILE, "utf-8")) };
3408
+ } catch {
3409
+ return DEFAULTS;
3370
3410
  }
3371
3411
  }
3372
- async function detectRepo(cwd = process.cwd()) {
3373
- const root = findGitRoot(cwd);
3374
- if (!root) {
3375
- throw new Error("Not inside a git repository. Run from a git repo or use `git init`.");
3412
+ function isTelemetryEnabled() {
3413
+ const config = readConfigSync();
3414
+ return config.telemetry !== false;
3415
+ }
3416
+
3417
+ // packages/cli/src/util/format.ts
3418
+ function output(data, json) {
3419
+ if (json) {
3420
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
3421
+ `);
3422
+ } else if (typeof data === "string") {
3423
+ process.stdout.write(`${data}
3424
+ `);
3425
+ } else {
3426
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
3427
+ `);
3428
+ }
3429
+ }
3430
+ function error(msg) {
3431
+ process.stderr.write(`Error: ${msg}
3432
+ `);
3433
+ }
3434
+
3435
+ // packages/cli/src/commands/config.ts
3436
+ async function configGetCommand(key) {
3437
+ try {
3438
+ const value = await configGet(key);
3439
+ output(value, false);
3440
+ } catch (err) {
3441
+ output(err instanceof Error ? err.message : String(err), false);
3442
+ }
3443
+ }
3444
+ async function configSetCommand(key, value) {
3445
+ try {
3446
+ await configSet(key, value);
3447
+ output(`Config updated: ${key} = ${value}`, false);
3448
+ } catch (err) {
3449
+ output(err instanceof Error ? err.message : String(err), false);
3450
+ process.exit(1);
3376
3451
  }
3377
- const remote_url = await parseRemoteUrl(root);
3378
- const name = basename(root);
3379
- return { root_path: root, name, remote_url };
3380
3452
  }
3381
3453
 
3382
3454
  // packages/cli/src/util/client.ts
@@ -3434,159 +3506,336 @@ async function request(method, path4, body, retries = 3) {
3434
3506
  var get = (path4) => request("GET", path4);
3435
3507
  var post = (path4, body) => request("POST", path4, body);
3436
3508
 
3437
- // packages/cli/src/util/format.ts
3438
- function output(data, json) {
3439
- if (json) {
3440
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
3441
- } else if (typeof data === "string") {
3442
- process.stdout.write(data + "\n");
3443
- } else {
3444
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
3509
+ // packages/cli/src/util/repo.ts
3510
+ import { existsSync } from "fs";
3511
+ import { readFile } from "fs/promises";
3512
+ import { basename, dirname, resolve } from "path";
3513
+ function findGitRoot(from) {
3514
+ let dir = resolve(from);
3515
+ while (true) {
3516
+ if (existsSync(resolve(dir, ".git"))) return dir;
3517
+ const parent = dirname(dir);
3518
+ if (parent === dir) return null;
3519
+ dir = parent;
3445
3520
  }
3446
3521
  }
3447
- function error(msg) {
3448
- process.stderr.write(`Error: ${msg}
3449
- `);
3450
- }
3451
-
3452
- // packages/cli/src/util/inject-claude-md.ts
3453
- import fs2 from "fs/promises";
3454
- import path2 from "path";
3455
-
3456
- // packages/cli/src/util/config.ts
3457
- import fs from "fs/promises";
3458
- import fsSync from "fs";
3459
- import path from "path";
3460
- import os from "os";
3461
- import { randomUUID } from "crypto";
3462
- var CONFIG_DIR = path.join(os.homedir(), ".lens");
3463
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
3464
- var DEFAULTS = {
3465
- inject_behavior: "always",
3466
- show_progress: true
3467
- };
3468
- async function readConfig() {
3522
+ async function parseRemoteUrl(gitRoot) {
3469
3523
  try {
3470
- const raw = await fs.readFile(CONFIG_FILE, "utf-8");
3471
- return { ...DEFAULTS, ...JSON.parse(raw) };
3524
+ const config = await readFile(resolve(gitRoot, ".git/config"), "utf-8");
3525
+ const match = config.match(/\[remote "origin"\][^[]*url\s*=\s*(.+)/m);
3526
+ return match?.[1]?.trim();
3472
3527
  } catch {
3473
- return DEFAULTS;
3528
+ return void 0;
3474
3529
  }
3475
3530
  }
3476
- async function writeConfig(config) {
3477
- await fs.mkdir(CONFIG_DIR, { recursive: true });
3478
- await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
3479
- }
3480
- async function configGet(key) {
3481
- const config = await readConfig();
3482
- if (key in config) {
3483
- return JSON.stringify(config[key], null, 2);
3531
+ async function detectRepo(cwd = process.cwd()) {
3532
+ const root = findGitRoot(cwd);
3533
+ if (!root) {
3534
+ throw new Error("Not inside a git repository. Run from a git repo or use `git init`.");
3484
3535
  }
3485
- return "null";
3536
+ const remote_url = await parseRemoteUrl(root);
3537
+ const name = basename(root);
3538
+ return { root_path: root, name, remote_url };
3486
3539
  }
3487
- async function configSet(key, value) {
3488
- const config = await readConfig();
3489
- if (key === "inject_behavior" && ["always", "skip", "once"].includes(value)) {
3490
- config.inject_behavior = value;
3491
- } else if (key === "show_progress") {
3492
- config.show_progress = value === "true";
3493
- } else if (key === "cloud_url") {
3494
- config.cloud_url = value;
3495
- } else if (key === "telemetry") {
3496
- config.telemetry = value === "true";
3540
+
3541
+ // packages/cli/src/util/ensure-repo.ts
3542
+ async function ensureRepo() {
3543
+ const info = await detectRepo();
3544
+ const res = await post("/repo/register", {
3545
+ root_path: info.root_path,
3546
+ name: info.name,
3547
+ remote_url: info.remote_url
3548
+ });
3549
+ return { repo_id: res.repo_id, name: res.name, root_path: info.root_path };
3550
+ }
3551
+
3552
+ // packages/cli/src/commands/context.ts
3553
+ async function contextCommand(goal, opts) {
3554
+ const { repo_id } = await ensureRepo();
3555
+ const res = await post("/context", { repo_id, goal });
3556
+ if (opts.json) {
3557
+ output(res, true);
3497
3558
  } else {
3498
- throw new Error(`Invalid config: ${key}=${value}`);
3559
+ output(res.context_pack, false);
3499
3560
  }
3500
- await writeConfig(config);
3501
3561
  }
3502
- function getCloudUrl() {
3503
- if (process.env.LENS_CLOUD_URL) return process.env.LENS_CLOUD_URL;
3562
+
3563
+ // packages/cli/src/commands/daemon-ctrl.ts
3564
+ import { execSync, spawn } from "child_process";
3565
+ import { existsSync as existsSync2, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "fs";
3566
+ import { createRequire } from "module";
3567
+ import { homedir } from "os";
3568
+ import { dirname as dirname2, join } from "path";
3569
+ import { fileURLToPath } from "url";
3570
+ var LENS_DIR = join(homedir(), ".lens");
3571
+ var PID_FILE = join(LENS_DIR, "daemon.pid");
3572
+ var LOG_FILE = join(LENS_DIR, "daemon.log");
3573
+ var LAUNCH_AGENTS_DIR = join(homedir(), "Library", "LaunchAgents");
3574
+ var PLIST_PATH = join(LAUNCH_AGENTS_DIR, "com.lens.daemon.plist");
3575
+ var SYSTEMD_DIR = join(homedir(), ".config", "systemd", "user");
3576
+ var SERVICE_PATH = join(SYSTEMD_DIR, "lens-daemon.service");
3577
+ var WIN_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
3578
+ var WIN_REG_NAME = "LensDaemon";
3579
+ function isDaemonRunning() {
3580
+ if (!existsSync2(PID_FILE)) return { running: false };
3581
+ const pid = Number(readFileSync(PID_FILE, "utf-8").trim());
3504
3582
  try {
3505
- const cfg2 = JSON.parse(fsSync.readFileSync(CONFIG_FILE, "utf-8"));
3506
- if (cfg2.cloud_url) return cfg2.cloud_url;
3583
+ process.kill(pid, 0);
3584
+ return { running: true, pid };
3507
3585
  } catch {
3586
+ unlinkSync(PID_FILE);
3587
+ return { running: false };
3508
3588
  }
3509
- return "https://cloud.lens-engine.com";
3510
3589
  }
3511
- function readConfigSync() {
3590
+ function setupDarwin(daemonScript) {
3591
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
3592
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3593
+ <plist version="1.0">
3594
+ <dict>
3595
+ <key>Label</key>
3596
+ <string>com.lens.daemon</string>
3597
+ <key>ProgramArguments</key>
3598
+ <array>
3599
+ <string>${process.execPath}</string>
3600
+ <string>${daemonScript}</string>
3601
+ </array>
3602
+ <key>RunAtLoad</key>
3603
+ <true/>
3604
+ <key>KeepAlive</key>
3605
+ <false/>
3606
+ <key>StandardOutPath</key>
3607
+ <string>${LOG_FILE}</string>
3608
+ <key>StandardErrorPath</key>
3609
+ <string>${LOG_FILE}</string>
3610
+ <key>EnvironmentVariables</key>
3611
+ <dict>
3612
+ <key>LENS_DAEMON</key>
3613
+ <string>1</string>
3614
+ </dict>
3615
+ </dict>
3616
+ </plist>
3617
+ `;
3618
+ const existing = existsSync2(PLIST_PATH) ? readFileSync(PLIST_PATH, "utf-8") : "";
3619
+ if (existing === plist) return;
3620
+ if (existing) {
3621
+ try {
3622
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
3623
+ } catch {
3624
+ }
3625
+ }
3626
+ mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
3627
+ writeFileSync(PLIST_PATH, plist);
3512
3628
  try {
3513
- return { ...DEFAULTS, ...JSON.parse(fsSync.readFileSync(CONFIG_FILE, "utf-8")) };
3629
+ execSync(`launchctl load "${PLIST_PATH}" 2>/dev/null`);
3514
3630
  } catch {
3515
- return DEFAULTS;
3516
3631
  }
3517
3632
  }
3518
- function isTelemetryEnabled() {
3519
- const config = readConfigSync();
3520
- return config.telemetry !== false;
3633
+ function teardownDarwin() {
3634
+ if (!existsSync2(PLIST_PATH)) return;
3635
+ try {
3636
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
3637
+ } catch {
3638
+ }
3521
3639
  }
3640
+ function setupLinux(daemonScript) {
3641
+ const unit = `[Unit]
3642
+ Description=LENS Daemon
3522
3643
 
3523
- // packages/cli/src/util/inject-claude-md.ts
3524
- var RLM_MARKER = "lens context";
3525
- var CANDIDATE_PATHS = ["CLAUDE.md", "agents.md", "AGENTS.md", ".CLAUDE.md", ".agents.md", ".AGENTS.md"];
3526
- async function findTargetFile(repoRoot) {
3527
- for (const relPath of CANDIDATE_PATHS) {
3528
- const fullPath = path2.join(repoRoot, relPath);
3529
- try {
3530
- await fs2.access(fullPath);
3531
- return fullPath;
3532
- } catch {
3533
- continue;
3534
- }
3644
+ [Service]
3645
+ ExecStart=${process.execPath} ${daemonScript}
3646
+ Restart=no
3647
+ Environment=LENS_DAEMON=1
3648
+ StandardOutput=append:${LOG_FILE}
3649
+ StandardError=append:${LOG_FILE}
3650
+
3651
+ [Install]
3652
+ WantedBy=default.target
3653
+ `;
3654
+ const existing = existsSync2(SERVICE_PATH) ? readFileSync(SERVICE_PATH, "utf-8") : "";
3655
+ if (existing === unit) return;
3656
+ mkdirSync(SYSTEMD_DIR, { recursive: true });
3657
+ writeFileSync(SERVICE_PATH, unit);
3658
+ try {
3659
+ execSync("systemctl --user daemon-reload 2>/dev/null");
3660
+ } catch {
3535
3661
  }
3536
- return null;
3537
- }
3538
- async function injectClaudeMd(repoRoot) {
3539
- const existingFile = await findTargetFile(repoRoot);
3540
- const targetFile = existingFile ?? path2.join(repoRoot, "CLAUDE.md");
3541
- let existingContent = "";
3542
3662
  try {
3543
- existingContent = await fs2.readFile(targetFile, "utf-8");
3544
- if (existingContent.toLowerCase().includes(RLM_MARKER)) {
3545
- return;
3546
- }
3663
+ execSync("systemctl --user enable lens-daemon.service 2>/dev/null");
3547
3664
  } catch {
3548
3665
  }
3549
- const config = await readConfig();
3550
- if (existingFile && config.inject_behavior === "once") {
3666
+ }
3667
+ function teardownLinux() {
3668
+ try {
3669
+ execSync("systemctl --user disable lens-daemon.service 2>/dev/null");
3670
+ } catch {
3671
+ }
3672
+ }
3673
+ function setupWindows(daemonScript) {
3674
+ const cmd = `"${process.execPath}" "${daemonScript}"`;
3675
+ try {
3676
+ const existing = execSync(`reg query "${WIN_REG_KEY}" /v ${WIN_REG_NAME} 2>nul`, { encoding: "utf-8" });
3677
+ if (existing.includes(cmd)) return;
3678
+ } catch {
3679
+ }
3680
+ try {
3681
+ execSync(`reg add "${WIN_REG_KEY}" /v ${WIN_REG_NAME} /t REG_SZ /d "${cmd}" /f 2>nul`);
3682
+ } catch {
3683
+ }
3684
+ }
3685
+ function teardownWindows() {
3686
+ try {
3687
+ execSync(`reg delete "${WIN_REG_KEY}" /v ${WIN_REG_NAME} /f 2>nul`);
3688
+ } catch {
3689
+ }
3690
+ }
3691
+ function setupAutoStart(daemonScript) {
3692
+ switch (process.platform) {
3693
+ case "darwin":
3694
+ setupDarwin(daemonScript);
3695
+ break;
3696
+ case "linux":
3697
+ setupLinux(daemonScript);
3698
+ break;
3699
+ case "win32":
3700
+ setupWindows(daemonScript);
3701
+ break;
3702
+ }
3703
+ }
3704
+ function removeAutoStart() {
3705
+ switch (process.platform) {
3706
+ case "darwin":
3707
+ teardownDarwin();
3708
+ break;
3709
+ case "linux":
3710
+ teardownLinux();
3711
+ break;
3712
+ case "win32":
3713
+ teardownWindows();
3714
+ break;
3715
+ }
3716
+ }
3717
+ async function startCommand() {
3718
+ const status = isDaemonRunning();
3719
+ if (status.running) {
3720
+ output(`LENS daemon already running (pid: ${status.pid})`, false);
3551
3721
  return;
3552
3722
  }
3553
- if (config.inject_behavior === "skip") {
3723
+ const selfDir = dirname2(fileURLToPath(import.meta.url));
3724
+ const sibling = join(selfDir, "daemon.js");
3725
+ let daemonScript;
3726
+ if (existsSync2(sibling)) {
3727
+ daemonScript = sibling;
3728
+ } else {
3729
+ try {
3730
+ const require2 = createRequire(import.meta.url);
3731
+ daemonScript = require2.resolve("@lens/daemon");
3732
+ } catch {
3733
+ error("Could not find @lens/daemon. Run `pnpm build` first.");
3734
+ return;
3735
+ }
3736
+ }
3737
+ mkdirSync(LENS_DIR, { recursive: true });
3738
+ setupAutoStart(daemonScript);
3739
+ if (isDaemonRunning().running) {
3740
+ const check2 = isDaemonRunning();
3741
+ output(`LENS daemon started (pid: ${check2.pid})`, false);
3554
3742
  return;
3555
3743
  }
3556
- const { content } = await get("/repo/template");
3557
- if (existingContent) {
3558
- await fs2.writeFile(targetFile, `${content}
3559
-
3560
- ${existingContent}`);
3744
+ const logFd = openSync(LOG_FILE, "a");
3745
+ const child = spawn(process.execPath, [daemonScript], {
3746
+ detached: true,
3747
+ stdio: ["ignore", logFd, logFd],
3748
+ env: { ...process.env, LENS_DAEMON: "1" }
3749
+ });
3750
+ child.unref();
3751
+ for (let i = 0; i < 20; i++) {
3752
+ await new Promise((r) => setTimeout(r, 250));
3753
+ if (isDaemonRunning().running) break;
3754
+ }
3755
+ const check = isDaemonRunning();
3756
+ if (check.running) {
3757
+ output(`LENS daemon started (pid: ${check.pid})`, false);
3561
3758
  } else {
3562
- await fs2.writeFile(targetFile, content);
3759
+ error(`LENS daemon failed to start. Check logs: ${LOG_FILE}`);
3563
3760
  }
3564
3761
  }
3565
-
3566
- // packages/cli/src/util/inject-mcp.ts
3567
- import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
3568
- import { join } from "path";
3569
- var MCP_ENTRY = {
3570
- command: "npx",
3571
- args: ["lens-daemon", "--stdio"]
3572
- };
3573
- function injectMcp(repoRoot) {
3574
- const mcpPath = join(repoRoot, ".mcp.json");
3575
- let existing = {};
3576
- if (existsSync2(mcpPath)) {
3577
- try {
3578
- existing = JSON.parse(readFileSync(mcpPath, "utf-8"));
3579
- } catch {
3762
+ async function stopCommand() {
3763
+ const status = isDaemonRunning();
3764
+ if (!status.running) {
3765
+ output("LENS daemon is not running", false);
3766
+ return;
3767
+ }
3768
+ removeAutoStart();
3769
+ process.kill(status.pid, "SIGTERM");
3770
+ for (let i = 0; i < 20; i++) {
3771
+ await new Promise((r) => setTimeout(r, 250));
3772
+ if (!isDaemonRunning().running) {
3773
+ output(`LENS daemon stopped (pid: ${status.pid})`, false);
3774
+ return;
3580
3775
  }
3581
3776
  }
3582
- if (!existing.mcpServers) existing.mcpServers = {};
3583
- const servers = existing.mcpServers;
3584
- if (servers.lens) return "exists";
3585
- const isNew = !existsSync2(mcpPath);
3586
- servers.lens = MCP_ENTRY;
3587
- writeFileSync(mcpPath, `${JSON.stringify(existing, null, 2)}
3588
- `);
3589
- return isNew ? "created" : "updated";
3777
+ error(`Daemon did not stop within 5s. Force kill with: kill -9 ${status.pid}`);
3778
+ }
3779
+
3780
+ // packages/cli/src/commands/daemon-stats.ts
3781
+ async function daemonStatsCommand(opts) {
3782
+ const s = await get("/daemon/stats");
3783
+ if (opts.json) {
3784
+ output(s, true);
3785
+ return;
3786
+ }
3787
+ const embedPct = s.total_chunks > 0 ? Math.round(s.total_embeddings / s.total_chunks * 100) : 0;
3788
+ const maintResult = s.last_maintenance_result ? `${s.last_maintenance_result.processed} processed, ${s.last_maintenance_result.errors} errors` : "\u2014";
3789
+ const lines = [
3790
+ "## LENS Daemon",
3791
+ "",
3792
+ ` Repos: ${s.repos_count}`,
3793
+ ` Chunks: ${s.total_chunks.toLocaleString()}`,
3794
+ ` Embeddings: ${s.total_embeddings.toLocaleString()} (${embedPct}%)`,
3795
+ ` DB Size: ${s.db_size_mb} MB`,
3796
+ "",
3797
+ " ## Maintenance Cron",
3798
+ ` Last maintenance: ${s.last_maintenance_at ?? "never"}`,
3799
+ ` Next maintenance: ${s.next_maintenance_at ?? "\u2014"}`,
3800
+ ` Last result: ${maintResult}`
3801
+ ];
3802
+ output(lines.join("\n"), false);
3803
+ }
3804
+
3805
+ // packages/cli/src/commands/dashboard.ts
3806
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
3807
+ import { homedir as homedir2 } from "os";
3808
+ import { join as join2 } from "path";
3809
+
3810
+ // packages/cli/src/util/browser.ts
3811
+ import { exec } from "child_process";
3812
+ function openBrowser(url) {
3813
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3814
+ exec(`${cmd} ${url}`);
3815
+ }
3816
+
3817
+ // packages/cli/src/commands/dashboard.ts
3818
+ var PID_FILE2 = join2(homedir2(), ".lens", "daemon.pid");
3819
+ function isDaemonRunning2() {
3820
+ if (!existsSync3(PID_FILE2)) return false;
3821
+ const pid = Number(readFileSync2(PID_FILE2, "utf-8").trim());
3822
+ try {
3823
+ process.kill(pid, 0);
3824
+ return true;
3825
+ } catch {
3826
+ return false;
3827
+ }
3828
+ }
3829
+ async function dashboardCommand() {
3830
+ if (!isDaemonRunning2()) {
3831
+ error("LENS daemon is not running. Start with: lens daemon start");
3832
+ return;
3833
+ }
3834
+ const port = process.env.LENS_PORT || "4111";
3835
+ const host = process.env.LENS_HOST || `http://127.0.0.1:${port}`;
3836
+ const url = `${host}/dashboard`;
3837
+ output(`Opening ${url}`, false);
3838
+ openBrowser(url);
3590
3839
  }
3591
3840
 
3592
3841
  // packages/cli/src/util/progress.ts
@@ -3618,7 +3867,7 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3618
3867
  if (connFailures >= MAX_CONN_FAILURES) {
3619
3868
  process.stdout.write(
3620
3869
  `
3621
- ${red("\u2717")} ${bold("Daemon unavailable")} \u2014 ${dim("connection lost after " + connFailures + " retries")}
3870
+ ${red("\u2717")} ${bold("Daemon unavailable")} \u2014 ${dim(`connection lost after ${connFailures} retries`)}
3622
3871
  `
3623
3872
  );
3624
3873
  process.stdout.write(` ${dim("Restart with: lens start")}
@@ -3693,7 +3942,9 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3693
3942
  if (embDone) {
3694
3943
  lines.push(` ${green("\u2713")} Embeddings ${dim(`${s.embedded_count}/${s.embeddable_count} code chunks`)}`);
3695
3944
  } else if (s.embedding_quota_exceeded) {
3696
- lines.push(` ${yellow("\u26A0")} Embeddings ${dim(`quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}`)}`);
3945
+ lines.push(
3946
+ ` ${yellow("\u26A0")} Embeddings ${dim(`quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}`)}`
3947
+ );
3697
3948
  } else if (s.embeddable_count === 0) {
3698
3949
  lines.push(` ${dim("\u25CB")} Embeddings ${dim("no code chunks")}`);
3699
3950
  } else {
@@ -3705,7 +3956,9 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3705
3956
  if (sumDone) {
3706
3957
  lines.push(` ${green("\u2713")} Summaries ${dim(`${s.purpose_count}/${s.purpose_total} files`)}`);
3707
3958
  } else if (s.purpose_quota_exceeded) {
3708
- lines.push(` ${yellow("\u26A0")} Summaries ${dim(`quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}`)}`);
3959
+ lines.push(
3960
+ ` ${yellow("\u26A0")} Summaries ${dim(`quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}`)}`
3961
+ );
3709
3962
  } else if (s.purpose_total > 0) {
3710
3963
  lines.push(
3711
3964
  ` ${cyan(f)} Summaries ${createBar(Math.round(s.purpose_count / s.purpose_total * 100), 20)} ${dim(`${s.purpose_count}/${s.purpose_total}`)}`
@@ -3754,7 +4007,8 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3754
4007
  if (lastRendered) {
3755
4008
  process.stdout.write(`\x1B[${TOTAL_LINES}A\x1B[J`);
3756
4009
  }
3757
- process.stdout.write(output2 + "\n");
4010
+ process.stdout.write(`${output2}
4011
+ `);
3758
4012
  lastRendered = output2;
3759
4013
  if (allDone) {
3760
4014
  done = true;
@@ -3774,65 +4028,6 @@ function sleep2(ms) {
3774
4028
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3775
4029
  }
3776
4030
 
3777
- // packages/cli/src/commands/register.ts
3778
- async function registerCommand(opts) {
3779
- const info = await detectRepo();
3780
- const res = await post("/repo/register", {
3781
- root_path: info.root_path,
3782
- name: info.name,
3783
- remote_url: info.remote_url
3784
- });
3785
- if (opts.json) {
3786
- output(res, true);
3787
- return;
3788
- }
3789
- const mcpResult = injectMcp(info.root_path);
3790
- if (res.created) {
3791
- await injectClaudeMd(info.root_path);
3792
- output(`Registered ${res.name} (repo_id: ${res.repo_id})`, false);
3793
- if (mcpResult === "created" || mcpResult === "updated") {
3794
- output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
3795
- }
3796
- const config = await readConfig();
3797
- if (config.show_progress) {
3798
- await showProgress(res.repo_id, res.name);
3799
- } else {
3800
- output(`Indexing started. Run \`lens status\` to check progress.`, false);
3801
- }
3802
- } else {
3803
- output(`Already registered ${res.name} (repo_id: ${res.repo_id})`, false);
3804
- if (mcpResult === "created" || mcpResult === "updated") {
3805
- output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
3806
- }
3807
- if (opts.inject) {
3808
- await injectClaudeMd(info.root_path);
3809
- output(`Injected LENS instructions into CLAUDE.md`, false);
3810
- }
3811
- }
3812
- }
3813
-
3814
- // packages/cli/src/util/ensure-repo.ts
3815
- async function ensureRepo() {
3816
- const info = await detectRepo();
3817
- const res = await post("/repo/register", {
3818
- root_path: info.root_path,
3819
- name: info.name,
3820
- remote_url: info.remote_url
3821
- });
3822
- return { repo_id: res.repo_id, name: res.name, root_path: info.root_path };
3823
- }
3824
-
3825
- // packages/cli/src/commands/context.ts
3826
- async function contextCommand(goal, opts) {
3827
- const { repo_id } = await ensureRepo();
3828
- const res = await post("/context", { repo_id, goal });
3829
- if (opts.json) {
3830
- output(res, true);
3831
- } else {
3832
- output(res.context_pack, false);
3833
- }
3834
- }
3835
-
3836
4031
  // packages/cli/src/commands/index.ts
3837
4032
  async function indexCommand(opts) {
3838
4033
  const { repo_id, name } = await ensureRepo();
@@ -3866,56 +4061,6 @@ async function indexCommand(opts) {
3866
4061
  await showProgress(repo_id, name);
3867
4062
  }
3868
4063
 
3869
- // packages/cli/src/commands/status.ts
3870
- var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
3871
- var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
3872
- var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
3873
- var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
3874
- async function statusCommand(opts) {
3875
- const { repo_id, name } = await ensureRepo();
3876
- const s = await get(`/repo/${repo_id}/status`);
3877
- if (opts.json) {
3878
- output(s, true);
3879
- return;
3880
- }
3881
- const staleTag = s.is_stale ? yellow2(" STALE") : "";
3882
- const check = green2("\u2713");
3883
- const pending = dim2("\u25CB");
3884
- const hasCaps = s.has_capabilities !== false;
3885
- const lines = [
3886
- ``,
3887
- ` ${bold2(name)}${staleTag}`,
3888
- dim2(` ${"\u2500".repeat(40)}`),
3889
- ` ${s.chunk_count > 0 ? check : pending} Chunks ${dim2(s.chunk_count.toLocaleString())}`,
3890
- ` ${s.metadata_count > 0 ? check : pending} Metadata ${dim2(`${s.metadata_count.toLocaleString()} files`)}`,
3891
- ` ${s.git_commits_analyzed > 0 ? check : pending} Git history ${dim2(`${s.git_commits_analyzed.toLocaleString()} files`)}`,
3892
- ` ${s.import_edge_count > 0 ? check : pending} Import graph ${dim2(`${s.import_edge_count.toLocaleString()} edges`)}`,
3893
- ` ${s.cochange_pairs > 0 ? check : pending} Co-changes ${dim2(`${s.cochange_pairs.toLocaleString()} pairs`)}`
3894
- ];
3895
- if (hasCaps) {
3896
- const warn = yellow2("\u26A0");
3897
- const embDone = s.embedded_pct >= 100 || s.embedded_count >= s.embeddable_count && s.embeddable_count > 0;
3898
- const embLabel = s.embedding_quota_exceeded ? `quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}` : s.embeddable_count > 0 ? `${s.embedded_count}/${s.embeddable_count} code chunks (${s.embedded_pct}%)` : "no code chunks";
3899
- const embIcon = s.embedding_quota_exceeded ? warn : embDone ? check : pending;
3900
- const vocabLabel = s.vocab_cluster_count > 0 ? `${s.vocab_cluster_count} clusters` : "...";
3901
- const vocabIcon = s.vocab_cluster_count > 0 ? check : pending;
3902
- const purDone = s.purpose_count > 0 && s.purpose_count >= s.purpose_total;
3903
- const purposeLabel = s.purpose_quota_exceeded ? `quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}` : s.purpose_total > 0 ? `${s.purpose_count}/${s.purpose_total} files` : "no files";
3904
- const purposeIcon = s.purpose_quota_exceeded ? warn : purDone ? check : pending;
3905
- lines.push(` ${vocabIcon} Vocab clust. ${dim2(vocabLabel)}`);
3906
- lines.push(` ${embIcon} Embeddings ${dim2(embLabel)}`);
3907
- lines.push(` ${purposeIcon} Summaries ${dim2(purposeLabel)}`);
3908
- } else {
3909
- lines.push(``);
3910
- lines.push(` ${yellow2("\u26A1")} ${bold2("Pro")}`);
3911
- lines.push(` ${dim2("\xB7 Vocab clusters")}`);
3912
- lines.push(` ${dim2("\xB7 Embeddings")}`);
3913
- lines.push(` ${dim2("\xB7 Summaries")}`);
3914
- lines.push(` ${dim2("lens login \u2192 upgrade to enable")}`);
3915
- }
3916
- output(lines.join("\n"), false);
3917
- }
3918
-
3919
4064
  // packages/cli/src/commands/list.ts
3920
4065
  async function listCommand(opts) {
3921
4066
  const res = await get("/repo/list/detailed");
@@ -3932,280 +4077,43 @@ async function listCommand(opts) {
3932
4077
  const lines = [header, sep];
3933
4078
  for (const r of res.repos) {
3934
4079
  const id = r.id.slice(0, 8);
3935
- const indexed = r.last_indexed_at ? timeAgo(r.last_indexed_at) : "never";
3936
- lines.push(
3937
- pad(id, 10) + pad(r.name, 20) + pad(r.index_status, 10) + pad(String(r.files_indexed), 8) + pad(r.chunk_count.toLocaleString(), 10) + pad(`${r.embedded_pct}%`, 8) + indexed
3938
- );
3939
- }
3940
- output(lines.join("\n"), false);
3941
- }
3942
- function pad(s, width) {
3943
- return s.length >= width ? s.slice(0, width - 1) + " " : s + " ".repeat(width - s.length);
3944
- }
3945
- function timeAgo(iso) {
3946
- const diff = Date.now() - new Date(iso).getTime();
3947
- const mins = Math.floor(diff / 6e4);
3948
- if (mins < 1) return "just now";
3949
- if (mins < 60) return `${mins}m ago`;
3950
- const hours = Math.floor(mins / 60);
3951
- if (hours < 24) return `${hours}h ago`;
3952
- const days = Math.floor(hours / 24);
3953
- return `${days}d ago`;
3954
- }
3955
-
3956
- // packages/cli/src/commands/remove.ts
3957
- async function removeCommand(opts) {
3958
- const { repo_id, name } = await ensureRepo();
3959
- if (!opts.yes) {
3960
- process.stderr.write(`Remove "${name}" (${repo_id})? All index data will be deleted.
3961
- `);
3962
- process.stderr.write(`Re-run with --yes to confirm.
3963
- `);
3964
- process.exit(1);
3965
- }
3966
- const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3967
- let i = 0;
3968
- let phase = "Stopping watcher";
3969
- const interval = setInterval(() => {
3970
- const frame = spinner[i % spinner.length];
3971
- process.stdout.write(`\r${frame} ${phase}...`);
3972
- i++;
3973
- }, 80);
3974
- await new Promise((r) => setTimeout(r, 200));
3975
- phase = `Removing ${name}`;
3976
- const res = await request("DELETE", `/repo/${repo_id}`);
3977
- clearInterval(interval);
3978
- if (opts.json) {
3979
- output(res, true);
3980
- } else {
3981
- process.stdout.write(`\r\u2713 Removed ${name}: ${res.chunks_removed} chunks deleted
3982
- `);
3983
- }
3984
- }
3985
-
3986
- // packages/cli/src/commands/daemon-stats.ts
3987
- async function daemonStatsCommand(opts) {
3988
- const s = await get("/daemon/stats");
3989
- if (opts.json) {
3990
- output(s, true);
3991
- return;
3992
- }
3993
- const embedPct = s.total_chunks > 0 ? Math.round(s.total_embeddings / s.total_chunks * 100) : 0;
3994
- const maintResult = s.last_maintenance_result ? `${s.last_maintenance_result.processed} processed, ${s.last_maintenance_result.errors} errors` : "\u2014";
3995
- const lines = [
3996
- "## LENS Daemon",
3997
- "",
3998
- ` Repos: ${s.repos_count}`,
3999
- ` Chunks: ${s.total_chunks.toLocaleString()}`,
4000
- ` Embeddings: ${s.total_embeddings.toLocaleString()} (${embedPct}%)`,
4001
- ` DB Size: ${s.db_size_mb} MB`,
4002
- "",
4003
- " ## Maintenance Cron",
4004
- ` Last maintenance: ${s.last_maintenance_at ?? "never"}`,
4005
- ` Next maintenance: ${s.next_maintenance_at ?? "\u2014"}`,
4006
- ` Last result: ${maintResult}`
4007
- ];
4008
- output(lines.join("\n"), false);
4009
- }
4010
-
4011
- // packages/cli/src/commands/watch.ts
4012
- async function watchCommand(opts) {
4013
- const { repo_id, name } = await ensureRepo();
4014
- const res = await post("/index/watch", { repo_id });
4015
- if (opts.json) {
4016
- output(res, true);
4017
- } else if (res.already_watching) {
4018
- output(`${name} is already being watched`, false);
4019
- } else {
4020
- output(`File watcher started for ${name}`, false);
4021
- }
4022
- }
4023
- async function unwatchCommand(opts) {
4024
- const { repo_id, name } = await ensureRepo();
4025
- const res = await post("/index/unwatch", { repo_id });
4026
- if (opts.json) {
4027
- output(res, true);
4028
- } else if (res.stopped) {
4029
- output(`File watcher stopped for ${name}`, false);
4030
- } else {
4031
- output(`${name} was not being watched`, false);
4032
- }
4033
- }
4034
- async function watchStatusCommand(opts) {
4035
- const { repo_id, name } = await ensureRepo();
4036
- const res = await get(`/index/watch-status/${repo_id}`);
4037
- if (opts.json) {
4038
- output(res, true);
4039
- } else if (res.watching) {
4040
- output(
4041
- `${name}: watching since ${res.started_at}
4042
- Changed: ${res.changed_files} files | Deleted: ${res.deleted_files} files`,
4043
- false
4044
- );
4045
- } else {
4046
- output(`${name}: not watching. Run \`lens repo watch\` to start.`, false);
4047
- }
4048
- }
4049
-
4050
- // packages/cli/src/commands/mcp.ts
4051
- async function mcpCommand() {
4052
- const { root_path } = await detectRepo();
4053
- const result = injectMcp(root_path);
4054
- if (result === "exists") {
4055
- output("LENS MCP entry already present in .mcp.json", false);
4056
- } else {
4057
- output("Wrote .mcp.json \u2014 agents will auto-discover LENS", false);
4058
- }
4059
- }
4060
-
4061
- // packages/cli/src/commands/config.ts
4062
- async function configGetCommand(key) {
4063
- try {
4064
- const value = await configGet(key);
4065
- output(value, false);
4066
- } catch (err) {
4067
- output(err instanceof Error ? err.message : String(err), false);
4068
- }
4069
- }
4070
- async function configSetCommand(key, value) {
4071
- try {
4072
- await configSet(key, value);
4073
- output(`Config updated: ${key} = ${value}`, false);
4074
- } catch (err) {
4075
- output(err instanceof Error ? err.message : String(err), false);
4076
- process.exit(1);
4077
- }
4078
- }
4079
-
4080
- // packages/cli/src/commands/daemon-ctrl.ts
4081
- import { existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync, openSync, mkdirSync } from "fs";
4082
- import { join as join2, dirname as dirname2 } from "path";
4083
- import { homedir } from "os";
4084
- import { spawn } from "child_process";
4085
- import { fileURLToPath } from "url";
4086
- import { createRequire } from "module";
4087
- var LENS_DIR = join2(homedir(), ".lens");
4088
- var PID_FILE = join2(LENS_DIR, "daemon.pid");
4089
- var LOG_FILE = join2(LENS_DIR, "daemon.log");
4090
- function isDaemonRunning() {
4091
- if (!existsSync3(PID_FILE)) return { running: false };
4092
- const pid = Number(readFileSync2(PID_FILE, "utf-8").trim());
4093
- try {
4094
- process.kill(pid, 0);
4095
- return { running: true, pid };
4096
- } catch {
4097
- unlinkSync(PID_FILE);
4098
- return { running: false };
4099
- }
4100
- }
4101
- async function startCommand() {
4102
- const status = isDaemonRunning();
4103
- if (status.running) {
4104
- output(`LENS daemon already running (pid: ${status.pid})`, false);
4105
- return;
4106
- }
4107
- const selfDir = dirname2(fileURLToPath(import.meta.url));
4108
- const sibling = join2(selfDir, "daemon.js");
4109
- let daemonScript;
4110
- if (existsSync3(sibling)) {
4111
- daemonScript = sibling;
4112
- } else {
4113
- try {
4114
- const require2 = createRequire(import.meta.url);
4115
- daemonScript = require2.resolve("@lens/daemon");
4116
- } catch {
4117
- error("Could not find @lens/daemon. Run `pnpm build` first.");
4118
- return;
4119
- }
4120
- }
4121
- mkdirSync(LENS_DIR, { recursive: true });
4122
- const logFd = openSync(LOG_FILE, "a");
4123
- const child = spawn(process.execPath, [daemonScript], {
4124
- detached: true,
4125
- stdio: ["ignore", logFd, logFd],
4126
- env: { ...process.env, LENS_DAEMON: "1" }
4127
- });
4128
- child.unref();
4129
- for (let i = 0; i < 20; i++) {
4130
- await new Promise((r) => setTimeout(r, 250));
4131
- if (isDaemonRunning().running) break;
4132
- }
4133
- const check = isDaemonRunning();
4134
- if (check.running) {
4135
- output(`LENS daemon started (pid: ${check.pid})`, false);
4136
- } else {
4137
- error(`LENS daemon failed to start. Check logs: ${LOG_FILE}`);
4138
- }
4139
- }
4140
- async function stopCommand() {
4141
- const status = isDaemonRunning();
4142
- if (!status.running) {
4143
- output("LENS daemon is not running", false);
4144
- return;
4145
- }
4146
- process.kill(status.pid, "SIGTERM");
4147
- for (let i = 0; i < 20; i++) {
4148
- await new Promise((r) => setTimeout(r, 250));
4149
- if (!isDaemonRunning().running) {
4150
- output(`LENS daemon stopped (pid: ${status.pid})`, false);
4151
- return;
4152
- }
4153
- }
4154
- error("Daemon did not stop within 5s. Force kill with: kill -9 " + status.pid);
4155
- }
4156
-
4157
- // packages/cli/src/commands/dashboard.ts
4158
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
4159
- import { join as join3 } from "path";
4160
- import { homedir as homedir2 } from "os";
4161
-
4162
- // packages/cli/src/util/browser.ts
4163
- import { exec } from "child_process";
4164
- function openBrowser(url) {
4165
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
4166
- exec(`${cmd} ${url}`);
4167
- }
4168
-
4169
- // packages/cli/src/commands/dashboard.ts
4170
- var PID_FILE2 = join3(homedir2(), ".lens", "daemon.pid");
4171
- function isDaemonRunning2() {
4172
- if (!existsSync4(PID_FILE2)) return false;
4173
- const pid = Number(readFileSync3(PID_FILE2, "utf-8").trim());
4174
- try {
4175
- process.kill(pid, 0);
4176
- return true;
4177
- } catch {
4178
- return false;
4080
+ const indexed = r.last_indexed_at ? timeAgo(r.last_indexed_at) : "never";
4081
+ lines.push(
4082
+ pad(id, 10) + pad(r.name, 20) + pad(r.index_status, 10) + pad(String(r.files_indexed), 8) + pad(r.chunk_count.toLocaleString(), 10) + pad(`${r.embedded_pct}%`, 8) + indexed
4083
+ );
4179
4084
  }
4085
+ output(lines.join("\n"), false);
4180
4086
  }
4181
- async function dashboardCommand() {
4182
- if (!isDaemonRunning2()) {
4183
- error("LENS daemon is not running. Start with: lens daemon start");
4184
- return;
4185
- }
4186
- const port = process.env.LENS_PORT || "4111";
4187
- const host = process.env.LENS_HOST || `http://127.0.0.1:${port}`;
4188
- const url = `${host}/dashboard`;
4189
- output(`Opening ${url}`, false);
4190
- openBrowser(url);
4087
+ function pad(s, width) {
4088
+ return s.length >= width ? `${s.slice(0, width - 1)} ` : s + " ".repeat(width - s.length);
4089
+ }
4090
+ function timeAgo(iso) {
4091
+ const diff = Date.now() - new Date(iso).getTime();
4092
+ const mins = Math.floor(diff / 6e4);
4093
+ if (mins < 1) return "just now";
4094
+ if (mins < 60) return `${mins}m ago`;
4095
+ const hours = Math.floor(mins / 60);
4096
+ if (hours < 24) return `${hours}h ago`;
4097
+ const days = Math.floor(hours / 24);
4098
+ return `${days}d ago`;
4191
4099
  }
4192
4100
 
4193
4101
  // packages/cli/src/commands/login.ts
4194
4102
  import http from "http";
4195
4103
 
4196
4104
  // packages/cli/src/util/auth.ts
4197
- import fs3 from "fs/promises";
4198
- import path3 from "path";
4105
+ import fs2 from "fs/promises";
4199
4106
  import os2 from "os";
4200
- var CONFIG_DIR2 = path3.join(os2.homedir(), ".lens");
4201
- var AUTH_FILE = path3.join(CONFIG_DIR2, "auth.json");
4107
+ import path2 from "path";
4108
+ var CONFIG_DIR2 = path2.join(os2.homedir(), ".lens");
4109
+ var AUTH_FILE = path2.join(CONFIG_DIR2, "auth.json");
4202
4110
  async function writeAuth(tokens) {
4203
- await fs3.mkdir(CONFIG_DIR2, { recursive: true });
4204
- await fs3.writeFile(AUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 384 });
4111
+ await fs2.mkdir(CONFIG_DIR2, { recursive: true });
4112
+ await fs2.writeFile(AUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 384 });
4205
4113
  }
4206
4114
  async function clearAuth() {
4207
4115
  try {
4208
- await fs3.unlink(AUTH_FILE);
4116
+ await fs2.unlink(AUTH_FILE);
4209
4117
  } catch {
4210
4118
  }
4211
4119
  }
@@ -4560,12 +4468,19 @@ async function loginCommand(opts) {
4560
4468
  }
4561
4469
  if (req.method === "POST" && url.pathname === "/token") {
4562
4470
  let body = "";
4563
- req.on("data", (c) => body += c);
4471
+ req.on("data", (c) => {
4472
+ body += c;
4473
+ });
4564
4474
  req.on("end", async () => {
4565
4475
  try {
4566
4476
  const { access_token, refresh_token, expires_in, user_email } = JSON.parse(body);
4567
4477
  const expires_at = Math.floor(Date.now() / 1e3) + Number(expires_in || 3600);
4568
- const tokens = { access_token, refresh_token, user_email, expires_at };
4478
+ const tokens = {
4479
+ access_token,
4480
+ refresh_token,
4481
+ user_email,
4482
+ expires_at
4483
+ };
4569
4484
  try {
4570
4485
  const keyRes = await fetch(`${CLOUD_API_URL}/auth/key`, {
4571
4486
  headers: { Authorization: `Bearer ${access_token}` }
@@ -4591,7 +4506,7 @@ async function loginCommand(opts) {
4591
4506
  cleanup();
4592
4507
  resolve2();
4593
4508
  setTimeout(() => process.exit(0), 100);
4594
- } catch (err) {
4509
+ } catch (_err) {
4595
4510
  res.writeHead(400);
4596
4511
  res.end("Bad request");
4597
4512
  }
@@ -4638,8 +4553,245 @@ async function logoutCommand() {
4638
4553
  output("Logged out of LENS cloud.", false);
4639
4554
  }
4640
4555
 
4556
+ // packages/cli/src/util/inject-mcp.ts
4557
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4558
+ import { join as join3 } from "path";
4559
+ var MCP_ENTRY = {
4560
+ command: "npx",
4561
+ args: ["lens-daemon", "--stdio"]
4562
+ };
4563
+ function injectMcp(repoRoot) {
4564
+ const mcpPath = join3(repoRoot, ".mcp.json");
4565
+ let existing = {};
4566
+ if (existsSync4(mcpPath)) {
4567
+ try {
4568
+ existing = JSON.parse(readFileSync3(mcpPath, "utf-8"));
4569
+ } catch {
4570
+ }
4571
+ }
4572
+ if (!existing.mcpServers) existing.mcpServers = {};
4573
+ const servers = existing.mcpServers;
4574
+ if (servers.lens) return "exists";
4575
+ const isNew = !existsSync4(mcpPath);
4576
+ servers.lens = MCP_ENTRY;
4577
+ writeFileSync2(mcpPath, `${JSON.stringify(existing, null, 2)}
4578
+ `);
4579
+ return isNew ? "created" : "updated";
4580
+ }
4581
+
4582
+ // packages/cli/src/commands/mcp.ts
4583
+ async function mcpCommand() {
4584
+ const { root_path } = await detectRepo();
4585
+ const result = injectMcp(root_path);
4586
+ if (result === "exists") {
4587
+ output("LENS MCP entry already present in .mcp.json", false);
4588
+ } else {
4589
+ output("Wrote .mcp.json \u2014 agents will auto-discover LENS", false);
4590
+ }
4591
+ }
4592
+
4593
+ // packages/cli/src/util/inject-claude-md.ts
4594
+ import fs3 from "fs/promises";
4595
+ import path3 from "path";
4596
+ var RLM_MARKER = "lens context";
4597
+ var CANDIDATE_PATHS = ["CLAUDE.md", "agents.md", "AGENTS.md", ".CLAUDE.md", ".agents.md", ".AGENTS.md"];
4598
+ async function findTargetFile(repoRoot) {
4599
+ for (const relPath of CANDIDATE_PATHS) {
4600
+ const fullPath = path3.join(repoRoot, relPath);
4601
+ try {
4602
+ await fs3.access(fullPath);
4603
+ return fullPath;
4604
+ } catch {
4605
+ }
4606
+ }
4607
+ return null;
4608
+ }
4609
+ async function injectClaudeMd(repoRoot) {
4610
+ const existingFile = await findTargetFile(repoRoot);
4611
+ const targetFile = existingFile ?? path3.join(repoRoot, "CLAUDE.md");
4612
+ let existingContent = "";
4613
+ try {
4614
+ existingContent = await fs3.readFile(targetFile, "utf-8");
4615
+ if (existingContent.toLowerCase().includes(RLM_MARKER)) {
4616
+ return;
4617
+ }
4618
+ } catch {
4619
+ }
4620
+ const config = await readConfig();
4621
+ if (existingFile && config.inject_behavior === "once") {
4622
+ return;
4623
+ }
4624
+ if (config.inject_behavior === "skip") {
4625
+ return;
4626
+ }
4627
+ const { content } = await get("/repo/template");
4628
+ if (existingContent) {
4629
+ await fs3.writeFile(targetFile, `${content}
4630
+
4631
+ ${existingContent}`);
4632
+ } else {
4633
+ await fs3.writeFile(targetFile, content);
4634
+ }
4635
+ }
4636
+
4637
+ // packages/cli/src/commands/register.ts
4638
+ async function registerCommand(opts) {
4639
+ const info = await detectRepo();
4640
+ const res = await post("/repo/register", {
4641
+ root_path: info.root_path,
4642
+ name: info.name,
4643
+ remote_url: info.remote_url
4644
+ });
4645
+ if (opts.json) {
4646
+ output(res, true);
4647
+ return;
4648
+ }
4649
+ const mcpResult = injectMcp(info.root_path);
4650
+ if (res.created) {
4651
+ await injectClaudeMd(info.root_path);
4652
+ output(`Registered ${res.name} (repo_id: ${res.repo_id})`, false);
4653
+ if (mcpResult === "created" || mcpResult === "updated") {
4654
+ output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
4655
+ }
4656
+ const config = await readConfig();
4657
+ if (config.show_progress) {
4658
+ await showProgress(res.repo_id, res.name);
4659
+ } else {
4660
+ output(`Indexing started. Run \`lens status\` to check progress.`, false);
4661
+ }
4662
+ } else {
4663
+ output(`Already registered ${res.name} (repo_id: ${res.repo_id})`, false);
4664
+ if (mcpResult === "created" || mcpResult === "updated") {
4665
+ output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
4666
+ }
4667
+ if (opts.inject) {
4668
+ await injectClaudeMd(info.root_path);
4669
+ output(`Injected LENS instructions into CLAUDE.md`, false);
4670
+ }
4671
+ }
4672
+ }
4673
+
4674
+ // packages/cli/src/commands/remove.ts
4675
+ async function removeCommand(opts) {
4676
+ const { repo_id, name } = await ensureRepo();
4677
+ if (!opts.yes) {
4678
+ process.stderr.write(`Remove "${name}" (${repo_id})? All index data will be deleted.
4679
+ `);
4680
+ process.stderr.write(`Re-run with --yes to confirm.
4681
+ `);
4682
+ process.exit(1);
4683
+ }
4684
+ const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4685
+ let i = 0;
4686
+ let phase = "Stopping watcher";
4687
+ const interval = setInterval(() => {
4688
+ const frame = spinner[i % spinner.length];
4689
+ process.stdout.write(`\r${frame} ${phase}...`);
4690
+ i++;
4691
+ }, 80);
4692
+ await new Promise((r) => setTimeout(r, 200));
4693
+ phase = `Removing ${name}`;
4694
+ const res = await request("DELETE", `/repo/${repo_id}`);
4695
+ clearInterval(interval);
4696
+ if (opts.json) {
4697
+ output(res, true);
4698
+ } else {
4699
+ process.stdout.write(`\r\u2713 Removed ${name}: ${res.chunks_removed} chunks deleted
4700
+ `);
4701
+ }
4702
+ }
4703
+
4704
+ // packages/cli/src/commands/status.ts
4705
+ var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
4706
+ var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
4707
+ var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
4708
+ var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
4709
+ async function statusCommand(opts) {
4710
+ const { repo_id, name } = await ensureRepo();
4711
+ const s = await get(`/repo/${repo_id}/status`);
4712
+ if (opts.json) {
4713
+ output(s, true);
4714
+ return;
4715
+ }
4716
+ const staleTag = s.is_stale ? yellow2(" STALE") : "";
4717
+ const check = green2("\u2713");
4718
+ const pending = dim2("\u25CB");
4719
+ const hasCaps = s.has_capabilities !== false;
4720
+ const lines = [
4721
+ ``,
4722
+ ` ${bold2(name)}${staleTag}`,
4723
+ dim2(` ${"\u2500".repeat(40)}`),
4724
+ ` ${s.chunk_count > 0 ? check : pending} Chunks ${dim2(s.chunk_count.toLocaleString())}`,
4725
+ ` ${s.metadata_count > 0 ? check : pending} Metadata ${dim2(`${s.metadata_count.toLocaleString()} files`)}`,
4726
+ ` ${s.git_commits_analyzed > 0 ? check : pending} Git history ${dim2(`${s.git_commits_analyzed.toLocaleString()} files`)}`,
4727
+ ` ${s.import_edge_count > 0 ? check : pending} Import graph ${dim2(`${s.import_edge_count.toLocaleString()} edges`)}`,
4728
+ ` ${s.cochange_pairs > 0 ? check : pending} Co-changes ${dim2(`${s.cochange_pairs.toLocaleString()} pairs`)}`
4729
+ ];
4730
+ if (hasCaps) {
4731
+ const warn = yellow2("\u26A0");
4732
+ const embDone = s.embedded_pct >= 100 || s.embedded_count >= s.embeddable_count && s.embeddable_count > 0;
4733
+ const embLabel = s.embedding_quota_exceeded ? `quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}` : s.embeddable_count > 0 ? `${s.embedded_count}/${s.embeddable_count} code chunks (${s.embedded_pct}%)` : "no code chunks";
4734
+ const embIcon = s.embedding_quota_exceeded ? warn : embDone ? check : pending;
4735
+ const vocabLabel = s.vocab_cluster_count > 0 ? `${s.vocab_cluster_count} clusters` : "...";
4736
+ const vocabIcon = s.vocab_cluster_count > 0 ? check : pending;
4737
+ const purDone = s.purpose_count > 0 && s.purpose_count >= s.purpose_total;
4738
+ const purposeLabel = s.purpose_quota_exceeded ? `quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}` : s.purpose_total > 0 ? `${s.purpose_count}/${s.purpose_total} files` : "no files";
4739
+ const purposeIcon = s.purpose_quota_exceeded ? warn : purDone ? check : pending;
4740
+ lines.push(` ${vocabIcon} Vocab clust. ${dim2(vocabLabel)}`);
4741
+ lines.push(` ${embIcon} Embeddings ${dim2(embLabel)}`);
4742
+ lines.push(` ${purposeIcon} Summaries ${dim2(purposeLabel)}`);
4743
+ } else {
4744
+ lines.push(``);
4745
+ lines.push(` ${yellow2("\u26A1")} ${bold2("Pro")}`);
4746
+ lines.push(` ${dim2("\xB7 Vocab clusters")}`);
4747
+ lines.push(` ${dim2("\xB7 Embeddings")}`);
4748
+ lines.push(` ${dim2("\xB7 Summaries")}`);
4749
+ lines.push(` ${dim2("lens login \u2192 upgrade to enable")}`);
4750
+ }
4751
+ output(lines.join("\n"), false);
4752
+ }
4753
+
4754
+ // packages/cli/src/commands/watch.ts
4755
+ async function watchCommand(opts) {
4756
+ const { repo_id, name } = await ensureRepo();
4757
+ const res = await post("/index/watch", { repo_id });
4758
+ if (opts.json) {
4759
+ output(res, true);
4760
+ } else if (res.already_watching) {
4761
+ output(`${name} is already being watched`, false);
4762
+ } else {
4763
+ output(`File watcher started for ${name}`, false);
4764
+ }
4765
+ }
4766
+ async function unwatchCommand(opts) {
4767
+ const { repo_id, name } = await ensureRepo();
4768
+ const res = await post("/index/unwatch", { repo_id });
4769
+ if (opts.json) {
4770
+ output(res, true);
4771
+ } else if (res.stopped) {
4772
+ output(`File watcher stopped for ${name}`, false);
4773
+ } else {
4774
+ output(`${name} was not being watched`, false);
4775
+ }
4776
+ }
4777
+ async function watchStatusCommand(opts) {
4778
+ const { repo_id, name } = await ensureRepo();
4779
+ const res = await get(`/index/watch-status/${repo_id}`);
4780
+ if (opts.json) {
4781
+ output(res, true);
4782
+ } else if (res.watching) {
4783
+ output(
4784
+ `${name}: watching since ${res.started_at}
4785
+ Changed: ${res.changed_files} files | Deleted: ${res.deleted_files} files`,
4786
+ false
4787
+ );
4788
+ } else {
4789
+ output(`${name}: not watching. Run \`lens repo watch\` to start.`, false);
4790
+ }
4791
+ }
4792
+
4641
4793
  // packages/cli/src/index.ts
4642
- var program2 = new Command().name("lens").description("LENS \u2014 Local-first repo context engine").version("0.1.18");
4794
+ var program2 = new Command().name("lens").description("LENS \u2014 Local-first repo context engine").version("0.1.20");
4643
4795
  function trackCommand(name) {
4644
4796
  if (!isTelemetryEnabled()) return;
4645
4797
  const BASE_URL2 = process.env.LENS_HOST ?? "http://127.0.0.1:4111";