pi-codex-search 0.1.2 → 0.1.3
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 +697 -117
- 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/src/codex.ts
CHANGED
|
@@ -1,203 +1,95 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export
|
|
47
|
-
|
|
1
|
+
export {
|
|
2
|
+
CodexError,
|
|
3
|
+
classifyError,
|
|
4
|
+
classifyHttpStatus,
|
|
5
|
+
classifyEventErrorMessage,
|
|
6
|
+
} from "./errors.ts";
|
|
7
|
+
export type { CodexErrorKind } from "./errors.ts";
|
|
8
|
+
export type {
|
|
9
|
+
CodexCitation,
|
|
10
|
+
CodexSearchCall,
|
|
11
|
+
CodexWebSearchResult,
|
|
12
|
+
Freshness,
|
|
13
|
+
ResponseLength,
|
|
14
|
+
SearchContextSize,
|
|
15
|
+
StandaloneExternalWebAccess,
|
|
16
|
+
} from "./modes/types.ts";
|
|
17
|
+
export type { CodexModel } from "./modes/types.ts";
|
|
18
|
+
export { runResponsesSearch } from "./modes/responses.ts";
|
|
19
|
+
export {
|
|
20
|
+
runStandaloneCommands,
|
|
21
|
+
externalWebAccessForFreshness,
|
|
22
|
+
hasAnyCommand,
|
|
23
|
+
assertSupportedStandaloneCombination,
|
|
24
|
+
isUnsupportedStandaloneCombination,
|
|
25
|
+
} from "./modes/standalone.ts";
|
|
26
|
+
export type {
|
|
27
|
+
SearchQuery,
|
|
28
|
+
OpenCommand,
|
|
29
|
+
FindCommand,
|
|
30
|
+
ClickCommand,
|
|
31
|
+
ScreenshotCommand,
|
|
32
|
+
FinanceCommand,
|
|
33
|
+
WeatherCommand,
|
|
34
|
+
SportsCommand,
|
|
35
|
+
TimeCommand,
|
|
36
|
+
StandaloneCommandsOptions,
|
|
37
|
+
} from "./modes/standalone.ts";
|
|
38
|
+
export {
|
|
39
|
+
createTransport,
|
|
40
|
+
normalizeCodexBaseUrl,
|
|
41
|
+
resolveCodexEndpoint,
|
|
42
|
+
resolveCodexSearchEndpoint,
|
|
43
|
+
} from "./transport.ts";
|
|
44
|
+
export type { CodexTransport } from "./transport.ts";
|
|
45
|
+
export { createRefStore } from "./ref-store.ts";
|
|
46
|
+
export type { RefStore } from "./ref-store.ts";
|
|
47
|
+
export { buildCodexUserAgent, getCodexOriginator } from "./ua.ts";
|
|
48
|
+
export {
|
|
49
|
+
getSharedCookieStore,
|
|
50
|
+
wrapFetchWithCookies,
|
|
51
|
+
ChatGptCloudflareCookieStore,
|
|
52
|
+
} from "./cookies.ts";
|
|
53
|
+
export type { FetchLike } from "./cookies.ts";
|
|
54
|
+
|
|
55
|
+
export interface FetchCodexModelsOptions {
|
|
48
56
|
token: string;
|
|
49
57
|
accountId: string;
|
|
50
|
-
model: string;
|
|
51
58
|
baseUrl?: string;
|
|
52
|
-
|
|
53
|
-
searchContextSize?: SearchContextSize;
|
|
59
|
+
clientVersion?: string;
|
|
54
60
|
signal?: AbortSignal;
|
|
55
|
-
onTextDelta?: (delta: string) => void;
|
|
56
61
|
fetchImpl?: typeof fetch;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
url?: string;
|
|
71
|
-
actionType?: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface CodexWebSearchResult {
|
|
75
|
-
responseId?: string;
|
|
76
|
-
model: string;
|
|
77
|
-
text: string;
|
|
78
|
-
searchCalls: CodexSearchCall[];
|
|
79
|
-
citations: CodexCitation[];
|
|
80
|
-
usage?: {
|
|
81
|
-
inputTokens?: number;
|
|
82
|
-
outputTokens?: number;
|
|
83
|
-
totalTokens?: number;
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface SseEvent {
|
|
88
|
-
type: string;
|
|
89
|
-
data?: unknown;
|
|
90
|
-
raw?: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface ResponseOutputText {
|
|
94
|
-
type?: string;
|
|
95
|
-
text?: string;
|
|
96
|
-
annotations?: Array<{
|
|
97
|
-
type?: string;
|
|
98
|
-
title?: string;
|
|
99
|
-
url?: string;
|
|
100
|
-
start_index?: number;
|
|
101
|
-
end_index?: number;
|
|
102
|
-
}>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface ResponseOutputItem {
|
|
106
|
-
id?: string;
|
|
107
|
-
type?: string;
|
|
108
|
-
status?: string;
|
|
109
|
-
role?: string;
|
|
110
|
-
action?: {
|
|
111
|
-
type?: string;
|
|
112
|
-
query?: string;
|
|
113
|
-
queries?: string[];
|
|
114
|
-
url?: string;
|
|
115
|
-
};
|
|
116
|
-
content?: ResponseOutputText[];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface ResponseUsage {
|
|
120
|
-
input_tokens?: number;
|
|
121
|
-
output_tokens?: number;
|
|
122
|
-
total_tokens?: number;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface ResponseEnvelope {
|
|
126
|
-
id?: string;
|
|
127
|
-
usage?: ResponseUsage;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
interface ResponseEventData {
|
|
131
|
-
response?: ResponseEnvelope;
|
|
132
|
-
item?: ResponseOutputItem;
|
|
133
|
-
delta?: string;
|
|
134
|
-
error?: {
|
|
135
|
-
message?: string;
|
|
136
|
-
code?: string;
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
|
|
141
|
-
const DEFAULT_CLIENT_VERSION = "1.0.0";
|
|
142
|
-
const ACCOUNT_ID_CLAIM = "https://api.openai.com/auth";
|
|
143
|
-
|
|
144
|
-
export function normalizeCodexBaseUrl(baseUrl: string | undefined): string {
|
|
145
|
-
const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_BASE_URL;
|
|
146
|
-
const normalized = raw.replace(/\/+$/, "");
|
|
147
|
-
if (normalized.endsWith("/codex/responses"))
|
|
148
|
-
return normalized.slice(0, -"/codex/responses".length);
|
|
149
|
-
if (normalized.endsWith("/codex")) return normalized.slice(0, -"/codex".length);
|
|
150
|
-
return normalized;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function resolveCodexEndpoint(
|
|
154
|
-
baseUrl: string | undefined,
|
|
155
|
-
path: "models" | "responses",
|
|
156
|
-
): string {
|
|
157
|
-
return `${normalizeCodexBaseUrl(baseUrl)}/codex/${path}`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function extractAccountIdFromToken(token: string): string | undefined {
|
|
161
|
-
const parts = token.split(".");
|
|
162
|
-
if (parts.length !== 3) return undefined;
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8")) as {
|
|
166
|
-
[ACCOUNT_ID_CLAIM]?: { chatgpt_account_id?: unknown };
|
|
167
|
-
};
|
|
168
|
-
const accountId = payload[ACCOUNT_ID_CLAIM]?.chatgpt_account_id;
|
|
169
|
-
return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
|
|
170
|
-
} catch {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
64
|
+
export async function fetchCodexModels(
|
|
65
|
+
options: FetchCodexModelsOptions,
|
|
66
|
+
): Promise<import("./modes/types.ts").CodexModel[]> {
|
|
67
|
+
const { CodexError, classifyHttpStatus } = await import("./errors.ts");
|
|
68
|
+
const { createTransport } = await import("./transport.ts");
|
|
69
|
+
const transport = createTransport({
|
|
70
|
+
token: options.token,
|
|
71
|
+
accountId: options.accountId,
|
|
72
|
+
baseUrl: options.baseUrl,
|
|
73
|
+
fetchImpl: options.fetchImpl as typeof fetch,
|
|
74
|
+
});
|
|
174
75
|
|
|
175
|
-
|
|
176
|
-
token: string;
|
|
177
|
-
accountId: string;
|
|
178
|
-
baseUrl?: string;
|
|
179
|
-
clientVersion?: string;
|
|
180
|
-
fetchImpl?: typeof fetch;
|
|
181
|
-
signal?: AbortSignal;
|
|
182
|
-
}): Promise<CodexModel[]> {
|
|
183
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
184
|
-
const endpoint = new URL(resolveCodexEndpoint(options.baseUrl, "models"));
|
|
76
|
+
const endpoint = new URL(transport.resolveEndpoint("models"));
|
|
185
77
|
endpoint.searchParams.set(
|
|
186
78
|
"client_version",
|
|
187
|
-
options.clientVersion ??
|
|
188
|
-
process.env.PI_CODEX_WEB_SEARCH_CLIENT_VERSION ??
|
|
189
|
-
DEFAULT_CLIENT_VERSION,
|
|
79
|
+
options.clientVersion ?? process.env.PI_CODEX_WEB_SEARCH_CLIENT_VERSION ?? "1.0.0",
|
|
190
80
|
);
|
|
191
81
|
|
|
192
|
-
const response = await
|
|
193
|
-
headers:
|
|
82
|
+
const response = await transport.fetch(endpoint.toString(), {
|
|
83
|
+
headers: transport.buildHeaders("application/json"),
|
|
194
84
|
signal: options.signal,
|
|
195
85
|
});
|
|
86
|
+
|
|
196
87
|
if (!response.ok) {
|
|
197
88
|
const status = response.status;
|
|
89
|
+
const text = await response.text();
|
|
198
90
|
throw new CodexError(
|
|
199
91
|
classifyHttpStatus(status),
|
|
200
|
-
`Codex models request failed: HTTP ${status} ${
|
|
92
|
+
`Codex models request failed: HTTP ${status}: ${text}`,
|
|
201
93
|
status,
|
|
202
94
|
);
|
|
203
95
|
}
|
|
@@ -220,221 +112,23 @@ export async function fetchCodexModels(options: {
|
|
|
220
112
|
.filter((model) => model.id.length > 0);
|
|
221
113
|
}
|
|
222
114
|
|
|
223
|
-
export function selectDefaultModel(
|
|
115
|
+
export function selectDefaultModel(
|
|
116
|
+
models: import("./modes/types.ts").CodexModel[],
|
|
117
|
+
): string | undefined {
|
|
224
118
|
return (models.find((model) => model.isDefault) ?? models[0])?.id;
|
|
225
119
|
}
|
|
226
120
|
|
|
227
|
-
export
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
231
|
-
const response = await fetcher(resolveCodexEndpoint(options.baseUrl, "responses"), {
|
|
232
|
-
method: "POST",
|
|
233
|
-
headers: buildCodexHeaders(options.token, options.accountId, "text/event-stream"),
|
|
234
|
-
body: JSON.stringify(buildWebSearchRequestBody(options)),
|
|
235
|
-
signal: options.signal,
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
if (!response.ok) {
|
|
239
|
-
const status = response.status;
|
|
240
|
-
throw new CodexError(
|
|
241
|
-
classifyHttpStatus(status),
|
|
242
|
-
`Codex web search request failed: HTTP ${status} ${await response.text()}`,
|
|
243
|
-
status,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
if (!response.body) {
|
|
247
|
-
throw new CodexError("transport", "Codex web search response did not include a body");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
let responseId: string | undefined;
|
|
251
|
-
let usage: ResponseUsage | undefined;
|
|
252
|
-
let streamedText = "";
|
|
253
|
-
const messageTextParts: string[] = [];
|
|
254
|
-
const searchCalls = new Map<string, CodexSearchCall>();
|
|
255
|
-
const citations = new Map<string, CodexCitation>();
|
|
256
|
-
|
|
257
|
-
for await (const event of parseSse(response.body)) {
|
|
258
|
-
const data = event.data as ResponseEventData | undefined;
|
|
259
|
-
if (!data) continue;
|
|
260
|
-
|
|
261
|
-
if (event.type === "response.created") {
|
|
262
|
-
responseId = data.response?.id;
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (event.type === "response.output_text.delta") {
|
|
267
|
-
const delta = data.delta ?? "";
|
|
268
|
-
streamedText += delta;
|
|
269
|
-
options.onTextDelta?.(delta);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (event.type === "response.output_item.added" && data.item?.type === "web_search_call") {
|
|
274
|
-
const item = data.item;
|
|
275
|
-
searchCalls.set(item.id ?? `search-${searchCalls.size + 1}`, {
|
|
276
|
-
id: item.id,
|
|
277
|
-
status: item.status,
|
|
278
|
-
});
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (event.type === "response.output_item.done") {
|
|
283
|
-
collectOutputItem(data.item, searchCalls, messageTextParts, citations);
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (event.type === "response.completed") {
|
|
288
|
-
usage = data.response?.usage;
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (event.type === "response.failed") {
|
|
293
|
-
const message = data.error?.message ?? data.error?.code ?? "Codex web search failed";
|
|
294
|
-
throw new CodexError(classifyEventErrorMessage(message), message);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
responseId,
|
|
300
|
-
model: options.model,
|
|
301
|
-
text: messageTextParts.join("") || streamedText,
|
|
302
|
-
searchCalls: [...searchCalls.values()],
|
|
303
|
-
citations: [...citations.values()],
|
|
304
|
-
usage: usage
|
|
305
|
-
? {
|
|
306
|
-
inputTokens: usage.input_tokens,
|
|
307
|
-
outputTokens: usage.output_tokens,
|
|
308
|
-
totalTokens: usage.total_tokens,
|
|
309
|
-
}
|
|
310
|
-
: undefined,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function buildCodexHeaders(token: string, accountId: string, accept: string): Headers {
|
|
315
|
-
const headers = new Headers();
|
|
316
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
317
|
-
headers.set("chatgpt-account-id", accountId);
|
|
318
|
-
headers.set("originator", "pi");
|
|
319
|
-
headers.set("OpenAI-Beta", "responses=experimental");
|
|
320
|
-
headers.set("accept", accept);
|
|
321
|
-
if (accept === "text/event-stream") {
|
|
322
|
-
headers.set("content-type", "application/json");
|
|
323
|
-
}
|
|
324
|
-
headers.set("User-Agent", "pi-codex-search");
|
|
325
|
-
return headers;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function buildWebSearchRequestBody(options: CodexWebSearchOptions) {
|
|
329
|
-
return {
|
|
330
|
-
model: options.model,
|
|
331
|
-
instructions:
|
|
332
|
-
"You are a concise web search assistant. Use web search, answer the query, and preserve source citations from annotations.",
|
|
333
|
-
input: [
|
|
334
|
-
{
|
|
335
|
-
type: "message",
|
|
336
|
-
role: "user",
|
|
337
|
-
content: [{ type: "input_text", text: options.query }],
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
tools: [
|
|
341
|
-
{
|
|
342
|
-
type: "web_search",
|
|
343
|
-
external_web_access: options.externalWebAccess ?? true,
|
|
344
|
-
search_context_size: options.searchContextSize ?? "medium",
|
|
345
|
-
},
|
|
346
|
-
],
|
|
347
|
-
tool_choice: "required",
|
|
348
|
-
parallel_tool_calls: true,
|
|
349
|
-
store: false,
|
|
350
|
-
stream: true,
|
|
351
|
-
include: [],
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function* parseSse(body: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent> {
|
|
356
|
-
const reader = body.getReader();
|
|
357
|
-
const decoder = new TextDecoder();
|
|
358
|
-
let buffer = "";
|
|
359
|
-
|
|
360
|
-
while (true) {
|
|
361
|
-
const { done, value } = await reader.read();
|
|
362
|
-
if (done) break;
|
|
363
|
-
buffer += decoder.decode(value, { stream: true });
|
|
364
|
-
|
|
365
|
-
let separatorIndex = buffer.indexOf("\n\n");
|
|
366
|
-
while (separatorIndex !== -1) {
|
|
367
|
-
const frame = buffer.slice(0, separatorIndex);
|
|
368
|
-
buffer = buffer.slice(separatorIndex + 2);
|
|
369
|
-
const event = parseSseFrame(frame);
|
|
370
|
-
if (event) yield event;
|
|
371
|
-
separatorIndex = buffer.indexOf("\n\n");
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
buffer += decoder.decode();
|
|
376
|
-
const event = parseSseFrame(buffer);
|
|
377
|
-
if (event) yield event;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function parseSseFrame(frame: string): SseEvent | undefined {
|
|
381
|
-
const lines = frame.split(/\r?\n/);
|
|
382
|
-
let type = "";
|
|
383
|
-
const dataLines: string[] = [];
|
|
384
|
-
|
|
385
|
-
for (const line of lines) {
|
|
386
|
-
if (line.startsWith("event:")) {
|
|
387
|
-
type = line.slice("event:".length).trim();
|
|
388
|
-
} else if (line.startsWith("data:")) {
|
|
389
|
-
dataLines.push(line.slice("data:".length).trimStart());
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (dataLines.length === 0) return undefined;
|
|
394
|
-
const raw = dataLines.join("\n");
|
|
395
|
-
if (raw === "[DONE]") return undefined;
|
|
121
|
+
export function extractAccountIdFromToken(token: string): string | undefined {
|
|
122
|
+
const parts = token.split(".");
|
|
123
|
+
if (parts.length !== 3) return undefined;
|
|
396
124
|
|
|
397
125
|
try {
|
|
398
|
-
|
|
126
|
+
const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8")) as {
|
|
127
|
+
"https://api.openai.com/auth"?: { chatgpt_account_id?: unknown };
|
|
128
|
+
};
|
|
129
|
+
const accountId = payload["https://api.openai.com/auth"]?.chatgpt_account_id;
|
|
130
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
|
|
399
131
|
} catch {
|
|
400
|
-
return
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function collectOutputItem(
|
|
405
|
-
item: ResponseOutputItem | undefined,
|
|
406
|
-
searchCalls: Map<string, CodexSearchCall>,
|
|
407
|
-
messageTextParts: string[],
|
|
408
|
-
citations: Map<string, CodexCitation>,
|
|
409
|
-
): void {
|
|
410
|
-
if (!item) return;
|
|
411
|
-
|
|
412
|
-
if (item.type === "web_search_call") {
|
|
413
|
-
const key = item.id ?? `search-${searchCalls.size + 1}`;
|
|
414
|
-
const query = item.action?.query ?? item.action?.queries?.join(", ");
|
|
415
|
-
searchCalls.set(key, {
|
|
416
|
-
id: item.id,
|
|
417
|
-
status: item.status,
|
|
418
|
-
query,
|
|
419
|
-
url: item.action?.url,
|
|
420
|
-
actionType: item.action?.type,
|
|
421
|
-
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (item.type !== "message" || item.role !== "assistant") return;
|
|
426
|
-
|
|
427
|
-
for (const part of item.content ?? []) {
|
|
428
|
-
if (part.type !== "output_text") continue;
|
|
429
|
-
messageTextParts.push(part.text ?? "");
|
|
430
|
-
for (const annotation of part.annotations ?? []) {
|
|
431
|
-
if (annotation.type !== "url_citation" || !annotation.url) continue;
|
|
432
|
-
citations.set(annotation.url, {
|
|
433
|
-
title: annotation.title,
|
|
434
|
-
url: annotation.url,
|
|
435
|
-
startIndex: annotation.start_index,
|
|
436
|
-
endIndex: annotation.end_index,
|
|
437
|
-
});
|
|
438
|
-
}
|
|
132
|
+
return undefined;
|
|
439
133
|
}
|
|
440
134
|
}
|