@locusai/cli 0.15.3 → 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.
Files changed (2) hide show
  1. package/bin/locus.js +1824 -1764
  2. 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 existsSync18, mkdirSync as mkdirSync8 } from "node:fs";
43455
- import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
43456
- import { basename as basename2, join as join18 } from "node:path";
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 = homedir2() + p.slice(1);
43469
+ p = homedir4() + p.slice(1);
43470
43470
  }
43471
43471
  return p;
43472
43472
  }
43473
43473
  function copyToStable(srcPath) {
43474
43474
  try {
43475
- mkdirSync8(STABLE_IMAGE_DIR, { recursive: true });
43475
+ mkdirSync9(STABLE_IMAGE_DIR, { recursive: true });
43476
43476
  const ts = Date.now();
43477
43477
  const name = `${ts}-${basename2(srcPath)}`;
43478
- const destPath = join18(STABLE_IMAGE_DIR, name);
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 = existsSync18(normalized);
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 = join18(tmpdir2(), "locus-images");
43572
+ STABLE_IMAGE_DIR = join20(tmpdir2(), "locus-images");
43573
43573
  });
43574
43574
 
43575
43575
  // src/repl/input-handler.ts
@@ -44783,1954 +44783,2034 @@ async function configCommand(args) {
44783
44783
  showConfigHelp();
44784
44784
  }
44785
44785
  }
44786
- // src/commands/discuss.ts
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 { parseArgs as parseArgs3 } from "node:util";
44794
- async function discussCommand(args) {
44795
- const { values, positionals } = parseArgs3({
44796
- args,
44797
- options: {
44798
- list: { type: "boolean" },
44799
- show: { type: "string" },
44800
- archive: { type: "string" },
44801
- delete: { type: "string" },
44802
- model: { type: "string" },
44803
- provider: { type: "string" },
44804
- "reasoning-effort": { type: "string" },
44805
- dir: { type: "string" }
44806
- },
44807
- strict: false,
44808
- allowPositionals: true
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
- const projectPath = values.dir || process.cwd();
44811
- requireInitialization(projectPath, "discuss");
44812
- const discussionManager = new DiscussionManager(projectPath);
44813
- if (values.list) {
44814
- return listDiscussions(discussionManager);
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 (values.show) {
44817
- return showDiscussion(discussionManager, values.show);
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
- if (values.archive) {
44820
- return archiveDiscussion(discussionManager, values.archive);
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
- if (values.delete) {
44823
- return deleteDiscussion(discussionManager, values.delete);
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 topic = positionals.join(" ").trim();
44826
- if (!topic) {
44827
- showDiscussHelp();
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
- const settings = new SettingsManager(projectPath).load();
44831
- const provider = resolveProvider3(values.provider || settings.provider);
44832
- const model = values.model || settings.model || DEFAULT_MODEL[provider];
44833
- const reasoningEffort = values["reasoning-effort"];
44834
- const aiRunner = createAiRunner(provider, {
44835
- projectPath,
44836
- model,
44837
- reasoningEffort
44838
- });
44839
- const log = (message, level) => {
44840
- const icon = level === "success" ? c.success("✔") : level === "error" ? c.error("✖") : level === "warn" ? c.warning("!") : c.info("●");
44841
- console.log(` ${icon} ${message}`);
44842
- };
44843
- const facilitator = new DiscussionFacilitator({
44844
- projectPath,
44845
- aiRunner,
44846
- discussionManager,
44847
- log,
44848
- provider,
44849
- model
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(" DISCUSSION ")} ${c.bold("Starting interactive discussion...")}
44853
- `);
44854
- console.log(` ${c.dim("Topic:")} ${c.bold(topic)}`);
44855
- console.log(` ${c.dim("Model:")} ${c.dim(`${model} (${provider})`)}
44856
- `);
44857
- const renderer = new ProgressRenderer;
44858
- let discussionId;
44859
- try {
44860
- renderer.showThinkingStarted();
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
- renderer.finalize();
44871
- } catch (error48) {
44872
- renderer.finalize();
44984
+ }
44985
+ async function resolveBinaries() {
44986
+ const binaryPath = await findTelegramBinary();
44987
+ if (!binaryPath) {
44873
44988
  console.error(`
44874
- ${c.error("✖")} ${c.red("Failed to start discussion:")} ${error48 instanceof Error ? error48.message : String(error48)}
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
- console.log(` ${c.dim("Type your response, or 'help' for commands.")}`);
44879
- console.log(` ${c.dim("Enter to send, Shift+Enter for newline. Use 'exit' or Ctrl+D to quit.")}
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
- }
44887
- console.log(`
44888
- ${c.dim("Discussion saved.")} ${c.dim("ID:")} ${c.cyan(discussionId)}`);
44889
- console.log(c.dim(`
44890
- Goodbye!
44891
- `));
44892
- inputHandler.stop();
44893
- process.exit(0);
44894
- };
44895
- const handleSubmit = async (input) => {
44896
- interrupted = false;
44897
- const trimmed = input.trim();
44898
- if (trimmed === "") {
44899
- inputHandler.showPrompt();
44900
- return;
44901
45000
  }
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
- process.stdout.write(summary);
44929
- process.stdout.write(`
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.
44930
45023
  `);
44931
- summaryRenderer.finalize();
44932
- const discussion2 = discussionManager.load(discussionId);
44933
- if (discussion2) {
44934
- console.log(`
44935
- ${c.success("")} ${c.success("Discussion completed!")}
44936
- `);
44937
- console.log(` ${c.dim("Messages:")} ${discussion2.messages.length} ${c.dim("Insights:")} ${discussion2.insights.length}
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)}
44986
- `);
44987
- }
44988
- }
44989
- } catch (error48) {
44990
- chunkRenderer.finalize();
44991
- console.error(`
44992
- ${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
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
- isProcessing = false;
44996
- if (!interrupted) {
44997
- inputHandler.showPrompt();
44998
- }
44999
- };
45000
- const inputHandler = new InputHandler({
45001
- prompt: c.cyan("> "),
45002
- continuationPrompt: c.dim("… "),
45003
- onSubmit: (input) => {
45004
- handleSubmit(input).catch((err) => {
45005
- console.error(`
45006
- ${c.error("✖")} ${c.red(err instanceof Error ? err.message : String(err))}
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 listDiscussions(discussionManager) {
45029
- const discussions = discussionManager.list();
45030
- if (discussions.length === 0) {
45082
+ async function stopSystemd() {
45083
+ if (!existsSync19(SYSTEMD_UNIT_PATH)) {
45031
45084
  console.log(`
45032
- ${c.dim("No discussions found.")}
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.header(" DISCUSSIONS ")} ${c.dim(`(${discussions.length})`)}
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 showDiscussion(discussionManager, id) {
45050
- const md = discussionManager.getMarkdown(id);
45051
- if (!md) {
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("✖")} ${c.red(`Discussion not found: ${id}`)}
45111
+ ${c.error("✖")} Failed to restart: ${result.stderr.trim()}
45054
45112
  `);
45055
- process.exit(1);
45113
+ return;
45056
45114
  }
45057
45115
  console.log(`
45058
- ${md}
45116
+ ${c.success("✔")} ${c.bold("Locus daemon restarted.")}
45059
45117
  `);
45060
45118
  }
45061
- function archiveDiscussion(discussionManager, id) {
45062
- try {
45063
- discussionManager.archive(id);
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("")} ${c.dim("Discussion archived.")}
45124
+ ${c.success("")} ${c.bold("Locus daemon is running")} ${c.dim("(systemd)")}
45066
45125
  `);
45067
- } catch (error48) {
45068
- console.error(`
45069
- ${c.error("")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
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
- process.exit(1);
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.success("")} ${c.dim("Discussion deleted.")}
45134
+ ${c.secondary("")} ${c.bold("Locus daemon is not installed")}
45079
45135
  `);
45080
- } catch (error48) {
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 showCurrentInsights(discussionManager, discussionId) {
45088
- const discussion2 = discussionManager.load(discussionId);
45089
- if (!discussion2 || discussion2.insights.length === 0) {
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()}`);
45188
+ return;
45189
+ }
45190
+ const logPath = join19(logDir, "locus.log");
45191
+ console.log(`
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}`)}
45200
+ `);
45201
+ }
45202
+ async function stopLaunchd() {
45203
+ const plistPath = getPlistPath();
45204
+ if (!existsSync19(plistPath)) {
45090
45205
  console.log(`
45091
- ${c.dim("No insights extracted yet.")}
45206
+ ${c.dim("No launchd service found. Nothing to stop.")}
45092
45207
  `);
45208
+ await killOrphanedProcesses();
45093
45209
  return;
45094
45210
  }
45211
+ console.log(` ${c.info("▶")} Unloading service...`);
45212
+ await runShell("launchctl", ["unload", plistPath]);
45213
+ unlinkSync6(plistPath);
45214
+ await killOrphanedProcesses();
45095
45215
  console.log(`
45096
- ${c.header(" INSIGHTS ")} ${c.dim(`(${discussion2.insights.length})`)}
45216
+ ${c.success("")} ${c.bold("Locus daemon stopped.")}
45097
45217
  `);
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
45218
  }
45108
- function formatInsightTag(type) {
45109
- switch (type) {
45110
- case "decision":
45111
- return c.green("[DECISION]");
45112
- case "requirement":
45113
- return c.blue("[REQUIREMENT]");
45114
- case "idea":
45115
- return c.yellow("[IDEA]");
45116
- case "concern":
45117
- return c.red("[CONCERN]");
45118
- case "learning":
45119
- return c.cyan("[LEARNING]");
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;
45120
45235
  }
45121
- }
45122
- function showReplHelp() {
45123
45236
  console.log(`
45124
- ${c.header(" DISCUSSION COMMANDS ")}
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.")}
45237
+ ${c.success("✔")} ${c.bold("Locus daemon restarted.")}
45140
45238
  `);
45141
45239
  }
45142
- function showDiscussHelp() {
45143
- console.log(`
45144
- ${c.header(" LOCUS DISCUSS ")} ${c.dim("— Interactive AI Discussion")}
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")}
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")}
45174
45272
  `);
45273
+ }
45175
45274
  }
45176
- // src/commands/docs.ts
45177
- init_index_node();
45178
- init_config_manager();
45179
- init_settings_manager();
45180
- init_utils3();
45181
- init_workspace_resolver();
45182
- import { parseArgs as parseArgs4 } from "node:util";
45183
- async function docsCommand(args) {
45275
+ async function daemonCommand(args) {
45276
+ const projectPath = process.cwd();
45277
+ requireInitialization(projectPath, "daemon");
45184
45278
  const subcommand = args[0];
45185
- const subArgs = args.slice(1);
45279
+ const platform = subcommand ? requirePlatform() : null;
45280
+ const isLinux = platform === "linux";
45186
45281
  switch (subcommand) {
45187
- case "sync":
45188
- await docsSyncCommand(subArgs);
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
- default:
45191
- showDocsHelp();
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
- async function docsSyncCommand(args) {
45196
- const { values } = parseArgs4({
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
- "api-key": { type: "string" },
45200
- "api-url": { type: "string" },
45201
- workspace: { type: "string" },
45202
- dir: { type: "string" },
45203
- help: { type: "boolean" }
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, "docs sync");
45213
- const configManager = new ConfigManager(projectPath);
45214
- configManager.updateVersion(VERSION2);
45215
- const settingsManager = new SettingsManager(projectPath);
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
- const apiBase = values["api-url"] || settings.apiUrl || "https://api.locusai.dev/api";
45227
- const resolver = new WorkspaceResolver({
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
- const client = new LocusClient({
45242
- baseUrl: apiBase,
45243
- token: apiKey
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 fetcher = new DocumentFetcher({
45246
- client,
45247
- workspaceId,
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
- log: (message, level) => {
45250
- if (level === "error") {
45251
- console.log(` ${c.error("✖")} ${message}`);
45252
- return;
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.info("")} ${c.bold("Syncing docs from API...")}
45379
+ ${c.header(" DISCUSSION ")} ${c.bold("Starting interactive discussion...")}
45380
+ `);
45381
+ console.log(` ${c.dim("Topic:")} ${c.bold(topic)}`);
45382
+ console.log(` ${c.dim("Model:")} ${c.dim(`${model} (${provider})`)}
45267
45383
  `);
45384
+ const renderer = new ProgressRenderer;
45385
+ let discussionId;
45268
45386
  try {
45269
- await fetcher.fetch();
45270
- console.log(`
45271
- ${c.success("✔")} ${c.success("Docs sync complete.")} ${c.dim("Local docs: .locus/documents")}
45387
+ renderer.showThinkingStarted();
45388
+ const result = await facilitator.startDiscussion(topic);
45389
+ renderer.showThinkingStopped();
45390
+ discussionId = result.discussion.id;
45391
+ process.stdout.write(`
45392
+ `);
45393
+ process.stdout.write(result.message);
45394
+ process.stdout.write(`
45395
+
45272
45396
  `);
45397
+ renderer.finalize();
45273
45398
  } catch (error48) {
45399
+ renderer.finalize();
45274
45400
  console.error(`
45275
- ${c.error("✖")} ${c.red(`Docs sync failed: ${error48 instanceof Error ? error48.message : String(error48)}`)}
45401
+ ${c.error("✖")} ${c.red("Failed to start discussion:")} ${error48 instanceof Error ? error48.message : String(error48)}
45276
45402
  `);
45277
45403
  process.exit(1);
45278
45404
  }
45279
- }
45280
- function showDocsHelp() {
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")}
45291
- `);
45292
- }
45293
- function showDocsSyncHelp() {
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
45304
-
45305
- ${c.header(" EXAMPLES ")}
45306
- ${c.dim("$")} ${c.primary("locus docs sync")}
45307
- ${c.dim("$")} ${c.primary("locus docs sync --workspace ws_123")}
45308
- `);
45309
- }
45310
- // src/commands/exec.ts
45311
- init_index_node();
45312
- import { randomUUID as randomUUID2 } from "node:crypto";
45313
- import { parseArgs as parseArgs5 } from "node:util";
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)}
45436
- `);
45437
- }
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
- return;
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.primary("Recent Exec Sessions:")}
45481
- `);
45482
- for (const session2 of sessions.slice(0, 10)) {
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
  `));
45495
- }
45496
- }
45497
- async show(sessionId) {
45498
- if (!sessionId) {
45499
- console.error(`
45500
- ${c.error("Error:")} Session ID is required
45501
- `);
45502
- console.log(` ${c.dim("Usage: locus exec sessions show <session-id>")}
45503
- `);
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();
45504
45427
  return;
45505
45428
  }
45506
- const session2 = this.historyManager.findSessionByPartialId(sessionId);
45507
- if (!session2) {
45508
- console.error(`
45509
- ${c.error("Error:")} Session ${c.cyan(sessionId)} not found
45510
- `);
45511
- console.log(` ${c.dim("Use 'locus exec sessions list' to see available sessions")}
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(`
45512
45454
  `);
45513
- return;
45514
- }
45515
- console.log(`
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})`)}
45455
+ process.stdout.write(summary);
45456
+ process.stdout.write(`
45519
45457
  `);
45520
- if (session2.messages.length === 0) {
45521
- console.log(` ${c.dim("(No messages in this session)")}
45458
+ summaryRenderer.finalize();
45459
+ const discussion2 = discussionManager.load(discussionId);
45460
+ if (discussion2) {
45461
+ console.log(`
45462
+ ${c.success("✔")} ${c.success("Discussion completed!")}
45522
45463
  `);
45523
- return;
45524
- }
45525
- console.log(c.dim(" ─".repeat(30)));
45526
- console.log();
45527
- for (const message of session2.messages) {
45528
- const role = message.role === "user" ? c.cyan("You") : c.green("AI");
45529
- const content = message.content;
45530
- console.log(` ${role}:`);
45531
- const lines = content.split(`
45464
+ console.log(` ${c.dim("Messages:")} ${discussion2.messages.length} ${c.dim("Insights:")} ${discussion2.insights.length}
45532
45465
  `);
45533
- for (const line of lines) {
45534
- console.log(` ${line}`);
45535
- }
45536
- console.log();
45537
- }
45538
- }
45539
- async delete(sessionId) {
45540
- if (!sessionId) {
45541
- console.error(`
45542
- ${c.error("Error:")} Session ID is required
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")}
45543
45469
  `);
45544
- console.log(` ${c.dim("Usage: locus exec sessions delete <session-id>")}
45470
+ } catch (error48) {
45471
+ summaryRenderer.finalize();
45472
+ console.error(`
45473
+ ${c.error("✖")} ${c.red("Failed to summarize:")} ${error48 instanceof Error ? error48.message : String(error48)}
45545
45474
  `);
45546
- return;
45475
+ }
45476
+ inputHandler.stop();
45477
+ process.exit(0);
45478
+ return;
45479
+ }
45547
45480
  }
45548
- const session2 = this.historyManager.findSessionByPartialId(sessionId);
45549
- if (!session2) {
45550
- console.error(`
45551
- ${c.error("Error:")} Session ${c.cyan(sessionId)} not found
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
45552
45486
  `);
45553
- return;
45487
+ }
45554
45488
  }
45555
- const deleted = this.historyManager.deleteSession(session2.id);
45556
- if (deleted) {
45557
- console.log(`
45558
- ${c.success("✔")} Deleted session ${c.cyan(this.getShortId(session2.id))}
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)}
45559
45513
  `);
45560
- } else {
45514
+ }
45515
+ }
45516
+ } catch (error48) {
45517
+ chunkRenderer.finalize();
45561
45518
  console.error(`
45562
- ${c.error("Error:")} Failed to delete session
45563
- `);
45564
- }
45565
- }
45566
- async clear() {
45567
- const count = this.historyManager.getSessionCount();
45568
- if (count === 0) {
45569
- console.log(`
45570
- ${c.dim("No sessions to clear.")}
45519
+ ${c.error("")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
45571
45520
  `);
45572
- return;
45573
45521
  }
45574
- const deleted = this.historyManager.clearAllSessions();
45575
- console.log(`
45576
- ${c.success("✔")} Cleared ${deleted} exec session${deleted === 1 ? "" : "s"}
45577
- `);
45578
- }
45579
- getShortId(sessionId) {
45580
- const parts = sessionId.split("-");
45581
- if (parts.length >= 3) {
45582
- return parts.slice(-1)[0].slice(0, 8);
45522
+ isProcessing = false;
45523
+ if (!interrupted) {
45524
+ inputHandler.showPrompt();
45583
45525
  }
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).")}
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))}
45603
45534
  `);
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" }
45535
+ inputHandler.showPrompt();
45536
+ });
45621
45537
  },
45622
- strict: false
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()
45623
45551
  });
45624
- const jsonStream = values["json-stream"];
45625
- const projectPath = values.dir || process.cwd();
45626
- if (jsonStream) {
45627
- await execJsonStream(values, positionals, projectPath);
45552
+ inputHandler.start();
45553
+ inputHandler.showPrompt();
45554
+ }
45555
+ function listDiscussions(discussionManager) {
45556
+ const discussions = discussionManager.list();
45557
+ if (discussions.length === 0) {
45558
+ console.log(`
45559
+ ${c.dim("No discussions found.")}
45560
+ `);
45561
+ console.log(` ${c.dim("Start one with:")} ${c.cyan('locus discuss "your topic"')}
45562
+ `);
45628
45563
  return;
45629
45564
  }
45630
- requireInitialization(projectPath, "exec");
45631
- if (positionals[0] === "sessions") {
45632
- const sessionAction = positionals[1];
45633
- const sessionArg = positionals[2];
45634
- const cmds = new SessionCommands(projectPath);
45635
- switch (sessionAction) {
45636
- case "list":
45637
- await cmds.list();
45638
- return;
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
- const promptInput = positionals.join(" ");
45670
- if (!promptInput) {
45671
- console.error(c.error('Error: Prompt is required. Usage: locus exec "your prompt" or locus exec --interactive'));
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
- }
45674
- const useStreaming = !values["no-stream"];
45675
- const reasoningEffort = values["reasoning-effort"];
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!")}
45583
+ }
45584
+ console.log(`
45585
+ ${md}
45725
45586
  `);
45726
- process.exit(1);
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.success("Execution finished!")}
45592
+ ${c.success("✔")} ${c.dim("Discussion archived.")}
45739
45593
  `);
45740
45594
  } catch (error48) {
45741
45595
  console.error(`
45742
- ${c.error("✖")} ${c.error("Execution failed:")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
45596
+ ${c.error("✖")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
45743
45597
  `);
45744
45598
  process.exit(1);
45745
45599
  }
45746
45600
  }
45747
- async function execJsonStream(values, positionals, projectPath) {
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
- try {
45774
- requireInitialization(projectPath, "exec");
45775
- } catch (initError) {
45776
- renderer.emitFatalError("CLI_NOT_FOUND", initError instanceof Error ? initError.message : String(initError));
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
- const message = error48 instanceof Error ? error48.message : String(error48);
45802
- if (!renderer.isDone()) {
45803
- renderer.emitFatalError("UNKNOWN", message);
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
- // src/commands/help.ts
45809
- init_index_node();
45810
- function showHelp2() {
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(" USAGE ")}
45813
- ${c.primary("locus")} ${c.dim("<command> [options]")}
45814
-
45815
- ${c.header(" COMMANDS ")}
45816
- ${c.success("init")} Initialize Locus in the current directory
45817
- ${c.success("config")} Manage settings (API key, provider, model)
45818
- ${c.dim("setup Interactive one-time setup")}
45819
- ${c.dim("show Show current settings")}
45820
- ${c.dim("set <k> <v> Update a setting")}
45821
- ${c.dim("remove Remove all settings")}
45822
- ${c.success("index")} Index the codebase for AI context
45823
- ${c.success("run")} Start agent to work on tasks sequentially
45824
- ${c.success("discuss")} Start an interactive AI discussion on a topic
45825
- ${c.dim("--list List all discussions")}
45826
- ${c.dim("--show <id> Show discussion details")}
45827
- ${c.dim("--archive <id> Archive a discussion")}
45828
- ${c.dim("--delete <id> Delete a discussion")}
45829
- ${c.success("plan")} Run async planning meeting to create sprint plans
45830
- ${c.success("docs")} Manage workspace docs
45831
- ${c.dim("sync Sync docs from API to .locus/documents")}
45832
- ${c.success("review")} Review open Locus PRs on GitHub with AI
45833
- ${c.dim("local Review staged changes locally (no GitHub)")}
45834
- ${c.success("telegram")} Configure the Telegram bot
45835
- ${c.dim("setup Interactive bot token and chat ID setup")}
45836
- ${c.dim("config Show current configuration")}
45837
- ${c.dim("set <k> <v> Update a config value")}
45838
- ${c.dim("remove Remove Telegram configuration")}
45839
- ${c.success("exec")} Run a prompt with repository context
45840
- ${c.dim("--interactive, -i Start interactive REPL mode")}
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
- ${c.header(" OPTIONS ")}
45857
- ${c.secondary("--help")} Show this help message
45858
- ${c.secondary("--provider")} <name> AI provider: ${c.dim("claude")} or ${c.dim("codex")} (default: ${c.dim("claude")})
45859
- ${c.secondary("--model")} <name> AI model (claude: ${c.dim("opus, sonnet, haiku")} | codex: ${c.dim("gpt-5.3-codex, gpt-5-codex-mini")})
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(" GETTING STARTED ")}
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
- ${c.header(" EXAMPLES ")}
45869
- ${c.dim("$")} ${c.primary("locus run")}
45870
- ${c.dim("$")} ${c.primary("locus docs sync")}
45871
- ${c.dim("$")} ${c.primary("locus review")}
45872
- ${c.dim("$")} ${c.primary("locus review local")}
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
- For more information, visit: ${c.underline("https://docs.locusai.dev")}
45666
+ ${c.dim("Type anything else to continue the discussion.")}
45880
45667
  `);
45881
45668
  }
45882
- // src/commands/index-codebase.ts
45883
- init_index_node();
45884
- init_config_manager();
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
- // src/tree-summarizer.ts
45888
- init_index_node();
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
- class TreeSummarizer {
45891
- aiRunner;
45892
- constructor(aiRunner) {
45893
- this.aiRunner = aiRunner;
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
- File Tree:
45904
- ${tree}`;
45905
- const output = await this.aiRunner.run(prompt);
45906
- const jsonStr = extractJsonFromLLMOutput(output);
45907
- return JSON.parse(jsonStr);
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
45691
+
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();
45707
+ init_utils3();
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;
45908
45720
  }
45909
45721
  }
45910
-
45911
- // src/commands/index-codebase.ts
45912
- init_utils3();
45913
- async function indexCommand(args) {
45914
- const { values } = parseArgs6({
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
- model: { type: "string" },
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, "index");
45925
- new ConfigManager(projectPath).updateVersion(VERSION2);
45926
- const provider = resolveProvider3(values.provider);
45927
- const model = values.model || DEFAULT_MODEL[provider];
45928
- const aiRunner = createAiRunner(provider, {
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
- model
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.step(" INDEX ")} ${c.primary("Analyzing codebase in")} ${c.bold(projectPath)}...`);
45793
+ ${c.info("")} ${c.bold("Syncing docs from API...")}
45794
+ `);
45936
45795
  try {
45937
- const index = await indexer.index((msg) => console.log(` ${c.dim(msg)}`), (tree) => summarizer.summarize(tree));
45938
- if (index) {
45939
- indexer.saveIndex(index);
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.error("Indexing failed:")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}`);
45944
- console.error(c.dim(` The agent might have limited context until indexing succeeds.
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.success("✔")} ${c.success("Indexing complete!")}
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
- // src/commands/init.ts
45952
- init_index_node();
45953
- init_config_manager();
45954
- init_utils3();
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.dim(`Version: ${VERSION2}`)}`);
45977
- } else {
45978
- console.log(` ${c.success("")} ${c.success("Configuration updated successfully!")}
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.bold("Changes:")}
45981
- ${updates.map((u) => `${c.primary("")} ${u}`).join(`
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
- await configManager.init(VERSION2);
45995
- console.log(`
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/index.ts
46014
- init_plan();
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
- init_workspace_resolver();
46022
- import { existsSync as existsSync19, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "node:fs";
46023
- import { join as join19 } from "node:path";
46024
- import { parseArgs as parseArgs7 } from "node:util";
46025
- async function reviewCommand(args) {
46026
- const subcommand = args[0];
46027
- if (subcommand === "local") {
46028
- return reviewLocalCommand(args.slice(1));
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
- return reviewPrsCommand(args);
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
- const provider = resolveProvider3(values.provider || settings.provider);
46060
- const model = values.model || settings.model || DEFAULT_MODEL[provider];
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
- const log = (msg, level = "info") => {
46075
- const colorFn = {
46076
- info: c.cyan,
46077
- success: c.green,
46078
- warn: c.yellow,
46079
- error: c.red
46080
- }[level];
46081
- const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
46082
- console.log(` ${colorFn(`${prefix} ${msg}`)}`);
46083
- };
46084
- const prService = new PrService(projectPath, log);
46085
- const unreviewedPrs = prService.listUnreviewedLocusPrs();
46086
- if (unreviewedPrs.length === 0) {
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.dim("No unreviewed Locus PRs found.")}
46007
+ ${c.primary("Recent Exec Sessions:")}
46089
46008
  `);
46090
- return;
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
- console.log(`
46093
- ${c.primary("\uD83D\uDD0D")} ${c.bold(`Found ${unreviewedPrs.length} unreviewed PR(s). Starting reviewer...`)}
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
- isShuttingDown = true;
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.info("Received shutdown signal. Stopping reviewer...")}`);
46112
- process.exit(0);
46113
- };
46114
- process.on("SIGINT", handleSignal);
46115
- process.on("SIGTERM", handleSignal);
46116
- await reviewer.run();
46117
- }
46118
- async function reviewLocalCommand(args) {
46119
- const { values } = parseArgs7({
46120
- args,
46121
- options: {
46122
- model: { type: "string" },
46123
- provider: { type: "string" },
46124
- dir: { type: "string" }
46125
- },
46126
- strict: false
46127
- });
46128
- const projectPath = values.dir || process.cwd();
46129
- requireInitialization(projectPath, "review local");
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();
46151
46064
  }
46152
- });
46153
- console.log(`
46154
- ${c.primary("\uD83D\uDD0D")} ${c.bold("Reviewing staged changes...")}
46065
+ }
46066
+ async delete(sessionId) {
46067
+ if (!sessionId) {
46068
+ console.error(`
46069
+ ${c.error("Error:")} Session ID is required
46155
46070
  `);
46156
- const report = await reviewService.reviewStagedChanges(null);
46157
- if (!report) {
46158
- console.log(` ${c.dim("No changes to review.")}
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;
46081
+ }
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))}
46086
+ `);
46087
+ } else {
46088
+ console.error(`
46089
+ ${c.error("Error:")} Failed to delete session
46090
+ `);
46091
+ }
46092
+ }
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"}
46159
46104
  `);
46160
- return;
46161
46105
  }
46162
- const reviewsDir = join19(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.reviewsDir);
46163
- if (!existsSync19(reviewsDir)) {
46164
- mkdirSync9(reviewsDir, { recursive: true });
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);
46165
46112
  }
46166
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
46167
- const reportPath = join19(reviewsDir, `review-${timestamp}.md`);
46168
- writeFileSync8(reportPath, report, "utf-8");
46113
+ }
46114
+ function showSessionsHelp() {
46169
46115
  console.log(`
46170
- ${c.success("✔")} ${c.success("Review complete!")}`);
46171
- console.log(` ${c.dim("Report saved to:")} ${c.primary(reportPath)}
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
- // src/commands/run.ts
46175
- init_index_node();
46176
- init_config_manager();
46177
- init_settings_manager();
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
- "skip-planning": { type: "boolean" },
46192
- "api-url": { type: "string" },
46193
- dir: { type: "string" }
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
- requireInitialization(projectPath, "run");
46199
- const configManager = new ConfigManager(projectPath);
46200
- configManager.updateVersion(VERSION2);
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`));
46153
+ if (jsonStream) {
46154
+ await execJsonStream(values, positionals, projectPath);
46155
+ return;
46156
+ }
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
46192
+ });
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'));
46208
46199
  process.exit(1);
46209
46200
  }
46210
- let workspaceId = values.workspace;
46211
- const provider = resolveProvider3(values.provider || settings.provider);
46212
- const model = values.model || settings.model || DEFAULT_MODEL[provider];
46213
- const apiBase = values["api-url"] || settings.apiUrl || "https://api.locusai.dev/api";
46201
+ const useStreaming = !values["no-stream"];
46202
+ const reasoningEffort = values["reasoning-effort"];
46203
+ const aiRunner = createAiRunner(provider, {
46204
+ projectPath,
46205
+ model,
46206
+ reasoningEffort
46207
+ });
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
46214
  try {
46215
- const resolver = new WorkspaceResolver({
46216
- apiKey,
46217
- apiBase,
46218
- workspaceId: values.workspace
46219
- });
46220
- workspaceId = await resolver.resolve();
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
+ `);
46221
46267
  } catch (error48) {
46222
- console.error(c.error(error48 instanceof Error ? error48.message : String(error48)));
46268
+ console.error(`
46269
+ ${c.error("✖")} ${c.error("Execution failed:")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
46270
+ `);
46223
46271
  process.exit(1);
46224
46272
  }
46225
- const reasoningEffort = values["reasoning-effort"];
46226
- const orchestrator = new AgentOrchestrator({
46227
- workspaceId,
46228
- sprintId: values.sprint || "",
46273
+ }
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",
46229
46282
  model,
46230
- provider,
46231
- reasoningEffort,
46232
- apiBase,
46233
- maxIterations: 100,
46234
- projectPath,
46235
- apiKey
46283
+ provider,
46284
+ cwd: projectPath
46236
46285
  });
46237
- orchestrator.on("agent:spawned", (data) => console.log(` ${c.info("●")} ${c.bold("Agent spawned:")} ${data.agentId}`));
46238
- orchestrator.on("task:assigned", (data) => console.log(` ${c.info("●")} ${c.bold("Claimed:")} ${data.title}`));
46239
- orchestrator.on("task:completed", (data) => console.log(` ${c.success("✔")} ${c.success("Completed:")} ${c.dim(data.taskId)}`));
46240
- orchestrator.on("task:failed", (data) => console.log(` ${c.error("✖")} ${c.error("Failed:")} ${c.bold(data.taskId)}: ${data.error}`));
46241
- orchestrator.on("agent:stale", (data) => console.log(` ${c.error("⚠")} ${c.error("Stale agent killed:")} ${data.agentId}`));
46242
- let isShuttingDown = false;
46243
- const handleSignal = async (signal) => {
46244
- if (isShuttingDown)
46286
+ const handleSignal = () => {
46287
+ if (renderer.isDone()) {
46245
46288
  return;
46246
- isShuttingDown = true;
46247
- console.log(`
46248
- ${c.info(`Received ${signal}. Stopping agent and cleaning up...`)}`);
46249
- await orchestrator.stop();
46250
- process.exit(0);
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
+ }
46251
46296
  };
46252
- process.on("SIGINT", () => handleSignal("SIGINT"));
46253
- process.on("SIGTERM", () => handleSignal("SIGTERM"));
46254
- console.log(`
46255
- ${c.primary("\uD83D\uDE80")} ${c.bold("Starting agent in")} ${c.primary(projectPath)}...`);
46256
- console.log(` ${c.dim("Tasks will be executed sequentially on a single branch")}`);
46257
- console.log(` ${c.dim("Changes will be committed and pushed after each task")}`);
46258
- console.log(` ${c.dim("A PR will be opened when all tasks are done")}`);
46259
- await orchestrator.start();
46260
- }
46261
- // src/commands/service.ts
46262
- init_index_node();
46263
- init_settings_manager();
46264
- init_utils3();
46265
- import { spawn as spawn4 } from "node:child_process";
46266
- import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "node:fs";
46267
- import { homedir as homedir3 } from "node:os";
46268
- import { dirname as dirname4, join as join20 } from "node:path";
46269
- async function findBinary() {
46270
- const result = await runShell("which", ["locus-telegram"]);
46271
- const p = result.stdout.trim();
46272
- return p?.startsWith?.("/") ? p : null;
46273
- }
46274
- async function findBinDir(binary) {
46275
- const result = await runShell("which", [binary]);
46276
- const p = result.stdout.trim();
46277
- if (p?.startsWith?.("/"))
46278
- return dirname4(p);
46279
- return null;
46280
- }
46281
- function resolveNvmBinDir() {
46282
- const nvmDir = process.env.NVM_DIR || join20(homedir3(), ".nvm");
46283
- const versionsDir = join20(nvmDir, "versions", "node");
46284
- if (!existsSync20(versionsDir))
46285
- return null;
46286
- let versions2;
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
- const alias = readFileSync15(aliasPath, "utf-8").trim();
46303
- const match = versions2.find((v) => v === `v${alias}` || v.startsWith(`v${alias}.`));
46304
- if (match) {
46305
- const bin2 = join20(versionsDir, match, "bin");
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
- return 0;
46319
- });
46320
- const bin = join20(versionsDir, sorted[0], "bin");
46321
- return existsSync20(bin) ? bin : null;
46322
- }
46323
- async function buildServicePath() {
46324
- const home = homedir3();
46325
- const dirs = new Set;
46326
- dirs.add("/usr/local/bin");
46327
- dirs.add("/usr/bin");
46328
- dirs.add("/bin");
46329
- const candidates = [
46330
- join20(home, ".bun", "bin"),
46331
- join20(home, ".local", "bin"),
46332
- join20(home, ".npm", "bin"),
46333
- join20(home, ".npm-global", "bin"),
46334
- join20(home, ".yarn", "bin")
46335
- ];
46336
- for (const d of candidates) {
46337
- if (existsSync20(d))
46338
- dirs.add(d);
46339
- }
46340
- const nvmBin = resolveNvmBinDir();
46341
- if (nvmBin)
46342
- dirs.add(nvmBin);
46343
- const fnmCurrent = join20(home, ".fnm", "current", "bin");
46344
- if (existsSync20(fnmCurrent))
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
- function showServiceHelp() {
46335
+ // src/commands/help.ts
46336
+ init_index_node();
46337
+ function showHelp2() {
46360
46338
  console.log(`
46361
- ${c.header(" SERVICE ")}
46362
- ${c.primary("locus service")} ${c.dim("<subcommand>")}
46339
+ ${c.header(" USAGE ")}
46340
+ ${c.primary("locus")} ${c.dim("<command> [options]")}
46363
46341
 
46364
- ${c.header(" SUBCOMMANDS ")}
46365
- ${c.success("install")} Install Locus as a system service
46366
- ${c.dim("Sets up systemd (Linux) or launchd (macOS)")}
46367
- ${c.dim("to run the Telegram bot + proposal scheduler")}
46368
- ${c.success("uninstall")} Remove the system service
46369
- ${c.success("status")} Check if the service is running
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 service install")}
46373
- ${c.dim("$")} ${c.primary("locus service status")}
46374
- ${c.dim("$")} ${c.primary("locus service uninstall")}
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
- function runShell(cmd, args) {
46378
- return new Promise((resolve2) => {
46379
- const proc = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
46380
- let stdout = "";
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
- function generateSystemdUnit(projectPath, user2, binaryPath, servicePath) {
46393
- return `[Unit]
46394
- Description=Locus AI Agent (Telegram bot + proposal scheduler)
46395
- After=network-online.target
46396
- 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";
46397
46415
 
46398
- [Service]
46399
- Type=simple
46400
- User=${user2}
46401
- WorkingDirectory=${projectPath}
46402
- ExecStart=${binaryPath}
46403
- Restart=on-failure
46404
- RestartSec=10
46405
- Environment=PATH=${servicePath}
46406
- Environment=HOME=${homedir3()}
46416
+ // src/tree-summarizer.ts
46417
+ init_index_node();
46407
46418
 
46408
- [Install]
46409
- WantedBy=multi-user.target
46410
- `;
46411
- }
46412
- async function installSystemd(projectPath) {
46413
- const user2 = process.env.USER || "root";
46414
- const binaryPath = await findBinary();
46415
- if (!binaryPath) {
46416
- console.error(`
46417
- ${c.error("✖")} ${c.bold("Could not find locus-telegram binary.")}
46418
- ` + ` Install with: ${c.primary("npm install -g @locusai/telegram")}
46419
- `);
46420
- process.exit(1);
46421
- }
46422
- if (!await findBinDir("claude")) {
46423
- console.warn(`
46424
- ${c.secondary("⚠")} ${c.bold("Could not find 'claude' CLI in PATH.")}
46425
- ` + ` The service needs the Claude Code CLI to execute tasks.
46426
- ` + ` Install with: ${c.primary("npm install -g @anthropic-ai/claude-code")}
46427
- `);
46428
- }
46429
- if (!await findBinDir("codex")) {
46430
- console.warn(`
46431
- ${c.secondary("⚠")} ${c.bold("Could not find 'codex' CLI in PATH.")}
46432
- ` + ` The service needs the Codex CLI if using the Codex provider.
46433
- ` + ` Install with: ${c.primary("npm install -g @openai/codex")}
46434
- `);
46435
- }
46436
- const servicePath = await buildServicePath();
46437
- const unit = generateSystemdUnit(projectPath, user2, binaryPath, servicePath);
46438
- console.log(`
46439
- ${c.info("▶")} Writing systemd unit to ${c.dim(SYSTEMD_UNIT_PATH)}`);
46440
- writeFileSync9(SYSTEMD_UNIT_PATH, unit, "utf-8");
46441
- console.log(` ${c.info("▶")} Reloading systemd daemon...`);
46442
- await runShell("systemctl", ["daemon-reload"]);
46443
- console.log(` ${c.info("▶")} Enabling and starting ${SERVICE_NAME}...`);
46444
- await runShell("systemctl", ["enable", SERVICE_NAME]);
46445
- const startResult = await runShell("systemctl", ["start", SERVICE_NAME]);
46446
- if (startResult.exitCode !== 0) {
46447
- console.error(`
46448
- ${c.error("✖")} Failed to start service: ${startResult.stderr.trim()}`);
46449
- console.error(` ${c.dim("Check logs with:")} ${c.primary(`journalctl -u ${SERVICE_NAME} -f`)}`);
46450
- return;
46419
+ class TreeSummarizer {
46420
+ aiRunner;
46421
+ constructor(aiRunner) {
46422
+ this.aiRunner = aiRunner;
46451
46423
  }
46452
- console.log(`
46453
- ${c.success("✔")} ${c.bold("Locus service installed and running!")}
46454
-
46455
- ${c.bold("Service:")} ${SERVICE_NAME}
46456
- ${c.bold("Unit file:")} ${SYSTEMD_UNIT_PATH}
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
+ }
46457
46431
 
46458
- ${c.bold("Useful commands:")}
46459
- ${c.dim("$")} ${c.primary(`sudo systemctl status ${SERVICE_NAME}`)}
46460
- ${c.dim("$")} ${c.primary(`sudo systemctl restart ${SERVICE_NAME}`)}
46461
- ${c.dim("$")} ${c.primary(`journalctl -u ${SERVICE_NAME} -f`)}
46462
- `);
46432
+ File Tree:
46433
+ ${tree}`;
46434
+ const output = await this.aiRunner.run(prompt);
46435
+ const jsonStr = extractJsonFromLLMOutput(output);
46436
+ return JSON.parse(jsonStr);
46437
+ }
46463
46438
  }
46464
- async function uninstallSystemd() {
46465
- if (!existsSync20(SYSTEMD_UNIT_PATH)) {
46466
- console.log(`
46467
- ${c.dim("No systemd service found. Nothing to remove.")}
46468
- `);
46469
- return;
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);
46463
+ console.log(`
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) {
46471
+ console.error(`
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
+ `));
46470
46475
  }
46471
- console.log(` ${c.info("▶")} Stopping and disabling ${SERVICE_NAME}...`);
46472
- await runShell("systemctl", ["stop", SERVICE_NAME]);
46473
- await runShell("systemctl", ["disable", SERVICE_NAME]);
46474
- const { unlinkSync: unlinkSync6 } = await import("node:fs");
46475
- unlinkSync6(SYSTEMD_UNIT_PATH);
46476
- await runShell("systemctl", ["daemon-reload"]);
46477
46476
  console.log(`
46478
- ${c.success("✔")} ${c.bold("Locus service removed.")}
46477
+ ${c.success("✔")} ${c.success("Indexing complete!")}
46479
46478
  `);
46480
46479
  }
46481
- async function statusSystemd() {
46482
- const result = await runShell("systemctl", ["is-active", SERVICE_NAME]);
46483
- const state = result.stdout.trim();
46484
- if (state === "active") {
46485
- console.log(`
46486
- ${c.success("●")} ${c.bold("Locus service is running")} ${c.dim("(systemd)")}
46487
- `);
46488
- } else if (existsSync20(SYSTEMD_UNIT_PATH)) {
46489
- console.log(`
46490
- ${c.secondary("●")} ${c.bold(`Locus service is ${state}`)} ${c.dim("(systemd)")}
46491
- `);
46492
- console.log(` ${c.dim("Start with:")} ${c.primary(`sudo systemctl start ${SERVICE_NAME}`)}
46493
- `);
46494
- } 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)) {
46495
46488
  console.log(`
46496
- ${c.secondary("")} ${c.bold("Locus service is not installed")}
46497
- `);
46498
- console.log(` ${c.dim("Install with:")} ${c.primary("locus service install")}
46499
- `);
46500
- }
46501
- }
46502
- function generatePlist(projectPath, binaryPath, binaryArgs, servicePath) {
46503
- const argsXml = [binaryPath, ...binaryArgs].map((a) => ` <string>${a}</string>`).join(`
46504
- `);
46505
- const logDir = join20(homedir3(), "Library/Logs/Locus");
46506
- return `<?xml version="1.0" encoding="UTF-8"?>
46507
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
46508
- <plist version="1.0">
46509
- <dict>
46510
- <key>Label</key>
46511
- <string>${PLIST_LABEL}</string>
46512
- <key>ProgramArguments</key>
46513
- <array>
46514
- ${argsXml}
46515
- </array>
46516
- <key>WorkingDirectory</key>
46517
- <string>${projectPath}</string>
46518
- <key>RunAtLoad</key>
46519
- <true/>
46520
- <key>KeepAlive</key>
46521
- <true/>
46522
- <key>StandardOutPath</key>
46523
- <string>${join20(logDir, "locus.log")}</string>
46524
- <key>StandardErrorPath</key>
46525
- <string>${join20(logDir, "locus-error.log")}</string>
46526
- <key>EnvironmentVariables</key>
46527
- <dict>
46528
- <key>PATH</key>
46529
- <string>${servicePath}</string>
46530
- </dict>
46531
- </dict>
46532
- </plist>
46533
- `;
46534
- }
46535
- async function installLaunchd(projectPath) {
46536
- const plistPath = getPlistPath();
46537
- if (existsSync20(plistPath)) {
46538
- await runShell("launchctl", ["unload", plistPath]);
46539
- }
46540
- const binaryPath = await findBinary();
46541
- if (!binaryPath) {
46542
- console.error(`
46543
- ${c.error("✖")} ${c.bold("Could not find locus-telegram binary.")}
46544
- Install with: ${c.primary("npm install -g @locusai/telegram")}
46489
+ ${c.info("ℹ️")} ${c.bold("Locus is already initialized. Updating configuration...")}
46545
46490
  `);
46546
- process.exit(1);
46547
- }
46548
- const binaryArgs = [];
46549
- if (!await findBinDir("claude")) {
46550
- console.warn(`
46551
- ${c.secondary("⚠")} ${c.bold("Could not find 'claude' CLI in PATH.")}
46552
- The service needs the Claude Code CLI to execute tasks.
46553
- Install with: ${c.primary("npm install -g @anthropic-ai/claude-code")}
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
+ `)}
46554
46512
  `);
46555
- }
46556
- if (!await findBinDir("codex")) {
46557
- console.warn(`
46558
- ${c.secondary("")} ${c.bold("Could not find 'codex' CLI in PATH.")}
46559
- The service needs the Codex CLI if using the Codex provider.
46560
- Install with: ${c.primary("npm install -g @openai/codex")}
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")}
46561
46520
  `);
46562
- }
46563
- const logDir = join20(homedir3(), "Library/Logs/Locus");
46564
- const { mkdirSync: mkdirSync10 } = await import("node:fs");
46565
- mkdirSync10(logDir, { recursive: true });
46566
- const launchAgentsDir = join20(homedir3(), "Library/LaunchAgents");
46567
- mkdirSync10(launchAgentsDir, { recursive: true });
46568
- const servicePath = await buildServicePath();
46569
- const plist = generatePlist(projectPath, binaryPath, binaryArgs, servicePath);
46570
- console.log(`
46571
- ${c.info("▶")} Writing plist to ${c.dim(plistPath)}`);
46572
- writeFileSync9(plistPath, plist, "utf-8");
46573
- console.log(` ${c.info("▶")} Loading service...`);
46574
- const loadResult = await runShell("launchctl", ["load", plistPath]);
46575
- if (loadResult.exitCode !== 0) {
46576
- console.error(`
46577
- ${c.error("✖")} Failed to load service: ${loadResult.stderr.trim()}`);
46578
46521
  return;
46579
46522
  }
46580
- const logPath = join20(logDir, "locus.log");
46523
+ await configManager.init(VERSION2);
46581
46524
  console.log(`
46582
- ${c.success("✔")} ${c.bold("Locus service installed and running!")}
46525
+ ${c.success(" Locus initialized successfully!")}
46583
46526
 
46584
- ${c.bold("Plist:")} ${plistPath}
46585
- ${c.bold("Logs:")} ${logPath}
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")}
46586
46532
 
46587
- ${c.bold("Useful commands:")}
46588
- ${c.dim("$")} ${c.primary(`launchctl list | grep ${PLIST_LABEL}`)}
46589
- ${c.dim("$")} ${c.primary(`tail -f ${logPath}`)}
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")}
46590
46539
  `);
46591
46540
  }
46592
- async function uninstallLaunchd() {
46593
- const plistPath = getPlistPath();
46594
- if (!existsSync20(plistPath)) {
46595
- console.log(`
46596
- ${c.dim("No launchd service found. Nothing to remove.")}
46597
- `);
46598
- return;
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));
46599
46558
  }
46600
- console.log(` ${c.info("▶")} Unloading service...`);
46601
- await runShell("launchctl", ["unload", plistPath]);
46602
- const { unlinkSync: unlinkSync6 } = await import("node:fs");
46603
- unlinkSync6(plistPath);
46604
- console.log(`
46605
- ${c.success("✔")} ${c.bold("Locus service removed.")}
46606
- `);
46559
+ return reviewPrsCommand(args);
46607
46560
  }
46608
- async function statusLaunchd() {
46609
- const plistPath = getPlistPath();
46610
- if (!existsSync20(plistPath)) {
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) {
46611
46616
  console.log(`
46612
- ${c.secondary("●")} ${c.bold("Locus service is not installed")}
46613
- `);
46614
- console.log(` ${c.dim("Install with:")} ${c.primary("locus service install")}
46617
+ ${c.dim("No unreviewed Locus PRs found.")}
46615
46618
  `);
46616
46619
  return;
46617
46620
  }
46618
- const result = await runShell("launchctl", ["list"]);
46619
- const lines = result.stdout.split(`
46620
- `);
46621
- const match = lines.find((l) => l.includes(PLIST_LABEL));
46622
- if (match) {
46623
- const parts = match.trim().split(/\s+/);
46624
- const pid = parts[0] === "-" ? null : parts[0];
46625
- if (pid) {
46626
- console.log(`
46627
- ${c.success("●")} ${c.bold("Locus service is running")} ${c.dim(`(PID ${pid}, launchd)`)}
46628
- `);
46629
- } else {
46630
- console.log(`
46631
- ${c.secondary("●")} ${c.bold("Locus service is stopped")} ${c.dim("(launchd)")}
46632
- `);
46633
- 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...`)}
46634
46623
  `);
46635
- }
46636
- } else {
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;
46637
46639
  console.log(`
46638
- ${c.secondary("●")} ${c.bold("Locus service is not loaded")} ${c.dim("(plist exists but not loaded)")}
46639
- `);
46640
- console.log(` ${c.dim("Load with:")} ${c.primary(`launchctl load ${plistPath}`)}
46641
- `);
46642
- }
46643
- }
46644
- function getPlatform() {
46645
- if (process.platform === "linux")
46646
- return "linux";
46647
- if (process.platform === "darwin")
46648
- return "darwin";
46649
- 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();
46650
46646
  }
46651
- async function installCommand(projectPath) {
46652
- const platform = getPlatform();
46653
- if (!platform) {
46654
- console.error(`
46655
- ${c.error("✖")} ${c.bold(`Unsupported platform: ${process.platform}`)}
46656
- Service management is supported on Linux (systemd) and macOS (launchd).
46657
- `);
46658
- process.exit(1);
46659
- }
46660
- const manager = new SettingsManager(projectPath);
46661
- const settings = manager.load();
46662
- if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
46663
- console.error(`
46664
- ${c.error("✖")} ${c.bold("Telegram is not configured.")}
46665
- Run ${c.primary("locus telegram setup")} first.
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...")}
46666
46684
  `);
46667
- process.exit(1);
46668
- }
46669
- if (!settings.apiKey) {
46670
- console.error(`
46671
- ${c.error("✖")} ${c.bold("API key is not configured.")}
46672
- 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.")}
46673
46688
  `);
46674
- process.exit(1);
46689
+ return;
46675
46690
  }
46676
- if (platform === "linux") {
46677
- await installSystemd(projectPath);
46678
- } else {
46679
- await installLaunchd(projectPath);
46691
+ const reviewsDir = join21(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.reviewsDir);
46692
+ if (!existsSync21(reviewsDir)) {
46693
+ mkdirSync10(reviewsDir, { recursive: true });
46680
46694
  }
46681
- }
46682
- async function uninstallCommand() {
46683
- const platform = getPlatform();
46684
- if (!platform) {
46685
- console.error(`
46686
- ${c.error("")} Unsupported platform: ${process.platform}
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)}
46687
46701
  `);
46688
- process.exit(1);
46689
- }
46690
- if (platform === "linux") {
46691
- await uninstallSystemd();
46692
- } else {
46693
- await uninstallLaunchd();
46694
- }
46695
46702
  }
46696
- async function statusCommandHandler() {
46697
- const platform = getPlatform();
46698
- if (!platform) {
46699
- console.error(`
46700
- ${c.error("✖")} Unsupported platform: ${process.platform}
46701
- `);
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`));
46702
46737
  process.exit(1);
46703
46738
  }
46704
- if (platform === "linux") {
46705
- await statusSystemd();
46706
- } else {
46707
- await statusLaunchd();
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);
46708
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();
46709
46789
  }
46790
+ // src/commands/service.ts
46791
+ init_index_node();
46710
46792
  async function serviceCommand(args) {
46711
- const projectPath = process.cwd();
46712
- requireInitialization(projectPath, "service");
46713
46793
  const subcommand = args[0];
46714
- switch (subcommand) {
46715
- case "install":
46716
- await installCommand(projectPath);
46717
- break;
46718
- case "uninstall":
46719
- await uninstallCommand();
46720
- break;
46721
- case "status":
46722
- await statusCommandHandler();
46723
- break;
46724
- default:
46725
- showServiceHelp();
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);
46726
46806
  }
46727
46807
  }
46728
46808
  // src/commands/telegram.ts
46729
46809
  init_index_node();
46730
46810
  init_settings_manager();
46731
46811
  import { spawn as spawn5 } from "node:child_process";
46732
- import { existsSync as existsSync21 } from "node:fs";
46733
- import { join as join21 } from "node:path";
46812
+ import { existsSync as existsSync22 } from "node:fs";
46813
+ import { join as join22 } from "node:path";
46734
46814
  import { createInterface as createInterface2 } from "node:readline";
46735
46815
  function ask2(question) {
46736
46816
  const rl = createInterface2({
@@ -46756,7 +46836,7 @@ function showTelegramHelp() {
46756
46836
  ${c.primary("locus telegram")} ${c.dim("<subcommand> [options]")}
46757
46837
 
46758
46838
  ${c.header(" SUBCOMMANDS ")}
46759
- ${c.success("run")} Start the Telegram bot
46839
+ ${c.success("start")} Start the Telegram bot
46760
46840
  ${c.success("setup")} Interactive Telegram bot setup (or pass flags below)
46761
46841
  ${c.success("config")} Show current Telegram configuration
46762
46842
  ${c.success("set")} Set a config value
@@ -46765,7 +46845,7 @@ function showTelegramHelp() {
46765
46845
  ${c.success("remove")} Remove Telegram configuration
46766
46846
 
46767
46847
  ${c.header(" EXAMPLES ")}
46768
- ${c.dim("$")} ${c.primary("locus telegram run")}
46848
+ ${c.dim("$")} ${c.primary("locus telegram start")}
46769
46849
  ${c.dim("$")} ${c.primary('locus telegram setup --token "123:ABC" --chat-id 987654')}
46770
46850
  ${c.dim("$")} ${c.primary("locus telegram config")}
46771
46851
  ${c.dim("$")} ${c.primary("locus telegram remove")}
@@ -46776,7 +46856,7 @@ function showTelegramHelp() {
46776
46856
  ${c.primary("locus config set <key> <value>")}
46777
46857
  `);
46778
46858
  }
46779
- async function setupCommand2(args, projectPath) {
46859
+ async function setup(args, projectPath) {
46780
46860
  let token;
46781
46861
  let chatId;
46782
46862
  for (let i = 0;i < args.length; i++) {
@@ -46848,11 +46928,11 @@ async function setupCommand2(args, projectPath) {
46848
46928
  ${c.primary("Chat ID:")} ${parsedChatId}
46849
46929
 
46850
46930
  ${c.bold("Next steps:")}
46851
- Install as service: ${c.primary("locus service install")}
46852
- Or run manually: ${c.primary("locus telegram run")}
46931
+ Start as daemon: ${c.primary("locus daemon start")}
46932
+ Or run manually: ${c.primary("locus telegram start")}
46853
46933
  `);
46854
46934
  }
46855
- function configCommand2(projectPath) {
46935
+ function showConfig(projectPath) {
46856
46936
  const manager = new SettingsManager(projectPath);
46857
46937
  const settings = manager.load();
46858
46938
  const tg = settings.telegram;
@@ -46868,36 +46948,28 @@ function configCommand2(projectPath) {
46868
46948
  console.log(` ${c.dim("File: .locus/settings.json (telegram section)")}
46869
46949
  `);
46870
46950
  const entries = [];
46871
- if (tg.botToken) {
46951
+ if (tg.botToken)
46872
46952
  entries.push(["botToken", maskToken(tg.botToken)]);
46873
- }
46874
- if (tg.chatId) {
46953
+ if (tg.chatId)
46875
46954
  entries.push(["chatId", String(tg.chatId)]);
46876
- }
46877
- if (tg.testMode !== undefined) {
46955
+ if (tg.testMode !== undefined)
46878
46956
  entries.push(["testMode", String(tg.testMode)]);
46879
- }
46880
- if (settings.apiKey) {
46957
+ if (settings.apiKey)
46881
46958
  entries.push(["apiKey (shared)", maskToken(settings.apiKey)]);
46882
- }
46883
- if (settings.apiUrl) {
46959
+ if (settings.apiUrl)
46884
46960
  entries.push(["apiUrl (shared)", settings.apiUrl]);
46885
- }
46886
- if (settings.provider) {
46961
+ if (settings.provider)
46887
46962
  entries.push(["provider (shared)", settings.provider]);
46888
- }
46889
- if (settings.model) {
46963
+ if (settings.model)
46890
46964
  entries.push(["model (shared)", settings.model]);
46891
- }
46892
- if (settings.workspaceId) {
46965
+ if (settings.workspaceId)
46893
46966
  entries.push(["workspaceId (shared)", settings.workspaceId]);
46894
- }
46895
46967
  for (const [key, value] of entries) {
46896
46968
  console.log(` ${c.primary(`${key}:`)} ${value}`);
46897
46969
  }
46898
46970
  console.log("");
46899
46971
  }
46900
- function setCommand2(args, projectPath) {
46972
+ function setValue(args, projectPath) {
46901
46973
  const key = args[0]?.trim();
46902
46974
  const value = args.slice(1).join(" ").trim();
46903
46975
  if (!key || !value) {
@@ -46940,7 +47012,7 @@ function setCommand2(args, projectPath) {
46940
47012
  ${c.success("✔")} Set ${c.primary(key)} = ${displayValue}
46941
47013
  `);
46942
47014
  }
46943
- function removeCommand2(projectPath) {
47015
+ function removeConfig(projectPath) {
46944
47016
  const manager = new SettingsManager(projectPath);
46945
47017
  const settings = manager.load();
46946
47018
  if (!settings.telegram) {
@@ -46955,7 +47027,7 @@ function removeCommand2(projectPath) {
46955
47027
  ${c.success("✔")} ${c.bold("Telegram configuration removed.")}
46956
47028
  `);
46957
47029
  }
46958
- function runBotCommand(projectPath) {
47030
+ function startBot(projectPath) {
46959
47031
  const manager = new SettingsManager(projectPath);
46960
47032
  const settings = manager.load();
46961
47033
  if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
@@ -46965,22 +47037,14 @@ function runBotCommand(projectPath) {
46965
47037
  `);
46966
47038
  process.exit(1);
46967
47039
  }
46968
- const monorepoTelegramEntry = join21(projectPath, "packages/telegram/src/index.ts");
46969
- const isMonorepo = existsSync21(monorepoTelegramEntry);
46970
- let cmd;
46971
- let args;
46972
- if (isMonorepo) {
46973
- cmd = "bun";
46974
- args = ["run", monorepoTelegramEntry];
46975
- } else {
46976
- cmd = "locus-telegram";
46977
- args = [];
46978
- }
46979
- const env = { ...process.env };
46980
- 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, {
46981
47045
  cwd: projectPath,
46982
47046
  stdio: "inherit",
46983
- env
47047
+ env: { ...process.env }
46984
47048
  });
46985
47049
  child.on("error", (err) => {
46986
47050
  if (err.code === "ENOENT" && !isMonorepo) {
@@ -47001,22 +47065,23 @@ function runBotCommand(projectPath) {
47001
47065
  }
47002
47066
  async function telegramCommand(args) {
47003
47067
  const projectPath = process.cwd();
47004
- const subcommand = args[0];
47068
+ const [subcommand, ...subArgs] = args;
47005
47069
  switch (subcommand) {
47070
+ case "start":
47006
47071
  case "run":
47007
- runBotCommand(projectPath);
47072
+ startBot(projectPath);
47008
47073
  break;
47009
47074
  case "setup":
47010
- await setupCommand2(args, projectPath);
47075
+ await setup(subArgs, projectPath);
47011
47076
  break;
47012
47077
  case "config":
47013
- configCommand2(projectPath);
47078
+ showConfig(projectPath);
47014
47079
  break;
47015
47080
  case "set":
47016
- setCommand2(args, projectPath);
47081
+ setValue(subArgs, projectPath);
47017
47082
  break;
47018
47083
  case "remove":
47019
- removeCommand2(projectPath);
47084
+ removeConfig(projectPath);
47020
47085
  break;
47021
47086
  default:
47022
47087
  showTelegramHelp();
@@ -47025,10 +47090,7 @@ async function telegramCommand(args) {
47025
47090
  // src/commands/upgrade.ts
47026
47091
  init_index_node();
47027
47092
  import { execSync as execSync3 } from "node:child_process";
47028
- import { existsSync as existsSync22 } from "node:fs";
47029
47093
  var PACKAGES = ["@locusai/cli", "@locusai/telegram"];
47030
- var SYSTEMD_UNIT_PATH2 = "/etc/systemd/system/locus.service";
47031
- var SYSTEMD_TELEGRAM_UNIT_PATH = "/etc/systemd/system/locus-telegram.service";
47032
47094
  function getInstalledVersion(pkg) {
47033
47095
  try {
47034
47096
  const output = execSync3(`npm list -g ${pkg} --depth=0 --json`, {
@@ -47055,6 +47117,7 @@ async function upgradeCommand() {
47055
47117
  console.log(`
47056
47118
  ${c.header(" UPGRADE ")}
47057
47119
  `);
47120
+ const daemonWasRunning = await isDaemonRunning();
47058
47121
  try {
47059
47122
  console.log(` ${c.dim("◌")} Cleaning npm cache...`);
47060
47123
  execSync3("npm cache clean --force", {
@@ -47066,6 +47129,7 @@ async function upgradeCommand() {
47066
47129
  console.log(` ${c.dim("⚠")} Could not clean npm cache, continuing...
47067
47130
  `);
47068
47131
  }
47132
+ let anyUpdated = false;
47069
47133
  for (const pkg of PACKAGES) {
47070
47134
  const current = getInstalledVersion(pkg);
47071
47135
  const latest = getLatestVersion(pkg);
@@ -47088,33 +47152,26 @@ async function upgradeCommand() {
47088
47152
  });
47089
47153
  console.log(` ${c.success("✔")} ${c.bold(pkg)} updated to ${c.primary(`v${latest}`)}
47090
47154
  `);
47155
+ anyUpdated = true;
47091
47156
  } catch {
47092
47157
  console.error(` ${c.error("✖")} Failed to update ${c.bold(pkg)}. Try manually:
47093
47158
  ` + ` ${c.primary(`npm install -g ${pkg}@latest`)}
47094
47159
  `);
47095
47160
  }
47096
47161
  }
47097
- if (process.platform === "linux") {
47098
- for (const unit of [SYSTEMD_UNIT_PATH2, SYSTEMD_TELEGRAM_UNIT_PATH]) {
47099
- if (!existsSync22(unit))
47100
- continue;
47101
- const split = unit.split("/").pop();
47102
- if (!split) {
47103
- throw "PATH NOTH FOUND";
47104
- }
47105
- const name = split.replace(".service", "");
47106
- try {
47107
- console.log(` ${c.info("▶")} Restarting ${name} service...`);
47108
- execSync3(`systemctl restart ${name}`, {
47109
- stdio: ["pipe", "pipe", "pipe"]
47110
- });
47111
- 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
47112
47167
  `);
47113
- } catch {
47114
- console.log(` ${c.dim("⚠")} Could not restart ${name} service (may need sudo)
47168
+ } else {
47169
+ console.log(` ${c.dim("⚠")} Could not restart daemon (may need sudo)
47115
47170
  `);
47116
- }
47117
47171
  }
47172
+ } else if (daemonWasRunning && !anyUpdated) {
47173
+ console.log(` ${c.dim("No updates — daemon left running")}
47174
+ `);
47118
47175
  }
47119
47176
  console.log("");
47120
47177
  }
@@ -47187,6 +47244,9 @@ async function main() {
47187
47244
  case "config":
47188
47245
  await configCommand(args);
47189
47246
  break;
47247
+ case "daemon":
47248
+ await daemonCommand(args);
47249
+ break;
47190
47250
  case "service":
47191
47251
  await serviceCommand(args);
47192
47252
  break;