@locusai/cli 0.21.17 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/locus.js +737 -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"]
|
|
@@ -8195,8 +8765,15 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
8195
8765
|
new SlashCommandCompletion(getAllCommandNames()),
|
|
8196
8766
|
new FilePathCompletion(projectRoot)
|
|
8197
8767
|
]);
|
|
8768
|
+
const basePrompt = `${cyan2("locus")} ${dim2(">")} `;
|
|
8769
|
+
const voice = new VoiceController({
|
|
8770
|
+
onStateChange: (state) => {
|
|
8771
|
+
const indicator = voiceStatusIndicator(state);
|
|
8772
|
+
input.setPrompt(indicator ? `${indicator}${basePrompt}` : basePrompt);
|
|
8773
|
+
}
|
|
8774
|
+
});
|
|
8198
8775
|
const input = new InputHandler({
|
|
8199
|
-
prompt:
|
|
8776
|
+
prompt: basePrompt,
|
|
8200
8777
|
getHistory: () => history.getEntries(),
|
|
8201
8778
|
onTab: (text) => completion.complete(text)
|
|
8202
8779
|
});
|
|
@@ -8249,12 +8826,33 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
8249
8826
|
onVerboseToggle: () => {
|
|
8250
8827
|
verbose = !verbose;
|
|
8251
8828
|
},
|
|
8252
|
-
getVerbose: () => verbose
|
|
8829
|
+
getVerbose: () => verbose,
|
|
8830
|
+
onVoiceToggle: () => {
|
|
8831
|
+
if (voice.getState() !== "idle")
|
|
8832
|
+
return;
|
|
8833
|
+
const started = voice.startRecording();
|
|
8834
|
+
if (started) {
|
|
8835
|
+
process.stderr.write(`${dim2("Recording... press")} ${bold2("Enter")} ${dim2("to stop")}
|
|
8836
|
+
`);
|
|
8837
|
+
}
|
|
8838
|
+
}
|
|
8253
8839
|
};
|
|
8254
8840
|
while (!shouldExit) {
|
|
8255
8841
|
const result = await input.readline();
|
|
8256
8842
|
switch (result.type) {
|
|
8257
8843
|
case "submit": {
|
|
8844
|
+
if (voice.getState() === "recording") {
|
|
8845
|
+
process.stderr.write(`${dim2("Transcribing...")}
|
|
8846
|
+
`);
|
|
8847
|
+
const transcribed = await voice.stopAndTranscribe();
|
|
8848
|
+
if (transcribed) {
|
|
8849
|
+
input.setInitialBuffer(transcribed);
|
|
8850
|
+
} else {
|
|
8851
|
+
process.stderr.write(`${yellow2("!")} No speech detected.
|
|
8852
|
+
`);
|
|
8853
|
+
}
|
|
8854
|
+
continue;
|
|
8855
|
+
}
|
|
8258
8856
|
const text = result.text.trim();
|
|
8259
8857
|
if (!text)
|
|
8260
8858
|
continue;
|
|
@@ -8305,6 +8903,7 @@ ${red2("✗")} ${msg}
|
|
|
8305
8903
|
break;
|
|
8306
8904
|
}
|
|
8307
8905
|
}
|
|
8906
|
+
voice.cancel();
|
|
8308
8907
|
const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
|
|
8309
8908
|
if (shouldPersistOnExit) {
|
|
8310
8909
|
sessionManager.save(session);
|
|
@@ -8370,7 +8969,7 @@ function printWelcome(session) {
|
|
|
8370
8969
|
}
|
|
8371
8970
|
process.stderr.write(`
|
|
8372
8971
|
`);
|
|
8373
|
-
process.stderr.write(` ${dim2("Type /help for commands, Shift+Enter for newline,
|
|
8972
|
+
process.stderr.write(` ${dim2("Type /help for commands, Shift+Enter for newline, /v for voice input")}
|
|
8374
8973
|
`);
|
|
8375
8974
|
process.stderr.write(`
|
|
8376
8975
|
`);
|
|
@@ -8390,6 +8989,7 @@ var init_repl = __esm(() => {
|
|
|
8390
8989
|
init_input_history();
|
|
8391
8990
|
init_model_config();
|
|
8392
8991
|
init_session_manager();
|
|
8992
|
+
init_voice();
|
|
8393
8993
|
LOCUS_LOGO = [
|
|
8394
8994
|
" ▄█ ",
|
|
8395
8995
|
" ▄▄████▄▄▄▄ ",
|
|
@@ -8587,11 +9187,11 @@ var init_exec = __esm(() => {
|
|
|
8587
9187
|
});
|
|
8588
9188
|
|
|
8589
9189
|
// src/core/submodule.ts
|
|
8590
|
-
import { execSync as
|
|
8591
|
-
import { existsSync as
|
|
8592
|
-
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";
|
|
8593
9193
|
function git2(args, cwd) {
|
|
8594
|
-
return
|
|
9194
|
+
return execSync11(`git ${args}`, {
|
|
8595
9195
|
cwd,
|
|
8596
9196
|
encoding: "utf-8",
|
|
8597
9197
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8605,7 +9205,7 @@ function gitSafe(args, cwd) {
|
|
|
8605
9205
|
}
|
|
8606
9206
|
}
|
|
8607
9207
|
function hasSubmodules(cwd) {
|
|
8608
|
-
return
|
|
9208
|
+
return existsSync18(join18(cwd, ".gitmodules"));
|
|
8609
9209
|
}
|
|
8610
9210
|
function listSubmodules(cwd) {
|
|
8611
9211
|
if (!hasSubmodules(cwd))
|
|
@@ -8625,7 +9225,7 @@ function listSubmodules(cwd) {
|
|
|
8625
9225
|
continue;
|
|
8626
9226
|
submodules.push({
|
|
8627
9227
|
path,
|
|
8628
|
-
absolutePath:
|
|
9228
|
+
absolutePath: join18(cwd, path),
|
|
8629
9229
|
dirty
|
|
8630
9230
|
});
|
|
8631
9231
|
}
|
|
@@ -8638,7 +9238,7 @@ function getDirtySubmodules(cwd) {
|
|
|
8638
9238
|
const submodules = listSubmodules(cwd);
|
|
8639
9239
|
const dirty = [];
|
|
8640
9240
|
for (const sub of submodules) {
|
|
8641
|
-
if (!
|
|
9241
|
+
if (!existsSync18(sub.absolutePath))
|
|
8642
9242
|
continue;
|
|
8643
9243
|
const status = gitSafe("status --porcelain", sub.absolutePath);
|
|
8644
9244
|
if (status && status.trim().length > 0) {
|
|
@@ -8659,7 +9259,7 @@ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
|
|
|
8659
9259
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
8660
9260
|
|
|
8661
9261
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
8662
|
-
|
|
9262
|
+
execSync11("git commit -F -", {
|
|
8663
9263
|
input: message,
|
|
8664
9264
|
cwd: sub.absolutePath,
|
|
8665
9265
|
encoding: "utf-8",
|
|
@@ -8725,7 +9325,7 @@ function pushSubmoduleBranches(cwd) {
|
|
|
8725
9325
|
const log = getLogger();
|
|
8726
9326
|
const submodules = listSubmodules(cwd);
|
|
8727
9327
|
for (const sub of submodules) {
|
|
8728
|
-
if (!
|
|
9328
|
+
if (!existsSync18(sub.absolutePath))
|
|
8729
9329
|
continue;
|
|
8730
9330
|
const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
|
|
8731
9331
|
if (!branch || branch === "HEAD")
|
|
@@ -8746,7 +9346,7 @@ var init_submodule = __esm(() => {
|
|
|
8746
9346
|
});
|
|
8747
9347
|
|
|
8748
9348
|
// src/core/agent.ts
|
|
8749
|
-
import { execSync as
|
|
9349
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
8750
9350
|
async function executeIssue(projectRoot, options) {
|
|
8751
9351
|
const log = getLogger();
|
|
8752
9352
|
const timer = createTimer();
|
|
@@ -8775,7 +9375,7 @@ ${cyan2("●")} ${bold2(`#${issueNumber}`)} ${issue.title}
|
|
|
8775
9375
|
}
|
|
8776
9376
|
let issueComments = [];
|
|
8777
9377
|
try {
|
|
8778
|
-
const commentsRaw =
|
|
9378
|
+
const commentsRaw = execSync12(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8779
9379
|
if (commentsRaw) {
|
|
8780
9380
|
issueComments = commentsRaw.split(`
|
|
8781
9381
|
`).filter(Boolean);
|
|
@@ -8939,12 +9539,12 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
|
|
|
8939
9539
|
}
|
|
8940
9540
|
async function createIssuePR(projectRoot, config, issue) {
|
|
8941
9541
|
try {
|
|
8942
|
-
const currentBranch =
|
|
9542
|
+
const currentBranch = execSync12("git rev-parse --abbrev-ref HEAD", {
|
|
8943
9543
|
cwd: projectRoot,
|
|
8944
9544
|
encoding: "utf-8",
|
|
8945
9545
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8946
9546
|
}).trim();
|
|
8947
|
-
const diff =
|
|
9547
|
+
const diff = execSync12(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8948
9548
|
cwd: projectRoot,
|
|
8949
9549
|
encoding: "utf-8",
|
|
8950
9550
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8954,7 +9554,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
8954
9554
|
return;
|
|
8955
9555
|
}
|
|
8956
9556
|
pushSubmoduleBranches(projectRoot);
|
|
8957
|
-
|
|
9557
|
+
execSync12(`git push -u origin ${currentBranch}`, {
|
|
8958
9558
|
cwd: projectRoot,
|
|
8959
9559
|
encoding: "utf-8",
|
|
8960
9560
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9009,9 +9609,9 @@ var init_agent = __esm(() => {
|
|
|
9009
9609
|
});
|
|
9010
9610
|
|
|
9011
9611
|
// src/core/conflict.ts
|
|
9012
|
-
import { execSync as
|
|
9612
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
9013
9613
|
function git3(args, cwd) {
|
|
9014
|
-
return
|
|
9614
|
+
return execSync13(`git ${args}`, {
|
|
9015
9615
|
cwd,
|
|
9016
9616
|
encoding: "utf-8",
|
|
9017
9617
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9140,19 +9740,19 @@ var init_conflict = __esm(() => {
|
|
|
9140
9740
|
|
|
9141
9741
|
// src/core/run-state.ts
|
|
9142
9742
|
import {
|
|
9143
|
-
existsSync as
|
|
9144
|
-
mkdirSync as
|
|
9743
|
+
existsSync as existsSync19,
|
|
9744
|
+
mkdirSync as mkdirSync13,
|
|
9145
9745
|
readFileSync as readFileSync12,
|
|
9146
|
-
unlinkSync as
|
|
9746
|
+
unlinkSync as unlinkSync5,
|
|
9147
9747
|
writeFileSync as writeFileSync8
|
|
9148
9748
|
} from "node:fs";
|
|
9149
|
-
import { dirname as dirname6, join as
|
|
9749
|
+
import { dirname as dirname6, join as join19 } from "node:path";
|
|
9150
9750
|
function getRunStatePath(projectRoot) {
|
|
9151
|
-
return
|
|
9751
|
+
return join19(projectRoot, ".locus", "run-state.json");
|
|
9152
9752
|
}
|
|
9153
9753
|
function loadRunState(projectRoot) {
|
|
9154
9754
|
const path = getRunStatePath(projectRoot);
|
|
9155
|
-
if (!
|
|
9755
|
+
if (!existsSync19(path))
|
|
9156
9756
|
return null;
|
|
9157
9757
|
try {
|
|
9158
9758
|
return JSON.parse(readFileSync12(path, "utf-8"));
|
|
@@ -9164,16 +9764,16 @@ function loadRunState(projectRoot) {
|
|
|
9164
9764
|
function saveRunState(projectRoot, state) {
|
|
9165
9765
|
const path = getRunStatePath(projectRoot);
|
|
9166
9766
|
const dir = dirname6(path);
|
|
9167
|
-
if (!
|
|
9168
|
-
|
|
9767
|
+
if (!existsSync19(dir)) {
|
|
9768
|
+
mkdirSync13(dir, { recursive: true });
|
|
9169
9769
|
}
|
|
9170
9770
|
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
9171
9771
|
`, "utf-8");
|
|
9172
9772
|
}
|
|
9173
9773
|
function clearRunState(projectRoot) {
|
|
9174
9774
|
const path = getRunStatePath(projectRoot);
|
|
9175
|
-
if (
|
|
9176
|
-
|
|
9775
|
+
if (existsSync19(path)) {
|
|
9776
|
+
unlinkSync5(path);
|
|
9177
9777
|
}
|
|
9178
9778
|
}
|
|
9179
9779
|
function createSprintRunState(sprint, branch, issues) {
|
|
@@ -9312,11 +9912,11 @@ var init_shutdown = __esm(() => {
|
|
|
9312
9912
|
});
|
|
9313
9913
|
|
|
9314
9914
|
// src/core/worktree.ts
|
|
9315
|
-
import { execSync as
|
|
9316
|
-
import { existsSync as
|
|
9317
|
-
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";
|
|
9318
9918
|
function git4(args, cwd) {
|
|
9319
|
-
return
|
|
9919
|
+
return execSync14(`git ${args}`, {
|
|
9320
9920
|
cwd,
|
|
9321
9921
|
encoding: "utf-8",
|
|
9322
9922
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9330,10 +9930,10 @@ function gitSafe3(args, cwd) {
|
|
|
9330
9930
|
}
|
|
9331
9931
|
}
|
|
9332
9932
|
function getWorktreeDir(projectRoot) {
|
|
9333
|
-
return
|
|
9933
|
+
return join20(projectRoot, ".locus", "worktrees");
|
|
9334
9934
|
}
|
|
9335
9935
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
9336
|
-
return
|
|
9936
|
+
return join20(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
9337
9937
|
}
|
|
9338
9938
|
function generateBranchName(issueNumber) {
|
|
9339
9939
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -9341,7 +9941,7 @@ function generateBranchName(issueNumber) {
|
|
|
9341
9941
|
}
|
|
9342
9942
|
function getWorktreeBranch(worktreePath) {
|
|
9343
9943
|
try {
|
|
9344
|
-
return
|
|
9944
|
+
return execSync14("git branch --show-current", {
|
|
9345
9945
|
cwd: worktreePath,
|
|
9346
9946
|
encoding: "utf-8",
|
|
9347
9947
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9353,7 +9953,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
9353
9953
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
9354
9954
|
const log = getLogger();
|
|
9355
9955
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
9356
|
-
if (
|
|
9956
|
+
if (existsSync20(worktreePath)) {
|
|
9357
9957
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
9358
9958
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
9359
9959
|
return {
|
|
@@ -9381,7 +9981,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
9381
9981
|
function removeWorktree(projectRoot, issueNumber) {
|
|
9382
9982
|
const log = getLogger();
|
|
9383
9983
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
9384
|
-
if (!
|
|
9984
|
+
if (!existsSync20(worktreePath)) {
|
|
9385
9985
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
9386
9986
|
return;
|
|
9387
9987
|
}
|
|
@@ -9400,7 +10000,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
9400
10000
|
function listWorktrees(projectRoot) {
|
|
9401
10001
|
const log = getLogger();
|
|
9402
10002
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
9403
|
-
if (!
|
|
10003
|
+
if (!existsSync20(worktreeDir)) {
|
|
9404
10004
|
return [];
|
|
9405
10005
|
}
|
|
9406
10006
|
const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -9420,7 +10020,7 @@ function listWorktrees(projectRoot) {
|
|
|
9420
10020
|
if (!match)
|
|
9421
10021
|
continue;
|
|
9422
10022
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
9423
|
-
const path =
|
|
10023
|
+
const path = join20(worktreeDir, entry);
|
|
9424
10024
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
9425
10025
|
let resolvedPath;
|
|
9426
10026
|
try {
|
|
@@ -9468,7 +10068,7 @@ var exports_run = {};
|
|
|
9468
10068
|
__export(exports_run, {
|
|
9469
10069
|
runCommand: () => runCommand
|
|
9470
10070
|
});
|
|
9471
|
-
import { execSync as
|
|
10071
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
9472
10072
|
function resolveExecutionContext(config, modelOverride) {
|
|
9473
10073
|
const model = modelOverride ?? config.ai.model;
|
|
9474
10074
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
@@ -9628,7 +10228,7 @@ ${yellow2("⚠")} A sprint run is already in progress.
|
|
|
9628
10228
|
}
|
|
9629
10229
|
if (!flags.dryRun) {
|
|
9630
10230
|
try {
|
|
9631
|
-
|
|
10231
|
+
execSync15(`git checkout -B ${branchName}`, {
|
|
9632
10232
|
cwd: projectRoot,
|
|
9633
10233
|
encoding: "utf-8",
|
|
9634
10234
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9678,7 +10278,7 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
|
|
|
9678
10278
|
let sprintContext;
|
|
9679
10279
|
if (i > 0 && !flags.dryRun) {
|
|
9680
10280
|
try {
|
|
9681
|
-
sprintContext =
|
|
10281
|
+
sprintContext = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
9682
10282
|
cwd: projectRoot,
|
|
9683
10283
|
encoding: "utf-8",
|
|
9684
10284
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9743,7 +10343,7 @@ ${bold2("Summary:")}
|
|
|
9743
10343
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
9744
10344
|
if (prNumber !== undefined) {
|
|
9745
10345
|
try {
|
|
9746
|
-
|
|
10346
|
+
execSync15(`git checkout ${config.agent.baseBranch}`, {
|
|
9747
10347
|
cwd: projectRoot,
|
|
9748
10348
|
encoding: "utf-8",
|
|
9749
10349
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9788,7 +10388,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
|
|
|
9788
10388
|
`);
|
|
9789
10389
|
if (!flags.dryRun) {
|
|
9790
10390
|
try {
|
|
9791
|
-
|
|
10391
|
+
execSync15(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
|
|
9792
10392
|
cwd: projectRoot,
|
|
9793
10393
|
encoding: "utf-8",
|
|
9794
10394
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9813,7 +10413,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
|
|
|
9813
10413
|
if (!flags.dryRun) {
|
|
9814
10414
|
if (result.success) {
|
|
9815
10415
|
try {
|
|
9816
|
-
|
|
10416
|
+
execSync15(`git checkout ${config.agent.baseBranch}`, {
|
|
9817
10417
|
cwd: projectRoot,
|
|
9818
10418
|
encoding: "utf-8",
|
|
9819
10419
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9950,13 +10550,13 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
|
9950
10550
|
`);
|
|
9951
10551
|
if (state.type === "sprint" && state.branch) {
|
|
9952
10552
|
try {
|
|
9953
|
-
const currentBranch =
|
|
10553
|
+
const currentBranch = execSync15("git rev-parse --abbrev-ref HEAD", {
|
|
9954
10554
|
cwd: projectRoot,
|
|
9955
10555
|
encoding: "utf-8",
|
|
9956
10556
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9957
10557
|
}).trim();
|
|
9958
10558
|
if (currentBranch !== state.branch) {
|
|
9959
|
-
|
|
10559
|
+
execSync15(`git checkout ${state.branch}`, {
|
|
9960
10560
|
cwd: projectRoot,
|
|
9961
10561
|
encoding: "utf-8",
|
|
9962
10562
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10023,7 +10623,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
|
|
|
10023
10623
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
10024
10624
|
if (prNumber !== undefined) {
|
|
10025
10625
|
try {
|
|
10026
|
-
|
|
10626
|
+
execSync15(`git checkout ${config.agent.baseBranch}`, {
|
|
10027
10627
|
cwd: projectRoot,
|
|
10028
10628
|
encoding: "utf-8",
|
|
10029
10629
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10059,14 +10659,14 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
10059
10659
|
process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
|
|
10060
10660
|
`);
|
|
10061
10661
|
}
|
|
10062
|
-
const status =
|
|
10662
|
+
const status = execSync15("git status --porcelain", {
|
|
10063
10663
|
cwd: projectRoot,
|
|
10064
10664
|
encoding: "utf-8",
|
|
10065
10665
|
stdio: ["pipe", "pipe", "pipe"]
|
|
10066
10666
|
}).trim();
|
|
10067
10667
|
if (!status)
|
|
10068
10668
|
return;
|
|
10069
|
-
|
|
10669
|
+
execSync15("git add -A", {
|
|
10070
10670
|
cwd: projectRoot,
|
|
10071
10671
|
encoding: "utf-8",
|
|
10072
10672
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10074,7 +10674,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
10074
10674
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
10075
10675
|
|
|
10076
10676
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
10077
|
-
|
|
10677
|
+
execSync15(`git commit -F -`, {
|
|
10078
10678
|
input: message,
|
|
10079
10679
|
cwd: projectRoot,
|
|
10080
10680
|
encoding: "utf-8",
|
|
@@ -10088,7 +10688,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
10088
10688
|
if (!config.agent.autoPR)
|
|
10089
10689
|
return;
|
|
10090
10690
|
try {
|
|
10091
|
-
const diff =
|
|
10691
|
+
const diff = execSync15(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
10092
10692
|
cwd: projectRoot,
|
|
10093
10693
|
encoding: "utf-8",
|
|
10094
10694
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10099,7 +10699,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
10099
10699
|
return;
|
|
10100
10700
|
}
|
|
10101
10701
|
pushSubmoduleBranches(projectRoot);
|
|
10102
|
-
|
|
10702
|
+
execSync15(`git push -u origin ${branchName}`, {
|
|
10103
10703
|
cwd: projectRoot,
|
|
10104
10704
|
encoding: "utf-8",
|
|
10105
10705
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10257,13 +10857,13 @@ __export(exports_plan, {
|
|
|
10257
10857
|
parsePlanArgs: () => parsePlanArgs
|
|
10258
10858
|
});
|
|
10259
10859
|
import {
|
|
10260
|
-
existsSync as
|
|
10261
|
-
mkdirSync as
|
|
10860
|
+
existsSync as existsSync21,
|
|
10861
|
+
mkdirSync as mkdirSync14,
|
|
10262
10862
|
readdirSync as readdirSync8,
|
|
10263
10863
|
readFileSync as readFileSync13,
|
|
10264
10864
|
writeFileSync as writeFileSync9
|
|
10265
10865
|
} from "node:fs";
|
|
10266
|
-
import { join as
|
|
10866
|
+
import { join as join21 } from "node:path";
|
|
10267
10867
|
function printHelp() {
|
|
10268
10868
|
process.stderr.write(`
|
|
10269
10869
|
${bold2("locus plan")} — AI-powered sprint planning
|
|
@@ -10294,12 +10894,12 @@ function normalizeSprintName(name) {
|
|
|
10294
10894
|
return name.trim().toLowerCase();
|
|
10295
10895
|
}
|
|
10296
10896
|
function getPlansDir(projectRoot) {
|
|
10297
|
-
return
|
|
10897
|
+
return join21(projectRoot, ".locus", "plans");
|
|
10298
10898
|
}
|
|
10299
10899
|
function ensurePlansDir(projectRoot) {
|
|
10300
10900
|
const dir = getPlansDir(projectRoot);
|
|
10301
|
-
if (!
|
|
10302
|
-
|
|
10901
|
+
if (!existsSync21(dir)) {
|
|
10902
|
+
mkdirSync14(dir, { recursive: true });
|
|
10303
10903
|
}
|
|
10304
10904
|
return dir;
|
|
10305
10905
|
}
|
|
@@ -10308,14 +10908,14 @@ function generateId() {
|
|
|
10308
10908
|
}
|
|
10309
10909
|
function loadPlanFile(projectRoot, id) {
|
|
10310
10910
|
const dir = getPlansDir(projectRoot);
|
|
10311
|
-
if (!
|
|
10911
|
+
if (!existsSync21(dir))
|
|
10312
10912
|
return null;
|
|
10313
10913
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
|
|
10314
10914
|
const match = files.find((f) => f.startsWith(id));
|
|
10315
10915
|
if (!match)
|
|
10316
10916
|
return null;
|
|
10317
10917
|
try {
|
|
10318
|
-
const content = readFileSync13(
|
|
10918
|
+
const content = readFileSync13(join21(dir, match), "utf-8");
|
|
10319
10919
|
return JSON.parse(content);
|
|
10320
10920
|
} catch {
|
|
10321
10921
|
return null;
|
|
@@ -10361,7 +10961,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
10361
10961
|
}
|
|
10362
10962
|
function handleListPlans(projectRoot) {
|
|
10363
10963
|
const dir = getPlansDir(projectRoot);
|
|
10364
|
-
if (!
|
|
10964
|
+
if (!existsSync21(dir)) {
|
|
10365
10965
|
process.stderr.write(`${dim2("No saved plans yet.")}
|
|
10366
10966
|
`);
|
|
10367
10967
|
return;
|
|
@@ -10379,7 +10979,7 @@ ${bold2("Saved Plans:")}
|
|
|
10379
10979
|
for (const file of files) {
|
|
10380
10980
|
const id = file.replace(".json", "");
|
|
10381
10981
|
try {
|
|
10382
|
-
const content = readFileSync13(
|
|
10982
|
+
const content = readFileSync13(join21(dir, file), "utf-8");
|
|
10383
10983
|
const plan = JSON.parse(content);
|
|
10384
10984
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
10385
10985
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -10490,7 +11090,7 @@ ${bold2("Approving plan:")}
|
|
|
10490
11090
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
10491
11091
|
const id = generateId();
|
|
10492
11092
|
const plansDir = ensurePlansDir(projectRoot);
|
|
10493
|
-
const planPath =
|
|
11093
|
+
const planPath = join21(plansDir, `${id}.json`);
|
|
10494
11094
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
10495
11095
|
const displayDirective = directive;
|
|
10496
11096
|
process.stderr.write(`
|
|
@@ -10532,7 +11132,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
|
|
|
10532
11132
|
`);
|
|
10533
11133
|
return;
|
|
10534
11134
|
}
|
|
10535
|
-
if (!
|
|
11135
|
+
if (!existsSync21(planPath)) {
|
|
10536
11136
|
process.stderr.write(`
|
|
10537
11137
|
${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
10538
11138
|
`);
|
|
@@ -10713,15 +11313,15 @@ ${directive}${sprintName ? `
|
|
|
10713
11313
|
|
|
10714
11314
|
**Sprint:** ${sprintName}` : ""}
|
|
10715
11315
|
</directive>`);
|
|
10716
|
-
const locusPath =
|
|
10717
|
-
if (
|
|
11316
|
+
const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
|
|
11317
|
+
if (existsSync21(locusPath)) {
|
|
10718
11318
|
const content = readFileSync13(locusPath, "utf-8");
|
|
10719
11319
|
parts.push(`<project-context>
|
|
10720
11320
|
${content.slice(0, 3000)}
|
|
10721
11321
|
</project-context>`);
|
|
10722
11322
|
}
|
|
10723
|
-
const learningsPath =
|
|
10724
|
-
if (
|
|
11323
|
+
const learningsPath = join21(projectRoot, ".locus", "LEARNINGS.md");
|
|
11324
|
+
if (existsSync21(learningsPath)) {
|
|
10725
11325
|
const content = readFileSync13(learningsPath, "utf-8");
|
|
10726
11326
|
parts.push(`<past-learnings>
|
|
10727
11327
|
${content.slice(0, 2000)}
|
|
@@ -10901,9 +11501,9 @@ var exports_review = {};
|
|
|
10901
11501
|
__export(exports_review, {
|
|
10902
11502
|
reviewCommand: () => reviewCommand
|
|
10903
11503
|
});
|
|
10904
|
-
import { execFileSync as execFileSync2, execSync as
|
|
10905
|
-
import { existsSync as
|
|
10906
|
-
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";
|
|
10907
11507
|
function printHelp2() {
|
|
10908
11508
|
process.stderr.write(`
|
|
10909
11509
|
${bold2("locus review")} — AI-powered code review
|
|
@@ -10987,7 +11587,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
|
|
|
10987
11587
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
10988
11588
|
let prInfo;
|
|
10989
11589
|
try {
|
|
10990
|
-
const result =
|
|
11590
|
+
const result = execSync16(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10991
11591
|
const raw = JSON.parse(result);
|
|
10992
11592
|
prInfo = {
|
|
10993
11593
|
number: raw.number,
|
|
@@ -11071,8 +11671,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
11071
11671
|
parts.push(`<role>
|
|
11072
11672
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
11073
11673
|
</role>`);
|
|
11074
|
-
const locusPath =
|
|
11075
|
-
if (
|
|
11674
|
+
const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
|
|
11675
|
+
if (existsSync22(locusPath)) {
|
|
11076
11676
|
const content = readFileSync14(locusPath, "utf-8");
|
|
11077
11677
|
parts.push(`<project-context>
|
|
11078
11678
|
${content.slice(0, 2000)}
|
|
@@ -11134,7 +11734,7 @@ var exports_iterate = {};
|
|
|
11134
11734
|
__export(exports_iterate, {
|
|
11135
11735
|
iterateCommand: () => iterateCommand
|
|
11136
11736
|
});
|
|
11137
|
-
import { execSync as
|
|
11737
|
+
import { execSync as execSync17 } from "node:child_process";
|
|
11138
11738
|
function printHelp3() {
|
|
11139
11739
|
process.stderr.write(`
|
|
11140
11740
|
${bold2("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -11352,12 +11952,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
|
|
|
11352
11952
|
}
|
|
11353
11953
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
11354
11954
|
try {
|
|
11355
|
-
const result =
|
|
11955
|
+
const result = execSync17(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
11356
11956
|
const parsed = JSON.parse(result);
|
|
11357
11957
|
if (parsed.length > 0) {
|
|
11358
11958
|
return parsed[0].number;
|
|
11359
11959
|
}
|
|
11360
|
-
const branchResult =
|
|
11960
|
+
const branchResult = execSync17(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
11361
11961
|
const branchParsed = JSON.parse(branchResult);
|
|
11362
11962
|
if (branchParsed.length > 0) {
|
|
11363
11963
|
return branchParsed[0].number;
|
|
@@ -11393,14 +11993,14 @@ __export(exports_discuss, {
|
|
|
11393
11993
|
discussCommand: () => discussCommand
|
|
11394
11994
|
});
|
|
11395
11995
|
import {
|
|
11396
|
-
existsSync as
|
|
11397
|
-
mkdirSync as
|
|
11996
|
+
existsSync as existsSync23,
|
|
11997
|
+
mkdirSync as mkdirSync15,
|
|
11398
11998
|
readdirSync as readdirSync9,
|
|
11399
11999
|
readFileSync as readFileSync15,
|
|
11400
|
-
unlinkSync as
|
|
12000
|
+
unlinkSync as unlinkSync6,
|
|
11401
12001
|
writeFileSync as writeFileSync10
|
|
11402
12002
|
} from "node:fs";
|
|
11403
|
-
import { join as
|
|
12003
|
+
import { join as join23 } from "node:path";
|
|
11404
12004
|
function printHelp4() {
|
|
11405
12005
|
process.stderr.write(`
|
|
11406
12006
|
${bold2("locus discuss")} — AI-powered architectural discussions
|
|
@@ -11422,12 +12022,12 @@ ${bold2("Examples:")}
|
|
|
11422
12022
|
`);
|
|
11423
12023
|
}
|
|
11424
12024
|
function getDiscussionsDir(projectRoot) {
|
|
11425
|
-
return
|
|
12025
|
+
return join23(projectRoot, ".locus", "discussions");
|
|
11426
12026
|
}
|
|
11427
12027
|
function ensureDiscussionsDir(projectRoot) {
|
|
11428
12028
|
const dir = getDiscussionsDir(projectRoot);
|
|
11429
|
-
if (!
|
|
11430
|
-
|
|
12029
|
+
if (!existsSync23(dir)) {
|
|
12030
|
+
mkdirSync15(dir, { recursive: true });
|
|
11431
12031
|
}
|
|
11432
12032
|
return dir;
|
|
11433
12033
|
}
|
|
@@ -11461,7 +12061,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
11461
12061
|
}
|
|
11462
12062
|
function listDiscussions(projectRoot) {
|
|
11463
12063
|
const dir = getDiscussionsDir(projectRoot);
|
|
11464
|
-
if (!
|
|
12064
|
+
if (!existsSync23(dir)) {
|
|
11465
12065
|
process.stderr.write(`${dim2("No discussions yet.")}
|
|
11466
12066
|
`);
|
|
11467
12067
|
return;
|
|
@@ -11478,7 +12078,7 @@ ${bold2("Discussions:")}
|
|
|
11478
12078
|
`);
|
|
11479
12079
|
for (const file of files) {
|
|
11480
12080
|
const id = file.replace(".md", "");
|
|
11481
|
-
const content = readFileSync15(
|
|
12081
|
+
const content = readFileSync15(join23(dir, file), "utf-8");
|
|
11482
12082
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
11483
12083
|
const title = titleMatch ? titleMatch[1] : id;
|
|
11484
12084
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -11496,7 +12096,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
11496
12096
|
return;
|
|
11497
12097
|
}
|
|
11498
12098
|
const dir = getDiscussionsDir(projectRoot);
|
|
11499
|
-
if (!
|
|
12099
|
+
if (!existsSync23(dir)) {
|
|
11500
12100
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
11501
12101
|
`);
|
|
11502
12102
|
return;
|
|
@@ -11508,7 +12108,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
11508
12108
|
`);
|
|
11509
12109
|
return;
|
|
11510
12110
|
}
|
|
11511
|
-
const content = readFileSync15(
|
|
12111
|
+
const content = readFileSync15(join23(dir, match), "utf-8");
|
|
11512
12112
|
process.stdout.write(`${content}
|
|
11513
12113
|
`);
|
|
11514
12114
|
}
|
|
@@ -11519,7 +12119,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
11519
12119
|
return;
|
|
11520
12120
|
}
|
|
11521
12121
|
const dir = getDiscussionsDir(projectRoot);
|
|
11522
|
-
if (!
|
|
12122
|
+
if (!existsSync23(dir)) {
|
|
11523
12123
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
11524
12124
|
`);
|
|
11525
12125
|
return;
|
|
@@ -11531,7 +12131,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
11531
12131
|
`);
|
|
11532
12132
|
return;
|
|
11533
12133
|
}
|
|
11534
|
-
|
|
12134
|
+
unlinkSync6(join23(dir, match));
|
|
11535
12135
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
11536
12136
|
`);
|
|
11537
12137
|
}
|
|
@@ -11544,7 +12144,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
11544
12144
|
return;
|
|
11545
12145
|
}
|
|
11546
12146
|
const dir = getDiscussionsDir(projectRoot);
|
|
11547
|
-
if (!
|
|
12147
|
+
if (!existsSync23(dir)) {
|
|
11548
12148
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
11549
12149
|
`);
|
|
11550
12150
|
return;
|
|
@@ -11556,7 +12156,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
11556
12156
|
`);
|
|
11557
12157
|
return;
|
|
11558
12158
|
}
|
|
11559
|
-
const content = readFileSync15(
|
|
12159
|
+
const content = readFileSync15(join23(dir, match), "utf-8");
|
|
11560
12160
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
11561
12161
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
11562
12162
|
await planCommand(projectRoot, [
|
|
@@ -11681,7 +12281,7 @@ ${turn.content}`;
|
|
|
11681
12281
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
11682
12282
|
].join(`
|
|
11683
12283
|
`);
|
|
11684
|
-
writeFileSync10(
|
|
12284
|
+
writeFileSync10(join23(dir, `${id}.md`), markdown, "utf-8");
|
|
11685
12285
|
process.stderr.write(`
|
|
11686
12286
|
${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
11687
12287
|
`);
|
|
@@ -11696,15 +12296,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
11696
12296
|
parts.push(`<role>
|
|
11697
12297
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
11698
12298
|
</role>`);
|
|
11699
|
-
const locusPath =
|
|
11700
|
-
if (
|
|
12299
|
+
const locusPath = join23(projectRoot, ".locus", "LOCUS.md");
|
|
12300
|
+
if (existsSync23(locusPath)) {
|
|
11701
12301
|
const content = readFileSync15(locusPath, "utf-8");
|
|
11702
12302
|
parts.push(`<project-context>
|
|
11703
12303
|
${content.slice(0, 3000)}
|
|
11704
12304
|
</project-context>`);
|
|
11705
12305
|
}
|
|
11706
|
-
const learningsPath =
|
|
11707
|
-
if (
|
|
12306
|
+
const learningsPath = join23(projectRoot, ".locus", "LEARNINGS.md");
|
|
12307
|
+
if (existsSync23(learningsPath)) {
|
|
11708
12308
|
const content = readFileSync15(learningsPath, "utf-8");
|
|
11709
12309
|
parts.push(`<past-learnings>
|
|
11710
12310
|
${content.slice(0, 2000)}
|
|
@@ -11776,8 +12376,8 @@ __export(exports_artifacts, {
|
|
|
11776
12376
|
formatDate: () => formatDate2,
|
|
11777
12377
|
artifactsCommand: () => artifactsCommand
|
|
11778
12378
|
});
|
|
11779
|
-
import { existsSync as
|
|
11780
|
-
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";
|
|
11781
12381
|
function printHelp5() {
|
|
11782
12382
|
process.stderr.write(`
|
|
11783
12383
|
${bold2("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -11797,14 +12397,14 @@ ${dim2("Artifact names support partial matching.")}
|
|
|
11797
12397
|
`);
|
|
11798
12398
|
}
|
|
11799
12399
|
function getArtifactsDir(projectRoot) {
|
|
11800
|
-
return
|
|
12400
|
+
return join24(projectRoot, ".locus", "artifacts");
|
|
11801
12401
|
}
|
|
11802
12402
|
function listArtifacts(projectRoot) {
|
|
11803
12403
|
const dir = getArtifactsDir(projectRoot);
|
|
11804
|
-
if (!
|
|
12404
|
+
if (!existsSync24(dir))
|
|
11805
12405
|
return [];
|
|
11806
12406
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
11807
|
-
const filePath =
|
|
12407
|
+
const filePath = join24(dir, fileName);
|
|
11808
12408
|
const stat = statSync5(filePath);
|
|
11809
12409
|
return {
|
|
11810
12410
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -11817,8 +12417,8 @@ function listArtifacts(projectRoot) {
|
|
|
11817
12417
|
function readArtifact(projectRoot, name) {
|
|
11818
12418
|
const dir = getArtifactsDir(projectRoot);
|
|
11819
12419
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
11820
|
-
const filePath =
|
|
11821
|
-
if (!
|
|
12420
|
+
const filePath = join24(dir, fileName);
|
|
12421
|
+
if (!existsSync24(filePath))
|
|
11822
12422
|
return null;
|
|
11823
12423
|
const stat = statSync5(filePath);
|
|
11824
12424
|
return {
|
|
@@ -11987,10 +12587,10 @@ __export(exports_sandbox2, {
|
|
|
11987
12587
|
parseSandboxLogsArgs: () => parseSandboxLogsArgs,
|
|
11988
12588
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs
|
|
11989
12589
|
});
|
|
11990
|
-
import { execSync as
|
|
12590
|
+
import { execSync as execSync18, spawn as spawn7 } from "node:child_process";
|
|
11991
12591
|
import { createHash } from "node:crypto";
|
|
11992
|
-
import { existsSync as
|
|
11993
|
-
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";
|
|
11994
12594
|
import { createInterface as createInterface3 } from "node:readline";
|
|
11995
12595
|
function printSandboxHelp() {
|
|
11996
12596
|
process.stderr.write(`
|
|
@@ -12155,7 +12755,7 @@ async function handleAgentLogin(projectRoot, agent) {
|
|
|
12155
12755
|
process.stderr.write(`${dim2("Login and then exit when ready.")}
|
|
12156
12756
|
|
|
12157
12757
|
`);
|
|
12158
|
-
const child =
|
|
12758
|
+
const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", projectRoot, sandboxName, agent], {
|
|
12159
12759
|
stdio: "inherit"
|
|
12160
12760
|
});
|
|
12161
12761
|
await new Promise((resolve2) => {
|
|
@@ -12199,7 +12799,7 @@ function handleRemove(projectRoot) {
|
|
|
12199
12799
|
process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
|
|
12200
12800
|
`);
|
|
12201
12801
|
try {
|
|
12202
|
-
|
|
12802
|
+
execSync18(`docker sandbox rm ${sandboxName}`, {
|
|
12203
12803
|
encoding: "utf-8",
|
|
12204
12804
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12205
12805
|
timeout: 15000
|
|
@@ -12446,9 +13046,9 @@ async function handleLogs(projectRoot, args) {
|
|
|
12446
13046
|
dockerArgs.push(sandboxName);
|
|
12447
13047
|
await runInteractiveCommand("docker", dockerArgs);
|
|
12448
13048
|
}
|
|
12449
|
-
function
|
|
13049
|
+
function detectPackageManager2(projectRoot) {
|
|
12450
13050
|
try {
|
|
12451
|
-
const raw = readFileSync17(
|
|
13051
|
+
const raw = readFileSync17(join25(projectRoot, "package.json"), "utf-8");
|
|
12452
13052
|
const pkgJson = JSON.parse(raw);
|
|
12453
13053
|
if (typeof pkgJson.packageManager === "string") {
|
|
12454
13054
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -12457,13 +13057,13 @@ function detectPackageManager(projectRoot) {
|
|
|
12457
13057
|
}
|
|
12458
13058
|
}
|
|
12459
13059
|
} catch {}
|
|
12460
|
-
if (
|
|
13060
|
+
if (existsSync25(join25(projectRoot, "bun.lock")) || existsSync25(join25(projectRoot, "bun.lockb"))) {
|
|
12461
13061
|
return "bun";
|
|
12462
13062
|
}
|
|
12463
|
-
if (
|
|
13063
|
+
if (existsSync25(join25(projectRoot, "yarn.lock"))) {
|
|
12464
13064
|
return "yarn";
|
|
12465
13065
|
}
|
|
12466
|
-
if (
|
|
13066
|
+
if (existsSync25(join25(projectRoot, "pnpm-lock.yaml"))) {
|
|
12467
13067
|
return "pnpm";
|
|
12468
13068
|
}
|
|
12469
13069
|
return "npm";
|
|
@@ -12484,7 +13084,7 @@ async function runSandboxSetup(sandboxName, projectRoot) {
|
|
|
12484
13084
|
const ecosystem = detectProjectEcosystem(projectRoot);
|
|
12485
13085
|
const isJS = isJavaScriptEcosystem(ecosystem);
|
|
12486
13086
|
if (isJS) {
|
|
12487
|
-
const pm =
|
|
13087
|
+
const pm = detectPackageManager2(projectRoot);
|
|
12488
13088
|
if (pm !== "npm") {
|
|
12489
13089
|
await ensurePackageManagerInSandbox(sandboxName, pm);
|
|
12490
13090
|
}
|
|
@@ -12512,8 +13112,8 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
|
|
|
12512
13112
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
12513
13113
|
`);
|
|
12514
13114
|
}
|
|
12515
|
-
const setupScript =
|
|
12516
|
-
if (
|
|
13115
|
+
const setupScript = join25(projectRoot, ".locus", "sandbox-setup.sh");
|
|
13116
|
+
if (existsSync25(setupScript)) {
|
|
12517
13117
|
process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
|
|
12518
13118
|
`);
|
|
12519
13119
|
const hookOk = await runInteractiveCommand("docker", [
|
|
@@ -12594,14 +13194,14 @@ function getActiveProviderSandbox(projectRoot, provider) {
|
|
|
12594
13194
|
}
|
|
12595
13195
|
function runInteractiveCommand(command, args) {
|
|
12596
13196
|
return new Promise((resolve2) => {
|
|
12597
|
-
const child =
|
|
13197
|
+
const child = spawn7(command, args, { stdio: "inherit" });
|
|
12598
13198
|
child.on("close", (code) => resolve2(code === 0));
|
|
12599
13199
|
child.on("error", () => resolve2(false));
|
|
12600
13200
|
});
|
|
12601
13201
|
}
|
|
12602
13202
|
async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
12603
13203
|
try {
|
|
12604
|
-
|
|
13204
|
+
execSync18(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
|
|
12605
13205
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12606
13206
|
timeout: 120000
|
|
12607
13207
|
});
|
|
@@ -12622,7 +13222,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
|
12622
13222
|
}
|
|
12623
13223
|
async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
12624
13224
|
try {
|
|
12625
|
-
|
|
13225
|
+
execSync18(`docker sandbox exec ${sandboxName} which ${pm}`, {
|
|
12626
13226
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12627
13227
|
timeout: 5000
|
|
12628
13228
|
});
|
|
@@ -12631,7 +13231,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
12631
13231
|
process.stderr.write(`Installing ${bold2(pm)} in sandbox...
|
|
12632
13232
|
`);
|
|
12633
13233
|
try {
|
|
12634
|
-
|
|
13234
|
+
execSync18(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
|
|
12635
13235
|
stdio: "inherit",
|
|
12636
13236
|
timeout: 120000
|
|
12637
13237
|
});
|
|
@@ -12643,7 +13243,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
12643
13243
|
}
|
|
12644
13244
|
async function ensureCodexInSandbox(sandboxName) {
|
|
12645
13245
|
try {
|
|
12646
|
-
|
|
13246
|
+
execSync18(`docker sandbox exec ${sandboxName} which codex`, {
|
|
12647
13247
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12648
13248
|
timeout: 5000
|
|
12649
13249
|
});
|
|
@@ -12651,7 +13251,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
12651
13251
|
process.stderr.write(`Installing codex in sandbox...
|
|
12652
13252
|
`);
|
|
12653
13253
|
try {
|
|
12654
|
-
|
|
13254
|
+
execSync18(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
12655
13255
|
} catch {
|
|
12656
13256
|
process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
|
|
12657
13257
|
`);
|
|
@@ -12660,7 +13260,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
12660
13260
|
}
|
|
12661
13261
|
function isSandboxAlive(name) {
|
|
12662
13262
|
try {
|
|
12663
|
-
const output =
|
|
13263
|
+
const output = execSync18("docker sandbox ls", {
|
|
12664
13264
|
encoding: "utf-8",
|
|
12665
13265
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12666
13266
|
timeout: 5000
|
|
@@ -12686,13 +13286,13 @@ init_context();
|
|
|
12686
13286
|
init_logger();
|
|
12687
13287
|
init_rate_limiter();
|
|
12688
13288
|
init_terminal();
|
|
12689
|
-
import { existsSync as
|
|
12690
|
-
import { join as
|
|
13289
|
+
import { existsSync as existsSync26, readFileSync as readFileSync18 } from "node:fs";
|
|
13290
|
+
import { join as join26 } from "node:path";
|
|
12691
13291
|
import { fileURLToPath } from "node:url";
|
|
12692
13292
|
function getCliVersion() {
|
|
12693
13293
|
const fallbackVersion = "0.0.0";
|
|
12694
|
-
const packageJsonPath =
|
|
12695
|
-
if (!
|
|
13294
|
+
const packageJsonPath = join26(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
13295
|
+
if (!existsSync26(packageJsonPath)) {
|
|
12696
13296
|
return fallbackVersion;
|
|
12697
13297
|
}
|
|
12698
13298
|
try {
|
|
@@ -12957,7 +13557,7 @@ async function main() {
|
|
|
12957
13557
|
try {
|
|
12958
13558
|
const root = getGitRoot(cwd);
|
|
12959
13559
|
if (isInitialized(root)) {
|
|
12960
|
-
logDir =
|
|
13560
|
+
logDir = join26(root, ".locus", "logs");
|
|
12961
13561
|
getRateLimiter(root);
|
|
12962
13562
|
}
|
|
12963
13563
|
} catch {}
|