@locusai/cli 0.15.4 → 0.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/locus.js +1827 -1785
  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,1972 +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
45000
  }
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
- }
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(`
44930
- `);
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)}
45012
+ process.exit(1);
45013
+ }
45014
+ return platform;
45015
+ }
45016
+ function validateConfig(projectPath) {
45017
+ const manager = new SettingsManager(projectPath);
45018
+ const settings = manager.load();
45019
+ if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
45020
+ console.error(`
45021
+ ${c.error("")} ${c.bold("Telegram is not configured.")}
45022
+ ` + ` Run ${c.primary("locus telegram setup")} first.
44986
45023
  `);
44987
- }
44988
- }
44989
- } 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) {
45090
- console.log(`
45091
- ${c.dim("No insights extracted yet.")}
45092
- `);
45140
+ function generatePlist(projectPath, bins) {
45141
+ const logDir = join19(homedir3(), "Library/Logs/Locus");
45142
+ return `<?xml version="1.0" encoding="UTF-8"?>
45143
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
45144
+ <plist version="1.0">
45145
+ <dict>
45146
+ <key>Label</key>
45147
+ <string>${PLIST_LABEL}</string>
45148
+ <key>ProgramArguments</key>
45149
+ <array>
45150
+ <string>${bins.binaryPath}</string>
45151
+ </array>
45152
+ <key>WorkingDirectory</key>
45153
+ <string>${projectPath}</string>
45154
+ <key>RunAtLoad</key>
45155
+ <true/>
45156
+ <key>KeepAlive</key>
45157
+ <true/>
45158
+ <key>StandardOutPath</key>
45159
+ <string>${join19(logDir, "locus.log")}</string>
45160
+ <key>StandardErrorPath</key>
45161
+ <string>${join19(logDir, "locus-error.log")}</string>
45162
+ <key>EnvironmentVariables</key>
45163
+ <dict>
45164
+ <key>PATH</key>
45165
+ <string>${bins.servicePath}</string>
45166
+ </dict>
45167
+ </dict>
45168
+ </plist>
45169
+ `;
45170
+ }
45171
+ async function startLaunchd(projectPath, bins) {
45172
+ const plistPath = getPlistPath();
45173
+ if (existsSync19(plistPath)) {
45174
+ await runShell("launchctl", ["unload", plistPath]);
45175
+ }
45176
+ const logDir = join19(homedir3(), "Library/Logs/Locus");
45177
+ mkdirSync8(logDir, { recursive: true });
45178
+ mkdirSync8(join19(homedir3(), "Library/LaunchAgents"), { recursive: true });
45179
+ const plist = generatePlist(projectPath, bins);
45180
+ console.log(`
45181
+ ${c.info("▶")} Writing plist to ${c.dim(plistPath)}`);
45182
+ writeFileSync8(plistPath, plist, "utf-8");
45183
+ console.log(` ${c.info("▶")} Loading service...`);
45184
+ const result = await runShell("launchctl", ["load", plistPath]);
45185
+ if (result.exitCode !== 0) {
45186
+ console.error(`
45187
+ ${c.error("✖")} Failed to load service: ${result.stderr.trim()}`);
45093
45188
  return;
45094
45189
  }
45190
+ const logPath = join19(logDir, "locus.log");
45095
45191
  console.log(`
45096
- ${c.header(" INSIGHTS ")} ${c.dim(`(${discussion2.insights.length})`)}
45192
+ ${c.success("")} ${c.bold("Locus daemon started!")}
45193
+
45194
+ ${c.bold("Plist:")} ${plistPath}
45195
+ ${c.bold("Logs:")} ${logPath}
45196
+
45197
+ ${c.bold("Useful commands:")}
45198
+ ${c.dim("$")} ${c.primary(`launchctl list | grep ${PLIST_LABEL}`)}
45199
+ ${c.dim("$")} ${c.primary(`tail -f ${logPath}`)}
45097
45200
  `);
45098
- for (const insight of discussion2.insights) {
45099
- const tag = formatInsightTag(insight.type);
45100
- console.log(` ${tag} ${c.bold(insight.title)}`);
45101
- console.log(` ${c.dim(insight.content)}`);
45102
- if (insight.tags.length > 0) {
45103
- console.log(` ${c.dim(`Tags: ${insight.tags.join(", ")}`)}`);
45104
- }
45105
- console.log("");
45106
- }
45107
45201
  }
45108
- function 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]");
45202
+ async function stopLaunchd() {
45203
+ const plistPath = getPlistPath();
45204
+ if (!existsSync19(plistPath)) {
45205
+ console.log(`
45206
+ ${c.dim("No launchd service found. Nothing to stop.")}
45207
+ `);
45208
+ await killOrphanedProcesses();
45209
+ return;
45120
45210
  }
45121
- }
45122
- function showReplHelp() {
45211
+ console.log(` ${c.info("▶")} Unloading service...`);
45212
+ await runShell("launchctl", ["unload", plistPath]);
45213
+ unlinkSync6(plistPath);
45214
+ await killOrphanedProcesses();
45123
45215
  console.log(`
45124
- ${c.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.")}
45216
+ ${c.success("✔")} ${c.bold("Locus daemon stopped.")}
45140
45217
  `);
45141
45218
  }
45142
- function showDiscussHelp() {
45219
+ async function restartLaunchd() {
45220
+ const plistPath = getPlistPath();
45221
+ if (!existsSync19(plistPath)) {
45222
+ console.log(`
45223
+ ${c.dim("No launchd service found. Use")} ${c.primary("locus daemon start")} ${c.dim("first.")}
45224
+ `);
45225
+ return;
45226
+ }
45227
+ console.log(` ${c.info("▶")} Restarting service...`);
45228
+ await runShell("launchctl", ["unload", plistPath]);
45229
+ const result = await runShell("launchctl", ["load", plistPath]);
45230
+ if (result.exitCode !== 0) {
45231
+ console.error(`
45232
+ ${c.error("✖")} Failed to restart: ${result.stderr.trim()}
45233
+ `);
45234
+ return;
45235
+ }
45143
45236
  console.log(`
45144
- ${c.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")}
45237
+ ${c.success("")} ${c.bold("Locus daemon restarted.")}
45174
45238
  `);
45175
45239
  }
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) {
45240
+ async function statusLaunchd() {
45241
+ const plistPath = getPlistPath();
45242
+ if (!existsSync19(plistPath)) {
45243
+ console.log(`
45244
+ ${c.secondary("●")} ${c.bold("Locus daemon is not installed")}
45245
+ `);
45246
+ console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
45247
+ `);
45248
+ return;
45249
+ }
45250
+ const result = await runShell("launchctl", ["list"]);
45251
+ const match = result.stdout.split(`
45252
+ `).find((l) => l.includes(PLIST_LABEL));
45253
+ if (match) {
45254
+ const parts = match.trim().split(/\s+/);
45255
+ const pid = parts[0] === "-" ? null : parts[0];
45256
+ if (pid) {
45257
+ console.log(`
45258
+ ${c.success("●")} ${c.bold("Locus daemon is running")} ${c.dim(`(PID ${pid}, launchd)`)}
45259
+ `);
45260
+ } else {
45261
+ console.log(`
45262
+ ${c.secondary("●")} ${c.bold("Locus daemon is stopped")} ${c.dim("(launchd)")}
45263
+ `);
45264
+ console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
45265
+ `);
45266
+ }
45267
+ } else {
45268
+ console.log(`
45269
+ ${c.secondary("●")} ${c.bold("Locus daemon is not loaded")} ${c.dim("(plist exists but not loaded)")}
45270
+ `);
45271
+ console.log(` ${c.dim("Start with:")} ${c.primary("locus daemon start")}
45272
+ `);
45273
+ }
45274
+ }
45275
+ async function daemonCommand(args) {
45276
+ const projectPath = process.cwd();
45277
+ requireInitialization(projectPath, "daemon");
45184
45278
  const subcommand = args[0];
45185
- const 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...")}
45267
45380
  `);
45268
- try {
45269
- await fetcher.fetch();
45270
- console.log(`
45271
- ${c.success("✔")} ${c.success("Docs sync complete.")} ${c.dim("Local docs: .locus/documents")}
45381
+ console.log(` ${c.dim("Topic:")} ${c.bold(topic)}`);
45382
+ console.log(` ${c.dim("Model:")} ${c.dim(`${model} (${provider})`)}
45272
45383
  `);
45273
- } catch (error48) {
45274
- console.error(`
45275
- ${c.error("✖")} ${c.red(`Docs sync failed: ${error48 instanceof Error ? error48.message : String(error48)}`)}
45276
- `);
45277
- process.exit(1);
45278
- }
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")}
45384
+ const renderer = new ProgressRenderer;
45385
+ let discussionId;
45386
+ try {
45387
+ renderer.showThinkingStarted();
45388
+ const result = await facilitator.startDiscussion(topic);
45389
+ renderer.showThinkingStopped();
45390
+ discussionId = result.discussion.id;
45391
+ process.stdout.write(`
45291
45392
  `);
45292
- }
45293
- 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
45393
+ process.stdout.write(result.message);
45394
+ process.stdout.write(`
45304
45395
 
45305
- ${c.header(" EXAMPLES ")}
45306
- ${c.dim("$")} ${c.primary("locus docs sync")}
45307
- ${c.dim("$")} ${c.primary("locus docs sync --workspace ws_123")}
45308
45396
  `);
45309
- }
45310
- // 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)}
45397
+ renderer.finalize();
45398
+ } catch (error48) {
45399
+ renderer.finalize();
45400
+ console.error(`
45401
+ ${c.error("✖")} ${c.red("Failed to start discussion:")} ${error48 instanceof Error ? error48.message : String(error48)}
45436
45402
  `);
45403
+ process.exit(1);
45437
45404
  }
45438
- }
45439
-
45440
- // src/commands/exec.ts
45441
- init_progress_renderer();
45442
- init_settings_manager();
45443
- init_utils3();
45444
-
45445
- // src/commands/exec-sessions.ts
45446
- init_index_node();
45447
- function formatRelativeTime(timestamp) {
45448
- const now = Date.now();
45449
- const diff = now - timestamp;
45450
- const seconds = Math.floor(diff / 1000);
45451
- const minutes = Math.floor(seconds / 60);
45452
- const hours = Math.floor(minutes / 60);
45453
- const days = Math.floor(hours / 24);
45454
- if (days > 0) {
45455
- return days === 1 ? "yesterday" : `${days} days ago`;
45456
- }
45457
- if (hours > 0) {
45458
- return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
45459
- }
45460
- if (minutes > 0) {
45461
- return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
45462
- }
45463
- return "just now";
45464
- }
45465
-
45466
- class SessionCommands {
45467
- historyManager;
45468
- constructor(projectPath) {
45469
- this.historyManager = new HistoryManager(projectPath);
45470
- }
45471
- async list() {
45472
- const sessions = this.historyManager.listSessions();
45473
- if (sessions.length === 0) {
45474
- console.log(`
45475
- ${c.dim("No exec sessions found.")}
45405
+ console.log(` ${c.dim("Type your response, or 'help' for commands.")}`);
45406
+ console.log(` ${c.dim("Enter to send, Shift+Enter for newline. Use 'exit' or Ctrl+D to quit.")}
45476
45407
  `);
45477
- 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
  `));
45419
+ inputHandler.stop();
45420
+ process.exit(0);
45421
+ };
45422
+ const handleSubmit = async (input) => {
45423
+ interrupted = false;
45424
+ const trimmed = input.trim();
45425
+ if (trimmed === "") {
45426
+ inputHandler.showPrompt();
45427
+ return;
45495
45428
  }
45496
- }
45497
- async show(sessionId) {
45498
- if (!sessionId) {
45499
- console.error(`
45500
- ${c.error("Error:")} Session ID is required
45429
+ if (!trimmed.includes(`
45430
+ `)) {
45431
+ const lowerInput = trimmed.toLowerCase();
45432
+ if (lowerInput === "help") {
45433
+ showReplHelp();
45434
+ inputHandler.showPrompt();
45435
+ return;
45436
+ }
45437
+ if (lowerInput === "exit" || lowerInput === "quit") {
45438
+ shutdown();
45439
+ return;
45440
+ }
45441
+ if (lowerInput === "insights") {
45442
+ showCurrentInsights(discussionManager, discussionId);
45443
+ inputHandler.showPrompt();
45444
+ return;
45445
+ }
45446
+ if (lowerInput === "summary") {
45447
+ isProcessing = true;
45448
+ const summaryRenderer = new ProgressRenderer;
45449
+ try {
45450
+ summaryRenderer.showThinkingStarted();
45451
+ const summary = await facilitator.summarizeDiscussion(discussionId);
45452
+ summaryRenderer.showThinkingStopped();
45453
+ process.stdout.write(`
45501
45454
  `);
45502
- console.log(` ${c.dim("Usage: locus exec sessions show <session-id>")}
45455
+ process.stdout.write(summary);
45456
+ process.stdout.write(`
45503
45457
  `);
45504
- return;
45505
- }
45506
- const session2 = this.historyManager.findSessionByPartialId(sessionId);
45507
- if (!session2) {
45508
- console.error(`
45509
- ${c.error("Error:")} Session ${c.cyan(sessionId)} not found
45458
+ summaryRenderer.finalize();
45459
+ const discussion2 = discussionManager.load(discussionId);
45460
+ if (discussion2) {
45461
+ console.log(`
45462
+ ${c.success("✔")} ${c.success("Discussion completed!")}
45510
45463
  `);
45511
- console.log(` ${c.dim("Use 'locus exec sessions list' to see available sessions")}
45464
+ console.log(` ${c.dim("Messages:")} ${discussion2.messages.length} ${c.dim("Insights:")} ${discussion2.insights.length}
45512
45465
  `);
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})`)}
45466
+ }
45467
+ console.log(` ${c.dim("To review:")} ${c.cyan(`locus discuss --show ${discussionId}`)}`);
45468
+ console.log(` ${c.dim("To list all:")} ${c.cyan("locus discuss --list")}
45519
45469
  `);
45520
- if (session2.messages.length === 0) {
45521
- console.log(` ${c.dim("(No messages in this session)")}
45470
+ } catch (error48) {
45471
+ summaryRenderer.finalize();
45472
+ console.error(`
45473
+ ${c.error("✖")} ${c.red("Failed to summarize:")} ${error48 instanceof Error ? error48.message : String(error48)}
45522
45474
  `);
45523
- return;
45475
+ }
45476
+ inputHandler.stop();
45477
+ process.exit(0);
45478
+ return;
45479
+ }
45524
45480
  }
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(`
45481
+ const images = detectImages(trimmed);
45482
+ if (images.length > 0) {
45483
+ for (const img of images) {
45484
+ const status = img.exists ? c.success("attached") : c.warning("not found");
45485
+ process.stdout.write(` ${c.cyan(`[Image: ${imageDisplayName(img.path)}]`)} ${status}\r
45486
+ `);
45487
+ }
45488
+ }
45489
+ const cleanedInput = stripImagePaths(trimmed, images);
45490
+ const effectiveInput = cleanedInput + buildImageContext(images);
45491
+ isProcessing = true;
45492
+ const chunkRenderer = new ProgressRenderer;
45493
+ try {
45494
+ chunkRenderer.showThinkingStarted();
45495
+ const stream4 = facilitator.continueDiscussionStream(discussionId, effectiveInput);
45496
+ let result = {
45497
+ response: "",
45498
+ insights: []
45499
+ };
45500
+ let iterResult = await stream4.next();
45501
+ while (!iterResult.done) {
45502
+ chunkRenderer.renderChunk(iterResult.value);
45503
+ iterResult = await stream4.next();
45504
+ }
45505
+ result = iterResult.value;
45506
+ chunkRenderer.finalize();
45507
+ if (result.insights.length > 0) {
45508
+ console.log("");
45509
+ for (const insight of result.insights) {
45510
+ const tag = formatInsightTag(insight.type);
45511
+ console.log(` ${tag} ${c.bold(insight.title)}`);
45512
+ console.log(` ${c.dim(insight.content)}
45532
45513
  `);
45533
- for (const line of lines) {
45534
- console.log(` ${line}`);
45514
+ }
45535
45515
  }
45536
- console.log();
45537
- }
45538
- }
45539
- async delete(sessionId) {
45540
- if (!sessionId) {
45541
- console.error(`
45542
- ${c.error("Error:")} Session ID is required
45543
- `);
45544
- console.log(` ${c.dim("Usage: locus exec sessions delete <session-id>")}
45545
- `);
45546
- return;
45547
- }
45548
- const session2 = this.historyManager.findSessionByPartialId(sessionId);
45549
- if (!session2) {
45516
+ } catch (error48) {
45517
+ chunkRenderer.finalize();
45550
45518
  console.error(`
45551
- ${c.error("Error:")} Session ${c.cyan(sessionId)} not found
45519
+ ${c.error("")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
45552
45520
  `);
45553
- return;
45554
45521
  }
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))}
45559
- `);
45560
- } else {
45561
- console.error(`
45562
- ${c.error("Error:")} Failed to delete session
45563
- `);
45522
+ isProcessing = false;
45523
+ if (!interrupted) {
45524
+ inputHandler.showPrompt();
45564
45525
  }
45565
- }
45566
- async clear() {
45567
- const count = this.historyManager.getSessionCount();
45568
- if (count === 0) {
45569
- console.log(`
45570
- ${c.dim("No sessions to clear.")}
45526
+ };
45527
+ const inputHandler = new InputHandler({
45528
+ prompt: c.cyan("> "),
45529
+ continuationPrompt: c.dim("… "),
45530
+ onSubmit: (input) => {
45531
+ handleSubmit(input).catch((err) => {
45532
+ console.error(`
45533
+ ${c.error("✖")} ${c.red(err instanceof Error ? err.message : String(err))}
45571
45534
  `);
45572
- return;
45573
- }
45574
- const deleted = this.historyManager.clearAllSessions();
45535
+ inputHandler.showPrompt();
45536
+ });
45537
+ },
45538
+ onInterrupt: () => {
45539
+ if (isProcessing) {
45540
+ interrupted = true;
45541
+ aiRunner.abort();
45542
+ isProcessing = false;
45543
+ console.log(c.dim(`
45544
+ [Interrupted]`));
45545
+ inputHandler.showPrompt();
45546
+ } else {
45547
+ shutdown();
45548
+ }
45549
+ },
45550
+ onClose: () => shutdown()
45551
+ });
45552
+ inputHandler.start();
45553
+ inputHandler.showPrompt();
45554
+ }
45555
+ function listDiscussions(discussionManager) {
45556
+ const discussions = discussionManager.list();
45557
+ if (discussions.length === 0) {
45575
45558
  console.log(`
45576
- ${c.success("✔")} Cleared ${deleted} exec session${deleted === 1 ? "" : "s"}
45559
+ ${c.dim("No discussions found.")}
45577
45560
  `);
45578
- }
45579
- getShortId(sessionId) {
45580
- const parts = sessionId.split("-");
45581
- if (parts.length >= 3) {
45582
- return parts.slice(-1)[0].slice(0, 8);
45583
- }
45584
- return sessionId.slice(0, 8);
45585
- }
45586
- }
45587
- function showSessionsHelp() {
45588
- console.log(`
45589
- ${c.primary("Session Commands")}
45590
-
45591
- ${c.success("list")} List recent exec sessions
45592
- ${c.success("show")} ${c.dim("<id>")} Show all messages in a session
45593
- ${c.success("delete")} ${c.dim("<id>")} Delete a specific session
45594
- ${c.success("clear")} Clear all exec sessions
45595
-
45596
- ${c.header(" EXAMPLES ")}
45597
- ${c.dim("$")} locus exec sessions list
45598
- ${c.dim("$")} locus exec sessions show e7f3a2b1
45599
- ${c.dim("$")} locus exec sessions delete e7f3a2b1
45600
- ${c.dim("$")} locus exec sessions clear
45601
-
45602
- ${c.dim("Session IDs can be partial (first 8 characters).")}
45561
+ console.log(` ${c.dim("Start one with:")} ${c.cyan('locus discuss "your topic"')}
45603
45562
  `);
45604
- }
45605
-
45606
- // src/commands/exec.ts
45607
- async function execCommand(args) {
45608
- const { values, positionals } = parseArgs5({
45609
- args,
45610
- options: {
45611
- model: { type: "string" },
45612
- provider: { type: "string" },
45613
- "reasoning-effort": { type: "string" },
45614
- dir: { type: "string" },
45615
- "no-stream": { type: "boolean" },
45616
- "no-status": { type: "boolean" },
45617
- interactive: { type: "boolean", short: "i" },
45618
- session: { type: "string", short: "s" },
45619
- "session-id": { type: "string" },
45620
- "json-stream": { type: "boolean" }
45621
- },
45622
- strict: false
45623
- });
45624
- const jsonStream = values["json-stream"];
45625
- const projectPath = values.dir || process.cwd();
45626
- if (jsonStream) {
45627
- await execJsonStream(values, positionals, projectPath);
45628
45563
  return;
45629
45564
  }
45630
- 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
45583
  }
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!")}
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);
45908
- }
45909
- }
45686
+ ${c.bold("REPL Commands:")}
45687
+ ${c.dim("summary")} Generate final summary and end the discussion
45688
+ ${c.dim("insights")} Show all insights extracted so far
45689
+ ${c.dim("exit")} Save and exit without generating a summary
45690
+ ${c.dim("help")} Show available commands
45910
45691
 
45911
- // src/commands/index-codebase.ts
45692
+ ${c.bold("Examples:")}
45693
+ ${c.dim("# Start a discussion about architecture")}
45694
+ ${c.cyan('locus discuss "how should we structure the auth system?"')}
45695
+
45696
+ ${c.dim("# Review a past discussion")}
45697
+ ${c.cyan("locus discuss --show disc-1234567890")}
45698
+
45699
+ ${c.dim("# List all discussions")}
45700
+ ${c.cyan("locus discuss --list")}
45701
+ `);
45702
+ }
45703
+ // src/commands/docs.ts
45704
+ init_index_node();
45705
+ init_config_manager();
45706
+ init_settings_manager();
45912
45707
  init_utils3();
45913
- async function indexCommand(args) {
45914
- const { values } = parseArgs6({
45708
+ init_workspace_resolver();
45709
+ import { parseArgs as parseArgs4 } from "node:util";
45710
+ async function docsCommand(args) {
45711
+ const subcommand = args[0];
45712
+ const subArgs = args.slice(1);
45713
+ switch (subcommand) {
45714
+ case "sync":
45715
+ await docsSyncCommand(subArgs);
45716
+ break;
45717
+ default:
45718
+ showDocsHelp();
45719
+ break;
45720
+ }
45721
+ }
45722
+ async function docsSyncCommand(args) {
45723
+ const { values } = parseArgs4({
45915
45724
  args,
45916
45725
  options: {
45726
+ "api-key": { type: "string" },
45727
+ "api-url": { type: "string" },
45728
+ workspace: { type: "string" },
45917
45729
  dir: { type: "string" },
45918
- 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();
46064
+ }
46065
+ }
46066
+ async delete(sessionId) {
46067
+ if (!sessionId) {
46068
+ console.error(`
46069
+ ${c.error("Error:")} Session ID is required
46070
+ `);
46071
+ console.log(` ${c.dim("Usage: locus exec sessions delete <session-id>")}
46072
+ `);
46073
+ return;
46074
+ }
46075
+ const session2 = this.historyManager.findSessionByPartialId(sessionId);
46076
+ if (!session2) {
46077
+ console.error(`
46078
+ ${c.error("Error:")} Session ${c.cyan(sessionId)} not found
46079
+ `);
46080
+ return;
46151
46081
  }
46152
- });
46153
- console.log(`
46154
- ${c.primary("\uD83D\uDD0D")} ${c.bold("Reviewing staged changes...")}
46082
+ const deleted = this.historyManager.deleteSession(session2.id);
46083
+ if (deleted) {
46084
+ console.log(`
46085
+ ${c.success("✔")} Deleted session ${c.cyan(this.getShortId(session2.id))}
46155
46086
  `);
46156
- const report = await reviewService.reviewStagedChanges(null);
46157
- if (!report) {
46158
- console.log(` ${c.dim("No changes to review.")}
46087
+ } else {
46088
+ console.error(`
46089
+ ${c.error("Error:")} Failed to delete session
46159
46090
  `);
46160
- return;
46091
+ }
46161
46092
  }
46162
- const reviewsDir = join19(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.reviewsDir);
46163
- if (!existsSync19(reviewsDir)) {
46164
- mkdirSync9(reviewsDir, { recursive: true });
46093
+ async clear() {
46094
+ const count = this.historyManager.getSessionCount();
46095
+ if (count === 0) {
46096
+ console.log(`
46097
+ ${c.dim("No sessions to clear.")}
46098
+ `);
46099
+ return;
46100
+ }
46101
+ const deleted = this.historyManager.clearAllSessions();
46102
+ console.log(`
46103
+ ${c.success("✔")} Cleared ${deleted} exec session${deleted === 1 ? "" : "s"}
46104
+ `);
46165
46105
  }
46166
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
46167
- const reportPath = join19(reviewsDir, `review-${timestamp}.md`);
46168
- writeFileSync8(reportPath, report, "utf-8");
46106
+ getShortId(sessionId) {
46107
+ const parts = sessionId.split("-");
46108
+ if (parts.length >= 3) {
46109
+ return parts.slice(-1)[0].slice(0, 8);
46110
+ }
46111
+ return sessionId.slice(0, 8);
46112
+ }
46113
+ }
46114
+ function showSessionsHelp() {
46169
46115
  console.log(`
46170
- ${c.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`));
46208
- process.exit(1);
46153
+ if (jsonStream) {
46154
+ await execJsonStream(values, positionals, projectPath);
46155
+ return;
46209
46156
  }
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";
46214
- try {
46215
- const resolver = new WorkspaceResolver({
46216
- apiKey,
46217
- apiBase,
46218
- workspaceId: values.workspace
46157
+ requireInitialization(projectPath, "exec");
46158
+ if (positionals[0] === "sessions") {
46159
+ const sessionAction = positionals[1];
46160
+ const sessionArg = positionals[2];
46161
+ const cmds = new SessionCommands(projectPath);
46162
+ switch (sessionAction) {
46163
+ case "list":
46164
+ await cmds.list();
46165
+ return;
46166
+ case "show":
46167
+ await cmds.show(sessionArg);
46168
+ return;
46169
+ case "delete":
46170
+ await cmds.delete(sessionArg);
46171
+ return;
46172
+ case "clear":
46173
+ await cmds.clear();
46174
+ return;
46175
+ default:
46176
+ showSessionsHelp();
46177
+ return;
46178
+ }
46179
+ }
46180
+ const execSettings = new SettingsManager(projectPath).load();
46181
+ const provider = resolveProvider3(values.provider || execSettings.provider);
46182
+ const model = values.model || execSettings.model || DEFAULT_MODEL[provider];
46183
+ const isInteractive = values.interactive;
46184
+ const sessionId = values.session;
46185
+ if (isInteractive) {
46186
+ const { InteractiveSession: InteractiveSession2 } = await Promise.resolve().then(() => (init_interactive_session(), exports_interactive_session));
46187
+ const session2 = new InteractiveSession2({
46188
+ projectPath,
46189
+ provider,
46190
+ model,
46191
+ sessionId
46219
46192
  });
46220
- workspaceId = await resolver.resolve();
46221
- } catch (error48) {
46222
- console.error(c.error(error48 instanceof Error ? error48.message : String(error48)));
46193
+ await session2.start();
46194
+ return;
46195
+ }
46196
+ const promptInput = positionals.join(" ");
46197
+ if (!promptInput) {
46198
+ console.error(c.error('Error: Prompt is required. Usage: locus exec "your prompt" or locus exec --interactive'));
46223
46199
  process.exit(1);
46224
46200
  }
46201
+ const useStreaming = !values["no-stream"];
46225
46202
  const reasoningEffort = values["reasoning-effort"];
46226
- const orchestrator = new AgentOrchestrator({
46227
- workspaceId,
46228
- sprintId: values.sprint || "",
46229
- model,
46230
- provider,
46231
- reasoningEffort,
46232
- apiBase,
46233
- maxIterations: 100,
46203
+ const aiRunner = createAiRunner(provider, {
46234
46204
  projectPath,
46235
- apiKey
46205
+ model,
46206
+ reasoningEffort
46236
46207
  });
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)
46245
- 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);
46251
- };
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;
46208
+ const builder = new PromptBuilder(projectPath);
46209
+ const fullPrompt = await builder.buildGenericPrompt(promptInput);
46210
+ console.log("");
46211
+ console.log(`${c.primary("\uD83D\uDE80")} ${c.bold("Executing prompt with repository context...")}`);
46212
+ console.log("");
46213
+ let responseContent = "";
46214
+ try {
46215
+ if (useStreaming) {
46216
+ const renderer = new ProgressRenderer;
46217
+ const statsTracker = new ExecutionStatsTracker;
46218
+ const stream4 = aiRunner.runStream(fullPrompt);
46219
+ renderer.showThinkingStarted();
46220
+ for await (const chunk of stream4) {
46221
+ switch (chunk.type) {
46222
+ case "text_delta":
46223
+ renderer.renderTextDelta(chunk.content);
46224
+ responseContent += chunk.content;
46225
+ break;
46226
+ case "tool_use":
46227
+ statsTracker.toolStarted(chunk.tool, chunk.id);
46228
+ renderer.showToolStarted(chunk.tool, chunk.id);
46229
+ break;
46230
+ case "thinking":
46231
+ renderer.showThinkingStarted();
46232
+ break;
46233
+ case "tool_result":
46234
+ if (chunk.success) {
46235
+ statsTracker.toolCompleted(chunk.tool, chunk.id);
46236
+ renderer.showToolCompleted(chunk.tool, undefined, chunk.id);
46237
+ } else {
46238
+ statsTracker.toolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
46239
+ renderer.showToolFailed(chunk.tool, chunk.error ?? "Unknown error", chunk.id);
46240
+ }
46241
+ break;
46242
+ case "result":
46243
+ break;
46244
+ case "error": {
46245
+ statsTracker.setError(chunk.error);
46246
+ renderer.renderError(chunk.error);
46247
+ renderer.finalize();
46248
+ const errorStats = statsTracker.finalize();
46249
+ renderer.showSummary(errorStats);
46250
+ console.error(`
46251
+ ${c.error("✖")} ${c.error("Execution failed!")}
46252
+ `);
46253
+ process.exit(1);
46254
+ }
46255
+ }
46256
+ }
46257
+ renderer.finalize();
46258
+ const stats = statsTracker.finalize();
46259
+ renderer.showSummary(stats);
46260
+ } else {
46261
+ responseContent = await aiRunner.run(fullPrompt);
46262
+ console.log(responseContent);
46263
+ }
46264
+ console.log(`
46265
+ ${c.success("✔")} ${c.success("Execution finished!")}
46266
+ `);
46267
+ } catch (error48) {
46268
+ console.error(`
46269
+ ${c.error("✖")} ${c.error("Execution failed:")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}
46270
+ `);
46271
+ process.exit(1);
46272
+ }
46280
46273
  }
46281
- function 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;
46274
+ async function execJsonStream(values, positionals, projectPath) {
46275
+ const sessionId = values["session-id"] ?? values.session ?? randomUUID2();
46276
+ const execSettings = new SettingsManager(projectPath).load();
46277
+ const provider = resolveProvider3(values.provider || execSettings.provider);
46278
+ const model = values.model || execSettings.model || DEFAULT_MODEL[provider];
46279
+ const renderer = new JsonStreamRenderer({
46280
+ sessionId,
46281
+ command: "exec",
46282
+ model,
46283
+ provider,
46284
+ cwd: projectPath
46285
+ });
46286
+ const handleSignal = () => {
46287
+ if (renderer.isDone()) {
46288
+ return;
46289
+ }
46290
+ renderer.emitFatalError("PROCESS_CRASHED", "Process terminated by signal");
46291
+ if (process.stdout.writableNeedDrain) {
46292
+ process.stdout.once("drain", () => process.exit(1));
46293
+ } else {
46294
+ process.exit(1);
46295
+ }
46296
+ };
46297
+ process.on("SIGINT", handleSignal);
46298
+ process.on("SIGTERM", handleSignal);
46287
46299
  try {
46288
- versions2 = readdirSync6(versionsDir).filter((d) => d.startsWith("v"));
46289
- } catch {
46290
- return null;
46291
- }
46292
- if (versions2.length === 0)
46293
- return null;
46294
- const currentNodeVersion = `v${process.versions.node}`;
46295
- const currentBin = join20(versionsDir, currentNodeVersion, "bin");
46296
- if (versions2.includes(currentNodeVersion) && existsSync20(currentBin)) {
46297
- return currentBin;
46298
- }
46299
- const aliasPath = join20(nvmDir, "alias", "default");
46300
- if (existsSync20(aliasPath)) {
46301
46300
  try {
46302
- 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
- async function killOrphanedProcesses() {
46393
- const result = await runShell("pgrep", ["-f", "locus-telegram"]);
46394
- const pids = result.stdout.trim().split(`
46395
- `).filter((p) => p.length > 0);
46396
- if (pids.length === 0)
46397
- return;
46398
- console.log(` ${c.info("▶")} Killing ${pids.length} orphaned locus-telegram process${pids.length > 1 ? "es" : ""}...`);
46399
- await runShell("pkill", ["-f", "locus-telegram"]);
46400
- await new Promise((resolve2) => setTimeout(resolve2, 2000));
46401
- const check2 = await runShell("pgrep", ["-f", "locus-telegram"]);
46402
- if (check2.stdout.trim().length > 0) {
46403
- await runShell("pkill", ["-9", "-f", "locus-telegram"]);
46404
- }
46405
- }
46406
- function generateSystemdUnit(projectPath, user2, binaryPath, servicePath) {
46407
- return `[Unit]
46408
- Description=Locus AI Agent (Telegram bot + proposal scheduler)
46409
- After=network-online.target
46410
- Wants=network-online.target
46411
+ // src/commands/index-codebase.ts
46412
+ init_index_node();
46413
+ init_config_manager();
46414
+ import { parseArgs as parseArgs6 } from "node:util";
46411
46415
 
46412
- [Service]
46413
- Type=simple
46414
- User=${user2}
46415
- WorkingDirectory=${projectPath}
46416
- ExecStart=${binaryPath}
46417
- Restart=on-failure
46418
- RestartSec=10
46419
- Environment=PATH=${servicePath}
46420
- Environment=HOME=${homedir3()}
46416
+ // src/tree-summarizer.ts
46417
+ init_index_node();
46421
46418
 
46422
- [Install]
46423
- WantedBy=multi-user.target
46424
- `;
46425
- }
46426
- async function installSystemd(projectPath) {
46427
- const user2 = process.env.USER || "root";
46428
- const binaryPath = await findBinary();
46429
- if (!binaryPath) {
46430
- console.error(`
46431
- ${c.error("✖")} ${c.bold("Could not find locus-telegram binary.")}
46432
- ` + ` Install with: ${c.primary("npm install -g @locusai/telegram")}
46433
- `);
46434
- process.exit(1);
46435
- }
46436
- if (!await findBinDir("claude")) {
46437
- console.warn(`
46438
- ${c.secondary("⚠")} ${c.bold("Could not find 'claude' CLI in PATH.")}
46439
- ` + ` The service needs the Claude Code CLI to execute tasks.
46440
- ` + ` Install with: ${c.primary("npm install -g @anthropic-ai/claude-code")}
46441
- `);
46419
+ class TreeSummarizer {
46420
+ aiRunner;
46421
+ constructor(aiRunner) {
46422
+ this.aiRunner = aiRunner;
46442
46423
  }
46443
- if (!await findBinDir("codex")) {
46444
- console.warn(`
46445
- ${c.secondary("⚠")} ${c.bold("Could not find 'codex' CLI in PATH.")}
46446
- ` + ` The service needs the Codex CLI if using the Codex provider.
46447
- ` + ` Install with: ${c.primary("npm install -g @openai/codex")}
46448
- `);
46424
+ async summarize(tree) {
46425
+ const prompt = `Analyze this file tree and generate a JSON index.
46426
+ Return ONLY a JSON object with this structure:
46427
+ {
46428
+ "symbols": {},
46429
+ "responsibilities": { "path": "Description" }
46430
+ }
46431
+
46432
+ File Tree:
46433
+ ${tree}`;
46434
+ const output = await this.aiRunner.run(prompt);
46435
+ const jsonStr = extractJsonFromLLMOutput(output);
46436
+ return JSON.parse(jsonStr);
46449
46437
  }
46450
- const servicePath = await buildServicePath();
46451
- const unit = generateSystemdUnit(projectPath, user2, binaryPath, servicePath);
46438
+ }
46439
+
46440
+ // src/commands/index-codebase.ts
46441
+ init_utils3();
46442
+ async function indexCommand(args) {
46443
+ const { values } = parseArgs6({
46444
+ args,
46445
+ options: {
46446
+ dir: { type: "string" },
46447
+ model: { type: "string" },
46448
+ provider: { type: "string" }
46449
+ },
46450
+ strict: false
46451
+ });
46452
+ const projectPath = values.dir || process.cwd();
46453
+ requireInitialization(projectPath, "index");
46454
+ new ConfigManager(projectPath).updateVersion(VERSION2);
46455
+ const provider = resolveProvider3(values.provider);
46456
+ const model = values.model || DEFAULT_MODEL[provider];
46457
+ const aiRunner = createAiRunner(provider, {
46458
+ projectPath,
46459
+ model
46460
+ });
46461
+ const summarizer = new TreeSummarizer(aiRunner);
46462
+ const indexer = new CodebaseIndexer(projectPath);
46452
46463
  console.log(`
46453
- ${c.info("")} Writing systemd unit to ${c.dim(SYSTEMD_UNIT_PATH)}`);
46454
- writeFileSync9(SYSTEMD_UNIT_PATH, unit, "utf-8");
46455
- console.log(` ${c.info("▶")} Reloading systemd daemon...`);
46456
- await runShell("systemctl", ["daemon-reload"]);
46457
- console.log(` ${c.info("▶")} Enabling and starting ${SERVICE_NAME}...`);
46458
- await runShell("systemctl", ["enable", SERVICE_NAME]);
46459
- const startResult = await runShell("systemctl", ["start", SERVICE_NAME]);
46460
- if (startResult.exitCode !== 0) {
46464
+ ${c.step(" INDEX ")} ${c.primary("Analyzing codebase in")} ${c.bold(projectPath)}...`);
46465
+ try {
46466
+ const index = await indexer.index((msg) => console.log(` ${c.dim(msg)}`), (tree) => summarizer.summarize(tree));
46467
+ if (index) {
46468
+ indexer.saveIndex(index);
46469
+ }
46470
+ } catch (error48) {
46461
46471
  console.error(`
46462
- ${c.error("✖")} Failed to start service: ${startResult.stderr.trim()}`);
46463
- console.error(` ${c.dim("Check logs with:")} ${c.primary(`journalctl -u ${SERVICE_NAME} -f`)}`);
46464
- return;
46465
- }
46466
- console.log(`
46467
- ${c.success("✔")} ${c.bold("Locus service installed and running!")}
46468
-
46469
- ${c.bold("Service:")} ${SERVICE_NAME}
46470
- ${c.bold("Unit file:")} ${SYSTEMD_UNIT_PATH}
46471
-
46472
- ${c.bold("Useful commands:")}
46473
- ${c.dim("$")} ${c.primary(`sudo systemctl status ${SERVICE_NAME}`)}
46474
- ${c.dim("$")} ${c.primary(`sudo systemctl restart ${SERVICE_NAME}`)}
46475
- ${c.dim("$")} ${c.primary(`journalctl -u ${SERVICE_NAME} -f`)}
46476
- `);
46477
- }
46478
- async function uninstallSystemd() {
46479
- if (!existsSync20(SYSTEMD_UNIT_PATH)) {
46480
- console.log(`
46481
- ${c.dim("No systemd service found. Nothing to remove.")}
46482
- `);
46483
- await killOrphanedProcesses();
46484
- return;
46472
+ ${c.error("✖")} ${c.error("Indexing failed:")} ${c.red(error48 instanceof Error ? error48.message : String(error48))}`);
46473
+ console.error(c.dim(` The agent might have limited context until indexing succeeds.
46474
+ `));
46485
46475
  }
46486
- console.log(` ${c.info("▶")} Stopping and disabling ${SERVICE_NAME}...`);
46487
- await runShell("systemctl", ["stop", SERVICE_NAME]);
46488
- await runShell("systemctl", ["disable", SERVICE_NAME]);
46489
- const { unlinkSync: unlinkSync6 } = await import("node:fs");
46490
- unlinkSync6(SYSTEMD_UNIT_PATH);
46491
- await runShell("systemctl", ["daemon-reload"]);
46492
- await killOrphanedProcesses();
46493
46476
  console.log(`
46494
- ${c.success("✔")} ${c.bold("Locus service removed.")}
46477
+ ${c.success("✔")} ${c.success("Indexing complete!")}
46495
46478
  `);
46496
46479
  }
46497
- async function statusSystemd() {
46498
- const result = await runShell("systemctl", ["is-active", SERVICE_NAME]);
46499
- const state = result.stdout.trim();
46500
- if (state === "active") {
46501
- console.log(`
46502
- ${c.success("●")} ${c.bold("Locus service is running")} ${c.dim("(systemd)")}
46503
- `);
46504
- } else if (existsSync20(SYSTEMD_UNIT_PATH)) {
46505
- console.log(`
46506
- ${c.secondary("●")} ${c.bold(`Locus service is ${state}`)} ${c.dim("(systemd)")}
46507
- `);
46508
- console.log(` ${c.dim("Start with:")} ${c.primary(`sudo systemctl start ${SERVICE_NAME}`)}
46509
- `);
46510
- } else {
46480
+ // src/commands/init.ts
46481
+ init_index_node();
46482
+ init_config_manager();
46483
+ init_utils3();
46484
+ async function initCommand() {
46485
+ const projectPath = process.cwd();
46486
+ const configManager = new ConfigManager(projectPath);
46487
+ if (isProjectInitialized(projectPath)) {
46511
46488
  console.log(`
46512
- ${c.secondary("")} ${c.bold("Locus service is not installed")}
46513
- `);
46514
- console.log(` ${c.dim("Install with:")} ${c.primary("locus service install")}
46515
- `);
46516
- }
46517
- }
46518
- function generatePlist(projectPath, binaryPath, binaryArgs, servicePath) {
46519
- const argsXml = [binaryPath, ...binaryArgs].map((a) => ` <string>${a}</string>`).join(`
46520
- `);
46521
- const logDir = join20(homedir3(), "Library/Logs/Locus");
46522
- return `<?xml version="1.0" encoding="UTF-8"?>
46523
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
46524
- <plist version="1.0">
46525
- <dict>
46526
- <key>Label</key>
46527
- <string>${PLIST_LABEL}</string>
46528
- <key>ProgramArguments</key>
46529
- <array>
46530
- ${argsXml}
46531
- </array>
46532
- <key>WorkingDirectory</key>
46533
- <string>${projectPath}</string>
46534
- <key>RunAtLoad</key>
46535
- <true/>
46536
- <key>KeepAlive</key>
46537
- <true/>
46538
- <key>StandardOutPath</key>
46539
- <string>${join20(logDir, "locus.log")}</string>
46540
- <key>StandardErrorPath</key>
46541
- <string>${join20(logDir, "locus-error.log")}</string>
46542
- <key>EnvironmentVariables</key>
46543
- <dict>
46544
- <key>PATH</key>
46545
- <string>${servicePath}</string>
46546
- </dict>
46547
- </dict>
46548
- </plist>
46549
- `;
46550
- }
46551
- async function installLaunchd(projectPath) {
46552
- const plistPath = getPlistPath();
46553
- if (existsSync20(plistPath)) {
46554
- await runShell("launchctl", ["unload", plistPath]);
46555
- }
46556
- const binaryPath = await findBinary();
46557
- if (!binaryPath) {
46558
- console.error(`
46559
- ${c.error("✖")} ${c.bold("Could not find locus-telegram binary.")}
46560
- Install with: ${c.primary("npm install -g @locusai/telegram")}
46489
+ ${c.info("ℹ️")} ${c.bold("Locus is already initialized. Updating configuration...")}
46561
46490
  `);
46562
- process.exit(1);
46563
- }
46564
- const binaryArgs = [];
46565
- if (!await findBinDir("claude")) {
46566
- console.warn(`
46567
- ${c.secondary("⚠")} ${c.bold("Could not find 'claude' CLI in PATH.")}
46568
- The service needs the Claude Code CLI to execute tasks.
46569
- 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
+ `)}
46570
46512
  `);
46571
- }
46572
- if (!await findBinDir("codex")) {
46573
- console.warn(`
46574
- ${c.secondary("")} ${c.bold("Could not find 'codex' CLI in PATH.")}
46575
- The service needs the Codex CLI if using the Codex provider.
46576
- 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")}
46577
46520
  `);
46578
- }
46579
- const logDir = join20(homedir3(), "Library/Logs/Locus");
46580
- const { mkdirSync: mkdirSync10 } = await import("node:fs");
46581
- mkdirSync10(logDir, { recursive: true });
46582
- const launchAgentsDir = join20(homedir3(), "Library/LaunchAgents");
46583
- mkdirSync10(launchAgentsDir, { recursive: true });
46584
- const servicePath = await buildServicePath();
46585
- const plist = generatePlist(projectPath, binaryPath, binaryArgs, servicePath);
46586
- console.log(`
46587
- ${c.info("▶")} Writing plist to ${c.dim(plistPath)}`);
46588
- writeFileSync9(plistPath, plist, "utf-8");
46589
- console.log(` ${c.info("▶")} Loading service...`);
46590
- const loadResult = await runShell("launchctl", ["load", plistPath]);
46591
- if (loadResult.exitCode !== 0) {
46592
- console.error(`
46593
- ${c.error("✖")} Failed to load service: ${loadResult.stderr.trim()}`);
46594
46521
  return;
46595
46522
  }
46596
- const logPath = join20(logDir, "locus.log");
46523
+ await configManager.init(VERSION2);
46597
46524
  console.log(`
46598
- ${c.success("✔")} ${c.bold("Locus service installed and running!")}
46525
+ ${c.success(" Locus initialized successfully!")}
46599
46526
 
46600
- ${c.bold("Plist:")} ${plistPath}
46601
- ${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")}
46602
46532
 
46603
- ${c.bold("Useful commands:")}
46604
- ${c.dim("$")} ${c.primary(`launchctl list | grep ${PLIST_LABEL}`)}
46605
- ${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")}
46606
46539
  `);
46607
46540
  }
46608
- async function uninstallLaunchd() {
46609
- const plistPath = getPlistPath();
46610
- if (!existsSync20(plistPath)) {
46611
- console.log(`
46612
- ${c.dim("No launchd service found. Nothing to remove.")}
46613
- `);
46614
- await killOrphanedProcesses();
46615
- 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));
46616
46558
  }
46617
- console.log(` ${c.info("▶")} Unloading service...`);
46618
- await runShell("launchctl", ["unload", plistPath]);
46619
- const { unlinkSync: unlinkSync6 } = await import("node:fs");
46620
- unlinkSync6(plistPath);
46621
- await killOrphanedProcesses();
46622
- console.log(`
46623
- ${c.success("✔")} ${c.bold("Locus service removed.")}
46624
- `);
46559
+ return reviewPrsCommand(args);
46625
46560
  }
46626
- async function statusLaunchd() {
46627
- const plistPath = getPlistPath();
46628
- 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) {
46629
46616
  console.log(`
46630
- ${c.secondary("●")} ${c.bold("Locus service is not installed")}
46631
- `);
46632
- console.log(` ${c.dim("Install with:")} ${c.primary("locus service install")}
46617
+ ${c.dim("No unreviewed Locus PRs found.")}
46633
46618
  `);
46634
46619
  return;
46635
46620
  }
46636
- const result = await runShell("launchctl", ["list"]);
46637
- const lines = result.stdout.split(`
46638
- `);
46639
- const match = lines.find((l) => l.includes(PLIST_LABEL));
46640
- if (match) {
46641
- const parts = match.trim().split(/\s+/);
46642
- const pid = parts[0] === "-" ? null : parts[0];
46643
- if (pid) {
46644
- console.log(`
46645
- ${c.success("●")} ${c.bold("Locus service is running")} ${c.dim(`(PID ${pid}, launchd)`)}
46646
- `);
46647
- } else {
46648
- console.log(`
46649
- ${c.secondary("●")} ${c.bold("Locus service is stopped")} ${c.dim("(launchd)")}
46650
- `);
46651
- console.log(` ${c.dim("Start with:")} ${c.primary(`launchctl load ${plistPath}`)}
46621
+ console.log(`
46622
+ ${c.primary("\uD83D\uDD0D")} ${c.bold(`Found ${unreviewedPrs.length} unreviewed PR(s). Starting reviewer...`)}
46652
46623
  `);
46653
- }
46654
- } 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;
46655
46639
  console.log(`
46656
- ${c.secondary("●")} ${c.bold("Locus service is not loaded")} ${c.dim("(plist exists but not loaded)")}
46657
- `);
46658
- console.log(` ${c.dim("Load with:")} ${c.primary(`launchctl load ${plistPath}`)}
46659
- `);
46660
- }
46661
- }
46662
- function getPlatform() {
46663
- if (process.platform === "linux")
46664
- return "linux";
46665
- if (process.platform === "darwin")
46666
- return "darwin";
46667
- return null;
46640
+ ${c.info("Received shutdown signal. Stopping reviewer...")}`);
46641
+ process.exit(0);
46642
+ };
46643
+ process.on("SIGINT", handleSignal);
46644
+ process.on("SIGTERM", handleSignal);
46645
+ await reviewer.run();
46668
46646
  }
46669
- async function installCommand(projectPath) {
46670
- const platform = getPlatform();
46671
- if (!platform) {
46672
- console.error(`
46673
- ${c.error("✖")} ${c.bold(`Unsupported platform: ${process.platform}`)}
46674
- Service management is supported on Linux (systemd) and macOS (launchd).
46675
- `);
46676
- process.exit(1);
46677
- }
46678
- const manager = new SettingsManager(projectPath);
46679
- const settings = manager.load();
46680
- if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
46681
- console.error(`
46682
- ${c.error("✖")} ${c.bold("Telegram is not configured.")}
46683
- 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...")}
46684
46684
  `);
46685
- process.exit(1);
46686
- }
46687
- if (!settings.apiKey) {
46688
- console.error(`
46689
- ${c.error("✖")} ${c.bold("API key is not configured.")}
46690
- Run ${c.primary("locus config setup --api-key <key>")} first.
46685
+ const report = await reviewService.reviewStagedChanges(null);
46686
+ if (!report) {
46687
+ console.log(` ${c.dim("No changes to review.")}
46691
46688
  `);
46692
- process.exit(1);
46689
+ return;
46693
46690
  }
46694
- if (platform === "linux") {
46695
- await installSystemd(projectPath);
46696
- } else {
46697
- await installLaunchd(projectPath);
46691
+ const reviewsDir = join21(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.reviewsDir);
46692
+ if (!existsSync21(reviewsDir)) {
46693
+ mkdirSync10(reviewsDir, { recursive: true });
46698
46694
  }
46699
- }
46700
- async function uninstallCommand() {
46701
- const platform = getPlatform();
46702
- if (!platform) {
46703
- console.error(`
46704
- ${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)}
46705
46701
  `);
46706
- process.exit(1);
46707
- }
46708
- if (platform === "linux") {
46709
- await uninstallSystemd();
46710
- } else {
46711
- await uninstallLaunchd();
46712
- }
46713
46702
  }
46714
- async function statusCommandHandler() {
46715
- const platform = getPlatform();
46716
- if (!platform) {
46717
- console.error(`
46718
- ${c.error("✖")} Unsupported platform: ${process.platform}
46719
- `);
46703
+ // src/commands/run.ts
46704
+ init_index_node();
46705
+ init_config_manager();
46706
+ init_settings_manager();
46707
+ init_utils3();
46708
+ init_workspace_resolver();
46709
+ import { parseArgs as parseArgs8 } from "node:util";
46710
+ async function runCommand(args) {
46711
+ const { values } = parseArgs8({
46712
+ args,
46713
+ options: {
46714
+ "api-key": { type: "string" },
46715
+ workspace: { type: "string" },
46716
+ sprint: { type: "string" },
46717
+ model: { type: "string" },
46718
+ provider: { type: "string" },
46719
+ "reasoning-effort": { type: "string" },
46720
+ "skip-planning": { type: "boolean" },
46721
+ "api-url": { type: "string" },
46722
+ dir: { type: "string" }
46723
+ },
46724
+ strict: false
46725
+ });
46726
+ const projectPath = values.dir || process.cwd();
46727
+ requireInitialization(projectPath, "run");
46728
+ const configManager = new ConfigManager(projectPath);
46729
+ configManager.updateVersion(VERSION2);
46730
+ const settingsManager = new SettingsManager(projectPath);
46731
+ const settings = settingsManager.load();
46732
+ const apiKey = values["api-key"] || settings.apiKey;
46733
+ if (!apiKey) {
46734
+ console.error(c.error("Error: API key is required"));
46735
+ console.error(c.dim(`Configure with: locus config setup --api-key <key>
46736
+ Or pass --api-key flag`));
46720
46737
  process.exit(1);
46721
46738
  }
46722
- if (platform === "linux") {
46723
- await statusSystemd();
46724
- } else {
46725
- 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);
46726
46753
  }
46754
+ const reasoningEffort = values["reasoning-effort"];
46755
+ const orchestrator = new AgentOrchestrator({
46756
+ workspaceId,
46757
+ sprintId: values.sprint || "",
46758
+ model,
46759
+ provider,
46760
+ reasoningEffort,
46761
+ apiBase,
46762
+ maxIterations: 100,
46763
+ projectPath,
46764
+ apiKey
46765
+ });
46766
+ orchestrator.on("agent:spawned", (data) => console.log(` ${c.info("●")} ${c.bold("Agent spawned:")} ${data.agentId}`));
46767
+ orchestrator.on("task:assigned", (data) => console.log(` ${c.info("●")} ${c.bold("Claimed:")} ${data.title}`));
46768
+ orchestrator.on("task:completed", (data) => console.log(` ${c.success("✔")} ${c.success("Completed:")} ${c.dim(data.taskId)}`));
46769
+ orchestrator.on("task:failed", (data) => console.log(` ${c.error("✖")} ${c.error("Failed:")} ${c.bold(data.taskId)}: ${data.error}`));
46770
+ orchestrator.on("agent:stale", (data) => console.log(` ${c.error("⚠")} ${c.error("Stale agent killed:")} ${data.agentId}`));
46771
+ let isShuttingDown = false;
46772
+ const handleSignal = async (signal) => {
46773
+ if (isShuttingDown)
46774
+ return;
46775
+ isShuttingDown = true;
46776
+ console.log(`
46777
+ ${c.info(`Received ${signal}. Stopping agent and cleaning up...`)}`);
46778
+ await orchestrator.stop();
46779
+ process.exit(0);
46780
+ };
46781
+ process.on("SIGINT", () => handleSignal("SIGINT"));
46782
+ process.on("SIGTERM", () => handleSignal("SIGTERM"));
46783
+ console.log(`
46784
+ ${c.primary("\uD83D\uDE80")} ${c.bold("Starting agent in")} ${c.primary(projectPath)}...`);
46785
+ console.log(` ${c.dim("Tasks will be executed sequentially on a single branch")}`);
46786
+ console.log(` ${c.dim("Changes will be committed and pushed after each task")}`);
46787
+ console.log(` ${c.dim("A PR will be opened when all tasks are done")}`);
46788
+ await orchestrator.start();
46727
46789
  }
46790
+ // src/commands/service.ts
46791
+ init_index_node();
46728
46792
  async function serviceCommand(args) {
46729
- const projectPath = process.cwd();
46730
- requireInitialization(projectPath, "service");
46731
46793
  const subcommand = args[0];
46732
- switch (subcommand) {
46733
- case "install":
46734
- await installCommand(projectPath);
46735
- break;
46736
- case "uninstall":
46737
- await uninstallCommand();
46738
- break;
46739
- case "status":
46740
- await statusCommandHandler();
46741
- break;
46742
- default:
46743
- 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);
46744
46806
  }
46745
46807
  }
46746
46808
  // src/commands/telegram.ts
46747
46809
  init_index_node();
46748
46810
  init_settings_manager();
46749
46811
  import { spawn as spawn5 } from "node:child_process";
46750
- import { existsSync as existsSync21 } from "node:fs";
46751
- import { join as join21 } from "node:path";
46812
+ import { existsSync as existsSync22 } from "node:fs";
46813
+ import { join as join22 } from "node:path";
46752
46814
  import { createInterface as createInterface2 } from "node:readline";
46753
46815
  function ask2(question) {
46754
46816
  const rl = createInterface2({
@@ -46774,7 +46836,7 @@ function showTelegramHelp() {
46774
46836
  ${c.primary("locus telegram")} ${c.dim("<subcommand> [options]")}
46775
46837
 
46776
46838
  ${c.header(" SUBCOMMANDS ")}
46777
- ${c.success("run")} Start the Telegram bot
46839
+ ${c.success("start")} Start the Telegram bot
46778
46840
  ${c.success("setup")} Interactive Telegram bot setup (or pass flags below)
46779
46841
  ${c.success("config")} Show current Telegram configuration
46780
46842
  ${c.success("set")} Set a config value
@@ -46783,7 +46845,7 @@ function showTelegramHelp() {
46783
46845
  ${c.success("remove")} Remove Telegram configuration
46784
46846
 
46785
46847
  ${c.header(" EXAMPLES ")}
46786
- ${c.dim("$")} ${c.primary("locus telegram run")}
46848
+ ${c.dim("$")} ${c.primary("locus telegram start")}
46787
46849
  ${c.dim("$")} ${c.primary('locus telegram setup --token "123:ABC" --chat-id 987654')}
46788
46850
  ${c.dim("$")} ${c.primary("locus telegram config")}
46789
46851
  ${c.dim("$")} ${c.primary("locus telegram remove")}
@@ -46794,7 +46856,7 @@ function showTelegramHelp() {
46794
46856
  ${c.primary("locus config set <key> <value>")}
46795
46857
  `);
46796
46858
  }
46797
- async function setupCommand2(args, projectPath) {
46859
+ async function setup(args, projectPath) {
46798
46860
  let token;
46799
46861
  let chatId;
46800
46862
  for (let i = 0;i < args.length; i++) {
@@ -46866,11 +46928,11 @@ async function setupCommand2(args, projectPath) {
46866
46928
  ${c.primary("Chat ID:")} ${parsedChatId}
46867
46929
 
46868
46930
  ${c.bold("Next steps:")}
46869
- Install as service: ${c.primary("locus service install")}
46870
- 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")}
46871
46933
  `);
46872
46934
  }
46873
- function configCommand2(projectPath) {
46935
+ function showConfig(projectPath) {
46874
46936
  const manager = new SettingsManager(projectPath);
46875
46937
  const settings = manager.load();
46876
46938
  const tg = settings.telegram;
@@ -46886,36 +46948,28 @@ function configCommand2(projectPath) {
46886
46948
  console.log(` ${c.dim("File: .locus/settings.json (telegram section)")}
46887
46949
  `);
46888
46950
  const entries = [];
46889
- if (tg.botToken) {
46951
+ if (tg.botToken)
46890
46952
  entries.push(["botToken", maskToken(tg.botToken)]);
46891
- }
46892
- if (tg.chatId) {
46953
+ if (tg.chatId)
46893
46954
  entries.push(["chatId", String(tg.chatId)]);
46894
- }
46895
- if (tg.testMode !== undefined) {
46955
+ if (tg.testMode !== undefined)
46896
46956
  entries.push(["testMode", String(tg.testMode)]);
46897
- }
46898
- if (settings.apiKey) {
46957
+ if (settings.apiKey)
46899
46958
  entries.push(["apiKey (shared)", maskToken(settings.apiKey)]);
46900
- }
46901
- if (settings.apiUrl) {
46959
+ if (settings.apiUrl)
46902
46960
  entries.push(["apiUrl (shared)", settings.apiUrl]);
46903
- }
46904
- if (settings.provider) {
46961
+ if (settings.provider)
46905
46962
  entries.push(["provider (shared)", settings.provider]);
46906
- }
46907
- if (settings.model) {
46963
+ if (settings.model)
46908
46964
  entries.push(["model (shared)", settings.model]);
46909
- }
46910
- if (settings.workspaceId) {
46965
+ if (settings.workspaceId)
46911
46966
  entries.push(["workspaceId (shared)", settings.workspaceId]);
46912
- }
46913
46967
  for (const [key, value] of entries) {
46914
46968
  console.log(` ${c.primary(`${key}:`)} ${value}`);
46915
46969
  }
46916
46970
  console.log("");
46917
46971
  }
46918
- function setCommand2(args, projectPath) {
46972
+ function setValue(args, projectPath) {
46919
46973
  const key = args[0]?.trim();
46920
46974
  const value = args.slice(1).join(" ").trim();
46921
46975
  if (!key || !value) {
@@ -46958,7 +47012,7 @@ function setCommand2(args, projectPath) {
46958
47012
  ${c.success("✔")} Set ${c.primary(key)} = ${displayValue}
46959
47013
  `);
46960
47014
  }
46961
- function removeCommand2(projectPath) {
47015
+ function removeConfig(projectPath) {
46962
47016
  const manager = new SettingsManager(projectPath);
46963
47017
  const settings = manager.load();
46964
47018
  if (!settings.telegram) {
@@ -46973,7 +47027,7 @@ function removeCommand2(projectPath) {
46973
47027
  ${c.success("✔")} ${c.bold("Telegram configuration removed.")}
46974
47028
  `);
46975
47029
  }
46976
- function runBotCommand(projectPath) {
47030
+ function startBot(projectPath) {
46977
47031
  const manager = new SettingsManager(projectPath);
46978
47032
  const settings = manager.load();
46979
47033
  if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
@@ -46983,22 +47037,14 @@ function runBotCommand(projectPath) {
46983
47037
  `);
46984
47038
  process.exit(1);
46985
47039
  }
46986
- const monorepoTelegramEntry = join21(projectPath, "packages/telegram/src/index.ts");
46987
- const isMonorepo = existsSync21(monorepoTelegramEntry);
46988
- let cmd;
46989
- let args;
46990
- if (isMonorepo) {
46991
- cmd = "bun";
46992
- args = ["run", monorepoTelegramEntry];
46993
- } else {
46994
- cmd = "locus-telegram";
46995
- args = [];
46996
- }
46997
- const env = { ...process.env };
46998
- const child = spawn5(cmd, args, {
47040
+ const monorepoEntry = join22(projectPath, "packages/telegram/src/index.ts");
47041
+ const isMonorepo = existsSync22(monorepoEntry);
47042
+ const cmd = isMonorepo ? "bun" : "locus-telegram";
47043
+ const cmdArgs = isMonorepo ? ["run", monorepoEntry] : [];
47044
+ const child = spawn5(cmd, cmdArgs, {
46999
47045
  cwd: projectPath,
47000
47046
  stdio: "inherit",
47001
- env
47047
+ env: { ...process.env }
47002
47048
  });
47003
47049
  child.on("error", (err) => {
47004
47050
  if (err.code === "ENOENT" && !isMonorepo) {
@@ -47019,22 +47065,23 @@ function runBotCommand(projectPath) {
47019
47065
  }
47020
47066
  async function telegramCommand(args) {
47021
47067
  const projectPath = process.cwd();
47022
- const subcommand = args[0];
47068
+ const [subcommand, ...subArgs] = args;
47023
47069
  switch (subcommand) {
47070
+ case "start":
47024
47071
  case "run":
47025
- runBotCommand(projectPath);
47072
+ startBot(projectPath);
47026
47073
  break;
47027
47074
  case "setup":
47028
- await setupCommand2(args, projectPath);
47075
+ await setup(subArgs, projectPath);
47029
47076
  break;
47030
47077
  case "config":
47031
- configCommand2(projectPath);
47078
+ showConfig(projectPath);
47032
47079
  break;
47033
47080
  case "set":
47034
- setCommand2(args, projectPath);
47081
+ setValue(subArgs, projectPath);
47035
47082
  break;
47036
47083
  case "remove":
47037
- removeCommand2(projectPath);
47084
+ removeConfig(projectPath);
47038
47085
  break;
47039
47086
  default:
47040
47087
  showTelegramHelp();
@@ -47043,10 +47090,7 @@ async function telegramCommand(args) {
47043
47090
  // src/commands/upgrade.ts
47044
47091
  init_index_node();
47045
47092
  import { execSync as execSync3 } from "node:child_process";
47046
- import { existsSync as existsSync22 } from "node:fs";
47047
47093
  var PACKAGES = ["@locusai/cli", "@locusai/telegram"];
47048
- var SYSTEMD_UNIT_PATH2 = "/etc/systemd/system/locus.service";
47049
- var SYSTEMD_TELEGRAM_UNIT_PATH = "/etc/systemd/system/locus-telegram.service";
47050
47094
  function getInstalledVersion(pkg) {
47051
47095
  try {
47052
47096
  const output = execSync3(`npm list -g ${pkg} --depth=0 --json`, {
@@ -47073,6 +47117,7 @@ async function upgradeCommand() {
47073
47117
  console.log(`
47074
47118
  ${c.header(" UPGRADE ")}
47075
47119
  `);
47120
+ const daemonWasRunning = await isDaemonRunning();
47076
47121
  try {
47077
47122
  console.log(` ${c.dim("◌")} Cleaning npm cache...`);
47078
47123
  execSync3("npm cache clean --force", {
@@ -47084,6 +47129,7 @@ async function upgradeCommand() {
47084
47129
  console.log(` ${c.dim("⚠")} Could not clean npm cache, continuing...
47085
47130
  `);
47086
47131
  }
47132
+ let anyUpdated = false;
47087
47133
  for (const pkg of PACKAGES) {
47088
47134
  const current = getInstalledVersion(pkg);
47089
47135
  const latest = getLatestVersion(pkg);
@@ -47106,33 +47152,26 @@ async function upgradeCommand() {
47106
47152
  });
47107
47153
  console.log(` ${c.success("✔")} ${c.bold(pkg)} updated to ${c.primary(`v${latest}`)}
47108
47154
  `);
47155
+ anyUpdated = true;
47109
47156
  } catch {
47110
47157
  console.error(` ${c.error("✖")} Failed to update ${c.bold(pkg)}. Try manually:
47111
47158
  ` + ` ${c.primary(`npm install -g ${pkg}@latest`)}
47112
47159
  `);
47113
47160
  }
47114
47161
  }
47115
- if (process.platform === "linux") {
47116
- for (const unit of [SYSTEMD_UNIT_PATH2, SYSTEMD_TELEGRAM_UNIT_PATH]) {
47117
- if (!existsSync22(unit))
47118
- continue;
47119
- const split = unit.split("/").pop();
47120
- if (!split) {
47121
- throw "PATH NOTH FOUND";
47122
- }
47123
- const name = split.replace(".service", "");
47124
- try {
47125
- console.log(` ${c.info("▶")} Restarting ${name} service...`);
47126
- execSync3(`systemctl restart ${name}`, {
47127
- stdio: ["pipe", "pipe", "pipe"]
47128
- });
47129
- console.log(` ${c.success("✔")} ${name} service restarted
47162
+ if (daemonWasRunning && anyUpdated) {
47163
+ console.log(` ${c.info("▶")} Restarting locus daemon...`);
47164
+ const restarted = await restartDaemonIfRunning();
47165
+ if (restarted) {
47166
+ console.log(` ${c.success("")} Locus daemon restarted
47130
47167
  `);
47131
- } catch {
47132
- 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)
47133
47170
  `);
47134
- }
47135
47171
  }
47172
+ } else if (daemonWasRunning && !anyUpdated) {
47173
+ console.log(` ${c.dim("No updates — daemon left running")}
47174
+ `);
47136
47175
  }
47137
47176
  console.log("");
47138
47177
  }
@@ -47205,6 +47244,9 @@ async function main() {
47205
47244
  case "config":
47206
47245
  await configCommand(args);
47207
47246
  break;
47247
+ case "daemon":
47248
+ await daemonCommand(args);
47249
+ break;
47208
47250
  case "service":
47209
47251
  await serviceCommand(args);
47210
47252
  break;