pi-codex-search 0.1.2 → 0.1.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 +41 -19
- package/index.ts +761 -110
- package/package.json +10 -5
- package/scripts/codex-e2e.ts +797 -0
- package/src/codex.ts +86 -392
- package/src/command.ts +415 -194
- package/src/config.ts +77 -4
- package/src/cookies.ts +131 -0
- package/src/errors.ts +56 -0
- package/src/modes/responses.ts +310 -0
- package/src/modes/standalone.ts +378 -0
- package/src/modes/types.ts +41 -0
- package/src/ref-store.ts +74 -0
- package/src/transport.ts +110 -0
- package/src/ua.ts +67 -0
package/index.ts
CHANGED
|
@@ -5,29 +5,42 @@ import {
|
|
|
5
5
|
type ExtensionContext,
|
|
6
6
|
type Theme,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { Static } from "@earendil-works/pi-ai";
|
|
8
9
|
import { Text } from "@earendil-works/pi-tui";
|
|
9
10
|
import {
|
|
11
|
+
assertSupportedStandaloneCombination,
|
|
10
12
|
classifyError,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
CodexError,
|
|
14
|
+
createRefStore,
|
|
15
|
+
createTransport,
|
|
14
16
|
extractAccountIdFromToken,
|
|
15
17
|
fetchCodexModels,
|
|
16
|
-
|
|
18
|
+
runResponsesSearch,
|
|
19
|
+
runStandaloneCommands,
|
|
17
20
|
selectDefaultModel,
|
|
21
|
+
type CodexCitation,
|
|
22
|
+
type CodexErrorKind,
|
|
23
|
+
type CodexSearchCall,
|
|
18
24
|
type SearchContextSize,
|
|
25
|
+
type StandaloneCommandsOptions,
|
|
19
26
|
} from "./src/codex.ts";
|
|
20
27
|
import { registerSettingsCommand } from "./src/command.ts";
|
|
21
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
STANDALONE_TOOL_NAME,
|
|
30
|
+
type Freshness,
|
|
31
|
+
isProjectTrustedContext,
|
|
32
|
+
loadConfig,
|
|
33
|
+
type ResolvedConfig,
|
|
34
|
+
} from "./src/config.ts";
|
|
22
35
|
|
|
23
36
|
const OPENAI_CODEX_PROVIDER = "openai-codex";
|
|
24
|
-
const MAX_QUERIES = 5;
|
|
25
37
|
|
|
26
38
|
interface QuerySuccess {
|
|
27
39
|
query: string;
|
|
28
40
|
text: string;
|
|
29
41
|
citations: CodexCitation[];
|
|
30
42
|
searchCalls: CodexSearchCall[];
|
|
43
|
+
refIds?: Record<string, string>;
|
|
31
44
|
responseId?: string;
|
|
32
45
|
usage?: {
|
|
33
46
|
inputTokens?: number;
|
|
@@ -42,6 +55,12 @@ interface QueryFailure {
|
|
|
42
55
|
message: string;
|
|
43
56
|
}
|
|
44
57
|
|
|
58
|
+
interface StandaloneCallPlan {
|
|
59
|
+
query: string;
|
|
60
|
+
buildOptions: () => StandaloneCommandsOptions;
|
|
61
|
+
openedUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
interface WebSearchFailureDetail {
|
|
46
65
|
kind: CodexErrorKind;
|
|
47
66
|
message: string;
|
|
@@ -49,9 +68,11 @@ interface WebSearchFailureDetail {
|
|
|
49
68
|
|
|
50
69
|
interface WebSearchDetails {
|
|
51
70
|
model: string;
|
|
71
|
+
api: string;
|
|
52
72
|
freshness: Freshness;
|
|
53
73
|
searchContextSize: SearchContextSize;
|
|
54
74
|
queryCount: number;
|
|
75
|
+
queries: string[];
|
|
55
76
|
failedQueryCount: number;
|
|
56
77
|
successes: QuerySuccess[];
|
|
57
78
|
failures: QueryFailure[];
|
|
@@ -59,65 +80,429 @@ interface WebSearchDetails {
|
|
|
59
80
|
partial?: boolean;
|
|
60
81
|
completed?: number;
|
|
61
82
|
total?: number;
|
|
83
|
+
elapsedMs?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildToolDescription(config: ResolvedConfig): string {
|
|
87
|
+
const toolName = config.toolName;
|
|
88
|
+
if (config.searchApi === "standalone") {
|
|
89
|
+
return `${toolName}: standalone webpage actions for explicit page inspection: open one URL, find text, click link ids, screenshot pages, or run finance/weather/sports/time lookups. Not for web search.`;
|
|
90
|
+
}
|
|
91
|
+
return `${toolName}: search the web using the configured ChatGPT Codex subscription.`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildSearchParametersSchema(config: ResolvedConfig) {
|
|
95
|
+
return Type.Object({
|
|
96
|
+
queries: Type.Array(Type.String({ minLength: 1 }), {
|
|
97
|
+
minItems: 1,
|
|
98
|
+
maxItems: config.batchSize,
|
|
99
|
+
description: `One or more search queries to run in parallel (max ${config.batchSize}).`,
|
|
100
|
+
}),
|
|
101
|
+
search_context_size: Type.Optional(
|
|
102
|
+
StringEnum(["low", "medium", "high"] as const, {
|
|
103
|
+
description: "Amount of web context to retrieve. Defaults to medium.",
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
freshness: Type.Optional(
|
|
107
|
+
StringEnum(["cached", "indexed", "live"] as const, {
|
|
108
|
+
description:
|
|
109
|
+
"Use 'live' for time-sensitive queries; 'indexed' for OpenAI-indexed web access; 'cached' for stable topics. Defaults to live.",
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const StandaloneParametersSchema = Type.Object({
|
|
116
|
+
search_context_size: Type.Optional(
|
|
117
|
+
StringEnum(["medium", "high"] as const, {
|
|
118
|
+
description:
|
|
119
|
+
'Amount of web context to retrieve. Defaults to medium. Standalone mode disables "low".',
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
freshness: Type.Optional(
|
|
123
|
+
StringEnum(["cached", "indexed", "live"] as const, {
|
|
124
|
+
description:
|
|
125
|
+
"Use 'live' for time-sensitive queries; 'indexed' for OpenAI-indexed web access; 'cached' for stable topics. Defaults to live.",
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
urls: Type.Optional(
|
|
129
|
+
Type.Array(Type.String({ minLength: 1 }), {
|
|
130
|
+
maxItems: 1,
|
|
131
|
+
description: "One URL to open/fetch directly.",
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
find: Type.Optional(
|
|
135
|
+
Type.Array(
|
|
136
|
+
Type.Object({
|
|
137
|
+
url: Type.String({ minLength: 1 }),
|
|
138
|
+
pattern: Type.String({ minLength: 1 }),
|
|
139
|
+
}),
|
|
140
|
+
{ maxItems: 1, description: "Find a pattern within a previously opened webpage." },
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
click: Type.Optional(
|
|
144
|
+
Type.Array(
|
|
145
|
+
Type.Object({
|
|
146
|
+
url: Type.String({ minLength: 1 }),
|
|
147
|
+
id: Type.Integer({ minimum: 0 }),
|
|
148
|
+
}),
|
|
149
|
+
{ maxItems: 1, description: "Follow one link id from a previously opened page." },
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
screenshot: Type.Optional(
|
|
153
|
+
Type.Array(
|
|
154
|
+
Type.Object({
|
|
155
|
+
url: Type.String({ minLength: 1 }),
|
|
156
|
+
pageno: Type.Integer({ minimum: 0 }),
|
|
157
|
+
}),
|
|
158
|
+
{ maxItems: 1, description: "Capture one screenshot of a previously opened page." },
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
finance: Type.Optional(
|
|
162
|
+
Type.Array(
|
|
163
|
+
Type.Object({
|
|
164
|
+
ticker: Type.String({ minLength: 1 }),
|
|
165
|
+
type: StringEnum(["equity", "fund", "crypto", "index"] as const),
|
|
166
|
+
market: Type.Optional(Type.String()),
|
|
167
|
+
}),
|
|
168
|
+
{ maxItems: 1, description: "Look up one stock/ETF/crypto/index price." },
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
weather: Type.Optional(
|
|
172
|
+
Type.Array(
|
|
173
|
+
Type.Object({
|
|
174
|
+
location: Type.String({ minLength: 1 }),
|
|
175
|
+
start: Type.Optional(Type.String()),
|
|
176
|
+
duration: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
177
|
+
}),
|
|
178
|
+
{ maxItems: 1, description: "Look up one weather forecast." },
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
sports: Type.Optional(
|
|
182
|
+
Type.Array(
|
|
183
|
+
Type.Object({
|
|
184
|
+
fn: StringEnum(["schedule", "standings"] as const),
|
|
185
|
+
league: StringEnum([
|
|
186
|
+
"nba",
|
|
187
|
+
"wnba",
|
|
188
|
+
"nfl",
|
|
189
|
+
"nhl",
|
|
190
|
+
"mlb",
|
|
191
|
+
"epl",
|
|
192
|
+
"ncaamb",
|
|
193
|
+
"ncaawb",
|
|
194
|
+
"ipl",
|
|
195
|
+
] as const),
|
|
196
|
+
team: Type.Optional(Type.String()),
|
|
197
|
+
opponent: Type.Optional(Type.String()),
|
|
198
|
+
date_from: Type.Optional(Type.String()),
|
|
199
|
+
date_to: Type.Optional(Type.String()),
|
|
200
|
+
num_games: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
201
|
+
locale: Type.Optional(Type.String()),
|
|
202
|
+
}),
|
|
203
|
+
{ maxItems: 1, description: "Look up one sports schedule or standings request." },
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
time: Type.Optional(
|
|
207
|
+
Type.Array(
|
|
208
|
+
Type.Object({
|
|
209
|
+
utc_offset: Type.String({ minLength: 1 }),
|
|
210
|
+
}),
|
|
211
|
+
{ maxItems: 1, description: "Get time for one UTC offset." },
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
type StandaloneParameters = Static<typeof StandaloneParametersSchema>;
|
|
217
|
+
|
|
218
|
+
type ToolParameters = Partial<
|
|
219
|
+
Omit<StandaloneParameters, "queries" | "search_context_size" | "freshness">
|
|
220
|
+
> & {
|
|
221
|
+
queries?: string[];
|
|
222
|
+
image_queries?: string[];
|
|
223
|
+
search_context_size?: SearchContextSize;
|
|
224
|
+
freshness?: Freshness;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function buildToolParameters(config: ResolvedConfig) {
|
|
228
|
+
return config.searchApi === "standalone"
|
|
229
|
+
? StandaloneParametersSchema
|
|
230
|
+
: buildSearchParametersSchema(config);
|
|
62
231
|
}
|
|
63
232
|
|
|
64
233
|
function buildTool(config: ResolvedConfig) {
|
|
65
234
|
return defineTool({
|
|
66
235
|
name: config.toolName,
|
|
67
|
-
label: "Codex Search",
|
|
68
|
-
description:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
236
|
+
label: config.searchApi === "standalone" ? "Codex Standalone Web" : "Codex Search",
|
|
237
|
+
description: buildToolDescription(config),
|
|
238
|
+
promptSnippet:
|
|
239
|
+
config.searchApi === "standalone"
|
|
240
|
+
? `${config.toolName}: use only for explicit standalone webpage actions: open one URL, find text in that opened page, follow page link ids, take screenshots, or run finance/weather/sports/time lookups. Do not use for web search.`
|
|
241
|
+
: `${config.toolName}: search the web using the configured ChatGPT Codex subscription.`,
|
|
242
|
+
promptGuidelines:
|
|
243
|
+
config.searchApi === "standalone"
|
|
244
|
+
? [
|
|
245
|
+
`Use ${config.toolName} only when the user explicitly asks to open, read, inspect, find within, click inside, screenshot, or run finance/weather/sports/time lookup actions.`,
|
|
246
|
+
"Do not use codex_standalone_web for ordinary web search, source gathering, or batches; use codex_search for search queries.",
|
|
247
|
+
"Send exactly one standalone action per tool call. Do not combine urls/find/click/screenshot/lookup actions in one call.",
|
|
248
|
+
"For webpage workflows, first open one exact URL with urls. After a successful open, do not open the same page again unless the user asks to reload it.",
|
|
249
|
+
"For follow-up find/click/screenshot, use the same URL string the user opened. Do not switch between www and non-www hosts, add/remove trailing paths, or upgrade search_context_size just to retry.",
|
|
250
|
+
"Do not use search_context_size low in standalone; use medium unless the user explicitly asks for high.",
|
|
251
|
+
"If a follow-up page action fails because the page was not opened in this session, open the exact requested URL once, then retry the follow-up once.",
|
|
252
|
+
"Do not ask the user for an access token; the tool uses pi's configured OpenAI Codex subscription.",
|
|
253
|
+
]
|
|
254
|
+
: [
|
|
255
|
+
`Use ${config.toolName} when current or source-backed information is needed.`,
|
|
256
|
+
`Batch up to ${config.batchSize} related queries in one call when grouped comparison matters; use separate calls when independent results unblock the next step.`,
|
|
257
|
+
config.standaloneEnabled
|
|
258
|
+
? "Use codex_standalone_web only when the user explicitly asks to open/read/inspect a webpage, find text inside an opened page, click a page link id, take a screenshot, or run finance/weather/sports/time lookups."
|
|
259
|
+
: "codex_standalone_web is not enabled in this session; if the user asks for webpage actions, say that the Standalone web tool must be enabled in /codex-search-settings.",
|
|
260
|
+
"Do not call codex_standalone_web merely to improve or duplicate a codex_search result.",
|
|
261
|
+
"Choose freshness per request: use 'live' for news, prices, releases, availability, laws, schedules, or other time-sensitive facts; use 'cached' for stable facts and docs; use 'indexed' when OpenAI-indexed web access is enough but live browsing is not needed.",
|
|
262
|
+
"Do not ask the user for an access token; the tool uses pi's configured OpenAI Codex subscription.",
|
|
263
|
+
],
|
|
264
|
+
parameters: buildToolParameters(config),
|
|
93
265
|
|
|
94
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
95
|
-
const queries = params.queries
|
|
96
|
-
|
|
97
|
-
|
|
266
|
+
async execute(_toolCallId, params: ToolParameters, signal, onUpdate, ctx) {
|
|
267
|
+
const queries = params.queries?.map((q) => q.trim()).filter((q) => q.length > 0) ?? [];
|
|
268
|
+
const imageQueries =
|
|
269
|
+
params.image_queries?.map((q) => q.trim()).filter((q) => q.length > 0) ?? [];
|
|
270
|
+
const urls = params.urls?.map((u) => u.trim()).filter((u) => u.length > 0) ?? [];
|
|
271
|
+
const findCommands = params.find?.filter((c) => c.url.trim() && c.pattern.trim()) ?? [];
|
|
272
|
+
const clickCommands = params.click?.filter((c) => c.url.trim()) ?? [];
|
|
273
|
+
const screenshotCommands = params.screenshot?.filter((c) => c.url.trim()) ?? [];
|
|
274
|
+
const financeCommands = params.finance ?? [];
|
|
275
|
+
const weatherCommands = params.weather ?? [];
|
|
276
|
+
const sportsCommands = params.sports ?? [];
|
|
277
|
+
const timeCommands = params.time?.map((c) => ({ utc_offset: c.utc_offset })) ?? [];
|
|
278
|
+
if (
|
|
279
|
+
queries.length === 0 &&
|
|
280
|
+
imageQueries.length === 0 &&
|
|
281
|
+
urls.length === 0 &&
|
|
282
|
+
findCommands.length === 0 &&
|
|
283
|
+
clickCommands.length === 0 &&
|
|
284
|
+
screenshotCommands.length === 0 &&
|
|
285
|
+
financeCommands.length === 0 &&
|
|
286
|
+
weatherCommands.length === 0 &&
|
|
287
|
+
sportsCommands.length === 0 &&
|
|
288
|
+
timeCommands.length === 0
|
|
289
|
+
) {
|
|
290
|
+
throw new CodexError(
|
|
291
|
+
"schema",
|
|
292
|
+
"At least one query, url, page action, or lookup command is required",
|
|
293
|
+
);
|
|
98
294
|
}
|
|
99
295
|
|
|
296
|
+
const startedAt = Date.now();
|
|
297
|
+
|
|
100
298
|
const token = await ctx.modelRegistry.getApiKeyForProvider(OPENAI_CODEX_PROVIDER);
|
|
101
299
|
if (!token) {
|
|
102
|
-
const err = new
|
|
300
|
+
const err = new CodexError(
|
|
301
|
+
"auth",
|
|
103
302
|
"OpenAI Codex subscription is not configured. Run `/login openai-codex` and choose ChatGPT Plus/Pro.",
|
|
104
303
|
);
|
|
105
|
-
(err as Error & { kind?: CodexErrorKind }).kind = "auth";
|
|
106
304
|
throw err;
|
|
107
305
|
}
|
|
108
306
|
|
|
109
307
|
const accountId = getConfiguredAccountId(ctx, token);
|
|
110
308
|
if (!accountId) {
|
|
111
|
-
|
|
309
|
+
throw new CodexError(
|
|
310
|
+
"auth",
|
|
112
311
|
"OpenAI Codex account id was not found in stored credentials or access token. Re-run `/login openai-codex`.",
|
|
113
312
|
);
|
|
114
|
-
(err as Error & { kind?: CodexErrorKind }).kind = "auth";
|
|
115
|
-
throw err;
|
|
116
313
|
}
|
|
117
314
|
|
|
118
315
|
const model = await resolveSearchModel(ctx, token, accountId, config, signal);
|
|
119
316
|
const freshness = params.freshness ?? config.defaultFreshness;
|
|
120
|
-
|
|
317
|
+
let searchContextSize = params.search_context_size ?? config.defaultSearchContextSize;
|
|
318
|
+
if (config.searchApi === "standalone") {
|
|
319
|
+
if (params.search_context_size === "low") {
|
|
320
|
+
assertSupportedStandaloneCombination("low", freshness);
|
|
321
|
+
}
|
|
322
|
+
if (searchContextSize === "low") searchContextSize = "medium";
|
|
323
|
+
assertSupportedStandaloneCombination(searchContextSize, freshness);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const transport = createTransport({
|
|
327
|
+
token,
|
|
328
|
+
accountId,
|
|
329
|
+
baseUrl: config.baseUrl,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (config.searchApi === "standalone") {
|
|
333
|
+
if (queries.length > 0 || imageQueries.length > 0) {
|
|
334
|
+
throw new CodexError(
|
|
335
|
+
"schema",
|
|
336
|
+
"codex_standalone_web does not support search or image search queries. Use codex_search for web search.",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const refStore = createRefStore();
|
|
340
|
+
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
341
|
+
await refStore.load(sessionDir);
|
|
342
|
+
|
|
343
|
+
const baseStandaloneOptions = {
|
|
344
|
+
model,
|
|
345
|
+
transport,
|
|
346
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
347
|
+
freshness,
|
|
348
|
+
searchContextSize,
|
|
349
|
+
maxOutputTokens: 8000,
|
|
350
|
+
signal,
|
|
351
|
+
};
|
|
352
|
+
const standaloneCalls: StandaloneCallPlan[] = [
|
|
353
|
+
...queries.map((q) => ({
|
|
354
|
+
query: q,
|
|
355
|
+
buildOptions: () => ({ ...baseStandaloneOptions, searchQuery: [{ q }] }),
|
|
356
|
+
})),
|
|
357
|
+
...imageQueries.map((q) => ({
|
|
358
|
+
query: `image: ${q}`,
|
|
359
|
+
buildOptions: () => ({ ...baseStandaloneOptions, imageQuery: [{ q }] }),
|
|
360
|
+
})),
|
|
361
|
+
...urls.map((url) => ({
|
|
362
|
+
query: `open: ${url}`,
|
|
363
|
+
openedUrl: url,
|
|
364
|
+
buildOptions: () => ({
|
|
365
|
+
...baseStandaloneOptions,
|
|
366
|
+
open: [{ refId: refStore.resolveRefId(url) ?? url }],
|
|
367
|
+
}),
|
|
368
|
+
})),
|
|
369
|
+
...findCommands.map((c: { url: string; pattern: string }) => ({
|
|
370
|
+
query: `find "${c.pattern}" in ${c.url}`,
|
|
371
|
+
buildOptions: () => ({
|
|
372
|
+
...baseStandaloneOptions,
|
|
373
|
+
find: [
|
|
374
|
+
{
|
|
375
|
+
refId: resolveStandalonePageRef(refStore, c.url, "find"),
|
|
376
|
+
pattern: c.pattern,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
}),
|
|
380
|
+
})),
|
|
381
|
+
...clickCommands.map((c: { url: string; id: number }) => ({
|
|
382
|
+
query: `click ${c.id} in ${c.url}`,
|
|
383
|
+
buildOptions: () => ({
|
|
384
|
+
...baseStandaloneOptions,
|
|
385
|
+
click: [{ refId: resolveStandalonePageRef(refStore, c.url, "click"), id: c.id }],
|
|
386
|
+
}),
|
|
387
|
+
})),
|
|
388
|
+
...screenshotCommands.map((c: { url: string; pageno: number }) => ({
|
|
389
|
+
query: `screenshot ${c.pageno} of ${c.url}`,
|
|
390
|
+
buildOptions: () => ({
|
|
391
|
+
...baseStandaloneOptions,
|
|
392
|
+
screenshot: [
|
|
393
|
+
{
|
|
394
|
+
refId: resolveStandalonePageRef(refStore, c.url, "screenshot"),
|
|
395
|
+
pageno: c.pageno,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
}),
|
|
399
|
+
})),
|
|
400
|
+
...financeCommands.map((c) => ({
|
|
401
|
+
query: `finance: ${c.ticker}`,
|
|
402
|
+
buildOptions: () => ({ ...baseStandaloneOptions, finance: [c] }),
|
|
403
|
+
})),
|
|
404
|
+
...weatherCommands.map((c) => ({
|
|
405
|
+
query: `weather: ${c.location}`,
|
|
406
|
+
buildOptions: () => ({ ...baseStandaloneOptions, weather: [c] }),
|
|
407
|
+
})),
|
|
408
|
+
...sportsCommands.map((c) => ({
|
|
409
|
+
query: `sports: ${c.fn} ${c.league}${c.team ? ` ${c.team}` : ""}`,
|
|
410
|
+
buildOptions: () => ({ ...baseStandaloneOptions, sports: [c] }),
|
|
411
|
+
})),
|
|
412
|
+
...timeCommands.map((c) => ({
|
|
413
|
+
query: `time: ${c.utc_offset}`,
|
|
414
|
+
buildOptions: () => ({ ...baseStandaloneOptions, time: [c] }),
|
|
415
|
+
})),
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
const total = standaloneCalls.length;
|
|
419
|
+
if (total > 1) {
|
|
420
|
+
throw new CodexError(
|
|
421
|
+
"schema",
|
|
422
|
+
`${config.toolName} accepts exactly one standalone action per tool call. Split the request or use codex_search for batched search.`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
let completed = 0;
|
|
426
|
+
const emitPartial = (partialText: string) => {
|
|
427
|
+
onUpdate?.({
|
|
428
|
+
content: [{ type: "text", text: partialText }],
|
|
429
|
+
details: buildDetails(config, model, freshness, searchContextSize, [], [], {
|
|
430
|
+
partial: true,
|
|
431
|
+
completed,
|
|
432
|
+
total,
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
if (total > 1) emitPartial(formatProgress(completed, total));
|
|
437
|
+
|
|
438
|
+
const successes: QuerySuccess[] = [];
|
|
439
|
+
const failures: QueryFailure[] = [];
|
|
440
|
+
for (const call of standaloneCalls) {
|
|
441
|
+
try {
|
|
442
|
+
const result = await runStandaloneCommands(call.buildOptions());
|
|
443
|
+
if (call.openedUrl) {
|
|
444
|
+
const refId = selectStandalonePageRefId(result.refIds);
|
|
445
|
+
if (refId) await refStore.remember(call.openedUrl, refId);
|
|
446
|
+
}
|
|
447
|
+
const success: QuerySuccess = {
|
|
448
|
+
query: call.query,
|
|
449
|
+
text: result.text,
|
|
450
|
+
citations: result.citations,
|
|
451
|
+
searchCalls: result.searchCalls,
|
|
452
|
+
};
|
|
453
|
+
if (result.refIds) success.refIds = result.refIds;
|
|
454
|
+
if (result.usage) success.usage = result.usage;
|
|
455
|
+
successes.push(success);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
const kind = classifyError(error);
|
|
458
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
459
|
+
failures.push({ query: call.query, kind, message });
|
|
460
|
+
} finally {
|
|
461
|
+
completed += 1;
|
|
462
|
+
if (total > 1) emitPartial(formatProgress(completed, total));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (successes.length === 0) {
|
|
467
|
+
const primary = failures[0];
|
|
468
|
+
const summary =
|
|
469
|
+
failures.length === 1
|
|
470
|
+
? (primary?.message ?? "Codex standalone request failed")
|
|
471
|
+
: `All ${failures.length} ${config.toolName} standalone actions failed: ${failures
|
|
472
|
+
.map((f, i) => `${i + 1}. [${f.kind}] ${f.message}`)
|
|
473
|
+
.join("; ")}`;
|
|
474
|
+
const err = new CodexError(primary?.kind ?? "unknown", summary) as CodexError & {
|
|
475
|
+
failures?: QueryFailure[];
|
|
476
|
+
};
|
|
477
|
+
err.failures = failures;
|
|
478
|
+
throw err;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
content: [{ type: "text", text: formatToolText(successes, failures) }],
|
|
483
|
+
details: buildDetails(config, model, freshness, searchContextSize, successes, failures, {
|
|
484
|
+
elapsedMs: Date.now() - startedAt,
|
|
485
|
+
}),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Responses API path: only search queries are supported.
|
|
490
|
+
if (
|
|
491
|
+
urls.length > 0 ||
|
|
492
|
+
findCommands.length > 0 ||
|
|
493
|
+
clickCommands.length > 0 ||
|
|
494
|
+
screenshotCommands.length > 0 ||
|
|
495
|
+
imageQueries.length > 0 ||
|
|
496
|
+
financeCommands.length > 0 ||
|
|
497
|
+
weatherCommands.length > 0 ||
|
|
498
|
+
sportsCommands.length > 0 ||
|
|
499
|
+
timeCommands.length > 0
|
|
500
|
+
) {
|
|
501
|
+
throw new CodexError(
|
|
502
|
+
"schema",
|
|
503
|
+
`Open webpage and domain lookups require codex_standalone_web. Current tool is codex_search. Search requests should stay on codex_search.`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
121
506
|
|
|
122
507
|
const total = queries.length;
|
|
123
508
|
let completed = 0;
|
|
@@ -126,25 +511,18 @@ function buildTool(config: ResolvedConfig) {
|
|
|
126
511
|
const emitPartial = (partialText: string) => {
|
|
127
512
|
onUpdate?.({
|
|
128
513
|
content: [{ type: "text", text: partialText }],
|
|
129
|
-
details: {
|
|
130
|
-
model,
|
|
131
|
-
freshness,
|
|
132
|
-
searchContextSize,
|
|
133
|
-
queryCount: total,
|
|
134
|
-
failedQueryCount: 0,
|
|
135
|
-
successes: [],
|
|
136
|
-
failures: [],
|
|
514
|
+
details: buildDetails(config, model, freshness, searchContextSize, [], [], {
|
|
137
515
|
partial: true,
|
|
138
516
|
completed,
|
|
139
517
|
total,
|
|
140
|
-
}
|
|
518
|
+
}),
|
|
141
519
|
});
|
|
142
520
|
};
|
|
143
521
|
|
|
144
522
|
if (total > 1) emitPartial(formatProgress(completed, total));
|
|
145
523
|
|
|
146
524
|
const settled = await Promise.allSettled(
|
|
147
|
-
queries.map(async (query) => {
|
|
525
|
+
queries.map(async (query: string) => {
|
|
148
526
|
const onTextDelta =
|
|
149
527
|
total === 1
|
|
150
528
|
? (delta: string) => {
|
|
@@ -152,20 +530,18 @@ function buildTool(config: ResolvedConfig) {
|
|
|
152
530
|
emitPartial(streamedText);
|
|
153
531
|
}
|
|
154
532
|
: undefined;
|
|
155
|
-
const fetchOpts: Parameters<typeof fetchCodexWebSearch>[0] = {
|
|
156
|
-
query,
|
|
157
|
-
token,
|
|
158
|
-
accountId,
|
|
159
|
-
model,
|
|
160
|
-
externalWebAccess: freshness === "live",
|
|
161
|
-
searchContextSize,
|
|
162
|
-
};
|
|
163
|
-
if (config.baseUrl !== undefined) fetchOpts.baseUrl = config.baseUrl;
|
|
164
|
-
if (signal) fetchOpts.signal = signal;
|
|
165
|
-
if (onTextDelta) fetchOpts.onTextDelta = onTextDelta;
|
|
166
|
-
|
|
167
533
|
try {
|
|
168
|
-
return await
|
|
534
|
+
return await runResponsesSearch({
|
|
535
|
+
query,
|
|
536
|
+
model,
|
|
537
|
+
transport,
|
|
538
|
+
externalWebAccess: freshness !== "cached",
|
|
539
|
+
searchContextSize,
|
|
540
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
541
|
+
threadId: ctx.sessionManager.getSessionId(),
|
|
542
|
+
signal,
|
|
543
|
+
onTextDelta,
|
|
544
|
+
});
|
|
169
545
|
} finally {
|
|
170
546
|
completed += 1;
|
|
171
547
|
if (total > 1) emitPartial(formatProgress(completed, total));
|
|
@@ -189,9 +565,9 @@ function buildTool(config: ResolvedConfig) {
|
|
|
189
565
|
if (outcome.value.usage !== undefined) success.usage = outcome.value.usage;
|
|
190
566
|
successes.push(success);
|
|
191
567
|
} else {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
568
|
+
const reason = outcome.reason;
|
|
569
|
+
const kind = classifyError(reason);
|
|
570
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
195
571
|
failures.push({ query, kind, message });
|
|
196
572
|
}
|
|
197
573
|
});
|
|
@@ -204,42 +580,41 @@ function buildTool(config: ResolvedConfig) {
|
|
|
204
580
|
: `All ${failures.length} ${config.toolName} queries failed: ${failures
|
|
205
581
|
.map((f, i) => `${i + 1}. [${f.kind}] ${f.message}`)
|
|
206
582
|
.join("; ")}`;
|
|
207
|
-
const err = new
|
|
208
|
-
kind?: CodexErrorKind;
|
|
583
|
+
const err = new CodexError(primary?.kind ?? "unknown", summary) as CodexError & {
|
|
209
584
|
failures?: QueryFailure[];
|
|
210
585
|
};
|
|
211
|
-
err.kind = primary?.kind ?? "unknown";
|
|
212
586
|
err.failures = failures;
|
|
213
587
|
throw err;
|
|
214
588
|
}
|
|
215
589
|
|
|
216
590
|
return {
|
|
217
591
|
content: [{ type: "text", text: formatToolText(successes, failures) }],
|
|
218
|
-
details: {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
searchContextSize,
|
|
222
|
-
queryCount: total,
|
|
223
|
-
failedQueryCount: failures.length,
|
|
224
|
-
successes,
|
|
225
|
-
failures,
|
|
226
|
-
} satisfies WebSearchDetails,
|
|
592
|
+
details: buildDetails(config, model, freshness, searchContextSize, successes, failures, {
|
|
593
|
+
elapsedMs: Date.now() - startedAt,
|
|
594
|
+
}),
|
|
227
595
|
};
|
|
228
596
|
},
|
|
229
597
|
|
|
230
598
|
renderCall(args, theme) {
|
|
231
|
-
const queries = Array.isArray(args.queries) ? args.queries : [];
|
|
232
599
|
const fresh = (args.freshness as string | undefined) ?? config.defaultFreshness;
|
|
233
|
-
const
|
|
600
|
+
const requestedCtxSize =
|
|
234
601
|
(args.search_context_size as string | undefined) ?? config.defaultSearchContextSize;
|
|
602
|
+
const ctxSize =
|
|
603
|
+
config.searchApi === "standalone" && requestedCtxSize === "low"
|
|
604
|
+
? "medium"
|
|
605
|
+
: requestedCtxSize;
|
|
606
|
+
const labels = buildCallLabels(args);
|
|
235
607
|
|
|
236
|
-
let text = theme.fg("toolTitle", theme.bold(
|
|
237
|
-
if (
|
|
238
|
-
text += theme.fg("accent", formatInline(
|
|
608
|
+
let text = theme.fg("toolTitle", theme.bold(config.toolName));
|
|
609
|
+
if (labels.length === 1) {
|
|
610
|
+
text += ` ${theme.fg("accent", formatInline(labels[0] ?? "", 90))}`;
|
|
239
611
|
} else {
|
|
240
|
-
text += theme.fg("accent", `${
|
|
612
|
+
text += ` ${theme.fg("accent", `${labels.length} actions`)}`;
|
|
613
|
+
}
|
|
614
|
+
text += theme.fg("dim", ` ${formatModeLabel(config.searchApi, ctxSize, fresh)}`);
|
|
615
|
+
if (labels.length > 1) {
|
|
616
|
+
text += `\n${renderCallQueries(labels, theme)}`;
|
|
241
617
|
}
|
|
242
|
-
text += theme.fg("dim", ` [${ctxSize}/${fresh}]`);
|
|
243
618
|
return new Text(text, 0, 0);
|
|
244
619
|
},
|
|
245
620
|
|
|
@@ -259,7 +634,7 @@ function buildTool(config: ResolvedConfig) {
|
|
|
259
634
|
const total = details.queryCount;
|
|
260
635
|
const failed = details.failedQueryCount;
|
|
261
636
|
const ok = total - failed;
|
|
262
|
-
const
|
|
637
|
+
const resultSuffix = formatResultSuffix(details);
|
|
263
638
|
|
|
264
639
|
let header: string;
|
|
265
640
|
if (ok === 0) {
|
|
@@ -267,16 +642,19 @@ function buildTool(config: ResolvedConfig) {
|
|
|
267
642
|
} else if (failed > 0) {
|
|
268
643
|
header = theme.fg(
|
|
269
644
|
"warning",
|
|
270
|
-
|
|
645
|
+
`Did ${ok}/${total} ${formatOperationNoun(details, total)}${formatDurationSuffix(details.elapsedMs)}${resultSuffix}`,
|
|
271
646
|
);
|
|
272
647
|
} else {
|
|
273
|
-
const
|
|
648
|
+
const operationCount = countSuccessOperations(details);
|
|
274
649
|
header = theme.fg(
|
|
275
650
|
"success",
|
|
276
|
-
|
|
651
|
+
`Did ${operationCount} ${formatOperationNoun(details, operationCount)}${formatDurationSuffix(details.elapsedMs)}${resultSuffix}`,
|
|
277
652
|
);
|
|
278
653
|
}
|
|
279
|
-
header += theme.fg(
|
|
654
|
+
header += theme.fg(
|
|
655
|
+
"muted",
|
|
656
|
+
` ${formatModeLabel(details.api, details.searchContextSize, details.freshness)}`,
|
|
657
|
+
);
|
|
280
658
|
|
|
281
659
|
if (!expanded) {
|
|
282
660
|
const preview = renderCollapsedPreview(details, theme);
|
|
@@ -309,9 +687,20 @@ export default function codexWebSearchExtension(pi: ExtensionAPI) {
|
|
|
309
687
|
registerSettingsCommand(pi);
|
|
310
688
|
|
|
311
689
|
pi.on("session_start", async (_event, ctx) => {
|
|
312
|
-
const config = await loadConfig(ctx.cwd);
|
|
690
|
+
const config = await loadConfig(ctx.cwd, isProjectTrustedContext(ctx));
|
|
313
691
|
if (!config.enabled) return;
|
|
314
|
-
|
|
692
|
+
|
|
693
|
+
pi.registerTool(buildTool({ ...config, searchApi: "responses", toolName: "codex_search" }));
|
|
694
|
+
if (config.standaloneEnabled) {
|
|
695
|
+
pi.registerTool(
|
|
696
|
+
buildTool({
|
|
697
|
+
...config,
|
|
698
|
+
searchApi: "standalone",
|
|
699
|
+
toolName: STANDALONE_TOOL_NAME,
|
|
700
|
+
batchSize: 1,
|
|
701
|
+
}),
|
|
702
|
+
);
|
|
703
|
+
}
|
|
315
704
|
});
|
|
316
705
|
}
|
|
317
706
|
|
|
@@ -333,22 +722,44 @@ async function resolveSearchModel(
|
|
|
333
722
|
if (config.model) return config.model;
|
|
334
723
|
if (ctx.model?.provider === OPENAI_CODEX_PROVIDER) return ctx.model.id;
|
|
335
724
|
|
|
336
|
-
const
|
|
725
|
+
const models = await fetchCodexModels({
|
|
337
726
|
token,
|
|
338
727
|
accountId,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const models = await fetchCodexModels(fetchOpts);
|
|
728
|
+
baseUrl: config.baseUrl,
|
|
729
|
+
clientVersion: config.clientVersion,
|
|
730
|
+
signal,
|
|
731
|
+
});
|
|
345
732
|
const model = selectDefaultModel(models);
|
|
346
733
|
if (!model) {
|
|
347
|
-
throw new
|
|
734
|
+
throw new CodexError("unknown", "Codex model list is empty.");
|
|
348
735
|
}
|
|
349
736
|
return model;
|
|
350
737
|
}
|
|
351
738
|
|
|
739
|
+
function buildDetails(
|
|
740
|
+
config: ResolvedConfig,
|
|
741
|
+
model: string,
|
|
742
|
+
freshness: Freshness,
|
|
743
|
+
searchContextSize: SearchContextSize,
|
|
744
|
+
successes: QuerySuccess[],
|
|
745
|
+
failures: QueryFailure[],
|
|
746
|
+
extra?: { partial?: boolean; completed?: number; total?: number; elapsedMs?: number },
|
|
747
|
+
): WebSearchDetails {
|
|
748
|
+
const queries = successes.map((s) => s.query).concat(failures.map((f) => f.query));
|
|
749
|
+
return {
|
|
750
|
+
model,
|
|
751
|
+
api: config.searchApi,
|
|
752
|
+
freshness,
|
|
753
|
+
searchContextSize,
|
|
754
|
+
queryCount: queries.length,
|
|
755
|
+
queries,
|
|
756
|
+
failedQueryCount: failures.length,
|
|
757
|
+
successes,
|
|
758
|
+
failures,
|
|
759
|
+
...extra,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
352
763
|
function formatProgress(completed: number, total: number): string {
|
|
353
764
|
return `Searching ${completed}/${total} ${completed === total ? "complete" : "in progress"}`;
|
|
354
765
|
}
|
|
@@ -374,10 +785,66 @@ function formatSuccessBlock(success: QuerySuccess, multiple: boolean): string {
|
|
|
374
785
|
const title = citation.title?.trim() || citation.url;
|
|
375
786
|
return `${index + 1}. ${title}: ${citation.url}`;
|
|
376
787
|
});
|
|
377
|
-
const
|
|
788
|
+
const refLines = Object.keys(success.refIds ?? {}).map(
|
|
789
|
+
(refId, index) => `${index + 1}. ${refId}`,
|
|
790
|
+
);
|
|
791
|
+
const sourceBlock = sourceLines.length > 0 ? `Sources:\n${sourceLines.join("\n")}` : "";
|
|
792
|
+
const refBlock = refLines.length > 0 ? `Source refs:\n${refLines.join("\n")}` : "";
|
|
793
|
+
const blocks = [text, sourceBlock, refBlock].filter((block) => block.length > 0);
|
|
794
|
+
const body = blocks.join("\n\n");
|
|
378
795
|
return multiple ? `## Query: ${success.query}\n\n${body}` : body;
|
|
379
796
|
}
|
|
380
797
|
|
|
798
|
+
function countSuccessCitations(details: WebSearchDetails): number {
|
|
799
|
+
return details.successes.reduce((acc, success) => acc + success.citations.length, 0);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function countSuccessWebActions(details: WebSearchDetails): number {
|
|
803
|
+
return details.successes.reduce((acc, success) => acc + success.searchCalls.length, 0);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function countSuccessOperations(details: WebSearchDetails): number {
|
|
807
|
+
if (details.api === "responses") return details.queryCount;
|
|
808
|
+
const count = countSuccessWebActions(details);
|
|
809
|
+
return count > 0 ? count : details.queryCount;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function formatOperationNoun(details: WebSearchDetails, count: number): string {
|
|
813
|
+
const singular = details.api === "standalone" ? "action" : "search";
|
|
814
|
+
const plural = details.api === "standalone" ? "actions" : "searches";
|
|
815
|
+
return count === 1 ? singular : plural;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function formatResultSuffix(details: WebSearchDetails): string {
|
|
819
|
+
const parts: string[] = [];
|
|
820
|
+
const webActionCount = countSuccessWebActions(details);
|
|
821
|
+
if (details.api === "responses" && webActionCount > 0) {
|
|
822
|
+
parts.push(`${webActionCount} web action${webActionCount === 1 ? "" : "s"}`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const citationCount = countSuccessCitations(details);
|
|
826
|
+
if (citationCount > 0) {
|
|
827
|
+
parts.push(`${citationCount} source${citationCount === 1 ? "" : "s"}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return parts.length > 0 ? ` · ${parts.join(" · ")}` : "";
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function formatDurationSuffix(elapsedMs: number | undefined): string {
|
|
834
|
+
if (elapsedMs === undefined) return "";
|
|
835
|
+
if (elapsedMs < 1000) return ` in ${elapsedMs}ms`;
|
|
836
|
+
return ` in ${Math.max(1, Math.round(elapsedMs / 1000))}s`;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function formatModeLabel(
|
|
840
|
+
api: string,
|
|
841
|
+
searchContextSize: string,
|
|
842
|
+
freshness: string | undefined,
|
|
843
|
+
): string {
|
|
844
|
+
if (api === "standalone") return `[${api}/${searchContextSize}]`;
|
|
845
|
+
return `[${api}/${searchContextSize}/${freshness ?? "live"}]`;
|
|
846
|
+
}
|
|
847
|
+
|
|
381
848
|
function formatFailureBlock(failure: QueryFailure, multiple: boolean): string {
|
|
382
849
|
const body = `[${failure.kind}] ${failure.message}`;
|
|
383
850
|
return multiple ? `## Query: ${failure.query}\n\nFAILED: ${body}` : `FAILED: ${body}`;
|
|
@@ -393,16 +860,200 @@ function renderPartial(details: WebSearchDetails | undefined, theme: Theme): str
|
|
|
393
860
|
}
|
|
394
861
|
|
|
395
862
|
function renderCollapsedPreview(details: WebSearchDetails, theme: Theme): string {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
863
|
+
const lines: string[] = [];
|
|
864
|
+
const successPreview = renderSuccessPreview(details, theme);
|
|
865
|
+
if (successPreview) lines.push(successPreview);
|
|
866
|
+
|
|
401
867
|
const firstFailure = details.failures[0];
|
|
402
868
|
if (firstFailure) {
|
|
403
|
-
|
|
869
|
+
lines.push(theme.fg("dim", formatInline(firstFailure.message, 110)));
|
|
404
870
|
}
|
|
405
|
-
return "";
|
|
871
|
+
return lines.join("\n");
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function renderSuccessPreview(details: WebSearchDetails, theme: Theme): string {
|
|
875
|
+
if (details.api !== "standalone") return "";
|
|
876
|
+
const success = details.successes[0];
|
|
877
|
+
if (!success) return "";
|
|
878
|
+
const line = firstNonEmptyLine(success.text);
|
|
879
|
+
if (!line) return "";
|
|
880
|
+
const actionType = success.searchCalls[0]?.actionType;
|
|
881
|
+
return theme.fg("dim", `${formatStandalonePreviewLabel(actionType)}: ${formatInline(line, 120)}`);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function firstNonEmptyLine(text: string): string {
|
|
885
|
+
return (
|
|
886
|
+
text
|
|
887
|
+
.split("\n")
|
|
888
|
+
.map((line) => line.trim())
|
|
889
|
+
.find((line) => line.length > 0) ?? ""
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function formatStandalonePreviewLabel(actionType: string | undefined): string {
|
|
894
|
+
if (actionType === "open_page" || actionType === "click") return "Opened";
|
|
895
|
+
if (actionType === "find_in_page") return "Found";
|
|
896
|
+
if (actionType === "screenshot") return "Screenshot";
|
|
897
|
+
return "Result";
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function renderCallQueries(queries: unknown[], theme: Theme): string {
|
|
901
|
+
const iconPrefix = " ⌕";
|
|
902
|
+
return formatQueryPreviewLines(queries)
|
|
903
|
+
.map(
|
|
904
|
+
(line) =>
|
|
905
|
+
`${theme.fg("accent", iconPrefix)}${theme.fg("dim", line.slice(iconPrefix.length))}`,
|
|
906
|
+
)
|
|
907
|
+
.join("\n");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function resolveStandalonePageRef(
|
|
911
|
+
refStore: ReturnType<typeof createRefStore>,
|
|
912
|
+
urlOrRef: string,
|
|
913
|
+
action: string,
|
|
914
|
+
): string {
|
|
915
|
+
const refId = refStore.resolveRefId(urlOrRef);
|
|
916
|
+
if (refId) return refId;
|
|
917
|
+
if (/^turn\d+(?:view|fetch)\d+$/.test(urlOrRef)) return urlOrRef;
|
|
918
|
+
throw new CodexError(
|
|
919
|
+
"schema",
|
|
920
|
+
`${action} requires opening ${urlOrRef} with codex_standalone_web urls first in this session.`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
export function selectStandalonePageRefId(
|
|
925
|
+
refIds: Record<string, string> | undefined,
|
|
926
|
+
): string | undefined {
|
|
927
|
+
const refs = Object.keys(refIds ?? {});
|
|
928
|
+
return (
|
|
929
|
+
refs.find((candidate) => /^turn\d+view\d+$/.test(candidate)) ??
|
|
930
|
+
refs.find((candidate) => /^turn\d+fetch\d+$/.test(candidate))
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export function formatQueryPreviewLines(queries: unknown[], maxLength = 110): string[] {
|
|
935
|
+
return queries.map((query, index) => ` ⌕ ${index + 1}. ${formatInline(query, maxLength)}`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function buildRequestLabels(input: {
|
|
939
|
+
queries: string[];
|
|
940
|
+
imageQueries: string[];
|
|
941
|
+
urls: string[];
|
|
942
|
+
findCommands: Array<{ url: string; pattern: string }>;
|
|
943
|
+
clickCommands: Array<{ url: string; id: number }>;
|
|
944
|
+
screenshotCommands: Array<{ url: string; pageno: number }>;
|
|
945
|
+
financeCommands: Array<{ ticker: string }>;
|
|
946
|
+
weatherCommands: Array<{ location: string }>;
|
|
947
|
+
sportsCommands: Array<{ fn: string; league: string; team?: string }>;
|
|
948
|
+
timeCommands: Array<{ utc_offset: string }>;
|
|
949
|
+
}): string[] {
|
|
950
|
+
return [
|
|
951
|
+
...input.queries,
|
|
952
|
+
...input.imageQueries.map((q) => `image: ${q}`),
|
|
953
|
+
...input.urls.map((url) => `open: ${url}`),
|
|
954
|
+
...input.findCommands.map((c) => `find "${c.pattern}" in ${c.url}`),
|
|
955
|
+
...input.clickCommands.map((c) => `click ${c.id} in ${c.url}`),
|
|
956
|
+
...input.screenshotCommands.map((c) => `screenshot ${c.pageno} of ${c.url}`),
|
|
957
|
+
...input.financeCommands.map((c) => `finance: ${c.ticker}`),
|
|
958
|
+
...input.weatherCommands.map((c) => `weather: ${c.location}`),
|
|
959
|
+
...input.sportsCommands.map((c) => `sports: ${c.fn} ${c.league}${c.team ? ` ${c.team}` : ""}`),
|
|
960
|
+
...input.timeCommands.map((c) => `time: ${c.utc_offset}`),
|
|
961
|
+
];
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function buildCallLabels(args: Record<string, unknown>): string[] {
|
|
965
|
+
return buildRequestLabels({
|
|
966
|
+
queries: Array.isArray(args.queries) ? args.queries.filter(isString) : [],
|
|
967
|
+
imageQueries: Array.isArray(args.image_queries) ? args.image_queries.filter(isString) : [],
|
|
968
|
+
urls: Array.isArray(args.urls) ? args.urls.filter(isString) : [],
|
|
969
|
+
findCommands: Array.isArray(args.find)
|
|
970
|
+
? args.find.filter(isFindArg).map((c) => ({ url: c.url, pattern: c.pattern }))
|
|
971
|
+
: [],
|
|
972
|
+
clickCommands: Array.isArray(args.click)
|
|
973
|
+
? args.click.filter(isClickArg).map((c) => ({ url: c.url, id: c.id }))
|
|
974
|
+
: [],
|
|
975
|
+
screenshotCommands: Array.isArray(args.screenshot)
|
|
976
|
+
? args.screenshot.filter(isScreenshotArg).map((c) => ({ url: c.url, pageno: c.pageno }))
|
|
977
|
+
: [],
|
|
978
|
+
financeCommands: Array.isArray(args.finance)
|
|
979
|
+
? args.finance.filter(isFinanceArg).map((c) => ({ ticker: c.ticker }))
|
|
980
|
+
: [],
|
|
981
|
+
weatherCommands: Array.isArray(args.weather)
|
|
982
|
+
? args.weather.filter(isWeatherArg).map((c) => ({ location: c.location }))
|
|
983
|
+
: [],
|
|
984
|
+
sportsCommands: Array.isArray(args.sports)
|
|
985
|
+
? args.sports.filter(isSportsArg).map((c) => ({ fn: c.fn, league: c.league, team: c.team }))
|
|
986
|
+
: [],
|
|
987
|
+
timeCommands: Array.isArray(args.time)
|
|
988
|
+
? args.time.filter(isTimeArg).map((c) => ({ utc_offset: c.utc_offset }))
|
|
989
|
+
: [],
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function isString(value: unknown): value is string {
|
|
994
|
+
return typeof value === "string";
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function isFindArg(value: unknown): value is { url: string; pattern: string } {
|
|
998
|
+
return (
|
|
999
|
+
typeof value === "object" &&
|
|
1000
|
+
value !== null &&
|
|
1001
|
+
typeof (value as { url?: unknown }).url === "string" &&
|
|
1002
|
+
typeof (value as { pattern?: unknown }).pattern === "string"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function isClickArg(value: unknown): value is { url: string; id: number } {
|
|
1007
|
+
return (
|
|
1008
|
+
typeof value === "object" &&
|
|
1009
|
+
value !== null &&
|
|
1010
|
+
typeof (value as { url?: unknown }).url === "string" &&
|
|
1011
|
+
typeof (value as { id?: unknown }).id === "number"
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function isScreenshotArg(value: unknown): value is { url: string; pageno: number } {
|
|
1016
|
+
return (
|
|
1017
|
+
typeof value === "object" &&
|
|
1018
|
+
value !== null &&
|
|
1019
|
+
typeof (value as { url?: unknown }).url === "string" &&
|
|
1020
|
+
typeof (value as { pageno?: unknown }).pageno === "number"
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function isFinanceArg(value: unknown): value is { ticker: string } {
|
|
1025
|
+
return (
|
|
1026
|
+
typeof value === "object" &&
|
|
1027
|
+
value !== null &&
|
|
1028
|
+
typeof (value as { ticker?: unknown }).ticker === "string"
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function isWeatherArg(value: unknown): value is { location: string } {
|
|
1033
|
+
return (
|
|
1034
|
+
typeof value === "object" &&
|
|
1035
|
+
value !== null &&
|
|
1036
|
+
typeof (value as { location?: unknown }).location === "string"
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function isSportsArg(value: unknown): value is { fn: string; league: string; team?: string } {
|
|
1041
|
+
return (
|
|
1042
|
+
typeof value === "object" &&
|
|
1043
|
+
value !== null &&
|
|
1044
|
+
typeof (value as { fn?: unknown }).fn === "string" &&
|
|
1045
|
+
typeof (value as { league?: unknown }).league === "string" &&
|
|
1046
|
+
((value as { team?: unknown }).team === undefined ||
|
|
1047
|
+
typeof (value as { team?: unknown }).team === "string")
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function isTimeArg(value: unknown): value is { utc_offset: string } {
|
|
1052
|
+
return (
|
|
1053
|
+
typeof value === "object" &&
|
|
1054
|
+
value !== null &&
|
|
1055
|
+
typeof (value as { utc_offset?: unknown }).utc_offset === "string"
|
|
1056
|
+
);
|
|
406
1057
|
}
|
|
407
1058
|
|
|
408
1059
|
function formatInline(value: unknown, maxLength = 90): string {
|