lens-engine 0.1.19 → 0.1.21

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,409 @@ 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)}
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);
3839
+ }
3840
+
3841
+ // packages/cli/src/commands/eval.ts
3842
+ function pct(n) {
3843
+ return `${Math.round(n * 100)}%`;
3844
+ }
3845
+ function printTable(summary) {
3846
+ const w = process.stdout.write.bind(process.stdout);
3847
+ w("\n LENS Eval \u2014 Baseline Results\n");
3848
+ w(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n");
3849
+ w(" Overall\n");
3850
+ w(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
3851
+ w(` Queries: ${summary.total}
3588
3852
  `);
3589
- return isNew ? "created" : "updated";
3853
+ w(` Hit@1: ${pct(summary.hit_at_1)}
3854
+ `);
3855
+ w(` Hit@3: ${pct(summary.hit_at_3)}
3856
+ `);
3857
+ w(` Entry@1: ${pct(summary.entry_hit_at_1)}
3858
+ `);
3859
+ w(` Entry@3: ${pct(summary.entry_hit_at_3)}
3860
+ `);
3861
+ w(` Recall@5: ${pct(summary.avg_recall_at_5)}
3862
+ `);
3863
+ w(` Avg Duration: ${summary.avg_duration_ms}ms
3864
+
3865
+ `);
3866
+ if (summary.by_kind.length > 1) {
3867
+ w(" By Kind\n");
3868
+ w(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
3869
+ for (const k of summary.by_kind) {
3870
+ w(` ${k.kind} (n=${k.count})
3871
+ `);
3872
+ w(
3873
+ ` Hit@1: ${pct(k.hit_at_1)} Hit@3: ${pct(k.hit_at_3)} Recall@5: ${pct(k.avg_recall_at_5)} Avg: ${k.avg_duration_ms}ms
3874
+ `
3875
+ );
3876
+ }
3877
+ w("\n");
3878
+ }
3879
+ w(" Per Query\n");
3880
+ w(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
3881
+ for (const r of summary.results) {
3882
+ const mark = r.hit_at_3 ? "+" : "-";
3883
+ const entryMark = r.entry_hit_at_3 ? "+" : "-";
3884
+ w(` [${mark}] ${r.id} ${r.query.slice(0, 50).padEnd(50)} `);
3885
+ w(
3886
+ `H@1:${r.hit_at_1 ? "Y" : "N"} H@3:${r.hit_at_3 ? "Y" : "N"} E@3:${entryMark} R@5:${pct(r.recall_at_5).padStart(4)} ${r.duration_ms}ms
3887
+ `
3888
+ );
3889
+ if (!r.hit_at_3) {
3890
+ w(
3891
+ ` expected: ${r.expected_files.slice(0, 2).map((f) => f.split("/").pop()).join(", ")}
3892
+ `
3893
+ );
3894
+ w(
3895
+ ` got top3: ${r.returned_files.slice(0, 3).map((f) => f.split("/").pop()).join(", ")}
3896
+ `
3897
+ );
3898
+ }
3899
+ }
3900
+ w("\n");
3901
+ }
3902
+ async function evalCommand(opts) {
3903
+ const { repo_id } = await ensureRepo();
3904
+ const body = { repo_id };
3905
+ if (opts.kind) body.filter_kind = opts.kind;
3906
+ const summary = await post("/eval/run", body);
3907
+ if (opts.json) {
3908
+ output(summary, true);
3909
+ } else {
3910
+ printTable(summary);
3911
+ }
3590
3912
  }
3591
3913
 
3592
3914
  // packages/cli/src/util/progress.ts
@@ -3618,7 +3940,7 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3618
3940
  if (connFailures >= MAX_CONN_FAILURES) {
3619
3941
  process.stdout.write(
3620
3942
  `
3621
- ${red("\u2717")} ${bold("Daemon unavailable")} \u2014 ${dim("connection lost after " + connFailures + " retries")}
3943
+ ${red("\u2717")} ${bold("Daemon unavailable")} \u2014 ${dim(`connection lost after ${connFailures} retries`)}
3622
3944
  `
3623
3945
  );
3624
3946
  process.stdout.write(` ${dim("Restart with: lens start")}
@@ -3693,7 +4015,9 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3693
4015
  if (embDone) {
3694
4016
  lines.push(` ${green("\u2713")} Embeddings ${dim(`${s.embedded_count}/${s.embeddable_count} code chunks`)}`);
3695
4017
  } else if (s.embedding_quota_exceeded) {
3696
- lines.push(` ${yellow("\u26A0")} Embeddings ${dim(`quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}`)}`);
4018
+ lines.push(
4019
+ ` ${yellow("\u26A0")} Embeddings ${dim(`quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}`)}`
4020
+ );
3697
4021
  } else if (s.embeddable_count === 0) {
3698
4022
  lines.push(` ${dim("\u25CB")} Embeddings ${dim("no code chunks")}`);
3699
4023
  } else {
@@ -3705,7 +4029,9 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3705
4029
  if (sumDone) {
3706
4030
  lines.push(` ${green("\u2713")} Summaries ${dim(`${s.purpose_count}/${s.purpose_total} files`)}`);
3707
4031
  } else if (s.purpose_quota_exceeded) {
3708
- lines.push(` ${yellow("\u26A0")} Summaries ${dim(`quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}`)}`);
4032
+ lines.push(
4033
+ ` ${yellow("\u26A0")} Summaries ${dim(`quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}`)}`
4034
+ );
3709
4035
  } else if (s.purpose_total > 0) {
3710
4036
  lines.push(
3711
4037
  ` ${cyan(f)} Summaries ${createBar(Math.round(s.purpose_count / s.purpose_total * 100), 20)} ${dim(`${s.purpose_count}/${s.purpose_total}`)}`
@@ -3754,7 +4080,8 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3754
4080
  if (lastRendered) {
3755
4081
  process.stdout.write(`\x1B[${TOTAL_LINES}A\x1B[J`);
3756
4082
  }
3757
- process.stdout.write(output2 + "\n");
4083
+ process.stdout.write(`${output2}
4084
+ `);
3758
4085
  lastRendered = output2;
3759
4086
  if (allDone) {
3760
4087
  done = true;
@@ -3764,8 +4091,8 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
3764
4091
  await sleep2(RENDER_INTERVAL);
3765
4092
  }
3766
4093
  }
3767
- function createBar(pct, width) {
3768
- const clamped = Math.min(pct, 100);
4094
+ function createBar(pct2, width) {
4095
+ const clamped = Math.min(pct2, 100);
3769
4096
  const filled = Math.round(clamped / 100 * width);
3770
4097
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
3771
4098
  return `\x1B[32m${bar}\x1B[0m`;
@@ -3774,65 +4101,6 @@ function sleep2(ms) {
3774
4101
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3775
4102
  }
3776
4103
 
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
4104
  // packages/cli/src/commands/index.ts
3837
4105
  async function indexCommand(opts) {
3838
4106
  const { repo_id, name } = await ensureRepo();
@@ -3866,56 +4134,6 @@ async function indexCommand(opts) {
3866
4134
  await showProgress(repo_id, name);
3867
4135
  }
3868
4136
 
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
4137
  // packages/cli/src/commands/list.ts
3920
4138
  async function listCommand(opts) {
3921
4139
  const res = await get("/repo/list/detailed");
@@ -3932,280 +4150,43 @@ async function listCommand(opts) {
3932
4150
  const lines = [header, sep];
3933
4151
  for (const r of res.repos) {
3934
4152
  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;
4153
+ const indexed = r.last_indexed_at ? timeAgo(r.last_indexed_at) : "never";
4154
+ lines.push(
4155
+ 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
4156
+ );
4179
4157
  }
4158
+ output(lines.join("\n"), false);
4180
4159
  }
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);
4160
+ function pad(s, width) {
4161
+ return s.length >= width ? `${s.slice(0, width - 1)} ` : s + " ".repeat(width - s.length);
4162
+ }
4163
+ function timeAgo(iso) {
4164
+ const diff = Date.now() - new Date(iso).getTime();
4165
+ const mins = Math.floor(diff / 6e4);
4166
+ if (mins < 1) return "just now";
4167
+ if (mins < 60) return `${mins}m ago`;
4168
+ const hours = Math.floor(mins / 60);
4169
+ if (hours < 24) return `${hours}h ago`;
4170
+ const days = Math.floor(hours / 24);
4171
+ return `${days}d ago`;
4191
4172
  }
4192
4173
 
4193
4174
  // packages/cli/src/commands/login.ts
4194
4175
  import http from "http";
4195
4176
 
4196
4177
  // packages/cli/src/util/auth.ts
4197
- import fs3 from "fs/promises";
4198
- import path3 from "path";
4178
+ import fs2 from "fs/promises";
4199
4179
  import os2 from "os";
4200
- var CONFIG_DIR2 = path3.join(os2.homedir(), ".lens");
4201
- var AUTH_FILE = path3.join(CONFIG_DIR2, "auth.json");
4180
+ import path2 from "path";
4181
+ var CONFIG_DIR2 = path2.join(os2.homedir(), ".lens");
4182
+ var AUTH_FILE = path2.join(CONFIG_DIR2, "auth.json");
4202
4183
  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 });
4184
+ await fs2.mkdir(CONFIG_DIR2, { recursive: true });
4185
+ await fs2.writeFile(AUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 384 });
4205
4186
  }
4206
4187
  async function clearAuth() {
4207
4188
  try {
4208
- await fs3.unlink(AUTH_FILE);
4189
+ await fs2.unlink(AUTH_FILE);
4209
4190
  } catch {
4210
4191
  }
4211
4192
  }
@@ -4560,12 +4541,19 @@ async function loginCommand(opts) {
4560
4541
  }
4561
4542
  if (req.method === "POST" && url.pathname === "/token") {
4562
4543
  let body = "";
4563
- req.on("data", (c) => body += c);
4544
+ req.on("data", (c) => {
4545
+ body += c;
4546
+ });
4564
4547
  req.on("end", async () => {
4565
4548
  try {
4566
4549
  const { access_token, refresh_token, expires_in, user_email } = JSON.parse(body);
4567
4550
  const expires_at = Math.floor(Date.now() / 1e3) + Number(expires_in || 3600);
4568
- const tokens = { access_token, refresh_token, user_email, expires_at };
4551
+ const tokens = {
4552
+ access_token,
4553
+ refresh_token,
4554
+ user_email,
4555
+ expires_at
4556
+ };
4569
4557
  try {
4570
4558
  const keyRes = await fetch(`${CLOUD_API_URL}/auth/key`, {
4571
4559
  headers: { Authorization: `Bearer ${access_token}` }
@@ -4591,7 +4579,7 @@ async function loginCommand(opts) {
4591
4579
  cleanup();
4592
4580
  resolve2();
4593
4581
  setTimeout(() => process.exit(0), 100);
4594
- } catch (err) {
4582
+ } catch (_err) {
4595
4583
  res.writeHead(400);
4596
4584
  res.end("Bad request");
4597
4585
  }
@@ -4638,8 +4626,245 @@ async function logoutCommand() {
4638
4626
  output("Logged out of LENS cloud.", false);
4639
4627
  }
4640
4628
 
4629
+ // packages/cli/src/util/inject-mcp.ts
4630
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4631
+ import { join as join3 } from "path";
4632
+ var MCP_ENTRY = {
4633
+ command: "npx",
4634
+ args: ["lens-daemon", "--stdio"]
4635
+ };
4636
+ function injectMcp(repoRoot) {
4637
+ const mcpPath = join3(repoRoot, ".mcp.json");
4638
+ let existing = {};
4639
+ if (existsSync4(mcpPath)) {
4640
+ try {
4641
+ existing = JSON.parse(readFileSync3(mcpPath, "utf-8"));
4642
+ } catch {
4643
+ }
4644
+ }
4645
+ if (!existing.mcpServers) existing.mcpServers = {};
4646
+ const servers = existing.mcpServers;
4647
+ if (servers.lens) return "exists";
4648
+ const isNew = !existsSync4(mcpPath);
4649
+ servers.lens = MCP_ENTRY;
4650
+ writeFileSync2(mcpPath, `${JSON.stringify(existing, null, 2)}
4651
+ `);
4652
+ return isNew ? "created" : "updated";
4653
+ }
4654
+
4655
+ // packages/cli/src/commands/mcp.ts
4656
+ async function mcpCommand() {
4657
+ const { root_path } = await detectRepo();
4658
+ const result = injectMcp(root_path);
4659
+ if (result === "exists") {
4660
+ output("LENS MCP entry already present in .mcp.json", false);
4661
+ } else {
4662
+ output("Wrote .mcp.json \u2014 agents will auto-discover LENS", false);
4663
+ }
4664
+ }
4665
+
4666
+ // packages/cli/src/util/inject-claude-md.ts
4667
+ import fs3 from "fs/promises";
4668
+ import path3 from "path";
4669
+ var RLM_MARKER = "lens context";
4670
+ var CANDIDATE_PATHS = ["CLAUDE.md", "agents.md", "AGENTS.md", ".CLAUDE.md", ".agents.md", ".AGENTS.md"];
4671
+ async function findTargetFile(repoRoot) {
4672
+ for (const relPath of CANDIDATE_PATHS) {
4673
+ const fullPath = path3.join(repoRoot, relPath);
4674
+ try {
4675
+ await fs3.access(fullPath);
4676
+ return fullPath;
4677
+ } catch {
4678
+ }
4679
+ }
4680
+ return null;
4681
+ }
4682
+ async function injectClaudeMd(repoRoot) {
4683
+ const existingFile = await findTargetFile(repoRoot);
4684
+ const targetFile = existingFile ?? path3.join(repoRoot, "CLAUDE.md");
4685
+ let existingContent = "";
4686
+ try {
4687
+ existingContent = await fs3.readFile(targetFile, "utf-8");
4688
+ if (existingContent.toLowerCase().includes(RLM_MARKER)) {
4689
+ return;
4690
+ }
4691
+ } catch {
4692
+ }
4693
+ const config = await readConfig();
4694
+ if (existingFile && config.inject_behavior === "once") {
4695
+ return;
4696
+ }
4697
+ if (config.inject_behavior === "skip") {
4698
+ return;
4699
+ }
4700
+ const { content } = await get("/repo/template");
4701
+ if (existingContent) {
4702
+ await fs3.writeFile(targetFile, `${content}
4703
+
4704
+ ${existingContent}`);
4705
+ } else {
4706
+ await fs3.writeFile(targetFile, content);
4707
+ }
4708
+ }
4709
+
4710
+ // packages/cli/src/commands/register.ts
4711
+ async function registerCommand(opts) {
4712
+ const info = await detectRepo();
4713
+ const res = await post("/repo/register", {
4714
+ root_path: info.root_path,
4715
+ name: info.name,
4716
+ remote_url: info.remote_url
4717
+ });
4718
+ if (opts.json) {
4719
+ output(res, true);
4720
+ return;
4721
+ }
4722
+ const mcpResult = injectMcp(info.root_path);
4723
+ if (res.created) {
4724
+ await injectClaudeMd(info.root_path);
4725
+ output(`Registered ${res.name} (repo_id: ${res.repo_id})`, false);
4726
+ if (mcpResult === "created" || mcpResult === "updated") {
4727
+ output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
4728
+ }
4729
+ const config = await readConfig();
4730
+ if (config.show_progress) {
4731
+ await showProgress(res.repo_id, res.name);
4732
+ } else {
4733
+ output(`Indexing started. Run \`lens status\` to check progress.`, false);
4734
+ }
4735
+ } else {
4736
+ output(`Already registered ${res.name} (repo_id: ${res.repo_id})`, false);
4737
+ if (mcpResult === "created" || mcpResult === "updated") {
4738
+ output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
4739
+ }
4740
+ if (opts.inject) {
4741
+ await injectClaudeMd(info.root_path);
4742
+ output(`Injected LENS instructions into CLAUDE.md`, false);
4743
+ }
4744
+ }
4745
+ }
4746
+
4747
+ // packages/cli/src/commands/remove.ts
4748
+ async function removeCommand(opts) {
4749
+ const { repo_id, name } = await ensureRepo();
4750
+ if (!opts.yes) {
4751
+ process.stderr.write(`Remove "${name}" (${repo_id})? All index data will be deleted.
4752
+ `);
4753
+ process.stderr.write(`Re-run with --yes to confirm.
4754
+ `);
4755
+ process.exit(1);
4756
+ }
4757
+ const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4758
+ let i = 0;
4759
+ let phase = "Stopping watcher";
4760
+ const interval = setInterval(() => {
4761
+ const frame = spinner[i % spinner.length];
4762
+ process.stdout.write(`\r${frame} ${phase}...`);
4763
+ i++;
4764
+ }, 80);
4765
+ await new Promise((r) => setTimeout(r, 200));
4766
+ phase = `Removing ${name}`;
4767
+ const res = await request("DELETE", `/repo/${repo_id}`);
4768
+ clearInterval(interval);
4769
+ if (opts.json) {
4770
+ output(res, true);
4771
+ } else {
4772
+ process.stdout.write(`\r\u2713 Removed ${name}: ${res.chunks_removed} chunks deleted
4773
+ `);
4774
+ }
4775
+ }
4776
+
4777
+ // packages/cli/src/commands/status.ts
4778
+ var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
4779
+ var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
4780
+ var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
4781
+ var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
4782
+ async function statusCommand(opts) {
4783
+ const { repo_id, name } = await ensureRepo();
4784
+ const s = await get(`/repo/${repo_id}/status`);
4785
+ if (opts.json) {
4786
+ output(s, true);
4787
+ return;
4788
+ }
4789
+ const staleTag = s.is_stale ? yellow2(" STALE") : "";
4790
+ const check = green2("\u2713");
4791
+ const pending = dim2("\u25CB");
4792
+ const hasCaps = s.has_capabilities !== false;
4793
+ const lines = [
4794
+ ``,
4795
+ ` ${bold2(name)}${staleTag}`,
4796
+ dim2(` ${"\u2500".repeat(40)}`),
4797
+ ` ${s.chunk_count > 0 ? check : pending} Chunks ${dim2(s.chunk_count.toLocaleString())}`,
4798
+ ` ${s.metadata_count > 0 ? check : pending} Metadata ${dim2(`${s.metadata_count.toLocaleString()} files`)}`,
4799
+ ` ${s.git_commits_analyzed > 0 ? check : pending} Git history ${dim2(`${s.git_commits_analyzed.toLocaleString()} files`)}`,
4800
+ ` ${s.import_edge_count > 0 ? check : pending} Import graph ${dim2(`${s.import_edge_count.toLocaleString()} edges`)}`,
4801
+ ` ${s.cochange_pairs > 0 ? check : pending} Co-changes ${dim2(`${s.cochange_pairs.toLocaleString()} pairs`)}`
4802
+ ];
4803
+ if (hasCaps) {
4804
+ const warn = yellow2("\u26A0");
4805
+ const embDone = s.embedded_pct >= 100 || s.embedded_count >= s.embeddable_count && s.embeddable_count > 0;
4806
+ 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";
4807
+ const embIcon = s.embedding_quota_exceeded ? warn : embDone ? check : pending;
4808
+ const vocabLabel = s.vocab_cluster_count > 0 ? `${s.vocab_cluster_count} clusters` : "...";
4809
+ const vocabIcon = s.vocab_cluster_count > 0 ? check : pending;
4810
+ const purDone = s.purpose_count > 0 && s.purpose_count >= s.purpose_total;
4811
+ 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";
4812
+ const purposeIcon = s.purpose_quota_exceeded ? warn : purDone ? check : pending;
4813
+ lines.push(` ${vocabIcon} Vocab clust. ${dim2(vocabLabel)}`);
4814
+ lines.push(` ${embIcon} Embeddings ${dim2(embLabel)}`);
4815
+ lines.push(` ${purposeIcon} Summaries ${dim2(purposeLabel)}`);
4816
+ } else {
4817
+ lines.push(``);
4818
+ lines.push(` ${yellow2("\u26A1")} ${bold2("Pro")}`);
4819
+ lines.push(` ${dim2("\xB7 Vocab clusters")}`);
4820
+ lines.push(` ${dim2("\xB7 Embeddings")}`);
4821
+ lines.push(` ${dim2("\xB7 Summaries")}`);
4822
+ lines.push(` ${dim2("lens login \u2192 upgrade to enable")}`);
4823
+ }
4824
+ output(lines.join("\n"), false);
4825
+ }
4826
+
4827
+ // packages/cli/src/commands/watch.ts
4828
+ async function watchCommand(opts) {
4829
+ const { repo_id, name } = await ensureRepo();
4830
+ const res = await post("/index/watch", { repo_id });
4831
+ if (opts.json) {
4832
+ output(res, true);
4833
+ } else if (res.already_watching) {
4834
+ output(`${name} is already being watched`, false);
4835
+ } else {
4836
+ output(`File watcher started for ${name}`, false);
4837
+ }
4838
+ }
4839
+ async function unwatchCommand(opts) {
4840
+ const { repo_id, name } = await ensureRepo();
4841
+ const res = await post("/index/unwatch", { repo_id });
4842
+ if (opts.json) {
4843
+ output(res, true);
4844
+ } else if (res.stopped) {
4845
+ output(`File watcher stopped for ${name}`, false);
4846
+ } else {
4847
+ output(`${name} was not being watched`, false);
4848
+ }
4849
+ }
4850
+ async function watchStatusCommand(opts) {
4851
+ const { repo_id, name } = await ensureRepo();
4852
+ const res = await get(`/index/watch-status/${repo_id}`);
4853
+ if (opts.json) {
4854
+ output(res, true);
4855
+ } else if (res.watching) {
4856
+ output(
4857
+ `${name}: watching since ${res.started_at}
4858
+ Changed: ${res.changed_files} files | Deleted: ${res.deleted_files} files`,
4859
+ false
4860
+ );
4861
+ } else {
4862
+ output(`${name}: not watching. Run \`lens repo watch\` to start.`, false);
4863
+ }
4864
+ }
4865
+
4641
4866
  // packages/cli/src/index.ts
4642
- var program2 = new Command().name("lens").description("LENS \u2014 Local-first repo context engine").version("0.1.19");
4867
+ var program2 = new Command().name("lens").description("LENS \u2014 Local-first repo context engine").version("0.1.20");
4643
4868
  function trackCommand(name) {
4644
4869
  if (!isTelemetryEnabled()) return;
4645
4870
  const BASE_URL2 = process.env.LENS_HOST ?? "http://127.0.0.1:4111";
@@ -4661,6 +4886,7 @@ repo.command("watch-status").description("Show file watcher status for current r
4661
4886
  repo.command("mcp").description("Write .mcp.json for MCP agent integration").action(() => run(() => mcpCommand()));
4662
4887
  program2.command("context <goal>").description("Build an intelligent context pack for a goal").option("--json", "Output as JSON", false).action((goal, opts) => run(() => contextCommand(goal, opts), "context"));
4663
4888
  program2.command("index").description("Index the current repo").option("--json", "Output as JSON", false).option("--force", "Full re-scan (default: diff scan, changed files only)", false).option("--status", "Show index status", false).action((opts) => run(() => indexCommand(opts), "index"));
4889
+ program2.command("eval").description("Run evaluation harness against current repo").option("--json", "Output as JSON", false).option("--kind <kind>", "Filter by query kind (natural, symbol, error_message)").action((opts) => run(() => evalCommand(opts), "eval"));
4664
4890
  program2.command("status").description("Show repo index/embedding status").option("--json", "Output as JSON", false).action((opts) => run(() => statusCommand(opts), "status"));
4665
4891
  var daemon = program2.command("daemon").description("Daemon management");
4666
4892
  daemon.command("stats").description("Show global daemon statistics").option("--json", "Output as JSON", false).action((opts) => run(() => daemonStatsCommand(opts)));