@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.
- package/bin/locus.js +745 -137
- 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: [
|
|
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: [
|
|
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: [
|
|
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
|
|
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 =
|
|
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:
|
|
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,
|
|
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
|
|
8586
|
-
import { existsSync as
|
|
8587
|
-
import { join as
|
|
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
|
|
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
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
9612
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
9008
9613
|
function git3(args, cwd) {
|
|
9009
|
-
return
|
|
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
|
|
9139
|
-
mkdirSync as
|
|
9743
|
+
existsSync as existsSync19,
|
|
9744
|
+
mkdirSync as mkdirSync13,
|
|
9140
9745
|
readFileSync as readFileSync12,
|
|
9141
|
-
unlinkSync as
|
|
9746
|
+
unlinkSync as unlinkSync5,
|
|
9142
9747
|
writeFileSync as writeFileSync8
|
|
9143
9748
|
} from "node:fs";
|
|
9144
|
-
import { dirname as dirname6, join as
|
|
9749
|
+
import { dirname as dirname6, join as join19 } from "node:path";
|
|
9145
9750
|
function getRunStatePath(projectRoot) {
|
|
9146
|
-
return
|
|
9751
|
+
return join19(projectRoot, ".locus", "run-state.json");
|
|
9147
9752
|
}
|
|
9148
9753
|
function loadRunState(projectRoot) {
|
|
9149
9754
|
const path = getRunStatePath(projectRoot);
|
|
9150
|
-
if (!
|
|
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 (!
|
|
9163
|
-
|
|
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 (
|
|
9171
|
-
|
|
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
|
|
9311
|
-
import { existsSync as
|
|
9312
|
-
import { join as
|
|
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
|
|
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
|
|
9933
|
+
return join20(projectRoot, ".locus", "worktrees");
|
|
9329
9934
|
}
|
|
9330
9935
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
9331
|
-
return
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
10256
|
-
mkdirSync as
|
|
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
|
|
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
|
|
10897
|
+
return join21(projectRoot, ".locus", "plans");
|
|
10293
10898
|
}
|
|
10294
10899
|
function ensurePlansDir(projectRoot) {
|
|
10295
10900
|
const dir = getPlansDir(projectRoot);
|
|
10296
|
-
if (!
|
|
10297
|
-
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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(
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
10712
|
-
if (
|
|
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 =
|
|
10719
|
-
if (
|
|
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
|
|
10900
|
-
import { existsSync as
|
|
10901
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
11070
|
-
if (
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
11392
|
-
mkdirSync as
|
|
11996
|
+
existsSync as existsSync23,
|
|
11997
|
+
mkdirSync as mkdirSync15,
|
|
11393
11998
|
readdirSync as readdirSync9,
|
|
11394
11999
|
readFileSync as readFileSync15,
|
|
11395
|
-
unlinkSync as
|
|
12000
|
+
unlinkSync as unlinkSync6,
|
|
11396
12001
|
writeFileSync as writeFileSync10
|
|
11397
12002
|
} from "node:fs";
|
|
11398
|
-
import { join as
|
|
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
|
|
12025
|
+
return join23(projectRoot, ".locus", "discussions");
|
|
11421
12026
|
}
|
|
11422
12027
|
function ensureDiscussionsDir(projectRoot) {
|
|
11423
12028
|
const dir = getDiscussionsDir(projectRoot);
|
|
11424
|
-
if (!
|
|
11425
|
-
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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(
|
|
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 =
|
|
11692
|
-
if (
|
|
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 =
|
|
11699
|
-
if (
|
|
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
|
|
11772
|
-
import { join as
|
|
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
|
|
12400
|
+
return join24(projectRoot, ".locus", "artifacts");
|
|
11793
12401
|
}
|
|
11794
12402
|
function listArtifacts(projectRoot) {
|
|
11795
12403
|
const dir = getArtifactsDir(projectRoot);
|
|
11796
|
-
if (!
|
|
12404
|
+
if (!existsSync24(dir))
|
|
11797
12405
|
return [];
|
|
11798
12406
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
11799
|
-
const filePath =
|
|
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 =
|
|
11813
|
-
if (!
|
|
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
|
|
12590
|
+
import { execSync as execSync18, spawn as spawn7 } from "node:child_process";
|
|
11983
12591
|
import { createHash } from "node:crypto";
|
|
11984
|
-
import { existsSync as
|
|
11985
|
-
import { basename as basename4, join as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
13049
|
+
function detectPackageManager2(projectRoot) {
|
|
12442
13050
|
try {
|
|
12443
|
-
const raw = readFileSync17(
|
|
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 (
|
|
13060
|
+
if (existsSync25(join25(projectRoot, "bun.lock")) || existsSync25(join25(projectRoot, "bun.lockb"))) {
|
|
12453
13061
|
return "bun";
|
|
12454
13062
|
}
|
|
12455
|
-
if (
|
|
13063
|
+
if (existsSync25(join25(projectRoot, "yarn.lock"))) {
|
|
12456
13064
|
return "yarn";
|
|
12457
13065
|
}
|
|
12458
|
-
if (
|
|
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 =
|
|
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 =
|
|
12508
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
12682
|
-
import { join as
|
|
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 =
|
|
12687
|
-
if (!
|
|
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 =
|
|
13560
|
+
logDir = join26(root, ".locus", "logs");
|
|
12953
13561
|
getRateLimiter(root);
|
|
12954
13562
|
}
|
|
12955
13563
|
} catch {}
|