@oh-my-pi/pi-coding-agent 13.10.1 → 13.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/package.json +7 -7
- package/src/commit/agentic/agent.ts +3 -1
- package/src/commit/agentic/index.ts +7 -1
- package/src/commit/analysis/conventional.ts +5 -1
- package/src/commit/analysis/summary.ts +5 -1
- package/src/commit/changelog/generate.ts +5 -1
- package/src/commit/changelog/index.ts +4 -0
- package/src/commit/map-reduce/index.ts +5 -0
- package/src/commit/map-reduce/map-phase.ts +17 -2
- package/src/commit/map-reduce/reduce-phase.ts +5 -1
- package/src/commit/model-selection.ts +38 -26
- package/src/commit/pipeline.ts +22 -11
- package/src/config/model-registry.ts +98 -17
- package/src/config/settings-schema.ts +31 -12
- package/src/config.ts +10 -3
- package/src/discovery/helpers.ts +10 -3
- package/src/exa/index.ts +1 -11
- package/src/exa/search.ts +1 -122
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/config.ts +1 -0
- package/src/lsp/defaults.json +3 -3
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +81 -0
- package/src/modes/components/settings-defs.ts +5 -0
- package/src/modes/components/todo-reminder.ts +8 -1
- package/src/modes/controllers/command-controller.ts +77 -3
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +2 -3
- package/src/modes/controllers/selector-controller.ts +18 -17
- package/src/modes/interactive-mode.ts +11 -7
- package/src/modes/theme/theme.ts +30 -27
- package/src/modes/types.ts +2 -1
- package/src/patch/hashline.ts +123 -22
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/code-search.md +45 -0
- package/src/prompts/tools/find.md +1 -0
- package/src/prompts/tools/grep.md +1 -0
- package/src/prompts/tools/hashline.md +26 -111
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/todo-write.md +11 -1
- package/src/sdk.ts +20 -16
- package/src/session/agent-session.ts +85 -7
- package/src/session/streaming-output.ts +17 -54
- package/src/slash-commands/builtin-registry.ts +10 -2
- package/src/task/executor.ts +10 -19
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +5 -10
- package/src/task/template.ts +4 -1
- package/src/task/types.ts +2 -0
- package/src/tools/ast-edit.ts +26 -7
- package/src/tools/ast-grep.ts +26 -9
- package/src/tools/exit-plan-mode.ts +6 -0
- package/src/tools/fetch.ts +37 -6
- package/src/tools/find.ts +13 -64
- package/src/tools/grep.ts +27 -10
- package/src/tools/output-meta.ts +10 -7
- package/src/tools/path-utils.ts +348 -0
- package/src/tools/read.ts +13 -26
- package/src/tools/todo-write.ts +27 -4
- package/src/utils/commit-message-generator.ts +27 -22
- package/src/utils/image-input.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/utils/title-generator.ts +36 -23
- package/src/utils/tool-choice.ts +28 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/youtube.ts +29 -0
- package/src/web/search/code-search.ts +385 -0
- package/src/web/search/index.ts +25 -280
- package/src/web/search/provider.ts +4 -1
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/types.ts +29 -0
- package/src/exa/company.ts +0 -26
- package/src/exa/linkedin.ts +0 -26
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { findCredential } from "./search/providers/utils";
|
|
3
|
+
|
|
4
|
+
const PARALLEL_API_URL = "https://api.parallel.ai";
|
|
5
|
+
const PARALLEL_SEARCH_URL = `${PARALLEL_API_URL}/v1beta/search`;
|
|
6
|
+
const PARALLEL_EXTRACT_URL = `${PARALLEL_API_URL}/v1beta/extract`;
|
|
7
|
+
const PARALLEL_BETA_HEADER = "search-extract-2025-10-10";
|
|
8
|
+
|
|
9
|
+
export interface ParallelUsageItem {
|
|
10
|
+
name?: string;
|
|
11
|
+
count?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ParallelSearchSource {
|
|
15
|
+
title: string;
|
|
16
|
+
url: string;
|
|
17
|
+
snippet?: string;
|
|
18
|
+
publishedDate?: string;
|
|
19
|
+
excerpts: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ParallelSearchResult {
|
|
23
|
+
requestId: string;
|
|
24
|
+
sources: ParallelSearchSource[];
|
|
25
|
+
warnings: string[];
|
|
26
|
+
usage: ParallelUsageItem[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ParallelExtractDocument {
|
|
30
|
+
url: string;
|
|
31
|
+
title?: string;
|
|
32
|
+
publishedDate?: string;
|
|
33
|
+
excerpts: string[];
|
|
34
|
+
fullContent?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ParallelExtractErrorEntry {
|
|
38
|
+
url: string;
|
|
39
|
+
errorType?: string;
|
|
40
|
+
httpStatusCode?: number;
|
|
41
|
+
content?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ParallelExtractResult {
|
|
45
|
+
requestId: string;
|
|
46
|
+
results: ParallelExtractDocument[];
|
|
47
|
+
errors: ParallelExtractErrorEntry[];
|
|
48
|
+
warnings: string[];
|
|
49
|
+
usage: ParallelUsageItem[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ParallelSearchOptions {
|
|
53
|
+
mode?: "fast" | "research";
|
|
54
|
+
maxCharsPerResult?: number;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ParallelExtractOptions {
|
|
59
|
+
objective?: string;
|
|
60
|
+
searchQueries?: string[];
|
|
61
|
+
excerpts?: boolean;
|
|
62
|
+
fullContent?: boolean;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class ParallelApiError extends Error {
|
|
67
|
+
readonly statusCode?: number;
|
|
68
|
+
|
|
69
|
+
constructor(message: string, statusCode?: number) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "ParallelApiError";
|
|
72
|
+
this.statusCode = statusCode;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function findParallelApiKey(): Promise<string | null> {
|
|
77
|
+
return findCredential(getEnvApiKey("parallel"), "parallel");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getParallelExtractContent(document: ParallelExtractDocument): string {
|
|
81
|
+
const excerptContent = document.excerpts
|
|
82
|
+
.filter(excerpt => excerpt.trim().length > 0)
|
|
83
|
+
.join("\n\n")
|
|
84
|
+
.trim();
|
|
85
|
+
if (excerptContent.length > 0) {
|
|
86
|
+
return excerptContent;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return document.fullContent?.trim() ?? "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isObject(value: unknown): value is object {
|
|
93
|
+
return typeof value === "object" && value !== null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getOwnValue(value: object, key: string): unknown {
|
|
97
|
+
return Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getString(value: object, key: string): string | undefined {
|
|
101
|
+
const candidate = getOwnValue(value, key);
|
|
102
|
+
return typeof candidate === "string" ? candidate : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getNumber(value: object, key: string): number | undefined {
|
|
106
|
+
const candidate = getOwnValue(value, key);
|
|
107
|
+
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getObjectArray(value: object, key: string): object[] {
|
|
111
|
+
const candidate = getOwnValue(value, key);
|
|
112
|
+
if (!Array.isArray(candidate)) return [];
|
|
113
|
+
return candidate.filter(isObject);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getStringArray(value: object, key: string): string[] {
|
|
117
|
+
const candidate = getOwnValue(value, key);
|
|
118
|
+
if (!Array.isArray(candidate)) return [];
|
|
119
|
+
return candidate.filter(item => typeof item === "string");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractParallelErrorMessage(payload: unknown): string | null {
|
|
123
|
+
if (!isObject(payload)) return null;
|
|
124
|
+
|
|
125
|
+
const directMessage = getString(payload, "message") ?? getString(payload, "detail") ?? getString(payload, "error");
|
|
126
|
+
if (directMessage && directMessage.trim().length > 0) {
|
|
127
|
+
return directMessage.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const errorObject = getOwnValue(payload, "error");
|
|
131
|
+
if (isObject(errorObject)) {
|
|
132
|
+
const nestedMessage = getString(errorObject, "message") ?? getString(errorObject, "detail");
|
|
133
|
+
if (nestedMessage && nestedMessage.trim().length > 0) {
|
|
134
|
+
return nestedMessage.trim();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createParallelApiError(statusCode: number, detail?: string): ParallelApiError {
|
|
142
|
+
return new ParallelApiError(
|
|
143
|
+
detail ? `Parallel API error (${statusCode}): ${detail}` : `Parallel API error (${statusCode})`,
|
|
144
|
+
statusCode,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseParallelErrorResponse(statusCode: number, responseText: string): ParallelApiError {
|
|
149
|
+
const trimmedResponseText = responseText.trim();
|
|
150
|
+
if (trimmedResponseText.length === 0) {
|
|
151
|
+
return createParallelApiError(statusCode);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const payload: unknown = JSON.parse(trimmedResponseText);
|
|
156
|
+
return createParallelApiError(statusCode, extractParallelErrorMessage(payload) ?? trimmedResponseText);
|
|
157
|
+
} catch {
|
|
158
|
+
return createParallelApiError(statusCode, trimmedResponseText);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getAuthHeaders(apiKey: string): {
|
|
163
|
+
Accept: string;
|
|
164
|
+
"Content-Type": string;
|
|
165
|
+
"x-api-key": string;
|
|
166
|
+
"parallel-beta": string;
|
|
167
|
+
} {
|
|
168
|
+
return {
|
|
169
|
+
Accept: "application/json",
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"x-api-key": apiKey,
|
|
172
|
+
"parallel-beta": PARALLEL_BETA_HEADER,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeSearchMode(mode: ParallelSearchOptions["mode"]): "fast" | "one-shot" {
|
|
177
|
+
return mode === "research" ? "one-shot" : "fast";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseUsageItems(payload: unknown): ParallelUsageItem[] {
|
|
181
|
+
if (!Array.isArray(payload)) return [];
|
|
182
|
+
|
|
183
|
+
const usageItems: ParallelUsageItem[] = [];
|
|
184
|
+
for (const item of payload) {
|
|
185
|
+
if (!isObject(item)) continue;
|
|
186
|
+
usageItems.push({
|
|
187
|
+
name: getString(item, "name"),
|
|
188
|
+
count: getNumber(item, "count"),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return usageItems;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseWarnings(payload: unknown): string[] {
|
|
195
|
+
if (!Array.isArray(payload)) return [];
|
|
196
|
+
|
|
197
|
+
const warnings: string[] = [];
|
|
198
|
+
for (const item of payload) {
|
|
199
|
+
if (typeof item === "string") {
|
|
200
|
+
warnings.push(item);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!isObject(item)) continue;
|
|
204
|
+
const message = getString(item, "message") ?? getString(item, "warning");
|
|
205
|
+
if (message) {
|
|
206
|
+
warnings.push(message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return warnings;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseSearchPayload(payload: unknown): ParallelSearchResult {
|
|
213
|
+
if (!isObject(payload)) {
|
|
214
|
+
throw new ParallelApiError("Parallel search returned an invalid response payload.");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const requestId = getString(payload, "search_id") ?? "";
|
|
218
|
+
const rawResults = getObjectArray(payload, "results");
|
|
219
|
+
const sources: ParallelSearchSource[] = [];
|
|
220
|
+
|
|
221
|
+
for (const item of rawResults) {
|
|
222
|
+
const url = getString(item, "url");
|
|
223
|
+
if (!url) continue;
|
|
224
|
+
|
|
225
|
+
const excerpts = getStringArray(item, "excerpts");
|
|
226
|
+
const snippet = excerpts.length > 0 ? excerpts.join("\n\n") : undefined;
|
|
227
|
+
sources.push({
|
|
228
|
+
title: getString(item, "title") ?? url,
|
|
229
|
+
url,
|
|
230
|
+
snippet,
|
|
231
|
+
publishedDate: getString(item, "publish_date"),
|
|
232
|
+
excerpts,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
requestId,
|
|
238
|
+
sources,
|
|
239
|
+
warnings: parseWarnings(getOwnValue(payload, "warnings")),
|
|
240
|
+
usage: parseUsageItems(getOwnValue(payload, "usage")),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseExtractPayload(payload: unknown): ParallelExtractResult {
|
|
245
|
+
if (!isObject(payload)) {
|
|
246
|
+
throw new ParallelApiError("Parallel extract returned an invalid response payload.");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const requestId = getString(payload, "extract_id") ?? "";
|
|
250
|
+
const resultItems: ParallelExtractDocument[] = [];
|
|
251
|
+
for (const item of getObjectArray(payload, "results")) {
|
|
252
|
+
const url = getString(item, "url");
|
|
253
|
+
if (!url) continue;
|
|
254
|
+
resultItems.push({
|
|
255
|
+
url,
|
|
256
|
+
title: getString(item, "title"),
|
|
257
|
+
publishedDate: getString(item, "publish_date"),
|
|
258
|
+
excerpts: getStringArray(item, "excerpts"),
|
|
259
|
+
fullContent: getString(item, "full_content"),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const errors: ParallelExtractErrorEntry[] = [];
|
|
264
|
+
for (const item of getObjectArray(payload, "errors")) {
|
|
265
|
+
const url = getString(item, "url");
|
|
266
|
+
if (!url) continue;
|
|
267
|
+
errors.push({
|
|
268
|
+
url,
|
|
269
|
+
errorType: getString(item, "error_type"),
|
|
270
|
+
httpStatusCode: getNumber(item, "http_status_code"),
|
|
271
|
+
content: getString(item, "content"),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
requestId,
|
|
277
|
+
results: resultItems,
|
|
278
|
+
errors,
|
|
279
|
+
warnings: parseWarnings(getOwnValue(payload, "warnings")),
|
|
280
|
+
usage: parseUsageItems(getOwnValue(payload, "usage")),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function searchWithParallel(
|
|
285
|
+
objective: string,
|
|
286
|
+
queries: string[],
|
|
287
|
+
options: ParallelSearchOptions = {},
|
|
288
|
+
): Promise<ParallelSearchResult> {
|
|
289
|
+
const apiKey = await findParallelApiKey();
|
|
290
|
+
if (!apiKey) {
|
|
291
|
+
throw new ParallelApiError(
|
|
292
|
+
"Parallel credentials not found. Set PARALLEL_API_KEY or login with 'omp /login parallel'.",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const response = await fetch(PARALLEL_SEARCH_URL, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: getAuthHeaders(apiKey),
|
|
299
|
+
body: JSON.stringify({
|
|
300
|
+
objective,
|
|
301
|
+
search_queries: queries,
|
|
302
|
+
mode: normalizeSearchMode(options.mode),
|
|
303
|
+
excerpts: {
|
|
304
|
+
max_chars_per_result: options.maxCharsPerResult ?? 10_000,
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
signal: options.signal,
|
|
308
|
+
});
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
throw parseParallelErrorResponse(response.status, await response.text());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const payload: unknown = await response.json();
|
|
314
|
+
return parseSearchPayload(payload);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function extractWithParallel(
|
|
318
|
+
urls: string[],
|
|
319
|
+
options: ParallelExtractOptions = {},
|
|
320
|
+
): Promise<ParallelExtractResult> {
|
|
321
|
+
const apiKey = await findParallelApiKey();
|
|
322
|
+
if (!apiKey) {
|
|
323
|
+
throw new ParallelApiError(
|
|
324
|
+
"Parallel credentials not found. Set PARALLEL_API_KEY or login with 'omp /login parallel'.",
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const response = await fetch(PARALLEL_EXTRACT_URL, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: getAuthHeaders(apiKey),
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
urls,
|
|
333
|
+
objective: options.objective,
|
|
334
|
+
search_queries: options.searchQueries,
|
|
335
|
+
excerpts: options.excerpts ?? true,
|
|
336
|
+
full_content: options.fullContent ?? false,
|
|
337
|
+
}),
|
|
338
|
+
signal: options.signal,
|
|
339
|
+
});
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
throw parseParallelErrorResponse(response.status, await response.text());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const payload: unknown = await response.json();
|
|
345
|
+
return parseExtractPayload(payload);
|
|
346
|
+
}
|
|
@@ -2,8 +2,10 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { ptree, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { settings } from "../../config/settings";
|
|
5
6
|
import { throwIfAborted } from "../../tools/tool-errors";
|
|
6
7
|
import { ensureTool } from "../../utils/tools-manager";
|
|
8
|
+
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../parallel";
|
|
7
9
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
8
10
|
import { buildResult, formatMediaDuration, formatNumber } from "./types";
|
|
9
11
|
|
|
@@ -109,6 +111,33 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
109
111
|
const notes: string[] = [];
|
|
110
112
|
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
111
113
|
|
|
114
|
+
// Prefer Parallel extract when credentials are available
|
|
115
|
+
if (settings.get("providers.parallelFetch") && (await findParallelApiKey())) {
|
|
116
|
+
try {
|
|
117
|
+
const parallelResult = await extractWithParallel([videoUrl], {
|
|
118
|
+
objective: "Extract the main content of this YouTube video page",
|
|
119
|
+
excerpts: true,
|
|
120
|
+
fullContent: false,
|
|
121
|
+
signal,
|
|
122
|
+
});
|
|
123
|
+
const firstDocument = parallelResult.results[0];
|
|
124
|
+
if (firstDocument) {
|
|
125
|
+
const content = getParallelExtractContent(firstDocument);
|
|
126
|
+
if (content.trim().length > 100) {
|
|
127
|
+
return buildResult(content, {
|
|
128
|
+
url,
|
|
129
|
+
finalUrl: videoUrl,
|
|
130
|
+
method: "parallel",
|
|
131
|
+
fetchedAt,
|
|
132
|
+
notes: ["Used Parallel extract for YouTube"],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
throwIfAborted(signal);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
// Ensure yt-dlp is available (auto-download if missing)
|
|
113
142
|
const ytdlp = await ensureTool("yt-dlp", { signal, silent: true });
|
|
114
143
|
if (!ytdlp) {
|