@locusai/cli 0.21.16 → 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 +745 -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"]
@@ -8147,6 +8717,11 @@ async function startRepl(options) {
8147
8717
  await executeOneShotPrompt(options.prompt, session, sessionManager, options);
8148
8718
  return;
8149
8719
  }
8720
+ if (!process.stdin.isTTY) {
8721
+ process.stderr.write(`${red2("✗")} Interactive REPL requires a terminal. Use: locus exec "your prompt"
8722
+ `);
8723
+ return;
8724
+ }
8150
8725
  await runInteractiveRepl(session, sessionManager, options);
8151
8726
  }
8152
8727
  async function executeOneShotPrompt(prompt, session, sessionManager, options) {
@@ -8190,8 +8765,15 @@ async function runInteractiveRepl(session, sessionManager, options) {
8190
8765
  new SlashCommandCompletion(getAllCommandNames()),
8191
8766
  new FilePathCompletion(projectRoot)
8192
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
+ });
8193
8775
  const input = new InputHandler({
8194
- prompt: `${cyan2("locus")} ${dim2(">")} `,
8776
+ prompt: basePrompt,
8195
8777
  getHistory: () => history.getEntries(),
8196
8778
  onTab: (text) => completion.complete(text)
8197
8779
  });
@@ -8244,12 +8826,33 @@ async function runInteractiveRepl(session, sessionManager, options) {
8244
8826
  onVerboseToggle: () => {
8245
8827
  verbose = !verbose;
8246
8828
  },
8247
- 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
+ }
8248
8839
  };
8249
8840
  while (!shouldExit) {
8250
8841
  const result = await input.readline();
8251
8842
  switch (result.type) {
8252
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
+ }
8253
8856
  const text = result.text.trim();
8254
8857
  if (!text)
8255
8858
  continue;
@@ -8300,6 +8903,7 @@ ${red2("✗")} ${msg}
8300
8903
  break;
8301
8904
  }
8302
8905
  }
8906
+ voice.cancel();
8303
8907
  const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
8304
8908
  if (shouldPersistOnExit) {
8305
8909
  sessionManager.save(session);
@@ -8365,7 +8969,7 @@ function printWelcome(session) {
8365
8969
  }
8366
8970
  process.stderr.write(`
8367
8971
  `);
8368
- 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")}
8369
8973
  `);
8370
8974
  process.stderr.write(`
8371
8975
  `);
@@ -8385,6 +8989,7 @@ var init_repl = __esm(() => {
8385
8989
  init_input_history();
8386
8990
  init_model_config();
8387
8991
  init_session_manager();
8992
+ init_voice();
8388
8993
  LOCUS_LOGO = [
8389
8994
  " ▄█ ",
8390
8995
  " ▄▄████▄▄▄▄ ",
@@ -8582,11 +9187,11 @@ var init_exec = __esm(() => {
8582
9187
  });
8583
9188
 
8584
9189
  // src/core/submodule.ts
8585
- import { execSync as execSync10 } from "node:child_process";
8586
- import { existsSync as existsSync17 } from "node:fs";
8587
- 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";
8588
9193
  function git2(args, cwd) {
8589
- return execSync10(`git ${args}`, {
9194
+ return execSync11(`git ${args}`, {
8590
9195
  cwd,
8591
9196
  encoding: "utf-8",
8592
9197
  stdio: ["pipe", "pipe", "pipe"]
@@ -8600,7 +9205,7 @@ function gitSafe(args, cwd) {
8600
9205
  }
8601
9206
  }
8602
9207
  function hasSubmodules(cwd) {
8603
- return existsSync17(join17(cwd, ".gitmodules"));
9208
+ return existsSync18(join18(cwd, ".gitmodules"));
8604
9209
  }
8605
9210
  function listSubmodules(cwd) {
8606
9211
  if (!hasSubmodules(cwd))
@@ -8620,7 +9225,7 @@ function listSubmodules(cwd) {
8620
9225
  continue;
8621
9226
  submodules.push({
8622
9227
  path,
8623
- absolutePath: join17(cwd, path),
9228
+ absolutePath: join18(cwd, path),
8624
9229
  dirty
8625
9230
  });
8626
9231
  }
@@ -8633,7 +9238,7 @@ function getDirtySubmodules(cwd) {
8633
9238
  const submodules = listSubmodules(cwd);
8634
9239
  const dirty = [];
8635
9240
  for (const sub of submodules) {
8636
- if (!existsSync17(sub.absolutePath))
9241
+ if (!existsSync18(sub.absolutePath))
8637
9242
  continue;
8638
9243
  const status = gitSafe("status --porcelain", sub.absolutePath);
8639
9244
  if (status && status.trim().length > 0) {
@@ -8654,7 +9259,7 @@ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
8654
9259
  const message = `chore: complete #${issueNumber} - ${issueTitle}
8655
9260
 
8656
9261
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
8657
- execSync10("git commit -F -", {
9262
+ execSync11("git commit -F -", {
8658
9263
  input: message,
8659
9264
  cwd: sub.absolutePath,
8660
9265
  encoding: "utf-8",
@@ -8720,7 +9325,7 @@ function pushSubmoduleBranches(cwd) {
8720
9325
  const log = getLogger();
8721
9326
  const submodules = listSubmodules(cwd);
8722
9327
  for (const sub of submodules) {
8723
- if (!existsSync17(sub.absolutePath))
9328
+ if (!existsSync18(sub.absolutePath))
8724
9329
  continue;
8725
9330
  const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
8726
9331
  if (!branch || branch === "HEAD")
@@ -8741,7 +9346,7 @@ var init_submodule = __esm(() => {
8741
9346
  });
8742
9347
 
8743
9348
  // src/core/agent.ts
8744
- import { execSync as execSync11 } from "node:child_process";
9349
+ import { execSync as execSync12 } from "node:child_process";
8745
9350
  async function executeIssue(projectRoot, options) {
8746
9351
  const log = getLogger();
8747
9352
  const timer = createTimer();
@@ -8770,7 +9375,7 @@ ${cyan2("●")} ${bold2(`#${issueNumber}`)} ${issue.title}
8770
9375
  }
8771
9376
  let issueComments = [];
8772
9377
  try {
8773
- 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();
8774
9379
  if (commentsRaw) {
8775
9380
  issueComments = commentsRaw.split(`
8776
9381
  `).filter(Boolean);
@@ -8934,12 +9539,12 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
8934
9539
  }
8935
9540
  async function createIssuePR(projectRoot, config, issue) {
8936
9541
  try {
8937
- const currentBranch = execSync11("git rev-parse --abbrev-ref HEAD", {
9542
+ const currentBranch = execSync12("git rev-parse --abbrev-ref HEAD", {
8938
9543
  cwd: projectRoot,
8939
9544
  encoding: "utf-8",
8940
9545
  stdio: ["pipe", "pipe", "pipe"]
8941
9546
  }).trim();
8942
- const diff = execSync11(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
9547
+ const diff = execSync12(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
8943
9548
  cwd: projectRoot,
8944
9549
  encoding: "utf-8",
8945
9550
  stdio: ["pipe", "pipe", "pipe"]
@@ -8949,7 +9554,7 @@ async function createIssuePR(projectRoot, config, issue) {
8949
9554
  return;
8950
9555
  }
8951
9556
  pushSubmoduleBranches(projectRoot);
8952
- execSync11(`git push -u origin ${currentBranch}`, {
9557
+ execSync12(`git push -u origin ${currentBranch}`, {
8953
9558
  cwd: projectRoot,
8954
9559
  encoding: "utf-8",
8955
9560
  stdio: ["pipe", "pipe", "pipe"]
@@ -9004,9 +9609,9 @@ var init_agent = __esm(() => {
9004
9609
  });
9005
9610
 
9006
9611
  // src/core/conflict.ts
9007
- import { execSync as execSync12 } from "node:child_process";
9612
+ import { execSync as execSync13 } from "node:child_process";
9008
9613
  function git3(args, cwd) {
9009
- return execSync12(`git ${args}`, {
9614
+ return execSync13(`git ${args}`, {
9010
9615
  cwd,
9011
9616
  encoding: "utf-8",
9012
9617
  stdio: ["pipe", "pipe", "pipe"]
@@ -9135,19 +9740,19 @@ var init_conflict = __esm(() => {
9135
9740
 
9136
9741
  // src/core/run-state.ts
9137
9742
  import {
9138
- existsSync as existsSync18,
9139
- mkdirSync as mkdirSync12,
9743
+ existsSync as existsSync19,
9744
+ mkdirSync as mkdirSync13,
9140
9745
  readFileSync as readFileSync12,
9141
- unlinkSync as unlinkSync4,
9746
+ unlinkSync as unlinkSync5,
9142
9747
  writeFileSync as writeFileSync8
9143
9748
  } from "node:fs";
9144
- import { dirname as dirname6, join as join18 } from "node:path";
9749
+ import { dirname as dirname6, join as join19 } from "node:path";
9145
9750
  function getRunStatePath(projectRoot) {
9146
- return join18(projectRoot, ".locus", "run-state.json");
9751
+ return join19(projectRoot, ".locus", "run-state.json");
9147
9752
  }
9148
9753
  function loadRunState(projectRoot) {
9149
9754
  const path = getRunStatePath(projectRoot);
9150
- if (!existsSync18(path))
9755
+ if (!existsSync19(path))
9151
9756
  return null;
9152
9757
  try {
9153
9758
  return JSON.parse(readFileSync12(path, "utf-8"));
@@ -9159,16 +9764,16 @@ function loadRunState(projectRoot) {
9159
9764
  function saveRunState(projectRoot, state) {
9160
9765
  const path = getRunStatePath(projectRoot);
9161
9766
  const dir = dirname6(path);
9162
- if (!existsSync18(dir)) {
9163
- mkdirSync12(dir, { recursive: true });
9767
+ if (!existsSync19(dir)) {
9768
+ mkdirSync13(dir, { recursive: true });
9164
9769
  }
9165
9770
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
9166
9771
  `, "utf-8");
9167
9772
  }
9168
9773
  function clearRunState(projectRoot) {
9169
9774
  const path = getRunStatePath(projectRoot);
9170
- if (existsSync18(path)) {
9171
- unlinkSync4(path);
9775
+ if (existsSync19(path)) {
9776
+ unlinkSync5(path);
9172
9777
  }
9173
9778
  }
9174
9779
  function createSprintRunState(sprint, branch, issues) {
@@ -9307,11 +9912,11 @@ var init_shutdown = __esm(() => {
9307
9912
  });
9308
9913
 
9309
9914
  // src/core/worktree.ts
9310
- import { execSync as execSync13 } from "node:child_process";
9311
- import { existsSync as existsSync19, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
9312
- 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";
9313
9918
  function git4(args, cwd) {
9314
- return execSync13(`git ${args}`, {
9919
+ return execSync14(`git ${args}`, {
9315
9920
  cwd,
9316
9921
  encoding: "utf-8",
9317
9922
  stdio: ["pipe", "pipe", "pipe"]
@@ -9325,10 +9930,10 @@ function gitSafe3(args, cwd) {
9325
9930
  }
9326
9931
  }
9327
9932
  function getWorktreeDir(projectRoot) {
9328
- return join19(projectRoot, ".locus", "worktrees");
9933
+ return join20(projectRoot, ".locus", "worktrees");
9329
9934
  }
9330
9935
  function getWorktreePath(projectRoot, issueNumber) {
9331
- return join19(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
9936
+ return join20(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
9332
9937
  }
9333
9938
  function generateBranchName(issueNumber) {
9334
9939
  const randomSuffix = Math.random().toString(36).slice(2, 8);
@@ -9336,7 +9941,7 @@ function generateBranchName(issueNumber) {
9336
9941
  }
9337
9942
  function getWorktreeBranch(worktreePath) {
9338
9943
  try {
9339
- return execSync13("git branch --show-current", {
9944
+ return execSync14("git branch --show-current", {
9340
9945
  cwd: worktreePath,
9341
9946
  encoding: "utf-8",
9342
9947
  stdio: ["pipe", "pipe", "pipe"]
@@ -9348,7 +9953,7 @@ function getWorktreeBranch(worktreePath) {
9348
9953
  function createWorktree(projectRoot, issueNumber, baseBranch) {
9349
9954
  const log = getLogger();
9350
9955
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
9351
- if (existsSync19(worktreePath)) {
9956
+ if (existsSync20(worktreePath)) {
9352
9957
  log.verbose(`Worktree already exists for issue #${issueNumber}`);
9353
9958
  const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
9354
9959
  return {
@@ -9376,7 +9981,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
9376
9981
  function removeWorktree(projectRoot, issueNumber) {
9377
9982
  const log = getLogger();
9378
9983
  const worktreePath = getWorktreePath(projectRoot, issueNumber);
9379
- if (!existsSync19(worktreePath)) {
9984
+ if (!existsSync20(worktreePath)) {
9380
9985
  log.verbose(`Worktree for issue #${issueNumber} does not exist`);
9381
9986
  return;
9382
9987
  }
@@ -9395,7 +10000,7 @@ function removeWorktree(projectRoot, issueNumber) {
9395
10000
  function listWorktrees(projectRoot) {
9396
10001
  const log = getLogger();
9397
10002
  const worktreeDir = getWorktreeDir(projectRoot);
9398
- if (!existsSync19(worktreeDir)) {
10003
+ if (!existsSync20(worktreeDir)) {
9399
10004
  return [];
9400
10005
  }
9401
10006
  const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
@@ -9415,7 +10020,7 @@ function listWorktrees(projectRoot) {
9415
10020
  if (!match)
9416
10021
  continue;
9417
10022
  const issueNumber = Number.parseInt(match[1], 10);
9418
- const path = join19(worktreeDir, entry);
10023
+ const path = join20(worktreeDir, entry);
9419
10024
  const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
9420
10025
  let resolvedPath;
9421
10026
  try {
@@ -9463,7 +10068,7 @@ var exports_run = {};
9463
10068
  __export(exports_run, {
9464
10069
  runCommand: () => runCommand
9465
10070
  });
9466
- import { execSync as execSync14 } from "node:child_process";
10071
+ import { execSync as execSync15 } from "node:child_process";
9467
10072
  function resolveExecutionContext(config, modelOverride) {
9468
10073
  const model = modelOverride ?? config.ai.model;
9469
10074
  const provider = inferProviderFromModel(model) ?? config.ai.provider;
@@ -9623,7 +10228,7 @@ ${yellow2("⚠")} A sprint run is already in progress.
9623
10228
  }
9624
10229
  if (!flags.dryRun) {
9625
10230
  try {
9626
- execSync14(`git checkout -B ${branchName}`, {
10231
+ execSync15(`git checkout -B ${branchName}`, {
9627
10232
  cwd: projectRoot,
9628
10233
  encoding: "utf-8",
9629
10234
  stdio: ["pipe", "pipe", "pipe"]
@@ -9673,7 +10278,7 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
9673
10278
  let sprintContext;
9674
10279
  if (i > 0 && !flags.dryRun) {
9675
10280
  try {
9676
- sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
10281
+ sprintContext = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD`, {
9677
10282
  cwd: projectRoot,
9678
10283
  encoding: "utf-8",
9679
10284
  stdio: ["pipe", "pipe", "pipe"]
@@ -9738,7 +10343,7 @@ ${bold2("Summary:")}
9738
10343
  const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
9739
10344
  if (prNumber !== undefined) {
9740
10345
  try {
9741
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10346
+ execSync15(`git checkout ${config.agent.baseBranch}`, {
9742
10347
  cwd: projectRoot,
9743
10348
  encoding: "utf-8",
9744
10349
  stdio: ["pipe", "pipe", "pipe"]
@@ -9783,7 +10388,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
9783
10388
  `);
9784
10389
  if (!flags.dryRun) {
9785
10390
  try {
9786
- execSync14(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
10391
+ execSync15(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
9787
10392
  cwd: projectRoot,
9788
10393
  encoding: "utf-8",
9789
10394
  stdio: ["pipe", "pipe", "pipe"]
@@ -9808,7 +10413,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
9808
10413
  if (!flags.dryRun) {
9809
10414
  if (result.success) {
9810
10415
  try {
9811
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10416
+ execSync15(`git checkout ${config.agent.baseBranch}`, {
9812
10417
  cwd: projectRoot,
9813
10418
  encoding: "utf-8",
9814
10419
  stdio: ["pipe", "pipe", "pipe"]
@@ -9945,13 +10550,13 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
9945
10550
  `);
9946
10551
  if (state.type === "sprint" && state.branch) {
9947
10552
  try {
9948
- const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
10553
+ const currentBranch = execSync15("git rev-parse --abbrev-ref HEAD", {
9949
10554
  cwd: projectRoot,
9950
10555
  encoding: "utf-8",
9951
10556
  stdio: ["pipe", "pipe", "pipe"]
9952
10557
  }).trim();
9953
10558
  if (currentBranch !== state.branch) {
9954
- execSync14(`git checkout ${state.branch}`, {
10559
+ execSync15(`git checkout ${state.branch}`, {
9955
10560
  cwd: projectRoot,
9956
10561
  encoding: "utf-8",
9957
10562
  stdio: ["pipe", "pipe", "pipe"]
@@ -10018,7 +10623,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
10018
10623
  const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
10019
10624
  if (prNumber !== undefined) {
10020
10625
  try {
10021
- execSync14(`git checkout ${config.agent.baseBranch}`, {
10626
+ execSync15(`git checkout ${config.agent.baseBranch}`, {
10022
10627
  cwd: projectRoot,
10023
10628
  encoding: "utf-8",
10024
10629
  stdio: ["pipe", "pipe", "pipe"]
@@ -10054,14 +10659,14 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
10054
10659
  process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
10055
10660
  `);
10056
10661
  }
10057
- const status = execSync14("git status --porcelain", {
10662
+ const status = execSync15("git status --porcelain", {
10058
10663
  cwd: projectRoot,
10059
10664
  encoding: "utf-8",
10060
10665
  stdio: ["pipe", "pipe", "pipe"]
10061
10666
  }).trim();
10062
10667
  if (!status)
10063
10668
  return;
10064
- execSync14("git add -A", {
10669
+ execSync15("git add -A", {
10065
10670
  cwd: projectRoot,
10066
10671
  encoding: "utf-8",
10067
10672
  stdio: ["pipe", "pipe", "pipe"]
@@ -10069,7 +10674,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
10069
10674
  const message = `chore: complete #${issueNumber} - ${issueTitle}
10070
10675
 
10071
10676
  Co-Authored-By: LocusAgent <agent@locusai.team>`;
10072
- execSync14(`git commit -F -`, {
10677
+ execSync15(`git commit -F -`, {
10073
10678
  input: message,
10074
10679
  cwd: projectRoot,
10075
10680
  encoding: "utf-8",
@@ -10083,7 +10688,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
10083
10688
  if (!config.agent.autoPR)
10084
10689
  return;
10085
10690
  try {
10086
- const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
10691
+ const diff = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
10087
10692
  cwd: projectRoot,
10088
10693
  encoding: "utf-8",
10089
10694
  stdio: ["pipe", "pipe", "pipe"]
@@ -10094,7 +10699,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
10094
10699
  return;
10095
10700
  }
10096
10701
  pushSubmoduleBranches(projectRoot);
10097
- execSync14(`git push -u origin ${branchName}`, {
10702
+ execSync15(`git push -u origin ${branchName}`, {
10098
10703
  cwd: projectRoot,
10099
10704
  encoding: "utf-8",
10100
10705
  stdio: ["pipe", "pipe", "pipe"]
@@ -10252,13 +10857,13 @@ __export(exports_plan, {
10252
10857
  parsePlanArgs: () => parsePlanArgs
10253
10858
  });
10254
10859
  import {
10255
- existsSync as existsSync20,
10256
- mkdirSync as mkdirSync13,
10860
+ existsSync as existsSync21,
10861
+ mkdirSync as mkdirSync14,
10257
10862
  readdirSync as readdirSync8,
10258
10863
  readFileSync as readFileSync13,
10259
10864
  writeFileSync as writeFileSync9
10260
10865
  } from "node:fs";
10261
- import { join as join20 } from "node:path";
10866
+ import { join as join21 } from "node:path";
10262
10867
  function printHelp() {
10263
10868
  process.stderr.write(`
10264
10869
  ${bold2("locus plan")} — AI-powered sprint planning
@@ -10289,12 +10894,12 @@ function normalizeSprintName(name) {
10289
10894
  return name.trim().toLowerCase();
10290
10895
  }
10291
10896
  function getPlansDir(projectRoot) {
10292
- return join20(projectRoot, ".locus", "plans");
10897
+ return join21(projectRoot, ".locus", "plans");
10293
10898
  }
10294
10899
  function ensurePlansDir(projectRoot) {
10295
10900
  const dir = getPlansDir(projectRoot);
10296
- if (!existsSync20(dir)) {
10297
- mkdirSync13(dir, { recursive: true });
10901
+ if (!existsSync21(dir)) {
10902
+ mkdirSync14(dir, { recursive: true });
10298
10903
  }
10299
10904
  return dir;
10300
10905
  }
@@ -10303,14 +10908,14 @@ function generateId() {
10303
10908
  }
10304
10909
  function loadPlanFile(projectRoot, id) {
10305
10910
  const dir = getPlansDir(projectRoot);
10306
- if (!existsSync20(dir))
10911
+ if (!existsSync21(dir))
10307
10912
  return null;
10308
10913
  const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
10309
10914
  const match = files.find((f) => f.startsWith(id));
10310
10915
  if (!match)
10311
10916
  return null;
10312
10917
  try {
10313
- const content = readFileSync13(join20(dir, match), "utf-8");
10918
+ const content = readFileSync13(join21(dir, match), "utf-8");
10314
10919
  return JSON.parse(content);
10315
10920
  } catch {
10316
10921
  return null;
@@ -10356,7 +10961,7 @@ async function planCommand(projectRoot, args, flags = {}) {
10356
10961
  }
10357
10962
  function handleListPlans(projectRoot) {
10358
10963
  const dir = getPlansDir(projectRoot);
10359
- if (!existsSync20(dir)) {
10964
+ if (!existsSync21(dir)) {
10360
10965
  process.stderr.write(`${dim2("No saved plans yet.")}
10361
10966
  `);
10362
10967
  return;
@@ -10374,7 +10979,7 @@ ${bold2("Saved Plans:")}
10374
10979
  for (const file of files) {
10375
10980
  const id = file.replace(".json", "");
10376
10981
  try {
10377
- const content = readFileSync13(join20(dir, file), "utf-8");
10982
+ const content = readFileSync13(join21(dir, file), "utf-8");
10378
10983
  const plan = JSON.parse(content);
10379
10984
  const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
10380
10985
  const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
@@ -10485,7 +11090,7 @@ ${bold2("Approving plan:")}
10485
11090
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
10486
11091
  const id = generateId();
10487
11092
  const plansDir = ensurePlansDir(projectRoot);
10488
- const planPath = join20(plansDir, `${id}.json`);
11093
+ const planPath = join21(plansDir, `${id}.json`);
10489
11094
  const planPathRelative = `.locus/plans/${id}.json`;
10490
11095
  const displayDirective = directive;
10491
11096
  process.stderr.write(`
@@ -10527,7 +11132,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
10527
11132
  `);
10528
11133
  return;
10529
11134
  }
10530
- if (!existsSync20(planPath)) {
11135
+ if (!existsSync21(planPath)) {
10531
11136
  process.stderr.write(`
10532
11137
  ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
10533
11138
  `);
@@ -10708,15 +11313,15 @@ ${directive}${sprintName ? `
10708
11313
 
10709
11314
  **Sprint:** ${sprintName}` : ""}
10710
11315
  </directive>`);
10711
- const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
10712
- if (existsSync20(locusPath)) {
11316
+ const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
11317
+ if (existsSync21(locusPath)) {
10713
11318
  const content = readFileSync13(locusPath, "utf-8");
10714
11319
  parts.push(`<project-context>
10715
11320
  ${content.slice(0, 3000)}
10716
11321
  </project-context>`);
10717
11322
  }
10718
- const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
10719
- if (existsSync20(learningsPath)) {
11323
+ const learningsPath = join21(projectRoot, ".locus", "LEARNINGS.md");
11324
+ if (existsSync21(learningsPath)) {
10720
11325
  const content = readFileSync13(learningsPath, "utf-8");
10721
11326
  parts.push(`<past-learnings>
10722
11327
  ${content.slice(0, 2000)}
@@ -10896,9 +11501,9 @@ var exports_review = {};
10896
11501
  __export(exports_review, {
10897
11502
  reviewCommand: () => reviewCommand
10898
11503
  });
10899
- import { execFileSync as execFileSync2, execSync as execSync15 } from "node:child_process";
10900
- import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
10901
- 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";
10902
11507
  function printHelp2() {
10903
11508
  process.stderr.write(`
10904
11509
  ${bold2("locus review")} — AI-powered code review
@@ -10982,7 +11587,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
10982
11587
  async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
10983
11588
  let prInfo;
10984
11589
  try {
10985
- 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"] });
10986
11591
  const raw = JSON.parse(result);
10987
11592
  prInfo = {
10988
11593
  number: raw.number,
@@ -11066,8 +11671,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
11066
11671
  parts.push(`<role>
11067
11672
  You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
11068
11673
  </role>`);
11069
- const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
11070
- if (existsSync21(locusPath)) {
11674
+ const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11675
+ if (existsSync22(locusPath)) {
11071
11676
  const content = readFileSync14(locusPath, "utf-8");
11072
11677
  parts.push(`<project-context>
11073
11678
  ${content.slice(0, 2000)}
@@ -11129,7 +11734,7 @@ var exports_iterate = {};
11129
11734
  __export(exports_iterate, {
11130
11735
  iterateCommand: () => iterateCommand
11131
11736
  });
11132
- import { execSync as execSync16 } from "node:child_process";
11737
+ import { execSync as execSync17 } from "node:child_process";
11133
11738
  function printHelp3() {
11134
11739
  process.stderr.write(`
11135
11740
  ${bold2("locus iterate")} — Re-execute tasks with PR feedback
@@ -11347,12 +11952,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
11347
11952
  }
11348
11953
  function findPRForIssue(projectRoot, issueNumber) {
11349
11954
  try {
11350
- 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"] });
11351
11956
  const parsed = JSON.parse(result);
11352
11957
  if (parsed.length > 0) {
11353
11958
  return parsed[0].number;
11354
11959
  }
11355
- 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"] });
11356
11961
  const branchParsed = JSON.parse(branchResult);
11357
11962
  if (branchParsed.length > 0) {
11358
11963
  return branchParsed[0].number;
@@ -11388,14 +11993,14 @@ __export(exports_discuss, {
11388
11993
  discussCommand: () => discussCommand
11389
11994
  });
11390
11995
  import {
11391
- existsSync as existsSync22,
11392
- mkdirSync as mkdirSync14,
11996
+ existsSync as existsSync23,
11997
+ mkdirSync as mkdirSync15,
11393
11998
  readdirSync as readdirSync9,
11394
11999
  readFileSync as readFileSync15,
11395
- unlinkSync as unlinkSync5,
12000
+ unlinkSync as unlinkSync6,
11396
12001
  writeFileSync as writeFileSync10
11397
12002
  } from "node:fs";
11398
- import { join as join22 } from "node:path";
12003
+ import { join as join23 } from "node:path";
11399
12004
  function printHelp4() {
11400
12005
  process.stderr.write(`
11401
12006
  ${bold2("locus discuss")} — AI-powered architectural discussions
@@ -11417,12 +12022,12 @@ ${bold2("Examples:")}
11417
12022
  `);
11418
12023
  }
11419
12024
  function getDiscussionsDir(projectRoot) {
11420
- return join22(projectRoot, ".locus", "discussions");
12025
+ return join23(projectRoot, ".locus", "discussions");
11421
12026
  }
11422
12027
  function ensureDiscussionsDir(projectRoot) {
11423
12028
  const dir = getDiscussionsDir(projectRoot);
11424
- if (!existsSync22(dir)) {
11425
- mkdirSync14(dir, { recursive: true });
12029
+ if (!existsSync23(dir)) {
12030
+ mkdirSync15(dir, { recursive: true });
11426
12031
  }
11427
12032
  return dir;
11428
12033
  }
@@ -11456,7 +12061,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
11456
12061
  }
11457
12062
  function listDiscussions(projectRoot) {
11458
12063
  const dir = getDiscussionsDir(projectRoot);
11459
- if (!existsSync22(dir)) {
12064
+ if (!existsSync23(dir)) {
11460
12065
  process.stderr.write(`${dim2("No discussions yet.")}
11461
12066
  `);
11462
12067
  return;
@@ -11473,7 +12078,7 @@ ${bold2("Discussions:")}
11473
12078
  `);
11474
12079
  for (const file of files) {
11475
12080
  const id = file.replace(".md", "");
11476
- const content = readFileSync15(join22(dir, file), "utf-8");
12081
+ const content = readFileSync15(join23(dir, file), "utf-8");
11477
12082
  const titleMatch = content.match(/^#\s+(.+)/m);
11478
12083
  const title = titleMatch ? titleMatch[1] : id;
11479
12084
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
@@ -11491,7 +12096,7 @@ function showDiscussion(projectRoot, id) {
11491
12096
  return;
11492
12097
  }
11493
12098
  const dir = getDiscussionsDir(projectRoot);
11494
- if (!existsSync22(dir)) {
12099
+ if (!existsSync23(dir)) {
11495
12100
  process.stderr.write(`${red2("✗")} No discussions found.
11496
12101
  `);
11497
12102
  return;
@@ -11503,7 +12108,7 @@ function showDiscussion(projectRoot, id) {
11503
12108
  `);
11504
12109
  return;
11505
12110
  }
11506
- const content = readFileSync15(join22(dir, match), "utf-8");
12111
+ const content = readFileSync15(join23(dir, match), "utf-8");
11507
12112
  process.stdout.write(`${content}
11508
12113
  `);
11509
12114
  }
@@ -11514,7 +12119,7 @@ function deleteDiscussion(projectRoot, id) {
11514
12119
  return;
11515
12120
  }
11516
12121
  const dir = getDiscussionsDir(projectRoot);
11517
- if (!existsSync22(dir)) {
12122
+ if (!existsSync23(dir)) {
11518
12123
  process.stderr.write(`${red2("✗")} No discussions found.
11519
12124
  `);
11520
12125
  return;
@@ -11526,7 +12131,7 @@ function deleteDiscussion(projectRoot, id) {
11526
12131
  `);
11527
12132
  return;
11528
12133
  }
11529
- unlinkSync5(join22(dir, match));
12134
+ unlinkSync6(join23(dir, match));
11530
12135
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
11531
12136
  `);
11532
12137
  }
@@ -11539,7 +12144,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
11539
12144
  return;
11540
12145
  }
11541
12146
  const dir = getDiscussionsDir(projectRoot);
11542
- if (!existsSync22(dir)) {
12147
+ if (!existsSync23(dir)) {
11543
12148
  process.stderr.write(`${red2("✗")} No discussions found.
11544
12149
  `);
11545
12150
  return;
@@ -11551,7 +12156,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
11551
12156
  `);
11552
12157
  return;
11553
12158
  }
11554
- const content = readFileSync15(join22(dir, match), "utf-8");
12159
+ const content = readFileSync15(join23(dir, match), "utf-8");
11555
12160
  const titleMatch = content.match(/^#\s+(.+)/m);
11556
12161
  const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
11557
12162
  await planCommand(projectRoot, [
@@ -11562,6 +12167,9 @@ ${content.slice(0, 8000)}`
11562
12167
  ], {});
11563
12168
  }
11564
12169
  async function promptForAnswers() {
12170
+ if (!process.stdin.isTTY) {
12171
+ return "";
12172
+ }
11565
12173
  const input = new InputHandler({
11566
12174
  prompt: `${cyan2("you")} ${dim2(">")} `
11567
12175
  });
@@ -11673,7 +12281,7 @@ ${turn.content}`;
11673
12281
  ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
11674
12282
  ].join(`
11675
12283
  `);
11676
- writeFileSync10(join22(dir, `${id}.md`), markdown, "utf-8");
12284
+ writeFileSync10(join23(dir, `${id}.md`), markdown, "utf-8");
11677
12285
  process.stderr.write(`
11678
12286
  ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
11679
12287
  `);
@@ -11688,15 +12296,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
11688
12296
  parts.push(`<role>
11689
12297
  You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
11690
12298
  </role>`);
11691
- const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
11692
- if (existsSync22(locusPath)) {
12299
+ const locusPath = join23(projectRoot, ".locus", "LOCUS.md");
12300
+ if (existsSync23(locusPath)) {
11693
12301
  const content = readFileSync15(locusPath, "utf-8");
11694
12302
  parts.push(`<project-context>
11695
12303
  ${content.slice(0, 3000)}
11696
12304
  </project-context>`);
11697
12305
  }
11698
- const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
11699
- if (existsSync22(learningsPath)) {
12306
+ const learningsPath = join23(projectRoot, ".locus", "LEARNINGS.md");
12307
+ if (existsSync23(learningsPath)) {
11700
12308
  const content = readFileSync15(learningsPath, "utf-8");
11701
12309
  parts.push(`<past-learnings>
11702
12310
  ${content.slice(0, 2000)}
@@ -11768,8 +12376,8 @@ __export(exports_artifacts, {
11768
12376
  formatDate: () => formatDate2,
11769
12377
  artifactsCommand: () => artifactsCommand
11770
12378
  });
11771
- import { existsSync as existsSync23, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
11772
- 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";
11773
12381
  function printHelp5() {
11774
12382
  process.stderr.write(`
11775
12383
  ${bold2("locus artifacts")} — View and manage AI-generated artifacts
@@ -11789,14 +12397,14 @@ ${dim2("Artifact names support partial matching.")}
11789
12397
  `);
11790
12398
  }
11791
12399
  function getArtifactsDir(projectRoot) {
11792
- return join23(projectRoot, ".locus", "artifacts");
12400
+ return join24(projectRoot, ".locus", "artifacts");
11793
12401
  }
11794
12402
  function listArtifacts(projectRoot) {
11795
12403
  const dir = getArtifactsDir(projectRoot);
11796
- if (!existsSync23(dir))
12404
+ if (!existsSync24(dir))
11797
12405
  return [];
11798
12406
  return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
11799
- const filePath = join23(dir, fileName);
12407
+ const filePath = join24(dir, fileName);
11800
12408
  const stat = statSync5(filePath);
11801
12409
  return {
11802
12410
  name: fileName.replace(/\.md$/, ""),
@@ -11809,8 +12417,8 @@ function listArtifacts(projectRoot) {
11809
12417
  function readArtifact(projectRoot, name) {
11810
12418
  const dir = getArtifactsDir(projectRoot);
11811
12419
  const fileName = name.endsWith(".md") ? name : `${name}.md`;
11812
- const filePath = join23(dir, fileName);
11813
- if (!existsSync23(filePath))
12420
+ const filePath = join24(dir, fileName);
12421
+ if (!existsSync24(filePath))
11814
12422
  return null;
11815
12423
  const stat = statSync5(filePath);
11816
12424
  return {
@@ -11979,10 +12587,10 @@ __export(exports_sandbox2, {
11979
12587
  parseSandboxLogsArgs: () => parseSandboxLogsArgs,
11980
12588
  parseSandboxInstallArgs: () => parseSandboxInstallArgs
11981
12589
  });
11982
- import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
12590
+ import { execSync as execSync18, spawn as spawn7 } from "node:child_process";
11983
12591
  import { createHash } from "node:crypto";
11984
- import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
11985
- 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";
11986
12594
  import { createInterface as createInterface3 } from "node:readline";
11987
12595
  function printSandboxHelp() {
11988
12596
  process.stderr.write(`
@@ -12147,7 +12755,7 @@ async function handleAgentLogin(projectRoot, agent) {
12147
12755
  process.stderr.write(`${dim2("Login and then exit when ready.")}
12148
12756
 
12149
12757
  `);
12150
- const child = spawn6("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
12758
+ const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
12151
12759
  stdio: "inherit"
12152
12760
  });
12153
12761
  await new Promise((resolve2) => {
@@ -12191,7 +12799,7 @@ function handleRemove(projectRoot) {
12191
12799
  process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
12192
12800
  `);
12193
12801
  try {
12194
- execSync17(`docker sandbox rm ${sandboxName}`, {
12802
+ execSync18(`docker sandbox rm ${sandboxName}`, {
12195
12803
  encoding: "utf-8",
12196
12804
  stdio: ["pipe", "pipe", "pipe"],
12197
12805
  timeout: 15000
@@ -12438,9 +13046,9 @@ async function handleLogs(projectRoot, args) {
12438
13046
  dockerArgs.push(sandboxName);
12439
13047
  await runInteractiveCommand("docker", dockerArgs);
12440
13048
  }
12441
- function detectPackageManager(projectRoot) {
13049
+ function detectPackageManager2(projectRoot) {
12442
13050
  try {
12443
- const raw = readFileSync17(join24(projectRoot, "package.json"), "utf-8");
13051
+ const raw = readFileSync17(join25(projectRoot, "package.json"), "utf-8");
12444
13052
  const pkgJson = JSON.parse(raw);
12445
13053
  if (typeof pkgJson.packageManager === "string") {
12446
13054
  const name = pkgJson.packageManager.split("@")[0];
@@ -12449,13 +13057,13 @@ function detectPackageManager(projectRoot) {
12449
13057
  }
12450
13058
  }
12451
13059
  } catch {}
12452
- if (existsSync24(join24(projectRoot, "bun.lock")) || existsSync24(join24(projectRoot, "bun.lockb"))) {
13060
+ if (existsSync25(join25(projectRoot, "bun.lock")) || existsSync25(join25(projectRoot, "bun.lockb"))) {
12453
13061
  return "bun";
12454
13062
  }
12455
- if (existsSync24(join24(projectRoot, "yarn.lock"))) {
13063
+ if (existsSync25(join25(projectRoot, "yarn.lock"))) {
12456
13064
  return "yarn";
12457
13065
  }
12458
- if (existsSync24(join24(projectRoot, "pnpm-lock.yaml"))) {
13066
+ if (existsSync25(join25(projectRoot, "pnpm-lock.yaml"))) {
12459
13067
  return "pnpm";
12460
13068
  }
12461
13069
  return "npm";
@@ -12476,7 +13084,7 @@ async function runSandboxSetup(sandboxName, projectRoot) {
12476
13084
  const ecosystem = detectProjectEcosystem(projectRoot);
12477
13085
  const isJS = isJavaScriptEcosystem(ecosystem);
12478
13086
  if (isJS) {
12479
- const pm = detectPackageManager(projectRoot);
13087
+ const pm = detectPackageManager2(projectRoot);
12480
13088
  if (pm !== "npm") {
12481
13089
  await ensurePackageManagerInSandbox(sandboxName, pm);
12482
13090
  }
@@ -12504,8 +13112,8 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
12504
13112
  ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
12505
13113
  `);
12506
13114
  }
12507
- const setupScript = join24(projectRoot, ".locus", "sandbox-setup.sh");
12508
- if (existsSync24(setupScript)) {
13115
+ const setupScript = join25(projectRoot, ".locus", "sandbox-setup.sh");
13116
+ if (existsSync25(setupScript)) {
12509
13117
  process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
12510
13118
  `);
12511
13119
  const hookOk = await runInteractiveCommand("docker", [
@@ -12586,14 +13194,14 @@ function getActiveProviderSandbox(projectRoot, provider) {
12586
13194
  }
12587
13195
  function runInteractiveCommand(command, args) {
12588
13196
  return new Promise((resolve2) => {
12589
- const child = spawn6(command, args, { stdio: "inherit" });
13197
+ const child = spawn7(command, args, { stdio: "inherit" });
12590
13198
  child.on("close", (code) => resolve2(code === 0));
12591
13199
  child.on("error", () => resolve2(false));
12592
13200
  });
12593
13201
  }
12594
13202
  async function createProviderSandbox(provider, sandboxName, projectRoot) {
12595
13203
  try {
12596
- execSync17(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
13204
+ execSync18(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
12597
13205
  stdio: ["pipe", "pipe", "pipe"],
12598
13206
  timeout: 120000
12599
13207
  });
@@ -12614,7 +13222,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
12614
13222
  }
12615
13223
  async function ensurePackageManagerInSandbox(sandboxName, pm) {
12616
13224
  try {
12617
- execSync17(`docker sandbox exec ${sandboxName} which ${pm}`, {
13225
+ execSync18(`docker sandbox exec ${sandboxName} which ${pm}`, {
12618
13226
  stdio: ["pipe", "pipe", "pipe"],
12619
13227
  timeout: 5000
12620
13228
  });
@@ -12623,7 +13231,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
12623
13231
  process.stderr.write(`Installing ${bold2(pm)} in sandbox...
12624
13232
  `);
12625
13233
  try {
12626
- execSync17(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
13234
+ execSync18(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
12627
13235
  stdio: "inherit",
12628
13236
  timeout: 120000
12629
13237
  });
@@ -12635,7 +13243,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
12635
13243
  }
12636
13244
  async function ensureCodexInSandbox(sandboxName) {
12637
13245
  try {
12638
- execSync17(`docker sandbox exec ${sandboxName} which codex`, {
13246
+ execSync18(`docker sandbox exec ${sandboxName} which codex`, {
12639
13247
  stdio: ["pipe", "pipe", "pipe"],
12640
13248
  timeout: 5000
12641
13249
  });
@@ -12643,7 +13251,7 @@ async function ensureCodexInSandbox(sandboxName) {
12643
13251
  process.stderr.write(`Installing codex in sandbox...
12644
13252
  `);
12645
13253
  try {
12646
- 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 });
12647
13255
  } catch {
12648
13256
  process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
12649
13257
  `);
@@ -12652,7 +13260,7 @@ async function ensureCodexInSandbox(sandboxName) {
12652
13260
  }
12653
13261
  function isSandboxAlive(name) {
12654
13262
  try {
12655
- const output = execSync17("docker sandbox ls", {
13263
+ const output = execSync18("docker sandbox ls", {
12656
13264
  encoding: "utf-8",
12657
13265
  stdio: ["pipe", "pipe", "pipe"],
12658
13266
  timeout: 5000
@@ -12678,13 +13286,13 @@ init_context();
12678
13286
  init_logger();
12679
13287
  init_rate_limiter();
12680
13288
  init_terminal();
12681
- import { existsSync as existsSync25, readFileSync as readFileSync18 } from "node:fs";
12682
- 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";
12683
13291
  import { fileURLToPath } from "node:url";
12684
13292
  function getCliVersion() {
12685
13293
  const fallbackVersion = "0.0.0";
12686
- const packageJsonPath = join25(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
12687
- if (!existsSync25(packageJsonPath)) {
13294
+ const packageJsonPath = join26(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
13295
+ if (!existsSync26(packageJsonPath)) {
12688
13296
  return fallbackVersion;
12689
13297
  }
12690
13298
  try {
@@ -12949,7 +13557,7 @@ async function main() {
12949
13557
  try {
12950
13558
  const root = getGitRoot(cwd);
12951
13559
  if (isInitialized(root)) {
12952
- logDir = join25(root, ".locus", "logs");
13560
+ logDir = join26(root, ".locus", "logs");
12953
13561
  getRateLimiter(root);
12954
13562
  }
12955
13563
  } catch {}