nuabase 3.0.0 → 3.1.0

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.
@@ -65,6 +65,9 @@ const SYSTEM_PROMPT$1 = [
65
65
  const claudeCodeAdapter = {
66
66
  id: "claude-code",
67
67
  binaryName: "claude",
68
+ // Prints as it works (stream-json with --include-partial-messages; see
69
+ // buildInvocation), so a long silence means it is stuck.
70
+ idleTimeoutMs: 12e4,
68
71
  apiKeyEnvVars: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
69
72
  modelAliases: { haiku: "haiku", sonnet: "sonnet", opus: "opus" },
70
73
  // Claude Code accepts any JSON Schema with an object root.
@@ -75,7 +78,13 @@ const claudeCodeAdapter = {
75
78
  const args = [
76
79
  "-p",
77
80
  "--output-format",
78
- "json",
81
+ "stream-json",
82
+ // Required: with -p, stream-json is refused without --verbose.
83
+ "--verbose",
84
+ // Prints the reply in pieces as it is generated, not each message once it is done.
85
+ // Without it a long, healthy reply is silent until it ends, and
86
+ // idleTimeoutMs stops it.
87
+ "--include-partial-messages",
79
88
  "--tools",
80
89
  "",
81
90
  "--setting-sources",
@@ -97,10 +106,8 @@ const claudeCodeAdapter = {
97
106
  return { args, stdin: request.prompt };
98
107
  },
99
108
  readResult(result, request) {
100
- let json;
101
- try {
102
- json = JSON.parse(result.stdout);
103
- } catch {
109
+ const json = lastResultLine(result.stdout);
110
+ if (!json) {
104
111
  return {
105
112
  kind: "failed",
106
113
  failure: classifyAgentFailure(
@@ -180,12 +187,28 @@ const claudeCodeAdapter = {
180
187
  ];
181
188
  }
182
189
  };
190
+ function lastResultLine(stdout) {
191
+ const lines = stdout.split("\n");
192
+ for (let i = lines.length - 1; i >= 0; i--) {
193
+ const line = lines[i].trim();
194
+ if (!line.startsWith("{")) continue;
195
+ try {
196
+ const parsed = JSON.parse(line);
197
+ if (parsed?.type === "result") return parsed;
198
+ } catch {
199
+ }
200
+ }
201
+ return void 0;
202
+ }
183
203
 
184
204
  const SCHEMA_FILE = "output-schema.json";
185
205
  const LAST_MESSAGE_FILE = "last-message.txt";
206
+ const DISABLED_FEATURES = ["shell_tool", "unified_exec", "apps", "plugins", "view_image"];
186
207
  const codexAdapter = {
187
208
  id: "codex",
188
209
  binaryName: "codex",
210
+ // Prints nothing while it reasons, so silence says nothing about whether it is stuck.
211
+ idleTimeoutMs: null,
189
212
  apiKeyEnvVars: ["OPENAI_API_KEY", "CODEX_API_KEY"],
190
213
  // Codex's models have no equivalent among the aliases; set one with localAgent({ model }).
191
214
  modelAliases: {},
@@ -203,6 +226,9 @@ const codexAdapter = {
203
226
  "--ignore-rules",
204
227
  "--sandbox",
205
228
  "read-only",
229
+ ...DISABLED_FEATURES.flatMap((feature) => ["--disable", feature]),
230
+ "-c",
231
+ "web_search=disabled",
206
232
  "--json",
207
233
  "-o",
208
234
  LAST_MESSAGE_FILE
@@ -316,6 +342,8 @@ const SYSTEM_PROMPT = [
316
342
  const piAdapter = {
317
343
  id: "pi",
318
344
  binaryName: "pi",
345
+ // Streams as it works (--mode json), so a long silence means it is stuck.
346
+ idleTimeoutMs: 12e4,
319
347
  // Every environment credential Pi's providers recognize. Under `auth:
320
348
  // "subscription"` these are removed so Pi bills its own `auth.json` login.
321
349
  apiKeyEnvVars: [
@@ -511,31 +539,58 @@ function spawnAndCollect(options, cwd) {
511
539
  stdio: ["pipe", "pipe", "pipe"],
512
540
  windowsHide: true
513
541
  });
542
+ const startedAt = Date.now();
514
543
  const stdout = [];
515
544
  const stderr = [];
516
- let timedOut = false;
545
+ let stopped = null;
546
+ let lastOutputAtMs = null;
517
547
  let killTimer;
518
- const timeoutTimer = setTimeout(() => {
519
- timedOut = true;
548
+ let idleTimer;
549
+ const stop = (cause) => {
550
+ if (stopped) return;
551
+ stopped = { ...cause, afterMs: Date.now() - startedAt, lastOutputAtMs };
552
+ clearTimers();
520
553
  child.kill("SIGINT");
521
554
  killTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
522
- }, options.timeoutMs);
523
- child.stdout.on("data", (chunk) => stdout.push(chunk));
524
- child.stderr.on("data", (chunk) => stderr.push(chunk));
525
- child.on("error", (error) => {
555
+ };
556
+ const resetIdleTimer = () => {
557
+ const idleTimeoutMs = options.idleTimeoutMs;
558
+ if (idleTimeoutMs === null || stopped) return;
559
+ clearTimeout(idleTimer);
560
+ idleTimer = setTimeout(() => stop({ by: "idle", limitMs: idleTimeoutMs }), idleTimeoutMs);
561
+ };
562
+ const timeoutTimer = setTimeout(() => stop({ by: "total", limitMs: options.timeoutMs }), options.timeoutMs);
563
+ resetIdleTimer();
564
+ const onAbort = () => stop({ by: "signal" });
565
+ const clearTimers = () => {
526
566
  clearTimeout(timeoutTimer);
567
+ clearTimeout(idleTimer);
568
+ options.signal?.removeEventListener("abort", onAbort);
569
+ };
570
+ options.signal?.addEventListener("abort", onAbort, { once: true });
571
+ if (options.signal?.aborted) onAbort();
572
+ const collect = (stream, chunks) => (chunk) => {
573
+ chunks.push(chunk);
574
+ lastOutputAtMs = Date.now() - startedAt;
575
+ resetIdleTimer();
576
+ options.onOutput?.(chunk, stream);
577
+ };
578
+ child.stdout.setEncoding("utf8").on("data", collect("stdout", stdout));
579
+ child.stderr.setEncoding("utf8").on("data", collect("stderr", stderr));
580
+ child.on("error", (error) => {
581
+ clearTimers();
527
582
  clearTimeout(killTimer);
528
583
  const reason = error.code === "ENOENT" ? "not found" : error.message;
529
584
  reject(new Error(`Could not start ${options.binaryPath}: ${reason}`));
530
585
  });
531
586
  child.on("close", (exitCode) => {
532
- clearTimeout(timeoutTimer);
587
+ clearTimers();
533
588
  clearTimeout(killTimer);
534
589
  resolve({
535
590
  exitCode,
536
- stdout: Buffer.concat(stdout).toString("utf8"),
537
- stderr: Buffer.concat(stderr).toString("utf8"),
538
- timedOut,
591
+ stdout: stdout.join(""),
592
+ stderr: stderr.join(""),
593
+ stopped,
539
594
  outputFiles: {}
540
595
  });
541
596
  });
@@ -581,12 +636,14 @@ async function detectAgent(adapter, binaryPathOverride) {
581
636
  runInvocation({
582
637
  binaryPath,
583
638
  invocation: { args: ["--version"], stdin: "" },
584
- timeoutMs: STATUS_TIMEOUT_MS
639
+ timeoutMs: STATUS_TIMEOUT_MS,
640
+ idleTimeoutMs: null
585
641
  }),
586
642
  runInvocation({
587
643
  binaryPath,
588
644
  invocation: { args: adapter.loginStatusArgs, stdin: "" },
589
645
  timeoutMs: STATUS_TIMEOUT_MS,
646
+ idleTimeoutMs: null,
590
647
  // Report the CLI's own login, not an API key that happens to be set.
591
648
  unsetEnv: adapter.apiKeyEnvVars
592
649
  })
@@ -626,12 +683,17 @@ class LocalAgentEngine {
626
683
  nativeSchema(envelopeSchema) {
627
684
  return this.runner.adapter.nativeSchema(envelopeSchema);
628
685
  }
629
- attempt(request) {
630
- return this.runner.limiter.run(() => this.runAgent(request));
686
+ async attempt(request, controls = {}) {
687
+ try {
688
+ return await this.runner.limiter.run(() => this.runAgent(request, controls), controls.signal);
689
+ } catch (error) {
690
+ if (controls.signal?.aborted) return CANCELLED;
691
+ throw error;
692
+ }
631
693
  }
632
- async runAgent(request) {
694
+ async runAgent(request, { onOutput, signal }) {
633
695
  const { adapter, binaryPath, logger, unsetEnv } = this.runner;
634
- const { model, effort, timeoutMs } = this.call;
696
+ const { model, effort, timeoutMs, idleTimeoutMs } = this.call;
635
697
  const spanId = strictJsonSchema.generateSpanId();
636
698
  const startTime = Date.now();
637
699
  const method = request.enforcement.kind;
@@ -653,13 +715,20 @@ class LocalAgentEngine {
653
715
  binaryPath,
654
716
  invocation: adapter.buildInvocation(request, { model, effort }),
655
717
  timeoutMs,
718
+ idleTimeoutMs,
719
+ onOutput: onOutput && guardListener(onOutput, logger, spanId),
720
+ signal,
656
721
  unsetEnv
657
722
  });
658
723
  } catch (error) {
659
724
  return failed({ kind: "fatal", message: error instanceof Error ? error.message : String(error) });
660
725
  }
661
- if (result.timedOut) {
662
- return failed({ kind: "fatal", message: `did not finish within ${timeoutMs}ms` });
726
+ if (result.stopped?.by === "signal") {
727
+ logger.info("LLM call cancelled", { span_id: spanId, duration_ms: Date.now() - startTime });
728
+ return CANCELLED;
729
+ }
730
+ if (result.stopped) {
731
+ return failed({ kind: "fatal", message: stopMessage(result.stopped, result) });
663
732
  }
664
733
  const reply = adapter.readResult(result, request);
665
734
  if (reply.kind === "failed") {
@@ -687,29 +756,73 @@ class LocalAgentEngine {
687
756
  };
688
757
  }
689
758
  }
759
+ const CANCELLED = {
760
+ kind: "failed",
761
+ failure: { kind: "cancelled", message: "The call was cancelled." }
762
+ };
763
+ function guardListener(onOutput, logger, spanId) {
764
+ let failed = false;
765
+ return (chunk, stream) => {
766
+ try {
767
+ onOutput(chunk, stream);
768
+ } catch (error) {
769
+ if (failed) return;
770
+ failed = true;
771
+ logger.warn("The onOutput listener threw; the agent runs on", {
772
+ span_id: spanId,
773
+ error: error instanceof Error ? error.message : String(error)
774
+ });
775
+ }
776
+ };
777
+ }
778
+ function stopMessage(stopped, result) {
779
+ const ran = `was stopped after ${seconds(stopped.afterMs)}`;
780
+ const chars = result.stdout.length + result.stderr.length;
781
+ let reason;
782
+ if (stopped.by === "idle") {
783
+ const before = stopped.lastOutputAtMs === null ? "It had printed nothing." : `It had printed ${chars} characters before that.`;
784
+ reason = `${ran}: it printed nothing for ${seconds(stopped.limitMs)} (idleTimeoutMs ${stopped.limitMs}). ${before} Raise idleTimeoutMs, or set it to null for no limit.`;
785
+ } else {
786
+ 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.`;
787
+ reason = `${ran}, its limit (timeoutMs ${stopped.limitMs}). ${before} Raise timeoutMs.`;
788
+ }
789
+ const stderr = stderrTail(result, 500);
790
+ return stderr ? `${reason} Its stderr ended: ${stderr}` : reason;
791
+ }
792
+ function seconds(ms) {
793
+ return ms < 1e4 ? `${(ms / 1e3).toFixed(1)} s` : `${Math.round(ms / 1e3)} s`;
794
+ }
690
795
 
796
+ const DEFAULT_TIMEOUT_MS = 6e5;
797
+ const MAX_LIMIT_MS = 2 ** 31 - 1;
691
798
  class LocalAgent {
692
- constructor(runner, call) {
799
+ constructor(runner, options) {
693
800
  this.runner = runner;
694
- this.call = call;
695
801
  this.agent = runner.adapter.id;
696
802
  this.logger = runner.logger;
803
+ this.call = {
804
+ model: options.model,
805
+ effort: options.effort,
806
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
807
+ idleTimeoutMs: options.idleTimeoutMs === void 0 ? runner.adapter.idleTimeoutMs : options.idleTimeoutMs
808
+ };
809
+ if (this.call.model !== void 0 && this.call.model.trim() === "") {
810
+ throw new TypeError("model must be a non-empty string, or undefined for the agent's default model");
811
+ }
812
+ if (!isLimit(this.call.timeoutMs)) {
813
+ throw new TypeError(`timeoutMs must be from 1 to ${MAX_LIMIT_MS} milliseconds`);
814
+ }
815
+ if (this.call.idleTimeoutMs !== null && !isLimit(this.call.idleTimeoutMs)) {
816
+ throw new TypeError(`idleTimeoutMs must be from 1 to ${MAX_LIMIT_MS} milliseconds, or null for no limit`);
817
+ }
697
818
  }
698
819
  /**
699
820
  * The same agent with other call options, sharing this one's executable,
700
821
  * environment, logger and limit on processes running at once. Options left
701
- * out keep this agent's; a model or effort given as undefined is the CLI's
702
- * default.
822
+ * out keep this agent's; an option given as undefined takes its default.
703
823
  */
704
824
  with(options) {
705
- if (options.model !== void 0 && options.model.trim() === "") {
706
- throw new TypeError("model must be a non-empty string; pass undefined to use the agent's default model");
707
- }
708
- return new LocalAgent(this.runner, {
709
- ...this.call,
710
- ...options,
711
- timeoutMs: options.timeoutMs ?? this.call.timeoutMs
712
- });
825
+ return new LocalAgent(this.runner, { ...this.call, ...options });
713
826
  }
714
827
  /**
715
828
  * No model choice runs the default model. An alias runs the CLI's equivalent
@@ -735,6 +848,9 @@ class LocalAgent {
735
848
  return { kind: "routed", engine: new LocalAgentEngine(this.runner, call) };
736
849
  }
737
850
  }
851
+ function isLimit(ms) {
852
+ return ms >= 1 && ms <= MAX_LIMIT_MS;
853
+ }
738
854
 
739
855
  class Semaphore {
740
856
  constructor(limit) {
@@ -742,9 +858,11 @@ class Semaphore {
742
858
  this.active = 0;
743
859
  this.waiting = [];
744
860
  }
745
- async run(task) {
861
+ /** Rejects with the signal's reason, without running `task`, when `signal` aborts while it waits. */
862
+ async run(task, signal) {
863
+ signal?.throwIfAborted();
746
864
  if (this.active >= this.limit) {
747
- await new Promise((resolve) => this.waiting.push(resolve));
865
+ await this.waitForTurn(signal);
748
866
  } else {
749
867
  this.active++;
750
868
  }
@@ -759,71 +877,111 @@ class Semaphore {
759
877
  }
760
878
  }
761
879
  }
880
+ /** Resolves once a finishing task hands over its place. */
881
+ waitForTurn(signal) {
882
+ return new Promise((resolve, reject) => {
883
+ const take = () => {
884
+ signal?.removeEventListener("abort", leave);
885
+ resolve();
886
+ };
887
+ const leave = () => {
888
+ this.waiting.splice(this.waiting.indexOf(take), 1);
889
+ reject(signal.reason);
890
+ };
891
+ this.waiting.push(take);
892
+ signal?.addEventListener("abort", leave, { once: true });
893
+ });
894
+ }
762
895
  }
763
896
 
764
- const DEFAULT_TIMEOUT_MS = 18e4;
765
897
  const DEFAULT_CONCURRENCY = 2;
766
898
  function localAgent(config) {
767
899
  if (!isLocalAgentId(config.agent)) {
768
900
  throw new TypeError(`Unknown local agent: ${String(config.agent)}`);
769
901
  }
770
- const settings = parseOptions(config);
771
902
  const adapter = AGENT_ADAPTERS[config.agent];
772
903
  const binaryPath = config.binaryPath ?? findOnPath(adapter.binaryName);
773
904
  if (!binaryPath) {
774
905
  throw new Error(`${adapter.binaryName} was not found on PATH. Install ${config.agent} or set binaryPath.`);
775
906
  }
776
- return setUpLocalAgent(adapter, binaryPath, settings);
907
+ return setUpLocalAgent(adapter, binaryPath, config);
777
908
  }
778
909
  async function findLocalAgent(options = {}) {
779
- const settings = parseOptions(options);
780
910
  const statuses = await detectLocalAgents();
781
911
  for (const status of statuses) {
782
912
  if (status.installed && status.loggedIn) {
783
- return setUpLocalAgent(AGENT_ADAPTERS[status.agent], status.binaryPath, settings);
913
+ return setUpLocalAgent(AGENT_ADAPTERS[status.agent], status.binaryPath, options);
784
914
  }
785
915
  }
786
916
  const summary = statuses.map((s) => `${s.agent}: ${s.installed ? "not logged in" : "not installed"}`).join(", ");
787
917
  throw new Error(`No logged-in coding agent found (${summary})`);
788
918
  }
789
- function parseOptions(options) {
790
- const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
791
- if (!Number.isInteger(concurrency) || concurrency < 1) {
792
- throw new TypeError("concurrency must be a positive integer");
793
- }
794
- if (options.model !== void 0 && options.model.trim() === "") {
795
- throw new TypeError("model must be a non-empty string; omit it to use the agent's default model");
796
- }
797
- return {
798
- model: options.model,
799
- effort: options.effort,
800
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
801
- concurrency,
802
- auth: options.auth ?? "subscription",
803
- logger: options.logger ?? new strictJsonSchema.ConsoleLogger()
804
- };
805
- }
806
- function setUpLocalAgent(adapter, binaryPath, settings) {
919
+ function setUpLocalAgent(adapter, binaryPath, options) {
807
920
  if (/\.(cmd|bat)$/i.test(binaryPath)) {
808
921
  throw new Error(
809
922
  `${binaryPath} is a .cmd shim, which local agents cannot run safely. Set binaryPath to the agent's native executable.`
810
923
  );
811
924
  }
925
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
926
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
927
+ throw new TypeError("concurrency must be a positive integer");
928
+ }
812
929
  const runner = {
813
930
  adapter,
814
931
  binaryPath,
815
- unsetEnv: settings.auth === "subscription" ? adapter.apiKeyEnvVars : [],
816
- limiter: new Semaphore(settings.concurrency),
817
- logger: settings.logger
932
+ unsetEnv: (options.auth ?? "subscription") === "subscription" ? adapter.apiKeyEnvVars : [],
933
+ limiter: new Semaphore(concurrency),
934
+ logger: options.logger ?? new strictJsonSchema.ConsoleLogger()
935
+ };
936
+ return new LocalAgent(runner, options);
937
+ }
938
+
939
+ const AGENTS = {
940
+ "claude-code": {
941
+ name: "Claude Code",
942
+ worksWith: "Claude Pro or Max, or an Anthropic Console account",
943
+ install: "npm install -g @anthropic-ai/claude-code",
944
+ signIn: "Run `claude`",
945
+ docsUrl: "https://github.com/anthropics/claude-code"
946
+ },
947
+ codex: {
948
+ name: "Codex",
949
+ worksWith: "ChatGPT Plus, Pro or Business, or an OpenAI API key",
950
+ install: "npm install -g @openai/codex",
951
+ signIn: "Run `codex login`",
952
+ docsUrl: "https://github.com/openai/codex"
953
+ },
954
+ pi: {
955
+ name: "Pi",
956
+ worksWith: "Many providers, including a Claude or ChatGPT plan",
957
+ install: "npm install -g @mariozechner/pi-coding-agent",
958
+ signIn: "Run `pi`, then type `/login`",
959
+ docsUrl: "https://github.com/badlogic/pi-mono"
960
+ }
961
+ };
962
+ function setupPrompt(agents) {
963
+ return [
964
+ "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:",
965
+ "",
966
+ ...agents.map(
967
+ (setup) => `- ${setup.name}. Works with: ${setup.worksWith}. Install: \`${setup.install}\`. Sign in, in a terminal: ${setup.signIn}.`
968
+ ),
969
+ "",
970
+ "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."
971
+ ].join("\n");
972
+ }
973
+ function localAgentSetupGuide() {
974
+ const agents = LOCAL_AGENT_IDS.map((agent) => ({ agent, ...AGENTS[agent] }));
975
+ return {
976
+ missing: "No coding agent is set up on this computer.",
977
+ 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.",
978
+ agents,
979
+ setupPrompt: setupPrompt(agents)
818
980
  };
819
- return new LocalAgent(runner, {
820
- model: settings.model,
821
- effort: settings.effort,
822
- timeoutMs: settings.timeoutMs
823
- });
824
981
  }
825
982
 
826
983
  const LINUX_MAX_ARGUMENT_BYTES = 131072;
984
+ const SHELL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
827
985
  function agentSessionMode(platform = process.platform) {
828
986
  if (platform === "win32") return "none";
829
987
  return platform === "darwin" ? "terminal" : "command";
@@ -832,6 +990,10 @@ async function startAgentSession(config) {
832
990
  if (!isLocalAgentId(config.agent)) {
833
991
  throw new TypeError(`Unknown local agent: ${String(config.agent)}`);
834
992
  }
993
+ const badName = config.unsetEnv?.find((name) => !SHELL_NAME.test(name));
994
+ if (badName !== void 0) {
995
+ throw new TypeError(`Not a shell variable name in unsetEnv: ${JSON.stringify(badName)}`);
996
+ }
835
997
  const platform = config.platform ?? process.platform;
836
998
  const mode = agentSessionMode(platform);
837
999
  if (mode === "none") {
@@ -851,7 +1013,7 @@ async function startAgentSession(config) {
851
1013
  const files = writeSessionFiles(config, binaryPath, adapter.sessionArgs(config.model, config.approval ?? "ask"));
852
1014
  if (mode === "command") return { kind: "command", files };
853
1015
  try {
854
- await node_util.promisify(node_child_process.execFile)("open", [files.launcherPath]);
1016
+ await node_util.promisify(node_child_process.execFile)("open", [files.launcherPath], { env: config.env ?? process.env });
855
1017
  } catch (error) {
856
1018
  return { kind: "open_failed", files, error: error instanceof Error ? error.message : String(error) };
857
1019
  }
@@ -866,7 +1028,7 @@ function writeSessionFiles(config, binaryPath, agentArgs) {
866
1028
  const promptPath = path__namespace.join(config.dir, `${name}.md`);
867
1029
  const launcherPath = path__namespace.join(config.dir, `${name}.command`);
868
1030
  fs__namespace.writeFileSync(promptPath, config.prompt, { mode: 384, flag: "wx" });
869
- fs__namespace.writeFileSync(launcherPath, sessionLauncher({ cwd: config.cwd, binaryPath, agentArgs, promptPath }), {
1031
+ fs__namespace.writeFileSync(launcherPath, sessionLauncher({ cwd: config.cwd, binaryPath, agentArgs, promptPath, unsetEnv: config.unsetEnv }), {
870
1032
  mode: 448,
871
1033
  flag: "wx"
872
1034
  });
@@ -876,10 +1038,17 @@ function defaultSessionName(agent) {
876
1038
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replaceAll(":", "-");
877
1039
  return `${stamp}-${agent}-${node_crypto.randomBytes(2).toString("hex")}`;
878
1040
  }
879
- function sessionLauncher({ cwd, binaryPath, agentArgs, promptPath }) {
1041
+ function sessionLauncher({
1042
+ cwd,
1043
+ binaryPath,
1044
+ agentArgs,
1045
+ promptPath,
1046
+ unsetEnv = []
1047
+ }) {
880
1048
  const command = [binaryPath, ...agentArgs].map(shellQuote).join(" ");
881
1049
  return [
882
1050
  "#!/bin/sh",
1051
+ ...unsetEnv.length > 0 ? [`unset ${unsetEnv.map(shellQuote).join(" ")}`] : [],
883
1052
  `cd ${shellQuote(cwd)} || exit 1`,
884
1053
  `exec ${command} -- "$(cat ${shellQuote(promptPath)})"`,
885
1054
  ""
@@ -895,5 +1064,6 @@ exports.detectLocalAgents = detectLocalAgents;
895
1064
  exports.findLocalAgent = findLocalAgent;
896
1065
  exports.isLocalAgentId = isLocalAgentId;
897
1066
  exports.localAgent = localAgent;
1067
+ exports.localAgentSetupGuide = localAgentSetupGuide;
898
1068
  exports.sessionLauncher = sessionLauncher;
899
1069
  exports.startAgentSession = startAgentSession;