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 +747 -521
- package/daemon.js +2329 -1182
- package/dashboard/assets/index-DmLHTAhP.css +1 -0
- package/dashboard/assets/index-ojhLsux_.js +341 -0
- package/dashboard/index.html +2 -2
- package/package.json +17 -3
- package/dashboard/assets/index-B-FwfnUH.css +0 -1
- package/dashboard/assets/index-FwosDwb9.js +0 -341
package/cli.js
CHANGED
|
@@ -3347,36 +3347,108 @@ var {
|
|
|
3347
3347
|
Help
|
|
3348
3348
|
} = import_index.default;
|
|
3349
3349
|
|
|
3350
|
-
// packages/cli/src/util/
|
|
3351
|
-
import {
|
|
3352
|
-
import
|
|
3353
|
-
import
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
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
|
|
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
|
|
3366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3373
|
-
const
|
|
3374
|
-
|
|
3375
|
-
|
|
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/
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
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
|
|
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
|
|
3471
|
-
|
|
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
|
|
3528
|
+
return void 0;
|
|
3474
3529
|
}
|
|
3475
3530
|
}
|
|
3476
|
-
async function
|
|
3477
|
-
|
|
3478
|
-
|
|
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
|
-
|
|
3536
|
+
const remote_url = await parseRemoteUrl(root);
|
|
3537
|
+
const name = basename(root);
|
|
3538
|
+
return { root_path: root, name, remote_url };
|
|
3486
3539
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
}
|
|
3496
|
-
|
|
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
|
-
|
|
3559
|
+
output(res.context_pack, false);
|
|
3499
3560
|
}
|
|
3500
|
-
await writeConfig(config);
|
|
3501
3561
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
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
|
-
|
|
3506
|
-
|
|
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
|
|
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
|
-
|
|
3629
|
+
execSync(`launchctl load "${PLIST_PATH}" 2>/dev/null`);
|
|
3514
3630
|
} catch {
|
|
3515
|
-
return DEFAULTS;
|
|
3516
3631
|
}
|
|
3517
3632
|
}
|
|
3518
|
-
function
|
|
3519
|
-
|
|
3520
|
-
|
|
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
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3550
|
-
|
|
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
|
-
|
|
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
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
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
|
-
|
|
3759
|
+
error(`LENS daemon failed to start. Check logs: ${LOG_FILE}`);
|
|
3563
3760
|
}
|
|
3564
3761
|
}
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
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
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
3768
|
-
const clamped = Math.min(
|
|
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
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
const
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
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
|
|
4198
|
-
import path3 from "path";
|
|
4178
|
+
import fs2 from "fs/promises";
|
|
4199
4179
|
import os2 from "os";
|
|
4200
|
-
|
|
4201
|
-
var
|
|
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
|
|
4204
|
-
await
|
|
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
|
|
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) =>
|
|
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 = {
|
|
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 (
|
|
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.
|
|
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)));
|