@locusai/cli 0.21.17 → 0.22.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 +737 -137
  2. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -3628,13 +3628,25 @@ class InputHandler {
3628
3628
  prompt;
3629
3629
  getHistory;
3630
3630
  onTab;
3631
+ activeInsertText = null;
3632
+ activeRender = null;
3631
3633
  locked = false;
3632
3634
  lastInterruptTime = 0;
3635
+ pendingInsert = null;
3633
3636
  constructor(options) {
3634
3637
  this.prompt = options.prompt;
3635
3638
  this.getHistory = options.getHistory ?? (() => []);
3636
3639
  this.onTab = options.onTab;
3637
3640
  }
3641
+ insertTextFromExternal(text) {
3642
+ if (this.activeInsertText && this.activeRender) {
3643
+ this.activeInsertText(text);
3644
+ this.activeRender();
3645
+ }
3646
+ }
3647
+ setInitialBuffer(text) {
3648
+ this.pendingInsert = text;
3649
+ }
3638
3650
  setPrompt(prompt) {
3639
3651
  this.prompt = prompt;
3640
3652
  }
@@ -3671,6 +3683,8 @@ class InputHandler {
3671
3683
  if (resolved)
3672
3684
  return;
3673
3685
  resolved = true;
3686
+ this.activeInsertText = null;
3687
+ this.activeRender = null;
3674
3688
  stdin.removeListener("data", onData);
3675
3689
  if (stdin.isTTY) {
3676
3690
  out.write(DISABLE_BRACKETED_PASTE + DISABLE_KITTY_KEYBOARD);
@@ -4138,6 +4152,12 @@ ${dim2("Press Ctrl+C again to exit")}\r
4138
4152
  if (stdin.isTTY) {
4139
4153
  out.write(ENABLE_BRACKETED_PASTE + ENABLE_KITTY_KEYBOARD);
4140
4154
  }
4155
+ this.activeInsertText = insertText;
4156
+ this.activeRender = render;
4157
+ if (this.pendingInsert) {
4158
+ insertText(this.pendingInsert);
4159
+ this.pendingInsert = null;
4160
+ }
4141
4161
  render();
4142
4162
  });
4143
4163
  }
@@ -7508,13 +7528,13 @@ function getSlashCommands() {
7508
7528
  return [
7509
7529
  {
7510
7530
  name: "/help",
7511
- aliases: ["/h", "/?"],
7531
+ aliases: ["/h"],
7512
7532
  description: "Show available commands",
7513
7533
  handler: cmdHelp
7514
7534
  },
7515
7535
  {
7516
7536
  name: "/clear",
7517
- aliases: ["/cls"],
7537
+ aliases: [],
7518
7538
  description: "Clear screen",
7519
7539
  handler: cmdClear
7520
7540
  },
@@ -7550,7 +7570,7 @@ function getSlashCommands() {
7550
7570
  },
7551
7571
  {
7552
7572
  name: "/undo",
7553
- aliases: ["/u"],
7573
+ aliases: [],
7554
7574
  description: "Undo last AI change",
7555
7575
  handler: cmdUndo
7556
7576
  },
@@ -7562,10 +7582,16 @@ function getSlashCommands() {
7562
7582
  },
7563
7583
  {
7564
7584
  name: "/verbose",
7565
- aliases: ["/v"],
7585
+ aliases: [],
7566
7586
  description: "Toggle verbose mode (show agent stderr streams)",
7567
7587
  handler: cmdVerbose
7568
7588
  },
7589
+ {
7590
+ name: "/voice",
7591
+ aliases: ["/v"],
7592
+ description: "Start voice recording (press Enter to stop)",
7593
+ handler: cmdVoice
7594
+ },
7569
7595
  {
7570
7596
  name: "/exit",
7571
7597
  aliases: ["/quit", "/q"],
@@ -7764,6 +7790,14 @@ function cmdVerbose(_args, ctx) {
7764
7790
  process.stderr.write(`${isOn ? green("✓") : dim2("○")} Verbose mode ${isOn ? bold2("on") : "off"} — agent streams ${isOn ? "visible" : "hidden"}.
7765
7791
  `);
7766
7792
  }
7793
+ function cmdVoice(_args, ctx) {
7794
+ if (ctx.onVoiceToggle) {
7795
+ ctx.onVoiceToggle();
7796
+ } else {
7797
+ process.stderr.write(`${red2("✗")} Voice input not available in this session.
7798
+ `);
7799
+ }
7800
+ }
7767
7801
  function cmdExit(_args, ctx) {
7768
7802
  ctx.onExit();
7769
7803
  }
@@ -8110,8 +8144,544 @@ var init_session_manager = __esm(() => {
8110
8144
  SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
8111
8145
  });
8112
8146
 
8147
+ // src/repl/voice.ts
8148
+ import { execSync as execSync9, spawn as spawn6 } from "node:child_process";
8149
+ import { existsSync as existsSync17, mkdirSync as mkdirSync12, unlinkSync as unlinkSync4 } from "node:fs";
8150
+ import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
8151
+ import { join as join17 } from "node:path";
8152
+ function getWhisperModelPath() {
8153
+ return join17(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
8154
+ }
8155
+ function commandExists(cmd) {
8156
+ try {
8157
+ const which = platform() === "win32" ? "where" : "which";
8158
+ execSync9(`${which} ${cmd}`, { stdio: "pipe" });
8159
+ return true;
8160
+ } catch {
8161
+ return false;
8162
+ }
8163
+ }
8164
+ function findWhisperBinary() {
8165
+ const candidates = ["whisper-cli", "whisper-cpp", "whisper", "main"];
8166
+ for (const name of candidates) {
8167
+ if (commandExists(name))
8168
+ return name;
8169
+ }
8170
+ for (const name of candidates) {
8171
+ const fullPath = join17(LOCUS_BIN_DIR, name);
8172
+ if (existsSync17(fullPath))
8173
+ return fullPath;
8174
+ }
8175
+ if (platform() === "darwin") {
8176
+ const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
8177
+ for (const dir of brewDirs) {
8178
+ for (const name of candidates) {
8179
+ const fullPath = join17(dir, name);
8180
+ if (existsSync17(fullPath))
8181
+ return fullPath;
8182
+ }
8183
+ }
8184
+ }
8185
+ return null;
8186
+ }
8187
+ function findSoxRecBinary() {
8188
+ if (commandExists("rec"))
8189
+ return "rec";
8190
+ if (commandExists("sox"))
8191
+ return "sox";
8192
+ if (platform() === "darwin") {
8193
+ const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
8194
+ for (const dir of brewDirs) {
8195
+ const recPath = join17(dir, "rec");
8196
+ if (existsSync17(recPath))
8197
+ return recPath;
8198
+ const soxPath = join17(dir, "sox");
8199
+ if (existsSync17(soxPath))
8200
+ return soxPath;
8201
+ }
8202
+ }
8203
+ return null;
8204
+ }
8205
+ function checkDependencies() {
8206
+ const soxBinary = findSoxRecBinary();
8207
+ const whisperBinary = findWhisperBinary();
8208
+ const modelDownloaded = existsSync17(getWhisperModelPath());
8209
+ return {
8210
+ sox: soxBinary !== null,
8211
+ whisper: whisperBinary !== null,
8212
+ whisperBinary,
8213
+ soxBinary,
8214
+ modelDownloaded
8215
+ };
8216
+ }
8217
+ function printDependencyHelp(deps) {
8218
+ const out = process.stderr;
8219
+ out.write(`
8220
+ ${bold2("Voice Input Setup")}
8221
+
8222
+ `);
8223
+ if (!deps.sox) {
8224
+ out.write(` ${red2("✗")} ${bold2("sox")} — audio recording
8225
+ `);
8226
+ if (platform() === "darwin") {
8227
+ out.write(` Install: ${cyan2("brew install sox")}
8228
+ `);
8229
+ } else {
8230
+ out.write(` Install: ${cyan2("sudo apt install sox")} or ${cyan2("sudo dnf install sox")}
8231
+ `);
8232
+ }
8233
+ } else {
8234
+ out.write(` ${green("✓")} ${bold2("sox")} — audio recording ${dim2(`(${deps.soxBinary})`)}
8235
+ `);
8236
+ }
8237
+ if (!deps.whisper) {
8238
+ out.write(` ${red2("✗")} ${bold2("whisper.cpp")} — speech-to-text
8239
+ `);
8240
+ if (platform() === "darwin") {
8241
+ out.write(` Install: ${cyan2("brew install whisper-cpp")}
8242
+ `);
8243
+ } else {
8244
+ out.write(` Install: Build from source — ${cyan2("https://github.com/ggerganov/whisper.cpp")}
8245
+ `);
8246
+ }
8247
+ } else {
8248
+ out.write(` ${green("✓")} ${bold2("whisper.cpp")} — speech-to-text ${dim2(`(${deps.whisperBinary})`)}
8249
+ `);
8250
+ }
8251
+ if (deps.whisper && !deps.modelDownloaded) {
8252
+ out.write(` ${yellow2("!")} Model ${bold2(WHISPER_MODEL)} not downloaded yet — will download on first use (~150MB)
8253
+ `);
8254
+ out.write(` Path: ${dim2(getWhisperModelPath())}
8255
+ `);
8256
+ } else if (deps.whisper && deps.modelDownloaded) {
8257
+ out.write(` ${green("✓")} Model ${bold2(WHISPER_MODEL)} ${dim2("ready")}
8258
+ `);
8259
+ }
8260
+ out.write(`
8261
+ `);
8262
+ if (!deps.sox || !deps.whisper) {
8263
+ out.write(` ${dim2("Install the missing dependencies above, then try again.")}
8264
+
8265
+ `);
8266
+ }
8267
+ }
8268
+ function detectPackageManager() {
8269
+ if (platform() === "darwin") {
8270
+ return commandExists("brew") ? "brew" : null;
8271
+ }
8272
+ if (commandExists("apt-get"))
8273
+ return "apt";
8274
+ if (commandExists("dnf"))
8275
+ return "dnf";
8276
+ if (commandExists("pacman"))
8277
+ return "pacman";
8278
+ return null;
8279
+ }
8280
+ function installSox(pm) {
8281
+ try {
8282
+ switch (pm) {
8283
+ case "brew":
8284
+ execSync9("brew install sox", { stdio: "inherit", timeout: 300000 });
8285
+ break;
8286
+ case "apt":
8287
+ execSync9("sudo apt-get install -y sox", {
8288
+ stdio: "inherit",
8289
+ timeout: 300000
8290
+ });
8291
+ break;
8292
+ case "dnf":
8293
+ execSync9("sudo dnf install -y sox", {
8294
+ stdio: "inherit",
8295
+ timeout: 300000
8296
+ });
8297
+ break;
8298
+ case "pacman":
8299
+ execSync9("sudo pacman -S --noconfirm sox", {
8300
+ stdio: "inherit",
8301
+ timeout: 300000
8302
+ });
8303
+ break;
8304
+ }
8305
+ return true;
8306
+ } catch {
8307
+ return false;
8308
+ }
8309
+ }
8310
+ function installWhisperCpp(pm) {
8311
+ if (pm === "brew") {
8312
+ try {
8313
+ execSync9("brew install whisper-cpp", {
8314
+ stdio: "inherit",
8315
+ timeout: 300000
8316
+ });
8317
+ return true;
8318
+ } catch {
8319
+ return false;
8320
+ }
8321
+ }
8322
+ return buildWhisperFromSource(pm);
8323
+ }
8324
+ function ensureBuildDeps(pm) {
8325
+ const hasCmake = commandExists("cmake");
8326
+ const hasCxx = commandExists("g++") || commandExists("c++");
8327
+ const hasGit = commandExists("git");
8328
+ if (hasCmake && hasCxx && hasGit)
8329
+ return true;
8330
+ process.stderr.write(` ${dim2("Installing build tools...")}
8331
+ `);
8332
+ try {
8333
+ switch (pm) {
8334
+ case "apt":
8335
+ execSync9("sudo apt-get install -y cmake g++ make git", {
8336
+ stdio: "inherit",
8337
+ timeout: 300000
8338
+ });
8339
+ break;
8340
+ case "dnf":
8341
+ execSync9("sudo dnf install -y cmake gcc-c++ make git", {
8342
+ stdio: "inherit",
8343
+ timeout: 300000
8344
+ });
8345
+ break;
8346
+ case "pacman":
8347
+ execSync9("sudo pacman -S --noconfirm cmake gcc make git", {
8348
+ stdio: "inherit",
8349
+ timeout: 300000
8350
+ });
8351
+ break;
8352
+ default:
8353
+ return false;
8354
+ }
8355
+ return true;
8356
+ } catch {
8357
+ return false;
8358
+ }
8359
+ }
8360
+ function buildWhisperFromSource(pm) {
8361
+ const out = process.stderr;
8362
+ const buildDir = join17(tmpdir4(), `locus-whisper-build-${process.pid}`);
8363
+ if (!ensureBuildDeps(pm)) {
8364
+ out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
8365
+ `);
8366
+ return false;
8367
+ }
8368
+ try {
8369
+ mkdirSync12(buildDir, { recursive: true });
8370
+ mkdirSync12(LOCUS_BIN_DIR, { recursive: true });
8371
+ out.write(` ${dim2("Cloning whisper.cpp...")}
8372
+ `);
8373
+ execSync9(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join17(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
8374
+ const srcDir = join17(buildDir, "whisper.cpp");
8375
+ const numCpus = cpus().length || 2;
8376
+ out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
8377
+ `);
8378
+ execSync9("cmake -B build -DCMAKE_BUILD_TYPE=Release", {
8379
+ cwd: srcDir,
8380
+ stdio: ["pipe", "pipe", "pipe"],
8381
+ timeout: 120000
8382
+ });
8383
+ execSync9(`cmake --build build --config Release -j${numCpus}`, {
8384
+ cwd: srcDir,
8385
+ stdio: ["pipe", "pipe", "pipe"],
8386
+ timeout: 600000
8387
+ });
8388
+ const destPath = join17(LOCUS_BIN_DIR, "whisper-cli");
8389
+ const binaryCandidates = [
8390
+ join17(srcDir, "build", "bin", "whisper-cli"),
8391
+ join17(srcDir, "build", "bin", "main")
8392
+ ];
8393
+ for (const candidate of binaryCandidates) {
8394
+ if (existsSync17(candidate)) {
8395
+ execSync9(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
8396
+ stdio: "pipe"
8397
+ });
8398
+ return true;
8399
+ }
8400
+ }
8401
+ out.write(` ${red2("✗")} Build completed but whisper-cli binary not found.
8402
+ `);
8403
+ return false;
8404
+ } catch (e) {
8405
+ out.write(` ${red2("✗")} Build failed: ${e instanceof Error ? e.message : String(e)}
8406
+ `);
8407
+ return false;
8408
+ } finally {
8409
+ try {
8410
+ execSync9(`rm -rf "${buildDir}"`, { stdio: "pipe" });
8411
+ } catch {}
8412
+ }
8413
+ }
8414
+ function autoInstallDependencies(deps) {
8415
+ if (platform() === "win32") {
8416
+ process.stderr.write(`
8417
+ ${red2("✗")} Voice input is not supported on Windows.
8418
+
8419
+ `);
8420
+ return false;
8421
+ }
8422
+ const pm = detectPackageManager();
8423
+ if (!pm) {
8424
+ process.stderr.write(`
8425
+ ${red2("✗")} No supported package manager found.
8426
+ `);
8427
+ if (platform() === "darwin") {
8428
+ process.stderr.write(` Install Homebrew first: ${cyan2("https://brew.sh")}
8429
+ `);
8430
+ }
8431
+ process.stderr.write(`
8432
+ `);
8433
+ return false;
8434
+ }
8435
+ const out = process.stderr;
8436
+ out.write(`
8437
+ ${bold2("Installing voice dependencies...")}
8438
+
8439
+ `);
8440
+ if (!deps.sox) {
8441
+ out.write(` ${dim2("Installing")} ${bold2("sox")} ${dim2("(audio recording)...")}
8442
+ `);
8443
+ if (!installSox(pm)) {
8444
+ out.write(` ${red2("✗")} Failed to install sox.
8445
+
8446
+ `);
8447
+ return false;
8448
+ }
8449
+ out.write(` ${green("✓")} sox installed
8450
+
8451
+ `);
8452
+ }
8453
+ if (!deps.whisper) {
8454
+ out.write(` ${dim2("Installing")} ${bold2("whisper.cpp")} ${dim2("(speech-to-text)...")}
8455
+ `);
8456
+ if (!installWhisperCpp(pm)) {
8457
+ out.write(` ${red2("✗")} Failed to install whisper.cpp.
8458
+
8459
+ `);
8460
+ return false;
8461
+ }
8462
+ out.write(` ${green("✓")} whisper.cpp installed
8463
+
8464
+ `);
8465
+ }
8466
+ out.write(`${green("✓")} Voice dependencies ready.
8467
+
8468
+ `);
8469
+ return true;
8470
+ }
8471
+ function downloadModel() {
8472
+ const modelPath = getWhisperModelPath();
8473
+ if (existsSync17(modelPath))
8474
+ return true;
8475
+ mkdirSync12(WHISPER_MODELS_DIR, { recursive: true });
8476
+ const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
8477
+ process.stderr.write(`${dim2("Downloading whisper model")} ${bold2(WHISPER_MODEL)} ${dim2("(~150MB)...")}
8478
+ `);
8479
+ try {
8480
+ if (commandExists("curl")) {
8481
+ execSync9(`curl -L -o "${modelPath}" "${url}"`, {
8482
+ stdio: ["pipe", "pipe", "pipe"],
8483
+ timeout: 300000
8484
+ });
8485
+ } else if (commandExists("wget")) {
8486
+ execSync9(`wget -O "${modelPath}" "${url}"`, {
8487
+ stdio: ["pipe", "pipe", "pipe"],
8488
+ timeout: 300000
8489
+ });
8490
+ } else {
8491
+ process.stderr.write(`${red2("✗")} Neither curl nor wget found. Download the model manually:
8492
+ `);
8493
+ process.stderr.write(` ${cyan2(url)}
8494
+ `);
8495
+ process.stderr.write(` Save to: ${dim2(modelPath)}
8496
+ `);
8497
+ return false;
8498
+ }
8499
+ process.stderr.write(`${green("✓")} Model downloaded to ${dim2(modelPath)}
8500
+ `);
8501
+ return true;
8502
+ } catch (e) {
8503
+ process.stderr.write(`${red2("✗")} Failed to download model: ${e instanceof Error ? e.message : String(e)}
8504
+ `);
8505
+ try {
8506
+ unlinkSync4(modelPath);
8507
+ } catch {}
8508
+ return false;
8509
+ }
8510
+ }
8511
+
8512
+ class VoiceController {
8513
+ state = "idle";
8514
+ recordProcess = null;
8515
+ tempFile;
8516
+ deps;
8517
+ onStateChange;
8518
+ constructor(options) {
8519
+ this.onStateChange = options.onStateChange;
8520
+ this.tempFile = join17(tmpdir4(), `locus-voice-${process.pid}.wav`);
8521
+ this.deps = checkDependencies();
8522
+ }
8523
+ getState() {
8524
+ return this.state;
8525
+ }
8526
+ startRecording() {
8527
+ if (this.state !== "idle")
8528
+ return false;
8529
+ this.deps = checkDependencies();
8530
+ if (!this.deps.sox || !this.deps.whisper) {
8531
+ if (!autoInstallDependencies(this.deps)) {
8532
+ return false;
8533
+ }
8534
+ this.deps = checkDependencies();
8535
+ if (!this.deps.sox || !this.deps.whisper) {
8536
+ printDependencyHelp(this.deps);
8537
+ return false;
8538
+ }
8539
+ }
8540
+ if (!this.deps.modelDownloaded) {
8541
+ if (!downloadModel()) {
8542
+ return false;
8543
+ }
8544
+ this.deps.modelDownloaded = true;
8545
+ }
8546
+ const args = this.buildRecordArgs();
8547
+ if (!args)
8548
+ return false;
8549
+ const binary = this.deps.soxBinary;
8550
+ if (!binary) {
8551
+ process.stderr.write(`${red2("✗")} sox binary not found. Please install sox and try again.
8552
+ `);
8553
+ return false;
8554
+ }
8555
+ const spawnArgs = binary === "rec" ? args : ["-d", ...args];
8556
+ this.recordProcess = spawn6(binary, spawnArgs, {
8557
+ stdio: ["pipe", "pipe", "pipe"]
8558
+ });
8559
+ this.recordProcess.on("error", (err) => {
8560
+ process.stderr.write(`\r
8561
+ ${red2("✗")} Recording failed: ${err.message}\r
8562
+ `);
8563
+ this.setState("idle");
8564
+ });
8565
+ this.setState("recording");
8566
+ return true;
8567
+ }
8568
+ buildRecordArgs() {
8569
+ return [
8570
+ "-r",
8571
+ "16000",
8572
+ "-c",
8573
+ "1",
8574
+ "-b",
8575
+ "16",
8576
+ this.tempFile
8577
+ ];
8578
+ }
8579
+ async stopAndTranscribe() {
8580
+ if (!this.recordProcess) {
8581
+ this.setState("idle");
8582
+ return null;
8583
+ }
8584
+ this.recordProcess.kill("SIGTERM");
8585
+ this.recordProcess = null;
8586
+ this.setState("idle");
8587
+ await sleep2(200);
8588
+ if (!existsSync17(this.tempFile)) {
8589
+ return null;
8590
+ }
8591
+ try {
8592
+ const text = await this.transcribe();
8593
+ return text || null;
8594
+ } catch (e) {
8595
+ process.stderr.write(`${red2("✗")} Transcription failed: ${e instanceof Error ? e.message : String(e)}
8596
+ `);
8597
+ return null;
8598
+ } finally {
8599
+ try {
8600
+ unlinkSync4(this.tempFile);
8601
+ } catch {}
8602
+ }
8603
+ }
8604
+ transcribe() {
8605
+ return new Promise((resolve2, reject) => {
8606
+ const binary = this.deps.whisperBinary;
8607
+ if (!binary) {
8608
+ process.stderr.write(`${red2("✗")} whisper.cpp binary not found. Please install whisper.cpp and try again.
8609
+ `);
8610
+ reject(new Error("whisper.cpp binary not found. Please install whisper.cpp and try again."));
8611
+ return;
8612
+ }
8613
+ const modelPath = getWhisperModelPath();
8614
+ const args = [
8615
+ "-m",
8616
+ modelPath,
8617
+ "-f",
8618
+ this.tempFile,
8619
+ "--no-timestamps",
8620
+ "--language",
8621
+ WHISPER_MODEL.endsWith(".en") ? "en" : "auto"
8622
+ ];
8623
+ const proc = spawn6(binary, args, {
8624
+ stdio: ["pipe", "pipe", "pipe"]
8625
+ });
8626
+ let stdout = "";
8627
+ let stderr = "";
8628
+ proc.stdout?.on("data", (data) => {
8629
+ stdout += data.toString();
8630
+ });
8631
+ proc.stderr?.on("data", (data) => {
8632
+ stderr += data.toString();
8633
+ });
8634
+ proc.on("error", (err) => {
8635
+ reject(new Error(`whisper.cpp failed to start: ${err.message}`));
8636
+ });
8637
+ proc.on("close", (code) => {
8638
+ if (code !== 0) {
8639
+ reject(new Error(`whisper.cpp exited with code ${code}: ${stderr.trim()}`));
8640
+ return;
8641
+ }
8642
+ const text = stdout.split(`
8643
+ `).map((line) => line.replace(/^\[.*?\]\s*/, "").trim()).filter((line) => line.length > 0).join(" ").trim();
8644
+ resolve2(text);
8645
+ });
8646
+ });
8647
+ }
8648
+ cancel() {
8649
+ if (this.recordProcess) {
8650
+ this.recordProcess.kill("SIGKILL");
8651
+ this.recordProcess = null;
8652
+ }
8653
+ try {
8654
+ unlinkSync4(this.tempFile);
8655
+ } catch {}
8656
+ this.setState("idle");
8657
+ }
8658
+ setState(state) {
8659
+ this.state = state;
8660
+ this.onStateChange(state);
8661
+ }
8662
+ }
8663
+ function voiceStatusIndicator(state) {
8664
+ switch (state) {
8665
+ case "recording":
8666
+ return `${red2(bold2("[REC]"))} `;
8667
+ case "transcribing":
8668
+ return ` ${yellow2("[...]")} `;
8669
+ default:
8670
+ return "";
8671
+ }
8672
+ }
8673
+ function sleep2(ms) {
8674
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
8675
+ }
8676
+ var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
8677
+ var init_voice = __esm(() => {
8678
+ init_terminal();
8679
+ WHISPER_MODELS_DIR = join17(homedir4(), ".locus", "whisper-models");
8680
+ LOCUS_BIN_DIR = join17(homedir4(), ".locus", "bin");
8681
+ });
8682
+
8113
8683
  // src/repl/repl.ts
8114
- import { execSync as execSync9 } from "node:child_process";
8684
+ import { execSync as execSync10 } from "node:child_process";
8115
8685
  async function startRepl(options) {
8116
8686
  const { projectRoot, config } = options;
8117
8687
  const sessionManager = new SessionManager(projectRoot);
@@ -8129,7 +8699,7 @@ async function startRepl(options) {
8129
8699
  } else {
8130
8700
  let branch = "main";
8131
8701
  try {
8132
- branch = execSync9("git rev-parse --abbrev-ref HEAD", {
8702
+ branch = execSync10("git rev-parse --abbrev-ref HEAD", {
8133
8703
  cwd: projectRoot,
8134
8704
  encoding: "utf-8",
8135
8705
  stdio: ["pipe", "pipe", "pipe"]
@@ -8195,8 +8765,15 @@ async function runInteractiveRepl(session, sessionManager, options) {
8195
8765
  new SlashCommandCompletion(getAllCommandNames()),
8196
8766
  new FilePathCompletion(projectRoot)
8197
8767
  ]);
8768
+ const basePrompt = `${cyan2("locus")} ${dim2(">")} `;
8769
+ const voice = new VoiceController({
8770
+ onStateChange: (state) => {
8771
+ const indicator = voiceStatusIndicator(state);
8772
+ input.setPrompt(indicator ? `${indicator}${basePrompt}` : basePrompt);
8773
+ }
8774
+ });
8198
8775
  const input = new InputHandler({
8199
- prompt: `${cyan2("locus")} ${dim2(">")} `,
8776
+ prompt: basePrompt,
8200
8777
  getHistory: () => history.getEntries(),
8201
8778
  onTab: (text) => completion.complete(text)
8202
8779
  });
@@ -8249,12 +8826,33 @@ async function runInteractiveRepl(session, sessionManager, options) {
8249
8826
  onVerboseToggle: () => {
8250
8827
  verbose = !verbose;
8251
8828
  },
8252
- getVerbose: () => verbose
8829
+ getVerbose: () => verbose,
8830
+ onVoiceToggle: () => {
8831
+ if (voice.getState() !== "idle")
8832
+ return;
8833
+ const started = voice.startRecording();
8834
+ if (started) {
8835
+ process.stderr.write(`${dim2("Recording... press")} ${bold2("Enter")} ${dim2("to stop")}
8836
+ `);
8837
+ }
8838
+ }
8253
8839
  };
8254
8840
  while (!shouldExit) {
8255
8841
  const result = await input.readline();
8256
8842
  switch (result.type) {
8257
8843
  case "submit": {
8844
+ if (voice.getState() === "recording") {
8845
+ process.stderr.write(`${dim2("Transcribing...")}
8846
+ `);
8847
+ const transcribed = await voice.stopAndTranscribe();
8848
+ if (transcribed) {
8849
+ input.setInitialBuffer(transcribed);
8850
+ } else {
8851
+ process.stderr.write(`${yellow2("!")} No speech detected.
8852
+ `);
8853
+ }
8854
+ continue;
8855
+ }
8258
8856
  const text = result.text.trim();
8259
8857
  if (!text)
8260
8858
  continue;
@@ -8305,6 +8903,7 @@ ${red2("✗")} ${msg}
8305
8903
  break;
8306
8904
  }
8307
8905
  }
8906
+ voice.cancel();
8308
8907
  const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
8309
8908
  if (shouldPersistOnExit) {
8310
8909
  sessionManager.save(session);
@@ -8370,7 +8969,7 @@ function printWelcome(session) {
8370
8969
  }
8371
8970
  process.stderr.write(`
8372
8971
  `);
8373
- process.stderr.write(` ${dim2("Type /help for commands, Shift+Enter for newline, Ctrl+C twice to exit")}
8972
+ process.stderr.write(` ${dim2("Type /help for commands, Shift+Enter for newline, /v for voice input")}
8374
8973
  `);
8375
8974
  process.stderr.write(`
8376
8975
  `);
@@ -8390,6 +8989,7 @@ var init_repl = __esm(() => {
8390
8989
  init_input_history();
8391
8990
  init_model_config();
8392
8991
  init_session_manager();
8992
+ init_voice();
8393
8993
  LOCUS_LOGO = [
8394
8994
  " ▄█ ",
8395
8995
  " ▄▄████▄▄▄▄ ",
@@ -8587,11 +9187,11 @@ var init_exec = __esm(() => {
8587
9187
  });
8588
9188
 
8589
9189
  // src/core/submodule.ts
8590
- import { execSync as execSync10 } from "node:child_process";
8591
- import { existsSync as existsSync17 } from "node:fs";
8592
- import { join as join17 } from "node:path";
9190
+ import { execSync as execSync11 } from "node:child_process";
9191
+ import { existsSync as existsSync18 } from "node:fs";
9192
+ import { join as join18 } from "node:path";
8593
9193
  function git2(args, cwd) {
8594
- return execSync10(`git ${args}`, {
9194
+ return execSync11(`git ${args}`, {
8595
9195
  cwd,
8596
9196
  encoding: "utf-8",
8597
9197
  stdio: ["pipe", "pipe", "pipe"]
@@ -8605,7 +9205,7 @@ function gitSafe(args, cwd) {
8605
9205
  }
8606
9206
  }
8607
9207
  function hasSubmodules(cwd) {
8608
- return existsSync17(join17(cwd, ".gitmodules"));
9208
+ return existsSync18(join18(cwd, ".gitmodules"));
8609
9209
  }
8610
9210
  function listSubmodules(cwd) {
8611
9211
  if (!hasSubmodules(cwd))
@@ -8625,7 +9225,7 @@ function listSubmodules(cwd) {
8625
9225
  continue;
8626
9226
  submodules.push({
8627
9227
  path,
8628
- absolutePath: join17(cwd, path),
9228
+ absolutePath: join18(cwd, path),
8629
9229
  dirty
8630
9230
  });
8631
9231
  }
@@ -8638,7 +9238,7 @@ function getDirtySubmodules(cwd) {
8638
9238
  const submodules = listSubmodules(cwd);
8639
9239
  const dirty = [];
8640
9240
  for (const sub of submodules) {
8641
- if (!existsSync17(sub.absolutePath))
9241
+ if (!existsSync18(sub.absolutePath))
8642
9242
  continue;
8643
9243
  const status = gitSafe("status --porcelain", sub.absolutePath);
8644
9244
  if (status && status.trim().length > 0) {
@@ -8659,7 +9259,7 @@ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
8659
9259
  const message = `chore: complete #${issueNumber} - ${issueTitle}
8660
9260
 
8661
9261
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
8662
- execSync10("git commit -F -", {
9262
+ execSync11("git commit -F -", {
8663
9263
  input: message,
8664
9264
  cwd: sub.absolutePath,
8665
9265
  encoding: "utf-8",
@@ -8725,7 +9325,7 @@ function pushSubmoduleBranches(cwd) {
8725
9325
  const log = getLogger();
8726
9326
  const submodules = listSubmodules(cwd);
8727
9327
  for (const sub of submodules) {
8728
- if (!existsSync17(sub.absolutePath))
9328
+ if (!existsSync18(sub.absolutePath))
8729
9329
  continue;
8730
9330
  const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
8731
9331
  if (!branch || branch === "HEAD")
@@ -8746,7 +9346,7 @@ var init_submodule = __esm(() => {
8746
9346
  });
8747
9347
 
8748
9348
  // src/core/agent.ts
8749
- import { execSync as execSync11 } from "node:child_process";
9349
+ import { execSync as execSync12 } from "node:child_process";
8750
9350
  async function executeIssue(projectRoot, options) {
8751
9351
  const log = getLogger();
8752
9352
  const timer = createTimer();
@@ -8775,7 +9375,7 @@ ${cyan2("●")} ${bold2(`#${issueNumber}`)} ${issue.title}
8775
9375
  }
8776
9376
  let issueComments = [];
8777
9377
  try {
8778
- const commentsRaw = execSync11(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
9378
+ const commentsRaw = execSync12(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
8779
9379
  if (commentsRaw) {
8780
9380
  issueComments = commentsRaw.split(`
8781
9381
  `).filter(Boolean);
@@ -8939,12 +9539,12 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
8939
9539
  }
8940
9540
  async function createIssuePR(projectRoot, config, issue) {
8941
9541
  try {
8942
- const currentBranch = execSync11("git rev-parse --abbrev-ref HEAD", {
9542
+ const currentBranch = execSync12("git rev-parse --abbrev-ref HEAD", {
8943
9543
  cwd: projectRoot,
8944
9544
  encoding: "utf-8",
8945
9545
  stdio: ["pipe", "pipe", "pipe"]
8946
9546
  }).trim();
8947
- const diff = execSync11(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9547
+ const diff = execSync12(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8948
9548
  cwd: projectRoot,
8949
9549
  encoding: "utf-8",
8950
9550
  stdio: ["pipe", "pipe", "pipe"]
@@ -8954,7 +9554,7 @@ async function createIssuePR(projectRoot, config, issue) {
8954
9554
  return;
8955
9555
  }
8956
9556
  pushSubmoduleBranches(projectRoot);
8957
- execSync11(`git push -u origin ${currentBranch}`, {
9557
+ execSync12(`git push -u origin ${currentBranch}`, {
8958
9558
  cwd: projectRoot,
8959
9559
  encoding: "utf-8",
8960
9560
  stdio: ["pipe", "pipe", "pipe"]
@@ -9009,9 +9609,9 @@ var init_agent = __esm(() => {
9009
9609
  });
9010
9610
 
9011
9611
  // src/core/conflict.ts
9012
- import { execSync as execSync12 } from "node:child_process";
9612
+ import { execSync as execSync13 } from "node:child_process";
9013
9613
  function git3(args, cwd) {
9014
- return execSync12(`git ${args}`, {
9614
+ return execSync13(`git ${args}`, {
9015
9615
  cwd,
9016
9616
  encoding: "utf-8",
9017
9617
  stdio: ["pipe", "pipe", "pipe"]
@@ -9140,19 +9740,19 @@ var init_conflict = __esm(() => {
9140
9740
 
9141
9741
  // src/core/run-state.ts
9142
9742
  import {
9143
- existsSync as existsSync18,
9144
- mkdirSync as mkdirSync12,
9743
+ existsSync as existsSync19,
9744
+ mkdirSync as mkdirSync13,
9145
9745
  readFileSync as readFileSync12,
9146
- unlinkSync as unlinkSync4,
9746
+ unlinkSync as unlinkSync5,
9147
9747
  writeFileSync as writeFileSync8
9148
9748
  } from "node:fs";
9149
- import { dirname as dirname6, join as join18 } from "node:path";
9749
+ import { dirname as dirname6, join as join19 } from "node:path";
9150
9750
  function getRunStatePath(projectRoot) {
9151
- return join18(projectRoot, ".locus", "run-state.json");
9751
+ return join19(projectRoot, ".locus", "run-state.json");
9152
9752
  }
9153
9753
  function loadRunState(projectRoot) {
9154
9754
  const path = getRunStatePath(projectRoot);
9155
- if (!existsSync18(path))
9755
+ if (!existsSync19(path))
9156
9756
  return null;
9157
9757
  try {
9158
9758
  return JSON.parse(readFileSync12(path, "utf-8"));
@@ -9164,16 +9764,16 @@ function loadRunState(projectRoot) {
9164
9764
  function saveRunState(projectRoot, state) {
9165
9765
  const path = getRunStatePath(projectRoot);
9166
9766
  const dir = dirname6(path);
9167
- if (!existsSync18(dir)) {
9168
- mkdirSync12(dir, { recursive: true });
9767
+ if (!existsSync19(dir)) {
9768
+ mkdirSync13(dir, { recursive: true });
9169
9769
  }
9170
9770
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
9171
9771
  `, "utf-8");
9172
9772
  }
9173
9773
  function clearRunState(projectRoot) {
9174
9774
  const path = getRunStatePath(projectRoot);
9175
- if (existsSync18(path)) {
9176
- unlinkSync4(path);
9775
+ if (existsSync19(path)) {
9776
+ unlinkSync5(path);
9177
9777
  }
9178
9778
  }
9179
9779
  function createSprintRunState(sprint, branch, issues) {
@@ -9312,11 +9912,11 @@ var init_shutdown = __esm(() => {
9312
9912
  });
9313
9913
 
9314
9914
  // src/core/worktree.ts
9315
- import { execSync as execSync13 } from "node:child_process";
9316
- import { existsSync as existsSync19, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
9317
- import { join as join19 } from "node:path";
9915
+ import { execSync as execSync14 } from "node:child_process";
9916
+ import { existsSync as existsSync20, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
9917
+ import { join as join20 } from "node:path";
9318
9918
  function git4(args, cwd) {
9319
- return execSync13(`git ${args}`, {
9919
+ return execSync14(`git ${args}`, {
9320
9920
  cwd,
9321
9921
  encoding: "utf-8",
9322
9922
  stdio: ["pipe", "pipe", "pipe"]
@@ -9330,10 +9930,10 @@ function gitSafe3(args, cwd) {
9330
9930
  }
9331
9931
  }
9332
9932
  function getWorktreeDir(projectRoot) {
9333
- return join19(projectRoot, ".locus", "worktrees");
9933
+ return join20(projectRoot, ".locus", "worktrees");
9334
9934
  }
9335
9935
  function getWorktreePath(projectRoot, issueNumber) {
9336
- return join19(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
9936
+ return join20(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
9337
9937
  }
9338
9938
  function generateBranchName(issueNumber) {
9339
9939
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -9341,7 +9941,7 @@ function generateBranchName(issueNumber) {
9341
9941
  }
9342
9942
  function getWorktreeBranch(worktreePath) {
9343
9943
  try {
9344
- return execSync13("git branch --show-current", {
9944
+ return execSync14("git branch --show-current", {
9345
9945
  cwd: worktreePath,
9346
9946
  encoding: "utf-8",
9347
9947
  stdio: ["pipe", "pipe", "pipe"]
@@ -9353,7 +9953,7 @@ function getWorktreeBranch(worktreePath) {
9353
9953
  function createWorktree(projectRoot, issueNumber, baseBranch) {
9354
9954
  const log = getLogger();
9355
9955
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
9356
- if (existsSync19(worktreePath)) {
9956
+ if (existsSync20(worktreePath)) {
9357
9957
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
9358
9958
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
9359
9959
  return {
@@ -9381,7 +9981,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
9381
9981
  function removeWorktree(projectRoot, issueNumber) {
9382
9982
  const log = getLogger();
9383
9983
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
9384
- if (!existsSync19(worktreePath)) {
9984
+ if (!existsSync20(worktreePath)) {
9385
9985
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
9386
9986
  return;
9387
9987
  }
@@ -9400,7 +10000,7 @@ function removeWorktree(projectRoot, issueNumber) {
9400
10000
  function listWorktrees(projectRoot) {
9401
10001
  const log = getLogger();
9402
10002
  const worktreeDir = getWorktreeDir(projectRoot);
9403
- if (!existsSync19(worktreeDir)) {
10003
+ if (!existsSync20(worktreeDir)) {
9404
10004
  return [];
9405
10005
  }
9406
10006
  const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
@@ -9420,7 +10020,7 @@ function listWorktrees(projectRoot) {
9420
10020
  if (!match)
9421
10021
  continue;
9422
10022
  const issueNumber = Number.parseInt(match[1], 10);
9423
- const path = join19(worktreeDir, entry);
10023
+ const path = join20(worktreeDir, entry);
9424
10024
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
9425
10025
  let resolvedPath;
9426
10026
  try {
@@ -9468,7 +10068,7 @@ var exports_run = {};
9468
10068
  __export(exports_run, {
9469
10069
  runCommand: () => runCommand
9470
10070
  });
9471
- import { execSync as execSync14 } from "node:child_process";
10071
+ import { execSync as execSync15 } from "node:child_process";
9472
10072
  function resolveExecutionContext(config, modelOverride) {
9473
10073
  const model = modelOverride ?? config.ai.model;
9474
10074
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -9628,7 +10228,7 @@ ${yellow2("⚠")} A sprint run is already in progress.
9628
10228
  }
9629
10229
  if (!flags.dryRun) {
9630
10230
  try {
9631
- execSync14(`git checkout -B ${branchName}`, {
10231
+ execSync15(`git checkout -B ${branchName}`, {
9632
10232
  cwd: projectRoot,
9633
10233
  encoding: "utf-8",
9634
10234
  stdio: ["pipe", "pipe", "pipe"]
@@ -9678,7 +10278,7 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
9678
10278
  let sprintContext;
9679
10279
  if (i > 0 && !flags.dryRun) {
9680
10280
  try {
9681
- sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
10281
+ sprintContext = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9682
10282
  cwd: projectRoot,
9683
10283
  encoding: "utf-8",
9684
10284
  stdio: ["pipe", "pipe", "pipe"]
@@ -9743,7 +10343,7 @@ ${bold2("Summary:")}
9743
10343
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9744
10344
  if (prNumber !== undefined) {
9745
10345
  try {
9746
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10346
+ execSync15(`git checkout ${config.agent.baseBranch}`, {
9747
10347
  cwd: projectRoot,
9748
10348
  encoding: "utf-8",
9749
10349
  stdio: ["pipe", "pipe", "pipe"]
@@ -9788,7 +10388,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
9788
10388
  `);
9789
10389
  if (!flags.dryRun) {
9790
10390
  try {
9791
- execSync14(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
10391
+ execSync15(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
9792
10392
  cwd: projectRoot,
9793
10393
  encoding: "utf-8",
9794
10394
  stdio: ["pipe", "pipe", "pipe"]
@@ -9813,7 +10413,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
9813
10413
  if (!flags.dryRun) {
9814
10414
  if (result.success) {
9815
10415
  try {
9816
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10416
+ execSync15(`git checkout ${config.agent.baseBranch}`, {
9817
10417
  cwd: projectRoot,
9818
10418
  encoding: "utf-8",
9819
10419
  stdio: ["pipe", "pipe", "pipe"]
@@ -9950,13 +10550,13 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
9950
10550
  `);
9951
10551
  if (state.type === "sprint" && state.branch) {
9952
10552
  try {
9953
- const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
10553
+ const currentBranch = execSync15("git rev-parse --abbrev-ref HEAD", {
9954
10554
  cwd: projectRoot,
9955
10555
  encoding: "utf-8",
9956
10556
  stdio: ["pipe", "pipe", "pipe"]
9957
10557
  }).trim();
9958
10558
  if (currentBranch !== state.branch) {
9959
- execSync14(`git checkout ${state.branch}`, {
10559
+ execSync15(`git checkout ${state.branch}`, {
9960
10560
  cwd: projectRoot,
9961
10561
  encoding: "utf-8",
9962
10562
  stdio: ["pipe", "pipe", "pipe"]
@@ -10023,7 +10623,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
10023
10623
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
10024
10624
  if (prNumber !== undefined) {
10025
10625
  try {
10026
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10626
+ execSync15(`git checkout ${config.agent.baseBranch}`, {
10027
10627
  cwd: projectRoot,
10028
10628
  encoding: "utf-8",
10029
10629
  stdio: ["pipe", "pipe", "pipe"]
@@ -10059,14 +10659,14 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
10059
10659
  process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
10060
10660
  `);
10061
10661
  }
10062
- const status = execSync14("git status --porcelain", {
10662
+ const status = execSync15("git status --porcelain", {
10063
10663
  cwd: projectRoot,
10064
10664
  encoding: "utf-8",
10065
10665
  stdio: ["pipe", "pipe", "pipe"]
10066
10666
  }).trim();
10067
10667
  if (!status)
10068
10668
  return;
10069
- execSync14("git add -A", {
10669
+ execSync15("git add -A", {
10070
10670
  cwd: projectRoot,
10071
10671
  encoding: "utf-8",
10072
10672
  stdio: ["pipe", "pipe", "pipe"]
@@ -10074,7 +10674,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
10074
10674
  const message = `chore: complete #${issueNumber} - ${issueTitle}
10075
10675
 
10076
10676
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
10077
- execSync14(`git commit -F -`, {
10677
+ execSync15(`git commit -F -`, {
10078
10678
  input: message,
10079
10679
  cwd: projectRoot,
10080
10680
  encoding: "utf-8",
@@ -10088,7 +10688,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
10088
10688
  if (!config.agent.autoPR)
10089
10689
  return;
10090
10690
  try {
10091
- const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
10691
+ const diff = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
10092
10692
  cwd: projectRoot,
10093
10693
  encoding: "utf-8",
10094
10694
  stdio: ["pipe", "pipe", "pipe"]
@@ -10099,7 +10699,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
10099
10699
  return;
10100
10700
  }
10101
10701
  pushSubmoduleBranches(projectRoot);
10102
- execSync14(`git push -u origin ${branchName}`, {
10702
+ execSync15(`git push -u origin ${branchName}`, {
10103
10703
  cwd: projectRoot,
10104
10704
  encoding: "utf-8",
10105
10705
  stdio: ["pipe", "pipe", "pipe"]
@@ -10257,13 +10857,13 @@ __export(exports_plan, {
10257
10857
  parsePlanArgs: () => parsePlanArgs
10258
10858
  });
10259
10859
  import {
10260
- existsSync as existsSync20,
10261
- mkdirSync as mkdirSync13,
10860
+ existsSync as existsSync21,
10861
+ mkdirSync as mkdirSync14,
10262
10862
  readdirSync as readdirSync8,
10263
10863
  readFileSync as readFileSync13,
10264
10864
  writeFileSync as writeFileSync9
10265
10865
  } from "node:fs";
10266
- import { join as join20 } from "node:path";
10866
+ import { join as join21 } from "node:path";
10267
10867
  function printHelp() {
10268
10868
  process.stderr.write(`
10269
10869
  ${bold2("locus plan")} — AI-powered sprint planning
@@ -10294,12 +10894,12 @@ function normalizeSprintName(name) {
10294
10894
  return name.trim().toLowerCase();
10295
10895
  }
10296
10896
  function getPlansDir(projectRoot) {
10297
- return join20(projectRoot, ".locus", "plans");
10897
+ return join21(projectRoot, ".locus", "plans");
10298
10898
  }
10299
10899
  function ensurePlansDir(projectRoot) {
10300
10900
  const dir = getPlansDir(projectRoot);
10301
- if (!existsSync20(dir)) {
10302
- mkdirSync13(dir, { recursive: true });
10901
+ if (!existsSync21(dir)) {
10902
+ mkdirSync14(dir, { recursive: true });
10303
10903
  }
10304
10904
  return dir;
10305
10905
  }
@@ -10308,14 +10908,14 @@ function generateId() {
10308
10908
  }
10309
10909
  function loadPlanFile(projectRoot, id) {
10310
10910
  const dir = getPlansDir(projectRoot);
10311
- if (!existsSync20(dir))
10911
+ if (!existsSync21(dir))
10312
10912
  return null;
10313
10913
  const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
10314
10914
  const match = files.find((f) => f.startsWith(id));
10315
10915
  if (!match)
10316
10916
  return null;
10317
10917
  try {
10318
- const content = readFileSync13(join20(dir, match), "utf-8");
10918
+ const content = readFileSync13(join21(dir, match), "utf-8");
10319
10919
  return JSON.parse(content);
10320
10920
  } catch {
10321
10921
  return null;
@@ -10361,7 +10961,7 @@ async function planCommand(projectRoot, args, flags = {}) {
10361
10961
  }
10362
10962
  function handleListPlans(projectRoot) {
10363
10963
  const dir = getPlansDir(projectRoot);
10364
- if (!existsSync20(dir)) {
10964
+ if (!existsSync21(dir)) {
10365
10965
  process.stderr.write(`${dim2("No saved plans yet.")}
10366
10966
  `);
10367
10967
  return;
@@ -10379,7 +10979,7 @@ ${bold2("Saved Plans:")}
10379
10979
  for (const file of files) {
10380
10980
  const id = file.replace(".json", "");
10381
10981
  try {
10382
- const content = readFileSync13(join20(dir, file), "utf-8");
10982
+ const content = readFileSync13(join21(dir, file), "utf-8");
10383
10983
  const plan = JSON.parse(content);
10384
10984
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
10385
10985
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -10490,7 +11090,7 @@ ${bold2("Approving plan:")}
10490
11090
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
10491
11091
  const id = generateId();
10492
11092
  const plansDir = ensurePlansDir(projectRoot);
10493
- const planPath = join20(plansDir, `${id}.json`);
11093
+ const planPath = join21(plansDir, `${id}.json`);
10494
11094
  const planPathRelative = `.locus/plans/${id}.json`;
10495
11095
  const displayDirective = directive;
10496
11096
  process.stderr.write(`
@@ -10532,7 +11132,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
10532
11132
  `);
10533
11133
  return;
10534
11134
  }
10535
- if (!existsSync20(planPath)) {
11135
+ if (!existsSync21(planPath)) {
10536
11136
  process.stderr.write(`
10537
11137
  ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
10538
11138
  `);
@@ -10713,15 +11313,15 @@ ${directive}${sprintName ? `
10713
11313
 
10714
11314
  **Sprint:** ${sprintName}` : ""}
10715
11315
  </directive>`);
10716
- const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
10717
- if (existsSync20(locusPath)) {
11316
+ const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
11317
+ if (existsSync21(locusPath)) {
10718
11318
  const content = readFileSync13(locusPath, "utf-8");
10719
11319
  parts.push(`<project-context>
10720
11320
  ${content.slice(0, 3000)}
10721
11321
  </project-context>`);
10722
11322
  }
10723
- const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
10724
- if (existsSync20(learningsPath)) {
11323
+ const learningsPath = join21(projectRoot, ".locus", "LEARNINGS.md");
11324
+ if (existsSync21(learningsPath)) {
10725
11325
  const content = readFileSync13(learningsPath, "utf-8");
10726
11326
  parts.push(`<past-learnings>
10727
11327
  ${content.slice(0, 2000)}
@@ -10901,9 +11501,9 @@ var exports_review = {};
10901
11501
  __export(exports_review, {
10902
11502
  reviewCommand: () => reviewCommand
10903
11503
  });
10904
- import { execFileSync as execFileSync2, execSync as execSync15 } from "node:child_process";
10905
- import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
10906
- import { join as join21 } from "node:path";
11504
+ import { execFileSync as execFileSync2, execSync as execSync16 } from "node:child_process";
11505
+ import { existsSync as existsSync22, readFileSync as readFileSync14 } from "node:fs";
11506
+ import { join as join22 } from "node:path";
10907
11507
  function printHelp2() {
10908
11508
  process.stderr.write(`
10909
11509
  ${bold2("locus review")} — AI-powered code review
@@ -10987,7 +11587,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
10987
11587
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10988
11588
  let prInfo;
10989
11589
  try {
10990
- const result = execSync15(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11590
+ const result = execSync16(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
10991
11591
  const raw = JSON.parse(result);
10992
11592
  prInfo = {
10993
11593
  number: raw.number,
@@ -11071,8 +11671,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
11071
11671
  parts.push(`<role>
11072
11672
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
11073
11673
  </role>`);
11074
- const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
11075
- if (existsSync21(locusPath)) {
11674
+ const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11675
+ if (existsSync22(locusPath)) {
11076
11676
  const content = readFileSync14(locusPath, "utf-8");
11077
11677
  parts.push(`<project-context>
11078
11678
  ${content.slice(0, 2000)}
@@ -11134,7 +11734,7 @@ var exports_iterate = {};
11134
11734
  __export(exports_iterate, {
11135
11735
  iterateCommand: () => iterateCommand
11136
11736
  });
11137
- import { execSync as execSync16 } from "node:child_process";
11737
+ import { execSync as execSync17 } from "node:child_process";
11138
11738
  function printHelp3() {
11139
11739
  process.stderr.write(`
11140
11740
  ${bold2("locus iterate")} — Re-execute tasks with PR feedback
@@ -11352,12 +11952,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
11352
11952
  }
11353
11953
  function findPRForIssue(projectRoot, issueNumber) {
11354
11954
  try {
11355
- const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11955
+ const result = execSync17(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11356
11956
  const parsed = JSON.parse(result);
11357
11957
  if (parsed.length > 0) {
11358
11958
  return parsed[0].number;
11359
11959
  }
11360
- const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11960
+ const branchResult = execSync17(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
11361
11961
  const branchParsed = JSON.parse(branchResult);
11362
11962
  if (branchParsed.length > 0) {
11363
11963
  return branchParsed[0].number;
@@ -11393,14 +11993,14 @@ __export(exports_discuss, {
11393
11993
  discussCommand: () => discussCommand
11394
11994
  });
11395
11995
  import {
11396
- existsSync as existsSync22,
11397
- mkdirSync as mkdirSync14,
11996
+ existsSync as existsSync23,
11997
+ mkdirSync as mkdirSync15,
11398
11998
  readdirSync as readdirSync9,
11399
11999
  readFileSync as readFileSync15,
11400
- unlinkSync as unlinkSync5,
12000
+ unlinkSync as unlinkSync6,
11401
12001
  writeFileSync as writeFileSync10
11402
12002
  } from "node:fs";
11403
- import { join as join22 } from "node:path";
12003
+ import { join as join23 } from "node:path";
11404
12004
  function printHelp4() {
11405
12005
  process.stderr.write(`
11406
12006
  ${bold2("locus discuss")} — AI-powered architectural discussions
@@ -11422,12 +12022,12 @@ ${bold2("Examples:")}
11422
12022
  `);
11423
12023
  }
11424
12024
  function getDiscussionsDir(projectRoot) {
11425
- return join22(projectRoot, ".locus", "discussions");
12025
+ return join23(projectRoot, ".locus", "discussions");
11426
12026
  }
11427
12027
  function ensureDiscussionsDir(projectRoot) {
11428
12028
  const dir = getDiscussionsDir(projectRoot);
11429
- if (!existsSync22(dir)) {
11430
- mkdirSync14(dir, { recursive: true });
12029
+ if (!existsSync23(dir)) {
12030
+ mkdirSync15(dir, { recursive: true });
11431
12031
  }
11432
12032
  return dir;
11433
12033
  }
@@ -11461,7 +12061,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
11461
12061
  }
11462
12062
  function listDiscussions(projectRoot) {
11463
12063
  const dir = getDiscussionsDir(projectRoot);
11464
- if (!existsSync22(dir)) {
12064
+ if (!existsSync23(dir)) {
11465
12065
  process.stderr.write(`${dim2("No discussions yet.")}
11466
12066
  `);
11467
12067
  return;
@@ -11478,7 +12078,7 @@ ${bold2("Discussions:")}
11478
12078
  `);
11479
12079
  for (const file of files) {
11480
12080
  const id = file.replace(".md", "");
11481
- const content = readFileSync15(join22(dir, file), "utf-8");
12081
+ const content = readFileSync15(join23(dir, file), "utf-8");
11482
12082
  const titleMatch = content.match(/^#\s+(.+)/m);
11483
12083
  const title = titleMatch ? titleMatch[1] : id;
11484
12084
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -11496,7 +12096,7 @@ function showDiscussion(projectRoot, id) {
11496
12096
  return;
11497
12097
  }
11498
12098
  const dir = getDiscussionsDir(projectRoot);
11499
- if (!existsSync22(dir)) {
12099
+ if (!existsSync23(dir)) {
11500
12100
  process.stderr.write(`${red2("✗")} No discussions found.
11501
12101
  `);
11502
12102
  return;
@@ -11508,7 +12108,7 @@ function showDiscussion(projectRoot, id) {
11508
12108
  `);
11509
12109
  return;
11510
12110
  }
11511
- const content = readFileSync15(join22(dir, match), "utf-8");
12111
+ const content = readFileSync15(join23(dir, match), "utf-8");
11512
12112
  process.stdout.write(`${content}
11513
12113
  `);
11514
12114
  }
@@ -11519,7 +12119,7 @@ function deleteDiscussion(projectRoot, id) {
11519
12119
  return;
11520
12120
  }
11521
12121
  const dir = getDiscussionsDir(projectRoot);
11522
- if (!existsSync22(dir)) {
12122
+ if (!existsSync23(dir)) {
11523
12123
  process.stderr.write(`${red2("✗")} No discussions found.
11524
12124
  `);
11525
12125
  return;
@@ -11531,7 +12131,7 @@ function deleteDiscussion(projectRoot, id) {
11531
12131
  `);
11532
12132
  return;
11533
12133
  }
11534
- unlinkSync5(join22(dir, match));
12134
+ unlinkSync6(join23(dir, match));
11535
12135
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
11536
12136
  `);
11537
12137
  }
@@ -11544,7 +12144,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
11544
12144
  return;
11545
12145
  }
11546
12146
  const dir = getDiscussionsDir(projectRoot);
11547
- if (!existsSync22(dir)) {
12147
+ if (!existsSync23(dir)) {
11548
12148
  process.stderr.write(`${red2("✗")} No discussions found.
11549
12149
  `);
11550
12150
  return;
@@ -11556,7 +12156,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
11556
12156
  `);
11557
12157
  return;
11558
12158
  }
11559
- const content = readFileSync15(join22(dir, match), "utf-8");
12159
+ const content = readFileSync15(join23(dir, match), "utf-8");
11560
12160
  const titleMatch = content.match(/^#\s+(.+)/m);
11561
12161
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
11562
12162
  await planCommand(projectRoot, [
@@ -11681,7 +12281,7 @@ ${turn.content}`;
11681
12281
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
11682
12282
  ].join(`
11683
12283
  `);
11684
- writeFileSync10(join22(dir, `${id}.md`), markdown, "utf-8");
12284
+ writeFileSync10(join23(dir, `${id}.md`), markdown, "utf-8");
11685
12285
  process.stderr.write(`
11686
12286
  ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
11687
12287
  `);
@@ -11696,15 +12296,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
11696
12296
  parts.push(`<role>
11697
12297
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
11698
12298
  </role>`);
11699
- const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11700
- if (existsSync22(locusPath)) {
12299
+ const locusPath = join23(projectRoot, ".locus", "LOCUS.md");
12300
+ if (existsSync23(locusPath)) {
11701
12301
  const content = readFileSync15(locusPath, "utf-8");
11702
12302
  parts.push(`<project-context>
11703
12303
  ${content.slice(0, 3000)}
11704
12304
  </project-context>`);
11705
12305
  }
11706
- const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
11707
- if (existsSync22(learningsPath)) {
12306
+ const learningsPath = join23(projectRoot, ".locus", "LEARNINGS.md");
12307
+ if (existsSync23(learningsPath)) {
11708
12308
  const content = readFileSync15(learningsPath, "utf-8");
11709
12309
  parts.push(`<past-learnings>
11710
12310
  ${content.slice(0, 2000)}
@@ -11776,8 +12376,8 @@ __export(exports_artifacts, {
11776
12376
  formatDate: () => formatDate2,
11777
12377
  artifactsCommand: () => artifactsCommand
11778
12378
  });
11779
- import { existsSync as existsSync23, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
11780
- import { join as join23 } from "node:path";
12379
+ import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
12380
+ import { join as join24 } from "node:path";
11781
12381
  function printHelp5() {
11782
12382
  process.stderr.write(`
11783
12383
  ${bold2("locus artifacts")} — View and manage AI-generated artifacts
@@ -11797,14 +12397,14 @@ ${dim2("Artifact names support partial matching.")}
11797
12397
  `);
11798
12398
  }
11799
12399
  function getArtifactsDir(projectRoot) {
11800
- return join23(projectRoot, ".locus", "artifacts");
12400
+ return join24(projectRoot, ".locus", "artifacts");
11801
12401
  }
11802
12402
  function listArtifacts(projectRoot) {
11803
12403
  const dir = getArtifactsDir(projectRoot);
11804
- if (!existsSync23(dir))
12404
+ if (!existsSync24(dir))
11805
12405
  return [];
11806
12406
  return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
11807
- const filePath = join23(dir, fileName);
12407
+ const filePath = join24(dir, fileName);
11808
12408
  const stat = statSync5(filePath);
11809
12409
  return {
11810
12410
  name: fileName.replace(/\.md$/, ""),
@@ -11817,8 +12417,8 @@ function listArtifacts(projectRoot) {
11817
12417
  function readArtifact(projectRoot, name) {
11818
12418
  const dir = getArtifactsDir(projectRoot);
11819
12419
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
11820
- const filePath = join23(dir, fileName);
11821
- if (!existsSync23(filePath))
12420
+ const filePath = join24(dir, fileName);
12421
+ if (!existsSync24(filePath))
11822
12422
  return null;
11823
12423
  const stat = statSync5(filePath);
11824
12424
  return {
@@ -11987,10 +12587,10 @@ __export(exports_sandbox2, {
11987
12587
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
11988
12588
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
11989
12589
  });
11990
- import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
12590
+ import { execSync as execSync18, spawn as spawn7 } from "node:child_process";
11991
12591
  import { createHash } from "node:crypto";
11992
- import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
11993
- import { basename as basename4, join as join24 } from "node:path";
12592
+ import { existsSync as existsSync25, readFileSync as readFileSync17 } from "node:fs";
12593
+ import { basename as basename4, join as join25 } from "node:path";
11994
12594
  import { createInterface as createInterface3 } from "node:readline";
11995
12595
  function printSandboxHelp() {
11996
12596
  process.stderr.write(`
@@ -12155,7 +12755,7 @@ async function handleAgentLogin(projectRoot, agent) {
12155
12755
  process.stderr.write(`${dim2("Login and then exit when ready.")}
12156
12756
 
12157
12757
  `);
12158
- const child = spawn6("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
12758
+ const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
12159
12759
  stdio: "inherit"
12160
12760
  });
12161
12761
  await new Promise((resolve2) => {
@@ -12199,7 +12799,7 @@ function handleRemove(projectRoot) {
12199
12799
  process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
12200
12800
  `);
12201
12801
  try {
12202
- execSync17(`docker sandbox rm ${sandboxName}`, {
12802
+ execSync18(`docker sandbox rm ${sandboxName}`, {
12203
12803
  encoding: "utf-8",
12204
12804
  stdio: ["pipe", "pipe", "pipe"],
12205
12805
  timeout: 15000
@@ -12446,9 +13046,9 @@ async function handleLogs(projectRoot, args) {
12446
13046
  dockerArgs.push(sandboxName);
12447
13047
  await runInteractiveCommand("docker", dockerArgs);
12448
13048
  }
12449
- function detectPackageManager(projectRoot) {
13049
+ function detectPackageManager2(projectRoot) {
12450
13050
  try {
12451
- const raw = readFileSync17(join24(projectRoot, "package.json"), "utf-8");
13051
+ const raw = readFileSync17(join25(projectRoot, "package.json"), "utf-8");
12452
13052
  const pkgJson = JSON.parse(raw);
12453
13053
  if (typeof pkgJson.packageManager === "string") {
12454
13054
  const name = pkgJson.packageManager.split("@")[0];
@@ -12457,13 +13057,13 @@ function detectPackageManager(projectRoot) {
12457
13057
  }
12458
13058
  }
12459
13059
  } catch {}
12460
- if (existsSync24(join24(projectRoot, "bun.lock")) || existsSync24(join24(projectRoot, "bun.lockb"))) {
13060
+ if (existsSync25(join25(projectRoot, "bun.lock")) || existsSync25(join25(projectRoot, "bun.lockb"))) {
12461
13061
  return "bun";
12462
13062
  }
12463
- if (existsSync24(join24(projectRoot, "yarn.lock"))) {
13063
+ if (existsSync25(join25(projectRoot, "yarn.lock"))) {
12464
13064
  return "yarn";
12465
13065
  }
12466
- if (existsSync24(join24(projectRoot, "pnpm-lock.yaml"))) {
13066
+ if (existsSync25(join25(projectRoot, "pnpm-lock.yaml"))) {
12467
13067
  return "pnpm";
12468
13068
  }
12469
13069
  return "npm";
@@ -12484,7 +13084,7 @@ async function runSandboxSetup(sandboxName, projectRoot) {
12484
13084
  const ecosystem = detectProjectEcosystem(projectRoot);
12485
13085
  const isJS = isJavaScriptEcosystem(ecosystem);
12486
13086
  if (isJS) {
12487
- const pm = detectPackageManager(projectRoot);
13087
+ const pm = detectPackageManager2(projectRoot);
12488
13088
  if (pm !== "npm") {
12489
13089
  await ensurePackageManagerInSandbox(sandboxName, pm);
12490
13090
  }
@@ -12512,8 +13112,8 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
12512
13112
  ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
12513
13113
  `);
12514
13114
  }
12515
- const setupScript = join24(projectRoot, ".locus", "sandbox-setup.sh");
12516
- if (existsSync24(setupScript)) {
13115
+ const setupScript = join25(projectRoot, ".locus", "sandbox-setup.sh");
13116
+ if (existsSync25(setupScript)) {
12517
13117
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
12518
13118
  `);
12519
13119
  const hookOk = await runInteractiveCommand("docker", [
@@ -12594,14 +13194,14 @@ function getActiveProviderSandbox(projectRoot, provider) {
12594
13194
  }
12595
13195
  function runInteractiveCommand(command, args) {
12596
13196
  return new Promise((resolve2) => {
12597
- const child = spawn6(command, args, { stdio: "inherit" });
13197
+ const child = spawn7(command, args, { stdio: "inherit" });
12598
13198
  child.on("close", (code) => resolve2(code === 0));
12599
13199
  child.on("error", () => resolve2(false));
12600
13200
  });
12601
13201
  }
12602
13202
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
12603
13203
  try {
12604
- execSync17(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
13204
+ execSync18(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
12605
13205
  stdio: ["pipe", "pipe", "pipe"],
12606
13206
  timeout: 120000
12607
13207
  });
@@ -12622,7 +13222,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
12622
13222
  }
12623
13223
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
12624
13224
  try {
12625
- execSync17(`docker sandbox exec ${sandboxName} which ${pm}`, {
13225
+ execSync18(`docker sandbox exec ${sandboxName} which ${pm}`, {
12626
13226
  stdio: ["pipe", "pipe", "pipe"],
12627
13227
  timeout: 5000
12628
13228
  });
@@ -12631,7 +13231,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
12631
13231
  process.stderr.write(`Installing ${bold2(pm)} in sandbox...
12632
13232
  `);
12633
13233
  try {
12634
- execSync17(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
13234
+ execSync18(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
12635
13235
  stdio: "inherit",
12636
13236
  timeout: 120000
12637
13237
  });
@@ -12643,7 +13243,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
12643
13243
  }
12644
13244
  async function ensureCodexInSandbox(sandboxName) {
12645
13245
  try {
12646
- execSync17(`docker sandbox exec ${sandboxName} which codex`, {
13246
+ execSync18(`docker sandbox exec ${sandboxName} which codex`, {
12647
13247
  stdio: ["pipe", "pipe", "pipe"],
12648
13248
  timeout: 5000
12649
13249
  });
@@ -12651,7 +13251,7 @@ async function ensureCodexInSandbox(sandboxName) {
12651
13251
  process.stderr.write(`Installing codex in sandbox...
12652
13252
  `);
12653
13253
  try {
12654
- execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
13254
+ execSync18(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
12655
13255
  } catch {
12656
13256
  process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
12657
13257
  `);
@@ -12660,7 +13260,7 @@ async function ensureCodexInSandbox(sandboxName) {
12660
13260
  }
12661
13261
  function isSandboxAlive(name) {
12662
13262
  try {
12663
- const output = execSync17("docker sandbox ls", {
13263
+ const output = execSync18("docker sandbox ls", {
12664
13264
  encoding: "utf-8",
12665
13265
  stdio: ["pipe", "pipe", "pipe"],
12666
13266
  timeout: 5000
@@ -12686,13 +13286,13 @@ init_context();
12686
13286
  init_logger();
12687
13287
  init_rate_limiter();
12688
13288
  init_terminal();
12689
- import { existsSync as existsSync25, readFileSync as readFileSync18 } from "node:fs";
12690
- import { join as join25 } from "node:path";
13289
+ import { existsSync as existsSync26, readFileSync as readFileSync18 } from "node:fs";
13290
+ import { join as join26 } from "node:path";
12691
13291
  import { fileURLToPath } from "node:url";
12692
13292
  function getCliVersion() {
12693
13293
  const fallbackVersion = "0.0.0";
12694
- const packageJsonPath = join25(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12695
- if (!existsSync25(packageJsonPath)) {
13294
+ const packageJsonPath = join26(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
13295
+ if (!existsSync26(packageJsonPath)) {
12696
13296
  return fallbackVersion;
12697
13297
  }
12698
13298
  try {
@@ -12957,7 +13557,7 @@ async function main() {
12957
13557
  try {
12958
13558
  const root = getGitRoot(cwd);
12959
13559
  if (isInitialized(root)) {
12960
- logDir = join25(root, ".locus", "logs");
13560
+ logDir = join26(root, ".locus", "logs");
12961
13561
  getRateLimiter(root);
12962
13562
  }
12963
13563
  } catch {}