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/esm/local-agent.mjs
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
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
|
|
522
|
+
let stopped = null;
|
|
523
|
+
let lastOutputAtMs = null;
|
|
494
524
|
let killTimer;
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
564
|
+
clearTimers();
|
|
510
565
|
clearTimeout(killTimer);
|
|
511
566
|
resolve({
|
|
512
567
|
exitCode,
|
|
513
|
-
stdout:
|
|
514
|
-
stderr:
|
|
515
|
-
|
|
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
|
-
|
|
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.
|
|
639
|
-
|
|
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,
|
|
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;
|
|
679
|
-
* default.
|
|
799
|
+
* out keep this agent's; an option given as undefined takes its default.
|
|
680
800
|
*/
|
|
681
801
|
with(options) {
|
|
682
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
793
|
-
limiter: new Semaphore(
|
|
794
|
-
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({
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 };
|