@locusai/cli 0.23.4 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/locus.js +312 -75
  2. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -11282,6 +11282,9 @@ var exports_status = {};
11282
11282
  __export(exports_status, {
11283
11283
  statusCommand: () => statusCommand
11284
11284
  });
11285
+ import { execSync as execSync18 } from "node:child_process";
11286
+ import { existsSync as existsSync22 } from "node:fs";
11287
+ import { dirname as dirname7, join as join22 } from "node:path";
11285
11288
  async function statusCommand(projectRoot) {
11286
11289
  const config = loadConfig(projectRoot);
11287
11290
  const spinner = new Spinner;
@@ -11362,12 +11365,245 @@ async function statusCommand(projectRoot) {
11362
11365
  }
11363
11366
  }
11364
11367
  } catch {}
11368
+ const agents = getActiveAgents(config.sandbox);
11369
+ if (agents.length > 0) {
11370
+ lines.push("");
11371
+ lines.push(` ${bold2("Agents:")}`);
11372
+ for (const agent of agents) {
11373
+ const icon = agent.status === "running" ? green("●") : dim2("○");
11374
+ const details = [agent.provider];
11375
+ if (agent.sandbox)
11376
+ details.push(dim2(`sandbox:${agent.sandbox}`));
11377
+ if (agent.pid)
11378
+ details.push(dim2(`pid:${agent.pid}`));
11379
+ if (agent.uptime)
11380
+ details.push(dim2(agent.uptime));
11381
+ if (agent.memory)
11382
+ details.push(dim2(agent.memory));
11383
+ lines.push(` ${icon} ${cyan2(agent.provider)} ${details.slice(1).join(" ")}`);
11384
+ if (agent.sandboxProcesses && agent.sandboxProcesses.length > 0) {
11385
+ for (const proc of agent.sandboxProcesses) {
11386
+ lines.push(` ${dim2("└")} ${yellow2(proc.name)} ${dim2(`pid:${proc.pid}`)}`);
11387
+ }
11388
+ } else if (agent.sandbox && agent.status === "running") {
11389
+ lines.push(` ${dim2("└ no agent processes detected")}`);
11390
+ }
11391
+ }
11392
+ }
11393
+ const registry = loadRegistry();
11394
+ const entries = Object.values(registry.packages);
11395
+ if (entries.length > 0) {
11396
+ const pm2Processes = getPm2Processes();
11397
+ lines.push("");
11398
+ lines.push(` ${bold2("Packages:")}`);
11399
+ for (const entry of entries) {
11400
+ const shortName = extractShortName(entry.name);
11401
+ const processName = `locus-${shortName}`;
11402
+ const proc = pm2Processes.find((p) => p.name === processName);
11403
+ const statusStr = proc ? proc.status === "online" ? green("online") : proc.status === "stopped" ? dim2("stopped") : yellow2(proc.status) : dim2("not running");
11404
+ const details = [`v${entry.version}`, statusStr];
11405
+ if (proc?.pid) {
11406
+ details.push(dim2(`pid:${proc.pid}`));
11407
+ }
11408
+ if (proc?.uptime) {
11409
+ details.push(dim2(formatUptime(proc.uptime)));
11410
+ }
11411
+ if (proc?.memory) {
11412
+ details.push(dim2(`${(proc.memory / (1024 * 1024)).toFixed(0)}MB`));
11413
+ }
11414
+ lines.push(` ${proc?.status === "online" ? green("●") : dim2("○")} ${cyan2(shortName)} ${details.join(" ")}`);
11415
+ }
11416
+ }
11365
11417
  spinner.stop();
11366
11418
  lines.push("");
11367
11419
  process.stderr.write(`
11368
11420
  ${drawBox(lines, { title: "Locus Status" })}
11369
11421
  `);
11370
11422
  }
11423
+ function getPm2Bin() {
11424
+ const pkgsBin = join22(getPackagesDir(), "node_modules", ".bin", "pm2");
11425
+ if (existsSync22(pkgsBin))
11426
+ return pkgsBin;
11427
+ let dir = process.cwd();
11428
+ while (dir !== dirname7(dir)) {
11429
+ const candidate = join22(dir, "node_modules", ".bin", "pm2");
11430
+ if (existsSync22(candidate))
11431
+ return candidate;
11432
+ dir = dirname7(dir);
11433
+ }
11434
+ try {
11435
+ const result = execSync18("which pm2", {
11436
+ encoding: "utf-8",
11437
+ stdio: ["pipe", "pipe", "pipe"]
11438
+ }).trim();
11439
+ if (result)
11440
+ return result;
11441
+ } catch {}
11442
+ return "npx pm2";
11443
+ }
11444
+ function getPm2Processes() {
11445
+ try {
11446
+ const pm2 = getPm2Bin();
11447
+ const output = execSync18(`${pm2} jlist`, {
11448
+ encoding: "utf-8",
11449
+ stdio: ["pipe", "pipe", "pipe"],
11450
+ timeout: 5000
11451
+ });
11452
+ const processes = JSON.parse(output);
11453
+ return processes.map((p) => ({
11454
+ name: p.name,
11455
+ status: p.pm2_env?.status ?? "unknown",
11456
+ pid: p.pid ?? null,
11457
+ uptime: p.pm2_env?.pm_uptime ?? null,
11458
+ memory: p.monit?.memory ?? null
11459
+ }));
11460
+ } catch {
11461
+ return [];
11462
+ }
11463
+ }
11464
+ function getActiveAgents(sandboxConfig) {
11465
+ const agents = [];
11466
+ if (sandboxConfig.enabled) {
11467
+ const sandboxes = getSandboxProcesses();
11468
+ for (const [provider, name] of Object.entries(sandboxConfig.providers)) {
11469
+ if (!name)
11470
+ continue;
11471
+ const sb = sandboxes.find((s) => s.name === name);
11472
+ const isRunning = !!sb && sb.status === "running";
11473
+ let sandboxProcesses;
11474
+ if (isRunning) {
11475
+ sandboxProcesses = getProcessesInsideSandbox(name);
11476
+ }
11477
+ agents.push({
11478
+ provider,
11479
+ status: isRunning ? "running" : "stopped",
11480
+ sandbox: name,
11481
+ sandboxProcesses
11482
+ });
11483
+ }
11484
+ } else {
11485
+ const procs = getAgentProcesses();
11486
+ agents.push(...procs);
11487
+ }
11488
+ return agents;
11489
+ }
11490
+ function getSandboxProcesses() {
11491
+ try {
11492
+ const output = execSync18("docker sandbox ls", {
11493
+ encoding: "utf-8",
11494
+ stdio: ["pipe", "pipe", "pipe"],
11495
+ timeout: 5000
11496
+ });
11497
+ const lines = output.trim().split(`
11498
+ `);
11499
+ if (lines.length < 2)
11500
+ return [];
11501
+ const header = lines[0];
11502
+ const statusIdx = header.search(/\bSTATUS\b/i);
11503
+ return lines.slice(1).map((line) => {
11504
+ const parts = line.trim().split(/\s+/);
11505
+ const name = parts[0] ?? "";
11506
+ let status = "";
11507
+ if (statusIdx >= 0 && line.length > statusIdx) {
11508
+ const rest = line.substring(statusIdx).trim();
11509
+ status = (rest.split(/\s+/)[0] ?? "").toLowerCase();
11510
+ } else {
11511
+ status = (parts[parts.length - 2] ?? parts[1] ?? "").toLowerCase();
11512
+ }
11513
+ return { name, status };
11514
+ }).filter((s) => s.name.length > 0);
11515
+ } catch {
11516
+ return [];
11517
+ }
11518
+ }
11519
+ function getProcessesInsideSandbox(sandboxName) {
11520
+ try {
11521
+ const output = execSync18(`docker sandbox exec ${sandboxName} sh -c "ps -eo pid,pcpu,pmem,args"`, {
11522
+ encoding: "utf-8",
11523
+ stdio: ["pipe", "pipe", "pipe"],
11524
+ timeout: 1e4
11525
+ });
11526
+ const lines = output.trim().split(`
11527
+ `).slice(1);
11528
+ const processes = [];
11529
+ for (const line of lines) {
11530
+ const lower = line.toLowerCase();
11531
+ let agentName;
11532
+ if (lower.includes("claude"))
11533
+ agentName = "claude";
11534
+ else if (lower.includes("codex"))
11535
+ agentName = "codex";
11536
+ if (!agentName)
11537
+ continue;
11538
+ const pidMatch = line.trim().match(/^(\d+)/);
11539
+ if (!pidMatch)
11540
+ continue;
11541
+ processes.push({
11542
+ name: agentName,
11543
+ pid: pidMatch[1],
11544
+ command: ""
11545
+ });
11546
+ }
11547
+ return processes;
11548
+ } catch {
11549
+ return [];
11550
+ }
11551
+ }
11552
+ function getAgentProcesses() {
11553
+ const agents = [];
11554
+ try {
11555
+ const output = execSync18("ps -eo pid,pcpu,pmem,args", {
11556
+ encoding: "utf-8",
11557
+ stdio: ["pipe", "pipe", "pipe"],
11558
+ timeout: 5000
11559
+ });
11560
+ const lines = output.trim().split(`
11561
+ `).slice(1);
11562
+ const providerMatches = {};
11563
+ for (const line of lines) {
11564
+ const trimmed = line.trim();
11565
+ if (!trimmed)
11566
+ continue;
11567
+ const match = trimmed.match(/^(\d+)\s+([\d.]+)\s+([\d.]+)\s+(.+)$/);
11568
+ if (!match)
11569
+ continue;
11570
+ const [, pid, cpu, mem, command] = match;
11571
+ const cmdLower = command.toLowerCase();
11572
+ if (cmdLower.includes("ps -eo") || cmdLower.includes("grep") || cmdLower.includes("locus status") || cmdLower.includes("locus-cli"))
11573
+ continue;
11574
+ for (const provider of ["claude", "codex"]) {
11575
+ if (providerMatches[provider])
11576
+ continue;
11577
+ const binPattern = new RegExp(`(^|/)${provider}(\\s|$|-)`);
11578
+ if (binPattern.test(command)) {
11579
+ providerMatches[provider] = { pid, cpu, mem, command };
11580
+ }
11581
+ }
11582
+ }
11583
+ for (const [provider, info] of Object.entries(providerMatches)) {
11584
+ agents.push({
11585
+ provider,
11586
+ status: "running",
11587
+ pid: info.pid,
11588
+ memory: info.mem !== "0.0" ? `${info.mem}%` : undefined
11589
+ });
11590
+ }
11591
+ } catch {}
11592
+ return agents;
11593
+ }
11594
+ function formatUptime(pmUptime) {
11595
+ const seconds = Math.floor((Date.now() - pmUptime) / 1000);
11596
+ if (seconds < 60)
11597
+ return `${seconds}s`;
11598
+ const minutes = Math.floor(seconds / 60);
11599
+ if (minutes < 60)
11600
+ return `${minutes}m`;
11601
+ const hours = Math.floor(minutes / 60);
11602
+ if (hours < 24)
11603
+ return `${hours}h ${minutes % 60}m`;
11604
+ const days = Math.floor(hours / 24);
11605
+ return `${days}d ${hours % 24}h`;
11606
+ }
11371
11607
  var init_status = __esm(() => {
11372
11608
  init_config();
11373
11609
  init_github();
@@ -11375,6 +11611,7 @@ var init_status = __esm(() => {
11375
11611
  init_worktree();
11376
11612
  init_progress();
11377
11613
  init_terminal();
11614
+ init_registry();
11378
11615
  });
11379
11616
 
11380
11617
  // src/commands/plan.ts
@@ -11386,13 +11623,13 @@ __export(exports_plan, {
11386
11623
  parsePlanArgs: () => parsePlanArgs
11387
11624
  });
11388
11625
  import {
11389
- existsSync as existsSync22,
11626
+ existsSync as existsSync23,
11390
11627
  mkdirSync as mkdirSync15,
11391
11628
  readdirSync as readdirSync8,
11392
11629
  readFileSync as readFileSync13,
11393
11630
  writeFileSync as writeFileSync10
11394
11631
  } from "node:fs";
11395
- import { join as join22 } from "node:path";
11632
+ import { join as join23 } from "node:path";
11396
11633
  function printHelp2() {
11397
11634
  process.stderr.write(`
11398
11635
  ${bold2("locus plan")} — AI-powered sprint planning
@@ -11422,11 +11659,11 @@ function normalizeSprintName(name) {
11422
11659
  return name.trim().toLowerCase();
11423
11660
  }
11424
11661
  function getPlansDir(projectRoot) {
11425
- return join22(projectRoot, ".locus", "plans");
11662
+ return join23(projectRoot, ".locus", "plans");
11426
11663
  }
11427
11664
  function ensurePlansDir(projectRoot) {
11428
11665
  const dir = getPlansDir(projectRoot);
11429
- if (!existsSync22(dir)) {
11666
+ if (!existsSync23(dir)) {
11430
11667
  mkdirSync15(dir, { recursive: true });
11431
11668
  }
11432
11669
  return dir;
@@ -11436,14 +11673,14 @@ function generateId() {
11436
11673
  }
11437
11674
  function loadPlanFile(projectRoot, id) {
11438
11675
  const dir = getPlansDir(projectRoot);
11439
- if (!existsSync22(dir))
11676
+ if (!existsSync23(dir))
11440
11677
  return null;
11441
11678
  const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
11442
11679
  const match = files.find((f) => f.startsWith(id));
11443
11680
  if (!match)
11444
11681
  return null;
11445
11682
  try {
11446
- const content = readFileSync13(join22(dir, match), "utf-8");
11683
+ const content = readFileSync13(join23(dir, match), "utf-8");
11447
11684
  return JSON.parse(content);
11448
11685
  } catch {
11449
11686
  return null;
@@ -11497,7 +11734,7 @@ async function planCommand(projectRoot, args, flags = {}) {
11497
11734
  }
11498
11735
  function handleListPlans(projectRoot) {
11499
11736
  const dir = getPlansDir(projectRoot);
11500
- if (!existsSync22(dir)) {
11737
+ if (!existsSync23(dir)) {
11501
11738
  process.stderr.write(`${dim2("No saved plans yet.")}
11502
11739
  `);
11503
11740
  return;
@@ -11515,7 +11752,7 @@ ${bold2("Saved Plans:")}
11515
11752
  for (const file of files) {
11516
11753
  const id = file.replace(".json", "");
11517
11754
  try {
11518
- const content = readFileSync13(join22(dir, file), "utf-8");
11755
+ const content = readFileSync13(join23(dir, file), "utf-8");
11519
11756
  const plan = JSON.parse(content);
11520
11757
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
11521
11758
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -11626,7 +11863,7 @@ ${bold2("Approving plan:")}
11626
11863
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
11627
11864
  const id = generateId();
11628
11865
  const plansDir = ensurePlansDir(projectRoot);
11629
- const planPath = join22(plansDir, `${id}.json`);
11866
+ const planPath = join23(plansDir, `${id}.json`);
11630
11867
  const planPathRelative = `.locus/plans/${id}.json`;
11631
11868
  const displayDirective = directive;
11632
11869
  process.stderr.write(`
@@ -11669,7 +11906,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
11669
11906
  `);
11670
11907
  return;
11671
11908
  }
11672
- if (!existsSync22(planPath)) {
11909
+ if (!existsSync23(planPath)) {
11673
11910
  process.stderr.write(`
11674
11911
  ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
11675
11912
  `);
@@ -11851,15 +12088,15 @@ ${directive}${sprintName ? `
11851
12088
 
11852
12089
  **Sprint:** ${sprintName}` : ""}
11853
12090
  </directive>`);
11854
- const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11855
- if (existsSync22(locusPath)) {
12091
+ const locusPath = join23(projectRoot, ".locus", "LOCUS.md");
12092
+ if (existsSync23(locusPath)) {
11856
12093
  const content = readFileSync13(locusPath, "utf-8");
11857
12094
  parts.push(`<project-context>
11858
12095
  ${content.slice(0, 3000)}
11859
12096
  </project-context>`);
11860
12097
  }
11861
- const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
11862
- if (existsSync22(learningsPath)) {
12098
+ const learningsPath = join23(projectRoot, ".locus", "LEARNINGS.md");
12099
+ if (existsSync23(learningsPath)) {
11863
12100
  const content = readFileSync13(learningsPath, "utf-8");
11864
12101
  parts.push(`<past-learnings>
11865
12102
  ${content.slice(0, 2000)}
@@ -12039,9 +12276,9 @@ var exports_review = {};
12039
12276
  __export(exports_review, {
12040
12277
  reviewCommand: () => reviewCommand
12041
12278
  });
12042
- import { execFileSync as execFileSync2, execSync as execSync18 } from "node:child_process";
12043
- import { existsSync as existsSync23, readFileSync as readFileSync14 } from "node:fs";
12044
- import { join as join23 } from "node:path";
12279
+ import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
12280
+ import { existsSync as existsSync24, readFileSync as readFileSync14 } from "node:fs";
12281
+ import { join as join24 } from "node:path";
12045
12282
  function printHelp3() {
12046
12283
  process.stderr.write(`
12047
12284
  ${bold2("locus review")} — AI-powered code review
@@ -12125,7 +12362,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
12125
12362
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
12126
12363
  let prInfo;
12127
12364
  try {
12128
- const result = execSync18(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12365
+ const result = execSync19(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12129
12366
  const raw = JSON.parse(result);
12130
12367
  prInfo = {
12131
12368
  number: raw.number,
@@ -12210,8 +12447,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
12210
12447
  parts.push(`<role>
12211
12448
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
12212
12449
  </role>`);
12213
- const locusPath = join23(projectRoot, ".locus", "LOCUS.md");
12214
- if (existsSync23(locusPath)) {
12450
+ const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
12451
+ if (existsSync24(locusPath)) {
12215
12452
  const content = readFileSync14(locusPath, "utf-8");
12216
12453
  parts.push(`<project-context>
12217
12454
  ${content.slice(0, 2000)}
@@ -12273,7 +12510,7 @@ var exports_iterate = {};
12273
12510
  __export(exports_iterate, {
12274
12511
  iterateCommand: () => iterateCommand
12275
12512
  });
12276
- import { execSync as execSync19 } from "node:child_process";
12513
+ import { execSync as execSync20 } from "node:child_process";
12277
12514
  function printHelp4() {
12278
12515
  process.stderr.write(`
12279
12516
  ${bold2("locus iterate")} — Re-execute tasks with PR feedback
@@ -12491,12 +12728,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
12491
12728
  }
12492
12729
  function findPRForIssue(projectRoot, issueNumber) {
12493
12730
  try {
12494
- const result = execSync19(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12731
+ const result = execSync20(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12495
12732
  const parsed = JSON.parse(result);
12496
12733
  if (parsed.length > 0) {
12497
12734
  return parsed[0].number;
12498
12735
  }
12499
- const branchResult = execSync19(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12736
+ const branchResult = execSync20(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12500
12737
  const branchParsed = JSON.parse(branchResult);
12501
12738
  if (branchParsed.length > 0) {
12502
12739
  return branchParsed[0].number;
@@ -12532,14 +12769,14 @@ __export(exports_discuss, {
12532
12769
  discussCommand: () => discussCommand
12533
12770
  });
12534
12771
  import {
12535
- existsSync as existsSync24,
12772
+ existsSync as existsSync25,
12536
12773
  mkdirSync as mkdirSync16,
12537
12774
  readdirSync as readdirSync9,
12538
12775
  readFileSync as readFileSync15,
12539
12776
  unlinkSync as unlinkSync6,
12540
12777
  writeFileSync as writeFileSync11
12541
12778
  } from "node:fs";
12542
- import { join as join24 } from "node:path";
12779
+ import { join as join25 } from "node:path";
12543
12780
  function printHelp5() {
12544
12781
  process.stderr.write(`
12545
12782
  ${bold2("locus discuss")} — AI-powered architectural discussions
@@ -12561,11 +12798,11 @@ ${bold2("Examples:")}
12561
12798
  `);
12562
12799
  }
12563
12800
  function getDiscussionsDir(projectRoot) {
12564
- return join24(projectRoot, ".locus", "discussions");
12801
+ return join25(projectRoot, ".locus", "discussions");
12565
12802
  }
12566
12803
  function ensureDiscussionsDir(projectRoot) {
12567
12804
  const dir = getDiscussionsDir(projectRoot);
12568
- if (!existsSync24(dir)) {
12805
+ if (!existsSync25(dir)) {
12569
12806
  mkdirSync16(dir, { recursive: true });
12570
12807
  }
12571
12808
  return dir;
@@ -12600,7 +12837,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
12600
12837
  }
12601
12838
  function listDiscussions(projectRoot) {
12602
12839
  const dir = getDiscussionsDir(projectRoot);
12603
- if (!existsSync24(dir)) {
12840
+ if (!existsSync25(dir)) {
12604
12841
  process.stderr.write(`${dim2("No discussions yet.")}
12605
12842
  `);
12606
12843
  return;
@@ -12617,7 +12854,7 @@ ${bold2("Discussions:")}
12617
12854
  `);
12618
12855
  for (const file of files) {
12619
12856
  const id = file.replace(".md", "");
12620
- const content = readFileSync15(join24(dir, file), "utf-8");
12857
+ const content = readFileSync15(join25(dir, file), "utf-8");
12621
12858
  const titleMatch = content.match(/^#\s+(.+)/m);
12622
12859
  const title = titleMatch ? titleMatch[1] : id;
12623
12860
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -12635,7 +12872,7 @@ function showDiscussion(projectRoot, id) {
12635
12872
  return;
12636
12873
  }
12637
12874
  const dir = getDiscussionsDir(projectRoot);
12638
- if (!existsSync24(dir)) {
12875
+ if (!existsSync25(dir)) {
12639
12876
  process.stderr.write(`${red2("✗")} No discussions found.
12640
12877
  `);
12641
12878
  return;
@@ -12647,7 +12884,7 @@ function showDiscussion(projectRoot, id) {
12647
12884
  `);
12648
12885
  return;
12649
12886
  }
12650
- const content = readFileSync15(join24(dir, match), "utf-8");
12887
+ const content = readFileSync15(join25(dir, match), "utf-8");
12651
12888
  process.stdout.write(`${content}
12652
12889
  `);
12653
12890
  }
@@ -12658,7 +12895,7 @@ function deleteDiscussion(projectRoot, id) {
12658
12895
  return;
12659
12896
  }
12660
12897
  const dir = getDiscussionsDir(projectRoot);
12661
- if (!existsSync24(dir)) {
12898
+ if (!existsSync25(dir)) {
12662
12899
  process.stderr.write(`${red2("✗")} No discussions found.
12663
12900
  `);
12664
12901
  return;
@@ -12670,7 +12907,7 @@ function deleteDiscussion(projectRoot, id) {
12670
12907
  `);
12671
12908
  return;
12672
12909
  }
12673
- unlinkSync6(join24(dir, match));
12910
+ unlinkSync6(join25(dir, match));
12674
12911
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
12675
12912
  `);
12676
12913
  }
@@ -12683,7 +12920,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
12683
12920
  return;
12684
12921
  }
12685
12922
  const dir = getDiscussionsDir(projectRoot);
12686
- if (!existsSync24(dir)) {
12923
+ if (!existsSync25(dir)) {
12687
12924
  process.stderr.write(`${red2("✗")} No discussions found.
12688
12925
  `);
12689
12926
  return;
@@ -12695,7 +12932,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
12695
12932
  `);
12696
12933
  return;
12697
12934
  }
12698
- const content = readFileSync15(join24(dir, match), "utf-8");
12935
+ const content = readFileSync15(join25(dir, match), "utf-8");
12699
12936
  const titleMatch = content.match(/^#\s+(.+)/m);
12700
12937
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
12701
12938
  await planCommand(projectRoot, [
@@ -12821,7 +13058,7 @@ ${turn.content}`;
12821
13058
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
12822
13059
  ].join(`
12823
13060
  `);
12824
- writeFileSync11(join24(dir, `${id}.md`), markdown, "utf-8");
13061
+ writeFileSync11(join25(dir, `${id}.md`), markdown, "utf-8");
12825
13062
  process.stderr.write(`
12826
13063
  ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
12827
13064
  `);
@@ -12836,15 +13073,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
12836
13073
  parts.push(`<role>
12837
13074
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
12838
13075
  </role>`);
12839
- const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
12840
- if (existsSync24(locusPath)) {
13076
+ const locusPath = join25(projectRoot, ".locus", "LOCUS.md");
13077
+ if (existsSync25(locusPath)) {
12841
13078
  const content = readFileSync15(locusPath, "utf-8");
12842
13079
  parts.push(`<project-context>
12843
13080
  ${content.slice(0, 3000)}
12844
13081
  </project-context>`);
12845
13082
  }
12846
- const learningsPath = join24(projectRoot, ".locus", "LEARNINGS.md");
12847
- if (existsSync24(learningsPath)) {
13083
+ const learningsPath = join25(projectRoot, ".locus", "LEARNINGS.md");
13084
+ if (existsSync25(learningsPath)) {
12848
13085
  const content = readFileSync15(learningsPath, "utf-8");
12849
13086
  parts.push(`<past-learnings>
12850
13087
  ${content.slice(0, 2000)}
@@ -12916,8 +13153,8 @@ __export(exports_artifacts, {
12916
13153
  formatDate: () => formatDate2,
12917
13154
  artifactsCommand: () => artifactsCommand
12918
13155
  });
12919
- import { existsSync as existsSync25, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
12920
- import { join as join25 } from "node:path";
13156
+ import { existsSync as existsSync26, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
13157
+ import { join as join26 } from "node:path";
12921
13158
  function printHelp6() {
12922
13159
  process.stderr.write(`
12923
13160
  ${bold2("locus artifacts")} — View and manage AI-generated artifacts
@@ -12937,14 +13174,14 @@ ${dim2("Artifact names support partial matching.")}
12937
13174
  `);
12938
13175
  }
12939
13176
  function getArtifactsDir(projectRoot) {
12940
- return join25(projectRoot, ".locus", "artifacts");
13177
+ return join26(projectRoot, ".locus", "artifacts");
12941
13178
  }
12942
13179
  function listArtifacts(projectRoot) {
12943
13180
  const dir = getArtifactsDir(projectRoot);
12944
- if (!existsSync25(dir))
13181
+ if (!existsSync26(dir))
12945
13182
  return [];
12946
13183
  return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
12947
- const filePath = join25(dir, fileName);
13184
+ const filePath = join26(dir, fileName);
12948
13185
  const stat = statSync5(filePath);
12949
13186
  return {
12950
13187
  name: fileName.replace(/\.md$/, ""),
@@ -12957,8 +13194,8 @@ function listArtifacts(projectRoot) {
12957
13194
  function readArtifact(projectRoot, name) {
12958
13195
  const dir = getArtifactsDir(projectRoot);
12959
13196
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
12960
- const filePath = join25(dir, fileName);
12961
- if (!existsSync25(filePath))
13197
+ const filePath = join26(dir, fileName);
13198
+ if (!existsSync26(filePath))
12962
13199
  return null;
12963
13200
  const stat = statSync5(filePath);
12964
13201
  return {
@@ -13125,7 +13362,7 @@ var exports_commit = {};
13125
13362
  __export(exports_commit, {
13126
13363
  commitCommand: () => commitCommand
13127
13364
  });
13128
- import { execSync as execSync20 } from "node:child_process";
13365
+ import { execSync as execSync21 } from "node:child_process";
13129
13366
  function printCommitHelp() {
13130
13367
  process.stderr.write(`
13131
13368
  ${bold2("locus commit")} — AI-powered commit message generation
@@ -13159,7 +13396,7 @@ async function commitCommand(projectRoot, args, flags = {}) {
13159
13396
  const config = loadConfig(projectRoot);
13160
13397
  let stagedDiff;
13161
13398
  try {
13162
- stagedDiff = execSync20("git diff --cached", {
13399
+ stagedDiff = execSync21("git diff --cached", {
13163
13400
  cwd: projectRoot,
13164
13401
  encoding: "utf-8",
13165
13402
  stdio: ["pipe", "pipe", "pipe"]
@@ -13176,7 +13413,7 @@ async function commitCommand(projectRoot, args, flags = {}) {
13176
13413
  }
13177
13414
  let stagedStat;
13178
13415
  try {
13179
- stagedStat = execSync20("git diff --cached --stat", {
13416
+ stagedStat = execSync21("git diff --cached --stat", {
13180
13417
  cwd: projectRoot,
13181
13418
  encoding: "utf-8",
13182
13419
  stdio: ["pipe", "pipe", "pipe"]
@@ -13186,7 +13423,7 @@ async function commitCommand(projectRoot, args, flags = {}) {
13186
13423
  }
13187
13424
  let recentCommits;
13188
13425
  try {
13189
- recentCommits = execSync20("git log --oneline -10", {
13426
+ recentCommits = execSync21("git log --oneline -10", {
13190
13427
  cwd: projectRoot,
13191
13428
  encoding: "utf-8",
13192
13429
  stdio: ["pipe", "pipe", "pipe"]
@@ -13246,7 +13483,7 @@ Co-Authored-By: LocusAgent <agent@locusai.team>`;
13246
13483
  return;
13247
13484
  }
13248
13485
  try {
13249
- execSync20("git commit -F -", {
13486
+ execSync21("git commit -F -", {
13250
13487
  input: fullMessage,
13251
13488
  cwd: projectRoot,
13252
13489
  encoding: "utf-8",
@@ -13317,10 +13554,10 @@ __export(exports_sandbox2, {
13317
13554
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
13318
13555
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
13319
13556
  });
13320
- import { execSync as execSync21, spawn as spawn7 } from "node:child_process";
13557
+ import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
13321
13558
  import { createHash } from "node:crypto";
13322
- import { existsSync as existsSync26, readFileSync as readFileSync17 } from "node:fs";
13323
- import { basename as basename4, join as join26 } from "node:path";
13559
+ import { existsSync as existsSync27, readFileSync as readFileSync17 } from "node:fs";
13560
+ import { basename as basename4, join as join27 } from "node:path";
13324
13561
  import { createInterface as createInterface3 } from "node:readline";
13325
13562
  function printSandboxHelp() {
13326
13563
  process.stderr.write(`
@@ -13562,7 +13799,7 @@ function handleRemove(projectRoot) {
13562
13799
  process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
13563
13800
  `);
13564
13801
  try {
13565
- execSync21(`docker sandbox rm ${sandboxName}`, {
13802
+ execSync22(`docker sandbox rm ${sandboxName}`, {
13566
13803
  encoding: "utf-8",
13567
13804
  stdio: ["pipe", "pipe", "pipe"],
13568
13805
  timeout: 15000
@@ -13850,7 +14087,7 @@ async function handleLogs(projectRoot, args) {
13850
14087
  }
13851
14088
  function detectPackageManager2(projectRoot) {
13852
14089
  try {
13853
- const raw = readFileSync17(join26(projectRoot, "package.json"), "utf-8");
14090
+ const raw = readFileSync17(join27(projectRoot, "package.json"), "utf-8");
13854
14091
  const pkgJson = JSON.parse(raw);
13855
14092
  if (typeof pkgJson.packageManager === "string") {
13856
14093
  const name = pkgJson.packageManager.split("@")[0];
@@ -13859,13 +14096,13 @@ function detectPackageManager2(projectRoot) {
13859
14096
  }
13860
14097
  }
13861
14098
  } catch {}
13862
- if (existsSync26(join26(projectRoot, "bun.lock")) || existsSync26(join26(projectRoot, "bun.lockb"))) {
14099
+ if (existsSync27(join27(projectRoot, "bun.lock")) || existsSync27(join27(projectRoot, "bun.lockb"))) {
13863
14100
  return "bun";
13864
14101
  }
13865
- if (existsSync26(join26(projectRoot, "yarn.lock"))) {
14102
+ if (existsSync27(join27(projectRoot, "yarn.lock"))) {
13866
14103
  return "yarn";
13867
14104
  }
13868
- if (existsSync26(join26(projectRoot, "pnpm-lock.yaml"))) {
14105
+ if (existsSync27(join27(projectRoot, "pnpm-lock.yaml"))) {
13869
14106
  return "pnpm";
13870
14107
  }
13871
14108
  return "npm";
@@ -13885,7 +14122,7 @@ function getInstallCommand(pm) {
13885
14122
  function verifyBinEntries(sandboxName, workdir) {
13886
14123
  try {
13887
14124
  const binDir = `${workdir}/node_modules/.bin/`;
13888
- const result = execSync21(`docker sandbox exec --privileged ${sandboxName} sh -c ${JSON.stringify(`ls ${JSON.stringify(binDir)} 2>/dev/null | head -20`)}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
14125
+ const result = execSync22(`docker sandbox exec --privileged ${sandboxName} sh -c ${JSON.stringify(`ls ${JSON.stringify(binDir)} 2>/dev/null | head -20`)}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
13889
14126
  if (!result) {
13890
14127
  process.stderr.write(`${yellow2("⚠")} node_modules/.bin is empty — binaries like biome may not be available.
13891
14128
  `);
@@ -13968,9 +14205,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
13968
14205
  ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
13969
14206
  `);
13970
14207
  }
13971
- const setupScript = join26(projectRoot, ".locus", "sandbox-setup.sh");
13972
- const containerSetupScript = containerWorkdir ? join26(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
13973
- if (existsSync26(setupScript)) {
14208
+ const setupScript = join27(projectRoot, ".locus", "sandbox-setup.sh");
14209
+ const containerSetupScript = containerWorkdir ? join27(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
14210
+ if (existsSync27(setupScript)) {
13974
14211
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
13975
14212
  `);
13976
14213
  const hookOk = await runInteractiveCommand("docker", [
@@ -14059,7 +14296,7 @@ function runInteractiveCommand(command, args) {
14059
14296
  }
14060
14297
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
14061
14298
  try {
14062
- execSync21(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
14299
+ execSync22(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
14063
14300
  stdio: ["pipe", "pipe", "pipe"],
14064
14301
  timeout: 120000
14065
14302
  });
@@ -14074,7 +14311,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
14074
14311
  }
14075
14312
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
14076
14313
  try {
14077
- execSync21(`docker sandbox exec --privileged ${sandboxName} which ${pm}`, {
14314
+ execSync22(`docker sandbox exec --privileged ${sandboxName} which ${pm}`, {
14078
14315
  stdio: ["pipe", "pipe", "pipe"],
14079
14316
  timeout: 5000
14080
14317
  });
@@ -14083,7 +14320,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
14083
14320
  process.stderr.write(`Installing ${bold2(pm)} in sandbox...
14084
14321
  `);
14085
14322
  try {
14086
- execSync21(`docker sandbox exec --privileged ${sandboxName} npm install -g ${npmPkg}`, {
14323
+ execSync22(`docker sandbox exec --privileged ${sandboxName} npm install -g ${npmPkg}`, {
14087
14324
  stdio: "inherit",
14088
14325
  timeout: 120000
14089
14326
  });
@@ -14095,7 +14332,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
14095
14332
  }
14096
14333
  async function ensureCodexInSandbox(sandboxName) {
14097
14334
  try {
14098
- execSync21(`docker sandbox exec --privileged ${sandboxName} which codex`, {
14335
+ execSync22(`docker sandbox exec --privileged ${sandboxName} which codex`, {
14099
14336
  stdio: ["pipe", "pipe", "pipe"],
14100
14337
  timeout: 5000
14101
14338
  });
@@ -14103,7 +14340,7 @@ async function ensureCodexInSandbox(sandboxName) {
14103
14340
  process.stderr.write(`Installing codex in sandbox...
14104
14341
  `);
14105
14342
  try {
14106
- execSync21(`docker sandbox exec --privileged ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
14343
+ execSync22(`docker sandbox exec --privileged ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
14107
14344
  } catch {
14108
14345
  process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
14109
14346
  `);
@@ -14112,7 +14349,7 @@ async function ensureCodexInSandbox(sandboxName) {
14112
14349
  }
14113
14350
  function isSandboxAlive(name) {
14114
14351
  try {
14115
- const output = execSync21("docker sandbox ls", {
14352
+ const output = execSync22("docker sandbox ls", {
14116
14353
  encoding: "utf-8",
14117
14354
  stdio: ["pipe", "pipe", "pipe"],
14118
14355
  timeout: 5000
@@ -14138,13 +14375,13 @@ init_context();
14138
14375
  init_logger();
14139
14376
  init_rate_limiter();
14140
14377
  init_terminal();
14141
- import { existsSync as existsSync27, readFileSync as readFileSync18 } from "node:fs";
14142
- import { join as join27 } from "node:path";
14378
+ import { existsSync as existsSync28, readFileSync as readFileSync18 } from "node:fs";
14379
+ import { join as join28 } from "node:path";
14143
14380
  import { fileURLToPath } from "node:url";
14144
14381
  function getCliVersion() {
14145
14382
  const fallbackVersion = "0.0.0";
14146
- const packageJsonPath = join27(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
14147
- if (!existsSync27(packageJsonPath)) {
14383
+ const packageJsonPath = join28(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
14384
+ if (!existsSync28(packageJsonPath)) {
14148
14385
  return fallbackVersion;
14149
14386
  }
14150
14387
  try {
@@ -14421,7 +14658,7 @@ async function main() {
14421
14658
  try {
14422
14659
  const root = getGitRoot(cwd);
14423
14660
  if (isInitialized(root)) {
14424
- logDir = join27(root, ".locus", "logs");
14661
+ logDir = join28(root, ".locus", "logs");
14425
14662
  getRateLimiter(root);
14426
14663
  }
14427
14664
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.23.4",
3
+ "version": "0.24.0",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "dependencies": {},
38
38
  "devDependencies": {
39
- "@locusai/sdk": "^0.23.4",
39
+ "@locusai/sdk": "^0.24.0",
40
40
  "@types/bun": "latest",
41
41
  "typescript": "^5.8.3"
42
42
  },