zidane 5.6.15 → 5.7.4
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/README.md +3 -1
- package/dist/{agent-CkJp_ZOR.d.ts → agent-BNS2nx_T.d.ts} +535 -15
- package/dist/agent-BNS2nx_T.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +4 -0
- package/dist/chat/pure.js +3 -0
- package/dist/chat.d.ts +31 -661
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +5 -3
- package/dist/chat.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
- package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/edit-utils-DnfNoj16.js +574 -0
- package/dist/edit-utils-DnfNoj16.js.map +1 -0
- package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
- package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
- package/dist/fetch-url-CPxfiXDa.js +518 -0
- package/dist/fetch-url-CPxfiXDa.js.map +1 -0
- package/dist/{image-sniff-CmlNMPMr.js → image-sniff-B7uFSNO1.js} +1 -1
- package/dist/{image-sniff-CmlNMPMr.js.map → image-sniff-B7uFSNO1.js.map} +1 -1
- package/dist/{index-CtIS28mN.d.ts → index-CZOwAJIX.d.ts} +2 -2
- package/dist/index-CZOwAJIX.d.ts.map +1 -0
- package/dist/{index-CsWckg9p.d.ts → index-Ck_AWt8P.d.ts} +3 -4
- package/dist/index-Ck_AWt8P.d.ts.map +1 -0
- package/dist/{index-BXQC3I4d.d.ts → index-KiS7w0dC.d.ts} +3 -3
- package/dist/index-KiS7w0dC.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-C55ZIcKz.js → interpolate-TySiqKzc.js} +23 -23
- package/dist/{interpolate-C55ZIcKz.js.map → interpolate-TySiqKzc.js.map} +1 -1
- package/dist/{login-CY9uShjX.js → login-BDeqENSe.js} +7 -58
- package/dist/login-BDeqENSe.js.map +1 -0
- package/dist/{mcp-DDOc8hOM.js → mcp-Kqzz-Rs_.js} +5 -5
- package/dist/{mcp-DDOc8hOM.js.map → mcp-Kqzz-Rs_.js.map} +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{messages-B-tuI2Ur.js → messages-CvRQTdbR.js} +93 -30
- package/dist/messages-CvRQTdbR.js.map +1 -0
- package/dist/{presets-CMkLtFFW.js → presets-JuOnSI-i.js} +2 -2
- package/dist/{presets-CMkLtFFW.js.map → presets-JuOnSI-i.js.map} +1 -1
- package/dist/presets.d.ts +3 -3
- package/dist/presets.js +1 -1
- package/dist/{providers-CRQQDuxx.js → providers-h4HJPbbv.js} +485 -31
- package/dist/providers-h4HJPbbv.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-BCT6eYxo.js → session-BzLou2_-.js} +2 -2
- package/dist/{session-BCT6eYxo.js.map → session-BzLou2_-.js.map} +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{stats-CIv4j3Sz.js → stats-DAKBEKjc.js} +12 -2
- package/dist/stats-DAKBEKjc.js.map +1 -0
- package/dist/{stdio-loader-OOOXzUvm.js → stdio-loader-Ce68wUmM.js} +4 -4
- package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +70 -0
- package/dist/tools/fetch-url.d.ts.map +1 -0
- package/dist/tools/fetch-url.js +2 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +190 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/{tools-0Kolu2bY.js → tools-BGtJK0vo.js} +1365 -420
- package/dist/tools-BGtJK0vo.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +1 -1
- package/dist/{turn-operations-DkLoiyF4.js → transcript-anchors-BTSZAPVc.js} +147 -2713
- package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
- package/dist/{transcript-anchors-C8IqWH4x.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
- package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
- package/dist/tui.d.ts +58 -28
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1348 -422
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-CCHfR9eC.js +1938 -0
- package/dist/turn-operations-CCHfR9eC.js.map +1 -0
- package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
- package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
- package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
- package/dist/types-BPw_i5vb.js.map +1 -0
- package/dist/{types-2PMY5Rlc.d.ts → types-CEAMIUXw.d.ts} +1 -1
- package/dist/types-CEAMIUXw.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +3 -3
- package/docs/CHAT.md +53 -6
- package/docs/SKILL.md +3 -0
- package/docs/TUI.md +7 -0
- package/package.json +18 -2
- package/dist/agent-CkJp_ZOR.d.ts.map +0 -1
- package/dist/index-BXQC3I4d.d.ts.map +0 -1
- package/dist/index-CsWckg9p.d.ts.map +0 -1
- package/dist/index-CtIS28mN.d.ts.map +0 -1
- package/dist/login-CY9uShjX.js.map +0 -1
- package/dist/messages-B-tuI2Ur.js.map +0 -1
- package/dist/providers-CRQQDuxx.js.map +0 -1
- package/dist/stats-CIv4j3Sz.js.map +0 -1
- package/dist/stdio-loader-OOOXzUvm.js.map +0 -1
- package/dist/tools-0Kolu2bY.js.map +0 -1
- package/dist/transcript-anchors-C8IqWH4x.d.ts.map +0 -1
- package/dist/turn-operations-DkLoiyF4.js.map +0 -1
- package/dist/types-2PMY5Rlc.d.ts.map +0 -1
- package/dist/types-oKPBdCmL.js.map +0 -1
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { c as
|
|
3
|
-
import { n as
|
|
4
|
-
import { E as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-
|
|
5
|
-
import { t as reconcileImageMediaType } from "./image-sniff-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { n as
|
|
1
|
+
import { c as formatTaskStatus, f as buildContextBreakdown, h as utf8ByteLength, l as formatTaskSummary, n as stripLineNumberPrefixes, r as styleReplacementForVia, s as formatDuration, t as resolveOldString, u as previewLine } from "./edit-utils-DnfNoj16.js";
|
|
2
|
+
import { a as createCursorOAuthProvider, c as anthropic, d as ANTHROPIC_EXTRA_MODELS, f as FAST_MODE_OPTIONS, n as openai, o as generatePkce, r as local, s as cerebras, t as openrouter } from "./providers-h4HJPbbv.js";
|
|
3
|
+
import { f as toTypedError, i as AgentProviderError, l as errorMessage, n as AgentBudgetExceededError, o as AgentToolPairingError, t as AgentAbortedError } from "./errors-CoQnKRf1.js";
|
|
4
|
+
import { E as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, k as renderSystemForWire, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-CvRQTdbR.js";
|
|
5
|
+
import { t as reconcileImageMediaType } from "./image-sniff-B7uFSNO1.js";
|
|
6
|
+
import { n as createProcessContext } from "./contexts-BD2U_xpi.js";
|
|
7
|
+
import { n as toolOutputByteLength, t as DEFAULT_AGENT_CLOCK } from "./types-BPw_i5vb.js";
|
|
8
|
+
import { b as escapeXml, f as installAllowedToolsGate, g as validateResourcePath, n as resolveSkills, t as interpolateShellCommands, u as buildCatalog, y as createSkillActivationState } from "./interpolate-TySiqKzc.js";
|
|
9
|
+
import { t as connectMcpServers } from "./mcp-Kqzz-Rs_.js";
|
|
10
|
+
import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
|
|
9
11
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
12
|
import { createHooks } from "hookable";
|
|
11
|
-
import {
|
|
12
|
-
import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
13
|
+
import { getModel, getModels } from "@earendil-works/pi-ai";
|
|
13
14
|
import { Buffer } from "node:buffer";
|
|
15
|
+
import { refreshAnthropicToken, refreshOpenAICodexToken, registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
|
|
16
|
+
import { createServer } from "node:http";
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
14
19
|
//#region src/aliasing.ts
|
|
15
20
|
/**
|
|
16
21
|
* Build alias lookup maps from a `toolAliases` record.
|
|
@@ -144,126 +149,966 @@ function rewriteMessagesToWire(messages, maps) {
|
|
|
144
149
|
}));
|
|
145
150
|
}
|
|
146
151
|
//#endregion
|
|
147
|
-
//#region src/chat/
|
|
148
|
-
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
152
|
+
//#region src/chat/oauth-page/server.ts
|
|
153
|
+
const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1";
|
|
154
|
+
function buildRequestHandler(opts, pending) {
|
|
155
|
+
return (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
158
|
+
if (url.pathname !== opts.path) {
|
|
159
|
+
respondError(res, 404, opts, "Callback route not found.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const error = url.searchParams.get("error");
|
|
163
|
+
if (error) {
|
|
164
|
+
respondError(res, 400, opts, `${opts.providerName} authentication did not complete.`, `Error: ${error}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const code = url.searchParams.get("code");
|
|
168
|
+
const state = url.searchParams.get("state");
|
|
169
|
+
if (!code || !state) {
|
|
170
|
+
respondError(res, 400, opts, "Missing code or state parameter.");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (state !== opts.expectedState) {
|
|
174
|
+
respondError(res, 400, opts, "State mismatch.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
178
|
+
res.end(opts.renderPage({
|
|
179
|
+
kind: "success",
|
|
180
|
+
provider: opts.providerName,
|
|
181
|
+
message: opts.successMessage
|
|
182
|
+
}));
|
|
183
|
+
pending.settle({
|
|
184
|
+
code,
|
|
185
|
+
state
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
respondError(res, 500, opts, "Internal error while processing the callback.");
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function respondError(res, status, opts, message, details) {
|
|
193
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
194
|
+
res.end(opts.renderPage({
|
|
195
|
+
kind: "error",
|
|
196
|
+
provider: opts.providerName,
|
|
197
|
+
message,
|
|
198
|
+
details
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
function buildStubHandle(redirectUri, server) {
|
|
202
|
+
return {
|
|
203
|
+
redirectUri,
|
|
204
|
+
waitForCode: async () => null,
|
|
205
|
+
cancelWait: () => {},
|
|
206
|
+
close: () => {
|
|
207
|
+
try {
|
|
208
|
+
server?.close();
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Start the loopback HTTP callback server. The returned handle is the
|
|
215
|
+
* caller's contract — they `await waitForCode()`, race it against manual
|
|
216
|
+
* paste, then `close()` in a `finally`.
|
|
217
|
+
*/
|
|
218
|
+
async function startCallbackServer(opts) {
|
|
219
|
+
const onListenError = opts.onListenError ?? "reject";
|
|
220
|
+
const redirectUri = `http://localhost:${opts.port}${opts.path}`;
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
let settled = false;
|
|
223
|
+
const pending = { settle: () => {} };
|
|
224
|
+
const waitPromise = new Promise((resolveWait) => {
|
|
225
|
+
pending.settle = (value) => {
|
|
226
|
+
if (settled) return;
|
|
227
|
+
settled = true;
|
|
228
|
+
resolveWait(value);
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
const server = createServer(buildRequestHandler(opts, pending));
|
|
232
|
+
server.on("error", (err) => {
|
|
233
|
+
if (onListenError === "resolveWithStub") {
|
|
234
|
+
pending.settle(null);
|
|
235
|
+
resolve(buildStubHandle(redirectUri, server));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
reject(err);
|
|
239
|
+
});
|
|
240
|
+
server.listen(opts.port, CALLBACK_HOST, () => {
|
|
241
|
+
resolve({
|
|
242
|
+
redirectUri,
|
|
243
|
+
waitForCode: () => waitPromise,
|
|
244
|
+
cancelWait: () => pending.settle(null),
|
|
245
|
+
close: () => {
|
|
246
|
+
try {
|
|
247
|
+
server.close();
|
|
248
|
+
} catch {}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/chat/oauth-page/anthropic.ts
|
|
256
|
+
const CLIENT_ID$1 = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
|
|
257
|
+
const AUTHORIZE_URL$1 = "https://claude.ai/oauth/authorize";
|
|
258
|
+
const TOKEN_URL$1 = "https://platform.claude.com/v1/oauth/token";
|
|
259
|
+
const CALLBACK_PORT$1 = 53692;
|
|
260
|
+
const CALLBACK_PATH$1 = "/callback";
|
|
261
|
+
const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
262
|
+
const PROVIDER_NAME$1 = "Anthropic";
|
|
263
|
+
/**
|
|
264
|
+
* Parse what the user pasted into the manual-code prompt. Accepts:
|
|
265
|
+
* - the bare authorization code
|
|
266
|
+
* - the full `redirect_uri?code=...&state=...` URL
|
|
267
|
+
* - the `code#state` shorthand Anthropic surfaces on the page
|
|
268
|
+
* - a raw query string with `code=...&state=...`
|
|
269
|
+
*
|
|
270
|
+
* Matches pi-ai's parse table so an existing user's muscle memory still works.
|
|
271
|
+
*/
|
|
272
|
+
function parseAuthorizationInput$1(input) {
|
|
273
|
+
const value = input.trim();
|
|
274
|
+
if (!value) return {};
|
|
275
|
+
try {
|
|
276
|
+
const url = new URL(value);
|
|
277
|
+
return {
|
|
278
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
279
|
+
state: url.searchParams.get("state") ?? void 0
|
|
280
|
+
};
|
|
281
|
+
} catch {}
|
|
282
|
+
if (value.includes("#")) {
|
|
283
|
+
const [code, state] = value.split("#", 2);
|
|
284
|
+
return {
|
|
285
|
+
code,
|
|
286
|
+
state
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (value.includes("code=")) {
|
|
290
|
+
const params = new URLSearchParams(value);
|
|
291
|
+
return {
|
|
292
|
+
code: params.get("code") ?? void 0,
|
|
293
|
+
state: params.get("state") ?? void 0
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return { code: value };
|
|
297
|
+
}
|
|
298
|
+
async function postJson(url, body) {
|
|
299
|
+
const response = await fetch(url, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: {
|
|
302
|
+
"Content-Type": "application/json",
|
|
303
|
+
"Accept": "application/json"
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify(body),
|
|
306
|
+
signal: AbortSignal.timeout(3e4)
|
|
307
|
+
});
|
|
308
|
+
const responseBody = await response.text();
|
|
309
|
+
if (!response.ok) throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);
|
|
310
|
+
return responseBody;
|
|
311
|
+
}
|
|
312
|
+
async function exchangeAuthorizationCode$1(code, state, verifier, redirectUri) {
|
|
313
|
+
const responseBody = await postJson(TOKEN_URL$1, {
|
|
314
|
+
grant_type: "authorization_code",
|
|
315
|
+
client_id: CLIENT_ID$1,
|
|
316
|
+
code,
|
|
317
|
+
state,
|
|
318
|
+
redirect_uri: redirectUri,
|
|
319
|
+
code_verifier: verifier
|
|
320
|
+
});
|
|
321
|
+
const tokenData = JSON.parse(responseBody);
|
|
322
|
+
return {
|
|
323
|
+
refresh: tokenData.refresh_token,
|
|
324
|
+
access: tokenData.access_token,
|
|
325
|
+
expires: Date.now() + tokenData.expires_in * 1e3 - 300 * 1e3
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async function loginAnthropicWithCustomPage(options) {
|
|
329
|
+
const { verifier, challenge } = await generatePkce();
|
|
330
|
+
const server = await startCallbackServer({
|
|
331
|
+
port: CALLBACK_PORT$1,
|
|
332
|
+
path: CALLBACK_PATH$1,
|
|
333
|
+
expectedState: verifier,
|
|
334
|
+
providerName: PROVIDER_NAME$1,
|
|
335
|
+
renderPage: options.renderPage,
|
|
336
|
+
successMessage: "Anthropic authentication completed. You can close this window.",
|
|
337
|
+
onListenError: "reject"
|
|
338
|
+
});
|
|
339
|
+
let code;
|
|
340
|
+
let state;
|
|
341
|
+
try {
|
|
342
|
+
const authParams = new URLSearchParams({
|
|
343
|
+
code: "true",
|
|
344
|
+
client_id: CLIENT_ID$1,
|
|
345
|
+
response_type: "code",
|
|
346
|
+
redirect_uri: server.redirectUri,
|
|
347
|
+
scope: SCOPES,
|
|
348
|
+
code_challenge: challenge,
|
|
349
|
+
code_challenge_method: "S256",
|
|
350
|
+
state: verifier
|
|
351
|
+
});
|
|
352
|
+
options.onAuth({
|
|
353
|
+
url: `${AUTHORIZE_URL$1}?${authParams.toString()}`,
|
|
354
|
+
instructions: "Complete login in your browser. If the browser is on another machine, paste the final redirect URL here."
|
|
355
|
+
});
|
|
356
|
+
if (options.onManualCodeInput) {
|
|
357
|
+
let manualInput;
|
|
358
|
+
let manualError;
|
|
359
|
+
const manualPromise = options.onManualCodeInput().then((input) => {
|
|
360
|
+
manualInput = input;
|
|
361
|
+
server.cancelWait();
|
|
362
|
+
}).catch((err) => {
|
|
363
|
+
manualError = err instanceof Error ? err : new Error(String(err));
|
|
364
|
+
server.cancelWait();
|
|
365
|
+
});
|
|
366
|
+
const result = await server.waitForCode();
|
|
367
|
+
if (manualError) throw manualError;
|
|
368
|
+
if (result?.code) {
|
|
369
|
+
code = result.code;
|
|
370
|
+
state = result.state;
|
|
371
|
+
} else if (manualInput) {
|
|
372
|
+
const parsed = parseAuthorizationInput$1(manualInput);
|
|
373
|
+
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
|
374
|
+
code = parsed.code;
|
|
375
|
+
state = parsed.state ?? verifier;
|
|
376
|
+
}
|
|
377
|
+
if (!code) {
|
|
378
|
+
await manualPromise;
|
|
379
|
+
if (manualError) throw manualError;
|
|
380
|
+
if (manualInput) {
|
|
381
|
+
const parsed = parseAuthorizationInput$1(manualInput);
|
|
382
|
+
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
|
383
|
+
code = parsed.code;
|
|
384
|
+
state = parsed.state ?? verifier;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
const result = await server.waitForCode();
|
|
389
|
+
if (result?.code) {
|
|
390
|
+
code = result.code;
|
|
391
|
+
state = result.state;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!code) {
|
|
395
|
+
const parsed = parseAuthorizationInput$1(await options.onPrompt({
|
|
396
|
+
message: "Paste the authorization code or full redirect URL:",
|
|
397
|
+
placeholder: server.redirectUri
|
|
398
|
+
}));
|
|
399
|
+
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
|
400
|
+
code = parsed.code;
|
|
401
|
+
state = parsed.state ?? verifier;
|
|
402
|
+
}
|
|
403
|
+
if (!code) throw new Error("Missing authorization code");
|
|
404
|
+
if (!state) throw new Error("Missing OAuth state");
|
|
405
|
+
options.onProgress?.("Exchanging authorization code for tokens...");
|
|
406
|
+
return await exchangeAuthorizationCode$1(code, state, verifier, server.redirectUri);
|
|
407
|
+
} finally {
|
|
408
|
+
server.close();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Build an `OAuthProviderInterface` that behaves identically to pi-ai's
|
|
413
|
+
* `anthropicOAuthProvider` except for the callback page HTML. Drop this
|
|
414
|
+
* onto `ProviderDescriptor.oauthProvider` to override.
|
|
415
|
+
*/
|
|
416
|
+
function createAnthropicOAuthProviderWithCustomPage(renderPage) {
|
|
417
|
+
return {
|
|
418
|
+
id: "anthropic",
|
|
419
|
+
name: "Anthropic (Claude Pro/Max)",
|
|
420
|
+
usesCallbackServer: true,
|
|
421
|
+
async login(callbacks) {
|
|
422
|
+
return loginAnthropicWithCustomPage({
|
|
423
|
+
renderPage,
|
|
424
|
+
onAuth: callbacks.onAuth,
|
|
425
|
+
onPrompt: callbacks.onPrompt,
|
|
426
|
+
onProgress: callbacks.onProgress,
|
|
427
|
+
onManualCodeInput: callbacks.onManualCodeInput
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
async refreshToken(credentials) {
|
|
431
|
+
return refreshAnthropicToken(credentials.refresh);
|
|
432
|
+
},
|
|
433
|
+
getApiKey(credentials) {
|
|
434
|
+
return credentials.access;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/chat/oauth-page/openai-codex.ts
|
|
440
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
441
|
+
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
442
|
+
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
443
|
+
const CALLBACK_PORT = 1455;
|
|
444
|
+
const CALLBACK_PATH = "/auth/callback";
|
|
445
|
+
const SCOPE = "openid profile email offline_access";
|
|
446
|
+
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
447
|
+
const PROVIDER_NAME = "OpenAI";
|
|
448
|
+
function createState() {
|
|
449
|
+
return randomBytes(16).toString("hex");
|
|
450
|
+
}
|
|
451
|
+
function parseAuthorizationInput(input) {
|
|
452
|
+
const value = input.trim();
|
|
453
|
+
if (!value) return {};
|
|
454
|
+
try {
|
|
455
|
+
const url = new URL(value);
|
|
456
|
+
return {
|
|
457
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
458
|
+
state: url.searchParams.get("state") ?? void 0
|
|
459
|
+
};
|
|
460
|
+
} catch {}
|
|
461
|
+
if (value.includes("#")) {
|
|
462
|
+
const [code, state] = value.split("#", 2);
|
|
463
|
+
return {
|
|
464
|
+
code,
|
|
465
|
+
state
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (value.includes("code=")) {
|
|
469
|
+
const params = new URLSearchParams(value);
|
|
470
|
+
return {
|
|
471
|
+
code: params.get("code") ?? void 0,
|
|
472
|
+
state: params.get("state") ?? void 0
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return { code: value };
|
|
476
|
+
}
|
|
477
|
+
function decodeJwt(token) {
|
|
478
|
+
try {
|
|
479
|
+
const parts = token.split(".");
|
|
480
|
+
if (parts.length !== 3) return null;
|
|
481
|
+
const payload = parts[1] ?? "";
|
|
482
|
+
const decoded = atob(payload);
|
|
483
|
+
return JSON.parse(decoded);
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function getAccountId(accessToken) {
|
|
489
|
+
const accountId = (decodeJwt(accessToken)?.[JWT_CLAIM_PATH])?.chatgpt_account_id;
|
|
490
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
491
|
+
}
|
|
492
|
+
async function exchangeAuthorizationCode(code, verifier, redirectUri) {
|
|
493
|
+
const response = await fetch(TOKEN_URL, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
496
|
+
body: new URLSearchParams({
|
|
497
|
+
grant_type: "authorization_code",
|
|
498
|
+
client_id: CLIENT_ID,
|
|
499
|
+
code,
|
|
500
|
+
code_verifier: verifier,
|
|
501
|
+
redirect_uri: redirectUri
|
|
502
|
+
})
|
|
503
|
+
});
|
|
504
|
+
if (!response.ok) {
|
|
505
|
+
const text = await response.text().catch(() => "");
|
|
506
|
+
return {
|
|
507
|
+
type: "failed",
|
|
508
|
+
message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const json = await response.json();
|
|
512
|
+
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") return {
|
|
513
|
+
type: "failed",
|
|
514
|
+
message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
type: "success",
|
|
518
|
+
access: json.access_token,
|
|
519
|
+
refresh: json.refresh_token,
|
|
520
|
+
expires: Date.now() + json.expires_in * 1e3
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function loginOpenAICodexWithCustomPage(options) {
|
|
524
|
+
const { verifier, challenge } = await generatePkce();
|
|
525
|
+
const state = createState();
|
|
526
|
+
const originator = options.originator ?? "pi";
|
|
527
|
+
const server = await startCallbackServer({
|
|
528
|
+
port: CALLBACK_PORT,
|
|
529
|
+
path: CALLBACK_PATH,
|
|
530
|
+
expectedState: state,
|
|
531
|
+
providerName: PROVIDER_NAME,
|
|
532
|
+
renderPage: options.renderPage,
|
|
533
|
+
successMessage: "OpenAI authentication completed. You can close this window.",
|
|
534
|
+
onListenError: "resolveWithStub"
|
|
535
|
+
});
|
|
536
|
+
const authUrl = new URL(AUTHORIZE_URL);
|
|
537
|
+
authUrl.searchParams.set("response_type", "code");
|
|
538
|
+
authUrl.searchParams.set("client_id", CLIENT_ID);
|
|
539
|
+
authUrl.searchParams.set("redirect_uri", server.redirectUri);
|
|
540
|
+
authUrl.searchParams.set("scope", SCOPE);
|
|
541
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
542
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
543
|
+
authUrl.searchParams.set("state", state);
|
|
544
|
+
authUrl.searchParams.set("id_token_add_organizations", "true");
|
|
545
|
+
authUrl.searchParams.set("codex_cli_simplified_flow", "true");
|
|
546
|
+
authUrl.searchParams.set("originator", originator);
|
|
547
|
+
options.onAuth({
|
|
548
|
+
url: authUrl.toString(),
|
|
549
|
+
instructions: "A browser window should open. Complete login to finish."
|
|
550
|
+
});
|
|
551
|
+
let code;
|
|
552
|
+
try {
|
|
553
|
+
if (options.onManualCodeInput) {
|
|
554
|
+
let manualCode;
|
|
555
|
+
let manualError;
|
|
556
|
+
const manualPromise = options.onManualCodeInput().then((input) => {
|
|
557
|
+
manualCode = input;
|
|
558
|
+
server.cancelWait();
|
|
559
|
+
}).catch((err) => {
|
|
560
|
+
manualError = err instanceof Error ? err : new Error(String(err));
|
|
561
|
+
server.cancelWait();
|
|
562
|
+
});
|
|
563
|
+
const result = await server.waitForCode();
|
|
564
|
+
if (manualError) throw manualError;
|
|
565
|
+
if (result?.code) code = result.code;
|
|
566
|
+
else if (manualCode) {
|
|
567
|
+
const parsed = parseAuthorizationInput(manualCode);
|
|
568
|
+
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
569
|
+
code = parsed.code;
|
|
570
|
+
}
|
|
571
|
+
if (!code) {
|
|
572
|
+
await manualPromise;
|
|
573
|
+
if (manualError) throw manualError;
|
|
574
|
+
if (manualCode) {
|
|
575
|
+
const parsed = parseAuthorizationInput(manualCode);
|
|
576
|
+
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
577
|
+
code = parsed.code;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
const result = await server.waitForCode();
|
|
582
|
+
if (result?.code) code = result.code;
|
|
583
|
+
}
|
|
584
|
+
if (!code) {
|
|
585
|
+
const parsed = parseAuthorizationInput(await options.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }));
|
|
586
|
+
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
587
|
+
code = parsed.code;
|
|
588
|
+
}
|
|
589
|
+
if (!code) throw new Error("Missing authorization code");
|
|
590
|
+
const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri);
|
|
591
|
+
if (tokenResult.type !== "success") throw new Error(tokenResult.message);
|
|
592
|
+
const accountId = getAccountId(tokenResult.access);
|
|
593
|
+
if (!accountId) throw new Error("Failed to extract accountId from token");
|
|
594
|
+
return {
|
|
595
|
+
access: tokenResult.access,
|
|
596
|
+
refresh: tokenResult.refresh,
|
|
597
|
+
expires: tokenResult.expires,
|
|
598
|
+
accountId
|
|
599
|
+
};
|
|
600
|
+
} finally {
|
|
601
|
+
server.close();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Build an `OAuthProviderInterface` that behaves identically to pi-ai's
|
|
606
|
+
* `openaiCodexOAuthProvider` except for the callback page HTML.
|
|
607
|
+
*/
|
|
608
|
+
function createOpenAICodexOAuthProviderWithCustomPage(renderPage) {
|
|
609
|
+
return {
|
|
610
|
+
id: "openai-codex",
|
|
611
|
+
name: "ChatGPT Plus/Pro (Codex Subscription)",
|
|
612
|
+
usesCallbackServer: true,
|
|
613
|
+
async login(callbacks) {
|
|
614
|
+
return loginOpenAICodexWithCustomPage({
|
|
615
|
+
renderPage,
|
|
616
|
+
onAuth: callbacks.onAuth,
|
|
617
|
+
onPrompt: callbacks.onPrompt,
|
|
618
|
+
onProgress: callbacks.onProgress,
|
|
619
|
+
onManualCodeInput: callbacks.onManualCodeInput
|
|
620
|
+
});
|
|
621
|
+
},
|
|
622
|
+
async refreshToken(credentials) {
|
|
623
|
+
return refreshOpenAICodexToken(credentials.refresh);
|
|
624
|
+
},
|
|
625
|
+
getApiKey(credentials) {
|
|
626
|
+
return credentials.access;
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/chat/oauth-page/render.ts
|
|
632
|
+
function escapeHtml(value) {
|
|
633
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Default zidane-themed page. Visually neutral but distinguishable from
|
|
637
|
+
* pi-ai's stock page — dark background, mono headings, no logo (host can
|
|
638
|
+
* pass a custom renderer to add one).
|
|
639
|
+
*/
|
|
640
|
+
const renderDefaultCallbackPage = (page) => {
|
|
641
|
+
const heading = escapeHtml(page.kind === "success" ? `Signed in to ${page.provider}` : `Could not sign in to ${page.provider}`);
|
|
642
|
+
const message = escapeHtml(page.message);
|
|
643
|
+
const details = page.details ? escapeHtml(page.details) : void 0;
|
|
644
|
+
return `<!doctype html>
|
|
645
|
+
<html lang="en">
|
|
646
|
+
<head>
|
|
647
|
+
<meta charset="utf-8" />
|
|
648
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
649
|
+
<title>${heading}</title>
|
|
650
|
+
<style>
|
|
651
|
+
:root {
|
|
652
|
+
--text: #f4f4f5;
|
|
653
|
+
--text-dim: #a1a1aa;
|
|
654
|
+
--accent: ${page.kind === "success" ? "#22d3ee" : "#f87171"};
|
|
655
|
+
--page-bg: #0a0a0a;
|
|
656
|
+
--panel-bg: #131316;
|
|
657
|
+
--border: #27272a;
|
|
658
|
+
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
659
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
660
|
+
}
|
|
661
|
+
* { box-sizing: border-box; }
|
|
662
|
+
html { color-scheme: dark; }
|
|
663
|
+
body {
|
|
664
|
+
margin: 0;
|
|
665
|
+
min-height: 100vh;
|
|
666
|
+
display: flex;
|
|
667
|
+
align-items: center;
|
|
668
|
+
justify-content: center;
|
|
669
|
+
padding: 24px;
|
|
670
|
+
background: var(--page-bg);
|
|
671
|
+
color: var(--text);
|
|
672
|
+
font-family: var(--font-sans);
|
|
673
|
+
}
|
|
674
|
+
main {
|
|
675
|
+
width: 100%;
|
|
676
|
+
max-width: 520px;
|
|
677
|
+
padding: 32px;
|
|
678
|
+
background: var(--panel-bg);
|
|
679
|
+
border: 1px solid var(--border);
|
|
680
|
+
border-radius: 12px;
|
|
681
|
+
}
|
|
682
|
+
.label {
|
|
683
|
+
font-family: var(--font-mono);
|
|
684
|
+
font-size: 12px;
|
|
685
|
+
letter-spacing: 0.08em;
|
|
686
|
+
text-transform: uppercase;
|
|
687
|
+
color: var(--accent);
|
|
688
|
+
margin-bottom: 12px;
|
|
689
|
+
}
|
|
690
|
+
h1 {
|
|
691
|
+
margin: 0 0 12px;
|
|
692
|
+
font-size: 22px;
|
|
693
|
+
font-weight: 600;
|
|
694
|
+
letter-spacing: -0.01em;
|
|
695
|
+
color: var(--text);
|
|
696
|
+
}
|
|
697
|
+
p {
|
|
698
|
+
margin: 0;
|
|
699
|
+
line-height: 1.6;
|
|
700
|
+
color: var(--text-dim);
|
|
701
|
+
font-size: 14px;
|
|
702
|
+
}
|
|
703
|
+
.details {
|
|
704
|
+
margin-top: 18px;
|
|
705
|
+
padding: 12px 14px;
|
|
706
|
+
background: var(--page-bg);
|
|
707
|
+
border: 1px solid var(--border);
|
|
708
|
+
border-radius: 8px;
|
|
709
|
+
font-family: var(--font-mono);
|
|
710
|
+
font-size: 12px;
|
|
711
|
+
color: var(--text-dim);
|
|
712
|
+
white-space: pre-wrap;
|
|
713
|
+
word-break: break-word;
|
|
714
|
+
}
|
|
715
|
+
</style>
|
|
716
|
+
</head>
|
|
717
|
+
<body>
|
|
718
|
+
<main>
|
|
719
|
+
<div class="label">zidane · OAuth · ${escapeHtml(page.provider)}</div>
|
|
720
|
+
<h1>${heading}</h1>
|
|
721
|
+
<p>${message}</p>
|
|
722
|
+
${details ? `<div class="details">${details}</div>` : ""}
|
|
723
|
+
</main>
|
|
724
|
+
</body>
|
|
725
|
+
</html>`;
|
|
726
|
+
};
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/chat/oauth-page/index.ts
|
|
729
|
+
/**
|
|
730
|
+
* Bundle helper — returns both Anthropic + OpenAI Codex providers wired
|
|
731
|
+
* with the same renderer. Hosts that want different renderers per provider
|
|
732
|
+
* should call the individual `create…WithCustomPage` factories instead.
|
|
733
|
+
*/
|
|
734
|
+
function createCustomCallbackOAuthProviders(renderPage) {
|
|
735
|
+
return {
|
|
736
|
+
anthropic: createAnthropicOAuthProviderWithCustomPage(renderPage),
|
|
737
|
+
openaiCodex: createOpenAICodexOAuthProviderWithCustomPage(renderPage)
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
let cursorRegistered = false;
|
|
741
|
+
/**
|
|
742
|
+
* Register Cursor with pi-ai's OAuth registry.
|
|
743
|
+
*
|
|
744
|
+
* Unlike Anthropic / Codex, Cursor is **not** built into pi-ai, so
|
|
745
|
+
* `getOAuthApiKey('cursor', …)` (used by `resolveOAuthApiKey` for lazy
|
|
746
|
+
* token refresh) would throw "Unknown OAuth provider" until we register it.
|
|
747
|
+
* Call once at startup from both the CLI auth path and the TUI. Idempotent.
|
|
257
748
|
*/
|
|
258
|
-
function
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
else if (path.startsWith(`${h}/`)) display = `~${path.slice(h.length)}`;
|
|
749
|
+
function registerCursorOAuthProvider() {
|
|
750
|
+
const provider = createCursorOAuthProvider();
|
|
751
|
+
if (!cursorRegistered) {
|
|
752
|
+
registerOAuthProvider(provider);
|
|
753
|
+
cursorRegistered = true;
|
|
264
754
|
}
|
|
265
|
-
|
|
266
|
-
|
|
755
|
+
return provider;
|
|
756
|
+
}
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/chat/providers.ts
|
|
759
|
+
/**
|
|
760
|
+
* pi-ai's stock Anthropic + Codex OAuth providers bake their own callback
|
|
761
|
+
* HTML; we route both through `renderDefaultCallbackPage` so the post-redirect
|
|
762
|
+
* page matches zidane's theme. The override lives in `src/chat/oauth-page/`
|
|
763
|
+
* — see that folder's `index.ts` for the deletion path once pi-ai exposes
|
|
764
|
+
* a `renderCallbackPage` hook upstream.
|
|
765
|
+
*/
|
|
766
|
+
const { anthropic: anthropicOAuthProvider, openaiCodex: openaiCodexOAuthProvider } = createCustomCallbackOAuthProviders(renderDefaultCallbackPage);
|
|
767
|
+
registerCursorOAuthProvider();
|
|
768
|
+
/** Convenience accessor — returns `credentialFileKey ?? key`. */
|
|
769
|
+
function credKeyOf(desc) {
|
|
770
|
+
return desc.credentialFileKey ?? desc.key;
|
|
771
|
+
}
|
|
772
|
+
/** Convenience accessor — returns `piProviderId ?? key`. */
|
|
773
|
+
function piIdOf(desc) {
|
|
774
|
+
return desc.piProviderId ?? desc.key;
|
|
775
|
+
}
|
|
776
|
+
const anthropicDescriptor = {
|
|
777
|
+
key: "anthropic",
|
|
778
|
+
label: "Anthropic",
|
|
779
|
+
factory: anthropic,
|
|
780
|
+
defaultModel: "claude-opus-4-8",
|
|
781
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
782
|
+
apiKeyPlaceholder: "sk-ant-…",
|
|
783
|
+
oauthProvider: anthropicOAuthProvider,
|
|
784
|
+
oauthHint: "Claude Pro/Max subscription",
|
|
785
|
+
extraModels: ANTHROPIC_EXTRA_MODELS,
|
|
786
|
+
optionsFor: (id) => FAST_MODE_OPTIONS[id] ? [FAST_MODE_OPTIONS[id]] : void 0
|
|
787
|
+
};
|
|
788
|
+
const openaiDescriptor = {
|
|
789
|
+
key: "openai",
|
|
790
|
+
label: "OpenAI",
|
|
791
|
+
factory: openai,
|
|
792
|
+
defaultModel: "gpt-5.5",
|
|
793
|
+
envKey: "OPENAI_CODEX_API_KEY",
|
|
794
|
+
credentialFileKey: "openai-codex",
|
|
795
|
+
piProviderId: "openai-codex",
|
|
796
|
+
apiKeyPlaceholder: "sk-… or eyJ… (Codex)",
|
|
797
|
+
oauthProvider: openaiCodexOAuthProvider
|
|
798
|
+
};
|
|
799
|
+
const openrouterDescriptor = {
|
|
800
|
+
key: "openrouter",
|
|
801
|
+
label: "OpenRouter",
|
|
802
|
+
factory: openrouter,
|
|
803
|
+
defaultModel: "anthropic/claude-sonnet-4-6",
|
|
804
|
+
envKey: "OPENROUTER_API_KEY",
|
|
805
|
+
apiKeyPlaceholder: "sk-or-…"
|
|
806
|
+
};
|
|
807
|
+
const cerebrasDescriptor = {
|
|
808
|
+
key: "cerebras",
|
|
809
|
+
label: "Cerebras",
|
|
810
|
+
factory: cerebras,
|
|
811
|
+
defaultModel: "zai-glm-4.7",
|
|
812
|
+
envKey: "CEREBRAS_API_KEY",
|
|
813
|
+
apiKeyPlaceholder: "csk-…"
|
|
814
|
+
};
|
|
815
|
+
/**
|
|
816
|
+
* Conservative context-window assumption for a user-configured local model.
|
|
817
|
+
* Local runtimes expose no reliable per-model metadata over the OpenAI-compat
|
|
818
|
+
* `/v1/models` endpoint (it returns ids, not context sizes), so we pick a
|
|
819
|
+
* floor that fits the smallest mainstream OSS models (Llama 3.x 8B, Qwen2.5)
|
|
820
|
+
* without over-promising. The footer's context indicator and auto-compaction
|
|
821
|
+
* lean on this — under-estimating is the safe direction (compact early rather
|
|
822
|
+
* than overflow the server's real window).
|
|
823
|
+
*
|
|
824
|
+
* Users running larger-context models (e.g. a 128K Qwen) can raise it via the
|
|
825
|
+
* `LOCAL_LLM_CONTEXT_WINDOW` env var / customField, which supports a single
|
|
826
|
+
* value or a per-model map (resolved by {@link resolveContextWindow}).
|
|
827
|
+
*/
|
|
828
|
+
const LOCAL_DEFAULT_CONTEXT_WINDOW = 8192;
|
|
829
|
+
/**
|
|
830
|
+
* Local OpenAI-compatible LLM (Ollama, vLLM, LM Studio, Lemonade, llama.cpp).
|
|
831
|
+
*
|
|
832
|
+
* No fixed base URL or model catalogue — both come from the user via
|
|
833
|
+
* `customFields`. No `envKey`, so the wizard skips the API-key prompt and
|
|
834
|
+
* treats the customFields-only credential as the auth signal (see
|
|
835
|
+
* `applyApiKeyEnv` + `detectAuth`).
|
|
836
|
+
*
|
|
837
|
+
* Vision / cache / reasoning are off by default in the factory — flip them
|
|
838
|
+
* by passing a custom descriptor that calls `local({ capabilities })`.
|
|
839
|
+
*
|
|
840
|
+
* `models` is a getter, not a static list: there's no fixed catalogue to ship,
|
|
841
|
+
* but once the user has configured a default model (mirrored into
|
|
842
|
+
* `LOCAL_LLM_DEFAULT_MODEL` by `applyApiKeyEnv`), we surface it as a
|
|
843
|
+
* single-entry list so the model picker + footer aren't empty. Resolved at
|
|
844
|
+
* access time because the env var is populated at TUI launch, after this
|
|
845
|
+
* module loads. Empty list when unconfigured — the picker hides, the user
|
|
846
|
+
* types a slug at the prompt as before.
|
|
847
|
+
*/
|
|
848
|
+
const localDescriptor = {
|
|
849
|
+
key: "local",
|
|
850
|
+
label: "Local LLM",
|
|
851
|
+
factory: local,
|
|
852
|
+
get models() {
|
|
853
|
+
const configured = process.env.LOCAL_LLM_DEFAULT_MODEL?.trim();
|
|
854
|
+
if (!configured) return [];
|
|
855
|
+
return [{
|
|
856
|
+
id: configured,
|
|
857
|
+
name: configured,
|
|
858
|
+
contextWindow: resolveContextWindow(process.env.LOCAL_LLM_CONTEXT_WINDOW, configured) ?? LOCAL_DEFAULT_CONTEXT_WINDOW,
|
|
859
|
+
input: ["text"]
|
|
860
|
+
}];
|
|
861
|
+
},
|
|
862
|
+
customFields: [
|
|
863
|
+
{
|
|
864
|
+
key: "baseURL",
|
|
865
|
+
label: "Base URL",
|
|
866
|
+
envVar: "LOCAL_LLM_BASE_URL",
|
|
867
|
+
placeholder: "http://localhost:11434/v1",
|
|
868
|
+
hint: "Where your local LLM server is reachable. Examples: Ollama → http://localhost:11434/v1, vLLM → http://localhost:8000/v1, LM Studio → http://localhost:1234/v1.",
|
|
869
|
+
required: true
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
key: "apiKey",
|
|
873
|
+
label: "API key",
|
|
874
|
+
envVar: "LOCAL_LLM_API_KEY",
|
|
875
|
+
placeholder: "leave blank if your server is unauthenticated"
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
key: "model",
|
|
879
|
+
label: "Default model",
|
|
880
|
+
envVar: "LOCAL_LLM_DEFAULT_MODEL",
|
|
881
|
+
placeholder: "e.g. llama3.1:8b, qwen2.5-coder, mistral-small",
|
|
882
|
+
hint: "Model id your server exposes. Leave blank to pick later from the model picker."
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
key: "contextWindow",
|
|
886
|
+
label: "Context window (tokens)",
|
|
887
|
+
envVar: "LOCAL_LLM_CONTEXT_WINDOW",
|
|
888
|
+
placeholder: "e.g. 32768 · or per-model: llama3.1:8b=32768, qwen2.5-coder=128k, 64k",
|
|
889
|
+
hint: "Context length(s) for your local model(s) — local runtimes don't advertise this, so set it here to enable the context indicator and auto-compaction. Use a single value (32768, 128k) for all models, or a comma-separated map of `model=window` with an optional bare value as the fallback. Press"
|
|
890
|
+
}
|
|
891
|
+
],
|
|
892
|
+
contextWindowEnvVar: "LOCAL_LLM_CONTEXT_WINDOW"
|
|
893
|
+
};
|
|
894
|
+
/**
|
|
895
|
+
* Default provider registry. Passed verbatim when `runTui` is invoked without
|
|
896
|
+
* an explicit `providers` option. Hosts that want to override per-provider
|
|
897
|
+
* metadata can spread this and replace specific entries:
|
|
898
|
+
*
|
|
899
|
+
* ```ts
|
|
900
|
+
* runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })
|
|
901
|
+
* ```
|
|
902
|
+
*
|
|
903
|
+
* `cursor` is intentionally NOT registered here: OAuth works, but inference
|
|
904
|
+
* isn't wired (`cursor.stream()` throws — Cursor speaks a protobuf/HTTP-2
|
|
905
|
+
* agent protocol, not OpenAI-compat). Shipping it in the picker only lets users
|
|
906
|
+
* select a model that errors every turn. The descriptor stays exported so a
|
|
907
|
+
* host can opt in (`{ ...BUILTIN_PROVIDERS, cursor: cursorDescriptor }`) once
|
|
908
|
+
* the transport lands — re-add it here at that point.
|
|
909
|
+
*/
|
|
910
|
+
const BUILTIN_PROVIDERS = {
|
|
911
|
+
anthropic: anthropicDescriptor,
|
|
912
|
+
openai: openaiDescriptor,
|
|
913
|
+
openrouter: openrouterDescriptor,
|
|
914
|
+
cerebras: cerebrasDescriptor,
|
|
915
|
+
local: localDescriptor
|
|
916
|
+
};
|
|
917
|
+
/**
|
|
918
|
+
* Resolve the model list for a given provider. Honors `descriptor.models`
|
|
919
|
+
* when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns
|
|
920
|
+
* `[]` for descriptors with no known mapping (custom providers without a
|
|
921
|
+
* model list) — callers should hide the model picker in that case.
|
|
922
|
+
*/
|
|
923
|
+
function modelsForDescriptor(descriptor) {
|
|
924
|
+
if (descriptor.models) return descriptor.models;
|
|
925
|
+
let bundled;
|
|
926
|
+
try {
|
|
927
|
+
bundled = getModels(piIdOf(descriptor));
|
|
928
|
+
} catch {
|
|
929
|
+
bundled = [];
|
|
930
|
+
}
|
|
931
|
+
if (descriptor.extraModels?.length) {
|
|
932
|
+
const bundledIds = new Set(bundled.map((m) => m.id));
|
|
933
|
+
bundled = [...descriptor.extraModels.filter((m) => !bundledIds.has(m.id)), ...bundled];
|
|
934
|
+
}
|
|
935
|
+
return descriptor.optionsFor ? bundled.map((m) => decorateOptions(m, descriptor)) : bundled;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Attach descriptor-resolved {@link ModelOption}s to a model that doesn't
|
|
939
|
+
* already declare its own. Pure / non-mutating — returns the input untouched
|
|
940
|
+
* when the model has options already or the descriptor resolves none.
|
|
941
|
+
*/
|
|
942
|
+
function decorateOptions(model, descriptor) {
|
|
943
|
+
if (model.options?.length || !descriptor.optionsFor) return model;
|
|
944
|
+
const options = descriptor.optionsFor(model.id);
|
|
945
|
+
return options?.length ? {
|
|
946
|
+
...model,
|
|
947
|
+
options
|
|
948
|
+
} : model;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Resolve a single model's metadata via the descriptor's model source.
|
|
952
|
+
* Mirrors {@link modelsForDescriptor} routing: descriptor's own list wins,
|
|
953
|
+
* pi-ai's registry is the fallback. Returns `null` when the model isn't
|
|
954
|
+
* known.
|
|
955
|
+
*/
|
|
956
|
+
function getModelInfo(descriptor, modelId) {
|
|
957
|
+
if (descriptor.models) return descriptor.models.find((m) => m.id === modelId) ?? null;
|
|
958
|
+
try {
|
|
959
|
+
const bundled = getModel(piIdOf(descriptor), modelId);
|
|
960
|
+
if (bundled) return decorateOptions(bundled, descriptor);
|
|
961
|
+
} catch {}
|
|
962
|
+
const extra = descriptor.extraModels?.find((m) => m.id === modelId);
|
|
963
|
+
return extra ? decorateOptions(extra, descriptor) : null;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Parse a user-supplied context-window string into a token count.
|
|
967
|
+
*
|
|
968
|
+
* Accepts a plain integer (`"32768"`), or a number with a `k`/`m` suffix
|
|
969
|
+
* (×1_000 / ×1_000_000, case-insensitive, optional space) — so `"128k"`
|
|
970
|
+
* yields `128_000`, matching how the footer renders windows (`fmtTokens`).
|
|
971
|
+
* Fractional values are floored (`"1.5k"` → `1500`). Returns `null` for
|
|
972
|
+
* empty, malformed, zero, or negative input so callers fall through to
|
|
973
|
+
* "window unknown" rather than a bogus ceiling.
|
|
974
|
+
*/
|
|
975
|
+
function parseContextWindow(raw) {
|
|
976
|
+
if (raw == null) return null;
|
|
977
|
+
const trimmed = raw.trim();
|
|
978
|
+
if (trimmed.length === 0) return null;
|
|
979
|
+
const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(trimmed);
|
|
980
|
+
if (!match) return null;
|
|
981
|
+
const suffix = match[2]?.toLowerCase();
|
|
982
|
+
const mult = suffix === "k" ? 1e3 : suffix === "m" ? 1e6 : 1;
|
|
983
|
+
const value = Math.floor(Number.parseFloat(match[1]) * mult);
|
|
984
|
+
return value >= 1 ? value : null;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Resolve a per-model context window from a user-supplied spec string.
|
|
988
|
+
*
|
|
989
|
+
* A spec is a comma-separated list of entries. An entry is either:
|
|
990
|
+
* - `model=window` — a per-model override (split on the **first** `=`, so
|
|
991
|
+
* model ids containing `:` or `/` survive), or
|
|
992
|
+
* - a bare `window` — the fallback applied to any model not named above.
|
|
993
|
+
*
|
|
994
|
+
* Windows are parsed by {@link parseContextWindow} (plain int or k/m suffix).
|
|
995
|
+
* Whitespace around entries and the `=` is ignored; malformed entries are
|
|
996
|
+
* skipped rather than poisoning the whole spec; a later duplicate key wins.
|
|
997
|
+
*
|
|
998
|
+
* Lookup order for `modelId`: exact map entry → bare fallback → `null`. A
|
|
999
|
+
* lone bare value (`"32768"`) therefore behaves as a single global window,
|
|
1000
|
+
* keeping the simple single-model config working.
|
|
1001
|
+
*
|
|
1002
|
+
* @example resolveContextWindow('llama3.1:8b=32768, qwen=128k, 64k', 'qwen') // 128000
|
|
1003
|
+
*/
|
|
1004
|
+
function resolveContextWindow(spec, modelId) {
|
|
1005
|
+
if (spec == null) return null;
|
|
1006
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
1007
|
+
let fallback = null;
|
|
1008
|
+
for (const entry of spec.split(",")) {
|
|
1009
|
+
const eq = entry.indexOf("=");
|
|
1010
|
+
if (eq === -1) {
|
|
1011
|
+
const bare = parseContextWindow(entry);
|
|
1012
|
+
if (bare != null) fallback = bare;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const key = entry.slice(0, eq).trim();
|
|
1016
|
+
const window = parseContextWindow(entry.slice(eq + 1));
|
|
1017
|
+
if (key.length > 0 && window != null) overrides.set(key, window);
|
|
1018
|
+
}
|
|
1019
|
+
return overrides.get(modelId) ?? fallback;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Look up the model's max context window via the descriptor's model source.
|
|
1023
|
+
* Falls back to {@link ProviderDescriptor.contextWindowEnvVar} (parsed via
|
|
1024
|
+
* {@link resolveContextWindow}, which supports a per-model map) when the
|
|
1025
|
+
* registry has nothing — this is how local LLMs, whose runtimes don't
|
|
1026
|
+
* advertise a window, get one. Returns `null` when neither source resolves a
|
|
1027
|
+
* value (custom slugs, providers without a registry or a configured window);
|
|
1028
|
+
* callers should hide the context indicator and skip auto-compaction then.
|
|
1029
|
+
*/
|
|
1030
|
+
function getContextWindow(descriptor, modelId) {
|
|
1031
|
+
const fromRegistry = getModelInfo(descriptor, modelId)?.contextWindow;
|
|
1032
|
+
if (fromRegistry != null) return fromRegistry;
|
|
1033
|
+
if (descriptor.contextWindowEnvVar) return resolveContextWindow(process.env[descriptor.contextWindowEnvVar], modelId);
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Reserved output budget subtracted from the raw context window when
|
|
1038
|
+
* computing the effective ceiling for compaction / warning thresholds.
|
|
1039
|
+
*
|
|
1040
|
+
* Aligned with Claude Code's `COMPACT_MAX_OUTPUT_TOKENS = 20_000`
|
|
1041
|
+
* (`utils/context.ts:12`) — covers p99.99 of summary + response output.
|
|
1042
|
+
* A turn that consumed N input tokens leaves `effectiveWindow - N`
|
|
1043
|
+
* headroom for the next prompt's response; once headroom shrinks
|
|
1044
|
+
* below the threshold, auto-compaction fires.
|
|
1045
|
+
*/
|
|
1046
|
+
const OUTPUT_RESERVE_TOKENS = 2e4;
|
|
1047
|
+
/**
|
|
1048
|
+
* Effective context window — what the next user turn can actually pack
|
|
1049
|
+
* before the provider reserves space for the assistant's reply. Equals
|
|
1050
|
+
* `rawWindow - OUTPUT_RESERVE_TOKENS`, clamped to `>= 1` so a tiny
|
|
1051
|
+
* (or absurd) window doesn't yield zero / negative thresholds.
|
|
1052
|
+
*
|
|
1053
|
+
* Used by both the footer's context indicator and the auto-compact
|
|
1054
|
+
* trigger so the two surfaces agree on "how full are we, really".
|
|
1055
|
+
* Pass `null` through unchanged so callers can pipe `getContextWindow`
|
|
1056
|
+
* directly without an intermediate check.
|
|
1057
|
+
*/
|
|
1058
|
+
function effectiveContextWindow(rawWindow) {
|
|
1059
|
+
if (rawWindow === null) return null;
|
|
1060
|
+
return Math.max(1, rawWindow - OUTPUT_RESERVE_TOKENS);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Whether the given model exposes a reasoning / extended-thinking knob.
|
|
1064
|
+
* Drives the TUI's effort picker visibility — the `ctrl+n` shortcut and
|
|
1065
|
+
* the bottom-bar `effortName` segment only surface when this is `true`.
|
|
1066
|
+
* Returns `false` for unknown models (no registry entry → no advertised
|
|
1067
|
+
* capability).
|
|
1068
|
+
*/
|
|
1069
|
+
function modelSupportsReasoning(descriptor, modelId) {
|
|
1070
|
+
return getModelInfo(descriptor, modelId)?.reasoning === true;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Custom {@link ModelOption}s the given model supports (e.g. fast mode), or `[]`
|
|
1074
|
+
* when none. Drives the TUI/GUI options picker visibility — surfaces only when
|
|
1075
|
+
* non-empty.
|
|
1076
|
+
*/
|
|
1077
|
+
function modelOptionsFor(descriptor, modelId) {
|
|
1078
|
+
return getModelInfo(descriptor, modelId)?.options ?? [];
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Normalize a model-options blob to an enabled-only `{ id: true }` map.
|
|
1082
|
+
*
|
|
1083
|
+
* Single source of truth shared by every surface that reads or persists model
|
|
1084
|
+
* options — the TUI run path, the GUI engine reader, and the GUI IPC handlers.
|
|
1085
|
+
* Tolerant of arbitrary input (persisted JSON metadata may be anything): a
|
|
1086
|
+
* non-object → `{}`, and only entries strictly equal to `true` survive. This
|
|
1087
|
+
* keeps the persisted shape minimal (no `{ fast: false }` noise) and means
|
|
1088
|
+
* callers never forward a disabled option to `agent.run`.
|
|
1089
|
+
*/
|
|
1090
|
+
function enabledModelOptions(raw) {
|
|
1091
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1092
|
+
const out = {};
|
|
1093
|
+
for (const [id, on] of Object.entries(raw)) if (on === true) out[id] = true;
|
|
1094
|
+
return out;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Resolve a model's remembered options for a (re)pick: keep only options the
|
|
1098
|
+
* model still declares AND that are enabled, dropping any that no longer apply
|
|
1099
|
+
* (e.g. switching away from a model that supported `fast`). Returns `undefined`
|
|
1100
|
+
* when nothing applies so callers can omit the field entirely.
|
|
1101
|
+
*
|
|
1102
|
+
* Shared by the TUI pick path and the launch-time resume path so "which options
|
|
1103
|
+
* survive a model switch / relaunch" is decided in exactly one place.
|
|
1104
|
+
*/
|
|
1105
|
+
function restoreModelOptions(descriptor, modelId, remembered) {
|
|
1106
|
+
const validIds = new Set(modelOptionsFor(descriptor, modelId).map((o) => o.id));
|
|
1107
|
+
if (validIds.size === 0) return void 0;
|
|
1108
|
+
const enabled = enabledModelOptions(remembered?.[modelId]);
|
|
1109
|
+
const filtered = {};
|
|
1110
|
+
for (const id of Object.keys(enabled)) if (validIds.has(id)) filtered[id] = true;
|
|
1111
|
+
return Object.keys(filtered).length > 0 ? filtered : void 0;
|
|
267
1112
|
}
|
|
268
1113
|
//#endregion
|
|
269
1114
|
//#region src/tools/read-state.ts
|
|
@@ -686,7 +1531,7 @@ function buildPersistedStub(input) {
|
|
|
686
1531
|
const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, PERSISTENCE_PREVIEW_BYTES);
|
|
687
1532
|
const previewMarker = previewSlice.length < input.output.length ? `\n…(${input.originalBytes - previewBytes} more bytes in persisted file)` : "";
|
|
688
1533
|
return [
|
|
689
|
-
`${PERSISTED_STUB_PREFIX}${
|
|
1534
|
+
`${PERSISTED_STUB_PREFIX}${escapeXml(input.toolName)}" bytes="${input.originalBytes}" path="${escapeXml(input.persistedPath)}">`,
|
|
690
1535
|
`${previewSlice}${previewMarker}`,
|
|
691
1536
|
"</persisted-output>"
|
|
692
1537
|
].join("\n");
|
|
@@ -739,14 +1584,6 @@ async function writeAtomic(path, content) {
|
|
|
739
1584
|
}
|
|
740
1585
|
}
|
|
741
1586
|
/**
|
|
742
|
-
* Byte length of a UTF-8 string. `Buffer.byteLength` is the canonical
|
|
743
|
-
* answer; pulling it through a small helper keeps `node:buffer` out of
|
|
744
|
-
* call sites that don't otherwise need it.
|
|
745
|
-
*/
|
|
746
|
-
function byteLength(text) {
|
|
747
|
-
return Buffer.byteLength(text, "utf8");
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
1587
|
* Take the first `cap` bytes of `text` without splitting a UTF-8
|
|
751
1588
|
* codepoint. Returns the substring AND its exact UTF-8 byte length so
|
|
752
1589
|
* the caller doesn't repeat the byte walk (the truncation marker in
|
|
@@ -770,7 +1607,7 @@ function sliceFirstBytes(text, cap) {
|
|
|
770
1607
|
slice: "",
|
|
771
1608
|
bytes: 0
|
|
772
1609
|
};
|
|
773
|
-
const total =
|
|
1610
|
+
const total = utf8ByteLength(text);
|
|
774
1611
|
if (total <= cap) return {
|
|
775
1612
|
slice: text,
|
|
776
1613
|
bytes: total
|
|
@@ -778,7 +1615,7 @@ function sliceFirstBytes(text, cap) {
|
|
|
778
1615
|
let bytes = 0;
|
|
779
1616
|
let charIdx = 0;
|
|
780
1617
|
for (const ch of text) {
|
|
781
|
-
const chBytes =
|
|
1618
|
+
const chBytes = utf8ByteLength(ch);
|
|
782
1619
|
if (bytes + chBytes > cap) break;
|
|
783
1620
|
bytes += chBytes;
|
|
784
1621
|
charIdx += ch.length;
|
|
@@ -788,18 +1625,6 @@ function sliceFirstBytes(text, cap) {
|
|
|
788
1625
|
bytes
|
|
789
1626
|
};
|
|
790
1627
|
}
|
|
791
|
-
/**
|
|
792
|
-
* Escape `&`, `"`, and `<` for safe inclusion in an XML attribute. The
|
|
793
|
-
* stub format embeds tool names and filesystem paths verbatim — both
|
|
794
|
-
* theoretically could contain one of these (Windows paths can't, but a
|
|
795
|
-
* tool alias is user-controlled). Keeping the attribute well-formed
|
|
796
|
-
* prevents a malicious or unusual tool name from breaking the wrapper
|
|
797
|
-
* parse. `&` is escaped first so the subsequent replacements don't
|
|
798
|
-
* double-encode the entities we just emitted.
|
|
799
|
-
*/
|
|
800
|
-
function escapeAttr(text) {
|
|
801
|
-
return text.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
802
|
-
}
|
|
803
1628
|
//#endregion
|
|
804
1629
|
//#region src/tools/validation.ts
|
|
805
1630
|
const TRUE_STRINGS = new Set([
|
|
@@ -1857,6 +2682,7 @@ async function executeTurn(ctx, turn, priorUsage) {
|
|
|
1857
2682
|
maxTokens: ctx.maxTokens ?? 16384,
|
|
1858
2683
|
thinking: ctx.thinking,
|
|
1859
2684
|
thinkingBudget: effectiveThinkingBudget,
|
|
2685
|
+
...ctx.modelOptions ? { modelOptions: ctx.modelOptions } : {},
|
|
1860
2686
|
cache: ctx.cache ?? true,
|
|
1861
2687
|
signal: ctx.signal
|
|
1862
2688
|
};
|
|
@@ -1917,6 +2743,22 @@ async function executeTurn(ctx, turn, priorUsage) {
|
|
|
1917
2743
|
turnId
|
|
1918
2744
|
});
|
|
1919
2745
|
},
|
|
2746
|
+
onServerToolUse({ id, name, input }) {
|
|
2747
|
+
ctx.hooks.callHook("stream:server_tool_use", {
|
|
2748
|
+
id,
|
|
2749
|
+
name,
|
|
2750
|
+
input,
|
|
2751
|
+
turnId
|
|
2752
|
+
});
|
|
2753
|
+
},
|
|
2754
|
+
onServerToolResult({ toolUseId, toolName, content }) {
|
|
2755
|
+
ctx.hooks.callHook("stream:server_tool_result", {
|
|
2756
|
+
toolUseId,
|
|
2757
|
+
toolName,
|
|
2758
|
+
content,
|
|
2759
|
+
turnId
|
|
2760
|
+
});
|
|
2761
|
+
},
|
|
1920
2762
|
onOAuthRefresh(refreshCtx) {
|
|
1921
2763
|
return ctx.hooks.callHook("oauth:refresh", refreshCtx);
|
|
1922
2764
|
}
|
|
@@ -2386,10 +3228,10 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
|
|
|
2386
3228
|
let removeAbortListener;
|
|
2387
3229
|
const cancellationPromise = new Promise((_, reject) => {
|
|
2388
3230
|
if (perCallAbort.signal.aborted) {
|
|
2389
|
-
reject(new Error(CANCELLED_BY_USER_SENTINEL));
|
|
3231
|
+
reject(/* @__PURE__ */ new Error(CANCELLED_BY_USER_SENTINEL));
|
|
2390
3232
|
return;
|
|
2391
3233
|
}
|
|
2392
|
-
const onAbort = () => reject(new Error(CANCELLED_BY_USER_SENTINEL));
|
|
3234
|
+
const onAbort = () => reject(/* @__PURE__ */ new Error(CANCELLED_BY_USER_SENTINEL));
|
|
2393
3235
|
perCallAbort.signal.addEventListener("abort", onAbort, { once: true });
|
|
2394
3236
|
removeAbortListener = () => perCallAbort.signal.removeEventListener("abort", onAbort);
|
|
2395
3237
|
});
|
|
@@ -2645,6 +3487,20 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
|
|
|
2645
3487
|
const drain = async () => {
|
|
2646
3488
|
if (inFlight.size > 0) await Promise.all([...inFlight.values()]);
|
|
2647
3489
|
};
|
|
3490
|
+
/**
|
|
3491
|
+
* Free a SINGLE slot: wait for the first in-flight call to settle rather
|
|
3492
|
+
* than the whole fleet. Used when an all-safe fleet hits `maxConcurrent` —
|
|
3493
|
+
* a sliding window dispatches the next safe call the moment ANY sibling
|
|
3494
|
+
* finishes, instead of stalling the entire batch on the slowest one (which
|
|
3495
|
+
* a full `drain()` would do). `dispatch()` never rejects (it captures both
|
|
3496
|
+
* outcomes into `results[]`), so `Promise.race` here can't throw. Each
|
|
3497
|
+
* dispatched promise carries a `.finally` that deletes its own slot from
|
|
3498
|
+
* `inFlight` BEFORE the promise resolves, so on return `inFlight.size` has
|
|
3499
|
+
* dropped by at least one and a dispatch is guaranteed to fit under the cap.
|
|
3500
|
+
*/
|
|
3501
|
+
const waitForSlot = async () => {
|
|
3502
|
+
if (inFlight.size > 0) await Promise.race([...inFlight.values()]);
|
|
3503
|
+
};
|
|
2648
3504
|
/** Whether every in-flight call is concurrency-safe. */
|
|
2649
3505
|
const fleetAllSafe = () => {
|
|
2650
3506
|
for (const idx of inFlight.keys()) if (!safe[idx]) return false;
|
|
@@ -2666,7 +3522,8 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
|
|
|
2666
3522
|
};
|
|
2667
3523
|
try {
|
|
2668
3524
|
for (let i = 0; i < N; i++) {
|
|
2669
|
-
if (!safe[i] || !fleetAllSafe()
|
|
3525
|
+
if (!safe[i] || !fleetAllSafe()) await drain();
|
|
3526
|
+
else if (inFlight.size >= maxConcurrent) await waitForSlot();
|
|
2670
3527
|
if (ctx.signal.aborted) {
|
|
2671
3528
|
await drain();
|
|
2672
3529
|
fillUnstarted(i, INTERRUPT_MESSAGE_FOR_TOOL_USE);
|
|
@@ -3758,6 +4615,81 @@ function createToolSearchTool(options) {
|
|
|
3758
4615
|
//#endregion
|
|
3759
4616
|
//#region src/agent.ts
|
|
3760
4617
|
/**
|
|
4618
|
+
* Single builder for a {@link ContextAssembly}, shared by the per-run snapshot
|
|
4619
|
+
* and the pre-run assembly so the two can't drift on optionality / field
|
|
4620
|
+
* ordering. Callers pass every piece (optional fields may be `undefined`); this
|
|
4621
|
+
* normalizes the spread-conditional shape both producers previously inlined.
|
|
4622
|
+
*/
|
|
4623
|
+
function assembleContextSnapshot(input) {
|
|
4624
|
+
return {
|
|
4625
|
+
modelId: input.modelId,
|
|
4626
|
+
system: input.system,
|
|
4627
|
+
baseSystem: input.baseSystem,
|
|
4628
|
+
...input.rulesBlock ? { rulesBlock: input.rulesBlock } : {},
|
|
4629
|
+
...input.rulesFiles?.length ? { rulesFiles: input.rulesFiles } : {},
|
|
4630
|
+
disclosedTools: input.disclosedTools,
|
|
4631
|
+
deferredEntries: input.deferredEntries,
|
|
4632
|
+
...input.mcpInstructions ? { mcpInstructions: input.mcpInstructions } : {},
|
|
4633
|
+
...input.skillsCatalog ? { skillsCatalog: input.skillsCatalog } : {},
|
|
4634
|
+
...input.subagentDefs ? { subagentDefs: input.subagentDefs } : {},
|
|
4635
|
+
wireTools: input.wireTools
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
/**
|
|
4639
|
+
* Lazily serialize an assembly's tool inputs into the byte-sized buckets the
|
|
4640
|
+
* context breakdown consumes. Deferred so the per-run snapshot stays cheap —
|
|
4641
|
+
* the `JSON.stringify` here only runs when `getContextBreakdown()` is called.
|
|
4642
|
+
*
|
|
4643
|
+
* MCP grouping preserves first-seen server order; native tools stay flat. The
|
|
4644
|
+
* output is byte-identical to the eager partition it replaced.
|
|
4645
|
+
*/
|
|
4646
|
+
function materializeAssemblyTools(assembly) {
|
|
4647
|
+
const toolsJson = [];
|
|
4648
|
+
const disclosedMcp = /* @__PURE__ */ new Map();
|
|
4649
|
+
for (const t of assembly.disclosedTools) {
|
|
4650
|
+
const json = JSON.stringify({
|
|
4651
|
+
name: t.name,
|
|
4652
|
+
description: t.description,
|
|
4653
|
+
inputSchema: t.inputSchema
|
|
4654
|
+
});
|
|
4655
|
+
if (t.mcpServer !== void 0) {
|
|
4656
|
+
const bucket = disclosedMcp.get(t.mcpServer) ?? [];
|
|
4657
|
+
bucket.push({
|
|
4658
|
+
name: t.name,
|
|
4659
|
+
json
|
|
4660
|
+
});
|
|
4661
|
+
disclosedMcp.set(t.mcpServer, bucket);
|
|
4662
|
+
} else toolsJson.push(json);
|
|
4663
|
+
}
|
|
4664
|
+
const deferredToolsJson = [];
|
|
4665
|
+
const deferredMcp = /* @__PURE__ */ new Map();
|
|
4666
|
+
for (const entry of assembly.deferredEntries) {
|
|
4667
|
+
const json = JSON.stringify({
|
|
4668
|
+
name: entry.name,
|
|
4669
|
+
description: entry.description,
|
|
4670
|
+
inputSchema: entry.inputSchema
|
|
4671
|
+
});
|
|
4672
|
+
if (entry.server !== void 0) {
|
|
4673
|
+
const bucket = deferredMcp.get(entry.server) ?? [];
|
|
4674
|
+
bucket.push({
|
|
4675
|
+
name: entry.name,
|
|
4676
|
+
json
|
|
4677
|
+
});
|
|
4678
|
+
deferredMcp.set(entry.server, bucket);
|
|
4679
|
+
} else deferredToolsJson.push(json);
|
|
4680
|
+
}
|
|
4681
|
+
const toGroups = (m) => [...m.entries()].map(([server, list]) => ({
|
|
4682
|
+
server,
|
|
4683
|
+
tools: list
|
|
4684
|
+
}));
|
|
4685
|
+
return {
|
|
4686
|
+
toolsJson,
|
|
4687
|
+
deferredToolsJson,
|
|
4688
|
+
mcpGroups: toGroups(disclosedMcp),
|
|
4689
|
+
deferredMcpGroups: toGroups(deferredMcp)
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
3761
4693
|
* Authoritative list of hook event names. Kept in sync with `AgentHooks` at
|
|
3762
4694
|
* compile time: the `satisfies` assertion below rejects any drift.
|
|
3763
4695
|
*/
|
|
@@ -3800,6 +4732,8 @@ const HOOK_EVENT_SET = new Set([
|
|
|
3800
4732
|
"stream:text",
|
|
3801
4733
|
"stream:end",
|
|
3802
4734
|
"stream:thinking",
|
|
4735
|
+
"stream:server_tool_use",
|
|
4736
|
+
"stream:server_tool_result",
|
|
3803
4737
|
"stream:error",
|
|
3804
4738
|
"stream:retry",
|
|
3805
4739
|
"oauth:refresh",
|
|
@@ -3822,6 +4756,8 @@ const HOOK_EVENT_SET = new Set([
|
|
|
3822
4756
|
"child:stream:text",
|
|
3823
4757
|
"child:stream:thinking",
|
|
3824
4758
|
"child:stream:end",
|
|
4759
|
+
"child:stream:server_tool_use",
|
|
4760
|
+
"child:stream:server_tool_result",
|
|
3825
4761
|
"child:stream:error",
|
|
3826
4762
|
"child:tool:gate",
|
|
3827
4763
|
"child:mcp:tool:gate",
|
|
@@ -3939,6 +4875,7 @@ function resolveBehavior(agentBehavior, runBehavior) {
|
|
|
3939
4875
|
maxTotalTokens: runBehavior?.maxTotalTokens ?? agentBehavior?.maxTotalTokens,
|
|
3940
4876
|
maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
|
|
3941
4877
|
thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
|
|
4878
|
+
modelOptions: runBehavior?.modelOptions ?? agentBehavior?.modelOptions,
|
|
3942
4879
|
schema: runBehavior?.schema ?? agentBehavior?.schema,
|
|
3943
4880
|
cache: runBehavior?.cache ?? agentBehavior?.cache ?? true,
|
|
3944
4881
|
toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget,
|
|
@@ -4120,29 +5057,43 @@ function renderMcpInstructionsSection(instructions) {
|
|
|
4120
5057
|
return parts.join("\n");
|
|
4121
5058
|
}
|
|
4122
5059
|
/**
|
|
4123
|
-
*
|
|
4124
|
-
*
|
|
4125
|
-
*
|
|
4126
|
-
* `
|
|
4127
|
-
* shape minted by this module; any caller-supplied custom id schemes
|
|
4128
|
-
* are ignored (they don't conflict with `run_N`).
|
|
4129
|
-
*
|
|
4130
|
-
* Returning 0 for a sessionless / clean session preserves the original
|
|
4131
|
-
* "first run is run_1" semantics.
|
|
5060
|
+
* Set of runIds belonging to subagent (depth > 0) runs in a session. Used to
|
|
5061
|
+
* filter child-run turns out of the resumed conversation and out of
|
|
5062
|
+
* last-turn-usage accounting — both need "everything a subagent produced",
|
|
5063
|
+
* keyed by `runId`. Returns an empty set for a sessionless agent.
|
|
4132
5064
|
*/
|
|
4133
|
-
function
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
5065
|
+
function childRunIdSet(session) {
|
|
5066
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5067
|
+
for (const r of session?.runs ?? []) if ((r.depth ?? 0) > 0) ids.add(r.id);
|
|
5068
|
+
return ids;
|
|
5069
|
+
}
|
|
5070
|
+
/**
|
|
5071
|
+
* Validate `agent.run()`'s prompt/resume preconditions and compute the
|
|
5072
|
+
* unresolved-tool-filtered resume view (returned for reuse in the seed block).
|
|
5073
|
+
*
|
|
5074
|
+
* `prompt` is required unless resuming a session that already has turns. The
|
|
5075
|
+
* resume guard inspects the trailing turn AFTER filtering unresolved tool_uses
|
|
5076
|
+
* (L1) — a session that ended on an orphan assistant turn (process death
|
|
5077
|
+
* mid-tool, `kill -9`) gets its trailing assistant dropped before the
|
|
5078
|
+
* "trailing must be user" check, so a recoverable session doesn't get a
|
|
5079
|
+
* spurious `cannot resume without prompt` error.
|
|
5080
|
+
*
|
|
5081
|
+
* Throws on an unrecoverable resume request; otherwise returns the filtered
|
|
5082
|
+
* turns (or `undefined` when the session has no turns).
|
|
5083
|
+
*/
|
|
5084
|
+
function validateAndPrepareResume(session, prompt) {
|
|
5085
|
+
const hasSessionTurns = !!session && session.turns.length > 0;
|
|
5086
|
+
if (!prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
|
|
5087
|
+
let resumeFilteredTurns;
|
|
5088
|
+
if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
|
|
5089
|
+
if (!prompt && resumeFilteredTurns) {
|
|
5090
|
+
const lastTurn = resumeFilteredTurns.at(-1);
|
|
5091
|
+
if (lastTurn && lastTurn.role !== "user") {
|
|
5092
|
+
const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
|
|
5093
|
+
throw new Error(`cannot resume without prompt: ${detail}. Pass a prompt to agent.run({ prompt: … }).`);
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
return resumeFilteredTurns;
|
|
4146
5097
|
}
|
|
4147
5098
|
function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, readState: agentReadState, skills: agentSkills, mcpConnector, eager, hooks: initialHooks, clock: agentClock }) {
|
|
4148
5099
|
const hooks = createHooks();
|
|
@@ -4160,16 +5111,43 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4160
5111
|
let running = false;
|
|
4161
5112
|
let idleResolve;
|
|
4162
5113
|
let idlePromise;
|
|
5114
|
+
function resetRunScope() {
|
|
5115
|
+
running = false;
|
|
5116
|
+
abortController = void 0;
|
|
5117
|
+
idleResolve?.();
|
|
5118
|
+
idlePromise = void 0;
|
|
5119
|
+
idleResolve = void 0;
|
|
5120
|
+
}
|
|
4163
5121
|
const pendingTaskNotifications = /* @__PURE__ */ new Map();
|
|
4164
5122
|
const pendingToolCancels = /* @__PURE__ */ new Map();
|
|
4165
5123
|
let executionHandle = null;
|
|
4166
5124
|
let mcpConnection = null;
|
|
5125
|
+
let lastContextAssembly = null;
|
|
4167
5126
|
let mcpWarmupPromise = null;
|
|
4168
5127
|
const allMcpServers = mcpServers ?? [];
|
|
4169
5128
|
const steeringQueue = [];
|
|
4170
5129
|
const followUpQueue = [];
|
|
4171
5130
|
let conversationTurns = session?.turns.slice() ?? [];
|
|
4172
|
-
let runCounter =
|
|
5131
|
+
let runCounter = 0;
|
|
5132
|
+
let scannedRuns = 0;
|
|
5133
|
+
let scannedTurns = 0;
|
|
5134
|
+
const considerRunId = (id) => {
|
|
5135
|
+
if (!id) return;
|
|
5136
|
+
const m = /^run_(\d+)$/.exec(id);
|
|
5137
|
+
if (!m) return;
|
|
5138
|
+
const n = Number.parseInt(m[1], 10);
|
|
5139
|
+
if (Number.isFinite(n) && n > runCounter) runCounter = n;
|
|
5140
|
+
};
|
|
5141
|
+
function syncRunCounter() {
|
|
5142
|
+
if (!session) return;
|
|
5143
|
+
if (session.runs.length < scannedRuns) scannedRuns = 0;
|
|
5144
|
+
if (session.turns.length < scannedTurns) scannedTurns = 0;
|
|
5145
|
+
for (let i = scannedRuns; i < session.runs.length; i++) considerRunId(session.runs[i].id);
|
|
5146
|
+
for (let i = scannedTurns; i < session.turns.length; i++) considerRunId(session.turns[i].runId);
|
|
5147
|
+
scannedRuns = session.runs.length;
|
|
5148
|
+
scannedTurns = session.turns.length;
|
|
5149
|
+
}
|
|
5150
|
+
syncRunCounter();
|
|
4173
5151
|
const skillsConfig = agentSkills;
|
|
4174
5152
|
const skillsEnabledValue = skillsConfig?.enabled;
|
|
4175
5153
|
const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
|
|
@@ -4213,24 +5191,14 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4213
5191
|
const skillActivationState = createSkillActivationState({ maxActive: skillsConfig?.maxActive });
|
|
4214
5192
|
async function run(options) {
|
|
4215
5193
|
if (running) throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
|
|
4216
|
-
const
|
|
4217
|
-
if (!options.prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
|
|
4218
|
-
let resumeFilteredTurns;
|
|
4219
|
-
if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
|
|
4220
|
-
if (!options.prompt && resumeFilteredTurns) {
|
|
4221
|
-
const lastTurn = resumeFilteredTurns.at(-1);
|
|
4222
|
-
if (lastTurn && lastTurn.role !== "user") {
|
|
4223
|
-
const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
|
|
4224
|
-
throw new Error(`cannot resume without prompt: ${detail}. Pass a prompt to agent.run({ prompt: … }).`);
|
|
4225
|
-
}
|
|
4226
|
-
}
|
|
5194
|
+
const resumeFilteredTurns = validateAndPrepareResume(session, options.prompt);
|
|
4227
5195
|
const clock = options.clock ?? agentClock ?? DEFAULT_AGENT_CLOCK;
|
|
4228
5196
|
let externalAbortListener;
|
|
4229
5197
|
const externalSignal = options.signal;
|
|
4230
5198
|
running = true;
|
|
4231
5199
|
try {
|
|
4232
5200
|
abortController = new AbortController();
|
|
4233
|
-
|
|
5201
|
+
syncRunCounter();
|
|
4234
5202
|
const runId = `run_${++runCounter}`;
|
|
4235
5203
|
const promptLabel = typeof options.prompt === "string" ? options.prompt : Array.isArray(options.prompt) ? options.prompt.filter((p) => p.type === "text").map((p) => p.text).join("\n") : "";
|
|
4236
5204
|
session?.startRun(runId, promptLabel, {
|
|
@@ -4306,8 +5274,10 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4306
5274
|
const thinking = options.thinking ?? "off";
|
|
4307
5275
|
const model = options.model ?? provider.meta.defaultModel;
|
|
4308
5276
|
const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
|
|
4309
|
-
const { maxConcurrentTools, maxTurns, maxCostUsd, maxTotalTokens, maxTokens, retry, thinkingBudget, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxConsecutivePauseTurns } = resolvedBehavior;
|
|
5277
|
+
const { maxConcurrentTools, maxTurns, maxCostUsd, maxTotalTokens, maxTokens, retry, thinkingBudget, modelOptions: behaviorModelOptions, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxConsecutivePauseTurns } = resolvedBehavior;
|
|
5278
|
+
const modelOptions = options.modelOptions ?? behaviorModelOptions;
|
|
4310
5279
|
let system = options.system || agentSystem || "You are a helpful assistant.";
|
|
5280
|
+
const baseSystemForBreakdown = renderSystemForWire(system);
|
|
4311
5281
|
if (skillsCatalog) system = appendStaticSection(system, skillsCatalog);
|
|
4312
5282
|
const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
|
|
4313
5283
|
...sourceTools,
|
|
@@ -4368,14 +5338,20 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4368
5338
|
initialUnlocked.add(toolSearchTool.spec.name);
|
|
4369
5339
|
}
|
|
4370
5340
|
const discoveryToolName = shouldInjectToolSearch ? "tool_search" : hostDefinedToolSearch ? toolAliases?.tool_search ?? "tool_search" : null;
|
|
4371
|
-
|
|
5341
|
+
let searchableCatalogText;
|
|
5342
|
+
if (disclosure.lazyEntries.length > 0) {
|
|
5343
|
+
searchableCatalogText = buildSearchableCatalog(disclosure.lazyEntries, { discoveryToolName });
|
|
5344
|
+
system = appendStaticSection(system, searchableCatalogText);
|
|
5345
|
+
}
|
|
4372
5346
|
if (surfaceMcpInstructions && mcpConnection?.instructions && mcpConnection.instructions.size > 0) {
|
|
4373
5347
|
const section = renderMcpInstructionsSection(mcpConnection.instructions);
|
|
4374
5348
|
if (section.length > 0) system = appendStaticSection(system, section);
|
|
4375
5349
|
}
|
|
4376
5350
|
const aliasMaps = buildAliasMaps(toolAliases, Object.keys(tools));
|
|
4377
5351
|
augmentMcpDoubleUnderscoreAliases(aliasMaps, Object.keys(tools));
|
|
5352
|
+
let formattedToolsCache = null;
|
|
4378
5353
|
function buildFormattedTools() {
|
|
5354
|
+
if (formattedToolsCache && formattedToolsCache.tailLen === dynamicUnlockOrder.length) return formattedToolsCache.value;
|
|
4379
5355
|
const specs = [];
|
|
4380
5356
|
for (const t of Object.values(tools)) {
|
|
4381
5357
|
if (!initialUnlocked.has(t.spec.name)) continue;
|
|
@@ -4394,12 +5370,52 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4394
5370
|
inputSchema: t.spec.inputSchema
|
|
4395
5371
|
});
|
|
4396
5372
|
}
|
|
4397
|
-
|
|
5373
|
+
const value = specs.length > 0 ? provider.formatTools(specs) : [];
|
|
5374
|
+
formattedToolsCache = {
|
|
5375
|
+
tailLen: dynamicUnlockOrder.length,
|
|
5376
|
+
value
|
|
5377
|
+
};
|
|
5378
|
+
return value;
|
|
4398
5379
|
}
|
|
4399
5380
|
const formattedTools = buildFormattedTools();
|
|
5381
|
+
{
|
|
5382
|
+
const disclosedTools = [];
|
|
5383
|
+
for (const t of Object.values(tools)) {
|
|
5384
|
+
if (!unlocked.has(t.spec.name)) continue;
|
|
5385
|
+
const wireName = aliasMaps.aliasByCanonical.get(t.spec.name) ?? t.spec.name;
|
|
5386
|
+
disclosedTools.push({
|
|
5387
|
+
name: wireName,
|
|
5388
|
+
description: t.spec.description || "",
|
|
5389
|
+
inputSchema: t.spec.inputSchema,
|
|
5390
|
+
...mcpToolNames.has(t.spec.name) ? { mcpServer: "mcp" } : {}
|
|
5391
|
+
});
|
|
5392
|
+
}
|
|
5393
|
+
const deferredEntries = disclosure.lazyEntries.map((entry) => ({
|
|
5394
|
+
name: entry.name,
|
|
5395
|
+
description: entry.description || "",
|
|
5396
|
+
inputSchema: entry.inputSchema,
|
|
5397
|
+
...entry.server ? { server: entry.server } : {}
|
|
5398
|
+
}));
|
|
5399
|
+
const mcpInstructionsText = surfaceMcpInstructions && mcpConnection?.instructions && mcpConnection.instructions.size > 0 ? renderMcpInstructionsSection(mcpConnection.instructions) : void 0;
|
|
5400
|
+
const rulesBlock = options.contextSections?.rulesBlock;
|
|
5401
|
+
const rulesFiles = options.contextSections?.rulesFiles;
|
|
5402
|
+
lastContextAssembly = assembleContextSnapshot({
|
|
5403
|
+
modelId: model,
|
|
5404
|
+
system: renderSystemForWire(system),
|
|
5405
|
+
baseSystem: baseSystemForBreakdown,
|
|
5406
|
+
rulesBlock,
|
|
5407
|
+
rulesFiles,
|
|
5408
|
+
disclosedTools,
|
|
5409
|
+
deferredEntries,
|
|
5410
|
+
mcpInstructions: mcpInstructionsText,
|
|
5411
|
+
skillsCatalog,
|
|
5412
|
+
subagentDefs: searchableCatalogText,
|
|
5413
|
+
wireTools: formattedTools
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
4400
5416
|
const turns = [];
|
|
4401
5417
|
if (session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt) && !options.parentRunId) {
|
|
4402
|
-
const childRunIds =
|
|
5418
|
+
const childRunIds = childRunIdSet(session);
|
|
4403
5419
|
const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
|
|
4404
5420
|
const filteredForRuntime = resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
|
|
4405
5421
|
turns.push(...filteredForRuntime);
|
|
@@ -4538,6 +5554,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4538
5554
|
...options.parentRunId ? { parentRunId: options.parentRunId } : {},
|
|
4539
5555
|
depth: runDepth,
|
|
4540
5556
|
thinkingBudget,
|
|
5557
|
+
...modelOptions ? { modelOptions } : {},
|
|
4541
5558
|
schema,
|
|
4542
5559
|
cache,
|
|
4543
5560
|
toolOutputBudget,
|
|
@@ -4646,20 +5663,12 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4646
5663
|
unregisterTurnSync?.();
|
|
4647
5664
|
unregisterToolResultsSync?.();
|
|
4648
5665
|
for (const unregister of perRunUnregisters) unregister();
|
|
4649
|
-
running = false;
|
|
4650
|
-
abortController = void 0;
|
|
4651
5666
|
steeringQueue.length = 0;
|
|
4652
5667
|
followUpQueue.length = 0;
|
|
4653
|
-
|
|
4654
|
-
idlePromise = void 0;
|
|
4655
|
-
idleResolve = void 0;
|
|
5668
|
+
resetRunScope();
|
|
4656
5669
|
}
|
|
4657
5670
|
} finally {
|
|
4658
|
-
|
|
4659
|
-
abortController = void 0;
|
|
4660
|
-
idleResolve?.();
|
|
4661
|
-
idlePromise = void 0;
|
|
4662
|
-
idleResolve = void 0;
|
|
5671
|
+
resetRunScope();
|
|
4663
5672
|
if (externalSignal && externalAbortListener) externalSignal.removeEventListener("abort", externalAbortListener);
|
|
4664
5673
|
}
|
|
4665
5674
|
}
|
|
@@ -4691,6 +5700,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4691
5700
|
conversationTurns = [];
|
|
4692
5701
|
steeringQueue.length = 0;
|
|
4693
5702
|
followUpQueue.length = 0;
|
|
5703
|
+
lastContextAssembly = null;
|
|
4694
5704
|
pendingTaskNotifications.clear();
|
|
4695
5705
|
const cleared = skillActivationState.clear();
|
|
4696
5706
|
for (const record of cleared) await hooks.callHook("skills:deactivate", {
|
|
@@ -4815,6 +5825,151 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4815
5825
|
pendingTaskNotifications.clear();
|
|
4816
5826
|
skillsCleanup();
|
|
4817
5827
|
skillsCleanup = () => {};
|
|
5828
|
+
lastContextAssembly = null;
|
|
5829
|
+
}
|
|
5830
|
+
/**
|
|
5831
|
+
* Last parent-run `TurnUsage` from the in-memory conversation — mirrors
|
|
5832
|
+
* `lastContextSizeFromTurns` (chat/store) but reads the live agent turns and
|
|
5833
|
+
* returns the full usage so callers can break out cache read/write/input.
|
|
5834
|
+
* Skips subagent (depth > 0) and zero-usage turns.
|
|
5835
|
+
*/
|
|
5836
|
+
function lastTurnUsage() {
|
|
5837
|
+
const childRunIds = childRunIdSet(session);
|
|
5838
|
+
for (let i = conversationTurns.length - 1; i >= 0; i--) {
|
|
5839
|
+
const turn = conversationTurns[i];
|
|
5840
|
+
if (turn.role !== "assistant" || !turn.usage) continue;
|
|
5841
|
+
if (turn.runId && childRunIds.has(turn.runId)) continue;
|
|
5842
|
+
if (effectiveInputFromTurn(turn.usage) === 0) continue;
|
|
5843
|
+
return turn.usage;
|
|
5844
|
+
}
|
|
5845
|
+
return null;
|
|
5846
|
+
}
|
|
5847
|
+
/**
|
|
5848
|
+
* Best-effort context assembly BEFORE the first run, so the breakdown is
|
|
5849
|
+
* available the moment the chat screen opens (no "send a message first").
|
|
5850
|
+
*
|
|
5851
|
+
* Resolves skills + MCP via `warmup()`, then assembles the same static pieces
|
|
5852
|
+
* `run()` captures — system prompt (with skills catalog + MCP instructions),
|
|
5853
|
+
* the wire tool specs, and MCP groupings. The disclosure subtleties don't
|
|
5854
|
+
* matter pre-run (nothing has been unlocked yet); we treat every native +
|
|
5855
|
+
* MCP tool as disclosed, matching the eager seed of a fresh run.
|
|
5856
|
+
*/
|
|
5857
|
+
async function buildPreRunAssembly(modelOverride) {
|
|
5858
|
+
if (destroyed) return null;
|
|
5859
|
+
try {
|
|
5860
|
+
await ensureSkillsResolved();
|
|
5861
|
+
} catch {}
|
|
5862
|
+
const model = modelOverride ?? provider.meta.defaultModel;
|
|
5863
|
+
const base = agentSystem || "You are a helpful assistant.";
|
|
5864
|
+
const baseSystem = renderSystemForWire(base);
|
|
5865
|
+
let system = base;
|
|
5866
|
+
if (skillsCatalog) system = appendStaticSection(system, skillsCatalog);
|
|
5867
|
+
const baseTools = mcpConnection ? {
|
|
5868
|
+
...sourceTools,
|
|
5869
|
+
...mcpConnection.tools
|
|
5870
|
+
} : sourceTools;
|
|
5871
|
+
const mcpNames = new Set(mcpConnection ? Object.keys(mcpConnection.tools) : []);
|
|
5872
|
+
const disclosedTools = [];
|
|
5873
|
+
const nativeSpecs = [];
|
|
5874
|
+
const mcpSpecs = [];
|
|
5875
|
+
for (const t of Object.values(baseTools)) {
|
|
5876
|
+
const spec = {
|
|
5877
|
+
name: t.spec.name,
|
|
5878
|
+
description: t.spec.description || "",
|
|
5879
|
+
inputSchema: t.spec.inputSchema
|
|
5880
|
+
};
|
|
5881
|
+
if (mcpNames.has(t.spec.name)) mcpSpecs.push(spec);
|
|
5882
|
+
else nativeSpecs.push(spec);
|
|
5883
|
+
}
|
|
5884
|
+
for (const spec of nativeSpecs) disclosedTools.push({
|
|
5885
|
+
name: spec.name,
|
|
5886
|
+
description: spec.description || "",
|
|
5887
|
+
inputSchema: spec.inputSchema
|
|
5888
|
+
});
|
|
5889
|
+
for (const spec of mcpSpecs) disclosedTools.push({
|
|
5890
|
+
name: spec.name,
|
|
5891
|
+
description: spec.description || "",
|
|
5892
|
+
inputSchema: spec.inputSchema,
|
|
5893
|
+
mcpServer: "mcp"
|
|
5894
|
+
});
|
|
5895
|
+
const wireTools = nativeSpecs.length + mcpSpecs.length > 0 ? provider.formatTools([...nativeSpecs, ...mcpSpecs]) : [];
|
|
5896
|
+
const mcpInstructionsText = mcpConnection?.instructions && mcpConnection.instructions.size > 0 ? renderMcpInstructionsSection(mcpConnection.instructions) : void 0;
|
|
5897
|
+
if (mcpInstructionsText) system = appendStaticSection(system, mcpInstructionsText);
|
|
5898
|
+
return assembleContextSnapshot({
|
|
5899
|
+
modelId: model,
|
|
5900
|
+
system: renderSystemForWire(system),
|
|
5901
|
+
baseSystem,
|
|
5902
|
+
disclosedTools,
|
|
5903
|
+
deferredEntries: [],
|
|
5904
|
+
mcpInstructions: mcpInstructionsText,
|
|
5905
|
+
skillsCatalog,
|
|
5906
|
+
wireTools
|
|
5907
|
+
});
|
|
5908
|
+
}
|
|
5909
|
+
async function getContextBreakdown(opts) {
|
|
5910
|
+
if (destroyed) return null;
|
|
5911
|
+
const assembly = lastContextAssembly ?? await buildPreRunAssembly(opts?.model);
|
|
5912
|
+
if (!assembly) return null;
|
|
5913
|
+
const usage = lastTurnUsage();
|
|
5914
|
+
const used = effectiveInputFromTurn(usage);
|
|
5915
|
+
const effectiveWindow = opts?.effectiveWindow ?? used;
|
|
5916
|
+
const autocompactBuffer = opts?.compactThreshold !== void 0 && opts.compactThreshold > 0 && opts.compactThreshold < 1 ? Math.max(0, Math.round((1 - opts.compactThreshold) * effectiveWindow)) : opts?.autocompactBuffer ?? 2e4;
|
|
5917
|
+
let exact;
|
|
5918
|
+
if (typeof provider.countTokens === "function") {
|
|
5919
|
+
const BASELINE = ".";
|
|
5920
|
+
const dummy = [{
|
|
5921
|
+
role: "user",
|
|
5922
|
+
content: [{
|
|
5923
|
+
type: "text",
|
|
5924
|
+
text: "."
|
|
5925
|
+
}]
|
|
5926
|
+
}];
|
|
5927
|
+
const count = (system, tools) => provider.countTokens({
|
|
5928
|
+
model: assembly.modelId,
|
|
5929
|
+
system,
|
|
5930
|
+
tools,
|
|
5931
|
+
messages: dummy
|
|
5932
|
+
}, opts?.signal);
|
|
5933
|
+
try {
|
|
5934
|
+
const [base, sysProbe, sysToolsProbe] = await Promise.all([
|
|
5935
|
+
count(BASELINE, []),
|
|
5936
|
+
count(assembly.system, []),
|
|
5937
|
+
count(assembly.system, assembly.wireTools)
|
|
5938
|
+
]);
|
|
5939
|
+
if (base !== null && sysProbe !== null) exact = {
|
|
5940
|
+
system: Math.max(0, sysProbe - base),
|
|
5941
|
+
...sysToolsProbe !== null ? { systemAndTools: Math.max(0, sysToolsProbe - base) } : {}
|
|
5942
|
+
};
|
|
5943
|
+
} catch {
|
|
5944
|
+
exact = void 0;
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
const materializedTools = materializeAssemblyTools(assembly);
|
|
5948
|
+
return buildContextBreakdown({
|
|
5949
|
+
modelId: assembly.modelId,
|
|
5950
|
+
system: assembly.system,
|
|
5951
|
+
baseSystem: assembly.baseSystem,
|
|
5952
|
+
...assembly.rulesBlock ? { rulesBlock: assembly.rulesBlock } : {},
|
|
5953
|
+
...assembly.rulesFiles?.length ? { rulesFiles: assembly.rulesFiles } : {},
|
|
5954
|
+
toolsJson: materializedTools.toolsJson,
|
|
5955
|
+
deferredToolsJson: materializedTools.deferredToolsJson,
|
|
5956
|
+
mcpGroups: materializedTools.mcpGroups,
|
|
5957
|
+
deferredMcpGroups: materializedTools.deferredMcpGroups,
|
|
5958
|
+
...assembly.mcpInstructions ? { mcpInstructions: assembly.mcpInstructions } : {},
|
|
5959
|
+
...assembly.skillsCatalog ? { skillsCatalog: assembly.skillsCatalog } : {},
|
|
5960
|
+
...assembly.subagentDefs ? { subagentDefs: assembly.subagentDefs } : {},
|
|
5961
|
+
used,
|
|
5962
|
+
effectiveWindow,
|
|
5963
|
+
autocompactBuffer,
|
|
5964
|
+
...exact ? { exact } : {},
|
|
5965
|
+
...usage ? { usage: {
|
|
5966
|
+
input: usage.input ?? 0,
|
|
5967
|
+
cacheRead: usage.cacheRead ?? 0,
|
|
5968
|
+
cacheCreation: usage.cacheCreation ?? 0,
|
|
5969
|
+
output: usage.output ?? 0
|
|
5970
|
+
} } : {},
|
|
5971
|
+
...skillActivationState.active().length > 0 ? { activeSkills: skillActivationState.active().map((s) => s.skill.name) } : {}
|
|
5972
|
+
});
|
|
4818
5973
|
}
|
|
4819
5974
|
const eagerHasWork = allMcpServers.length > 0 || !skillsDisabled && !!skillsConfig;
|
|
4820
5975
|
if (eager && eagerHasWork) warmup().catch(() => {});
|
|
@@ -4851,225 +6006,11 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4851
6006
|
return skillActivationState.active();
|
|
4852
6007
|
},
|
|
4853
6008
|
meta: Object.freeze({ ...provider.meta }),
|
|
6009
|
+
getContextBreakdown,
|
|
4854
6010
|
[Symbol.asyncDispose]: destroy
|
|
4855
6011
|
};
|
|
4856
6012
|
}
|
|
4857
6013
|
//#endregion
|
|
4858
|
-
//#region src/tools/edit-utils.ts
|
|
4859
|
-
/**
|
|
4860
|
-
* Internal helpers shared between the `edit` and `multi_edit` tools.
|
|
4861
|
-
*
|
|
4862
|
-
* Not part of the public API — intentionally not re-exported from `tools/index.ts`
|
|
4863
|
-
* or the package barrel.
|
|
4864
|
-
*/
|
|
4865
|
-
/**
|
|
4866
|
-
* Count exact (non-overlapping) occurrences of `needle` in `haystack`.
|
|
4867
|
-
* Returns 0 for an empty needle — both edit tools reject empty `old_string`
|
|
4868
|
-
* up front, so this branch is defensive rather than semantic.
|
|
4869
|
-
*/
|
|
4870
|
-
function countExactMatches(haystack, needle) {
|
|
4871
|
-
if (needle.length === 0) return 0;
|
|
4872
|
-
let count = 0;
|
|
4873
|
-
let idx = 0;
|
|
4874
|
-
while (true) {
|
|
4875
|
-
const next = haystack.indexOf(needle, idx);
|
|
4876
|
-
if (next === -1) break;
|
|
4877
|
-
count++;
|
|
4878
|
-
idx = next + needle.length;
|
|
4879
|
-
}
|
|
4880
|
-
return count;
|
|
4881
|
-
}
|
|
4882
|
-
/** Map curly quotes (any of the four) to their straight ASCII equivalents. */
|
|
4883
|
-
function normalizeQuotes(str) {
|
|
4884
|
-
return str.replaceAll("‘", "'").replaceAll("’", "'").replaceAll("“", "\"").replaceAll("”", "\"");
|
|
4885
|
-
}
|
|
4886
|
-
/**
|
|
4887
|
-
* Substitutions Anthropic's API applies to assistant output before the model
|
|
4888
|
-
* sees it. The model emits the sanitized form; the file on disk contains the
|
|
4889
|
-
* unsanitized form. We undo the substitutions on `old_string` so the search
|
|
4890
|
-
* lands on the actual file contents.
|
|
4891
|
-
*
|
|
4892
|
-
* Verbatim from `claude-code/tools/FileEditTool/utils.ts`.
|
|
4893
|
-
*/
|
|
4894
|
-
const DESANITIZATIONS = [
|
|
4895
|
-
["<fnr>", "<function_results>"],
|
|
4896
|
-
["<n>", "<name>"],
|
|
4897
|
-
["</n>", "</name>"],
|
|
4898
|
-
["<o>", "<output>"],
|
|
4899
|
-
["</o>", "</output>"],
|
|
4900
|
-
["<e>", "<error>"],
|
|
4901
|
-
["</e>", "</error>"],
|
|
4902
|
-
["<s>", "<system>"],
|
|
4903
|
-
["</s>", "</system>"],
|
|
4904
|
-
["<r>", "<result>"],
|
|
4905
|
-
["</r>", "</result>"],
|
|
4906
|
-
["< META_START >", "<META_START>"],
|
|
4907
|
-
["< META_END >", "<META_END>"],
|
|
4908
|
-
["< EOT >", "<EOT>"],
|
|
4909
|
-
["< META >", "<META>"],
|
|
4910
|
-
["< SOS >", "<SOS>"],
|
|
4911
|
-
["\n\nH:", "\n\nHuman:"],
|
|
4912
|
-
["\n\nA:", "\n\nAssistant:"]
|
|
4913
|
-
];
|
|
4914
|
-
/**
|
|
4915
|
-
* Apply the SDK desanitization table to a string. Exported so the edit tools
|
|
4916
|
-
* can apply it to `new_string` whenever `old_string` matched via a
|
|
4917
|
-
* desanitize-class fallback — keeps the file's unsanitized form on disk
|
|
4918
|
-
* instead of writing the model's abbreviated form back.
|
|
4919
|
-
*/
|
|
4920
|
-
function desanitize(s) {
|
|
4921
|
-
let out = s;
|
|
4922
|
-
for (const [from, to] of DESANITIZATIONS) out = out.replaceAll(from, to);
|
|
4923
|
-
return out;
|
|
4924
|
-
}
|
|
4925
|
-
/**
|
|
4926
|
-
* Strip line-number prefixes from each line of a needle, used as a recovery
|
|
4927
|
-
* fallback when the model pastes a `read_file` chunk verbatim into
|
|
4928
|
-
* `old_string` — the on-disk file doesn't carry the metadata prefix.
|
|
4929
|
-
*
|
|
4930
|
-
* Accepts three separator characters so a model that learned on a different
|
|
4931
|
-
* agent stack still works here: `\t` (Claude Code compact, our default),
|
|
4932
|
-
* `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one
|
|
4933
|
-
* of `\t | →`. The 9-digit ceiling covers files up to ~1B lines without
|
|
4934
|
-
* overshooting into legitimate `\d{N}<sep>` content like Markdown table
|
|
4935
|
-
* cells with long numeric IDs.
|
|
4936
|
-
*/
|
|
4937
|
-
const LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
|
|
4938
|
-
function stripLineNumberPrefixes(s) {
|
|
4939
|
-
return s.replace(LINE_NUMBER_PREFIX_RE, "");
|
|
4940
|
-
}
|
|
4941
|
-
/**
|
|
4942
|
-
* Search `target` in `normFile` and slice the matching span out of the
|
|
4943
|
-
* original `haystack`, counting all non-overlapping occurrences. `normFile`
|
|
4944
|
-
* is the haystack with whatever transform (quotes / desanitize / combined)
|
|
4945
|
-
* was applied to make the indices align — slicing the original haystack
|
|
4946
|
-
* preserves the file's actual typography so `replace_all` writes back the
|
|
4947
|
-
* file's form, not the model's.
|
|
4948
|
-
*
|
|
4949
|
-
* Pre-condition: `normFile.length === haystack.length` (every transform
|
|
4950
|
-
* we use is one-to-one). Returns null on miss.
|
|
4951
|
-
*/
|
|
4952
|
-
function locateAndCount(haystack, normFile, target, via) {
|
|
4953
|
-
const idx = normFile.indexOf(target);
|
|
4954
|
-
if (idx === -1) return null;
|
|
4955
|
-
const actual = haystack.slice(idx, idx + target.length);
|
|
4956
|
-
let occ = 0;
|
|
4957
|
-
let cursor = 0;
|
|
4958
|
-
while (true) {
|
|
4959
|
-
const next = normFile.indexOf(target, cursor);
|
|
4960
|
-
if (next === -1) break;
|
|
4961
|
-
occ++;
|
|
4962
|
-
cursor = next + target.length;
|
|
4963
|
-
}
|
|
4964
|
-
return {
|
|
4965
|
-
actual,
|
|
4966
|
-
occurrences: occ,
|
|
4967
|
-
via
|
|
4968
|
-
};
|
|
4969
|
-
}
|
|
4970
|
-
function resolveOldString(haystack, needle) {
|
|
4971
|
-
const exact = countExactMatches(haystack, needle);
|
|
4972
|
-
if (exact > 0) return {
|
|
4973
|
-
actual: needle,
|
|
4974
|
-
occurrences: exact,
|
|
4975
|
-
via: "exact"
|
|
4976
|
-
};
|
|
4977
|
-
const normNeedle = normalizeQuotes(needle);
|
|
4978
|
-
const normFile = normalizeQuotes(haystack);
|
|
4979
|
-
if (normNeedle !== needle || normFile !== haystack) {
|
|
4980
|
-
const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
|
|
4981
|
-
if (m) return m;
|
|
4982
|
-
}
|
|
4983
|
-
const desan = desanitize(needle);
|
|
4984
|
-
if (desan !== needle) {
|
|
4985
|
-
const desanCount = countExactMatches(haystack, desan);
|
|
4986
|
-
if (desanCount > 0) return {
|
|
4987
|
-
actual: desan,
|
|
4988
|
-
occurrences: desanCount,
|
|
4989
|
-
via: "desanitize"
|
|
4990
|
-
};
|
|
4991
|
-
}
|
|
4992
|
-
const combo = desanitize(normNeedle);
|
|
4993
|
-
if (combo !== needle) {
|
|
4994
|
-
const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
|
|
4995
|
-
if (m) return m;
|
|
4996
|
-
}
|
|
4997
|
-
const stripped = stripLineNumberPrefixes(needle);
|
|
4998
|
-
if (stripped !== needle && stripped.trim().length > 0) {
|
|
4999
|
-
const count = countExactMatches(haystack, stripped);
|
|
5000
|
-
if (count > 0) return {
|
|
5001
|
-
actual: stripped,
|
|
5002
|
-
occurrences: count,
|
|
5003
|
-
via: "line-numbers"
|
|
5004
|
-
};
|
|
5005
|
-
const strippedNorm = normalizeQuotes(stripped);
|
|
5006
|
-
if (strippedNorm !== stripped || normFile !== haystack) {
|
|
5007
|
-
const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
|
|
5008
|
-
if (m) return m;
|
|
5009
|
-
}
|
|
5010
|
-
}
|
|
5011
|
-
return null;
|
|
5012
|
-
}
|
|
5013
|
-
/**
|
|
5014
|
-
* Apply the same recovery transforms used to find `old_string` to
|
|
5015
|
-
* `new_string`, so the file gets back its native form: desanitize when
|
|
5016
|
-
* the model emitted `<n>` for `<name>`, strip line-number prefixes when
|
|
5017
|
-
* the match required them, then re-curlify when the match required
|
|
5018
|
-
* quote normalization. Shared between `edit` and `multi_edit`.
|
|
5019
|
-
*/
|
|
5020
|
-
function styleReplacementForVia(replacement, via, actual) {
|
|
5021
|
-
let out = replacement;
|
|
5022
|
-
if (via === "desanitize" || via === "quotes+desanitize") out = desanitize(out);
|
|
5023
|
-
if (via === "line-numbers" || via === "quotes+line-numbers") out = stripLineNumberPrefixes(out);
|
|
5024
|
-
if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers") out = preserveQuoteStyle(actual, out);
|
|
5025
|
-
return out;
|
|
5026
|
-
}
|
|
5027
|
-
/**
|
|
5028
|
-
* When `old_string` matched via curly-quote normalization, re-style
|
|
5029
|
-
* `new_string` so the file's typography is preserved across the edit.
|
|
5030
|
-
* Detects whether the matched file region had curly singles, doubles, or
|
|
5031
|
-
* both, and applies the matching curlification to the replacement.
|
|
5032
|
-
*
|
|
5033
|
-
* Apostrophes in contractions (`don't`, `it's`) get the right-single curly
|
|
5034
|
-
* quote regardless of opening context — that's the canonical typographer's
|
|
5035
|
-
* convention for English. Other quotes use a simple
|
|
5036
|
-
* preceded-by-whitespace-or-opening-punctuation heuristic.
|
|
5037
|
-
*/
|
|
5038
|
-
function preserveQuoteStyle(actual, replacement) {
|
|
5039
|
-
const hasDouble = actual.includes("“") || actual.includes("”");
|
|
5040
|
-
const hasSingle = actual.includes("‘") || actual.includes("’");
|
|
5041
|
-
if (!hasDouble && !hasSingle) return replacement;
|
|
5042
|
-
let out = replacement;
|
|
5043
|
-
if (hasDouble) out = applyCurly(out, "\"", "“", "”", false);
|
|
5044
|
-
if (hasSingle) out = applyCurly(out, "'", "‘", "’", true);
|
|
5045
|
-
return out;
|
|
5046
|
-
}
|
|
5047
|
-
function applyCurly(s, straight, left, right, contractionAware) {
|
|
5048
|
-
const chars = [...s];
|
|
5049
|
-
const result = [];
|
|
5050
|
-
for (let i = 0; i < chars.length; i++) {
|
|
5051
|
-
if (chars[i] !== straight) {
|
|
5052
|
-
result.push(chars[i]);
|
|
5053
|
-
continue;
|
|
5054
|
-
}
|
|
5055
|
-
if (contractionAware) {
|
|
5056
|
-
const prev = i > 0 ? chars[i - 1] : "";
|
|
5057
|
-
const next = i < chars.length - 1 ? chars[i + 1] : "";
|
|
5058
|
-
if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
|
|
5059
|
-
result.push(right);
|
|
5060
|
-
continue;
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
result.push(isOpeningContext(chars, i) ? left : right);
|
|
5064
|
-
}
|
|
5065
|
-
return result.join("");
|
|
5066
|
-
}
|
|
5067
|
-
function isOpeningContext(chars, i) {
|
|
5068
|
-
if (i === 0) return true;
|
|
5069
|
-
const prev = chars[i - 1];
|
|
5070
|
-
return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "—" || prev === "–";
|
|
5071
|
-
}
|
|
5072
|
-
//#endregion
|
|
5073
6014
|
//#region src/tools/path-suggest.ts
|
|
5074
6015
|
/**
|
|
5075
6016
|
* Find a sibling file in the same directory sharing `path`'s basename
|
|
@@ -6008,6 +6949,8 @@ const BUBBLED_EVENTS = [
|
|
|
6008
6949
|
"stream:thinking",
|
|
6009
6950
|
"stream:end",
|
|
6010
6951
|
"stream:error",
|
|
6952
|
+
"stream:server_tool_use",
|
|
6953
|
+
"stream:server_tool_result",
|
|
6011
6954
|
"tool:dispatched",
|
|
6012
6955
|
"tool:before",
|
|
6013
6956
|
"tool:after",
|
|
@@ -6028,6 +6971,8 @@ const CHILD_EVENT_NAME = {
|
|
|
6028
6971
|
"stream:thinking": "child:stream:thinking",
|
|
6029
6972
|
"stream:end": "child:stream:end",
|
|
6030
6973
|
"stream:error": "child:stream:error",
|
|
6974
|
+
"stream:server_tool_use": "child:stream:server_tool_use",
|
|
6975
|
+
"stream:server_tool_result": "child:stream:server_tool_result",
|
|
6031
6976
|
"tool:dispatched": "child:tool:dispatched",
|
|
6032
6977
|
"tool:before": "child:tool:before",
|
|
6033
6978
|
"tool:after": "child:tool:after",
|
|
@@ -6524,6 +7469,6 @@ const writeFile$1 = {
|
|
|
6524
7469
|
}
|
|
6525
7470
|
};
|
|
6526
7471
|
//#endregion
|
|
6527
|
-
export {
|
|
7472
|
+
export { getReadState as A, enabledModelOptions as B, PERSISTED_STUB_PREFIX as C, maybePersistToolResult as D, cleanupPersistedSession as E, OUTPUT_RESERVE_TOKENS as F, modelSupportsReasoning as G, getModelInfo as H, anthropicDescriptor as I, openrouterDescriptor as J, modelsForDescriptor as K, cerebrasDescriptor as L, readStateKey as M, resolveReadStateMap as N, resolvePersistDir as O, BUILTIN_PROVIDERS as P, credKeyOf as R, validateToolArgs as S, buildPersistedStub as T, localDescriptor as U, getContextWindow as V, modelOptionsFor as W, restoreModelOptions as X, piIdOf as Y, shell as _, multiEdit as a, TOOL_USE_CANCELLED_MESSAGE as b, grep as c, createAgent as d, createToolSearchTool as f, createShellTool as g, createSkillsReadTool as h, readFile$1 as i, hashContent as j, resolveTasksDir as k, glob$1 as l, createSkillsRunScriptTool as m, createSpawnTool as n, listFiles as o, createSkillsUseTool as p, openaiDescriptor as q, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, INTERRUPT_MESSAGE_FOR_TOOL_USE as v, PERSISTENCE_PREVIEW_BYTES as w, TOOL_USE_SKIPPED_MESSAGE as x, SHELL_CASCADE_CANCEL_MESSAGE as y, effectiveContextWindow as z };
|
|
6528
7473
|
|
|
6529
|
-
//# sourceMappingURL=tools-
|
|
7474
|
+
//# sourceMappingURL=tools-BGtJK0vo.js.map
|