leglas 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,12 +66,15 @@ and sessions that clean up after themselves.
66
66
  precise request for your agent, file path included. The composer
67
67
  carries its own agent picker, the way every chat you already use
68
68
  carries a model picker: the CLIs found on your machine (Claude Code,
69
- Codex, Cursor) are one click away next to the send button, each asked
70
- for its login status so a signed-out CLI says so before a run instead
71
- of failing one. Not on that list? Type the command that runs your
72
- agent once, in the same menu, and Leglas hands it each request; or
73
- connect an agent Leglas can't spawn (an IDE panel, an MCP host) with
74
- the copy-paste MCP wiring behind "Connect another agent". Each run
69
+ Codex, Cursor) are one click away next to the send button. Leglas checks
70
+ your shell path and the conventional per-user install locations, then asks
71
+ each CLI for its login status so a signed-out agent says so before a run
72
+ instead of failing one. Claude Code and Codex can optionally override effort
73
+ for this project, remembered separately for each agent; `Agent default`
74
+ leaves the CLI's own setting untouched. To
75
+ work from an IDE panel or chat host, choose
76
+ "Connect agent via MCP" for the exact setup and a live confirmation
77
+ once the agent uses a Leglas tool. Each run
75
78
  reports in a card above the field: who is working, what file they
76
79
  are touching, how long it has been, a stop button while it runs and
77
80
  retry when it fails. Your agent, your subscription, no keys.
@@ -207,6 +210,13 @@ wrong. `npx leglas watch` in another terminal is the same loop with the
207
210
  agent's own output in view, and it needs no flag once an agent has been
208
211
  picked in the interface.
209
212
 
213
+ To use a CLI that is not in the picker, keep the custom command explicit
214
+ in the terminal:
215
+
216
+ ```sh
217
+ npx leglas watch --run "my-agent {prompt}"
218
+ ```
219
+
210
220
  ### MCP server
211
221
 
212
222
  For agent hosts that cannot run shell commands, `leglas-mcp` exposes the
package/dist/bin.js CHANGED
@@ -587,9 +587,12 @@ function nextRequest(requests, failed) {
587
587
 
588
588
  // ../server/dist/agents.js
589
589
  import { spawn } from "child_process";
590
- import { constants } from "fs";
590
+ import { constants, readdirSync } from "fs";
591
591
  import { access, mkdir, readFile, writeFile } from "fs/promises";
592
592
  import { delimiter, dirname, isAbsolute, join, relative } from "path";
593
+ var AGENT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
594
+ var effortFlag = (effort) => effort === null ? [] : ["--effort", effort];
595
+ var codexEffortConfig = (effort) => effort === null ? [] : ["-c", `model_reasoning_effort=${effort}`];
593
596
  var CODEX_WORKSPACE_CONFIG = [
594
597
  "-c",
595
598
  "sandbox_workspace_write.network_access=true"
@@ -598,22 +601,25 @@ var KNOWN_AGENTS = {
598
601
  claude: {
599
602
  name: "Claude",
600
603
  binary: "claude",
601
- args: (prompt) => [
604
+ efforts: AGENT_EFFORTS,
605
+ args: (prompt, effort = null) => [
602
606
  "-p",
603
607
  prompt,
604
608
  "--output-format",
605
609
  "stream-json",
606
610
  "--verbose",
607
611
  "--permission-mode",
608
- "acceptEdits"
612
+ "acceptEdits",
613
+ ...effortFlag(effort)
609
614
  ],
610
- terminalArgs: (prompt) => [
615
+ terminalArgs: (prompt, effort = null) => [
611
616
  "-p",
612
617
  prompt,
613
618
  "--permission-mode",
614
- "acceptEdits"
619
+ "acceptEdits",
620
+ ...effortFlag(effort)
615
621
  ],
616
- resumeArgs: (sessionId, prompt) => [
622
+ resumeArgs: (sessionId, prompt, effort = null) => [
617
623
  "-p",
618
624
  "--resume",
619
625
  sessionId,
@@ -622,7 +628,8 @@ var KNOWN_AGENTS = {
622
628
  "stream-json",
623
629
  "--verbose",
624
630
  "--permission-mode",
625
- "acceptEdits"
631
+ "acceptEdits",
632
+ ...effortFlag(effort)
626
633
  ],
627
634
  // Non-interactive Claude cannot approve a Bash call: acceptEdits covers
628
635
  // files, so a command the prompt requires is refused every time with
@@ -650,6 +657,7 @@ var KNOWN_AGENTS = {
650
657
  codex: {
651
658
  name: "Codex",
652
659
  binary: "codex",
660
+ efforts: AGENT_EFFORTS,
653
661
  // `--skip-git-repo-check` is what lets Codex run at all in a project the
654
662
  // user never put under version control: without it codex-cli refuses
655
663
  // before it reaches a model, with "Not inside a trusted directory and
@@ -658,18 +666,20 @@ var KNOWN_AGENTS = {
658
666
  // moves that precondition and only that: `-s workspace-write` still
659
667
  // confines writes to the project, so the sandbox boundary is unchanged,
660
668
  // and in a git repository the flag does nothing at all.
661
- args: (prompt) => [
669
+ args: (prompt, effort = null) => [
662
670
  "exec",
663
671
  "--json",
664
672
  ...CODEX_WORKSPACE_CONFIG,
673
+ ...codexEffortConfig(effort),
665
674
  "-s",
666
675
  "workspace-write",
667
676
  "--skip-git-repo-check",
668
677
  prompt
669
678
  ],
670
- terminalArgs: (prompt) => [
679
+ terminalArgs: (prompt, effort = null) => [
671
680
  "exec",
672
681
  ...CODEX_WORKSPACE_CONFIG,
682
+ ...codexEffortConfig(effort),
673
683
  "-s",
674
684
  "workspace-write",
675
685
  "--skip-git-repo-check",
@@ -678,12 +688,13 @@ var KNOWN_AGENTS = {
678
688
  // No sandbox flag here: `codex exec resume` refuses it and inherits the
679
689
  // session's own sandbox, which the first turn set to workspace-write. The
680
690
  // repository check is per invocation, so resume needs the flag of its own.
681
- resumeArgs: (sessionId, prompt) => [
691
+ resumeArgs: (sessionId, prompt, effort = null) => [
682
692
  "exec",
683
693
  "resume",
684
694
  sessionId,
685
695
  "--json",
686
696
  ...CODEX_WORKSPACE_CONFIG,
697
+ ...codexEffortConfig(effort),
687
698
  "--skip-git-repo-check",
688
699
  prompt
689
700
  ],
@@ -695,8 +706,14 @@ var KNOWN_AGENTS = {
695
706
  cursor: {
696
707
  name: "Cursor",
697
708
  binary: "cursor-agent",
698
- args: (prompt) => ["-p", prompt, "--output-format", "stream-json"],
699
- terminalArgs: (prompt) => ["-p", prompt],
709
+ efforts: [],
710
+ args: (prompt, _effort = null) => [
711
+ "-p",
712
+ prompt,
713
+ "--output-format",
714
+ "stream-json"
715
+ ],
716
+ terminalArgs: (prompt, _effort = null) => ["-p", prompt],
700
717
  authArgs: ["status"],
701
718
  // UNVERIFIED: cursor-agent was not available on the build machine. The
702
719
  // reading is deliberately loose, and anything ambiguous stays unknown.
@@ -714,7 +731,11 @@ function execProbe(binary, args, timeoutMs = PROBE_TIMEOUT_MS) {
714
731
  return new Promise((resolve) => {
715
732
  let child;
716
733
  try {
717
- child = spawn(binary, [...args], { shell: false, stdio: ["ignore", "pipe", "ignore"] });
734
+ child = spawn(binary, [...args], {
735
+ env: agentEnvironment(),
736
+ shell: false,
737
+ stdio: ["ignore", "pipe", "ignore"]
738
+ });
718
739
  } catch {
719
740
  return resolve(null);
720
741
  }
@@ -737,8 +758,50 @@ function execProbe(binary, args, timeoutMs = PROBE_TIMEOUT_MS) {
737
758
  });
738
759
  });
739
760
  }
761
+ function agentSearchPath(env = process.env, platform = process.platform) {
762
+ const home = env.HOME ?? env.USERPROFILE ?? "";
763
+ const npmPrefix = env.NPM_CONFIG_PREFIX;
764
+ const versionBins = (root, suffix) => {
765
+ try {
766
+ return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(root, entry.name, ...suffix));
767
+ } catch {
768
+ return [];
769
+ }
770
+ };
771
+ const candidates = [
772
+ ...(env.PATH ?? "").split(delimiter),
773
+ env.PNPM_HOME,
774
+ env.NVM_BIN,
775
+ env.BUN_INSTALL === void 0 ? void 0 : join(env.BUN_INSTALL, "bin"),
776
+ env.CARGO_HOME === void 0 ? void 0 : join(env.CARGO_HOME, "bin"),
777
+ npmPrefix === void 0 ? void 0 : platform === "win32" ? npmPrefix : join(npmPrefix, "bin"),
778
+ home === "" ? void 0 : join(home, ".local", "bin"),
779
+ home === "" ? void 0 : join(home, ".npm-global", "bin"),
780
+ home === "" ? void 0 : join(home, ".bun", "bin"),
781
+ home === "" ? void 0 : join(home, ".cargo", "bin"),
782
+ home === "" ? void 0 : join(home, ".volta", "bin"),
783
+ home === "" ? void 0 : join(home, ".asdf", "shims"),
784
+ home === "" ? void 0 : join(home, ".local", "share", "mise", "shims"),
785
+ home === "" ? void 0 : join(home, ".local", "share", "pnpm"),
786
+ home === "" ? void 0 : join(home, "Library", "pnpm"),
787
+ ...home === "" ? [] : versionBins(join(home, ".nvm", "versions", "node"), ["bin"]),
788
+ ...home === "" ? [] : versionBins(join(home, ".local", "share", "fnm", "node-versions"), [
789
+ "installation",
790
+ "bin"
791
+ ]),
792
+ platform === "win32" ? env.APPDATA : void 0,
793
+ platform === "darwin" ? "/opt/homebrew/bin" : void 0,
794
+ platform === "darwin" ? "/usr/local/bin" : void 0,
795
+ platform === "darwin" ? "/Applications/Codex.app/Contents/Resources" : void 0,
796
+ platform === "darwin" ? "/Applications/Codex++.app/Contents/Resources" : void 0
797
+ ].filter((entry) => typeof entry === "string" && entry !== "");
798
+ return [...new Set(candidates)].join(delimiter);
799
+ }
800
+ function agentEnvironment(env = process.env) {
801
+ return { ...env, PATH: agentSearchPath(env) };
802
+ }
740
803
  async function pathLookup(binary) {
741
- const entries = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
804
+ const entries = agentSearchPath().split(delimiter).filter((entry) => entry !== "");
742
805
  const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
743
806
  for (const entry of entries) {
744
807
  for (const extension of extensions) {
@@ -755,14 +818,22 @@ async function detectAgents(lookup = pathLookup, probe2 = execProbe) {
755
818
  const entries = Object.entries(KNOWN_AGENTS);
756
819
  return Promise.all(entries.map(async ([id, adapter]) => {
757
820
  const available = await lookup(adapter.binary).catch(() => false);
758
- if (!available)
759
- return { id, name: adapter.name, available, auth: "unknown" };
821
+ if (!available) {
822
+ return {
823
+ id,
824
+ name: adapter.name,
825
+ available,
826
+ auth: "unknown",
827
+ efforts: adapter.efforts
828
+ };
829
+ }
760
830
  const result2 = await probe2(adapter.binary, adapter.authArgs).catch(() => null);
761
831
  return {
762
832
  id,
763
833
  name: adapter.name,
764
834
  available,
765
- auth: result2 === null ? "unknown" : adapter.authVerdict(result2)
835
+ auth: result2 === null ? "unknown" : adapter.authVerdict(result2),
836
+ efforts: adapter.efforts
766
837
  };
767
838
  }));
768
839
  }
@@ -889,6 +960,9 @@ function retryFrom(agent, line) {
889
960
  function isAgentChoice(value) {
890
961
  return value === "custom" || typeof value === "string" && Object.hasOwn(KNOWN_AGENTS, value);
891
962
  }
963
+ function isAgentEffort(value) {
964
+ return typeof value === "string" && AGENT_EFFORTS.includes(value);
965
+ }
892
966
  async function readWatchConfig(cwd) {
893
967
  try {
894
968
  const parsed2 = JSON.parse(await readFile(join(cwd, WATCH_PATH), "utf8"));
@@ -899,14 +973,28 @@ async function readWatchConfig(cwd) {
899
973
  }
900
974
  async function readAgentChoice(cwd) {
901
975
  const config = await readWatchConfig(cwd);
976
+ const agent = isAgentChoice(config.agent) ? config.agent : null;
977
+ const efforts = record(config.efforts);
902
978
  return {
903
- agent: isAgentChoice(config.agent) ? config.agent : null,
979
+ agent,
980
+ effort: agent !== null && agent !== "custom" && isAgentEffort(efforts?.[agent]) ? efforts[agent] : null,
904
981
  run: typeof config.run === "string" && config.run !== "" ? config.run : null
905
982
  };
906
983
  }
907
984
  async function saveAgentChoice(cwd, choice) {
908
985
  const config = await readWatchConfig(cwd);
909
986
  config.agent = choice.agent;
987
+ if (choice.agent !== "custom" && choice.effort !== void 0) {
988
+ const efforts = record(config.efforts) ?? {};
989
+ if (choice.effort === null)
990
+ delete efforts[choice.agent];
991
+ else
992
+ efforts[choice.agent] = choice.effort;
993
+ if (Object.keys(efforts).length === 0)
994
+ delete config.efforts;
995
+ else
996
+ config.efforts = efforts;
997
+ }
910
998
  if (choice.run !== void 0)
911
999
  config.run = choice.run;
912
1000
  const path = join(cwd, WATCH_PATH);
@@ -1762,7 +1850,7 @@ function resolveCommand(choice, prompt, sessionId = null, registration = null) {
1762
1850
  agent: choice.agent,
1763
1851
  name: adapter.name,
1764
1852
  command: adapter.binary,
1765
- args: [...adapter.resumeArgs(sessionId, prompt), ...allow],
1853
+ args: [...adapter.resumeArgs(sessionId, prompt, choice.effort), ...allow],
1766
1854
  resumed: true
1767
1855
  };
1768
1856
  }
@@ -1770,7 +1858,7 @@ function resolveCommand(choice, prompt, sessionId = null, registration = null) {
1770
1858
  agent: choice.agent,
1771
1859
  name: adapter.name,
1772
1860
  command: adapter.binary,
1773
- args: [...adapter.args(prompt), ...allow],
1861
+ args: [...adapter.args(prompt, choice.effort), ...allow],
1774
1862
  resumed: false
1775
1863
  };
1776
1864
  }
@@ -1793,7 +1881,7 @@ function lineReader(stream, onLine) {
1793
1881
  return flush;
1794
1882
  }
1795
1883
  function defaultSpawn(command, args, options) {
1796
- return nodeSpawn(command, args, options);
1884
+ return nodeSpawn(command, args, { ...options, env: agentEnvironment() });
1797
1885
  }
1798
1886
  function startRunner(options) {
1799
1887
  const spawn5 = options.spawn ?? defaultSpawn;
@@ -2303,18 +2391,16 @@ async function startServer(options) {
2303
2391
  });
2304
2392
  return agentsInflight;
2305
2393
  };
2306
- const currentAgents = () => {
2307
- if (agentsCache === null)
2394
+ const currentAgents = (refresh = false) => {
2395
+ if (refresh || agentsCache === null || Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
2308
2396
  return probeAgents();
2309
- if (Date.now() - agentsCache.at > AGENTS_FRESH_MS) {
2310
- void probeAgents().catch(() => {
2311
- });
2312
2397
  }
2313
2398
  return Promise.resolve(agentsCache.agents);
2314
2399
  };
2315
2400
  const server = http2.createServer((req, res) => {
2316
2401
  const url = req.url ?? "/";
2317
2402
  const path = url.split("?")[0] ?? "/";
2403
+ const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
2318
2404
  if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
2319
2405
  return sendJson(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
2320
2406
  }
@@ -2454,10 +2540,14 @@ async function startServer(options) {
2454
2540
  });
2455
2541
  }
2456
2542
  if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
2457
- return void Promise.all([currentAgents(), readAgentChoice(cwd)]).then(([agents, choice]) => sendJson(res, 200, {
2543
+ return void Promise.all([
2544
+ currentAgents(query.get("refresh") === "1"),
2545
+ readAgentChoice(cwd)
2546
+ ]).then(([agents, choice]) => sendJson(res, 200, {
2458
2547
  agents,
2459
2548
  choice: choice.agent,
2460
- customRun: choice.run
2549
+ customRun: choice.run,
2550
+ effort: choice.effort
2461
2551
  }));
2462
2552
  }
2463
2553
  if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
@@ -2485,7 +2575,17 @@ async function startServer(options) {
2485
2575
  if (parsed2.run !== void 0 && typeof parsed2.run !== "string") {
2486
2576
  return sendJson(res, 400, { ok: false, error: "The custom run command must be a string." });
2487
2577
  }
2578
+ const effort = parsed2.effort === null || isAgentEffort(parsed2.effort) ? parsed2.effort : void 0;
2579
+ if (parsed2.effort !== void 0 && effort === void 0) {
2580
+ return sendJson(res, 400, { ok: false, error: "Effort must be a supported level or null." });
2581
+ }
2488
2582
  if (parsed2.agent === "custom") {
2583
+ if (effort !== void 0) {
2584
+ return sendJson(res, 400, {
2585
+ ok: false,
2586
+ error: "Custom agents manage effort in their own command."
2587
+ });
2588
+ }
2489
2589
  if (typeof parsed2.run !== "string") {
2490
2590
  return sendJson(res, 400, { ok: false, error: "A custom agent needs a run command." });
2491
2591
  }
@@ -2494,7 +2594,16 @@ async function startServer(options) {
2494
2594
  return sendJson(res, 400, { ok: false, error: template.error });
2495
2595
  return void saveAgentChoice(cwd, { agent: "custom", run: parsed2.run }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
2496
2596
  }
2497
- return void saveAgentChoice(cwd, { agent: parsed2.agent }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
2597
+ if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed2.agent].efforts.includes(effort)) {
2598
+ return sendJson(res, 400, {
2599
+ ok: false,
2600
+ error: `${KNOWN_AGENTS[parsed2.agent].name} does not expose an effort override.`
2601
+ });
2602
+ }
2603
+ return void saveAgentChoice(cwd, {
2604
+ agent: parsed2.agent,
2605
+ ...effort === void 0 ? {} : { effort }
2606
+ }).then(() => sendJson(res, 200, { ok: true }), () => sendJson(res, 500, { ok: false, error: "Agent choice could not be saved." }));
2498
2607
  });
2499
2608
  }
2500
2609
  if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
@@ -3778,7 +3887,7 @@ async function runWatch(options, deps) {
3778
3887
  const adapter = KNOWN_AGENTS[saved.agent];
3779
3888
  template = {
3780
3889
  command: adapter.binary,
3781
- args: adapter.terminalArgs(PROMPT_TOKEN)
3890
+ args: adapter.terminalArgs(PROMPT_TOKEN, saved.effort)
3782
3891
  };
3783
3892
  shownCommand2 = [template.command, ...template.args].join(" ");
3784
3893
  synthesizedAgent = adapter.name;