@locusai/cli 0.15.4 → 0.15.5
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 +1827 -1785
- package/package.json +4 -4
package/bin/locus.js
CHANGED
|
@@ -43451,9 +43451,9 @@ var init_progress_renderer = __esm(() => {
|
|
|
43451
43451
|
});
|
|
43452
43452
|
|
|
43453
43453
|
// src/repl/image-detect.ts
|
|
43454
|
-
import { copyFileSync, existsSync as
|
|
43455
|
-
import { homedir as
|
|
43456
|
-
import { basename as basename2, join as
|
|
43454
|
+
import { copyFileSync, existsSync as existsSync20, mkdirSync as mkdirSync9 } from "node:fs";
|
|
43455
|
+
import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
|
|
43456
|
+
import { basename as basename2, join as join20 } from "node:path";
|
|
43457
43457
|
function hasImageExtension(p) {
|
|
43458
43458
|
const dot = p.lastIndexOf(".");
|
|
43459
43459
|
if (dot === -1)
|
|
@@ -43466,16 +43466,16 @@ function resolvePath(raw) {
|
|
|
43466
43466
|
p = p.slice(1, -1);
|
|
43467
43467
|
}
|
|
43468
43468
|
if (p.startsWith("~/")) {
|
|
43469
|
-
p =
|
|
43469
|
+
p = homedir4() + p.slice(1);
|
|
43470
43470
|
}
|
|
43471
43471
|
return p;
|
|
43472
43472
|
}
|
|
43473
43473
|
function copyToStable(srcPath) {
|
|
43474
43474
|
try {
|
|
43475
|
-
|
|
43475
|
+
mkdirSync9(STABLE_IMAGE_DIR, { recursive: true });
|
|
43476
43476
|
const ts = Date.now();
|
|
43477
43477
|
const name = `${ts}-${basename2(srcPath)}`;
|
|
43478
|
-
const destPath =
|
|
43478
|
+
const destPath = join20(STABLE_IMAGE_DIR, name);
|
|
43479
43479
|
copyFileSync(srcPath, destPath);
|
|
43480
43480
|
return destPath;
|
|
43481
43481
|
} catch {
|
|
@@ -43495,7 +43495,7 @@ function detectImages(input) {
|
|
|
43495
43495
|
let exists = false;
|
|
43496
43496
|
let stablePath = normalized;
|
|
43497
43497
|
try {
|
|
43498
|
-
exists =
|
|
43498
|
+
exists = existsSync20(normalized);
|
|
43499
43499
|
} catch {}
|
|
43500
43500
|
if (exists) {
|
|
43501
43501
|
const copied = copyToStable(normalized);
|
|
@@ -43569,7 +43569,7 @@ var init_image_detect = __esm(() => {
|
|
|
43569
43569
|
".tif",
|
|
43570
43570
|
".tiff"
|
|
43571
43571
|
]);
|
|
43572
|
-
STABLE_IMAGE_DIR =
|
|
43572
|
+
STABLE_IMAGE_DIR = join20(tmpdir2(), "locus-images");
|
|
43573
43573
|
});
|
|
43574
43574
|
|
|
43575
43575
|
// src/repl/input-handler.ts
|
|
@@ -44783,1972 +44783,2034 @@ async function configCommand(args) {
|
|
|
44783
44783
|
showConfigHelp();
|
|
44784
44784
|
}
|
|
44785
44785
|
}
|
|
44786
|
-
// src/commands/
|
|
44786
|
+
// src/commands/daemon.ts
|
|
44787
44787
|
init_index_node();
|
|
44788
|
-
init_progress_renderer();
|
|
44789
|
-
init_image_detect();
|
|
44790
|
-
init_input_handler();
|
|
44791
44788
|
init_settings_manager();
|
|
44792
44789
|
init_utils3();
|
|
44793
|
-
import {
|
|
44794
|
-
|
|
44795
|
-
|
|
44796
|
-
|
|
44797
|
-
|
|
44798
|
-
|
|
44799
|
-
|
|
44800
|
-
|
|
44801
|
-
|
|
44802
|
-
|
|
44803
|
-
|
|
44804
|
-
|
|
44805
|
-
|
|
44806
|
-
|
|
44807
|
-
|
|
44808
|
-
|
|
44790
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync8, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
44791
|
+
import { homedir as homedir3 } from "node:os";
|
|
44792
|
+
import { join as join19 } from "node:path";
|
|
44793
|
+
|
|
44794
|
+
// src/utils/process.ts
|
|
44795
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
44796
|
+
import { existsSync as existsSync18, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "node:fs";
|
|
44797
|
+
import { homedir as homedir2 } from "node:os";
|
|
44798
|
+
import { dirname as dirname4, join as join18 } from "node:path";
|
|
44799
|
+
function runShell(cmd, args) {
|
|
44800
|
+
return new Promise((resolve2) => {
|
|
44801
|
+
const proc = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
44802
|
+
let stdout = "";
|
|
44803
|
+
let stderr = "";
|
|
44804
|
+
proc.stdout?.on("data", (d) => {
|
|
44805
|
+
stdout += d.toString();
|
|
44806
|
+
});
|
|
44807
|
+
proc.stderr?.on("data", (d) => {
|
|
44808
|
+
stderr += d.toString();
|
|
44809
|
+
});
|
|
44810
|
+
proc.on("close", (exitCode) => resolve2({ exitCode, stdout, stderr }));
|
|
44811
|
+
proc.on("error", (err) => resolve2({ exitCode: 1, stdout, stderr: err.message }));
|
|
44809
44812
|
});
|
|
44810
|
-
|
|
44811
|
-
|
|
44812
|
-
const
|
|
44813
|
-
|
|
44814
|
-
|
|
44813
|
+
}
|
|
44814
|
+
async function findTelegramBinary() {
|
|
44815
|
+
const result = await runShell("which", ["locus-telegram"]);
|
|
44816
|
+
const p = result.stdout.trim();
|
|
44817
|
+
return p?.startsWith?.("/") ? p : null;
|
|
44818
|
+
}
|
|
44819
|
+
async function findBinDir(binary) {
|
|
44820
|
+
const result = await runShell("which", [binary]);
|
|
44821
|
+
const p = result.stdout.trim();
|
|
44822
|
+
if (p?.startsWith?.("/"))
|
|
44823
|
+
return dirname4(p);
|
|
44824
|
+
return null;
|
|
44825
|
+
}
|
|
44826
|
+
function resolveNvmBinDir() {
|
|
44827
|
+
const nvmDir = process.env.NVM_DIR || join18(homedir2(), ".nvm");
|
|
44828
|
+
const versionsDir = join18(nvmDir, "versions", "node");
|
|
44829
|
+
if (!existsSync18(versionsDir))
|
|
44830
|
+
return null;
|
|
44831
|
+
let versions2;
|
|
44832
|
+
try {
|
|
44833
|
+
versions2 = readdirSync6(versionsDir).filter((d) => d.startsWith("v"));
|
|
44834
|
+
} catch {
|
|
44835
|
+
return null;
|
|
44815
44836
|
}
|
|
44816
|
-
if (
|
|
44817
|
-
return
|
|
44837
|
+
if (versions2.length === 0)
|
|
44838
|
+
return null;
|
|
44839
|
+
const currentNodeVersion = `v${process.versions.node}`;
|
|
44840
|
+
const currentBin = join18(versionsDir, currentNodeVersion, "bin");
|
|
44841
|
+
if (versions2.includes(currentNodeVersion) && existsSync18(currentBin)) {
|
|
44842
|
+
return currentBin;
|
|
44818
44843
|
}
|
|
44819
|
-
|
|
44820
|
-
|
|
44844
|
+
const aliasPath = join18(nvmDir, "alias", "default");
|
|
44845
|
+
if (existsSync18(aliasPath)) {
|
|
44846
|
+
try {
|
|
44847
|
+
const alias = readFileSync15(aliasPath, "utf-8").trim();
|
|
44848
|
+
const match = versions2.find((v) => v === `v${alias}` || v.startsWith(`v${alias}.`));
|
|
44849
|
+
if (match) {
|
|
44850
|
+
const bin2 = join18(versionsDir, match, "bin");
|
|
44851
|
+
if (existsSync18(bin2))
|
|
44852
|
+
return bin2;
|
|
44853
|
+
}
|
|
44854
|
+
} catch {}
|
|
44821
44855
|
}
|
|
44822
|
-
|
|
44823
|
-
|
|
44856
|
+
const sorted = versions2.sort((a, b) => {
|
|
44857
|
+
const pa = a.slice(1).split(".").map(Number);
|
|
44858
|
+
const pb = b.slice(1).split(".").map(Number);
|
|
44859
|
+
for (let i = 0;i < 3; i++) {
|
|
44860
|
+
if ((pa[i] || 0) !== (pb[i] || 0))
|
|
44861
|
+
return (pb[i] || 0) - (pa[i] || 0);
|
|
44862
|
+
}
|
|
44863
|
+
return 0;
|
|
44864
|
+
});
|
|
44865
|
+
const bin = join18(versionsDir, sorted[0], "bin");
|
|
44866
|
+
return existsSync18(bin) ? bin : null;
|
|
44867
|
+
}
|
|
44868
|
+
async function buildServicePath() {
|
|
44869
|
+
const home = homedir2();
|
|
44870
|
+
const dirs = new Set;
|
|
44871
|
+
dirs.add("/usr/local/bin");
|
|
44872
|
+
dirs.add("/usr/bin");
|
|
44873
|
+
dirs.add("/bin");
|
|
44874
|
+
const candidates = [
|
|
44875
|
+
join18(home, ".bun", "bin"),
|
|
44876
|
+
join18(home, ".local", "bin"),
|
|
44877
|
+
join18(home, ".npm", "bin"),
|
|
44878
|
+
join18(home, ".npm-global", "bin"),
|
|
44879
|
+
join18(home, ".yarn", "bin")
|
|
44880
|
+
];
|
|
44881
|
+
for (const d of candidates) {
|
|
44882
|
+
if (existsSync18(d))
|
|
44883
|
+
dirs.add(d);
|
|
44824
44884
|
}
|
|
44825
|
-
const
|
|
44826
|
-
if (
|
|
44827
|
-
|
|
44885
|
+
const nvmBin = resolveNvmBinDir();
|
|
44886
|
+
if (nvmBin)
|
|
44887
|
+
dirs.add(nvmBin);
|
|
44888
|
+
const fnmCurrent = join18(home, ".fnm", "current", "bin");
|
|
44889
|
+
if (existsSync18(fnmCurrent))
|
|
44890
|
+
dirs.add(fnmCurrent);
|
|
44891
|
+
for (const bin of ["claude", "codex"]) {
|
|
44892
|
+
const dir = await findBinDir(bin);
|
|
44893
|
+
if (dir)
|
|
44894
|
+
dirs.add(dir);
|
|
44895
|
+
}
|
|
44896
|
+
return Array.from(dirs).join(":");
|
|
44897
|
+
}
|
|
44898
|
+
var SERVICE_NAME = "locus";
|
|
44899
|
+
var SYSTEMD_UNIT_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
44900
|
+
var PLIST_LABEL = "com.locus.agent";
|
|
44901
|
+
function getPlistPath() {
|
|
44902
|
+
return join18(homedir2(), "Library/LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
44903
|
+
}
|
|
44904
|
+
function getPlatform() {
|
|
44905
|
+
if (process.platform === "linux")
|
|
44906
|
+
return "linux";
|
|
44907
|
+
if (process.platform === "darwin")
|
|
44908
|
+
return "darwin";
|
|
44909
|
+
return null;
|
|
44910
|
+
}
|
|
44911
|
+
async function killOrphanedProcesses() {
|
|
44912
|
+
const result = await runShell("pgrep", ["-f", "locus-telegram"]);
|
|
44913
|
+
const pids = result.stdout.trim().split(`
|
|
44914
|
+
`).filter((p) => p.length > 0);
|
|
44915
|
+
if (pids.length === 0)
|
|
44828
44916
|
return;
|
|
44917
|
+
console.log(` Killing ${pids.length} orphaned locus-telegram process${pids.length > 1 ? "es" : ""}...`);
|
|
44918
|
+
await runShell("pkill", ["-f", "locus-telegram"]);
|
|
44919
|
+
await new Promise((resolve2) => setTimeout(resolve2, 2000));
|
|
44920
|
+
const check2 = await runShell("pgrep", ["-f", "locus-telegram"]);
|
|
44921
|
+
if (check2.stdout.trim().length > 0) {
|
|
44922
|
+
await runShell("pkill", ["-9", "-f", "locus-telegram"]);
|
|
44829
44923
|
}
|
|
44830
|
-
|
|
44831
|
-
|
|
44832
|
-
const
|
|
44833
|
-
|
|
44834
|
-
|
|
44835
|
-
|
|
44836
|
-
|
|
44837
|
-
|
|
44838
|
-
|
|
44839
|
-
|
|
44840
|
-
|
|
44841
|
-
|
|
44842
|
-
|
|
44843
|
-
|
|
44844
|
-
|
|
44845
|
-
|
|
44846
|
-
|
|
44847
|
-
|
|
44848
|
-
|
|
44849
|
-
|
|
44850
|
-
|
|
44924
|
+
}
|
|
44925
|
+
async function isDaemonRunning() {
|
|
44926
|
+
const platform = getPlatform();
|
|
44927
|
+
if (platform === "linux") {
|
|
44928
|
+
const result = await runShell("systemctl", ["is-active", SERVICE_NAME]);
|
|
44929
|
+
return result.stdout.trim() === "active";
|
|
44930
|
+
}
|
|
44931
|
+
if (platform === "darwin") {
|
|
44932
|
+
const plistPath = getPlistPath();
|
|
44933
|
+
if (!existsSync18(plistPath))
|
|
44934
|
+
return false;
|
|
44935
|
+
const result = await runShell("launchctl", ["list"]);
|
|
44936
|
+
const match = result.stdout.split(`
|
|
44937
|
+
`).find((l) => l.includes(PLIST_LABEL));
|
|
44938
|
+
if (!match)
|
|
44939
|
+
return false;
|
|
44940
|
+
const pid = match.trim().split(/\s+/)[0];
|
|
44941
|
+
return pid !== "-";
|
|
44942
|
+
}
|
|
44943
|
+
return false;
|
|
44944
|
+
}
|
|
44945
|
+
async function restartDaemonIfRunning() {
|
|
44946
|
+
const platform = getPlatform();
|
|
44947
|
+
if (!platform)
|
|
44948
|
+
return false;
|
|
44949
|
+
const running = await isDaemonRunning();
|
|
44950
|
+
if (!running)
|
|
44951
|
+
return false;
|
|
44952
|
+
if (platform === "linux") {
|
|
44953
|
+
const result = await runShell("systemctl", ["restart", SERVICE_NAME]);
|
|
44954
|
+
return result.exitCode === 0;
|
|
44955
|
+
}
|
|
44956
|
+
if (platform === "darwin") {
|
|
44957
|
+
const plistPath = getPlistPath();
|
|
44958
|
+
await runShell("launchctl", ["unload", plistPath]);
|
|
44959
|
+
const result = await runShell("launchctl", ["load", plistPath]);
|
|
44960
|
+
return result.exitCode === 0;
|
|
44961
|
+
}
|
|
44962
|
+
return false;
|
|
44963
|
+
}
|
|
44964
|
+
|
|
44965
|
+
// src/commands/daemon.ts
|
|
44966
|
+
function showDaemonHelp() {
|
|
44851
44967
|
console.log(`
|
|
44852
|
-
${c.header("
|
|
44853
|
-
|
|
44854
|
-
|
|
44855
|
-
|
|
44856
|
-
|
|
44857
|
-
|
|
44858
|
-
|
|
44859
|
-
|
|
44860
|
-
|
|
44861
|
-
const result = await facilitator.startDiscussion(topic);
|
|
44862
|
-
renderer.showThinkingStopped();
|
|
44863
|
-
discussionId = result.discussion.id;
|
|
44864
|
-
process.stdout.write(`
|
|
44865
|
-
`);
|
|
44866
|
-
process.stdout.write(result.message);
|
|
44867
|
-
process.stdout.write(`
|
|
44968
|
+
${c.header(" DAEMON ")}
|
|
44969
|
+
${c.primary("locus daemon")} ${c.dim("<subcommand>")}
|
|
44970
|
+
|
|
44971
|
+
${c.header(" SUBCOMMANDS ")}
|
|
44972
|
+
${c.success("start")} Install and start Locus as a background service
|
|
44973
|
+
${c.dim("Sets up systemd (Linux) or launchd (macOS)")}
|
|
44974
|
+
${c.success("stop")} Stop and remove the background service
|
|
44975
|
+
${c.success("restart")} Restart the background service
|
|
44976
|
+
${c.success("status")} Check if the service is running
|
|
44868
44977
|
|
|
44978
|
+
${c.header(" EXAMPLES ")}
|
|
44979
|
+
${c.dim("$")} ${c.primary("locus daemon start")}
|
|
44980
|
+
${c.dim("$")} ${c.primary("locus daemon status")}
|
|
44981
|
+
${c.dim("$")} ${c.primary("locus daemon restart")}
|
|
44982
|
+
${c.dim("$")} ${c.primary("locus daemon stop")}
|
|
44869
44983
|
`);
|
|
44870
|
-
|
|
44871
|
-
|
|
44872
|
-
|
|
44984
|
+
}
|
|
44985
|
+
async function resolveBinaries() {
|
|
44986
|
+
const binaryPath = await findTelegramBinary();
|
|
44987
|
+
if (!binaryPath) {
|
|
44873
44988
|
console.error(`
|
|
44874
|
-
${c.error("✖")} ${c.
|
|
44989
|
+
${c.error("✖")} ${c.bold("Could not find locus-telegram binary.")}
|
|
44990
|
+
` + ` Install with: ${c.primary("npm install -g @locusai/telegram")}
|
|
44875
44991
|
`);
|
|
44876
44992
|
process.exit(1);
|
|
44877
44993
|
}
|
|
44878
|
-
|
|
44879
|
-
|
|
44994
|
+
for (const bin of ["claude", "codex"]) {
|
|
44995
|
+
if (!await findBinDir(bin)) {
|
|
44996
|
+
console.warn(`
|
|
44997
|
+
${c.secondary("⚠")} ${c.bold(`Could not find '${bin}' CLI in PATH.`)}
|
|
44998
|
+
` + ` The service may need it to execute tasks.
|
|
44880
44999
|
`);
|
|
44881
|
-
let isProcessing = false;
|
|
44882
|
-
let interrupted = false;
|
|
44883
|
-
const shutdown = () => {
|
|
44884
|
-
if (isProcessing) {
|
|
44885
|
-
aiRunner.abort();
|
|
44886
45000
|
}
|
|
44887
|
-
|
|
44888
|
-
|
|
44889
|
-
|
|
44890
|
-
|
|
44891
|
-
|
|
44892
|
-
|
|
44893
|
-
|
|
44894
|
-
|
|
44895
|
-
|
|
44896
|
-
|
|
44897
|
-
const trimmed = input.trim();
|
|
44898
|
-
if (trimmed === "") {
|
|
44899
|
-
inputHandler.showPrompt();
|
|
44900
|
-
return;
|
|
44901
|
-
}
|
|
44902
|
-
if (!trimmed.includes(`
|
|
44903
|
-
`)) {
|
|
44904
|
-
const lowerInput = trimmed.toLowerCase();
|
|
44905
|
-
if (lowerInput === "help") {
|
|
44906
|
-
showReplHelp();
|
|
44907
|
-
inputHandler.showPrompt();
|
|
44908
|
-
return;
|
|
44909
|
-
}
|
|
44910
|
-
if (lowerInput === "exit" || lowerInput === "quit") {
|
|
44911
|
-
shutdown();
|
|
44912
|
-
return;
|
|
44913
|
-
}
|
|
44914
|
-
if (lowerInput === "insights") {
|
|
44915
|
-
showCurrentInsights(discussionManager, discussionId);
|
|
44916
|
-
inputHandler.showPrompt();
|
|
44917
|
-
return;
|
|
44918
|
-
}
|
|
44919
|
-
if (lowerInput === "summary") {
|
|
44920
|
-
isProcessing = true;
|
|
44921
|
-
const summaryRenderer = new ProgressRenderer;
|
|
44922
|
-
try {
|
|
44923
|
-
summaryRenderer.showThinkingStarted();
|
|
44924
|
-
const summary = await facilitator.summarizeDiscussion(discussionId);
|
|
44925
|
-
summaryRenderer.showThinkingStopped();
|
|
44926
|
-
process.stdout.write(`
|
|
45001
|
+
}
|
|
45002
|
+
const servicePath = await buildServicePath();
|
|
45003
|
+
return { binaryPath, servicePath };
|
|
45004
|
+
}
|
|
45005
|
+
function requirePlatform() {
|
|
45006
|
+
const platform = getPlatform();
|
|
45007
|
+
if (!platform) {
|
|
45008
|
+
console.error(`
|
|
45009
|
+
${c.error("✖")} ${c.bold(`Unsupported platform: ${process.platform}`)}
|
|
45010
|
+
` + ` Daemon management is supported on Linux (systemd) and macOS (launchd).
|
|
44927
45011
|
`);
|
|
44928
|
-
|
|
44929
|
-
|
|
44930
|
-
|
|
44931
|
-
|
|
44932
|
-
|
|
44933
|
-
|
|
44934
|
-
|
|
44935
|
-
|
|
44936
|
-
`
|
|
44937
|
-
|
|
44938
|
-
`)
|
|
44939
|
-
}
|
|
44940
|
-
console.log(` ${c.dim("To review:")} ${c.cyan(`locus discuss --show ${discussionId}`)}`);
|
|
44941
|
-
console.log(` ${c.dim("To list all:")} ${c.cyan("locus discuss --list")}
|
|
44942
|
-
`);
|
|
44943
|
-
} catch (error48) {
|
|
44944
|
-
summaryRenderer.finalize();
|
|
44945
|
-
console.error(`
|
|
44946
|
-
${c.error("✖")} ${c.red("Failed to summarize:")} ${error48 instanceof Error ? error48.message : String(error48)}
|
|
44947
|
-
`);
|
|
44948
|
-
}
|
|
44949
|
-
inputHandler.stop();
|
|
44950
|
-
process.exit(0);
|
|
44951
|
-
return;
|
|
44952
|
-
}
|
|
44953
|
-
}
|
|
44954
|
-
const images = detectImages(trimmed);
|
|
44955
|
-
if (images.length > 0) {
|
|
44956
|
-
for (const img of images) {
|
|
44957
|
-
const status = img.exists ? c.success("attached") : c.warning("not found");
|
|
44958
|
-
process.stdout.write(` ${c.cyan(`[Image: ${imageDisplayName(img.path)}]`)} ${status}\r
|
|
44959
|
-
`);
|
|
44960
|
-
}
|
|
44961
|
-
}
|
|
44962
|
-
const cleanedInput = stripImagePaths(trimmed, images);
|
|
44963
|
-
const effectiveInput = cleanedInput + buildImageContext(images);
|
|
44964
|
-
isProcessing = true;
|
|
44965
|
-
const chunkRenderer = new ProgressRenderer;
|
|
44966
|
-
try {
|
|
44967
|
-
chunkRenderer.showThinkingStarted();
|
|
44968
|
-
const stream4 = facilitator.continueDiscussionStream(discussionId, effectiveInput);
|
|
44969
|
-
let result = {
|
|
44970
|
-
response: "",
|
|
44971
|
-
insights: []
|
|
44972
|
-
};
|
|
44973
|
-
let iterResult = await stream4.next();
|
|
44974
|
-
while (!iterResult.done) {
|
|
44975
|
-
chunkRenderer.renderChunk(iterResult.value);
|
|
44976
|
-
iterResult = await stream4.next();
|
|
44977
|
-
}
|
|
44978
|
-
result = iterResult.value;
|
|
44979
|
-
chunkRenderer.finalize();
|
|
44980
|
-
if (result.insights.length > 0) {
|
|
44981
|
-
console.log("");
|
|
44982
|
-
for (const insight of result.insights) {
|
|
44983
|
-
const tag = formatInsightTag(insight.type);
|
|
44984
|
-
console.log(` ${tag} ${c.bold(insight.title)}`);
|
|
44985
|
-
console.log(` ${c.dim(insight.content)}
|
|
45012
|
+
process.exit(1);
|
|
45013
|
+
}
|
|
45014
|
+
return platform;
|
|
45015
|
+
}
|
|
45016
|
+
function validateConfig(projectPath) {
|
|
45017
|
+
const manager = new SettingsManager(projectPath);
|
|
45018
|
+
const settings = manager.load();
|
|
45019
|
+
if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
|
|
45020
|
+
console.error(`
|
|
45021
|
+
${c.error("✖")} ${c.bold("Telegram is not configured.")}
|
|
45022
|
+
` + ` Run ${c.primary("locus telegram setup")} first.
|
|
44986
45023
|
`);
|
|
44987
|
-
|
|
44988
|
-
|
|
44989
|
-
|
|
44990
|
-
|
|
44991
|
-
|
|
44992
|
-
${c.
|
|
45024
|
+
process.exit(1);
|
|
45025
|
+
}
|
|
45026
|
+
if (!settings.apiKey) {
|
|
45027
|
+
console.error(`
|
|
45028
|
+
${c.error("✖")} ${c.bold("API key is not configured.")}
|
|
45029
|
+
` + ` Run ${c.primary("locus config setup --api-key <key>")} first.
|
|
44993
45030
|
`);
|
|
44994
|
-
|
|
44995
|
-
|
|
44996
|
-
|
|
44997
|
-
|
|
44998
|
-
|
|
44999
|
-
|
|
45000
|
-
|
|
45001
|
-
|
|
45002
|
-
|
|
45003
|
-
|
|
45004
|
-
|
|
45005
|
-
|
|
45006
|
-
|
|
45031
|
+
process.exit(1);
|
|
45032
|
+
}
|
|
45033
|
+
}
|
|
45034
|
+
function generateSystemdUnit(projectPath, user2, bins) {
|
|
45035
|
+
return `[Unit]
|
|
45036
|
+
Description=Locus AI Agent (Telegram bot + proposal scheduler)
|
|
45037
|
+
After=network-online.target
|
|
45038
|
+
Wants=network-online.target
|
|
45039
|
+
|
|
45040
|
+
[Service]
|
|
45041
|
+
Type=simple
|
|
45042
|
+
User=${user2}
|
|
45043
|
+
WorkingDirectory=${projectPath}
|
|
45044
|
+
ExecStart=${bins.binaryPath}
|
|
45045
|
+
Restart=on-failure
|
|
45046
|
+
RestartSec=10
|
|
45047
|
+
Environment=PATH=${bins.servicePath}
|
|
45048
|
+
Environment=HOME=${homedir3()}
|
|
45049
|
+
|
|
45050
|
+
[Install]
|
|
45051
|
+
WantedBy=multi-user.target
|
|
45052
|
+
`;
|
|
45053
|
+
}
|
|
45054
|
+
async function startSystemd(projectPath, bins) {
|
|
45055
|
+
const user2 = process.env.USER || "root";
|
|
45056
|
+
const unit = generateSystemdUnit(projectPath, user2, bins);
|
|
45057
|
+
console.log(`
|
|
45058
|
+
${c.info("▶")} Writing systemd unit to ${c.dim(SYSTEMD_UNIT_PATH)}`);
|
|
45059
|
+
writeFileSync8(SYSTEMD_UNIT_PATH, unit, "utf-8");
|
|
45060
|
+
console.log(` ${c.info("▶")} Reloading systemd daemon...`);
|
|
45061
|
+
await runShell("systemctl", ["daemon-reload"]);
|
|
45062
|
+
console.log(` ${c.info("▶")} Enabling and starting ${SERVICE_NAME}...`);
|
|
45063
|
+
await runShell("systemctl", ["enable", SERVICE_NAME]);
|
|
45064
|
+
const result = await runShell("systemctl", ["start", SERVICE_NAME]);
|
|
45065
|
+
if (result.exitCode !== 0) {
|
|
45066
|
+
console.error(`
|
|
45067
|
+
${c.error("✖")} Failed to start service: ${result.stderr.trim()}`);
|
|
45068
|
+
console.error(` ${c.dim("Check logs with:")} ${c.primary(`journalctl -u ${SERVICE_NAME} -f`)}`);
|
|
45069
|
+
return;
|
|
45070
|
+
}
|
|
45071
|
+
console.log(`
|
|
45072
|
+
${c.success("✔")} ${c.bold("Locus daemon started!")}
|
|
45073
|
+
|
|
45074
|
+
${c.bold("Service:")} ${SERVICE_NAME}
|
|
45075
|
+
${c.bold("Unit file:")} ${SYSTEMD_UNIT_PATH}
|
|
45076
|
+
|
|
45077
|
+
${c.bold("Useful commands:")}
|
|
45078
|
+
${c.dim("$")} ${c.primary(`sudo systemctl status ${SERVICE_NAME}`)}
|
|
45079
|
+
${c.dim("$")} ${c.primary(`journalctl -u ${SERVICE_NAME} -f`)}
|
|
45007
45080
|
`);
|
|
45008
|
-
inputHandler.showPrompt();
|
|
45009
|
-
});
|
|
45010
|
-
},
|
|
45011
|
-
onInterrupt: () => {
|
|
45012
|
-
if (isProcessing) {
|
|
45013
|
-
interrupted = true;
|
|
45014
|
-
aiRunner.abort();
|
|
45015
|
-
isProcessing = false;
|
|
45016
|
-
console.log(c.dim(`
|
|
45017
|
-
[Interrupted]`));
|
|
45018
|
-
inputHandler.showPrompt();
|
|
45019
|
-
} else {
|
|
45020
|
-
shutdown();
|
|
45021
|
-
}
|
|
45022
|
-
},
|
|
45023
|
-
onClose: () => shutdown()
|
|
45024
|
-
});
|
|
45025
|
-
inputHandler.start();
|
|
45026
|
-
inputHandler.showPrompt();
|
|
45027
45081
|
}
|
|
45028
|
-
function
|
|
45029
|
-
|
|
45030
|
-
if (discussions.length === 0) {
|
|
45082
|
+
async function stopSystemd() {
|
|
45083
|
+
if (!existsSync19(SYSTEMD_UNIT_PATH)) {
|
|
45031
45084
|
console.log(`
|
|
45032
|
-
${c.dim("No
|
|
45033
|
-
`);
|
|
45034
|
-
console.log(` ${c.dim("Start one with:")} ${c.cyan('locus discuss "your topic"')}
|
|
45085
|
+
${c.dim("No systemd service found. Nothing to stop.")}
|
|
45035
45086
|
`);
|
|
45087
|
+
await killOrphanedProcesses();
|
|
45036
45088
|
return;
|
|
45037
45089
|
}
|
|
45090
|
+
console.log(` ${c.info("▶")} Stopping and disabling ${SERVICE_NAME}...`);
|
|
45091
|
+
await runShell("systemctl", ["stop", SERVICE_NAME]);
|
|
45092
|
+
await runShell("systemctl", ["disable", SERVICE_NAME]);
|
|
45093
|
+
unlinkSync6(SYSTEMD_UNIT_PATH);
|
|
45094
|
+
await runShell("systemctl", ["daemon-reload"]);
|
|
45095
|
+
await killOrphanedProcesses();
|
|
45038
45096
|
console.log(`
|
|
45039
|
-
${c.
|
|
45097
|
+
${c.success("✔")} ${c.bold("Locus daemon stopped.")}
|
|
45040
45098
|
`);
|
|
45041
|
-
for (const disc of discussions) {
|
|
45042
|
-
const statusIcon = disc.status === "active" ? c.warning("◯") : disc.status === "completed" ? c.success("✔") : c.dim("⊘");
|
|
45043
|
-
console.log(` ${statusIcon} ${c.bold(disc.title)} ${c.dim(`[${disc.status}]`)} ${c.dim(`— ${disc.messages.length} messages, ${disc.insights.length} insights`)}`);
|
|
45044
|
-
console.log(` ${c.dim("ID:")} ${disc.id}`);
|
|
45045
|
-
console.log(` ${c.dim("Created:")} ${disc.createdAt}`);
|
|
45046
|
-
console.log("");
|
|
45047
|
-
}
|
|
45048
45099
|
}
|
|
45049
|
-
function
|
|
45050
|
-
|
|
45051
|
-
|
|
45100
|
+
async function restartSystemd() {
|
|
45101
|
+
if (!existsSync19(SYSTEMD_UNIT_PATH)) {
|
|
45102
|
+
console.log(`
|
|
45103
|
+
${c.dim("No systemd service found. Use")} ${c.primary("locus daemon start")} ${c.dim("first.")}
|
|
45104
|
+
`);
|
|
45105
|
+
return;
|
|
45106
|
+
}
|
|
45107
|
+
console.log(` ${c.info("▶")} Restarting ${SERVICE_NAME}...`);
|
|
45108
|
+
const result = await runShell("systemctl", ["restart", SERVICE_NAME]);
|
|
45109
|
+
if (result.exitCode !== 0) {
|
|
45052
45110
|
console.error(`
|
|
45053
|
-
${c.error("✖")}
|
|
45111
|
+
${c.error("✖")} Failed to restart: ${result.stderr.trim()}
|
|
45054
45112
|
`);
|
|
45055
|
-
|
|
45113
|
+
return;
|
|
45056
45114
|
}
|
|
45057
45115
|
console.log(`
|
|
45058
|
-
${
|
|
45116
|
+
${c.success("✔")} ${c.bold("Locus daemon restarted.")}
|
|
45059
45117
|
`);
|
|
45060
45118
|
}
|
|
45061
|
-
function
|
|
45062
|
-
|
|
45063
|
-
|
|
45119
|
+
async function statusSystemd() {
|
|
45120
|
+
const result = await runShell("systemctl", ["is-active", SERVICE_NAME]);
|
|
45121
|
+
const state = result.stdout.trim();
|
|
45122
|
+
if (state === "active") {
|
|
45064
45123
|
console.log(`
|
|
45065
|
-
${c.success("
|
|
45124
|
+
${c.success("●")} ${c.bold("Locus daemon is running")} ${c.dim("(systemd)")}
|
|
45066
45125
|
`);
|
|
45067
|
-
}
|
|
45068
|
-
console.
|
|
45069
|
-
${c.
|
|
45126
|
+
} else if (existsSync19(SYSTEMD_UNIT_PATH)) {
|
|
45127
|
+
console.log(`
|
|
45128
|
+
${c.secondary("●")} ${c.bold(`Locus daemon is ${state}`)} ${c.dim("(systemd)")}
|
|
45070
45129
|
`);
|
|
45071
|
-
|
|
45072
|
-
|
|
45073
|
-
}
|
|
45074
|
-
function deleteDiscussion(discussionManager, id) {
|
|
45075
|
-
try {
|
|
45076
|
-
discussionManager.delete(id);
|
|
45130
|
+
console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
|
|
45131
|
+
`);
|
|
45132
|
+
} else {
|
|
45077
45133
|
console.log(`
|
|
45078
|
-
${c.
|
|
45134
|
+
${c.secondary("●")} ${c.bold("Locus daemon is not installed")}
|
|
45079
45135
|
`);
|
|
45080
|
-
}
|
|
45081
|
-
console.error(`
|
|
45082
|
-
${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
|
|
45136
|
+
console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
|
|
45083
45137
|
`);
|
|
45084
|
-
process.exit(1);
|
|
45085
45138
|
}
|
|
45086
45139
|
}
|
|
45087
|
-
function
|
|
45088
|
-
const
|
|
45089
|
-
|
|
45090
|
-
|
|
45091
|
-
|
|
45092
|
-
|
|
45140
|
+
function generatePlist(projectPath, bins) {
|
|
45141
|
+
const logDir = join19(homedir3(), "Library/Logs/Locus");
|
|
45142
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
45143
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
45144
|
+
<plist version="1.0">
|
|
45145
|
+
<dict>
|
|
45146
|
+
<key>Label</key>
|
|
45147
|
+
<string>${PLIST_LABEL}</string>
|
|
45148
|
+
<key>ProgramArguments</key>
|
|
45149
|
+
<array>
|
|
45150
|
+
<string>${bins.binaryPath}</string>
|
|
45151
|
+
</array>
|
|
45152
|
+
<key>WorkingDirectory</key>
|
|
45153
|
+
<string>${projectPath}</string>
|
|
45154
|
+
<key>RunAtLoad</key>
|
|
45155
|
+
<true/>
|
|
45156
|
+
<key>KeepAlive</key>
|
|
45157
|
+
<true/>
|
|
45158
|
+
<key>StandardOutPath</key>
|
|
45159
|
+
<string>${join19(logDir, "locus.log")}</string>
|
|
45160
|
+
<key>StandardErrorPath</key>
|
|
45161
|
+
<string>${join19(logDir, "locus-error.log")}</string>
|
|
45162
|
+
<key>EnvironmentVariables</key>
|
|
45163
|
+
<dict>
|
|
45164
|
+
<key>PATH</key>
|
|
45165
|
+
<string>${bins.servicePath}</string>
|
|
45166
|
+
</dict>
|
|
45167
|
+
</dict>
|
|
45168
|
+
</plist>
|
|
45169
|
+
`;
|
|
45170
|
+
}
|
|
45171
|
+
async function startLaunchd(projectPath, bins) {
|
|
45172
|
+
const plistPath = getPlistPath();
|
|
45173
|
+
if (existsSync19(plistPath)) {
|
|
45174
|
+
await runShell("launchctl", ["unload", plistPath]);
|
|
45175
|
+
}
|
|
45176
|
+
const logDir = join19(homedir3(), "Library/Logs/Locus");
|
|
45177
|
+
mkdirSync8(logDir, { recursive: true });
|
|
45178
|
+
mkdirSync8(join19(homedir3(), "Library/LaunchAgents"), { recursive: true });
|
|
45179
|
+
const plist = generatePlist(projectPath, bins);
|
|
45180
|
+
console.log(`
|
|
45181
|
+
${c.info("▶")} Writing plist to ${c.dim(plistPath)}`);
|
|
45182
|
+
writeFileSync8(plistPath, plist, "utf-8");
|
|
45183
|
+
console.log(` ${c.info("▶")} Loading service...`);
|
|
45184
|
+
const result = await runShell("launchctl", ["load", plistPath]);
|
|
45185
|
+
if (result.exitCode !== 0) {
|
|
45186
|
+
console.error(`
|
|
45187
|
+
${c.error("✖")} Failed to load service: ${result.stderr.trim()}`);
|
|
45093
45188
|
return;
|
|
45094
45189
|
}
|
|
45190
|
+
const logPath = join19(logDir, "locus.log");
|
|
45095
45191
|
console.log(`
|
|
45096
|
-
${c.
|
|
45192
|
+
${c.success("✔")} ${c.bold("Locus daemon started!")}
|
|
45193
|
+
|
|
45194
|
+
${c.bold("Plist:")} ${plistPath}
|
|
45195
|
+
${c.bold("Logs:")} ${logPath}
|
|
45196
|
+
|
|
45197
|
+
${c.bold("Useful commands:")}
|
|
45198
|
+
${c.dim("$")} ${c.primary(`launchctl list | grep ${PLIST_LABEL}`)}
|
|
45199
|
+
${c.dim("$")} ${c.primary(`tail -f ${logPath}`)}
|
|
45097
45200
|
`);
|
|
45098
|
-
for (const insight of discussion2.insights) {
|
|
45099
|
-
const tag = formatInsightTag(insight.type);
|
|
45100
|
-
console.log(` ${tag} ${c.bold(insight.title)}`);
|
|
45101
|
-
console.log(` ${c.dim(insight.content)}`);
|
|
45102
|
-
if (insight.tags.length > 0) {
|
|
45103
|
-
console.log(` ${c.dim(`Tags: ${insight.tags.join(", ")}`)}`);
|
|
45104
|
-
}
|
|
45105
|
-
console.log("");
|
|
45106
|
-
}
|
|
45107
45201
|
}
|
|
45108
|
-
function
|
|
45109
|
-
|
|
45110
|
-
|
|
45111
|
-
|
|
45112
|
-
|
|
45113
|
-
|
|
45114
|
-
|
|
45115
|
-
|
|
45116
|
-
case "concern":
|
|
45117
|
-
return c.red("[CONCERN]");
|
|
45118
|
-
case "learning":
|
|
45119
|
-
return c.cyan("[LEARNING]");
|
|
45202
|
+
async function stopLaunchd() {
|
|
45203
|
+
const plistPath = getPlistPath();
|
|
45204
|
+
if (!existsSync19(plistPath)) {
|
|
45205
|
+
console.log(`
|
|
45206
|
+
${c.dim("No launchd service found. Nothing to stop.")}
|
|
45207
|
+
`);
|
|
45208
|
+
await killOrphanedProcesses();
|
|
45209
|
+
return;
|
|
45120
45210
|
}
|
|
45121
|
-
}
|
|
45122
|
-
|
|
45211
|
+
console.log(` ${c.info("▶")} Unloading service...`);
|
|
45212
|
+
await runShell("launchctl", ["unload", plistPath]);
|
|
45213
|
+
unlinkSync6(plistPath);
|
|
45214
|
+
await killOrphanedProcesses();
|
|
45123
45215
|
console.log(`
|
|
45124
|
-
${c.
|
|
45125
|
-
|
|
45126
|
-
${c.cyan("summary")} Generate a final summary and end the discussion
|
|
45127
|
-
${c.cyan("insights")} Show all insights extracted so far
|
|
45128
|
-
${c.cyan("exit")} Save and exit without generating a summary
|
|
45129
|
-
${c.cyan("help")} Show this help message
|
|
45130
|
-
|
|
45131
|
-
${c.header(" KEY BINDINGS ")}
|
|
45132
|
-
|
|
45133
|
-
${c.cyan("Enter")} Send message
|
|
45134
|
-
${c.cyan("Shift+Enter")} Insert newline (also: Alt+Enter, Ctrl+J)
|
|
45135
|
-
${c.cyan("Ctrl+C")} Interrupt / clear input / exit
|
|
45136
|
-
${c.cyan("Ctrl+U")} Clear current input
|
|
45137
|
-
${c.cyan("Ctrl+W")} Delete last word
|
|
45138
|
-
|
|
45139
|
-
${c.dim("Type anything else to continue the discussion.")}
|
|
45216
|
+
${c.success("✔")} ${c.bold("Locus daemon stopped.")}
|
|
45140
45217
|
`);
|
|
45141
45218
|
}
|
|
45142
|
-
function
|
|
45219
|
+
async function restartLaunchd() {
|
|
45220
|
+
const plistPath = getPlistPath();
|
|
45221
|
+
if (!existsSync19(plistPath)) {
|
|
45222
|
+
console.log(`
|
|
45223
|
+
${c.dim("No launchd service found. Use")} ${c.primary("locus daemon start")} ${c.dim("first.")}
|
|
45224
|
+
`);
|
|
45225
|
+
return;
|
|
45226
|
+
}
|
|
45227
|
+
console.log(` ${c.info("▶")} Restarting service...`);
|
|
45228
|
+
await runShell("launchctl", ["unload", plistPath]);
|
|
45229
|
+
const result = await runShell("launchctl", ["load", plistPath]);
|
|
45230
|
+
if (result.exitCode !== 0) {
|
|
45231
|
+
console.error(`
|
|
45232
|
+
${c.error("✖")} Failed to restart: ${result.stderr.trim()}
|
|
45233
|
+
`);
|
|
45234
|
+
return;
|
|
45235
|
+
}
|
|
45143
45236
|
console.log(`
|
|
45144
|
-
${c.
|
|
45145
|
-
|
|
45146
|
-
${c.bold("Usage:")}
|
|
45147
|
-
${c.cyan('locus discuss "topic"')} Start a discussion on a topic
|
|
45148
|
-
${c.cyan("locus discuss --list")} List all discussions
|
|
45149
|
-
${c.cyan("locus discuss --show <id>")} Show discussion details
|
|
45150
|
-
${c.cyan("locus discuss --archive <id>")} Archive a discussion
|
|
45151
|
-
${c.cyan("locus discuss --delete <id>")} Delete a discussion
|
|
45152
|
-
|
|
45153
|
-
${c.bold("Options:")}
|
|
45154
|
-
${c.dim("--model <model>")} AI model (claude: opus, sonnet, haiku | codex: gpt-5.3-codex, gpt-5-codex-mini)
|
|
45155
|
-
${c.dim("--provider <p>")} AI provider (claude, codex)
|
|
45156
|
-
${c.dim("--reasoning-effort <level>")} Reasoning effort (low, medium, high)
|
|
45157
|
-
${c.dim("--dir <path>")} Project directory
|
|
45158
|
-
|
|
45159
|
-
${c.bold("REPL Commands:")}
|
|
45160
|
-
${c.dim("summary")} Generate final summary and end the discussion
|
|
45161
|
-
${c.dim("insights")} Show all insights extracted so far
|
|
45162
|
-
${c.dim("exit")} Save and exit without generating a summary
|
|
45163
|
-
${c.dim("help")} Show available commands
|
|
45164
|
-
|
|
45165
|
-
${c.bold("Examples:")}
|
|
45166
|
-
${c.dim("# Start a discussion about architecture")}
|
|
45167
|
-
${c.cyan('locus discuss "how should we structure the auth system?"')}
|
|
45168
|
-
|
|
45169
|
-
${c.dim("# Review a past discussion")}
|
|
45170
|
-
${c.cyan("locus discuss --show disc-1234567890")}
|
|
45171
|
-
|
|
45172
|
-
${c.dim("# List all discussions")}
|
|
45173
|
-
${c.cyan("locus discuss --list")}
|
|
45237
|
+
${c.success("✔")} ${c.bold("Locus daemon restarted.")}
|
|
45174
45238
|
`);
|
|
45175
45239
|
}
|
|
45176
|
-
|
|
45177
|
-
|
|
45178
|
-
|
|
45179
|
-
|
|
45180
|
-
|
|
45181
|
-
|
|
45182
|
-
|
|
45183
|
-
|
|
45240
|
+
async function statusLaunchd() {
|
|
45241
|
+
const plistPath = getPlistPath();
|
|
45242
|
+
if (!existsSync19(plistPath)) {
|
|
45243
|
+
console.log(`
|
|
45244
|
+
${c.secondary("●")} ${c.bold("Locus daemon is not installed")}
|
|
45245
|
+
`);
|
|
45246
|
+
console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
|
|
45247
|
+
`);
|
|
45248
|
+
return;
|
|
45249
|
+
}
|
|
45250
|
+
const result = await runShell("launchctl", ["list"]);
|
|
45251
|
+
const match = result.stdout.split(`
|
|
45252
|
+
`).find((l) => l.includes(PLIST_LABEL));
|
|
45253
|
+
if (match) {
|
|
45254
|
+
const parts = match.trim().split(/\s+/);
|
|
45255
|
+
const pid = parts[0] === "-" ? null : parts[0];
|
|
45256
|
+
if (pid) {
|
|
45257
|
+
console.log(`
|
|
45258
|
+
${c.success("●")} ${c.bold("Locus daemon is running")} ${c.dim(`(PID ${pid}, launchd)`)}
|
|
45259
|
+
`);
|
|
45260
|
+
} else {
|
|
45261
|
+
console.log(`
|
|
45262
|
+
${c.secondary("●")} ${c.bold("Locus daemon is stopped")} ${c.dim("(launchd)")}
|
|
45263
|
+
`);
|
|
45264
|
+
console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
|
|
45265
|
+
`);
|
|
45266
|
+
}
|
|
45267
|
+
} else {
|
|
45268
|
+
console.log(`
|
|
45269
|
+
${c.secondary("●")} ${c.bold("Locus daemon is not loaded")} ${c.dim("(plist exists but not loaded)")}
|
|
45270
|
+
`);
|
|
45271
|
+
console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
|
|
45272
|
+
`);
|
|
45273
|
+
}
|
|
45274
|
+
}
|
|
45275
|
+
async function daemonCommand(args) {
|
|
45276
|
+
const projectPath = process.cwd();
|
|
45277
|
+
requireInitialization(projectPath, "daemon");
|
|
45184
45278
|
const subcommand = args[0];
|
|
45185
|
-
const
|
|
45279
|
+
const platform = subcommand ? requirePlatform() : null;
|
|
45280
|
+
const isLinux = platform === "linux";
|
|
45186
45281
|
switch (subcommand) {
|
|
45187
|
-
case "
|
|
45188
|
-
|
|
45282
|
+
case "start": {
|
|
45283
|
+
validateConfig(projectPath);
|
|
45284
|
+
const bins = await resolveBinaries();
|
|
45285
|
+
if (isLinux)
|
|
45286
|
+
await startSystemd(projectPath, bins);
|
|
45287
|
+
else
|
|
45288
|
+
await startLaunchd(projectPath, bins);
|
|
45189
45289
|
break;
|
|
45190
|
-
|
|
45191
|
-
|
|
45290
|
+
}
|
|
45291
|
+
case "stop":
|
|
45292
|
+
if (isLinux)
|
|
45293
|
+
await stopSystemd();
|
|
45294
|
+
else
|
|
45295
|
+
await stopLaunchd();
|
|
45296
|
+
break;
|
|
45297
|
+
case "restart":
|
|
45298
|
+
if (isLinux)
|
|
45299
|
+
await restartSystemd();
|
|
45300
|
+
else
|
|
45301
|
+
await restartLaunchd();
|
|
45192
45302
|
break;
|
|
45303
|
+
case "status":
|
|
45304
|
+
if (isLinux)
|
|
45305
|
+
await statusSystemd();
|
|
45306
|
+
else
|
|
45307
|
+
await statusLaunchd();
|
|
45308
|
+
break;
|
|
45309
|
+
default:
|
|
45310
|
+
showDaemonHelp();
|
|
45193
45311
|
}
|
|
45194
45312
|
}
|
|
45195
|
-
|
|
45196
|
-
|
|
45313
|
+
// src/commands/discuss.ts
|
|
45314
|
+
init_index_node();
|
|
45315
|
+
init_progress_renderer();
|
|
45316
|
+
init_image_detect();
|
|
45317
|
+
init_input_handler();
|
|
45318
|
+
init_settings_manager();
|
|
45319
|
+
init_utils3();
|
|
45320
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
45321
|
+
async function discussCommand(args) {
|
|
45322
|
+
const { values, positionals } = parseArgs3({
|
|
45197
45323
|
args,
|
|
45198
45324
|
options: {
|
|
45199
|
-
|
|
45200
|
-
|
|
45201
|
-
|
|
45202
|
-
|
|
45203
|
-
|
|
45325
|
+
list: { type: "boolean" },
|
|
45326
|
+
show: { type: "string" },
|
|
45327
|
+
archive: { type: "string" },
|
|
45328
|
+
delete: { type: "string" },
|
|
45329
|
+
model: { type: "string" },
|
|
45330
|
+
provider: { type: "string" },
|
|
45331
|
+
"reasoning-effort": { type: "string" },
|
|
45332
|
+
dir: { type: "string" }
|
|
45204
45333
|
},
|
|
45205
|
-
strict: false
|
|
45334
|
+
strict: false,
|
|
45335
|
+
allowPositionals: true
|
|
45206
45336
|
});
|
|
45207
|
-
if (values.help) {
|
|
45208
|
-
showDocsSyncHelp();
|
|
45209
|
-
return;
|
|
45210
|
-
}
|
|
45211
45337
|
const projectPath = values.dir || process.cwd();
|
|
45212
|
-
requireInitialization(projectPath, "
|
|
45213
|
-
const
|
|
45214
|
-
|
|
45215
|
-
|
|
45216
|
-
const settings = settingsManager.load();
|
|
45217
|
-
const apiKey = values["api-key"] || settings.apiKey;
|
|
45218
|
-
if (!apiKey) {
|
|
45219
|
-
console.error(`
|
|
45220
|
-
${c.error("✖")} ${c.red("API key is required")}
|
|
45221
|
-
` + ` ${c.dim(`Configure with: locus config setup --api-key <key>
|
|
45222
|
-
Or pass --api-key flag`)}
|
|
45223
|
-
`);
|
|
45224
|
-
process.exit(1);
|
|
45338
|
+
requireInitialization(projectPath, "discuss");
|
|
45339
|
+
const discussionManager = new DiscussionManager(projectPath);
|
|
45340
|
+
if (values.list) {
|
|
45341
|
+
return listDiscussions(discussionManager);
|
|
45225
45342
|
}
|
|
45226
|
-
|
|
45227
|
-
|
|
45228
|
-
apiKey,
|
|
45229
|
-
apiBase,
|
|
45230
|
-
workspaceId: values.workspace
|
|
45231
|
-
});
|
|
45232
|
-
let workspaceId;
|
|
45233
|
-
try {
|
|
45234
|
-
workspaceId = await resolver.resolve();
|
|
45235
|
-
} catch (error48) {
|
|
45236
|
-
console.error(`
|
|
45237
|
-
${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
|
|
45238
|
-
`);
|
|
45239
|
-
process.exit(1);
|
|
45343
|
+
if (values.show) {
|
|
45344
|
+
return showDiscussion(discussionManager, values.show);
|
|
45240
45345
|
}
|
|
45241
|
-
|
|
45242
|
-
|
|
45243
|
-
|
|
45346
|
+
if (values.archive) {
|
|
45347
|
+
return archiveDiscussion(discussionManager, values.archive);
|
|
45348
|
+
}
|
|
45349
|
+
if (values.delete) {
|
|
45350
|
+
return deleteDiscussion(discussionManager, values.delete);
|
|
45351
|
+
}
|
|
45352
|
+
const topic = positionals.join(" ").trim();
|
|
45353
|
+
if (!topic) {
|
|
45354
|
+
showDiscussHelp();
|
|
45355
|
+
return;
|
|
45356
|
+
}
|
|
45357
|
+
const settings = new SettingsManager(projectPath).load();
|
|
45358
|
+
const provider = resolveProvider3(values.provider || settings.provider);
|
|
45359
|
+
const model = values.model || settings.model || DEFAULT_MODEL[provider];
|
|
45360
|
+
const reasoningEffort = values["reasoning-effort"];
|
|
45361
|
+
const aiRunner = createAiRunner(provider, {
|
|
45362
|
+
projectPath,
|
|
45363
|
+
model,
|
|
45364
|
+
reasoningEffort
|
|
45244
45365
|
});
|
|
45245
|
-
const
|
|
45246
|
-
|
|
45247
|
-
|
|
45366
|
+
const log = (message, level) => {
|
|
45367
|
+
const icon = level === "success" ? c.success("✔") : level === "error" ? c.error("✖") : level === "warn" ? c.warning("!") : c.info("●");
|
|
45368
|
+
console.log(` ${icon} ${message}`);
|
|
45369
|
+
};
|
|
45370
|
+
const facilitator = new DiscussionFacilitator({
|
|
45248
45371
|
projectPath,
|
|
45249
|
-
|
|
45250
|
-
|
|
45251
|
-
|
|
45252
|
-
|
|
45253
|
-
|
|
45254
|
-
if (level === "warn") {
|
|
45255
|
-
console.log(` ${c.warning("!")} ${message}`);
|
|
45256
|
-
return;
|
|
45257
|
-
}
|
|
45258
|
-
if (level === "success") {
|
|
45259
|
-
console.log(` ${c.success("✔")} ${message}`);
|
|
45260
|
-
return;
|
|
45261
|
-
}
|
|
45262
|
-
console.log(` ${c.info("●")} ${message}`);
|
|
45263
|
-
}
|
|
45372
|
+
aiRunner,
|
|
45373
|
+
discussionManager,
|
|
45374
|
+
log,
|
|
45375
|
+
provider,
|
|
45376
|
+
model
|
|
45264
45377
|
});
|
|
45265
45378
|
console.log(`
|
|
45266
|
-
${c.
|
|
45379
|
+
${c.header(" DISCUSSION ")} ${c.bold("Starting interactive discussion...")}
|
|
45267
45380
|
`);
|
|
45268
|
-
|
|
45269
|
-
|
|
45270
|
-
console.log(`
|
|
45271
|
-
${c.success("✔")} ${c.success("Docs sync complete.")} ${c.dim("Local docs: .locus/documents")}
|
|
45381
|
+
console.log(` ${c.dim("Topic:")} ${c.bold(topic)}`);
|
|
45382
|
+
console.log(` ${c.dim("Model:")} ${c.dim(`${model} (${provider})`)}
|
|
45272
45383
|
`);
|
|
45273
|
-
|
|
45274
|
-
|
|
45275
|
-
|
|
45276
|
-
|
|
45277
|
-
|
|
45278
|
-
|
|
45279
|
-
|
|
45280
|
-
|
|
45281
|
-
console.log(`
|
|
45282
|
-
${c.header(" DOCS ")}
|
|
45283
|
-
${c.primary("locus docs")} ${c.dim("<command> [options]")}
|
|
45284
|
-
|
|
45285
|
-
${c.header(" COMMANDS ")}
|
|
45286
|
-
${c.success("sync")} Sync workspace docs from API to .locus/documents
|
|
45287
|
-
|
|
45288
|
-
${c.header(" EXAMPLES ")}
|
|
45289
|
-
${c.dim("$")} ${c.primary("locus docs sync")}
|
|
45290
|
-
${c.dim("$")} ${c.primary("locus docs sync --workspace ws_123")}
|
|
45384
|
+
const renderer = new ProgressRenderer;
|
|
45385
|
+
let discussionId;
|
|
45386
|
+
try {
|
|
45387
|
+
renderer.showThinkingStarted();
|
|
45388
|
+
const result = await facilitator.startDiscussion(topic);
|
|
45389
|
+
renderer.showThinkingStopped();
|
|
45390
|
+
discussionId = result.discussion.id;
|
|
45391
|
+
process.stdout.write(`
|
|
45291
45392
|
`);
|
|
45292
|
-
|
|
45293
|
-
|
|
45294
|
-
console.log(`
|
|
45295
|
-
${c.header(" DOCS SYNC ")}
|
|
45296
|
-
${c.primary("locus docs sync")} ${c.dim("[options]")}
|
|
45297
|
-
|
|
45298
|
-
${c.header(" OPTIONS ")}
|
|
45299
|
-
${c.secondary("--api-key")} <key> API key override (reads from settings.json)
|
|
45300
|
-
${c.secondary("--api-url")} <url> API base URL (default: https://api.locusai.dev/api)
|
|
45301
|
-
${c.secondary("--workspace")} <id> Workspace ID (optional if persisted or resolvable)
|
|
45302
|
-
${c.secondary("--dir")} <path> Project directory (default: current)
|
|
45303
|
-
${c.secondary("--help")} Show docs sync help
|
|
45393
|
+
process.stdout.write(result.message);
|
|
45394
|
+
process.stdout.write(`
|
|
45304
45395
|
|
|
45305
|
-
${c.header(" EXAMPLES ")}
|
|
45306
|
-
${c.dim("$")} ${c.primary("locus docs sync")}
|
|
45307
|
-
${c.dim("$")} ${c.primary("locus docs sync --workspace ws_123")}
|
|
45308
45396
|
`);
|
|
45309
|
-
|
|
45310
|
-
|
|
45311
|
-
|
|
45312
|
-
|
|
45313
|
-
|
|
45314
|
-
|
|
45315
|
-
// src/display/json-stream-renderer.ts
|
|
45316
|
-
init_src();
|
|
45317
|
-
|
|
45318
|
-
class JsonStreamRenderer {
|
|
45319
|
-
sessionId;
|
|
45320
|
-
command;
|
|
45321
|
-
model;
|
|
45322
|
-
provider;
|
|
45323
|
-
cwd;
|
|
45324
|
-
statsTracker;
|
|
45325
|
-
started = false;
|
|
45326
|
-
done = false;
|
|
45327
|
-
constructor(options) {
|
|
45328
|
-
this.sessionId = options.sessionId;
|
|
45329
|
-
this.command = options.command;
|
|
45330
|
-
this.model = options.model;
|
|
45331
|
-
this.provider = options.provider;
|
|
45332
|
-
this.cwd = options.cwd;
|
|
45333
|
-
this.statsTracker = new ExecutionStatsTracker;
|
|
45334
|
-
}
|
|
45335
|
-
emitStart() {
|
|
45336
|
-
if (this.started)
|
|
45337
|
-
return;
|
|
45338
|
-
this.started = true;
|
|
45339
|
-
this.emit(createCliStreamEvent(CliStreamEventType.START, this.sessionId, {
|
|
45340
|
-
command: this.command,
|
|
45341
|
-
model: this.model,
|
|
45342
|
-
provider: this.provider,
|
|
45343
|
-
cwd: this.cwd
|
|
45344
|
-
}));
|
|
45345
|
-
}
|
|
45346
|
-
handleChunk(chunk) {
|
|
45347
|
-
this.ensureStarted();
|
|
45348
|
-
switch (chunk.type) {
|
|
45349
|
-
case "text_delta":
|
|
45350
|
-
this.emit(createCliStreamEvent(CliStreamEventType.TEXT_DELTA, this.sessionId, {
|
|
45351
|
-
content: chunk.content
|
|
45352
|
-
}));
|
|
45353
|
-
break;
|
|
45354
|
-
case "thinking":
|
|
45355
|
-
this.emit(createCliStreamEvent(CliStreamEventType.THINKING, this.sessionId, {
|
|
45356
|
-
content: chunk.content
|
|
45357
|
-
}));
|
|
45358
|
-
break;
|
|
45359
|
-
case "tool_use":
|
|
45360
|
-
this.statsTracker.toolStarted(chunk.tool, chunk.id);
|
|
45361
|
-
this.emit(createCliStreamEvent(CliStreamEventType.TOOL_STARTED, this.sessionId, {
|
|
45362
|
-
tool: chunk.tool,
|
|
45363
|
-
toolId: chunk.id,
|
|
45364
|
-
parameters: chunk.parameters
|
|
45365
|
-
}));
|
|
45366
|
-
break;
|
|
45367
|
-
case "tool_result":
|
|
45368
|
-
if (chunk.success) {
|
|
45369
|
-
this.statsTracker.toolCompleted(chunk.tool, chunk.id);
|
|
45370
|
-
} else {
|
|
45371
|
-
this.statsTracker.toolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
|
|
45372
|
-
}
|
|
45373
|
-
this.emit(createCliStreamEvent(CliStreamEventType.TOOL_COMPLETED, this.sessionId, {
|
|
45374
|
-
tool: chunk.tool,
|
|
45375
|
-
toolId: chunk.id,
|
|
45376
|
-
success: chunk.success,
|
|
45377
|
-
duration: chunk.duration,
|
|
45378
|
-
error: chunk.error
|
|
45379
|
-
}));
|
|
45380
|
-
break;
|
|
45381
|
-
case "tool_parameters":
|
|
45382
|
-
break;
|
|
45383
|
-
case "result":
|
|
45384
|
-
break;
|
|
45385
|
-
case "error":
|
|
45386
|
-
this.statsTracker.setError(chunk.error);
|
|
45387
|
-
this.emitError("UNKNOWN", chunk.error);
|
|
45388
|
-
break;
|
|
45389
|
-
}
|
|
45390
|
-
}
|
|
45391
|
-
emitStatus(status, message) {
|
|
45392
|
-
this.ensureStarted();
|
|
45393
|
-
this.emit(createCliStreamEvent(CliStreamEventType.STATUS, this.sessionId, {
|
|
45394
|
-
status,
|
|
45395
|
-
message
|
|
45396
|
-
}));
|
|
45397
|
-
}
|
|
45398
|
-
emitError(code, message, options) {
|
|
45399
|
-
this.ensureStarted();
|
|
45400
|
-
this.emit(createCliStreamEvent(CliStreamEventType.ERROR, this.sessionId, {
|
|
45401
|
-
error: createProtocolError(code, message, options)
|
|
45402
|
-
}));
|
|
45403
|
-
}
|
|
45404
|
-
emitDone(exitCode) {
|
|
45405
|
-
if (this.done)
|
|
45406
|
-
return;
|
|
45407
|
-
this.done = true;
|
|
45408
|
-
this.ensureStarted();
|
|
45409
|
-
const stats = this.statsTracker.finalize();
|
|
45410
|
-
this.emit(createCliStreamEvent(CliStreamEventType.DONE, this.sessionId, {
|
|
45411
|
-
exitCode,
|
|
45412
|
-
duration: stats.duration,
|
|
45413
|
-
toolsUsed: stats.toolsUsed.length > 0 ? stats.toolsUsed : undefined,
|
|
45414
|
-
tokensUsed: stats.tokensUsed,
|
|
45415
|
-
success: exitCode === 0
|
|
45416
|
-
}));
|
|
45417
|
-
}
|
|
45418
|
-
emitFatalError(code, message, options) {
|
|
45419
|
-
this.statsTracker.setError(message);
|
|
45420
|
-
this.emitError(code, message, {
|
|
45421
|
-
...options,
|
|
45422
|
-
recoverable: false
|
|
45423
|
-
});
|
|
45424
|
-
this.emitDone(1);
|
|
45425
|
-
}
|
|
45426
|
-
isDone() {
|
|
45427
|
-
return this.done;
|
|
45428
|
-
}
|
|
45429
|
-
ensureStarted() {
|
|
45430
|
-
if (!this.started) {
|
|
45431
|
-
this.emitStart();
|
|
45432
|
-
}
|
|
45433
|
-
}
|
|
45434
|
-
emit(event) {
|
|
45435
|
-
process.stdout.write(`${JSON.stringify(event)}
|
|
45397
|
+
renderer.finalize();
|
|
45398
|
+
} catch (error48) {
|
|
45399
|
+
renderer.finalize();
|
|
45400
|
+
console.error(`
|
|
45401
|
+
${c.error("✖")} ${c.red("Failed to start discussion:")} ${error48 instanceof Error ? error48.message : String(error48)}
|
|
45436
45402
|
`);
|
|
45403
|
+
process.exit(1);
|
|
45437
45404
|
}
|
|
45438
|
-
}
|
|
45439
|
-
|
|
45440
|
-
// src/commands/exec.ts
|
|
45441
|
-
init_progress_renderer();
|
|
45442
|
-
init_settings_manager();
|
|
45443
|
-
init_utils3();
|
|
45444
|
-
|
|
45445
|
-
// src/commands/exec-sessions.ts
|
|
45446
|
-
init_index_node();
|
|
45447
|
-
function formatRelativeTime(timestamp) {
|
|
45448
|
-
const now = Date.now();
|
|
45449
|
-
const diff = now - timestamp;
|
|
45450
|
-
const seconds = Math.floor(diff / 1000);
|
|
45451
|
-
const minutes = Math.floor(seconds / 60);
|
|
45452
|
-
const hours = Math.floor(minutes / 60);
|
|
45453
|
-
const days = Math.floor(hours / 24);
|
|
45454
|
-
if (days > 0) {
|
|
45455
|
-
return days === 1 ? "yesterday" : `${days} days ago`;
|
|
45456
|
-
}
|
|
45457
|
-
if (hours > 0) {
|
|
45458
|
-
return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
|
|
45459
|
-
}
|
|
45460
|
-
if (minutes > 0) {
|
|
45461
|
-
return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
|
|
45462
|
-
}
|
|
45463
|
-
return "just now";
|
|
45464
|
-
}
|
|
45465
|
-
|
|
45466
|
-
class SessionCommands {
|
|
45467
|
-
historyManager;
|
|
45468
|
-
constructor(projectPath) {
|
|
45469
|
-
this.historyManager = new HistoryManager(projectPath);
|
|
45470
|
-
}
|
|
45471
|
-
async list() {
|
|
45472
|
-
const sessions = this.historyManager.listSessions();
|
|
45473
|
-
if (sessions.length === 0) {
|
|
45474
|
-
console.log(`
|
|
45475
|
-
${c.dim("No exec sessions found.")}
|
|
45405
|
+
console.log(` ${c.dim("Type your response, or 'help' for commands.")}`);
|
|
45406
|
+
console.log(` ${c.dim("Enter to send, Shift+Enter for newline. Use 'exit' or Ctrl+D to quit.")}
|
|
45476
45407
|
`);
|
|
45477
|
-
|
|
45408
|
+
let isProcessing = false;
|
|
45409
|
+
let interrupted = false;
|
|
45410
|
+
const shutdown = () => {
|
|
45411
|
+
if (isProcessing) {
|
|
45412
|
+
aiRunner.abort();
|
|
45478
45413
|
}
|
|
45479
45414
|
console.log(`
|
|
45480
|
-
${c.
|
|
45481
|
-
`
|
|
45482
|
-
|
|
45483
|
-
const shortId = this.getShortId(session2.id);
|
|
45484
|
-
const age = formatRelativeTime(session2.updatedAt);
|
|
45485
|
-
const msgCount = session2.messages.length;
|
|
45486
|
-
const firstUserMsg = session2.messages.find((m) => m.role === "user");
|
|
45487
|
-
const preview = firstUserMsg ? firstUserMsg.content.slice(0, 50).replace(/\n/g, " ") : "(empty session)";
|
|
45488
|
-
console.log(` ${c.cyan(shortId)} ${c.gray("-")} ${preview}${firstUserMsg && firstUserMsg.content.length > 50 ? "..." : ""}`);
|
|
45489
|
-
console.log(` ${c.dim(`${msgCount} messages • ${age}`)}`);
|
|
45490
|
-
console.log();
|
|
45491
|
-
}
|
|
45492
|
-
if (sessions.length > 10) {
|
|
45493
|
-
console.log(c.dim(` ... and ${sessions.length - 10} more sessions
|
|
45415
|
+
${c.dim("Discussion saved.")} ${c.dim("ID:")} ${c.cyan(discussionId)}`);
|
|
45416
|
+
console.log(c.dim(`
|
|
45417
|
+
Goodbye!
|
|
45494
45418
|
`));
|
|
45419
|
+
inputHandler.stop();
|
|
45420
|
+
process.exit(0);
|
|
45421
|
+
};
|
|
45422
|
+
const handleSubmit = async (input) => {
|
|
45423
|
+
interrupted = false;
|
|
45424
|
+
const trimmed = input.trim();
|
|
45425
|
+
if (trimmed === "") {
|
|
45426
|
+
inputHandler.showPrompt();
|
|
45427
|
+
return;
|
|
45495
45428
|
}
|
|
45496
|
-
|
|
45497
|
-
|
|
45498
|
-
|
|
45499
|
-
|
|
45500
|
-
|
|
45429
|
+
if (!trimmed.includes(`
|
|
45430
|
+
`)) {
|
|
45431
|
+
const lowerInput = trimmed.toLowerCase();
|
|
45432
|
+
if (lowerInput === "help") {
|
|
45433
|
+
showReplHelp();
|
|
45434
|
+
inputHandler.showPrompt();
|
|
45435
|
+
return;
|
|
45436
|
+
}
|
|
45437
|
+
if (lowerInput === "exit" || lowerInput === "quit") {
|
|
45438
|
+
shutdown();
|
|
45439
|
+
return;
|
|
45440
|
+
}
|
|
45441
|
+
if (lowerInput === "insights") {
|
|
45442
|
+
showCurrentInsights(discussionManager, discussionId);
|
|
45443
|
+
inputHandler.showPrompt();
|
|
45444
|
+
return;
|
|
45445
|
+
}
|
|
45446
|
+
if (lowerInput === "summary") {
|
|
45447
|
+
isProcessing = true;
|
|
45448
|
+
const summaryRenderer = new ProgressRenderer;
|
|
45449
|
+
try {
|
|
45450
|
+
summaryRenderer.showThinkingStarted();
|
|
45451
|
+
const summary = await facilitator.summarizeDiscussion(discussionId);
|
|
45452
|
+
summaryRenderer.showThinkingStopped();
|
|
45453
|
+
process.stdout.write(`
|
|
45501
45454
|
`);
|
|
45502
|
-
|
|
45455
|
+
process.stdout.write(summary);
|
|
45456
|
+
process.stdout.write(`
|
|
45503
45457
|
`);
|
|
45504
|
-
|
|
45505
|
-
|
|
45506
|
-
|
|
45507
|
-
|
|
45508
|
-
|
|
45509
|
-
${c.error("Error:")} Session ${c.cyan(sessionId)} not found
|
|
45458
|
+
summaryRenderer.finalize();
|
|
45459
|
+
const discussion2 = discussionManager.load(discussionId);
|
|
45460
|
+
if (discussion2) {
|
|
45461
|
+
console.log(`
|
|
45462
|
+
${c.success("✔")} ${c.success("Discussion completed!")}
|
|
45510
45463
|
`);
|
|
45511
|
-
|
|
45464
|
+
console.log(` ${c.dim("Messages:")} ${discussion2.messages.length} ${c.dim("Insights:")} ${discussion2.insights.length}
|
|
45512
45465
|
`);
|
|
45513
|
-
|
|
45514
|
-
|
|
45515
|
-
|
|
45516
|
-
${c.primary("Session:")} ${c.cyan(session2.id)}`);
|
|
45517
|
-
console.log(` ${c.dim(`Created: ${new Date(session2.createdAt).toLocaleString()}`)}`);
|
|
45518
|
-
console.log(` ${c.dim(`Model: ${session2.metadata.model} (${session2.metadata.provider})`)}
|
|
45466
|
+
}
|
|
45467
|
+
console.log(` ${c.dim("To review:")} ${c.cyan(`locus discuss --show ${discussionId}`)}`);
|
|
45468
|
+
console.log(` ${c.dim("To list all:")} ${c.cyan("locus discuss --list")}
|
|
45519
45469
|
`);
|
|
45520
|
-
|
|
45521
|
-
|
|
45470
|
+
} catch (error48) {
|
|
45471
|
+
summaryRenderer.finalize();
|
|
45472
|
+
console.error(`
|
|
45473
|
+
${c.error("✖")} ${c.red("Failed to summarize:")} ${error48 instanceof Error ? error48.message : String(error48)}
|
|
45522
45474
|
`);
|
|
45523
|
-
|
|
45475
|
+
}
|
|
45476
|
+
inputHandler.stop();
|
|
45477
|
+
process.exit(0);
|
|
45478
|
+
return;
|
|
45479
|
+
}
|
|
45524
45480
|
}
|
|
45525
|
-
|
|
45526
|
-
|
|
45527
|
-
|
|
45528
|
-
|
|
45529
|
-
|
|
45530
|
-
|
|
45531
|
-
|
|
45481
|
+
const images = detectImages(trimmed);
|
|
45482
|
+
if (images.length > 0) {
|
|
45483
|
+
for (const img of images) {
|
|
45484
|
+
const status = img.exists ? c.success("attached") : c.warning("not found");
|
|
45485
|
+
process.stdout.write(` ${c.cyan(`[Image: ${imageDisplayName(img.path)}]`)} ${status}\r
|
|
45486
|
+
`);
|
|
45487
|
+
}
|
|
45488
|
+
}
|
|
45489
|
+
const cleanedInput = stripImagePaths(trimmed, images);
|
|
45490
|
+
const effectiveInput = cleanedInput + buildImageContext(images);
|
|
45491
|
+
isProcessing = true;
|
|
45492
|
+
const chunkRenderer = new ProgressRenderer;
|
|
45493
|
+
try {
|
|
45494
|
+
chunkRenderer.showThinkingStarted();
|
|
45495
|
+
const stream4 = facilitator.continueDiscussionStream(discussionId, effectiveInput);
|
|
45496
|
+
let result = {
|
|
45497
|
+
response: "",
|
|
45498
|
+
insights: []
|
|
45499
|
+
};
|
|
45500
|
+
let iterResult = await stream4.next();
|
|
45501
|
+
while (!iterResult.done) {
|
|
45502
|
+
chunkRenderer.renderChunk(iterResult.value);
|
|
45503
|
+
iterResult = await stream4.next();
|
|
45504
|
+
}
|
|
45505
|
+
result = iterResult.value;
|
|
45506
|
+
chunkRenderer.finalize();
|
|
45507
|
+
if (result.insights.length > 0) {
|
|
45508
|
+
console.log("");
|
|
45509
|
+
for (const insight of result.insights) {
|
|
45510
|
+
const tag = formatInsightTag(insight.type);
|
|
45511
|
+
console.log(` ${tag} ${c.bold(insight.title)}`);
|
|
45512
|
+
console.log(` ${c.dim(insight.content)}
|
|
45532
45513
|
`);
|
|
45533
|
-
|
|
45534
|
-
console.log(` ${line}`);
|
|
45514
|
+
}
|
|
45535
45515
|
}
|
|
45536
|
-
|
|
45537
|
-
|
|
45538
|
-
}
|
|
45539
|
-
async delete(sessionId) {
|
|
45540
|
-
if (!sessionId) {
|
|
45541
|
-
console.error(`
|
|
45542
|
-
${c.error("Error:")} Session ID is required
|
|
45543
|
-
`);
|
|
45544
|
-
console.log(` ${c.dim("Usage: locus exec sessions delete <session-id>")}
|
|
45545
|
-
`);
|
|
45546
|
-
return;
|
|
45547
|
-
}
|
|
45548
|
-
const session2 = this.historyManager.findSessionByPartialId(sessionId);
|
|
45549
|
-
if (!session2) {
|
|
45516
|
+
} catch (error48) {
|
|
45517
|
+
chunkRenderer.finalize();
|
|
45550
45518
|
console.error(`
|
|
45551
|
-
${c.error("
|
|
45519
|
+
${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
|
|
45552
45520
|
`);
|
|
45553
|
-
return;
|
|
45554
45521
|
}
|
|
45555
|
-
|
|
45556
|
-
if (
|
|
45557
|
-
|
|
45558
|
-
${c.success("✔")} Deleted session ${c.cyan(this.getShortId(session2.id))}
|
|
45559
|
-
`);
|
|
45560
|
-
} else {
|
|
45561
|
-
console.error(`
|
|
45562
|
-
${c.error("Error:")} Failed to delete session
|
|
45563
|
-
`);
|
|
45522
|
+
isProcessing = false;
|
|
45523
|
+
if (!interrupted) {
|
|
45524
|
+
inputHandler.showPrompt();
|
|
45564
45525
|
}
|
|
45565
|
-
}
|
|
45566
|
-
|
|
45567
|
-
|
|
45568
|
-
|
|
45569
|
-
|
|
45570
|
-
|
|
45526
|
+
};
|
|
45527
|
+
const inputHandler = new InputHandler({
|
|
45528
|
+
prompt: c.cyan("> "),
|
|
45529
|
+
continuationPrompt: c.dim("… "),
|
|
45530
|
+
onSubmit: (input) => {
|
|
45531
|
+
handleSubmit(input).catch((err) => {
|
|
45532
|
+
console.error(`
|
|
45533
|
+
${c.error("✖")} ${c.red(err instanceof Error ? err.message : String(err))}
|
|
45571
45534
|
`);
|
|
45572
|
-
|
|
45573
|
-
|
|
45574
|
-
|
|
45535
|
+
inputHandler.showPrompt();
|
|
45536
|
+
});
|
|
45537
|
+
},
|
|
45538
|
+
onInterrupt: () => {
|
|
45539
|
+
if (isProcessing) {
|
|
45540
|
+
interrupted = true;
|
|
45541
|
+
aiRunner.abort();
|
|
45542
|
+
isProcessing = false;
|
|
45543
|
+
console.log(c.dim(`
|
|
45544
|
+
[Interrupted]`));
|
|
45545
|
+
inputHandler.showPrompt();
|
|
45546
|
+
} else {
|
|
45547
|
+
shutdown();
|
|
45548
|
+
}
|
|
45549
|
+
},
|
|
45550
|
+
onClose: () => shutdown()
|
|
45551
|
+
});
|
|
45552
|
+
inputHandler.start();
|
|
45553
|
+
inputHandler.showPrompt();
|
|
45554
|
+
}
|
|
45555
|
+
function listDiscussions(discussionManager) {
|
|
45556
|
+
const discussions = discussionManager.list();
|
|
45557
|
+
if (discussions.length === 0) {
|
|
45575
45558
|
console.log(`
|
|
45576
|
-
${c.
|
|
45559
|
+
${c.dim("No discussions found.")}
|
|
45577
45560
|
`);
|
|
45578
|
-
}
|
|
45579
|
-
getShortId(sessionId) {
|
|
45580
|
-
const parts = sessionId.split("-");
|
|
45581
|
-
if (parts.length >= 3) {
|
|
45582
|
-
return parts.slice(-1)[0].slice(0, 8);
|
|
45583
|
-
}
|
|
45584
|
-
return sessionId.slice(0, 8);
|
|
45585
|
-
}
|
|
45586
|
-
}
|
|
45587
|
-
function showSessionsHelp() {
|
|
45588
|
-
console.log(`
|
|
45589
|
-
${c.primary("Session Commands")}
|
|
45590
|
-
|
|
45591
|
-
${c.success("list")} List recent exec sessions
|
|
45592
|
-
${c.success("show")} ${c.dim("<id>")} Show all messages in a session
|
|
45593
|
-
${c.success("delete")} ${c.dim("<id>")} Delete a specific session
|
|
45594
|
-
${c.success("clear")} Clear all exec sessions
|
|
45595
|
-
|
|
45596
|
-
${c.header(" EXAMPLES ")}
|
|
45597
|
-
${c.dim("$")} locus exec sessions list
|
|
45598
|
-
${c.dim("$")} locus exec sessions show e7f3a2b1
|
|
45599
|
-
${c.dim("$")} locus exec sessions delete e7f3a2b1
|
|
45600
|
-
${c.dim("$")} locus exec sessions clear
|
|
45601
|
-
|
|
45602
|
-
${c.dim("Session IDs can be partial (first 8 characters).")}
|
|
45561
|
+
console.log(` ${c.dim("Start one with:")} ${c.cyan('locus discuss "your topic"')}
|
|
45603
45562
|
`);
|
|
45604
|
-
}
|
|
45605
|
-
|
|
45606
|
-
// src/commands/exec.ts
|
|
45607
|
-
async function execCommand(args) {
|
|
45608
|
-
const { values, positionals } = parseArgs5({
|
|
45609
|
-
args,
|
|
45610
|
-
options: {
|
|
45611
|
-
model: { type: "string" },
|
|
45612
|
-
provider: { type: "string" },
|
|
45613
|
-
"reasoning-effort": { type: "string" },
|
|
45614
|
-
dir: { type: "string" },
|
|
45615
|
-
"no-stream": { type: "boolean" },
|
|
45616
|
-
"no-status": { type: "boolean" },
|
|
45617
|
-
interactive: { type: "boolean", short: "i" },
|
|
45618
|
-
session: { type: "string", short: "s" },
|
|
45619
|
-
"session-id": { type: "string" },
|
|
45620
|
-
"json-stream": { type: "boolean" }
|
|
45621
|
-
},
|
|
45622
|
-
strict: false
|
|
45623
|
-
});
|
|
45624
|
-
const jsonStream = values["json-stream"];
|
|
45625
|
-
const projectPath = values.dir || process.cwd();
|
|
45626
|
-
if (jsonStream) {
|
|
45627
|
-
await execJsonStream(values, positionals, projectPath);
|
|
45628
45563
|
return;
|
|
45629
45564
|
}
|
|
45630
|
-
|
|
45631
|
-
|
|
45632
|
-
|
|
45633
|
-
|
|
45634
|
-
const
|
|
45635
|
-
|
|
45636
|
-
|
|
45637
|
-
|
|
45638
|
-
|
|
45639
|
-
case "show":
|
|
45640
|
-
await cmds.show(sessionArg);
|
|
45641
|
-
return;
|
|
45642
|
-
case "delete":
|
|
45643
|
-
await cmds.delete(sessionArg);
|
|
45644
|
-
return;
|
|
45645
|
-
case "clear":
|
|
45646
|
-
await cmds.clear();
|
|
45647
|
-
return;
|
|
45648
|
-
default:
|
|
45649
|
-
showSessionsHelp();
|
|
45650
|
-
return;
|
|
45651
|
-
}
|
|
45652
|
-
}
|
|
45653
|
-
const execSettings = new SettingsManager(projectPath).load();
|
|
45654
|
-
const provider = resolveProvider3(values.provider || execSettings.provider);
|
|
45655
|
-
const model = values.model || execSettings.model || DEFAULT_MODEL[provider];
|
|
45656
|
-
const isInteractive = values.interactive;
|
|
45657
|
-
const sessionId = values.session;
|
|
45658
|
-
if (isInteractive) {
|
|
45659
|
-
const { InteractiveSession: InteractiveSession2 } = await Promise.resolve().then(() => (init_interactive_session(), exports_interactive_session));
|
|
45660
|
-
const session2 = new InteractiveSession2({
|
|
45661
|
-
projectPath,
|
|
45662
|
-
provider,
|
|
45663
|
-
model,
|
|
45664
|
-
sessionId
|
|
45665
|
-
});
|
|
45666
|
-
await session2.start();
|
|
45667
|
-
return;
|
|
45565
|
+
console.log(`
|
|
45566
|
+
${c.header(" DISCUSSIONS ")} ${c.dim(`(${discussions.length})`)}
|
|
45567
|
+
`);
|
|
45568
|
+
for (const disc of discussions) {
|
|
45569
|
+
const statusIcon = disc.status === "active" ? c.warning("◯") : disc.status === "completed" ? c.success("✔") : c.dim("⊘");
|
|
45570
|
+
console.log(` ${statusIcon} ${c.bold(disc.title)} ${c.dim(`[${disc.status}]`)} ${c.dim(`— ${disc.messages.length} messages, ${disc.insights.length} insights`)}`);
|
|
45571
|
+
console.log(` ${c.dim("ID:")} ${disc.id}`);
|
|
45572
|
+
console.log(` ${c.dim("Created:")} ${disc.createdAt}`);
|
|
45573
|
+
console.log("");
|
|
45668
45574
|
}
|
|
45669
|
-
|
|
45670
|
-
|
|
45671
|
-
|
|
45575
|
+
}
|
|
45576
|
+
function showDiscussion(discussionManager, id) {
|
|
45577
|
+
const md = discussionManager.getMarkdown(id);
|
|
45578
|
+
if (!md) {
|
|
45579
|
+
console.error(`
|
|
45580
|
+
${c.error("✖")} ${c.red(`Discussion not found: ${id}`)}
|
|
45581
|
+
`);
|
|
45672
45582
|
process.exit(1);
|
|
45673
45583
|
}
|
|
45674
|
-
|
|
45675
|
-
|
|
45676
|
-
const aiRunner = createAiRunner(provider, {
|
|
45677
|
-
projectPath,
|
|
45678
|
-
model,
|
|
45679
|
-
reasoningEffort
|
|
45680
|
-
});
|
|
45681
|
-
const builder = new PromptBuilder(projectPath);
|
|
45682
|
-
const fullPrompt = await builder.buildGenericPrompt(promptInput);
|
|
45683
|
-
console.log("");
|
|
45684
|
-
console.log(`${c.primary("\uD83D\uDE80")} ${c.bold("Executing prompt with repository context...")}`);
|
|
45685
|
-
console.log("");
|
|
45686
|
-
let responseContent = "";
|
|
45687
|
-
try {
|
|
45688
|
-
if (useStreaming) {
|
|
45689
|
-
const renderer = new ProgressRenderer;
|
|
45690
|
-
const statsTracker = new ExecutionStatsTracker;
|
|
45691
|
-
const stream4 = aiRunner.runStream(fullPrompt);
|
|
45692
|
-
renderer.showThinkingStarted();
|
|
45693
|
-
for await (const chunk of stream4) {
|
|
45694
|
-
switch (chunk.type) {
|
|
45695
|
-
case "text_delta":
|
|
45696
|
-
renderer.renderTextDelta(chunk.content);
|
|
45697
|
-
responseContent += chunk.content;
|
|
45698
|
-
break;
|
|
45699
|
-
case "tool_use":
|
|
45700
|
-
statsTracker.toolStarted(chunk.tool, chunk.id);
|
|
45701
|
-
renderer.showToolStarted(chunk.tool, chunk.id);
|
|
45702
|
-
break;
|
|
45703
|
-
case "thinking":
|
|
45704
|
-
renderer.showThinkingStarted();
|
|
45705
|
-
break;
|
|
45706
|
-
case "tool_result":
|
|
45707
|
-
if (chunk.success) {
|
|
45708
|
-
statsTracker.toolCompleted(chunk.tool, chunk.id);
|
|
45709
|
-
renderer.showToolCompleted(chunk.tool, undefined, chunk.id);
|
|
45710
|
-
} else {
|
|
45711
|
-
statsTracker.toolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
|
|
45712
|
-
renderer.showToolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
|
|
45713
|
-
}
|
|
45714
|
-
break;
|
|
45715
|
-
case "result":
|
|
45716
|
-
break;
|
|
45717
|
-
case "error": {
|
|
45718
|
-
statsTracker.setError(chunk.error);
|
|
45719
|
-
renderer.renderError(chunk.error);
|
|
45720
|
-
renderer.finalize();
|
|
45721
|
-
const errorStats = statsTracker.finalize();
|
|
45722
|
-
renderer.showSummary(errorStats);
|
|
45723
|
-
console.error(`
|
|
45724
|
-
${c.error("✖")} ${c.error("Execution failed!")}
|
|
45584
|
+
console.log(`
|
|
45585
|
+
${md}
|
|
45725
45586
|
`);
|
|
45726
|
-
|
|
45727
|
-
|
|
45728
|
-
|
|
45729
|
-
|
|
45730
|
-
renderer.finalize();
|
|
45731
|
-
const stats = statsTracker.finalize();
|
|
45732
|
-
renderer.showSummary(stats);
|
|
45733
|
-
} else {
|
|
45734
|
-
responseContent = await aiRunner.run(fullPrompt);
|
|
45735
|
-
console.log(responseContent);
|
|
45736
|
-
}
|
|
45587
|
+
}
|
|
45588
|
+
function archiveDiscussion(discussionManager, id) {
|
|
45589
|
+
try {
|
|
45590
|
+
discussionManager.archive(id);
|
|
45737
45591
|
console.log(`
|
|
45738
|
-
${c.success("✔")} ${c.
|
|
45592
|
+
${c.success("✔")} ${c.dim("Discussion archived.")}
|
|
45739
45593
|
`);
|
|
45740
45594
|
} catch (error48) {
|
|
45741
45595
|
console.error(`
|
|
45742
|
-
${c.error("✖")} ${c.
|
|
45596
|
+
${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
|
|
45743
45597
|
`);
|
|
45744
45598
|
process.exit(1);
|
|
45745
45599
|
}
|
|
45746
45600
|
}
|
|
45747
|
-
|
|
45748
|
-
const sessionId = values["session-id"] ?? values.session ?? randomUUID2();
|
|
45749
|
-
const execSettings = new SettingsManager(projectPath).load();
|
|
45750
|
-
const provider = resolveProvider3(values.provider || execSettings.provider);
|
|
45751
|
-
const model = values.model || execSettings.model || DEFAULT_MODEL[provider];
|
|
45752
|
-
const renderer = new JsonStreamRenderer({
|
|
45753
|
-
sessionId,
|
|
45754
|
-
command: "exec",
|
|
45755
|
-
model,
|
|
45756
|
-
provider,
|
|
45757
|
-
cwd: projectPath
|
|
45758
|
-
});
|
|
45759
|
-
const handleSignal = () => {
|
|
45760
|
-
if (renderer.isDone()) {
|
|
45761
|
-
return;
|
|
45762
|
-
}
|
|
45763
|
-
renderer.emitFatalError("PROCESS_CRASHED", "Process terminated by signal");
|
|
45764
|
-
if (process.stdout.writableNeedDrain) {
|
|
45765
|
-
process.stdout.once("drain", () => process.exit(1));
|
|
45766
|
-
} else {
|
|
45767
|
-
process.exit(1);
|
|
45768
|
-
}
|
|
45769
|
-
};
|
|
45770
|
-
process.on("SIGINT", handleSignal);
|
|
45771
|
-
process.on("SIGTERM", handleSignal);
|
|
45601
|
+
function deleteDiscussion(discussionManager, id) {
|
|
45772
45602
|
try {
|
|
45773
|
-
|
|
45774
|
-
|
|
45775
|
-
|
|
45776
|
-
|
|
45777
|
-
process.exit(1);
|
|
45778
|
-
}
|
|
45779
|
-
const promptInput = positionals.join(" ");
|
|
45780
|
-
if (!promptInput) {
|
|
45781
|
-
renderer.emitFatalError("UNKNOWN", 'Prompt is required. Usage: locus exec --json-stream "your prompt"');
|
|
45782
|
-
process.exit(1);
|
|
45783
|
-
}
|
|
45784
|
-
renderer.emitStart();
|
|
45785
|
-
renderer.emitStatus("running", "Building prompt context");
|
|
45786
|
-
const aiRunner = createAiRunner(provider, {
|
|
45787
|
-
projectPath,
|
|
45788
|
-
model
|
|
45789
|
-
});
|
|
45790
|
-
const builder = new PromptBuilder(projectPath);
|
|
45791
|
-
const fullPrompt = await builder.buildGenericPrompt(promptInput);
|
|
45792
|
-
renderer.emitStatus("streaming", "Streaming AI response");
|
|
45793
|
-
const stream4 = aiRunner.runStream(fullPrompt);
|
|
45794
|
-
for await (const chunk of stream4) {
|
|
45795
|
-
renderer.handleChunk(chunk);
|
|
45796
|
-
}
|
|
45797
|
-
renderer.emitDone(0);
|
|
45798
|
-
process.removeListener("SIGINT", handleSignal);
|
|
45799
|
-
process.removeListener("SIGTERM", handleSignal);
|
|
45603
|
+
discussionManager.delete(id);
|
|
45604
|
+
console.log(`
|
|
45605
|
+
${c.success("✔")} ${c.dim("Discussion deleted.")}
|
|
45606
|
+
`);
|
|
45800
45607
|
} catch (error48) {
|
|
45801
|
-
|
|
45802
|
-
|
|
45803
|
-
|
|
45804
|
-
}
|
|
45608
|
+
console.error(`
|
|
45609
|
+
${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
|
|
45610
|
+
`);
|
|
45805
45611
|
process.exit(1);
|
|
45806
45612
|
}
|
|
45807
45613
|
}
|
|
45808
|
-
|
|
45809
|
-
|
|
45810
|
-
|
|
45614
|
+
function showCurrentInsights(discussionManager, discussionId) {
|
|
45615
|
+
const discussion2 = discussionManager.load(discussionId);
|
|
45616
|
+
if (!discussion2 || discussion2.insights.length === 0) {
|
|
45617
|
+
console.log(`
|
|
45618
|
+
${c.dim("No insights extracted yet.")}
|
|
45619
|
+
`);
|
|
45620
|
+
return;
|
|
45621
|
+
}
|
|
45811
45622
|
console.log(`
|
|
45812
|
-
${c.header("
|
|
45813
|
-
|
|
45814
|
-
|
|
45815
|
-
|
|
45816
|
-
${c.
|
|
45817
|
-
${c.
|
|
45818
|
-
|
|
45819
|
-
|
|
45820
|
-
|
|
45821
|
-
|
|
45822
|
-
|
|
45823
|
-
|
|
45824
|
-
|
|
45825
|
-
|
|
45826
|
-
|
|
45827
|
-
|
|
45828
|
-
|
|
45829
|
-
|
|
45830
|
-
|
|
45831
|
-
|
|
45832
|
-
|
|
45833
|
-
|
|
45834
|
-
|
|
45835
|
-
|
|
45836
|
-
|
|
45837
|
-
|
|
45838
|
-
|
|
45839
|
-
|
|
45840
|
-
|
|
45841
|
-
${c.dim("--session, -s <id> Resume a previous session")}
|
|
45842
|
-
${c.dim("sessions list List recent sessions")}
|
|
45843
|
-
${c.dim("sessions show <id> Show session messages")}
|
|
45844
|
-
${c.dim("sessions delete <id> Delete a session")}
|
|
45845
|
-
${c.dim("sessions clear Clear all sessions")}
|
|
45846
|
-
${c.success("service")} Manage the Locus system service
|
|
45847
|
-
${c.dim("install Install as systemd/launchd service")}
|
|
45848
|
-
${c.dim("uninstall Remove the system service")}
|
|
45849
|
-
${c.dim("status Check if service is running")}
|
|
45850
|
-
${c.success("artifacts")} List and manage knowledge artifacts
|
|
45851
|
-
${c.dim("show <name> Show artifact content")}
|
|
45852
|
-
${c.dim("plan <name> Convert artifact to a plan")}
|
|
45853
|
-
${c.success("version")} Show installed package versions
|
|
45854
|
-
${c.success("upgrade")} Update CLI and Telegram to the latest version
|
|
45623
|
+
${c.header(" INSIGHTS ")} ${c.dim(`(${discussion2.insights.length})`)}
|
|
45624
|
+
`);
|
|
45625
|
+
for (const insight of discussion2.insights) {
|
|
45626
|
+
const tag = formatInsightTag(insight.type);
|
|
45627
|
+
console.log(` ${tag} ${c.bold(insight.title)}`);
|
|
45628
|
+
console.log(` ${c.dim(insight.content)}`);
|
|
45629
|
+
if (insight.tags.length > 0) {
|
|
45630
|
+
console.log(` ${c.dim(`Tags: ${insight.tags.join(", ")}`)}`);
|
|
45631
|
+
}
|
|
45632
|
+
console.log("");
|
|
45633
|
+
}
|
|
45634
|
+
}
|
|
45635
|
+
function formatInsightTag(type) {
|
|
45636
|
+
switch (type) {
|
|
45637
|
+
case "decision":
|
|
45638
|
+
return c.green("[DECISION]");
|
|
45639
|
+
case "requirement":
|
|
45640
|
+
return c.blue("[REQUIREMENT]");
|
|
45641
|
+
case "idea":
|
|
45642
|
+
return c.yellow("[IDEA]");
|
|
45643
|
+
case "concern":
|
|
45644
|
+
return c.red("[CONCERN]");
|
|
45645
|
+
case "learning":
|
|
45646
|
+
return c.cyan("[LEARNING]");
|
|
45647
|
+
}
|
|
45648
|
+
}
|
|
45649
|
+
function showReplHelp() {
|
|
45650
|
+
console.log(`
|
|
45651
|
+
${c.header(" DISCUSSION COMMANDS ")}
|
|
45855
45652
|
|
|
45856
|
-
|
|
45857
|
-
${c.
|
|
45858
|
-
${c.
|
|
45859
|
-
${c.
|
|
45860
|
-
${c.secondary("--reasoning-effort")} <level> Codex reasoning effort: ${c.dim("low, medium, high")} (default: model default)
|
|
45653
|
+
${c.cyan("summary")} Generate a final summary and end the discussion
|
|
45654
|
+
${c.cyan("insights")} Show all insights extracted so far
|
|
45655
|
+
${c.cyan("exit")} Save and exit without generating a summary
|
|
45656
|
+
${c.cyan("help")} Show this help message
|
|
45861
45657
|
|
|
45862
|
-
${c.header("
|
|
45863
|
-
${c.dim("$")} ${c.primary("locus init")}
|
|
45864
|
-
${c.dim("$")} ${c.primary("locus config setup")}
|
|
45865
|
-
${c.dim("$")} ${c.primary("locus telegram setup")}
|
|
45866
|
-
${c.dim("$")} ${c.primary("locus service install")}
|
|
45658
|
+
${c.header(" KEY BINDINGS ")}
|
|
45867
45659
|
|
|
45868
|
-
|
|
45869
|
-
${c.
|
|
45870
|
-
${c.
|
|
45871
|
-
${c.
|
|
45872
|
-
${c.
|
|
45873
|
-
${c.dim("$")} ${c.primary("locus telegram setup")}
|
|
45874
|
-
${c.dim("$")} ${c.primary('locus discuss "how should we design the auth system?"')}
|
|
45875
|
-
${c.dim("$")} ${c.primary("locus exec sessions list")}
|
|
45876
|
-
${c.dim("$")} ${c.primary("locus artifacts")}
|
|
45877
|
-
${c.dim("$")} ${c.primary("locus service install")}
|
|
45660
|
+
${c.cyan("Enter")} Send message
|
|
45661
|
+
${c.cyan("Shift+Enter")} Insert newline (also: Alt+Enter, Ctrl+J)
|
|
45662
|
+
${c.cyan("Ctrl+C")} Interrupt / clear input / exit
|
|
45663
|
+
${c.cyan("Ctrl+U")} Clear current input
|
|
45664
|
+
${c.cyan("Ctrl+W")} Delete last word
|
|
45878
45665
|
|
|
45879
|
-
|
|
45666
|
+
${c.dim("Type anything else to continue the discussion.")}
|
|
45880
45667
|
`);
|
|
45881
45668
|
}
|
|
45882
|
-
|
|
45883
|
-
|
|
45884
|
-
|
|
45885
|
-
import { parseArgs as parseArgs6 } from "node:util";
|
|
45669
|
+
function showDiscussHelp() {
|
|
45670
|
+
console.log(`
|
|
45671
|
+
${c.header(" LOCUS DISCUSS ")} ${c.dim("— Interactive AI Discussion")}
|
|
45886
45672
|
|
|
45887
|
-
|
|
45888
|
-
|
|
45673
|
+
${c.bold("Usage:")}
|
|
45674
|
+
${c.cyan('locus discuss "topic"')} Start a discussion on a topic
|
|
45675
|
+
${c.cyan("locus discuss --list")} List all discussions
|
|
45676
|
+
${c.cyan("locus discuss --show <id>")} Show discussion details
|
|
45677
|
+
${c.cyan("locus discuss --archive <id>")} Archive a discussion
|
|
45678
|
+
${c.cyan("locus discuss --delete <id>")} Delete a discussion
|
|
45889
45679
|
|
|
45890
|
-
|
|
45891
|
-
|
|
45892
|
-
|
|
45893
|
-
|
|
45894
|
-
|
|
45895
|
-
async summarize(tree) {
|
|
45896
|
-
const prompt = `Analyze this file tree and generate a JSON index.
|
|
45897
|
-
Return ONLY a JSON object with this structure:
|
|
45898
|
-
{
|
|
45899
|
-
"symbols": {},
|
|
45900
|
-
"responsibilities": { "path": "Description" }
|
|
45901
|
-
}
|
|
45680
|
+
${c.bold("Options:")}
|
|
45681
|
+
${c.dim("--model <model>")} AI model (claude: opus, sonnet, haiku | codex: gpt-5.3-codex, gpt-5-codex-mini)
|
|
45682
|
+
${c.dim("--provider <p>")} AI provider (claude, codex)
|
|
45683
|
+
${c.dim("--reasoning-effort <level>")} Reasoning effort (low, medium, high)
|
|
45684
|
+
${c.dim("--dir <path>")} Project directory
|
|
45902
45685
|
|
|
45903
|
-
|
|
45904
|
-
${
|
|
45905
|
-
|
|
45906
|
-
|
|
45907
|
-
|
|
45908
|
-
}
|
|
45909
|
-
}
|
|
45686
|
+
${c.bold("REPL Commands:")}
|
|
45687
|
+
${c.dim("summary")} Generate final summary and end the discussion
|
|
45688
|
+
${c.dim("insights")} Show all insights extracted so far
|
|
45689
|
+
${c.dim("exit")} Save and exit without generating a summary
|
|
45690
|
+
${c.dim("help")} Show available commands
|
|
45910
45691
|
|
|
45911
|
-
|
|
45692
|
+
${c.bold("Examples:")}
|
|
45693
|
+
${c.dim("# Start a discussion about architecture")}
|
|
45694
|
+
${c.cyan('locus discuss "how should we structure the auth system?"')}
|
|
45695
|
+
|
|
45696
|
+
${c.dim("# Review a past discussion")}
|
|
45697
|
+
${c.cyan("locus discuss --show disc-1234567890")}
|
|
45698
|
+
|
|
45699
|
+
${c.dim("# List all discussions")}
|
|
45700
|
+
${c.cyan("locus discuss --list")}
|
|
45701
|
+
`);
|
|
45702
|
+
}
|
|
45703
|
+
// src/commands/docs.ts
|
|
45704
|
+
init_index_node();
|
|
45705
|
+
init_config_manager();
|
|
45706
|
+
init_settings_manager();
|
|
45912
45707
|
init_utils3();
|
|
45913
|
-
|
|
45914
|
-
|
|
45708
|
+
init_workspace_resolver();
|
|
45709
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
45710
|
+
async function docsCommand(args) {
|
|
45711
|
+
const subcommand = args[0];
|
|
45712
|
+
const subArgs = args.slice(1);
|
|
45713
|
+
switch (subcommand) {
|
|
45714
|
+
case "sync":
|
|
45715
|
+
await docsSyncCommand(subArgs);
|
|
45716
|
+
break;
|
|
45717
|
+
default:
|
|
45718
|
+
showDocsHelp();
|
|
45719
|
+
break;
|
|
45720
|
+
}
|
|
45721
|
+
}
|
|
45722
|
+
async function docsSyncCommand(args) {
|
|
45723
|
+
const { values } = parseArgs4({
|
|
45915
45724
|
args,
|
|
45916
45725
|
options: {
|
|
45726
|
+
"api-key": { type: "string" },
|
|
45727
|
+
"api-url": { type: "string" },
|
|
45728
|
+
workspace: { type: "string" },
|
|
45917
45729
|
dir: { type: "string" },
|
|
45918
|
-
|
|
45919
|
-
provider: { type: "string" }
|
|
45730
|
+
help: { type: "boolean" }
|
|
45920
45731
|
},
|
|
45921
45732
|
strict: false
|
|
45922
45733
|
});
|
|
45734
|
+
if (values.help) {
|
|
45735
|
+
showDocsSyncHelp();
|
|
45736
|
+
return;
|
|
45737
|
+
}
|
|
45923
45738
|
const projectPath = values.dir || process.cwd();
|
|
45924
|
-
requireInitialization(projectPath, "
|
|
45925
|
-
new ConfigManager(projectPath)
|
|
45926
|
-
|
|
45927
|
-
const
|
|
45928
|
-
const
|
|
45739
|
+
requireInitialization(projectPath, "docs sync");
|
|
45740
|
+
const configManager = new ConfigManager(projectPath);
|
|
45741
|
+
configManager.updateVersion(VERSION2);
|
|
45742
|
+
const settingsManager = new SettingsManager(projectPath);
|
|
45743
|
+
const settings = settingsManager.load();
|
|
45744
|
+
const apiKey = values["api-key"] || settings.apiKey;
|
|
45745
|
+
if (!apiKey) {
|
|
45746
|
+
console.error(`
|
|
45747
|
+
${c.error("✖")} ${c.red("API key is required")}
|
|
45748
|
+
` + ` ${c.dim(`Configure with: locus config setup --api-key <key>
|
|
45749
|
+
Or pass --api-key flag`)}
|
|
45750
|
+
`);
|
|
45751
|
+
process.exit(1);
|
|
45752
|
+
}
|
|
45753
|
+
const apiBase = values["api-url"] || settings.apiUrl || "https://api.locusai.dev/api";
|
|
45754
|
+
const resolver = new WorkspaceResolver({
|
|
45755
|
+
apiKey,
|
|
45756
|
+
apiBase,
|
|
45757
|
+
workspaceId: values.workspace
|
|
45758
|
+
});
|
|
45759
|
+
let workspaceId;
|
|
45760
|
+
try {
|
|
45761
|
+
workspaceId = await resolver.resolve();
|
|
45762
|
+
} catch (error48) {
|
|
45763
|
+
console.error(`
|
|
45764
|
+
${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
|
|
45765
|
+
`);
|
|
45766
|
+
process.exit(1);
|
|
45767
|
+
}
|
|
45768
|
+
const client = new LocusClient({
|
|
45769
|
+
baseUrl: apiBase,
|
|
45770
|
+
token: apiKey
|
|
45771
|
+
});
|
|
45772
|
+
const fetcher = new DocumentFetcher({
|
|
45773
|
+
client,
|
|
45774
|
+
workspaceId,
|
|
45929
45775
|
projectPath,
|
|
45930
|
-
|
|
45776
|
+
log: (message, level) => {
|
|
45777
|
+
if (level === "error") {
|
|
45778
|
+
console.log(` ${c.error("✖")} ${message}`);
|
|
45779
|
+
return;
|
|
45780
|
+
}
|
|
45781
|
+
if (level === "warn") {
|
|
45782
|
+
console.log(` ${c.warning("!")} ${message}`);
|
|
45783
|
+
return;
|
|
45784
|
+
}
|
|
45785
|
+
if (level === "success") {
|
|
45786
|
+
console.log(` ${c.success("✔")} ${message}`);
|
|
45787
|
+
return;
|
|
45788
|
+
}
|
|
45789
|
+
console.log(` ${c.info("●")} ${message}`);
|
|
45790
|
+
}
|
|
45931
45791
|
});
|
|
45932
|
-
const summarizer = new TreeSummarizer(aiRunner);
|
|
45933
|
-
const indexer = new CodebaseIndexer(projectPath);
|
|
45934
45792
|
console.log(`
|
|
45935
|
-
${c.
|
|
45793
|
+
${c.info("●")} ${c.bold("Syncing docs from API...")}
|
|
45794
|
+
`);
|
|
45936
45795
|
try {
|
|
45937
|
-
|
|
45938
|
-
|
|
45939
|
-
|
|
45940
|
-
|
|
45796
|
+
await fetcher.fetch();
|
|
45797
|
+
console.log(`
|
|
45798
|
+
${c.success("✔")} ${c.success("Docs sync complete.")} ${c.dim("Local docs: .locus/documents")}
|
|
45799
|
+
`);
|
|
45941
45800
|
} catch (error48) {
|
|
45942
45801
|
console.error(`
|
|
45943
|
-
${c.error("✖")} ${c.
|
|
45944
|
-
|
|
45945
|
-
|
|
45802
|
+
${c.error("✖")} ${c.red(`Docs sync failed: ${error48 instanceof Error ? error48.message : String(error48)}`)}
|
|
45803
|
+
`);
|
|
45804
|
+
process.exit(1);
|
|
45946
45805
|
}
|
|
45806
|
+
}
|
|
45807
|
+
function showDocsHelp() {
|
|
45947
45808
|
console.log(`
|
|
45948
|
-
${c.
|
|
45809
|
+
${c.header(" DOCS ")}
|
|
45810
|
+
${c.primary("locus docs")} ${c.dim("<command> [options]")}
|
|
45811
|
+
|
|
45812
|
+
${c.header(" COMMANDS ")}
|
|
45813
|
+
${c.success("sync")} Sync workspace docs from API to .locus/documents
|
|
45814
|
+
|
|
45815
|
+
${c.header(" EXAMPLES ")}
|
|
45816
|
+
${c.dim("$")} ${c.primary("locus docs sync")}
|
|
45817
|
+
${c.dim("$")} ${c.primary("locus docs sync --workspace ws_123")}
|
|
45949
45818
|
`);
|
|
45950
45819
|
}
|
|
45951
|
-
|
|
45952
|
-
|
|
45953
|
-
|
|
45954
|
-
|
|
45955
|
-
async function initCommand() {
|
|
45956
|
-
const projectPath = process.cwd();
|
|
45957
|
-
const configManager = new ConfigManager(projectPath);
|
|
45958
|
-
if (isProjectInitialized(projectPath)) {
|
|
45959
|
-
console.log(`
|
|
45960
|
-
${c.info("ℹ️")} ${c.bold("Locus is already initialized. Updating configuration...")}
|
|
45961
|
-
`);
|
|
45962
|
-
const result = await configManager.reinit(VERSION2);
|
|
45963
|
-
const updates = [];
|
|
45964
|
-
if (result.versionUpdated) {
|
|
45965
|
-
updates.push(`Version updated: ${c.dim(result.previousVersion || "unknown")} → ${c.primary(VERSION2)}`);
|
|
45966
|
-
}
|
|
45967
|
-
if (result.directoriesCreated.length > 0) {
|
|
45968
|
-
updates.push(`Directories created: ${result.directoriesCreated.map((d) => c.dim(d)).join(", ")}`);
|
|
45969
|
-
}
|
|
45970
|
-
if (result.gitignoreUpdated) {
|
|
45971
|
-
updates.push(`Gitignore updated with Locus patterns`);
|
|
45972
|
-
}
|
|
45973
|
-
if (updates.length === 0) {
|
|
45974
|
-
console.log(` ${c.success("✔")} ${c.success("Configuration is already up to date!")}
|
|
45820
|
+
function showDocsSyncHelp() {
|
|
45821
|
+
console.log(`
|
|
45822
|
+
${c.header(" DOCS SYNC ")}
|
|
45823
|
+
${c.primary("locus docs sync")} ${c.dim("[options]")}
|
|
45975
45824
|
|
|
45976
|
-
${c.
|
|
45977
|
-
}
|
|
45978
|
-
|
|
45825
|
+
${c.header(" OPTIONS ")}
|
|
45826
|
+
${c.secondary("--api-key")} <key> API key override (reads from settings.json)
|
|
45827
|
+
${c.secondary("--api-url")} <url> API base URL (default: https://api.locusai.dev/api)
|
|
45828
|
+
${c.secondary("--workspace")} <id> Workspace ID (optional if persisted or resolvable)
|
|
45829
|
+
${c.secondary("--dir")} <path> Project directory (default: current)
|
|
45830
|
+
${c.secondary("--help")} Show docs sync help
|
|
45979
45831
|
|
|
45980
|
-
${c.
|
|
45981
|
-
${
|
|
45982
|
-
|
|
45832
|
+
${c.header(" EXAMPLES ")}
|
|
45833
|
+
${c.dim("$")} ${c.primary("locus docs sync")}
|
|
45834
|
+
${c.dim("$")} ${c.primary("locus docs sync --workspace ws_123")}
|
|
45983
45835
|
`);
|
|
45836
|
+
}
|
|
45837
|
+
// src/commands/exec.ts
|
|
45838
|
+
init_index_node();
|
|
45839
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
45840
|
+
import { parseArgs as parseArgs5 } from "node:util";
|
|
45841
|
+
|
|
45842
|
+
// src/display/json-stream-renderer.ts
|
|
45843
|
+
init_src();
|
|
45844
|
+
|
|
45845
|
+
class JsonStreamRenderer {
|
|
45846
|
+
sessionId;
|
|
45847
|
+
command;
|
|
45848
|
+
model;
|
|
45849
|
+
provider;
|
|
45850
|
+
cwd;
|
|
45851
|
+
statsTracker;
|
|
45852
|
+
started = false;
|
|
45853
|
+
done = false;
|
|
45854
|
+
constructor(options) {
|
|
45855
|
+
this.sessionId = options.sessionId;
|
|
45856
|
+
this.command = options.command;
|
|
45857
|
+
this.model = options.model;
|
|
45858
|
+
this.provider = options.provider;
|
|
45859
|
+
this.cwd = options.cwd;
|
|
45860
|
+
this.statsTracker = new ExecutionStatsTracker;
|
|
45861
|
+
}
|
|
45862
|
+
emitStart() {
|
|
45863
|
+
if (this.started)
|
|
45864
|
+
return;
|
|
45865
|
+
this.started = true;
|
|
45866
|
+
this.emit(createCliStreamEvent(CliStreamEventType.START, this.sessionId, {
|
|
45867
|
+
command: this.command,
|
|
45868
|
+
model: this.model,
|
|
45869
|
+
provider: this.provider,
|
|
45870
|
+
cwd: this.cwd
|
|
45871
|
+
}));
|
|
45872
|
+
}
|
|
45873
|
+
handleChunk(chunk) {
|
|
45874
|
+
this.ensureStarted();
|
|
45875
|
+
switch (chunk.type) {
|
|
45876
|
+
case "text_delta":
|
|
45877
|
+
this.emit(createCliStreamEvent(CliStreamEventType.TEXT_DELTA, this.sessionId, {
|
|
45878
|
+
content: chunk.content
|
|
45879
|
+
}));
|
|
45880
|
+
break;
|
|
45881
|
+
case "thinking":
|
|
45882
|
+
this.emit(createCliStreamEvent(CliStreamEventType.THINKING, this.sessionId, {
|
|
45883
|
+
content: chunk.content
|
|
45884
|
+
}));
|
|
45885
|
+
break;
|
|
45886
|
+
case "tool_use":
|
|
45887
|
+
this.statsTracker.toolStarted(chunk.tool, chunk.id);
|
|
45888
|
+
this.emit(createCliStreamEvent(CliStreamEventType.TOOL_STARTED, this.sessionId, {
|
|
45889
|
+
tool: chunk.tool,
|
|
45890
|
+
toolId: chunk.id,
|
|
45891
|
+
parameters: chunk.parameters
|
|
45892
|
+
}));
|
|
45893
|
+
break;
|
|
45894
|
+
case "tool_result":
|
|
45895
|
+
if (chunk.success) {
|
|
45896
|
+
this.statsTracker.toolCompleted(chunk.tool, chunk.id);
|
|
45897
|
+
} else {
|
|
45898
|
+
this.statsTracker.toolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
|
|
45899
|
+
}
|
|
45900
|
+
this.emit(createCliStreamEvent(CliStreamEventType.TOOL_COMPLETED, this.sessionId, {
|
|
45901
|
+
tool: chunk.tool,
|
|
45902
|
+
toolId: chunk.id,
|
|
45903
|
+
success: chunk.success,
|
|
45904
|
+
duration: chunk.duration,
|
|
45905
|
+
error: chunk.error
|
|
45906
|
+
}));
|
|
45907
|
+
break;
|
|
45908
|
+
case "tool_parameters":
|
|
45909
|
+
break;
|
|
45910
|
+
case "result":
|
|
45911
|
+
break;
|
|
45912
|
+
case "error":
|
|
45913
|
+
this.statsTracker.setError(chunk.error);
|
|
45914
|
+
this.emitError("UNKNOWN", chunk.error);
|
|
45915
|
+
break;
|
|
45916
|
+
}
|
|
45917
|
+
}
|
|
45918
|
+
emitStatus(status, message) {
|
|
45919
|
+
this.ensureStarted();
|
|
45920
|
+
this.emit(createCliStreamEvent(CliStreamEventType.STATUS, this.sessionId, {
|
|
45921
|
+
status,
|
|
45922
|
+
message
|
|
45923
|
+
}));
|
|
45924
|
+
}
|
|
45925
|
+
emitError(code, message, options) {
|
|
45926
|
+
this.ensureStarted();
|
|
45927
|
+
this.emit(createCliStreamEvent(CliStreamEventType.ERROR, this.sessionId, {
|
|
45928
|
+
error: createProtocolError(code, message, options)
|
|
45929
|
+
}));
|
|
45930
|
+
}
|
|
45931
|
+
emitDone(exitCode) {
|
|
45932
|
+
if (this.done)
|
|
45933
|
+
return;
|
|
45934
|
+
this.done = true;
|
|
45935
|
+
this.ensureStarted();
|
|
45936
|
+
const stats = this.statsTracker.finalize();
|
|
45937
|
+
this.emit(createCliStreamEvent(CliStreamEventType.DONE, this.sessionId, {
|
|
45938
|
+
exitCode,
|
|
45939
|
+
duration: stats.duration,
|
|
45940
|
+
toolsUsed: stats.toolsUsed.length > 0 ? stats.toolsUsed : undefined,
|
|
45941
|
+
tokensUsed: stats.tokensUsed,
|
|
45942
|
+
success: exitCode === 0
|
|
45943
|
+
}));
|
|
45944
|
+
}
|
|
45945
|
+
emitFatalError(code, message, options) {
|
|
45946
|
+
this.statsTracker.setError(message);
|
|
45947
|
+
this.emitError(code, message, {
|
|
45948
|
+
...options,
|
|
45949
|
+
recoverable: false
|
|
45950
|
+
});
|
|
45951
|
+
this.emitDone(1);
|
|
45952
|
+
}
|
|
45953
|
+
isDone() {
|
|
45954
|
+
return this.done;
|
|
45955
|
+
}
|
|
45956
|
+
ensureStarted() {
|
|
45957
|
+
if (!this.started) {
|
|
45958
|
+
this.emitStart();
|
|
45984
45959
|
}
|
|
45985
|
-
console.log(` ${c.bold("Next steps:")}
|
|
45986
|
-
1. Run '${c.primary("locus config setup")}' to configure your API key
|
|
45987
|
-
2. Run '${c.primary("locus index")}' to index your codebase
|
|
45988
|
-
3. Run '${c.primary("locus run")}' to start an agent
|
|
45989
|
-
|
|
45990
|
-
For more information, visit: ${c.underline("https://docs.locusai.dev")}
|
|
45991
|
-
`);
|
|
45992
|
-
return;
|
|
45993
45960
|
}
|
|
45994
|
-
|
|
45995
|
-
|
|
45996
|
-
${c.success("✨ Locus initialized successfully!")}
|
|
45997
|
-
|
|
45998
|
-
${c.bold("Created:")}
|
|
45999
|
-
${c.primary("\uD83D\uDCC1")} ${c.bold(".locus/")} ${c.dim("Configuration directory")}
|
|
46000
|
-
${c.primary("\uD83D\uDCC4")} ${c.bold(".locus/config.json")} ${c.dim("Project settings")}
|
|
46001
|
-
${c.primary("\uD83D\uDCDD")} ${c.bold(".locus/LOCUS.md")} ${c.dim("AI agent instructions")}
|
|
46002
|
-
${c.primary("\uD83D\uDCDD")} ${c.bold(".locus/LEARNINGS.md")} ${c.dim("Continuous learning log")}
|
|
46003
|
-
|
|
46004
|
-
${c.bold("Next steps:")}
|
|
46005
|
-
1. Run '${c.primary("locus config setup")}' to configure your API key
|
|
46006
|
-
2. Run '${c.primary("locus index")}' to index your codebase
|
|
46007
|
-
3. Run '${c.primary("locus run")}' to start an agent
|
|
46008
|
-
|
|
46009
|
-
For more information, visit: ${c.underline("https://docs.locusai.dev")}
|
|
45961
|
+
emit(event) {
|
|
45962
|
+
process.stdout.write(`${JSON.stringify(event)}
|
|
46010
45963
|
`);
|
|
45964
|
+
}
|
|
46011
45965
|
}
|
|
46012
45966
|
|
|
46013
|
-
// src/commands/
|
|
46014
|
-
|
|
46015
|
-
|
|
46016
|
-
// src/commands/review.ts
|
|
46017
|
-
init_index_node();
|
|
46018
|
-
init_config_manager();
|
|
45967
|
+
// src/commands/exec.ts
|
|
45968
|
+
init_progress_renderer();
|
|
46019
45969
|
init_settings_manager();
|
|
46020
45970
|
init_utils3();
|
|
46021
|
-
|
|
46022
|
-
|
|
46023
|
-
|
|
46024
|
-
|
|
46025
|
-
|
|
46026
|
-
const
|
|
46027
|
-
|
|
46028
|
-
|
|
45971
|
+
|
|
45972
|
+
// src/commands/exec-sessions.ts
|
|
45973
|
+
init_index_node();
|
|
45974
|
+
function formatRelativeTime(timestamp) {
|
|
45975
|
+
const now = Date.now();
|
|
45976
|
+
const diff = now - timestamp;
|
|
45977
|
+
const seconds = Math.floor(diff / 1000);
|
|
45978
|
+
const minutes = Math.floor(seconds / 60);
|
|
45979
|
+
const hours = Math.floor(minutes / 60);
|
|
45980
|
+
const days = Math.floor(hours / 24);
|
|
45981
|
+
if (days > 0) {
|
|
45982
|
+
return days === 1 ? "yesterday" : `${days} days ago`;
|
|
46029
45983
|
}
|
|
46030
|
-
|
|
46031
|
-
}
|
|
46032
|
-
async function reviewPrsCommand(args) {
|
|
46033
|
-
const { values } = parseArgs7({
|
|
46034
|
-
args,
|
|
46035
|
-
options: {
|
|
46036
|
-
"api-key": { type: "string" },
|
|
46037
|
-
workspace: { type: "string" },
|
|
46038
|
-
model: { type: "string" },
|
|
46039
|
-
provider: { type: "string" },
|
|
46040
|
-
"api-url": { type: "string" },
|
|
46041
|
-
dir: { type: "string" }
|
|
46042
|
-
},
|
|
46043
|
-
strict: false
|
|
46044
|
-
});
|
|
46045
|
-
const projectPath = values.dir || process.cwd();
|
|
46046
|
-
requireInitialization(projectPath, "review");
|
|
46047
|
-
const configManager = new ConfigManager(projectPath);
|
|
46048
|
-
configManager.updateVersion(VERSION2);
|
|
46049
|
-
const settingsManager = new SettingsManager(projectPath);
|
|
46050
|
-
const settings = settingsManager.load();
|
|
46051
|
-
const apiKey = values["api-key"] || settings.apiKey;
|
|
46052
|
-
if (!apiKey) {
|
|
46053
|
-
console.error(c.error("Error: API key is required for PR review"));
|
|
46054
|
-
console.error(c.dim(`Configure with: locus config setup --api-key <key>
|
|
46055
|
-
Or pass --api-key flag`));
|
|
46056
|
-
console.error(c.dim("For local staged-changes review, use: locus review local"));
|
|
46057
|
-
process.exit(1);
|
|
45984
|
+
if (hours > 0) {
|
|
45985
|
+
return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
|
|
46058
45986
|
}
|
|
46059
|
-
|
|
46060
|
-
|
|
46061
|
-
const apiBase = values["api-url"] || settings.apiUrl || "https://api.locusai.dev/api";
|
|
46062
|
-
let workspaceId;
|
|
46063
|
-
try {
|
|
46064
|
-
const resolver = new WorkspaceResolver({
|
|
46065
|
-
apiKey,
|
|
46066
|
-
apiBase,
|
|
46067
|
-
workspaceId: values.workspace
|
|
46068
|
-
});
|
|
46069
|
-
workspaceId = await resolver.resolve();
|
|
46070
|
-
} catch (error48) {
|
|
46071
|
-
console.error(c.error(error48 instanceof Error ? error48.message : String(error48)));
|
|
46072
|
-
process.exit(1);
|
|
45987
|
+
if (minutes > 0) {
|
|
45988
|
+
return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
|
|
46073
45989
|
}
|
|
46074
|
-
|
|
46075
|
-
|
|
46076
|
-
|
|
46077
|
-
|
|
46078
|
-
|
|
46079
|
-
|
|
46080
|
-
|
|
46081
|
-
|
|
46082
|
-
|
|
46083
|
-
|
|
46084
|
-
|
|
46085
|
-
|
|
46086
|
-
|
|
45990
|
+
return "just now";
|
|
45991
|
+
}
|
|
45992
|
+
|
|
45993
|
+
class SessionCommands {
|
|
45994
|
+
historyManager;
|
|
45995
|
+
constructor(projectPath) {
|
|
45996
|
+
this.historyManager = new HistoryManager(projectPath);
|
|
45997
|
+
}
|
|
45998
|
+
async list() {
|
|
45999
|
+
const sessions = this.historyManager.listSessions();
|
|
46000
|
+
if (sessions.length === 0) {
|
|
46001
|
+
console.log(`
|
|
46002
|
+
${c.dim("No exec sessions found.")}
|
|
46003
|
+
`);
|
|
46004
|
+
return;
|
|
46005
|
+
}
|
|
46087
46006
|
console.log(`
|
|
46088
|
-
${c.
|
|
46007
|
+
${c.primary("Recent Exec Sessions:")}
|
|
46089
46008
|
`);
|
|
46090
|
-
|
|
46009
|
+
for (const session2 of sessions.slice(0, 10)) {
|
|
46010
|
+
const shortId = this.getShortId(session2.id);
|
|
46011
|
+
const age = formatRelativeTime(session2.updatedAt);
|
|
46012
|
+
const msgCount = session2.messages.length;
|
|
46013
|
+
const firstUserMsg = session2.messages.find((m) => m.role === "user");
|
|
46014
|
+
const preview = firstUserMsg ? firstUserMsg.content.slice(0, 50).replace(/\n/g, " ") : "(empty session)";
|
|
46015
|
+
console.log(` ${c.cyan(shortId)} ${c.gray("-")} ${preview}${firstUserMsg && firstUserMsg.content.length > 50 ? "..." : ""}`);
|
|
46016
|
+
console.log(` ${c.dim(`${msgCount} messages • ${age}`)}`);
|
|
46017
|
+
console.log();
|
|
46018
|
+
}
|
|
46019
|
+
if (sessions.length > 10) {
|
|
46020
|
+
console.log(c.dim(` ... and ${sessions.length - 10} more sessions
|
|
46021
|
+
`));
|
|
46022
|
+
}
|
|
46091
46023
|
}
|
|
46092
|
-
|
|
46093
|
-
|
|
46024
|
+
async show(sessionId) {
|
|
46025
|
+
if (!sessionId) {
|
|
46026
|
+
console.error(`
|
|
46027
|
+
${c.error("Error:")} Session ID is required
|
|
46028
|
+
`);
|
|
46029
|
+
console.log(` ${c.dim("Usage: locus exec sessions show <session-id>")}
|
|
46094
46030
|
`);
|
|
46095
|
-
const agentId = `reviewer-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
46096
|
-
const reviewer = new ReviewerWorker({
|
|
46097
|
-
agentId,
|
|
46098
|
-
workspaceId,
|
|
46099
|
-
apiBase,
|
|
46100
|
-
projectPath,
|
|
46101
|
-
apiKey,
|
|
46102
|
-
model,
|
|
46103
|
-
provider
|
|
46104
|
-
});
|
|
46105
|
-
let isShuttingDown = false;
|
|
46106
|
-
const handleSignal = () => {
|
|
46107
|
-
if (isShuttingDown)
|
|
46108
46031
|
return;
|
|
46109
|
-
|
|
46032
|
+
}
|
|
46033
|
+
const session2 = this.historyManager.findSessionByPartialId(sessionId);
|
|
46034
|
+
if (!session2) {
|
|
46035
|
+
console.error(`
|
|
46036
|
+
${c.error("Error:")} Session ${c.cyan(sessionId)} not found
|
|
46037
|
+
`);
|
|
46038
|
+
console.log(` ${c.dim("Use 'locus exec sessions list' to see available sessions")}
|
|
46039
|
+
`);
|
|
46040
|
+
return;
|
|
46041
|
+
}
|
|
46110
46042
|
console.log(`
|
|
46111
|
-
${c.
|
|
46112
|
-
|
|
46113
|
-
}
|
|
46114
|
-
|
|
46115
|
-
|
|
46116
|
-
|
|
46117
|
-
|
|
46118
|
-
|
|
46119
|
-
|
|
46120
|
-
|
|
46121
|
-
|
|
46122
|
-
|
|
46123
|
-
|
|
46124
|
-
|
|
46125
|
-
|
|
46126
|
-
|
|
46127
|
-
|
|
46128
|
-
|
|
46129
|
-
|
|
46130
|
-
const localSettings = new SettingsManager(projectPath).load();
|
|
46131
|
-
const provider = resolveProvider3(values.provider || localSettings.provider);
|
|
46132
|
-
const model = values.model || localSettings.model || DEFAULT_MODEL[provider];
|
|
46133
|
-
const aiRunner = createAiRunner(provider, {
|
|
46134
|
-
projectPath,
|
|
46135
|
-
model
|
|
46136
|
-
});
|
|
46137
|
-
const reviewService = new ReviewService({
|
|
46138
|
-
aiRunner,
|
|
46139
|
-
projectPath,
|
|
46140
|
-
log: (msg, level) => {
|
|
46141
|
-
switch (level) {
|
|
46142
|
-
case "error":
|
|
46143
|
-
console.log(` ${c.error("✖")} ${msg}`);
|
|
46144
|
-
break;
|
|
46145
|
-
case "success":
|
|
46146
|
-
console.log(` ${c.success("✔")} ${msg}`);
|
|
46147
|
-
break;
|
|
46148
|
-
default:
|
|
46149
|
-
console.log(` ${c.dim(msg)}`);
|
|
46043
|
+
${c.primary("Session:")} ${c.cyan(session2.id)}`);
|
|
46044
|
+
console.log(` ${c.dim(`Created: ${new Date(session2.createdAt).toLocaleString()}`)}`);
|
|
46045
|
+
console.log(` ${c.dim(`Model: ${session2.metadata.model} (${session2.metadata.provider})`)}
|
|
46046
|
+
`);
|
|
46047
|
+
if (session2.messages.length === 0) {
|
|
46048
|
+
console.log(` ${c.dim("(No messages in this session)")}
|
|
46049
|
+
`);
|
|
46050
|
+
return;
|
|
46051
|
+
}
|
|
46052
|
+
console.log(c.dim(" ─".repeat(30)));
|
|
46053
|
+
console.log();
|
|
46054
|
+
for (const message of session2.messages) {
|
|
46055
|
+
const role = message.role === "user" ? c.cyan("You") : c.green("AI");
|
|
46056
|
+
const content = message.content;
|
|
46057
|
+
console.log(` ${role}:`);
|
|
46058
|
+
const lines = content.split(`
|
|
46059
|
+
`);
|
|
46060
|
+
for (const line of lines) {
|
|
46061
|
+
console.log(` ${line}`);
|
|
46150
46062
|
}
|
|
46063
|
+
console.log();
|
|
46064
|
+
}
|
|
46065
|
+
}
|
|
46066
|
+
async delete(sessionId) {
|
|
46067
|
+
if (!sessionId) {
|
|
46068
|
+
console.error(`
|
|
46069
|
+
${c.error("Error:")} Session ID is required
|
|
46070
|
+
`);
|
|
46071
|
+
console.log(` ${c.dim("Usage: locus exec sessions delete <session-id>")}
|
|
46072
|
+
`);
|
|
46073
|
+
return;
|
|
46074
|
+
}
|
|
46075
|
+
const session2 = this.historyManager.findSessionByPartialId(sessionId);
|
|
46076
|
+
if (!session2) {
|
|
46077
|
+
console.error(`
|
|
46078
|
+
${c.error("Error:")} Session ${c.cyan(sessionId)} not found
|
|
46079
|
+
`);
|
|
46080
|
+
return;
|
|
46151
46081
|
}
|
|
46152
|
-
|
|
46153
|
-
|
|
46154
|
-
|
|
46082
|
+
const deleted = this.historyManager.deleteSession(session2.id);
|
|
46083
|
+
if (deleted) {
|
|
46084
|
+
console.log(`
|
|
46085
|
+
${c.success("✔")} Deleted session ${c.cyan(this.getShortId(session2.id))}
|
|
46155
46086
|
`);
|
|
46156
|
-
|
|
46157
|
-
|
|
46158
|
-
|
|
46087
|
+
} else {
|
|
46088
|
+
console.error(`
|
|
46089
|
+
${c.error("Error:")} Failed to delete session
|
|
46159
46090
|
`);
|
|
46160
|
-
|
|
46091
|
+
}
|
|
46161
46092
|
}
|
|
46162
|
-
|
|
46163
|
-
|
|
46164
|
-
|
|
46093
|
+
async clear() {
|
|
46094
|
+
const count = this.historyManager.getSessionCount();
|
|
46095
|
+
if (count === 0) {
|
|
46096
|
+
console.log(`
|
|
46097
|
+
${c.dim("No sessions to clear.")}
|
|
46098
|
+
`);
|
|
46099
|
+
return;
|
|
46100
|
+
}
|
|
46101
|
+
const deleted = this.historyManager.clearAllSessions();
|
|
46102
|
+
console.log(`
|
|
46103
|
+
${c.success("✔")} Cleared ${deleted} exec session${deleted === 1 ? "" : "s"}
|
|
46104
|
+
`);
|
|
46165
46105
|
}
|
|
46166
|
-
|
|
46167
|
-
|
|
46168
|
-
|
|
46106
|
+
getShortId(sessionId) {
|
|
46107
|
+
const parts = sessionId.split("-");
|
|
46108
|
+
if (parts.length >= 3) {
|
|
46109
|
+
return parts.slice(-1)[0].slice(0, 8);
|
|
46110
|
+
}
|
|
46111
|
+
return sessionId.slice(0, 8);
|
|
46112
|
+
}
|
|
46113
|
+
}
|
|
46114
|
+
function showSessionsHelp() {
|
|
46169
46115
|
console.log(`
|
|
46170
|
-
${c.
|
|
46171
|
-
|
|
46116
|
+
${c.primary("Session Commands")}
|
|
46117
|
+
|
|
46118
|
+
${c.success("list")} List recent exec sessions
|
|
46119
|
+
${c.success("show")} ${c.dim("<id>")} Show all messages in a session
|
|
46120
|
+
${c.success("delete")} ${c.dim("<id>")} Delete a specific session
|
|
46121
|
+
${c.success("clear")} Clear all exec sessions
|
|
46122
|
+
|
|
46123
|
+
${c.header(" EXAMPLES ")}
|
|
46124
|
+
${c.dim("$")} locus exec sessions list
|
|
46125
|
+
${c.dim("$")} locus exec sessions show e7f3a2b1
|
|
46126
|
+
${c.dim("$")} locus exec sessions delete e7f3a2b1
|
|
46127
|
+
${c.dim("$")} locus exec sessions clear
|
|
46128
|
+
|
|
46129
|
+
${c.dim("Session IDs can be partial (first 8 characters).")}
|
|
46172
46130
|
`);
|
|
46173
46131
|
}
|
|
46174
|
-
|
|
46175
|
-
|
|
46176
|
-
|
|
46177
|
-
|
|
46178
|
-
init_utils3();
|
|
46179
|
-
init_workspace_resolver();
|
|
46180
|
-
import { parseArgs as parseArgs8 } from "node:util";
|
|
46181
|
-
async function runCommand(args) {
|
|
46182
|
-
const { values } = parseArgs8({
|
|
46132
|
+
|
|
46133
|
+
// src/commands/exec.ts
|
|
46134
|
+
async function execCommand(args) {
|
|
46135
|
+
const { values, positionals } = parseArgs5({
|
|
46183
46136
|
args,
|
|
46184
46137
|
options: {
|
|
46185
|
-
"api-key": { type: "string" },
|
|
46186
|
-
workspace: { type: "string" },
|
|
46187
|
-
sprint: { type: "string" },
|
|
46188
46138
|
model: { type: "string" },
|
|
46189
46139
|
provider: { type: "string" },
|
|
46190
46140
|
"reasoning-effort": { type: "string" },
|
|
46191
|
-
|
|
46192
|
-
"
|
|
46193
|
-
|
|
46141
|
+
dir: { type: "string" },
|
|
46142
|
+
"no-stream": { type: "boolean" },
|
|
46143
|
+
"no-status": { type: "boolean" },
|
|
46144
|
+
interactive: { type: "boolean", short: "i" },
|
|
46145
|
+
session: { type: "string", short: "s" },
|
|
46146
|
+
"session-id": { type: "string" },
|
|
46147
|
+
"json-stream": { type: "boolean" }
|
|
46194
46148
|
},
|
|
46195
46149
|
strict: false
|
|
46196
46150
|
});
|
|
46151
|
+
const jsonStream = values["json-stream"];
|
|
46197
46152
|
const projectPath = values.dir || process.cwd();
|
|
46198
|
-
|
|
46199
|
-
|
|
46200
|
-
|
|
46201
|
-
const settingsManager = new SettingsManager(projectPath);
|
|
46202
|
-
const settings = settingsManager.load();
|
|
46203
|
-
const apiKey = values["api-key"] || settings.apiKey;
|
|
46204
|
-
if (!apiKey) {
|
|
46205
|
-
console.error(c.error("Error: API key is required"));
|
|
46206
|
-
console.error(c.dim(`Configure with: locus config setup --api-key <key>
|
|
46207
|
-
Or pass --api-key flag`));
|
|
46208
|
-
process.exit(1);
|
|
46153
|
+
if (jsonStream) {
|
|
46154
|
+
await execJsonStream(values, positionals, projectPath);
|
|
46155
|
+
return;
|
|
46209
46156
|
}
|
|
46210
|
-
|
|
46211
|
-
|
|
46212
|
-
|
|
46213
|
-
|
|
46214
|
-
|
|
46215
|
-
|
|
46216
|
-
|
|
46217
|
-
|
|
46218
|
-
|
|
46157
|
+
requireInitialization(projectPath, "exec");
|
|
46158
|
+
if (positionals[0] === "sessions") {
|
|
46159
|
+
const sessionAction = positionals[1];
|
|
46160
|
+
const sessionArg = positionals[2];
|
|
46161
|
+
const cmds = new SessionCommands(projectPath);
|
|
46162
|
+
switch (sessionAction) {
|
|
46163
|
+
case "list":
|
|
46164
|
+
await cmds.list();
|
|
46165
|
+
return;
|
|
46166
|
+
case "show":
|
|
46167
|
+
await cmds.show(sessionArg);
|
|
46168
|
+
return;
|
|
46169
|
+
case "delete":
|
|
46170
|
+
await cmds.delete(sessionArg);
|
|
46171
|
+
return;
|
|
46172
|
+
case "clear":
|
|
46173
|
+
await cmds.clear();
|
|
46174
|
+
return;
|
|
46175
|
+
default:
|
|
46176
|
+
showSessionsHelp();
|
|
46177
|
+
return;
|
|
46178
|
+
}
|
|
46179
|
+
}
|
|
46180
|
+
const execSettings = new SettingsManager(projectPath).load();
|
|
46181
|
+
const provider = resolveProvider3(values.provider || execSettings.provider);
|
|
46182
|
+
const model = values.model || execSettings.model || DEFAULT_MODEL[provider];
|
|
46183
|
+
const isInteractive = values.interactive;
|
|
46184
|
+
const sessionId = values.session;
|
|
46185
|
+
if (isInteractive) {
|
|
46186
|
+
const { InteractiveSession: InteractiveSession2 } = await Promise.resolve().then(() => (init_interactive_session(), exports_interactive_session));
|
|
46187
|
+
const session2 = new InteractiveSession2({
|
|
46188
|
+
projectPath,
|
|
46189
|
+
provider,
|
|
46190
|
+
model,
|
|
46191
|
+
sessionId
|
|
46219
46192
|
});
|
|
46220
|
-
|
|
46221
|
-
|
|
46222
|
-
|
|
46193
|
+
await session2.start();
|
|
46194
|
+
return;
|
|
46195
|
+
}
|
|
46196
|
+
const promptInput = positionals.join(" ");
|
|
46197
|
+
if (!promptInput) {
|
|
46198
|
+
console.error(c.error('Error: Prompt is required. Usage: locus exec "your prompt" or locus exec --interactive'));
|
|
46223
46199
|
process.exit(1);
|
|
46224
46200
|
}
|
|
46201
|
+
const useStreaming = !values["no-stream"];
|
|
46225
46202
|
const reasoningEffort = values["reasoning-effort"];
|
|
46226
|
-
const
|
|
46227
|
-
workspaceId,
|
|
46228
|
-
sprintId: values.sprint || "",
|
|
46229
|
-
model,
|
|
46230
|
-
provider,
|
|
46231
|
-
reasoningEffort,
|
|
46232
|
-
apiBase,
|
|
46233
|
-
maxIterations: 100,
|
|
46203
|
+
const aiRunner = createAiRunner(provider, {
|
|
46234
46204
|
projectPath,
|
|
46235
|
-
|
|
46205
|
+
model,
|
|
46206
|
+
reasoningEffort
|
|
46236
46207
|
});
|
|
46237
|
-
|
|
46238
|
-
|
|
46239
|
-
|
|
46240
|
-
|
|
46241
|
-
|
|
46242
|
-
let
|
|
46243
|
-
|
|
46244
|
-
if (
|
|
46245
|
-
|
|
46246
|
-
|
|
46247
|
-
|
|
46248
|
-
|
|
46249
|
-
|
|
46250
|
-
|
|
46251
|
-
|
|
46252
|
-
|
|
46253
|
-
|
|
46254
|
-
|
|
46255
|
-
|
|
46256
|
-
|
|
46257
|
-
|
|
46258
|
-
|
|
46259
|
-
|
|
46260
|
-
|
|
46261
|
-
|
|
46262
|
-
|
|
46263
|
-
|
|
46264
|
-
|
|
46265
|
-
|
|
46266
|
-
|
|
46267
|
-
|
|
46268
|
-
|
|
46269
|
-
|
|
46270
|
-
|
|
46271
|
-
|
|
46272
|
-
|
|
46273
|
-
|
|
46274
|
-
|
|
46275
|
-
|
|
46276
|
-
|
|
46277
|
-
|
|
46278
|
-
|
|
46279
|
-
|
|
46208
|
+
const builder = new PromptBuilder(projectPath);
|
|
46209
|
+
const fullPrompt = await builder.buildGenericPrompt(promptInput);
|
|
46210
|
+
console.log("");
|
|
46211
|
+
console.log(`${c.primary("\uD83D\uDE80")} ${c.bold("Executing prompt with repository context...")}`);
|
|
46212
|
+
console.log("");
|
|
46213
|
+
let responseContent = "";
|
|
46214
|
+
try {
|
|
46215
|
+
if (useStreaming) {
|
|
46216
|
+
const renderer = new ProgressRenderer;
|
|
46217
|
+
const statsTracker = new ExecutionStatsTracker;
|
|
46218
|
+
const stream4 = aiRunner.runStream(fullPrompt);
|
|
46219
|
+
renderer.showThinkingStarted();
|
|
46220
|
+
for await (const chunk of stream4) {
|
|
46221
|
+
switch (chunk.type) {
|
|
46222
|
+
case "text_delta":
|
|
46223
|
+
renderer.renderTextDelta(chunk.content);
|
|
46224
|
+
responseContent += chunk.content;
|
|
46225
|
+
break;
|
|
46226
|
+
case "tool_use":
|
|
46227
|
+
statsTracker.toolStarted(chunk.tool, chunk.id);
|
|
46228
|
+
renderer.showToolStarted(chunk.tool, chunk.id);
|
|
46229
|
+
break;
|
|
46230
|
+
case "thinking":
|
|
46231
|
+
renderer.showThinkingStarted();
|
|
46232
|
+
break;
|
|
46233
|
+
case "tool_result":
|
|
46234
|
+
if (chunk.success) {
|
|
46235
|
+
statsTracker.toolCompleted(chunk.tool, chunk.id);
|
|
46236
|
+
renderer.showToolCompleted(chunk.tool, undefined, chunk.id);
|
|
46237
|
+
} else {
|
|
46238
|
+
statsTracker.toolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
|
|
46239
|
+
renderer.showToolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
|
|
46240
|
+
}
|
|
46241
|
+
break;
|
|
46242
|
+
case "result":
|
|
46243
|
+
break;
|
|
46244
|
+
case "error": {
|
|
46245
|
+
statsTracker.setError(chunk.error);
|
|
46246
|
+
renderer.renderError(chunk.error);
|
|
46247
|
+
renderer.finalize();
|
|
46248
|
+
const errorStats = statsTracker.finalize();
|
|
46249
|
+
renderer.showSummary(errorStats);
|
|
46250
|
+
console.error(`
|
|
46251
|
+
${c.error("✖")} ${c.error("Execution failed!")}
|
|
46252
|
+
`);
|
|
46253
|
+
process.exit(1);
|
|
46254
|
+
}
|
|
46255
|
+
}
|
|
46256
|
+
}
|
|
46257
|
+
renderer.finalize();
|
|
46258
|
+
const stats = statsTracker.finalize();
|
|
46259
|
+
renderer.showSummary(stats);
|
|
46260
|
+
} else {
|
|
46261
|
+
responseContent = await aiRunner.run(fullPrompt);
|
|
46262
|
+
console.log(responseContent);
|
|
46263
|
+
}
|
|
46264
|
+
console.log(`
|
|
46265
|
+
${c.success("✔")} ${c.success("Execution finished!")}
|
|
46266
|
+
`);
|
|
46267
|
+
} catch (error48) {
|
|
46268
|
+
console.error(`
|
|
46269
|
+
${c.error("✖")} ${c.error("Execution failed:")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
|
|
46270
|
+
`);
|
|
46271
|
+
process.exit(1);
|
|
46272
|
+
}
|
|
46280
46273
|
}
|
|
46281
|
-
function
|
|
46282
|
-
const
|
|
46283
|
-
const
|
|
46284
|
-
|
|
46285
|
-
|
|
46286
|
-
|
|
46274
|
+
async function execJsonStream(values, positionals, projectPath) {
|
|
46275
|
+
const sessionId = values["session-id"] ?? values.session ?? randomUUID2();
|
|
46276
|
+
const execSettings = new SettingsManager(projectPath).load();
|
|
46277
|
+
const provider = resolveProvider3(values.provider || execSettings.provider);
|
|
46278
|
+
const model = values.model || execSettings.model || DEFAULT_MODEL[provider];
|
|
46279
|
+
const renderer = new JsonStreamRenderer({
|
|
46280
|
+
sessionId,
|
|
46281
|
+
command: "exec",
|
|
46282
|
+
model,
|
|
46283
|
+
provider,
|
|
46284
|
+
cwd: projectPath
|
|
46285
|
+
});
|
|
46286
|
+
const handleSignal = () => {
|
|
46287
|
+
if (renderer.isDone()) {
|
|
46288
|
+
return;
|
|
46289
|
+
}
|
|
46290
|
+
renderer.emitFatalError("PROCESS_CRASHED", "Process terminated by signal");
|
|
46291
|
+
if (process.stdout.writableNeedDrain) {
|
|
46292
|
+
process.stdout.once("drain", () => process.exit(1));
|
|
46293
|
+
} else {
|
|
46294
|
+
process.exit(1);
|
|
46295
|
+
}
|
|
46296
|
+
};
|
|
46297
|
+
process.on("SIGINT", handleSignal);
|
|
46298
|
+
process.on("SIGTERM", handleSignal);
|
|
46287
46299
|
try {
|
|
46288
|
-
versions2 = readdirSync6(versionsDir).filter((d) => d.startsWith("v"));
|
|
46289
|
-
} catch {
|
|
46290
|
-
return null;
|
|
46291
|
-
}
|
|
46292
|
-
if (versions2.length === 0)
|
|
46293
|
-
return null;
|
|
46294
|
-
const currentNodeVersion = `v${process.versions.node}`;
|
|
46295
|
-
const currentBin = join20(versionsDir, currentNodeVersion, "bin");
|
|
46296
|
-
if (versions2.includes(currentNodeVersion) && existsSync20(currentBin)) {
|
|
46297
|
-
return currentBin;
|
|
46298
|
-
}
|
|
46299
|
-
const aliasPath = join20(nvmDir, "alias", "default");
|
|
46300
|
-
if (existsSync20(aliasPath)) {
|
|
46301
46300
|
try {
|
|
46302
|
-
|
|
46303
|
-
|
|
46304
|
-
|
|
46305
|
-
|
|
46306
|
-
if (existsSync20(bin2))
|
|
46307
|
-
return bin2;
|
|
46308
|
-
}
|
|
46309
|
-
} catch {}
|
|
46310
|
-
}
|
|
46311
|
-
const sorted = versions2.sort((a, b) => {
|
|
46312
|
-
const pa = a.slice(1).split(".").map(Number);
|
|
46313
|
-
const pb = b.slice(1).split(".").map(Number);
|
|
46314
|
-
for (let i = 0;i < 3; i++) {
|
|
46315
|
-
if ((pa[i] || 0) !== (pb[i] || 0))
|
|
46316
|
-
return (pb[i] || 0) - (pa[i] || 0);
|
|
46301
|
+
requireInitialization(projectPath, "exec");
|
|
46302
|
+
} catch (initError) {
|
|
46303
|
+
renderer.emitFatalError("CLI_NOT_FOUND", initError instanceof Error ? initError.message : String(initError));
|
|
46304
|
+
process.exit(1);
|
|
46317
46305
|
}
|
|
46318
|
-
|
|
46319
|
-
|
|
46320
|
-
|
|
46321
|
-
|
|
46322
|
-
}
|
|
46323
|
-
|
|
46324
|
-
|
|
46325
|
-
|
|
46326
|
-
|
|
46327
|
-
|
|
46328
|
-
|
|
46329
|
-
|
|
46330
|
-
|
|
46331
|
-
|
|
46332
|
-
|
|
46333
|
-
|
|
46334
|
-
|
|
46335
|
-
|
|
46336
|
-
|
|
46337
|
-
|
|
46338
|
-
|
|
46339
|
-
}
|
|
46340
|
-
|
|
46341
|
-
|
|
46342
|
-
|
|
46343
|
-
|
|
46344
|
-
|
|
46345
|
-
dirs.add(fnmCurrent);
|
|
46346
|
-
for (const bin of ["claude", "codex"]) {
|
|
46347
|
-
const dir = await findBinDir(bin);
|
|
46348
|
-
if (dir)
|
|
46349
|
-
dirs.add(dir);
|
|
46306
|
+
const promptInput = positionals.join(" ");
|
|
46307
|
+
if (!promptInput) {
|
|
46308
|
+
renderer.emitFatalError("UNKNOWN", 'Prompt is required. Usage: locus exec --json-stream "your prompt"');
|
|
46309
|
+
process.exit(1);
|
|
46310
|
+
}
|
|
46311
|
+
renderer.emitStart();
|
|
46312
|
+
renderer.emitStatus("running", "Building prompt context");
|
|
46313
|
+
const aiRunner = createAiRunner(provider, {
|
|
46314
|
+
projectPath,
|
|
46315
|
+
model
|
|
46316
|
+
});
|
|
46317
|
+
const builder = new PromptBuilder(projectPath);
|
|
46318
|
+
const fullPrompt = await builder.buildGenericPrompt(promptInput);
|
|
46319
|
+
renderer.emitStatus("streaming", "Streaming AI response");
|
|
46320
|
+
const stream4 = aiRunner.runStream(fullPrompt);
|
|
46321
|
+
for await (const chunk of stream4) {
|
|
46322
|
+
renderer.handleChunk(chunk);
|
|
46323
|
+
}
|
|
46324
|
+
renderer.emitDone(0);
|
|
46325
|
+
process.removeListener("SIGINT", handleSignal);
|
|
46326
|
+
process.removeListener("SIGTERM", handleSignal);
|
|
46327
|
+
} catch (error48) {
|
|
46328
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
46329
|
+
if (!renderer.isDone()) {
|
|
46330
|
+
renderer.emitFatalError("UNKNOWN", message);
|
|
46331
|
+
}
|
|
46332
|
+
process.exit(1);
|
|
46350
46333
|
}
|
|
46351
|
-
return Array.from(dirs).join(":");
|
|
46352
|
-
}
|
|
46353
|
-
var SERVICE_NAME = "locus";
|
|
46354
|
-
var SYSTEMD_UNIT_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
46355
|
-
var PLIST_LABEL = "com.locus.agent";
|
|
46356
|
-
function getPlistPath() {
|
|
46357
|
-
return join20(homedir3(), "Library/LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
46358
46334
|
}
|
|
46359
|
-
|
|
46335
|
+
// src/commands/help.ts
|
|
46336
|
+
init_index_node();
|
|
46337
|
+
function showHelp2() {
|
|
46360
46338
|
console.log(`
|
|
46361
|
-
${c.header("
|
|
46362
|
-
${c.primary("locus
|
|
46339
|
+
${c.header(" USAGE ")}
|
|
46340
|
+
${c.primary("locus")} ${c.dim("<command> [options]")}
|
|
46363
46341
|
|
|
46364
|
-
${c.header("
|
|
46365
|
-
${c.success("
|
|
46366
|
-
|
|
46367
|
-
|
|
46368
|
-
|
|
46369
|
-
|
|
46342
|
+
${c.header(" COMMANDS ")}
|
|
46343
|
+
${c.success("init")} Initialize Locus in the current directory
|
|
46344
|
+
${c.success("config")} Manage settings (API key, provider, model)
|
|
46345
|
+
${c.dim("setup Interactive one-time setup")}
|
|
46346
|
+
${c.dim("show Show current settings")}
|
|
46347
|
+
${c.dim("set <k> <v> Update a setting")}
|
|
46348
|
+
${c.dim("remove Remove all settings")}
|
|
46349
|
+
${c.success("index")} Index the codebase for AI context
|
|
46350
|
+
${c.success("run")} Start agent to work on tasks sequentially
|
|
46351
|
+
${c.success("discuss")} Start an interactive AI discussion on a topic
|
|
46352
|
+
${c.dim("--list List all discussions")}
|
|
46353
|
+
${c.dim("--show <id> Show discussion details")}
|
|
46354
|
+
${c.dim("--archive <id> Archive a discussion")}
|
|
46355
|
+
${c.dim("--delete <id> Delete a discussion")}
|
|
46356
|
+
${c.success("plan")} Run async planning meeting to create sprint plans
|
|
46357
|
+
${c.success("docs")} Manage workspace docs
|
|
46358
|
+
${c.dim("sync Sync docs from API to .locus/documents")}
|
|
46359
|
+
${c.success("review")} Review open Locus PRs on GitHub with AI
|
|
46360
|
+
${c.dim("local Review staged changes locally (no GitHub)")}
|
|
46361
|
+
${c.success("telegram")} Manage the Telegram bot
|
|
46362
|
+
${c.dim("start Start the Telegram bot")}
|
|
46363
|
+
${c.dim("setup Interactive bot token and chat ID setup")}
|
|
46364
|
+
${c.dim("config Show current configuration")}
|
|
46365
|
+
${c.dim("set <k> <v> Update a config value")}
|
|
46366
|
+
${c.dim("remove Remove Telegram configuration")}
|
|
46367
|
+
${c.success("daemon")} Manage the Locus background service
|
|
46368
|
+
${c.dim("start Install and start the daemon")}
|
|
46369
|
+
${c.dim("stop Stop and remove the daemon")}
|
|
46370
|
+
${c.dim("restart Restart the daemon")}
|
|
46371
|
+
${c.dim("status Check if the daemon is running")}
|
|
46372
|
+
${c.success("exec")} Run a prompt with repository context
|
|
46373
|
+
${c.dim("--interactive, -i Start interactive REPL mode")}
|
|
46374
|
+
${c.dim("--session, -s <id> Resume a previous session")}
|
|
46375
|
+
${c.dim("sessions list List recent sessions")}
|
|
46376
|
+
${c.dim("sessions show <id> Show session messages")}
|
|
46377
|
+
${c.dim("sessions delete <id> Delete a session")}
|
|
46378
|
+
${c.dim("sessions clear Clear all sessions")}
|
|
46379
|
+
${c.success("artifacts")} List and manage knowledge artifacts
|
|
46380
|
+
${c.dim("show <name> Show artifact content")}
|
|
46381
|
+
${c.dim("plan <name> Convert artifact to a plan")}
|
|
46382
|
+
${c.success("version")} Show installed package versions
|
|
46383
|
+
${c.success("upgrade")} Update CLI and Telegram to the latest version
|
|
46384
|
+
|
|
46385
|
+
${c.header(" OPTIONS ")}
|
|
46386
|
+
${c.secondary("--help")} Show this help message
|
|
46387
|
+
${c.secondary("--provider")} <name> AI provider: ${c.dim("claude")} or ${c.dim("codex")} (default: ${c.dim("claude")})
|
|
46388
|
+
${c.secondary("--model")} <name> AI model (claude: ${c.dim("opus, sonnet, haiku")} | codex: ${c.dim("gpt-5.3-codex, gpt-5-codex-mini")})
|
|
46389
|
+
${c.secondary("--reasoning-effort")} <level> Codex reasoning effort: ${c.dim("low, medium, high")} (default: model default)
|
|
46390
|
+
|
|
46391
|
+
${c.header(" GETTING STARTED ")}
|
|
46392
|
+
${c.dim("$")} ${c.primary("locus init")}
|
|
46393
|
+
${c.dim("$")} ${c.primary("locus config setup")}
|
|
46394
|
+
${c.dim("$")} ${c.primary("locus telegram setup")}
|
|
46395
|
+
${c.dim("$")} ${c.primary("locus daemon start")}
|
|
46370
46396
|
|
|
46371
46397
|
${c.header(" EXAMPLES ")}
|
|
46372
|
-
${c.dim("$")} ${c.primary("locus
|
|
46373
|
-
${c.dim("$")} ${c.primary("locus
|
|
46374
|
-
${c.dim("$")} ${c.primary("locus
|
|
46398
|
+
${c.dim("$")} ${c.primary("locus run")}
|
|
46399
|
+
${c.dim("$")} ${c.primary("locus docs sync")}
|
|
46400
|
+
${c.dim("$")} ${c.primary("locus review")}
|
|
46401
|
+
${c.dim("$")} ${c.primary("locus review local")}
|
|
46402
|
+
${c.dim("$")} ${c.primary("locus telegram start")}
|
|
46403
|
+
${c.dim("$")} ${c.primary('locus discuss "how should we design the auth system?"')}
|
|
46404
|
+
${c.dim("$")} ${c.primary("locus exec sessions list")}
|
|
46405
|
+
${c.dim("$")} ${c.primary("locus artifacts")}
|
|
46406
|
+
${c.dim("$")} ${c.primary("locus daemon start")}
|
|
46407
|
+
|
|
46408
|
+
For more information, visit: ${c.underline("https://docs.locusai.dev")}
|
|
46375
46409
|
`);
|
|
46376
46410
|
}
|
|
46377
|
-
|
|
46378
|
-
|
|
46379
|
-
|
|
46380
|
-
|
|
46381
|
-
let stderr = "";
|
|
46382
|
-
proc.stdout?.on("data", (d) => {
|
|
46383
|
-
stdout += d.toString();
|
|
46384
|
-
});
|
|
46385
|
-
proc.stderr?.on("data", (d) => {
|
|
46386
|
-
stderr += d.toString();
|
|
46387
|
-
});
|
|
46388
|
-
proc.on("close", (exitCode) => resolve2({ exitCode, stdout, stderr }));
|
|
46389
|
-
proc.on("error", (err) => resolve2({ exitCode: 1, stdout, stderr: err.message }));
|
|
46390
|
-
});
|
|
46391
|
-
}
|
|
46392
|
-
async function killOrphanedProcesses() {
|
|
46393
|
-
const result = await runShell("pgrep", ["-f", "locus-telegram"]);
|
|
46394
|
-
const pids = result.stdout.trim().split(`
|
|
46395
|
-
`).filter((p) => p.length > 0);
|
|
46396
|
-
if (pids.length === 0)
|
|
46397
|
-
return;
|
|
46398
|
-
console.log(` ${c.info("▶")} Killing ${pids.length} orphaned locus-telegram process${pids.length > 1 ? "es" : ""}...`);
|
|
46399
|
-
await runShell("pkill", ["-f", "locus-telegram"]);
|
|
46400
|
-
await new Promise((resolve2) => setTimeout(resolve2, 2000));
|
|
46401
|
-
const check2 = await runShell("pgrep", ["-f", "locus-telegram"]);
|
|
46402
|
-
if (check2.stdout.trim().length > 0) {
|
|
46403
|
-
await runShell("pkill", ["-9", "-f", "locus-telegram"]);
|
|
46404
|
-
}
|
|
46405
|
-
}
|
|
46406
|
-
function generateSystemdUnit(projectPath, user2, binaryPath, servicePath) {
|
|
46407
|
-
return `[Unit]
|
|
46408
|
-
Description=Locus AI Agent (Telegram bot + proposal scheduler)
|
|
46409
|
-
After=network-online.target
|
|
46410
|
-
Wants=network-online.target
|
|
46411
|
+
// src/commands/index-codebase.ts
|
|
46412
|
+
init_index_node();
|
|
46413
|
+
init_config_manager();
|
|
46414
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
46411
46415
|
|
|
46412
|
-
|
|
46413
|
-
|
|
46414
|
-
User=${user2}
|
|
46415
|
-
WorkingDirectory=${projectPath}
|
|
46416
|
-
ExecStart=${binaryPath}
|
|
46417
|
-
Restart=on-failure
|
|
46418
|
-
RestartSec=10
|
|
46419
|
-
Environment=PATH=${servicePath}
|
|
46420
|
-
Environment=HOME=${homedir3()}
|
|
46416
|
+
// src/tree-summarizer.ts
|
|
46417
|
+
init_index_node();
|
|
46421
46418
|
|
|
46422
|
-
|
|
46423
|
-
|
|
46424
|
-
|
|
46425
|
-
|
|
46426
|
-
async function installSystemd(projectPath) {
|
|
46427
|
-
const user2 = process.env.USER || "root";
|
|
46428
|
-
const binaryPath = await findBinary();
|
|
46429
|
-
if (!binaryPath) {
|
|
46430
|
-
console.error(`
|
|
46431
|
-
${c.error("✖")} ${c.bold("Could not find locus-telegram binary.")}
|
|
46432
|
-
` + ` Install with: ${c.primary("npm install -g @locusai/telegram")}
|
|
46433
|
-
`);
|
|
46434
|
-
process.exit(1);
|
|
46435
|
-
}
|
|
46436
|
-
if (!await findBinDir("claude")) {
|
|
46437
|
-
console.warn(`
|
|
46438
|
-
${c.secondary("⚠")} ${c.bold("Could not find 'claude' CLI in PATH.")}
|
|
46439
|
-
` + ` The service needs the Claude Code CLI to execute tasks.
|
|
46440
|
-
` + ` Install with: ${c.primary("npm install -g @anthropic-ai/claude-code")}
|
|
46441
|
-
`);
|
|
46419
|
+
class TreeSummarizer {
|
|
46420
|
+
aiRunner;
|
|
46421
|
+
constructor(aiRunner) {
|
|
46422
|
+
this.aiRunner = aiRunner;
|
|
46442
46423
|
}
|
|
46443
|
-
|
|
46444
|
-
|
|
46445
|
-
|
|
46446
|
-
|
|
46447
|
-
|
|
46448
|
-
|
|
46424
|
+
async summarize(tree) {
|
|
46425
|
+
const prompt = `Analyze this file tree and generate a JSON index.
|
|
46426
|
+
Return ONLY a JSON object with this structure:
|
|
46427
|
+
{
|
|
46428
|
+
"symbols": {},
|
|
46429
|
+
"responsibilities": { "path": "Description" }
|
|
46430
|
+
}
|
|
46431
|
+
|
|
46432
|
+
File Tree:
|
|
46433
|
+
${tree}`;
|
|
46434
|
+
const output = await this.aiRunner.run(prompt);
|
|
46435
|
+
const jsonStr = extractJsonFromLLMOutput(output);
|
|
46436
|
+
return JSON.parse(jsonStr);
|
|
46449
46437
|
}
|
|
46450
|
-
|
|
46451
|
-
|
|
46438
|
+
}
|
|
46439
|
+
|
|
46440
|
+
// src/commands/index-codebase.ts
|
|
46441
|
+
init_utils3();
|
|
46442
|
+
async function indexCommand(args) {
|
|
46443
|
+
const { values } = parseArgs6({
|
|
46444
|
+
args,
|
|
46445
|
+
options: {
|
|
46446
|
+
dir: { type: "string" },
|
|
46447
|
+
model: { type: "string" },
|
|
46448
|
+
provider: { type: "string" }
|
|
46449
|
+
},
|
|
46450
|
+
strict: false
|
|
46451
|
+
});
|
|
46452
|
+
const projectPath = values.dir || process.cwd();
|
|
46453
|
+
requireInitialization(projectPath, "index");
|
|
46454
|
+
new ConfigManager(projectPath).updateVersion(VERSION2);
|
|
46455
|
+
const provider = resolveProvider3(values.provider);
|
|
46456
|
+
const model = values.model || DEFAULT_MODEL[provider];
|
|
46457
|
+
const aiRunner = createAiRunner(provider, {
|
|
46458
|
+
projectPath,
|
|
46459
|
+
model
|
|
46460
|
+
});
|
|
46461
|
+
const summarizer = new TreeSummarizer(aiRunner);
|
|
46462
|
+
const indexer = new CodebaseIndexer(projectPath);
|
|
46452
46463
|
console.log(`
|
|
46453
|
-
${c.
|
|
46454
|
-
|
|
46455
|
-
|
|
46456
|
-
|
|
46457
|
-
|
|
46458
|
-
|
|
46459
|
-
|
|
46460
|
-
if (startResult.exitCode !== 0) {
|
|
46464
|
+
${c.step(" INDEX ")} ${c.primary("Analyzing codebase in")} ${c.bold(projectPath)}...`);
|
|
46465
|
+
try {
|
|
46466
|
+
const index = await indexer.index((msg) => console.log(` ${c.dim(msg)}`), (tree) => summarizer.summarize(tree));
|
|
46467
|
+
if (index) {
|
|
46468
|
+
indexer.saveIndex(index);
|
|
46469
|
+
}
|
|
46470
|
+
} catch (error48) {
|
|
46461
46471
|
console.error(`
|
|
46462
|
-
${c.error("✖")}
|
|
46463
|
-
console.error(
|
|
46464
|
-
|
|
46465
|
-
}
|
|
46466
|
-
console.log(`
|
|
46467
|
-
${c.success("✔")} ${c.bold("Locus service installed and running!")}
|
|
46468
|
-
|
|
46469
|
-
${c.bold("Service:")} ${SERVICE_NAME}
|
|
46470
|
-
${c.bold("Unit file:")} ${SYSTEMD_UNIT_PATH}
|
|
46471
|
-
|
|
46472
|
-
${c.bold("Useful commands:")}
|
|
46473
|
-
${c.dim("$")} ${c.primary(`sudo systemctl status ${SERVICE_NAME}`)}
|
|
46474
|
-
${c.dim("$")} ${c.primary(`sudo systemctl restart ${SERVICE_NAME}`)}
|
|
46475
|
-
${c.dim("$")} ${c.primary(`journalctl -u ${SERVICE_NAME} -f`)}
|
|
46476
|
-
`);
|
|
46477
|
-
}
|
|
46478
|
-
async function uninstallSystemd() {
|
|
46479
|
-
if (!existsSync20(SYSTEMD_UNIT_PATH)) {
|
|
46480
|
-
console.log(`
|
|
46481
|
-
${c.dim("No systemd service found. Nothing to remove.")}
|
|
46482
|
-
`);
|
|
46483
|
-
await killOrphanedProcesses();
|
|
46484
|
-
return;
|
|
46472
|
+
${c.error("✖")} ${c.error("Indexing failed:")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}`);
|
|
46473
|
+
console.error(c.dim(` The agent might have limited context until indexing succeeds.
|
|
46474
|
+
`));
|
|
46485
46475
|
}
|
|
46486
|
-
console.log(` ${c.info("▶")} Stopping and disabling ${SERVICE_NAME}...`);
|
|
46487
|
-
await runShell("systemctl", ["stop", SERVICE_NAME]);
|
|
46488
|
-
await runShell("systemctl", ["disable", SERVICE_NAME]);
|
|
46489
|
-
const { unlinkSync: unlinkSync6 } = await import("node:fs");
|
|
46490
|
-
unlinkSync6(SYSTEMD_UNIT_PATH);
|
|
46491
|
-
await runShell("systemctl", ["daemon-reload"]);
|
|
46492
|
-
await killOrphanedProcesses();
|
|
46493
46476
|
console.log(`
|
|
46494
|
-
${c.success("✔")} ${c.
|
|
46477
|
+
${c.success("✔")} ${c.success("Indexing complete!")}
|
|
46495
46478
|
`);
|
|
46496
46479
|
}
|
|
46497
|
-
|
|
46498
|
-
|
|
46499
|
-
|
|
46500
|
-
|
|
46501
|
-
|
|
46502
|
-
|
|
46503
|
-
|
|
46504
|
-
|
|
46505
|
-
console.log(`
|
|
46506
|
-
${c.secondary("●")} ${c.bold(`Locus service is ${state}`)} ${c.dim("(systemd)")}
|
|
46507
|
-
`);
|
|
46508
|
-
console.log(` ${c.dim("Start with:")} ${c.primary(`sudo systemctl start ${SERVICE_NAME}`)}
|
|
46509
|
-
`);
|
|
46510
|
-
} else {
|
|
46480
|
+
// src/commands/init.ts
|
|
46481
|
+
init_index_node();
|
|
46482
|
+
init_config_manager();
|
|
46483
|
+
init_utils3();
|
|
46484
|
+
async function initCommand() {
|
|
46485
|
+
const projectPath = process.cwd();
|
|
46486
|
+
const configManager = new ConfigManager(projectPath);
|
|
46487
|
+
if (isProjectInitialized(projectPath)) {
|
|
46511
46488
|
console.log(`
|
|
46512
|
-
${c.
|
|
46513
|
-
`);
|
|
46514
|
-
console.log(` ${c.dim("Install with:")} ${c.primary("locus service install")}
|
|
46515
|
-
`);
|
|
46516
|
-
}
|
|
46517
|
-
}
|
|
46518
|
-
function generatePlist(projectPath, binaryPath, binaryArgs, servicePath) {
|
|
46519
|
-
const argsXml = [binaryPath, ...binaryArgs].map((a) => ` <string>${a}</string>`).join(`
|
|
46520
|
-
`);
|
|
46521
|
-
const logDir = join20(homedir3(), "Library/Logs/Locus");
|
|
46522
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
46523
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
46524
|
-
<plist version="1.0">
|
|
46525
|
-
<dict>
|
|
46526
|
-
<key>Label</key>
|
|
46527
|
-
<string>${PLIST_LABEL}</string>
|
|
46528
|
-
<key>ProgramArguments</key>
|
|
46529
|
-
<array>
|
|
46530
|
-
${argsXml}
|
|
46531
|
-
</array>
|
|
46532
|
-
<key>WorkingDirectory</key>
|
|
46533
|
-
<string>${projectPath}</string>
|
|
46534
|
-
<key>RunAtLoad</key>
|
|
46535
|
-
<true/>
|
|
46536
|
-
<key>KeepAlive</key>
|
|
46537
|
-
<true/>
|
|
46538
|
-
<key>StandardOutPath</key>
|
|
46539
|
-
<string>${join20(logDir, "locus.log")}</string>
|
|
46540
|
-
<key>StandardErrorPath</key>
|
|
46541
|
-
<string>${join20(logDir, "locus-error.log")}</string>
|
|
46542
|
-
<key>EnvironmentVariables</key>
|
|
46543
|
-
<dict>
|
|
46544
|
-
<key>PATH</key>
|
|
46545
|
-
<string>${servicePath}</string>
|
|
46546
|
-
</dict>
|
|
46547
|
-
</dict>
|
|
46548
|
-
</plist>
|
|
46549
|
-
`;
|
|
46550
|
-
}
|
|
46551
|
-
async function installLaunchd(projectPath) {
|
|
46552
|
-
const plistPath = getPlistPath();
|
|
46553
|
-
if (existsSync20(plistPath)) {
|
|
46554
|
-
await runShell("launchctl", ["unload", plistPath]);
|
|
46555
|
-
}
|
|
46556
|
-
const binaryPath = await findBinary();
|
|
46557
|
-
if (!binaryPath) {
|
|
46558
|
-
console.error(`
|
|
46559
|
-
${c.error("✖")} ${c.bold("Could not find locus-telegram binary.")}
|
|
46560
|
-
Install with: ${c.primary("npm install -g @locusai/telegram")}
|
|
46489
|
+
${c.info("ℹ️")} ${c.bold("Locus is already initialized. Updating configuration...")}
|
|
46561
46490
|
`);
|
|
46562
|
-
|
|
46563
|
-
|
|
46564
|
-
|
|
46565
|
-
|
|
46566
|
-
|
|
46567
|
-
|
|
46568
|
-
|
|
46569
|
-
|
|
46491
|
+
const result = await configManager.reinit(VERSION2);
|
|
46492
|
+
const updates = [];
|
|
46493
|
+
if (result.versionUpdated) {
|
|
46494
|
+
updates.push(`Version updated: ${c.dim(result.previousVersion || "unknown")} → ${c.primary(VERSION2)}`);
|
|
46495
|
+
}
|
|
46496
|
+
if (result.directoriesCreated.length > 0) {
|
|
46497
|
+
updates.push(`Directories created: ${result.directoriesCreated.map((d) => c.dim(d)).join(", ")}`);
|
|
46498
|
+
}
|
|
46499
|
+
if (result.gitignoreUpdated) {
|
|
46500
|
+
updates.push(`Gitignore updated with Locus patterns`);
|
|
46501
|
+
}
|
|
46502
|
+
if (updates.length === 0) {
|
|
46503
|
+
console.log(` ${c.success("✔")} ${c.success("Configuration is already up to date!")}
|
|
46504
|
+
|
|
46505
|
+
${c.dim(`Version: ${VERSION2}`)}`);
|
|
46506
|
+
} else {
|
|
46507
|
+
console.log(` ${c.success("✔")} ${c.success("Configuration updated successfully!")}
|
|
46508
|
+
|
|
46509
|
+
${c.bold("Changes:")}
|
|
46510
|
+
${updates.map((u) => `${c.primary("•")} ${u}`).join(`
|
|
46511
|
+
`)}
|
|
46570
46512
|
`);
|
|
46571
|
-
|
|
46572
|
-
|
|
46573
|
-
|
|
46574
|
-
|
|
46575
|
-
|
|
46576
|
-
|
|
46513
|
+
}
|
|
46514
|
+
console.log(` ${c.bold("Next steps:")}
|
|
46515
|
+
1. Run '${c.primary("locus config setup")}' to configure your API key
|
|
46516
|
+
2. Run '${c.primary("locus index")}' to index your codebase
|
|
46517
|
+
3. Run '${c.primary("locus run")}' to start an agent
|
|
46518
|
+
|
|
46519
|
+
For more information, visit: ${c.underline("https://docs.locusai.dev")}
|
|
46577
46520
|
`);
|
|
46578
|
-
}
|
|
46579
|
-
const logDir = join20(homedir3(), "Library/Logs/Locus");
|
|
46580
|
-
const { mkdirSync: mkdirSync10 } = await import("node:fs");
|
|
46581
|
-
mkdirSync10(logDir, { recursive: true });
|
|
46582
|
-
const launchAgentsDir = join20(homedir3(), "Library/LaunchAgents");
|
|
46583
|
-
mkdirSync10(launchAgentsDir, { recursive: true });
|
|
46584
|
-
const servicePath = await buildServicePath();
|
|
46585
|
-
const plist = generatePlist(projectPath, binaryPath, binaryArgs, servicePath);
|
|
46586
|
-
console.log(`
|
|
46587
|
-
${c.info("▶")} Writing plist to ${c.dim(plistPath)}`);
|
|
46588
|
-
writeFileSync9(plistPath, plist, "utf-8");
|
|
46589
|
-
console.log(` ${c.info("▶")} Loading service...`);
|
|
46590
|
-
const loadResult = await runShell("launchctl", ["load", plistPath]);
|
|
46591
|
-
if (loadResult.exitCode !== 0) {
|
|
46592
|
-
console.error(`
|
|
46593
|
-
${c.error("✖")} Failed to load service: ${loadResult.stderr.trim()}`);
|
|
46594
46521
|
return;
|
|
46595
46522
|
}
|
|
46596
|
-
|
|
46523
|
+
await configManager.init(VERSION2);
|
|
46597
46524
|
console.log(`
|
|
46598
|
-
${c.success("
|
|
46525
|
+
${c.success("✨ Locus initialized successfully!")}
|
|
46599
46526
|
|
|
46600
|
-
${c.bold("
|
|
46601
|
-
|
|
46527
|
+
${c.bold("Created:")}
|
|
46528
|
+
${c.primary("\uD83D\uDCC1")} ${c.bold(".locus/")} ${c.dim("Configuration directory")}
|
|
46529
|
+
${c.primary("\uD83D\uDCC4")} ${c.bold(".locus/config.json")} ${c.dim("Project settings")}
|
|
46530
|
+
${c.primary("\uD83D\uDCDD")} ${c.bold(".locus/LOCUS.md")} ${c.dim("AI agent instructions")}
|
|
46531
|
+
${c.primary("\uD83D\uDCDD")} ${c.bold(".locus/LEARNINGS.md")} ${c.dim("Continuous learning log")}
|
|
46602
46532
|
|
|
46603
|
-
${c.bold("
|
|
46604
|
-
${c.
|
|
46605
|
-
${c.
|
|
46533
|
+
${c.bold("Next steps:")}
|
|
46534
|
+
1. Run '${c.primary("locus config setup")}' to configure your API key
|
|
46535
|
+
2. Run '${c.primary("locus index")}' to index your codebase
|
|
46536
|
+
3. Run '${c.primary("locus run")}' to start an agent
|
|
46537
|
+
|
|
46538
|
+
For more information, visit: ${c.underline("https://docs.locusai.dev")}
|
|
46606
46539
|
`);
|
|
46607
46540
|
}
|
|
46608
|
-
|
|
46609
|
-
|
|
46610
|
-
|
|
46611
|
-
|
|
46612
|
-
|
|
46613
|
-
|
|
46614
|
-
|
|
46615
|
-
|
|
46541
|
+
|
|
46542
|
+
// src/commands/index.ts
|
|
46543
|
+
init_plan();
|
|
46544
|
+
|
|
46545
|
+
// src/commands/review.ts
|
|
46546
|
+
init_index_node();
|
|
46547
|
+
init_config_manager();
|
|
46548
|
+
init_settings_manager();
|
|
46549
|
+
init_utils3();
|
|
46550
|
+
init_workspace_resolver();
|
|
46551
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "node:fs";
|
|
46552
|
+
import { join as join21 } from "node:path";
|
|
46553
|
+
import { parseArgs as parseArgs7 } from "node:util";
|
|
46554
|
+
async function reviewCommand(args) {
|
|
46555
|
+
const subcommand = args[0];
|
|
46556
|
+
if (subcommand === "local") {
|
|
46557
|
+
return reviewLocalCommand(args.slice(1));
|
|
46616
46558
|
}
|
|
46617
|
-
|
|
46618
|
-
await runShell("launchctl", ["unload", plistPath]);
|
|
46619
|
-
const { unlinkSync: unlinkSync6 } = await import("node:fs");
|
|
46620
|
-
unlinkSync6(plistPath);
|
|
46621
|
-
await killOrphanedProcesses();
|
|
46622
|
-
console.log(`
|
|
46623
|
-
${c.success("✔")} ${c.bold("Locus service removed.")}
|
|
46624
|
-
`);
|
|
46559
|
+
return reviewPrsCommand(args);
|
|
46625
46560
|
}
|
|
46626
|
-
async function
|
|
46627
|
-
const
|
|
46628
|
-
|
|
46561
|
+
async function reviewPrsCommand(args) {
|
|
46562
|
+
const { values } = parseArgs7({
|
|
46563
|
+
args,
|
|
46564
|
+
options: {
|
|
46565
|
+
"api-key": { type: "string" },
|
|
46566
|
+
workspace: { type: "string" },
|
|
46567
|
+
model: { type: "string" },
|
|
46568
|
+
provider: { type: "string" },
|
|
46569
|
+
"api-url": { type: "string" },
|
|
46570
|
+
dir: { type: "string" }
|
|
46571
|
+
},
|
|
46572
|
+
strict: false
|
|
46573
|
+
});
|
|
46574
|
+
const projectPath = values.dir || process.cwd();
|
|
46575
|
+
requireInitialization(projectPath, "review");
|
|
46576
|
+
const configManager = new ConfigManager(projectPath);
|
|
46577
|
+
configManager.updateVersion(VERSION2);
|
|
46578
|
+
const settingsManager = new SettingsManager(projectPath);
|
|
46579
|
+
const settings = settingsManager.load();
|
|
46580
|
+
const apiKey = values["api-key"] || settings.apiKey;
|
|
46581
|
+
if (!apiKey) {
|
|
46582
|
+
console.error(c.error("Error: API key is required for PR review"));
|
|
46583
|
+
console.error(c.dim(`Configure with: locus config setup --api-key <key>
|
|
46584
|
+
Or pass --api-key flag`));
|
|
46585
|
+
console.error(c.dim("For local staged-changes review, use: locus review local"));
|
|
46586
|
+
process.exit(1);
|
|
46587
|
+
}
|
|
46588
|
+
const provider = resolveProvider3(values.provider || settings.provider);
|
|
46589
|
+
const model = values.model || settings.model || DEFAULT_MODEL[provider];
|
|
46590
|
+
const apiBase = values["api-url"] || settings.apiUrl || "https://api.locusai.dev/api";
|
|
46591
|
+
let workspaceId;
|
|
46592
|
+
try {
|
|
46593
|
+
const resolver = new WorkspaceResolver({
|
|
46594
|
+
apiKey,
|
|
46595
|
+
apiBase,
|
|
46596
|
+
workspaceId: values.workspace
|
|
46597
|
+
});
|
|
46598
|
+
workspaceId = await resolver.resolve();
|
|
46599
|
+
} catch (error48) {
|
|
46600
|
+
console.error(c.error(error48 instanceof Error ? error48.message : String(error48)));
|
|
46601
|
+
process.exit(1);
|
|
46602
|
+
}
|
|
46603
|
+
const log = (msg, level = "info") => {
|
|
46604
|
+
const colorFn = {
|
|
46605
|
+
info: c.cyan,
|
|
46606
|
+
success: c.green,
|
|
46607
|
+
warn: c.yellow,
|
|
46608
|
+
error: c.red
|
|
46609
|
+
}[level];
|
|
46610
|
+
const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
|
|
46611
|
+
console.log(` ${colorFn(`${prefix} ${msg}`)}`);
|
|
46612
|
+
};
|
|
46613
|
+
const prService = new PrService(projectPath, log);
|
|
46614
|
+
const unreviewedPrs = prService.listUnreviewedLocusPrs();
|
|
46615
|
+
if (unreviewedPrs.length === 0) {
|
|
46629
46616
|
console.log(`
|
|
46630
|
-
${c.
|
|
46631
|
-
`);
|
|
46632
|
-
console.log(` ${c.dim("Install with:")} ${c.primary("locus service install")}
|
|
46617
|
+
${c.dim("No unreviewed Locus PRs found.")}
|
|
46633
46618
|
`);
|
|
46634
46619
|
return;
|
|
46635
46620
|
}
|
|
46636
|
-
|
|
46637
|
-
|
|
46638
|
-
`);
|
|
46639
|
-
const match = lines.find((l) => l.includes(PLIST_LABEL));
|
|
46640
|
-
if (match) {
|
|
46641
|
-
const parts = match.trim().split(/\s+/);
|
|
46642
|
-
const pid = parts[0] === "-" ? null : parts[0];
|
|
46643
|
-
if (pid) {
|
|
46644
|
-
console.log(`
|
|
46645
|
-
${c.success("●")} ${c.bold("Locus service is running")} ${c.dim(`(PID ${pid}, launchd)`)}
|
|
46646
|
-
`);
|
|
46647
|
-
} else {
|
|
46648
|
-
console.log(`
|
|
46649
|
-
${c.secondary("●")} ${c.bold("Locus service is stopped")} ${c.dim("(launchd)")}
|
|
46650
|
-
`);
|
|
46651
|
-
console.log(` ${c.dim("Start with:")} ${c.primary(`launchctl load ${plistPath}`)}
|
|
46621
|
+
console.log(`
|
|
46622
|
+
${c.primary("\uD83D\uDD0D")} ${c.bold(`Found ${unreviewedPrs.length} unreviewed PR(s). Starting reviewer...`)}
|
|
46652
46623
|
`);
|
|
46653
|
-
|
|
46654
|
-
|
|
46624
|
+
const agentId = `reviewer-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
46625
|
+
const reviewer = new ReviewerWorker({
|
|
46626
|
+
agentId,
|
|
46627
|
+
workspaceId,
|
|
46628
|
+
apiBase,
|
|
46629
|
+
projectPath,
|
|
46630
|
+
apiKey,
|
|
46631
|
+
model,
|
|
46632
|
+
provider
|
|
46633
|
+
});
|
|
46634
|
+
let isShuttingDown = false;
|
|
46635
|
+
const handleSignal = () => {
|
|
46636
|
+
if (isShuttingDown)
|
|
46637
|
+
return;
|
|
46638
|
+
isShuttingDown = true;
|
|
46655
46639
|
console.log(`
|
|
46656
|
-
|
|
46657
|
-
|
|
46658
|
-
|
|
46659
|
-
|
|
46660
|
-
|
|
46661
|
-
|
|
46662
|
-
function getPlatform() {
|
|
46663
|
-
if (process.platform === "linux")
|
|
46664
|
-
return "linux";
|
|
46665
|
-
if (process.platform === "darwin")
|
|
46666
|
-
return "darwin";
|
|
46667
|
-
return null;
|
|
46640
|
+
${c.info("Received shutdown signal. Stopping reviewer...")}`);
|
|
46641
|
+
process.exit(0);
|
|
46642
|
+
};
|
|
46643
|
+
process.on("SIGINT", handleSignal);
|
|
46644
|
+
process.on("SIGTERM", handleSignal);
|
|
46645
|
+
await reviewer.run();
|
|
46668
46646
|
}
|
|
46669
|
-
async function
|
|
46670
|
-
const
|
|
46671
|
-
|
|
46672
|
-
|
|
46673
|
-
|
|
46674
|
-
|
|
46675
|
-
|
|
46676
|
-
|
|
46677
|
-
|
|
46678
|
-
|
|
46679
|
-
const
|
|
46680
|
-
|
|
46681
|
-
|
|
46682
|
-
|
|
46683
|
-
|
|
46647
|
+
async function reviewLocalCommand(args) {
|
|
46648
|
+
const { values } = parseArgs7({
|
|
46649
|
+
args,
|
|
46650
|
+
options: {
|
|
46651
|
+
model: { type: "string" },
|
|
46652
|
+
provider: { type: "string" },
|
|
46653
|
+
dir: { type: "string" }
|
|
46654
|
+
},
|
|
46655
|
+
strict: false
|
|
46656
|
+
});
|
|
46657
|
+
const projectPath = values.dir || process.cwd();
|
|
46658
|
+
requireInitialization(projectPath, "review local");
|
|
46659
|
+
const localSettings = new SettingsManager(projectPath).load();
|
|
46660
|
+
const provider = resolveProvider3(values.provider || localSettings.provider);
|
|
46661
|
+
const model = values.model || localSettings.model || DEFAULT_MODEL[provider];
|
|
46662
|
+
const aiRunner = createAiRunner(provider, {
|
|
46663
|
+
projectPath,
|
|
46664
|
+
model
|
|
46665
|
+
});
|
|
46666
|
+
const reviewService = new ReviewService({
|
|
46667
|
+
aiRunner,
|
|
46668
|
+
projectPath,
|
|
46669
|
+
log: (msg, level) => {
|
|
46670
|
+
switch (level) {
|
|
46671
|
+
case "error":
|
|
46672
|
+
console.log(` ${c.error("✖")} ${msg}`);
|
|
46673
|
+
break;
|
|
46674
|
+
case "success":
|
|
46675
|
+
console.log(` ${c.success("✔")} ${msg}`);
|
|
46676
|
+
break;
|
|
46677
|
+
default:
|
|
46678
|
+
console.log(` ${c.dim(msg)}`);
|
|
46679
|
+
}
|
|
46680
|
+
}
|
|
46681
|
+
});
|
|
46682
|
+
console.log(`
|
|
46683
|
+
${c.primary("\uD83D\uDD0D")} ${c.bold("Reviewing staged changes...")}
|
|
46684
46684
|
`);
|
|
46685
|
-
|
|
46686
|
-
|
|
46687
|
-
|
|
46688
|
-
console.error(`
|
|
46689
|
-
${c.error("✖")} ${c.bold("API key is not configured.")}
|
|
46690
|
-
Run ${c.primary("locus config setup --api-key <key>")} first.
|
|
46685
|
+
const report = await reviewService.reviewStagedChanges(null);
|
|
46686
|
+
if (!report) {
|
|
46687
|
+
console.log(` ${c.dim("No changes to review.")}
|
|
46691
46688
|
`);
|
|
46692
|
-
|
|
46689
|
+
return;
|
|
46693
46690
|
}
|
|
46694
|
-
|
|
46695
|
-
|
|
46696
|
-
|
|
46697
|
-
await installLaunchd(projectPath);
|
|
46691
|
+
const reviewsDir = join21(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.reviewsDir);
|
|
46692
|
+
if (!existsSync21(reviewsDir)) {
|
|
46693
|
+
mkdirSync10(reviewsDir, { recursive: true });
|
|
46698
46694
|
}
|
|
46699
|
-
|
|
46700
|
-
|
|
46701
|
-
|
|
46702
|
-
|
|
46703
|
-
|
|
46704
|
-
${c.
|
|
46695
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
46696
|
+
const reportPath = join21(reviewsDir, `review-${timestamp}.md`);
|
|
46697
|
+
writeFileSync9(reportPath, report, "utf-8");
|
|
46698
|
+
console.log(`
|
|
46699
|
+
${c.success("✔")} ${c.success("Review complete!")}`);
|
|
46700
|
+
console.log(` ${c.dim("Report saved to:")} ${c.primary(reportPath)}
|
|
46705
46701
|
`);
|
|
46706
|
-
process.exit(1);
|
|
46707
|
-
}
|
|
46708
|
-
if (platform === "linux") {
|
|
46709
|
-
await uninstallSystemd();
|
|
46710
|
-
} else {
|
|
46711
|
-
await uninstallLaunchd();
|
|
46712
|
-
}
|
|
46713
46702
|
}
|
|
46714
|
-
|
|
46715
|
-
|
|
46716
|
-
|
|
46717
|
-
|
|
46718
|
-
|
|
46719
|
-
|
|
46703
|
+
// src/commands/run.ts
|
|
46704
|
+
init_index_node();
|
|
46705
|
+
init_config_manager();
|
|
46706
|
+
init_settings_manager();
|
|
46707
|
+
init_utils3();
|
|
46708
|
+
init_workspace_resolver();
|
|
46709
|
+
import { parseArgs as parseArgs8 } from "node:util";
|
|
46710
|
+
async function runCommand(args) {
|
|
46711
|
+
const { values } = parseArgs8({
|
|
46712
|
+
args,
|
|
46713
|
+
options: {
|
|
46714
|
+
"api-key": { type: "string" },
|
|
46715
|
+
workspace: { type: "string" },
|
|
46716
|
+
sprint: { type: "string" },
|
|
46717
|
+
model: { type: "string" },
|
|
46718
|
+
provider: { type: "string" },
|
|
46719
|
+
"reasoning-effort": { type: "string" },
|
|
46720
|
+
"skip-planning": { type: "boolean" },
|
|
46721
|
+
"api-url": { type: "string" },
|
|
46722
|
+
dir: { type: "string" }
|
|
46723
|
+
},
|
|
46724
|
+
strict: false
|
|
46725
|
+
});
|
|
46726
|
+
const projectPath = values.dir || process.cwd();
|
|
46727
|
+
requireInitialization(projectPath, "run");
|
|
46728
|
+
const configManager = new ConfigManager(projectPath);
|
|
46729
|
+
configManager.updateVersion(VERSION2);
|
|
46730
|
+
const settingsManager = new SettingsManager(projectPath);
|
|
46731
|
+
const settings = settingsManager.load();
|
|
46732
|
+
const apiKey = values["api-key"] || settings.apiKey;
|
|
46733
|
+
if (!apiKey) {
|
|
46734
|
+
console.error(c.error("Error: API key is required"));
|
|
46735
|
+
console.error(c.dim(`Configure with: locus config setup --api-key <key>
|
|
46736
|
+
Or pass --api-key flag`));
|
|
46720
46737
|
process.exit(1);
|
|
46721
46738
|
}
|
|
46722
|
-
|
|
46723
|
-
|
|
46724
|
-
|
|
46725
|
-
|
|
46739
|
+
let workspaceId = values.workspace;
|
|
46740
|
+
const provider = resolveProvider3(values.provider || settings.provider);
|
|
46741
|
+
const model = values.model || settings.model || DEFAULT_MODEL[provider];
|
|
46742
|
+
const apiBase = values["api-url"] || settings.apiUrl || "https://api.locusai.dev/api";
|
|
46743
|
+
try {
|
|
46744
|
+
const resolver = new WorkspaceResolver({
|
|
46745
|
+
apiKey,
|
|
46746
|
+
apiBase,
|
|
46747
|
+
workspaceId: values.workspace
|
|
46748
|
+
});
|
|
46749
|
+
workspaceId = await resolver.resolve();
|
|
46750
|
+
} catch (error48) {
|
|
46751
|
+
console.error(c.error(error48 instanceof Error ? error48.message : String(error48)));
|
|
46752
|
+
process.exit(1);
|
|
46726
46753
|
}
|
|
46754
|
+
const reasoningEffort = values["reasoning-effort"];
|
|
46755
|
+
const orchestrator = new AgentOrchestrator({
|
|
46756
|
+
workspaceId,
|
|
46757
|
+
sprintId: values.sprint || "",
|
|
46758
|
+
model,
|
|
46759
|
+
provider,
|
|
46760
|
+
reasoningEffort,
|
|
46761
|
+
apiBase,
|
|
46762
|
+
maxIterations: 100,
|
|
46763
|
+
projectPath,
|
|
46764
|
+
apiKey
|
|
46765
|
+
});
|
|
46766
|
+
orchestrator.on("agent:spawned", (data) => console.log(` ${c.info("●")} ${c.bold("Agent spawned:")} ${data.agentId}`));
|
|
46767
|
+
orchestrator.on("task:assigned", (data) => console.log(` ${c.info("●")} ${c.bold("Claimed:")} ${data.title}`));
|
|
46768
|
+
orchestrator.on("task:completed", (data) => console.log(` ${c.success("✔")} ${c.success("Completed:")} ${c.dim(data.taskId)}`));
|
|
46769
|
+
orchestrator.on("task:failed", (data) => console.log(` ${c.error("✖")} ${c.error("Failed:")} ${c.bold(data.taskId)}: ${data.error}`));
|
|
46770
|
+
orchestrator.on("agent:stale", (data) => console.log(` ${c.error("⚠")} ${c.error("Stale agent killed:")} ${data.agentId}`));
|
|
46771
|
+
let isShuttingDown = false;
|
|
46772
|
+
const handleSignal = async (signal) => {
|
|
46773
|
+
if (isShuttingDown)
|
|
46774
|
+
return;
|
|
46775
|
+
isShuttingDown = true;
|
|
46776
|
+
console.log(`
|
|
46777
|
+
${c.info(`Received ${signal}. Stopping agent and cleaning up...`)}`);
|
|
46778
|
+
await orchestrator.stop();
|
|
46779
|
+
process.exit(0);
|
|
46780
|
+
};
|
|
46781
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
46782
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
46783
|
+
console.log(`
|
|
46784
|
+
${c.primary("\uD83D\uDE80")} ${c.bold("Starting agent in")} ${c.primary(projectPath)}...`);
|
|
46785
|
+
console.log(` ${c.dim("Tasks will be executed sequentially on a single branch")}`);
|
|
46786
|
+
console.log(` ${c.dim("Changes will be committed and pushed after each task")}`);
|
|
46787
|
+
console.log(` ${c.dim("A PR will be opened when all tasks are done")}`);
|
|
46788
|
+
await orchestrator.start();
|
|
46727
46789
|
}
|
|
46790
|
+
// src/commands/service.ts
|
|
46791
|
+
init_index_node();
|
|
46728
46792
|
async function serviceCommand(args) {
|
|
46729
|
-
const projectPath = process.cwd();
|
|
46730
|
-
requireInitialization(projectPath, "service");
|
|
46731
46793
|
const subcommand = args[0];
|
|
46732
|
-
|
|
46733
|
-
|
|
46734
|
-
|
|
46735
|
-
|
|
46736
|
-
|
|
46737
|
-
|
|
46738
|
-
|
|
46739
|
-
|
|
46740
|
-
|
|
46741
|
-
|
|
46742
|
-
|
|
46743
|
-
|
|
46794
|
+
const mapping = {
|
|
46795
|
+
install: "start",
|
|
46796
|
+
uninstall: "stop",
|
|
46797
|
+
status: "status"
|
|
46798
|
+
};
|
|
46799
|
+
const mapped = subcommand ? mapping[subcommand] : undefined;
|
|
46800
|
+
if (mapped) {
|
|
46801
|
+
console.log(` ${c.dim(`Hint: 'locus service ${subcommand}' is now 'locus daemon ${mapped}'`)}
|
|
46802
|
+
`);
|
|
46803
|
+
await daemonCommand([mapped, ...args.slice(1)]);
|
|
46804
|
+
} else {
|
|
46805
|
+
await daemonCommand(args);
|
|
46744
46806
|
}
|
|
46745
46807
|
}
|
|
46746
46808
|
// src/commands/telegram.ts
|
|
46747
46809
|
init_index_node();
|
|
46748
46810
|
init_settings_manager();
|
|
46749
46811
|
import { spawn as spawn5 } from "node:child_process";
|
|
46750
|
-
import { existsSync as
|
|
46751
|
-
import { join as
|
|
46812
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
46813
|
+
import { join as join22 } from "node:path";
|
|
46752
46814
|
import { createInterface as createInterface2 } from "node:readline";
|
|
46753
46815
|
function ask2(question) {
|
|
46754
46816
|
const rl = createInterface2({
|
|
@@ -46774,7 +46836,7 @@ function showTelegramHelp() {
|
|
|
46774
46836
|
${c.primary("locus telegram")} ${c.dim("<subcommand> [options]")}
|
|
46775
46837
|
|
|
46776
46838
|
${c.header(" SUBCOMMANDS ")}
|
|
46777
|
-
${c.success("
|
|
46839
|
+
${c.success("start")} Start the Telegram bot
|
|
46778
46840
|
${c.success("setup")} Interactive Telegram bot setup (or pass flags below)
|
|
46779
46841
|
${c.success("config")} Show current Telegram configuration
|
|
46780
46842
|
${c.success("set")} Set a config value
|
|
@@ -46783,7 +46845,7 @@ function showTelegramHelp() {
|
|
|
46783
46845
|
${c.success("remove")} Remove Telegram configuration
|
|
46784
46846
|
|
|
46785
46847
|
${c.header(" EXAMPLES ")}
|
|
46786
|
-
${c.dim("$")} ${c.primary("locus telegram
|
|
46848
|
+
${c.dim("$")} ${c.primary("locus telegram start")}
|
|
46787
46849
|
${c.dim("$")} ${c.primary('locus telegram setup --token "123:ABC" --chat-id 987654')}
|
|
46788
46850
|
${c.dim("$")} ${c.primary("locus telegram config")}
|
|
46789
46851
|
${c.dim("$")} ${c.primary("locus telegram remove")}
|
|
@@ -46794,7 +46856,7 @@ function showTelegramHelp() {
|
|
|
46794
46856
|
${c.primary("locus config set <key> <value>")}
|
|
46795
46857
|
`);
|
|
46796
46858
|
}
|
|
46797
|
-
async function
|
|
46859
|
+
async function setup(args, projectPath) {
|
|
46798
46860
|
let token;
|
|
46799
46861
|
let chatId;
|
|
46800
46862
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -46866,11 +46928,11 @@ async function setupCommand2(args, projectPath) {
|
|
|
46866
46928
|
${c.primary("Chat ID:")} ${parsedChatId}
|
|
46867
46929
|
|
|
46868
46930
|
${c.bold("Next steps:")}
|
|
46869
|
-
|
|
46870
|
-
Or run manually:
|
|
46931
|
+
Start as daemon: ${c.primary("locus daemon start")}
|
|
46932
|
+
Or run manually: ${c.primary("locus telegram start")}
|
|
46871
46933
|
`);
|
|
46872
46934
|
}
|
|
46873
|
-
function
|
|
46935
|
+
function showConfig(projectPath) {
|
|
46874
46936
|
const manager = new SettingsManager(projectPath);
|
|
46875
46937
|
const settings = manager.load();
|
|
46876
46938
|
const tg = settings.telegram;
|
|
@@ -46886,36 +46948,28 @@ function configCommand2(projectPath) {
|
|
|
46886
46948
|
console.log(` ${c.dim("File: .locus/settings.json (telegram section)")}
|
|
46887
46949
|
`);
|
|
46888
46950
|
const entries = [];
|
|
46889
|
-
if (tg.botToken)
|
|
46951
|
+
if (tg.botToken)
|
|
46890
46952
|
entries.push(["botToken", maskToken(tg.botToken)]);
|
|
46891
|
-
|
|
46892
|
-
if (tg.chatId) {
|
|
46953
|
+
if (tg.chatId)
|
|
46893
46954
|
entries.push(["chatId", String(tg.chatId)]);
|
|
46894
|
-
|
|
46895
|
-
if (tg.testMode !== undefined) {
|
|
46955
|
+
if (tg.testMode !== undefined)
|
|
46896
46956
|
entries.push(["testMode", String(tg.testMode)]);
|
|
46897
|
-
|
|
46898
|
-
if (settings.apiKey) {
|
|
46957
|
+
if (settings.apiKey)
|
|
46899
46958
|
entries.push(["apiKey (shared)", maskToken(settings.apiKey)]);
|
|
46900
|
-
|
|
46901
|
-
if (settings.apiUrl) {
|
|
46959
|
+
if (settings.apiUrl)
|
|
46902
46960
|
entries.push(["apiUrl (shared)", settings.apiUrl]);
|
|
46903
|
-
|
|
46904
|
-
if (settings.provider) {
|
|
46961
|
+
if (settings.provider)
|
|
46905
46962
|
entries.push(["provider (shared)", settings.provider]);
|
|
46906
|
-
|
|
46907
|
-
if (settings.model) {
|
|
46963
|
+
if (settings.model)
|
|
46908
46964
|
entries.push(["model (shared)", settings.model]);
|
|
46909
|
-
|
|
46910
|
-
if (settings.workspaceId) {
|
|
46965
|
+
if (settings.workspaceId)
|
|
46911
46966
|
entries.push(["workspaceId (shared)", settings.workspaceId]);
|
|
46912
|
-
}
|
|
46913
46967
|
for (const [key, value] of entries) {
|
|
46914
46968
|
console.log(` ${c.primary(`${key}:`)} ${value}`);
|
|
46915
46969
|
}
|
|
46916
46970
|
console.log("");
|
|
46917
46971
|
}
|
|
46918
|
-
function
|
|
46972
|
+
function setValue(args, projectPath) {
|
|
46919
46973
|
const key = args[0]?.trim();
|
|
46920
46974
|
const value = args.slice(1).join(" ").trim();
|
|
46921
46975
|
if (!key || !value) {
|
|
@@ -46958,7 +47012,7 @@ function setCommand2(args, projectPath) {
|
|
|
46958
47012
|
${c.success("✔")} Set ${c.primary(key)} = ${displayValue}
|
|
46959
47013
|
`);
|
|
46960
47014
|
}
|
|
46961
|
-
function
|
|
47015
|
+
function removeConfig(projectPath) {
|
|
46962
47016
|
const manager = new SettingsManager(projectPath);
|
|
46963
47017
|
const settings = manager.load();
|
|
46964
47018
|
if (!settings.telegram) {
|
|
@@ -46973,7 +47027,7 @@ function removeCommand2(projectPath) {
|
|
|
46973
47027
|
${c.success("✔")} ${c.bold("Telegram configuration removed.")}
|
|
46974
47028
|
`);
|
|
46975
47029
|
}
|
|
46976
|
-
function
|
|
47030
|
+
function startBot(projectPath) {
|
|
46977
47031
|
const manager = new SettingsManager(projectPath);
|
|
46978
47032
|
const settings = manager.load();
|
|
46979
47033
|
if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
|
|
@@ -46983,22 +47037,14 @@ function runBotCommand(projectPath) {
|
|
|
46983
47037
|
`);
|
|
46984
47038
|
process.exit(1);
|
|
46985
47039
|
}
|
|
46986
|
-
const
|
|
46987
|
-
const isMonorepo =
|
|
46988
|
-
|
|
46989
|
-
|
|
46990
|
-
|
|
46991
|
-
cmd = "bun";
|
|
46992
|
-
args = ["run", monorepoTelegramEntry];
|
|
46993
|
-
} else {
|
|
46994
|
-
cmd = "locus-telegram";
|
|
46995
|
-
args = [];
|
|
46996
|
-
}
|
|
46997
|
-
const env = { ...process.env };
|
|
46998
|
-
const child = spawn5(cmd, args, {
|
|
47040
|
+
const monorepoEntry = join22(projectPath, "packages/telegram/src/index.ts");
|
|
47041
|
+
const isMonorepo = existsSync22(monorepoEntry);
|
|
47042
|
+
const cmd = isMonorepo ? "bun" : "locus-telegram";
|
|
47043
|
+
const cmdArgs = isMonorepo ? ["run", monorepoEntry] : [];
|
|
47044
|
+
const child = spawn5(cmd, cmdArgs, {
|
|
46999
47045
|
cwd: projectPath,
|
|
47000
47046
|
stdio: "inherit",
|
|
47001
|
-
env
|
|
47047
|
+
env: { ...process.env }
|
|
47002
47048
|
});
|
|
47003
47049
|
child.on("error", (err) => {
|
|
47004
47050
|
if (err.code === "ENOENT" && !isMonorepo) {
|
|
@@ -47019,22 +47065,23 @@ function runBotCommand(projectPath) {
|
|
|
47019
47065
|
}
|
|
47020
47066
|
async function telegramCommand(args) {
|
|
47021
47067
|
const projectPath = process.cwd();
|
|
47022
|
-
const subcommand = args
|
|
47068
|
+
const [subcommand, ...subArgs] = args;
|
|
47023
47069
|
switch (subcommand) {
|
|
47070
|
+
case "start":
|
|
47024
47071
|
case "run":
|
|
47025
|
-
|
|
47072
|
+
startBot(projectPath);
|
|
47026
47073
|
break;
|
|
47027
47074
|
case "setup":
|
|
47028
|
-
await
|
|
47075
|
+
await setup(subArgs, projectPath);
|
|
47029
47076
|
break;
|
|
47030
47077
|
case "config":
|
|
47031
|
-
|
|
47078
|
+
showConfig(projectPath);
|
|
47032
47079
|
break;
|
|
47033
47080
|
case "set":
|
|
47034
|
-
|
|
47081
|
+
setValue(subArgs, projectPath);
|
|
47035
47082
|
break;
|
|
47036
47083
|
case "remove":
|
|
47037
|
-
|
|
47084
|
+
removeConfig(projectPath);
|
|
47038
47085
|
break;
|
|
47039
47086
|
default:
|
|
47040
47087
|
showTelegramHelp();
|
|
@@ -47043,10 +47090,7 @@ async function telegramCommand(args) {
|
|
|
47043
47090
|
// src/commands/upgrade.ts
|
|
47044
47091
|
init_index_node();
|
|
47045
47092
|
import { execSync as execSync3 } from "node:child_process";
|
|
47046
|
-
import { existsSync as existsSync22 } from "node:fs";
|
|
47047
47093
|
var PACKAGES = ["@locusai/cli", "@locusai/telegram"];
|
|
47048
|
-
var SYSTEMD_UNIT_PATH2 = "/etc/systemd/system/locus.service";
|
|
47049
|
-
var SYSTEMD_TELEGRAM_UNIT_PATH = "/etc/systemd/system/locus-telegram.service";
|
|
47050
47094
|
function getInstalledVersion(pkg) {
|
|
47051
47095
|
try {
|
|
47052
47096
|
const output = execSync3(`npm list -g ${pkg} --depth=0 --json`, {
|
|
@@ -47073,6 +47117,7 @@ async function upgradeCommand() {
|
|
|
47073
47117
|
console.log(`
|
|
47074
47118
|
${c.header(" UPGRADE ")}
|
|
47075
47119
|
`);
|
|
47120
|
+
const daemonWasRunning = await isDaemonRunning();
|
|
47076
47121
|
try {
|
|
47077
47122
|
console.log(` ${c.dim("◌")} Cleaning npm cache...`);
|
|
47078
47123
|
execSync3("npm cache clean --force", {
|
|
@@ -47084,6 +47129,7 @@ async function upgradeCommand() {
|
|
|
47084
47129
|
console.log(` ${c.dim("⚠")} Could not clean npm cache, continuing...
|
|
47085
47130
|
`);
|
|
47086
47131
|
}
|
|
47132
|
+
let anyUpdated = false;
|
|
47087
47133
|
for (const pkg of PACKAGES) {
|
|
47088
47134
|
const current = getInstalledVersion(pkg);
|
|
47089
47135
|
const latest = getLatestVersion(pkg);
|
|
@@ -47106,33 +47152,26 @@ async function upgradeCommand() {
|
|
|
47106
47152
|
});
|
|
47107
47153
|
console.log(` ${c.success("✔")} ${c.bold(pkg)} updated to ${c.primary(`v${latest}`)}
|
|
47108
47154
|
`);
|
|
47155
|
+
anyUpdated = true;
|
|
47109
47156
|
} catch {
|
|
47110
47157
|
console.error(` ${c.error("✖")} Failed to update ${c.bold(pkg)}. Try manually:
|
|
47111
47158
|
` + ` ${c.primary(`npm install -g ${pkg}@latest`)}
|
|
47112
47159
|
`);
|
|
47113
47160
|
}
|
|
47114
47161
|
}
|
|
47115
|
-
if (
|
|
47116
|
-
|
|
47117
|
-
|
|
47118
|
-
|
|
47119
|
-
|
|
47120
|
-
if (!split) {
|
|
47121
|
-
throw "PATH NOTH FOUND";
|
|
47122
|
-
}
|
|
47123
|
-
const name = split.replace(".service", "");
|
|
47124
|
-
try {
|
|
47125
|
-
console.log(` ${c.info("▶")} Restarting ${name} service...`);
|
|
47126
|
-
execSync3(`systemctl restart ${name}`, {
|
|
47127
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
47128
|
-
});
|
|
47129
|
-
console.log(` ${c.success("✔")} ${name} service restarted
|
|
47162
|
+
if (daemonWasRunning && anyUpdated) {
|
|
47163
|
+
console.log(` ${c.info("▶")} Restarting locus daemon...`);
|
|
47164
|
+
const restarted = await restartDaemonIfRunning();
|
|
47165
|
+
if (restarted) {
|
|
47166
|
+
console.log(` ${c.success("✔")} Locus daemon restarted
|
|
47130
47167
|
`);
|
|
47131
|
-
|
|
47132
|
-
|
|
47168
|
+
} else {
|
|
47169
|
+
console.log(` ${c.dim("⚠")} Could not restart daemon (may need sudo)
|
|
47133
47170
|
`);
|
|
47134
|
-
}
|
|
47135
47171
|
}
|
|
47172
|
+
} else if (daemonWasRunning && !anyUpdated) {
|
|
47173
|
+
console.log(` ${c.dim("No updates — daemon left running")}
|
|
47174
|
+
`);
|
|
47136
47175
|
}
|
|
47137
47176
|
console.log("");
|
|
47138
47177
|
}
|
|
@@ -47205,6 +47244,9 @@ async function main() {
|
|
|
47205
47244
|
case "config":
|
|
47206
47245
|
await configCommand(args);
|
|
47207
47246
|
break;
|
|
47247
|
+
case "daemon":
|
|
47248
|
+
await daemonCommand(args);
|
|
47249
|
+
break;
|
|
47208
47250
|
case "service":
|
|
47209
47251
|
await serviceCommand(args);
|
|
47210
47252
|
break;
|