nuabase 3.0.1 → 3.1.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.
@@ -42,6 +42,9 @@ const SYSTEM_PROMPT$1 = [
42
42
  const claudeCodeAdapter = {
43
43
  id: "claude-code",
44
44
  binaryName: "claude",
45
+ // Prints as it works (stream-json with --include-partial-messages; see
46
+ // buildInvocation), so a long silence means it is stuck.
47
+ idleTimeoutMs: 12e4,
45
48
  apiKeyEnvVars: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
46
49
  modelAliases: { haiku: "haiku", sonnet: "sonnet", opus: "opus" },
47
50
  // Claude Code accepts any JSON Schema with an object root.
@@ -52,7 +55,13 @@ const claudeCodeAdapter = {
52
55
  const args = [
53
56
  "-p",
54
57
  "--output-format",
55
- "json",
58
+ "stream-json",
59
+ // Required: with -p, stream-json is refused without --verbose.
60
+ "--verbose",
61
+ // Prints the reply in pieces as it is generated, not each message once it is done.
62
+ // Without it a long, healthy reply is silent until it ends, and
63
+ // idleTimeoutMs stops it.
64
+ "--include-partial-messages",
56
65
  "--tools",
57
66
  "",
58
67
  "--setting-sources",
@@ -74,10 +83,8 @@ const claudeCodeAdapter = {
74
83
  return { args, stdin: request.prompt };
75
84
  },
76
85
  readResult(result, request) {
77
- let json;
78
- try {
79
- json = JSON.parse(result.stdout);
80
- } catch {
86
+ const json = lastResultLine(result.stdout);
87
+ if (!json) {
81
88
  return {
82
89
  kind: "failed",
83
90
  failure: classifyAgentFailure(
@@ -157,12 +164,28 @@ const claudeCodeAdapter = {
157
164
  ];
158
165
  }
159
166
  };
167
+ function lastResultLine(stdout) {
168
+ const lines = stdout.split("\n");
169
+ for (let i = lines.length - 1; i >= 0; i--) {
170
+ const line = lines[i].trim();
171
+ if (!line.startsWith("{")) continue;
172
+ try {
173
+ const parsed = JSON.parse(line);
174
+ if (parsed?.type === "result") return parsed;
175
+ } catch {
176
+ }
177
+ }
178
+ return void 0;
179
+ }
160
180
 
161
181
  const SCHEMA_FILE = "output-schema.json";
162
182
  const LAST_MESSAGE_FILE = "last-message.txt";
183
+ const DISABLED_FEATURES = ["shell_tool", "unified_exec", "apps", "plugins", "view_image"];
163
184
  const codexAdapter = {
164
185
  id: "codex",
165
186
  binaryName: "codex",
187
+ // Prints nothing while it reasons, so silence says nothing about whether it is stuck.
188
+ idleTimeoutMs: null,
166
189
  apiKeyEnvVars: ["OPENAI_API_KEY", "CODEX_API_KEY"],
167
190
  // Codex's models have no equivalent among the aliases; set one with localAgent({ model }).
168
191
  modelAliases: {},
@@ -180,6 +203,9 @@ const codexAdapter = {
180
203
  "--ignore-rules",
181
204
  "--sandbox",
182
205
  "read-only",
206
+ ...DISABLED_FEATURES.flatMap((feature) => ["--disable", feature]),
207
+ "-c",
208
+ "web_search=disabled",
183
209
  "--json",
184
210
  "-o",
185
211
  LAST_MESSAGE_FILE
@@ -293,6 +319,8 @@ const SYSTEM_PROMPT = [
293
319
  const piAdapter = {
294
320
  id: "pi",
295
321
  binaryName: "pi",
322
+ // Streams as it works (--mode json), so a long silence means it is stuck.
323
+ idleTimeoutMs: 12e4,
296
324
  // Every environment credential Pi's providers recognize. Under `auth:
297
325
  // "subscription"` these are removed so Pi bills its own `auth.json` login.
298
326
  apiKeyEnvVars: [
@@ -488,31 +516,58 @@ function spawnAndCollect(options, cwd) {
488
516
  stdio: ["pipe", "pipe", "pipe"],
489
517
  windowsHide: true
490
518
  });
519
+ const startedAt = Date.now();
491
520
  const stdout = [];
492
521
  const stderr = [];
493
- let timedOut = false;
522
+ let stopped = null;
523
+ let lastOutputAtMs = null;
494
524
  let killTimer;
495
- const timeoutTimer = setTimeout(() => {
496
- timedOut = true;
525
+ let idleTimer;
526
+ const stop = (cause) => {
527
+ if (stopped) return;
528
+ stopped = { ...cause, afterMs: Date.now() - startedAt, lastOutputAtMs };
529
+ clearTimers();
497
530
  child.kill("SIGINT");
498
531
  killTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
499
- }, options.timeoutMs);
500
- child.stdout.on("data", (chunk) => stdout.push(chunk));
501
- child.stderr.on("data", (chunk) => stderr.push(chunk));
502
- child.on("error", (error) => {
532
+ };
533
+ const resetIdleTimer = () => {
534
+ const idleTimeoutMs = options.idleTimeoutMs;
535
+ if (idleTimeoutMs === null || stopped) return;
536
+ clearTimeout(idleTimer);
537
+ idleTimer = setTimeout(() => stop({ by: "idle", limitMs: idleTimeoutMs }), idleTimeoutMs);
538
+ };
539
+ const timeoutTimer = setTimeout(() => stop({ by: "total", limitMs: options.timeoutMs }), options.timeoutMs);
540
+ resetIdleTimer();
541
+ const onAbort = () => stop({ by: "signal" });
542
+ const clearTimers = () => {
503
543
  clearTimeout(timeoutTimer);
544
+ clearTimeout(idleTimer);
545
+ options.signal?.removeEventListener("abort", onAbort);
546
+ };
547
+ options.signal?.addEventListener("abort", onAbort, { once: true });
548
+ if (options.signal?.aborted) onAbort();
549
+ const collect = (stream, chunks) => (chunk) => {
550
+ chunks.push(chunk);
551
+ lastOutputAtMs = Date.now() - startedAt;
552
+ resetIdleTimer();
553
+ options.onOutput?.(chunk, stream);
554
+ };
555
+ child.stdout.setEncoding("utf8").on("data", collect("stdout", stdout));
556
+ child.stderr.setEncoding("utf8").on("data", collect("stderr", stderr));
557
+ child.on("error", (error) => {
558
+ clearTimers();
504
559
  clearTimeout(killTimer);
505
560
  const reason = error.code === "ENOENT" ? "not found" : error.message;
506
561
  reject(new Error(`Could not start ${options.binaryPath}: ${reason}`));
507
562
  });
508
563
  child.on("close", (exitCode) => {
509
- clearTimeout(timeoutTimer);
564
+ clearTimers();
510
565
  clearTimeout(killTimer);
511
566
  resolve({
512
567
  exitCode,
513
- stdout: Buffer.concat(stdout).toString("utf8"),
514
- stderr: Buffer.concat(stderr).toString("utf8"),
515
- timedOut,
568
+ stdout: stdout.join(""),
569
+ stderr: stderr.join(""),
570
+ stopped,
516
571
  outputFiles: {}
517
572
  });
518
573
  });
@@ -558,12 +613,14 @@ async function detectAgent(adapter, binaryPathOverride) {
558
613
  runInvocation({
559
614
  binaryPath,
560
615
  invocation: { args: ["--version"], stdin: "" },
561
- timeoutMs: STATUS_TIMEOUT_MS
616
+ timeoutMs: STATUS_TIMEOUT_MS,
617
+ idleTimeoutMs: null
562
618
  }),
563
619
  runInvocation({
564
620
  binaryPath,
565
621
  invocation: { args: adapter.loginStatusArgs, stdin: "" },
566
622
  timeoutMs: STATUS_TIMEOUT_MS,
623
+ idleTimeoutMs: null,
567
624
  // Report the CLI's own login, not an API key that happens to be set.
568
625
  unsetEnv: adapter.apiKeyEnvVars
569
626
  })
@@ -603,12 +660,17 @@ class LocalAgentEngine {
603
660
  nativeSchema(envelopeSchema) {
604
661
  return this.runner.adapter.nativeSchema(envelopeSchema);
605
662
  }
606
- attempt(request) {
607
- return this.runner.limiter.run(() => this.runAgent(request));
663
+ async attempt(request, controls = {}) {
664
+ try {
665
+ return await this.runner.limiter.run(() => this.runAgent(request, controls), controls.signal);
666
+ } catch (error) {
667
+ if (controls.signal?.aborted) return CANCELLED;
668
+ throw error;
669
+ }
608
670
  }
609
- async runAgent(request) {
671
+ async runAgent(request, { onOutput, signal }) {
610
672
  const { adapter, binaryPath, logger, unsetEnv } = this.runner;
611
- const { model, effort, timeoutMs } = this.call;
673
+ const { model, effort, timeoutMs, idleTimeoutMs } = this.call;
612
674
  const spanId = generateSpanId();
613
675
  const startTime = Date.now();
614
676
  const method = request.enforcement.kind;
@@ -630,13 +692,20 @@ class LocalAgentEngine {
630
692
  binaryPath,
631
693
  invocation: adapter.buildInvocation(request, { model, effort }),
632
694
  timeoutMs,
695
+ idleTimeoutMs,
696
+ onOutput: onOutput && guardListener(onOutput, logger, spanId),
697
+ signal,
633
698
  unsetEnv
634
699
  });
635
700
  } catch (error) {
636
701
  return failed({ kind: "fatal", message: error instanceof Error ? error.message : String(error) });
637
702
  }
638
- if (result.timedOut) {
639
- return failed({ kind: "fatal", message: `did not finish within ${timeoutMs}ms` });
703
+ if (result.stopped?.by === "signal") {
704
+ logger.info("LLM call cancelled", { span_id: spanId, duration_ms: Date.now() - startTime });
705
+ return CANCELLED;
706
+ }
707
+ if (result.stopped) {
708
+ return failed({ kind: "fatal", message: stopMessage(result.stopped, result) });
640
709
  }
641
710
  const reply = adapter.readResult(result, request);
642
711
  if (reply.kind === "failed") {
@@ -664,29 +733,73 @@ class LocalAgentEngine {
664
733
  };
665
734
  }
666
735
  }
736
+ const CANCELLED = {
737
+ kind: "failed",
738
+ failure: { kind: "cancelled", message: "The call was cancelled." }
739
+ };
740
+ function guardListener(onOutput, logger, spanId) {
741
+ let failed = false;
742
+ return (chunk, stream) => {
743
+ try {
744
+ onOutput(chunk, stream);
745
+ } catch (error) {
746
+ if (failed) return;
747
+ failed = true;
748
+ logger.warn("The onOutput listener threw; the agent runs on", {
749
+ span_id: spanId,
750
+ error: error instanceof Error ? error.message : String(error)
751
+ });
752
+ }
753
+ };
754
+ }
755
+ function stopMessage(stopped, result) {
756
+ const ran = `was stopped after ${seconds(stopped.afterMs)}`;
757
+ const chars = result.stdout.length + result.stderr.length;
758
+ let reason;
759
+ if (stopped.by === "idle") {
760
+ const before = stopped.lastOutputAtMs === null ? "It had printed nothing." : `It had printed ${chars} characters before that.`;
761
+ reason = `${ran}: it printed nothing for ${seconds(stopped.limitMs)} (idleTimeoutMs ${stopped.limitMs}). ${before} Raise idleTimeoutMs, or set it to null for no limit.`;
762
+ } else {
763
+ const before = stopped.lastOutputAtMs === null ? "It had printed nothing." : `It had printed ${chars} characters, the last ${seconds(stopped.afterMs - stopped.lastOutputAtMs)} before it was stopped.`;
764
+ reason = `${ran}, its limit (timeoutMs ${stopped.limitMs}). ${before} Raise timeoutMs.`;
765
+ }
766
+ const stderr = stderrTail(result, 500);
767
+ return stderr ? `${reason} Its stderr ended: ${stderr}` : reason;
768
+ }
769
+ function seconds(ms) {
770
+ return ms < 1e4 ? `${(ms / 1e3).toFixed(1)} s` : `${Math.round(ms / 1e3)} s`;
771
+ }
667
772
 
773
+ const DEFAULT_TIMEOUT_MS = 6e5;
774
+ const MAX_LIMIT_MS = 2 ** 31 - 1;
668
775
  class LocalAgent {
669
- constructor(runner, call) {
776
+ constructor(runner, options) {
670
777
  this.runner = runner;
671
- this.call = call;
672
778
  this.agent = runner.adapter.id;
673
779
  this.logger = runner.logger;
780
+ this.call = {
781
+ model: options.model,
782
+ effort: options.effort,
783
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
784
+ idleTimeoutMs: options.idleTimeoutMs === void 0 ? runner.adapter.idleTimeoutMs : options.idleTimeoutMs
785
+ };
786
+ if (this.call.model !== void 0 && this.call.model.trim() === "") {
787
+ throw new TypeError("model must be a non-empty string, or undefined for the agent's default model");
788
+ }
789
+ if (!isLimit(this.call.timeoutMs)) {
790
+ throw new TypeError(`timeoutMs must be from 1 to ${MAX_LIMIT_MS} milliseconds`);
791
+ }
792
+ if (this.call.idleTimeoutMs !== null && !isLimit(this.call.idleTimeoutMs)) {
793
+ throw new TypeError(`idleTimeoutMs must be from 1 to ${MAX_LIMIT_MS} milliseconds, or null for no limit`);
794
+ }
674
795
  }
675
796
  /**
676
797
  * The same agent with other call options, sharing this one's executable,
677
798
  * environment, logger and limit on processes running at once. Options left
678
- * out keep this agent's; a model or effort given as undefined is the CLI's
679
- * default.
799
+ * out keep this agent's; an option given as undefined takes its default.
680
800
  */
681
801
  with(options) {
682
- if (options.model !== void 0 && options.model.trim() === "") {
683
- throw new TypeError("model must be a non-empty string; pass undefined to use the agent's default model");
684
- }
685
- return new LocalAgent(this.runner, {
686
- ...this.call,
687
- ...options,
688
- timeoutMs: options.timeoutMs ?? this.call.timeoutMs
689
- });
802
+ return new LocalAgent(this.runner, { ...this.call, ...options });
690
803
  }
691
804
  /**
692
805
  * No model choice runs the default model. An alias runs the CLI's equivalent
@@ -712,6 +825,9 @@ class LocalAgent {
712
825
  return { kind: "routed", engine: new LocalAgentEngine(this.runner, call) };
713
826
  }
714
827
  }
828
+ function isLimit(ms) {
829
+ return ms >= 1 && ms <= MAX_LIMIT_MS;
830
+ }
715
831
 
716
832
  class Semaphore {
717
833
  constructor(limit) {
@@ -719,9 +835,11 @@ class Semaphore {
719
835
  this.active = 0;
720
836
  this.waiting = [];
721
837
  }
722
- async run(task) {
838
+ /** Rejects with the signal's reason, without running `task`, when `signal` aborts while it waits. */
839
+ async run(task, signal) {
840
+ signal?.throwIfAborted();
723
841
  if (this.active >= this.limit) {
724
- await new Promise((resolve) => this.waiting.push(resolve));
842
+ await this.waitForTurn(signal);
725
843
  } else {
726
844
  this.active++;
727
845
  }
@@ -736,71 +854,131 @@ class Semaphore {
736
854
  }
737
855
  }
738
856
  }
857
+ /** Resolves once a finishing task hands over its place. */
858
+ waitForTurn(signal) {
859
+ return new Promise((resolve, reject) => {
860
+ const take = () => {
861
+ signal?.removeEventListener("abort", leave);
862
+ resolve();
863
+ };
864
+ const leave = () => {
865
+ this.waiting.splice(this.waiting.indexOf(take), 1);
866
+ reject(signal.reason);
867
+ };
868
+ this.waiting.push(take);
869
+ signal?.addEventListener("abort", leave, { once: true });
870
+ });
871
+ }
739
872
  }
740
873
 
741
- const DEFAULT_TIMEOUT_MS = 18e4;
742
874
  const DEFAULT_CONCURRENCY = 2;
743
875
  function localAgent(config) {
744
876
  if (!isLocalAgentId(config.agent)) {
745
877
  throw new TypeError(`Unknown local agent: ${String(config.agent)}`);
746
878
  }
747
- const settings = parseOptions(config);
748
879
  const adapter = AGENT_ADAPTERS[config.agent];
749
880
  const binaryPath = config.binaryPath ?? findOnPath(adapter.binaryName);
750
881
  if (!binaryPath) {
751
882
  throw new Error(`${adapter.binaryName} was not found on PATH. Install ${config.agent} or set binaryPath.`);
752
883
  }
753
- return setUpLocalAgent(adapter, binaryPath, settings);
884
+ return setUpLocalAgent(adapter, binaryPath, config);
754
885
  }
755
886
  async function findLocalAgent(options = {}) {
756
- const settings = parseOptions(options);
757
887
  const statuses = await detectLocalAgents();
758
888
  for (const status of statuses) {
759
889
  if (status.installed && status.loggedIn) {
760
- return setUpLocalAgent(AGENT_ADAPTERS[status.agent], status.binaryPath, settings);
890
+ return setUpLocalAgent(AGENT_ADAPTERS[status.agent], status.binaryPath, options);
761
891
  }
762
892
  }
763
893
  const summary = statuses.map((s) => `${s.agent}: ${s.installed ? "not logged in" : "not installed"}`).join(", ");
764
894
  throw new Error(`No logged-in coding agent found (${summary})`);
765
895
  }
766
- function parseOptions(options) {
767
- const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
768
- if (!Number.isInteger(concurrency) || concurrency < 1) {
769
- throw new TypeError("concurrency must be a positive integer");
770
- }
771
- if (options.model !== void 0 && options.model.trim() === "") {
772
- throw new TypeError("model must be a non-empty string; omit it to use the agent's default model");
773
- }
774
- return {
775
- model: options.model,
776
- effort: options.effort,
777
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
778
- concurrency,
779
- auth: options.auth ?? "subscription",
780
- logger: options.logger ?? new ConsoleLogger()
781
- };
782
- }
783
- function setUpLocalAgent(adapter, binaryPath, settings) {
896
+ function setUpLocalAgent(adapter, binaryPath, options) {
784
897
  if (/\.(cmd|bat)$/i.test(binaryPath)) {
785
898
  throw new Error(
786
899
  `${binaryPath} is a .cmd shim, which local agents cannot run safely. Set binaryPath to the agent's native executable.`
787
900
  );
788
901
  }
902
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
903
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
904
+ throw new TypeError("concurrency must be a positive integer");
905
+ }
789
906
  const runner = {
790
907
  adapter,
791
908
  binaryPath,
792
- unsetEnv: settings.auth === "subscription" ? adapter.apiKeyEnvVars : [],
793
- limiter: new Semaphore(settings.concurrency),
794
- logger: settings.logger
909
+ unsetEnv: (options.auth ?? "subscription") === "subscription" ? adapter.apiKeyEnvVars : [],
910
+ limiter: new Semaphore(concurrency),
911
+ logger: options.logger ?? new ConsoleLogger()
912
+ };
913
+ return new LocalAgent(runner, options);
914
+ }
915
+
916
+ const AGENTS = {
917
+ "claude-code": {
918
+ name: "Claude Code",
919
+ worksWith: "Claude Pro or Max, or an Anthropic Console account",
920
+ install: {
921
+ macos: "brew install --cask claude-code",
922
+ linux: "curl -fsSL https://claude.ai/install.sh | bash",
923
+ other: "npm install -g @anthropic-ai/claude-code"
924
+ },
925
+ signIn: "Run `claude`",
926
+ docsUrl: "https://code.claude.com/docs/en/setup"
927
+ },
928
+ codex: {
929
+ name: "Codex",
930
+ worksWith: "ChatGPT Plus, Pro or Business, or an OpenAI API key",
931
+ install: {
932
+ macos: "brew install --cask codex",
933
+ linux: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
934
+ other: "npm install -g @openai/codex"
935
+ },
936
+ signIn: "Run `codex login`",
937
+ docsUrl: "https://learn.chatgpt.com/docs/codex/cli"
938
+ },
939
+ pi: {
940
+ name: "Pi",
941
+ worksWith: "Many providers, including a Claude or ChatGPT plan",
942
+ install: {
943
+ macos: "npm install -g --ignore-scripts @earendil-works/pi-coding-agent",
944
+ linux: "curl -fsSL https://pi.dev/install.sh | sh",
945
+ other: "npm install -g --ignore-scripts @earendil-works/pi-coding-agent"
946
+ },
947
+ signIn: "Run `pi`, then type `/login`",
948
+ docsUrl: "https://pi.dev/docs/latest/quickstart"
949
+ }
950
+ };
951
+ function installCommand(commands, platform) {
952
+ if (platform === "darwin") return commands.macos;
953
+ return platform === "linux" ? commands.linux : commands.other;
954
+ }
955
+ function setupPrompt(agents) {
956
+ return [
957
+ "I use an app that runs a coding agent's command-line program (CLI) on this computer. Please install one of these CLIs here and help me sign in to it:",
958
+ "",
959
+ ...agents.map(
960
+ (setup) => `- ${setup.name}. Works with: ${setup.worksWith}. Install: \`${setup.install}\`. Sign in, in a terminal: ${setup.signIn}.`
961
+ ),
962
+ "",
963
+ "Pick the one that works with the plan or account I already have, and ask me if you can't tell. Signing in is interactive, so if you can't do it yourself, tell me the command to run in a terminal. Once the CLI runs and is signed in, tell me, so I can check again in the app."
964
+ ].join("\n");
965
+ }
966
+ function localAgentSetupGuide(platform = process.platform) {
967
+ const agents = LOCAL_AGENT_IDS.map((agent) => ({
968
+ agent,
969
+ ...AGENTS[agent],
970
+ install: installCommand(AGENTS[agent].install, platform)
971
+ }));
972
+ return {
973
+ missing: "No coding agent is set up on this computer.",
974
+ cliOnly: "This app runs the agent's command-line program (CLI). A desktop app, browser chat or editor extension, such as the Claude or ChatGPT app, isn't enough on its own.",
975
+ agents,
976
+ setupPrompt: setupPrompt(agents)
795
977
  };
796
- return new LocalAgent(runner, {
797
- model: settings.model,
798
- effort: settings.effort,
799
- timeoutMs: settings.timeoutMs
800
- });
801
978
  }
802
979
 
803
980
  const LINUX_MAX_ARGUMENT_BYTES = 131072;
981
+ const SHELL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
804
982
  function agentSessionMode(platform = process.platform) {
805
983
  if (platform === "win32") return "none";
806
984
  return platform === "darwin" ? "terminal" : "command";
@@ -809,6 +987,10 @@ async function startAgentSession(config) {
809
987
  if (!isLocalAgentId(config.agent)) {
810
988
  throw new TypeError(`Unknown local agent: ${String(config.agent)}`);
811
989
  }
990
+ const badName = config.unsetEnv?.find((name) => !SHELL_NAME.test(name));
991
+ if (badName !== void 0) {
992
+ throw new TypeError(`Not a shell variable name in unsetEnv: ${JSON.stringify(badName)}`);
993
+ }
812
994
  const platform = config.platform ?? process.platform;
813
995
  const mode = agentSessionMode(platform);
814
996
  if (mode === "none") {
@@ -828,7 +1010,7 @@ async function startAgentSession(config) {
828
1010
  const files = writeSessionFiles(config, binaryPath, adapter.sessionArgs(config.model, config.approval ?? "ask"));
829
1011
  if (mode === "command") return { kind: "command", files };
830
1012
  try {
831
- await promisify(execFile)("open", [files.launcherPath]);
1013
+ await promisify(execFile)("open", [files.launcherPath], { env: config.env ?? process.env });
832
1014
  } catch (error) {
833
1015
  return { kind: "open_failed", files, error: error instanceof Error ? error.message : String(error) };
834
1016
  }
@@ -843,7 +1025,7 @@ function writeSessionFiles(config, binaryPath, agentArgs) {
843
1025
  const promptPath = path.join(config.dir, `${name}.md`);
844
1026
  const launcherPath = path.join(config.dir, `${name}.command`);
845
1027
  fs.writeFileSync(promptPath, config.prompt, { mode: 384, flag: "wx" });
846
- fs.writeFileSync(launcherPath, sessionLauncher({ cwd: config.cwd, binaryPath, agentArgs, promptPath }), {
1028
+ fs.writeFileSync(launcherPath, sessionLauncher({ cwd: config.cwd, binaryPath, agentArgs, promptPath, unsetEnv: config.unsetEnv }), {
847
1029
  mode: 448,
848
1030
  flag: "wx"
849
1031
  });
@@ -853,10 +1035,17 @@ function defaultSessionName(agent) {
853
1035
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replaceAll(":", "-");
854
1036
  return `${stamp}-${agent}-${randomBytes(2).toString("hex")}`;
855
1037
  }
856
- function sessionLauncher({ cwd, binaryPath, agentArgs, promptPath }) {
1038
+ function sessionLauncher({
1039
+ cwd,
1040
+ binaryPath,
1041
+ agentArgs,
1042
+ promptPath,
1043
+ unsetEnv = []
1044
+ }) {
857
1045
  const command = [binaryPath, ...agentArgs].map(shellQuote).join(" ");
858
1046
  return [
859
1047
  "#!/bin/sh",
1048
+ ...unsetEnv.length > 0 ? [`unset ${unsetEnv.map(shellQuote).join(" ")}`] : [],
860
1049
  `cd ${shellQuote(cwd)} || exit 1`,
861
1050
  `exec ${command} -- "$(cat ${shellQuote(promptPath)})"`,
862
1051
  ""
@@ -866,4 +1055,4 @@ function shellQuote(value) {
866
1055
  return `'${value.replaceAll("'", `'\\''`)}'`;
867
1056
  }
868
1057
 
869
- export { LOCAL_AGENT_IDS, agentSessionMode, detectLocalAgents, findLocalAgent, isLocalAgentId, localAgent, sessionLauncher, startAgentSession };
1058
+ export { LOCAL_AGENT_IDS, agentSessionMode, detectLocalAgents, findLocalAgent, isLocalAgentId, localAgent, localAgentSetupGuide, sessionLauncher, startAgentSession };
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { N as NormalizedUsage, A as AnswerOrigin, S as SchemaEnforcementKind, L as LlmProviderId, M as ModelInput } from './chunks/local-agent-Bj-QKikS.cjs';
1
+ import { N as NormalizedUsage, A as AnswerOrigin, S as SchemaEnforcementKind, L as LlmProviderId, M as ModelInput, E as EngineOutputListener } from './chunks/local-agent-CPIXKS41.cjs';
2
2
  import { LocalAgent } from './local-agent.cjs';
3
- export { b as LocalAgentId } from './chunks/local-agent-Bj-QKikS.cjs';
3
+ export { b as LocalAgentId } from './chunks/local-agent-CPIXKS41.cjs';
4
4
  import { z } from 'zod';
5
5
  export { z } from 'zod';
6
6
 
@@ -30,6 +30,8 @@ type CastResultFailure = {
30
30
  reason: string;
31
31
  /** Attempts made; 0 when the request was refused before any (such as an invalid schema). */
32
32
  attempts: number;
33
+ /** True when the call's `signal` cancelled it. A cancelled call is not retried. */
34
+ cancelled: boolean;
33
35
  source: CastResultSource;
34
36
  latencyMs: number;
35
37
  };
@@ -64,12 +66,20 @@ type GetOptions<TSchema extends z.ZodTypeAny> = {
64
66
  input?: unknown;
65
67
  output: OutputDef<TSchema>;
66
68
  model?: ModelInput;
69
+ /** Gets what a local agent prints while this call runs. */
70
+ onOutput?: EngineOutputListener;
71
+ /** Cancels this call; it then fails with `cancelled: true`. */
72
+ signal?: AbortSignal;
67
73
  };
68
74
  type ListOptions<TSchema extends z.ZodTypeAny, TData extends Record<string, unknown>, PrimaryKey extends keyof TData & string, TName extends string = string> = {
69
75
  input: TData[];
70
76
  primaryKey: PrimaryKey;
71
77
  output: OutputDef<TSchema, TName>;
72
78
  model?: ModelInput;
79
+ /** Gets what a local agent prints while this call runs. */
80
+ onOutput?: EngineOutputListener;
81
+ /** Cancels this call; it then fails with `cancelled: true`. */
82
+ signal?: AbortSignal;
73
83
  };
74
84
  type ListResultRow<TSchema extends z.ZodTypeAny, PrimaryKey extends string, TName extends string = string> = {
75
85
  [K in PrimaryKey]: string | number;
@@ -90,5 +100,5 @@ declare class Nua {
90
100
  list<TSchema extends z.ZodTypeAny, TData extends Record<string, unknown>, PrimaryKey extends keyof TData & string, TName extends string = string>(prompt: string, options: ListOptions<TSchema, TData, PrimaryKey, TName>): Promise<CastResult<ListResultRow<TSchema, PrimaryKey, TName>[]>>;
91
101
  }
92
102
 
93
- export { AnswerOrigin, LlmProviderId, ModelInput, NormalizedUsage, Nua, SchemaEnforcementKind };
103
+ export { AnswerOrigin, EngineOutputListener, LlmProviderId, ModelInput, NormalizedUsage, Nua, SchemaEnforcementKind };
94
104
  export type { CastResult, CastResultSource, DirectConfig, DirectMeta, GetOptions, ListOptions, ListResultRow, OutputDef, ProviderApiKeys };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { N as NormalizedUsage, A as AnswerOrigin, S as SchemaEnforcementKind, L as LlmProviderId, M as ModelInput } from './chunks/local-agent-Bj-QKikS.mjs';
1
+ import { N as NormalizedUsage, A as AnswerOrigin, S as SchemaEnforcementKind, L as LlmProviderId, M as ModelInput, E as EngineOutputListener } from './chunks/local-agent-CPIXKS41.mjs';
2
2
  import { LocalAgent } from './local-agent.mjs';
3
- export { b as LocalAgentId } from './chunks/local-agent-Bj-QKikS.mjs';
3
+ export { b as LocalAgentId } from './chunks/local-agent-CPIXKS41.mjs';
4
4
  import { z } from 'zod';
5
5
  export { z } from 'zod';
6
6
 
@@ -30,6 +30,8 @@ type CastResultFailure = {
30
30
  reason: string;
31
31
  /** Attempts made; 0 when the request was refused before any (such as an invalid schema). */
32
32
  attempts: number;
33
+ /** True when the call's `signal` cancelled it. A cancelled call is not retried. */
34
+ cancelled: boolean;
33
35
  source: CastResultSource;
34
36
  latencyMs: number;
35
37
  };
@@ -64,12 +66,20 @@ type GetOptions<TSchema extends z.ZodTypeAny> = {
64
66
  input?: unknown;
65
67
  output: OutputDef<TSchema>;
66
68
  model?: ModelInput;
69
+ /** Gets what a local agent prints while this call runs. */
70
+ onOutput?: EngineOutputListener;
71
+ /** Cancels this call; it then fails with `cancelled: true`. */
72
+ signal?: AbortSignal;
67
73
  };
68
74
  type ListOptions<TSchema extends z.ZodTypeAny, TData extends Record<string, unknown>, PrimaryKey extends keyof TData & string, TName extends string = string> = {
69
75
  input: TData[];
70
76
  primaryKey: PrimaryKey;
71
77
  output: OutputDef<TSchema, TName>;
72
78
  model?: ModelInput;
79
+ /** Gets what a local agent prints while this call runs. */
80
+ onOutput?: EngineOutputListener;
81
+ /** Cancels this call; it then fails with `cancelled: true`. */
82
+ signal?: AbortSignal;
73
83
  };
74
84
  type ListResultRow<TSchema extends z.ZodTypeAny, PrimaryKey extends string, TName extends string = string> = {
75
85
  [K in PrimaryKey]: string | number;
@@ -90,5 +100,5 @@ declare class Nua {
90
100
  list<TSchema extends z.ZodTypeAny, TData extends Record<string, unknown>, PrimaryKey extends keyof TData & string, TName extends string = string>(prompt: string, options: ListOptions<TSchema, TData, PrimaryKey, TName>): Promise<CastResult<ListResultRow<TSchema, PrimaryKey, TName>[]>>;
91
101
  }
92
102
 
93
- export { AnswerOrigin, LlmProviderId, ModelInput, NormalizedUsage, Nua, SchemaEnforcementKind };
103
+ export { AnswerOrigin, EngineOutputListener, LlmProviderId, ModelInput, NormalizedUsage, Nua, SchemaEnforcementKind };
94
104
  export type { CastResult, CastResultSource, DirectConfig, DirectMeta, GetOptions, ListOptions, ListResultRow, OutputDef, ProviderApiKeys };