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.
- package/CHANGELOG.md +27 -0
- package/README.md +23 -3
- package/dist/chunks/{local-agent-Bj-QKikS.d.cts → local-agent-CPIXKS41.d.cts} +61 -19
- package/dist/chunks/{local-agent-Bj-QKikS.d.mts → local-agent-CPIXKS41.d.mts} +61 -19
- package/dist/cjs/index.cjs +67 -21
- package/dist/cjs/local-agent.cjs +261 -71
- package/dist/esm/index.mjs +67 -21
- package/dist/esm/local-agent.mjs +261 -72
- package/dist/index.d.cts +13 -3
- package/dist/index.d.mts +13 -3
- package/dist/local-agent.d.cts +55 -14
- package/dist/local-agent.d.mts +55 -14
- package/package.json +1 -1
package/dist/cjs/local-agent.cjs
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
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
|
|
545
|
+
let stopped = null;
|
|
546
|
+
let lastOutputAtMs = null;
|
|
517
547
|
let killTimer;
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
587
|
+
clearTimers();
|
|
533
588
|
clearTimeout(killTimer);
|
|
534
589
|
resolve({
|
|
535
590
|
exitCode,
|
|
536
|
-
stdout:
|
|
537
|
-
stderr:
|
|
538
|
-
|
|
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
|
-
|
|
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.
|
|
662
|
-
|
|
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,
|
|
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;
|
|
702
|
-
* default.
|
|
822
|
+
* out keep this agent's; an option given as undefined takes its default.
|
|
703
823
|
*/
|
|
704
824
|
with(options) {
|
|
705
|
-
|
|
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
|
-
|
|
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
|
|
865
|
+
await this.waitForTurn(signal);
|
|
748
866
|
} else {
|
|
749
867
|
this.active++;
|
|
750
868
|
}
|
|
@@ -759,71 +877,131 @@ 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,
|
|
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,
|
|
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
|
|
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:
|
|
816
|
-
limiter: new Semaphore(
|
|
817
|
-
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: {
|
|
944
|
+
macos: "brew install --cask claude-code",
|
|
945
|
+
linux: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
946
|
+
other: "npm install -g @anthropic-ai/claude-code"
|
|
947
|
+
},
|
|
948
|
+
signIn: "Run `claude`",
|
|
949
|
+
docsUrl: "https://code.claude.com/docs/en/setup"
|
|
950
|
+
},
|
|
951
|
+
codex: {
|
|
952
|
+
name: "Codex",
|
|
953
|
+
worksWith: "ChatGPT Plus, Pro or Business, or an OpenAI API key",
|
|
954
|
+
install: {
|
|
955
|
+
macos: "brew install --cask codex",
|
|
956
|
+
linux: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
|
957
|
+
other: "npm install -g @openai/codex"
|
|
958
|
+
},
|
|
959
|
+
signIn: "Run `codex login`",
|
|
960
|
+
docsUrl: "https://learn.chatgpt.com/docs/codex/cli"
|
|
961
|
+
},
|
|
962
|
+
pi: {
|
|
963
|
+
name: "Pi",
|
|
964
|
+
worksWith: "Many providers, including a Claude or ChatGPT plan",
|
|
965
|
+
install: {
|
|
966
|
+
macos: "npm install -g --ignore-scripts @earendil-works/pi-coding-agent",
|
|
967
|
+
linux: "curl -fsSL https://pi.dev/install.sh | sh",
|
|
968
|
+
other: "npm install -g --ignore-scripts @earendil-works/pi-coding-agent"
|
|
969
|
+
},
|
|
970
|
+
signIn: "Run `pi`, then type `/login`",
|
|
971
|
+
docsUrl: "https://pi.dev/docs/latest/quickstart"
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
function installCommand(commands, platform) {
|
|
975
|
+
if (platform === "darwin") return commands.macos;
|
|
976
|
+
return platform === "linux" ? commands.linux : commands.other;
|
|
977
|
+
}
|
|
978
|
+
function setupPrompt(agents) {
|
|
979
|
+
return [
|
|
980
|
+
"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:",
|
|
981
|
+
"",
|
|
982
|
+
...agents.map(
|
|
983
|
+
(setup) => `- ${setup.name}. Works with: ${setup.worksWith}. Install: \`${setup.install}\`. Sign in, in a terminal: ${setup.signIn}.`
|
|
984
|
+
),
|
|
985
|
+
"",
|
|
986
|
+
"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."
|
|
987
|
+
].join("\n");
|
|
988
|
+
}
|
|
989
|
+
function localAgentSetupGuide(platform = process.platform) {
|
|
990
|
+
const agents = LOCAL_AGENT_IDS.map((agent) => ({
|
|
991
|
+
agent,
|
|
992
|
+
...AGENTS[agent],
|
|
993
|
+
install: installCommand(AGENTS[agent].install, platform)
|
|
994
|
+
}));
|
|
995
|
+
return {
|
|
996
|
+
missing: "No coding agent is set up on this computer.",
|
|
997
|
+
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.",
|
|
998
|
+
agents,
|
|
999
|
+
setupPrompt: setupPrompt(agents)
|
|
818
1000
|
};
|
|
819
|
-
return new LocalAgent(runner, {
|
|
820
|
-
model: settings.model,
|
|
821
|
-
effort: settings.effort,
|
|
822
|
-
timeoutMs: settings.timeoutMs
|
|
823
|
-
});
|
|
824
1001
|
}
|
|
825
1002
|
|
|
826
1003
|
const LINUX_MAX_ARGUMENT_BYTES = 131072;
|
|
1004
|
+
const SHELL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
827
1005
|
function agentSessionMode(platform = process.platform) {
|
|
828
1006
|
if (platform === "win32") return "none";
|
|
829
1007
|
return platform === "darwin" ? "terminal" : "command";
|
|
@@ -832,6 +1010,10 @@ async function startAgentSession(config) {
|
|
|
832
1010
|
if (!isLocalAgentId(config.agent)) {
|
|
833
1011
|
throw new TypeError(`Unknown local agent: ${String(config.agent)}`);
|
|
834
1012
|
}
|
|
1013
|
+
const badName = config.unsetEnv?.find((name) => !SHELL_NAME.test(name));
|
|
1014
|
+
if (badName !== void 0) {
|
|
1015
|
+
throw new TypeError(`Not a shell variable name in unsetEnv: ${JSON.stringify(badName)}`);
|
|
1016
|
+
}
|
|
835
1017
|
const platform = config.platform ?? process.platform;
|
|
836
1018
|
const mode = agentSessionMode(platform);
|
|
837
1019
|
if (mode === "none") {
|
|
@@ -851,7 +1033,7 @@ async function startAgentSession(config) {
|
|
|
851
1033
|
const files = writeSessionFiles(config, binaryPath, adapter.sessionArgs(config.model, config.approval ?? "ask"));
|
|
852
1034
|
if (mode === "command") return { kind: "command", files };
|
|
853
1035
|
try {
|
|
854
|
-
await node_util.promisify(node_child_process.execFile)("open", [files.launcherPath]);
|
|
1036
|
+
await node_util.promisify(node_child_process.execFile)("open", [files.launcherPath], { env: config.env ?? process.env });
|
|
855
1037
|
} catch (error) {
|
|
856
1038
|
return { kind: "open_failed", files, error: error instanceof Error ? error.message : String(error) };
|
|
857
1039
|
}
|
|
@@ -866,7 +1048,7 @@ function writeSessionFiles(config, binaryPath, agentArgs) {
|
|
|
866
1048
|
const promptPath = path__namespace.join(config.dir, `${name}.md`);
|
|
867
1049
|
const launcherPath = path__namespace.join(config.dir, `${name}.command`);
|
|
868
1050
|
fs__namespace.writeFileSync(promptPath, config.prompt, { mode: 384, flag: "wx" });
|
|
869
|
-
fs__namespace.writeFileSync(launcherPath, sessionLauncher({ cwd: config.cwd, binaryPath, agentArgs, promptPath }), {
|
|
1051
|
+
fs__namespace.writeFileSync(launcherPath, sessionLauncher({ cwd: config.cwd, binaryPath, agentArgs, promptPath, unsetEnv: config.unsetEnv }), {
|
|
870
1052
|
mode: 448,
|
|
871
1053
|
flag: "wx"
|
|
872
1054
|
});
|
|
@@ -876,10 +1058,17 @@ function defaultSessionName(agent) {
|
|
|
876
1058
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replaceAll(":", "-");
|
|
877
1059
|
return `${stamp}-${agent}-${node_crypto.randomBytes(2).toString("hex")}`;
|
|
878
1060
|
}
|
|
879
|
-
function sessionLauncher({
|
|
1061
|
+
function sessionLauncher({
|
|
1062
|
+
cwd,
|
|
1063
|
+
binaryPath,
|
|
1064
|
+
agentArgs,
|
|
1065
|
+
promptPath,
|
|
1066
|
+
unsetEnv = []
|
|
1067
|
+
}) {
|
|
880
1068
|
const command = [binaryPath, ...agentArgs].map(shellQuote).join(" ");
|
|
881
1069
|
return [
|
|
882
1070
|
"#!/bin/sh",
|
|
1071
|
+
...unsetEnv.length > 0 ? [`unset ${unsetEnv.map(shellQuote).join(" ")}`] : [],
|
|
883
1072
|
`cd ${shellQuote(cwd)} || exit 1`,
|
|
884
1073
|
`exec ${command} -- "$(cat ${shellQuote(promptPath)})"`,
|
|
885
1074
|
""
|
|
@@ -895,5 +1084,6 @@ exports.detectLocalAgents = detectLocalAgents;
|
|
|
895
1084
|
exports.findLocalAgent = findLocalAgent;
|
|
896
1085
|
exports.isLocalAgentId = isLocalAgentId;
|
|
897
1086
|
exports.localAgent = localAgent;
|
|
1087
|
+
exports.localAgentSetupGuide = localAgentSetupGuide;
|
|
898
1088
|
exports.sessionLauncher = sessionLauncher;
|
|
899
1089
|
exports.startAgentSession = startAgentSession;
|