openmausbot 0.1.74 → 0.1.76
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/dist/assets/index-BtT047dk.css +1 -0
- package/dist/assets/{index-CwoQdM0z.js → index-C6blGSq5.js} +1 -1
- package/dist/assets/index-fKXFpB_J.js +305 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +6 -2
- package/dist-server/drivers/agents-proxy.js +133 -11
- package/dist-server/index.js +3379 -1431
- package/dist-server/mcp-gate.js +28 -18
- package/dist-server/openmausbot.js +557 -325
- package/dist-server/pair-cli.js +557 -325
- package/dist-server/permission-proxy.js +119 -14
- package/dist-server/server/agent-tool-policy.js +1 -0
- package/dist-server/server/atomic.js +37 -1
- package/dist-server/server/auto-approve.js +15 -0
- package/dist-server/server/bot-overview.js +5 -1
- package/dist-server/server/chief-of-staff.js +17 -12
- package/dist-server/server/delegations.js +177 -63
- package/dist-server/server/drivers/acp/core.js +2 -1
- package/dist-server/server/drivers/acp/grok.js +12 -1
- package/dist-server/server/drivers/acp/hermes.js +21 -3
- package/dist-server/server/drivers/agents-proxy.js +113 -10
- package/dist-server/server/drivers/claude.js +91 -17
- package/dist-server/server/drivers/codex.js +15 -4
- package/dist-server/server/drivers/openai-chat.js +90 -67
- package/dist-server/server/drivers/openai-compat.js +9 -1
- package/dist-server/server/drivers/pi.js +18 -3
- package/dist-server/server/drivers/retry.js +4 -0
- package/dist-server/server/env-path.js +18 -17
- package/dist-server/server/index.js +1172 -302
- package/dist-server/server/message-db.js +21 -0
- package/dist-server/server/peer-roster.js +48 -9
- package/dist-server/server/permission-proxy.js +90 -15
- package/dist-server/server/procs.js +19 -3
- package/dist-server/server/room-handoffs.js +237 -0
- package/dist-server/server/section-context.js +64 -14
- package/dist-server/server/setup-mode.js +6 -7
- package/dist-server/server/store.js +266 -8
- package/dist-server/server/surface.js +16 -5
- package/dist-server/server/team-backup.js +17 -1
- package/dist-server/server/team-setup-requests.js +252 -0
- package/dist-server/server/thread-events.js +10 -1
- package/dist-server/server/turn-dispatch-guard.js +31 -3
- package/dist-server/server/workspace.js +13 -0
- package/dist-server/shared/approval-mode.js +7 -0
- package/dist-server/shared/ask-question.js +172 -0
- package/dist-server/shared/provider-safety.js +6 -0
- package/dist-server/shared/team-backup.js +1 -0
- package/dist-server/shared/team-setup.js +1 -0
- package/package.json +1 -1
- package/dist/assets/index-BRLHsQ5g.js +0 -298
- package/dist/assets/index-ByLQ7a3F.css +0 -1
|
@@ -22,81 +22,104 @@ export function createOpenAIChatRuntime(options) {
|
|
|
22
22
|
createdAt: new Date().toISOString(),
|
|
23
23
|
});
|
|
24
24
|
const complete = async (messages, model, stream, signal, onDelta) => {
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!stream) {
|
|
37
|
-
const json = await response.json();
|
|
38
|
-
const message = json.choices?.[0]?.message;
|
|
39
|
-
return {
|
|
40
|
-
text: typeof message?.content === "string" ? message.content : "",
|
|
41
|
-
reasoning: options.reasoning && typeof message?.reasoning_content === "string"
|
|
42
|
-
? message.reasoning_content
|
|
43
|
-
: "",
|
|
44
|
-
usage: usageFrom(json.usage),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
if (!response.body) {
|
|
48
|
-
throw new Error(options.noBodyError ?? `${options.httpErrorLabel} returned no response body`);
|
|
49
|
-
}
|
|
50
|
-
let text = "";
|
|
51
|
-
let reasoning = "";
|
|
52
|
-
let usage = null;
|
|
53
|
-
const reader = response.body.getReader();
|
|
54
|
-
const decoder = new TextDecoder();
|
|
55
|
-
let buffer = "";
|
|
25
|
+
// Idle timer that is renewed on every received chunk during streaming
|
|
26
|
+
const timeoutController = new AbortController();
|
|
27
|
+
let idleTimer = null;
|
|
28
|
+
const resetIdleTimer = () => {
|
|
29
|
+
if (idleTimer)
|
|
30
|
+
clearTimeout(idleTimer);
|
|
31
|
+
idleTimer = setTimeout(() => {
|
|
32
|
+
timeoutController.abort(new DOMException("Streaming idle timeout elapsed", "AbortError"));
|
|
33
|
+
}, options.timeoutMs);
|
|
34
|
+
};
|
|
35
|
+
resetIdleTimer();
|
|
56
36
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
37
|
+
const activeSignal = signal
|
|
38
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
39
|
+
: timeoutController.signal;
|
|
40
|
+
const response = await fetch(`${options.apiUrl}/chat/completions`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { authorization: `Bearer ${options.apiKey}`, "content-type": "application/json" },
|
|
43
|
+
body: JSON.stringify(options.requestBody(model, messages, stream)),
|
|
44
|
+
signal: activeSignal,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const body = await response.text().catch(() => "");
|
|
48
|
+
throw new Error(`${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
if (!stream) {
|
|
51
|
+
const json = await response.json();
|
|
52
|
+
const message = json.choices?.[0]?.message;
|
|
53
|
+
return {
|
|
54
|
+
text: typeof message?.content === "string" ? message.content : "",
|
|
55
|
+
reasoning: options.reasoning && typeof message?.reasoning_content === "string"
|
|
56
|
+
? message.reasoning_content
|
|
57
|
+
: "",
|
|
58
|
+
usage: usageFrom(json.usage),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!response.body) {
|
|
62
|
+
throw new Error(options.noBodyError ?? `${options.httpErrorLabel} returned no response body`);
|
|
63
|
+
}
|
|
64
|
+
let text = "";
|
|
65
|
+
let reasoning = "";
|
|
66
|
+
let usage = null;
|
|
67
|
+
const reader = response.body.getReader();
|
|
68
|
+
const decoder = new TextDecoder();
|
|
69
|
+
let buffer = "";
|
|
70
|
+
try {
|
|
71
|
+
readLoop: for (;;) {
|
|
72
|
+
const { done, value } = await reader.read();
|
|
73
|
+
if (done) {
|
|
74
|
+
if (buffer.trim() === "data: [DONE]")
|
|
75
|
+
break;
|
|
76
|
+
throw new DOMException("Stream ended before completion", "AbortError");
|
|
86
77
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
resetIdleTimer();
|
|
79
|
+
buffer += decoder.decode(value, { stream: true });
|
|
80
|
+
let newline;
|
|
81
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
82
|
+
const line = buffer.slice(0, newline).trim();
|
|
83
|
+
buffer = buffer.slice(newline + 1);
|
|
84
|
+
if (!line.startsWith("data:"))
|
|
85
|
+
continue;
|
|
86
|
+
const data = line.slice(5).trim();
|
|
87
|
+
if (data === "[DONE]")
|
|
88
|
+
break readLoop;
|
|
89
|
+
let chunk;
|
|
90
|
+
try {
|
|
91
|
+
chunk = JSON.parse(data);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
97
|
+
const reasoningDelta = options.reasoning && typeof delta?.reasoning_content === "string"
|
|
98
|
+
? delta.reasoning_content
|
|
99
|
+
: "";
|
|
100
|
+
const contentDelta = typeof delta?.content === "string" ? delta.content : "";
|
|
101
|
+
if (reasoningDelta) {
|
|
102
|
+
reasoning += reasoningDelta;
|
|
103
|
+
onDelta?.(reasoningDelta, "reasoning_text");
|
|
104
|
+
}
|
|
105
|
+
if (contentDelta) {
|
|
106
|
+
text += contentDelta;
|
|
107
|
+
onDelta?.(contentDelta, "assistant_text");
|
|
108
|
+
}
|
|
109
|
+
if (chunk.usage)
|
|
110
|
+
usage = usageFrom(chunk.usage);
|
|
90
111
|
}
|
|
91
|
-
if (chunk.usage)
|
|
92
|
-
usage = usageFrom(chunk.usage);
|
|
93
112
|
}
|
|
94
113
|
}
|
|
114
|
+
finally {
|
|
115
|
+
await reader.cancel().catch(() => { });
|
|
116
|
+
}
|
|
117
|
+
return { text, reasoning, usage };
|
|
95
118
|
}
|
|
96
119
|
finally {
|
|
97
|
-
|
|
120
|
+
if (idleTimer)
|
|
121
|
+
clearTimeout(idleTimer);
|
|
98
122
|
}
|
|
99
|
-
return { text, reasoning, usage };
|
|
100
123
|
};
|
|
101
124
|
const messagesFor = (turn) => [
|
|
102
125
|
...(turn.system ? [{ role: "system", content: turn.system }] : []),
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createOpenAIChatRuntime } from "./openai-chat.js";
|
|
2
2
|
const DRIVER_KIND = "openai-compat";
|
|
3
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 180_000;
|
|
4
|
+
const idleTimeoutMs = () => {
|
|
5
|
+
const raw = process.env.OPENMAUS_OPENAI_COMPAT_IDLE_TIMEOUT_MS;
|
|
6
|
+
if (!raw)
|
|
7
|
+
return DEFAULT_IDLE_TIMEOUT_MS;
|
|
8
|
+
const value = Number(raw);
|
|
9
|
+
return Number.isSafeInteger(value) && value >= 1_000 && value <= 2_147_483_647 ? value : DEFAULT_IDLE_TIMEOUT_MS;
|
|
10
|
+
};
|
|
3
11
|
const DEFAULT_MODELS = {
|
|
4
12
|
default: "meta-llama/llama-3.3-70b-instruct",
|
|
5
13
|
options: [
|
|
@@ -127,7 +135,7 @@ export const OpenAICompatDriver = {
|
|
|
127
135
|
httpErrorLabel: "upstream",
|
|
128
136
|
missingKeyError: `no API key — set ${config.apiKeyEnv} or add it to the instance config`,
|
|
129
137
|
unavailableReason: `no API key — set ${config.apiKeyEnv} or add it to the instance config`,
|
|
130
|
-
timeoutMs:
|
|
138
|
+
timeoutMs: idleTimeoutMs(),
|
|
131
139
|
reasoning: true,
|
|
132
140
|
billing: "metered",
|
|
133
141
|
includeUsageInCompleted: true,
|
|
@@ -26,13 +26,14 @@ import { join } from "node:path";
|
|
|
26
26
|
import { PROVIDER_CREDENTIAL_ENV, stripWorkspaceCredentialEnv } from "../config.js";
|
|
27
27
|
import { computerProxyEnv } from "../container-computer.js";
|
|
28
28
|
import { augmentedPath } from "../env-path.js";
|
|
29
|
-
import { describeSpawnFailure, killCliTree, spawnCli } from "../procs.js";
|
|
29
|
+
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
30
30
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
31
31
|
import { EFFORT_LEVELS, newEventId, newId } from "../contracts.js";
|
|
32
32
|
import { decodeInjectId, encodeInjectId, hostApiKey, localHost, mergeLocalInject, } from "./local-inject.js";
|
|
33
33
|
import { appendNative } from "./native.js";
|
|
34
34
|
const DRIVER_KIND = "piAgent";
|
|
35
35
|
const PI_ARGS = ["--mode", "rpc", "--no-session"];
|
|
36
|
+
const PI_MODEL_UPDATE_ARGS = ["update", "--models", "--no-approve"];
|
|
36
37
|
const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
|
|
37
38
|
function readPiPromptImages(turn) {
|
|
38
39
|
return (turn.images ?? []).map((image) => ({
|
|
@@ -317,6 +318,14 @@ export async function fetchPiModels(cli, env) {
|
|
|
317
318
|
}
|
|
318
319
|
});
|
|
319
320
|
}
|
|
321
|
+
/** Refresh pi's provider-owned catalog cache. This is deliberately called
|
|
322
|
+
* only by the explicit model-picker refresh action, never during app startup.
|
|
323
|
+
* Failure is non-fatal: the caller still probes the last usable cache. */
|
|
324
|
+
export async function updatePiModelCatalog(cli, env) {
|
|
325
|
+
return new Promise((resolve) => {
|
|
326
|
+
execCli(cli, PI_MODEL_UPDATE_ARGS, { env, timeout: 60_000, maxBuffer: 1024 * 1024 }, (error) => resolve(!error));
|
|
327
|
+
});
|
|
328
|
+
}
|
|
320
329
|
function decodeConfig(raw) {
|
|
321
330
|
if (raw === null || raw === undefined)
|
|
322
331
|
return { cli: "pi", fullAuto: false };
|
|
@@ -364,7 +373,7 @@ export const PiDriver = {
|
|
|
364
373
|
const { instanceId, config } = input;
|
|
365
374
|
const catalogEnv = piEnvironment({ ...process.env, ...input.environment });
|
|
366
375
|
let models = EMPTY;
|
|
367
|
-
const
|
|
376
|
+
const readModels = async () => {
|
|
368
377
|
let base = models;
|
|
369
378
|
try {
|
|
370
379
|
const resolved = await fetchPiModels(config.cli, catalogEnv);
|
|
@@ -384,7 +393,13 @@ export const PiDriver = {
|
|
|
384
393
|
models = base;
|
|
385
394
|
}
|
|
386
395
|
};
|
|
387
|
-
|
|
396
|
+
const refreshModels = async () => {
|
|
397
|
+
await updatePiModelCatalog(config.cli, catalogEnv);
|
|
398
|
+
await readModels();
|
|
399
|
+
};
|
|
400
|
+
// Startup stays local and fast. Only the explicit Refresh button crosses
|
|
401
|
+
// pi's model-catalog network boundary.
|
|
402
|
+
await readModels();
|
|
388
403
|
const listeners = new Set();
|
|
389
404
|
// one active turn per thread
|
|
390
405
|
const active = new Map();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isProviderSafetyBlock } from "../../shared/provider-safety.js";
|
|
1
2
|
export const RETRY_MAX_ATTEMPTS = 3;
|
|
2
3
|
/** Backoff schedule before attempt N (N is 1-based over retries): 1s / 3s / 8s. */
|
|
3
4
|
export const BACKOFF_BASE_MS = [1_000, 3_000, 8_000];
|
|
@@ -42,6 +43,9 @@ const messageOf = (err) => {
|
|
|
42
43
|
*/
|
|
43
44
|
export function classifyError(err) {
|
|
44
45
|
const text = messageOf(err);
|
|
46
|
+
// A surrounding HTTP 5xx/429 must not replay a provider safety block.
|
|
47
|
+
if (isProviderSafetyBlock(text))
|
|
48
|
+
return { transient: false, reason: "provider_safety" };
|
|
45
49
|
if (err && "exitCode" in err) {
|
|
46
50
|
const { exitCode: code } = err;
|
|
47
51
|
if (code !== null && code < 0)
|
|
@@ -207,8 +207,8 @@ function isFile(p) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
/** PATHEXT-aware `which`. A path-ish cli is probed where it points. */
|
|
210
|
-
function whichWin(cli) {
|
|
211
|
-
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
210
|
+
function whichWin(cli, env) {
|
|
211
|
+
const exts = ((env ?? process.env).PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
212
212
|
// an extensionless name is not runnable on Windows, so PATHEXT wins over
|
|
213
213
|
// the bare file — npm installs both `claude` (a sh script) and `claude.cmd`
|
|
214
214
|
const probe = (base) => {
|
|
@@ -217,7 +217,8 @@ function whichWin(cli) {
|
|
|
217
217
|
};
|
|
218
218
|
if (/[\\/]/.test(cli) || /^[a-zA-Z]:/.test(cli))
|
|
219
219
|
return probe(cli);
|
|
220
|
-
|
|
220
|
+
const path = env ? (env.PATH ?? env.Path ?? "") : augmentedPath();
|
|
221
|
+
for (const dir of path.split(delimiter)) {
|
|
221
222
|
if (!dir)
|
|
222
223
|
continue;
|
|
223
224
|
const hit = probe(join(dir, cli));
|
|
@@ -229,20 +230,20 @@ function whichWin(cli) {
|
|
|
229
230
|
/** node.exe to run a script with: the one npm's shim would pick, else PATH,
|
|
230
231
|
* else this executable only when it really is Node. In a packaged app,
|
|
231
232
|
* process.execPath is Electron and must never be mistaken for node.exe. */
|
|
232
|
-
function nodeExe(near) {
|
|
233
|
+
function nodeExe(near, env) {
|
|
233
234
|
const local = join(near, "node.exe");
|
|
234
235
|
if (isFile(local))
|
|
235
236
|
return local;
|
|
236
237
|
// Ask for the executable explicitly so a custom PATHEXT ordering cannot
|
|
237
238
|
// make a stray node.cmd hide the real node.exe beside it.
|
|
238
|
-
const onPath = whichWin("node.exe");
|
|
239
|
+
const onPath = whichWin("node.exe", env);
|
|
239
240
|
if (onPath && extname(onPath).toLowerCase() === ".exe")
|
|
240
241
|
return onPath;
|
|
241
242
|
return process.versions.electron ? null : process.execPath;
|
|
242
243
|
}
|
|
243
244
|
/** npm/pnpm .cmd shims all spell their target as "%dp0%\..." (or
|
|
244
245
|
* "%~dp0\..."). Whatever of those exists on disk is what the shim runs. */
|
|
245
|
-
function parseCmdShim(shim) {
|
|
246
|
+
function parseCmdShim(shim, env) {
|
|
246
247
|
let text;
|
|
247
248
|
try {
|
|
248
249
|
text = readFileSync(shim, "utf8");
|
|
@@ -256,7 +257,7 @@ function parseCmdShim(shim) {
|
|
|
256
257
|
.filter((p) => isFile(p) && basename(p).toLowerCase() !== "node.exe");
|
|
257
258
|
const script = targets.find((p) => /\.[cm]?js$/i.test(p));
|
|
258
259
|
if (script) {
|
|
259
|
-
const node = nodeExe(dir);
|
|
260
|
+
const node = nodeExe(dir, env);
|
|
260
261
|
if (node)
|
|
261
262
|
return { command: node, args: [script] };
|
|
262
263
|
}
|
|
@@ -265,7 +266,7 @@ function parseCmdShim(shim) {
|
|
|
265
266
|
}
|
|
266
267
|
/** `#!/usr/bin/env node` → `node <script>`. Only node: nothing else has a
|
|
267
268
|
* meaningful Windows equivalent worth guessing at. */
|
|
268
|
-
function parseNodeShebang(file) {
|
|
269
|
+
function parseNodeShebang(file, env) {
|
|
269
270
|
let head = "";
|
|
270
271
|
let fd = null;
|
|
271
272
|
try {
|
|
@@ -289,7 +290,7 @@ function parseNodeShebang(file) {
|
|
|
289
290
|
}
|
|
290
291
|
if (!/^#!.*\bnode(\.exe)?\b/.test(head))
|
|
291
292
|
return null;
|
|
292
|
-
const node = nodeExe(dirname(file));
|
|
293
|
+
const node = nodeExe(dirname(file), env);
|
|
293
294
|
return node ? { command: node, args: [file] } : null;
|
|
294
295
|
}
|
|
295
296
|
/**
|
|
@@ -297,31 +298,31 @@ function parseNodeShebang(file) {
|
|
|
297
298
|
* everywhere but win32 — POSIX already resolves PATH and #! itself.
|
|
298
299
|
*/
|
|
299
300
|
/** Resolve a single command word (no tokenizer) — the platform spawn rules. */
|
|
300
|
-
function resolveWord(cli, args) {
|
|
301
|
+
function resolveWord(cli, args, env) {
|
|
301
302
|
if (process.platform !== "win32")
|
|
302
303
|
return { command: cli, args };
|
|
303
|
-
const file = whichWin(cli);
|
|
304
|
+
const file = whichWin(cli, env);
|
|
304
305
|
// not found: hand back the name so spawn reports its own ENOENT
|
|
305
306
|
if (!file)
|
|
306
307
|
return { command: cli, args };
|
|
307
308
|
const ext = extname(file).toLowerCase();
|
|
308
309
|
if (ext === ".cmd" || ext === ".bat") {
|
|
309
|
-
const direct = parseCmdShim(file);
|
|
310
|
+
const direct = parseCmdShim(file, env);
|
|
310
311
|
return direct ? { command: direct.command, args: [...direct.args, ...args] } : { command: file, args };
|
|
311
312
|
}
|
|
312
313
|
if (ext === ".exe" || ext === ".com")
|
|
313
314
|
return { command: file, args };
|
|
314
|
-
const viaNode = parseNodeShebang(file);
|
|
315
|
+
const viaNode = parseNodeShebang(file, env);
|
|
315
316
|
return viaNode ? { command: viaNode.command, args: [...viaNode.args, ...args] } : { command: file, args };
|
|
316
317
|
}
|
|
317
|
-
export function resolveCliSpawn(cli, args) {
|
|
318
|
+
export function resolveCliSpawn(cli, args, env) {
|
|
318
319
|
// An EXISTING FILE wins over the tokenizer: paths with spaces come from
|
|
319
320
|
// our own candidate list unquoted ("/Applications/My Tools/claude"), and
|
|
320
321
|
// splitting those would shred them. Only a bare word (no spaces) or an
|
|
321
322
|
// explicitly-quoted/wrapper string reaches the split below.
|
|
322
323
|
const trimmed = cli.trim();
|
|
323
324
|
if (trimmed.includes(" ") && existsSync(trimmed))
|
|
324
|
-
return resolveWord(trimmed, args);
|
|
325
|
+
return resolveWord(trimmed, args, env);
|
|
325
326
|
// A `cli` value may carry fixed leading arguments — wrapper scripts like
|
|
326
327
|
// `/usr/local/bin/ag claude agp` are one string in the Engines panel. ONE
|
|
327
328
|
// tokenizer pass, then resolve the head directly (never re-tokenized: a
|
|
@@ -335,7 +336,7 @@ export function resolveCliSpawn(cli, args) {
|
|
|
335
336
|
// empty input → hand the raw string to spawn so IT reports the ENOENT
|
|
336
337
|
if (!head)
|
|
337
338
|
return { command: cli, args };
|
|
338
|
-
return resolveWord(head, [...fixed, ...args]);
|
|
339
|
+
return resolveWord(head, [...fixed, ...args], env);
|
|
339
340
|
}
|
|
340
|
-
return resolveWord(cli, args);
|
|
341
|
+
return resolveWord(cli, args, env);
|
|
341
342
|
}
|