@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
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 +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +24 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +5 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +321 -184
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
package/src/tools/write.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
AgentToolContext,
|
|
10
10
|
AgentToolResult,
|
|
11
11
|
AgentToolUpdateCallback,
|
|
12
|
-
|
|
12
|
+
ToolApprovalDecision,
|
|
13
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
14
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
15
15
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -500,7 +500,7 @@ function parseSqliteWriteTarget(subPath: string, queryString: string): { table:
|
|
|
500
500
|
*/
|
|
501
501
|
export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails> {
|
|
502
502
|
readonly name = "write";
|
|
503
|
-
readonly approval = (args: unknown):
|
|
503
|
+
readonly approval = (args: unknown): ToolApprovalDecision => {
|
|
504
504
|
const rawPath = (args as Partial<WriteParams>).path;
|
|
505
505
|
if (typeof rawPath !== "string") return "write";
|
|
506
506
|
// Unwrap a hashline `[path#TAG]` wrapper first (parity with execute) so a
|
|
@@ -532,7 +532,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
532
532
|
}
|
|
533
533
|
if (!isRecord(parsed)) return "exec";
|
|
534
534
|
try {
|
|
535
|
-
|
|
535
|
+
// The tier is the mounted tool's own (argument-dependent) approval; the
|
|
536
|
+
// policyKey makes the outer gate consult `tools.approval.<device>` for
|
|
537
|
+
// this dispatch before falling back to `tools.approval.write`, so users
|
|
538
|
+
// can scope allow/deny/prompt to a single device (issue #7923).
|
|
539
|
+
return { tier: resolveToolTier(inst, parsed), policyKey: xdevTarget.name! };
|
|
536
540
|
} catch {
|
|
537
541
|
return "exec";
|
|
538
542
|
}
|
package/src/utils/local-date.ts
CHANGED
|
@@ -5,3 +5,16 @@ export function formatLocalCalendarDate(date: Date = new Date()): string {
|
|
|
5
5
|
const day = String(date.getDate()).padStart(2, "0");
|
|
6
6
|
return `${year}-${month}-${day}`;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** Format a local date and minute with a compact numeric UTC offset. */
|
|
10
|
+
export function formatLocalDateTimeWithOffset(date: Date): string {
|
|
11
|
+
const offsetMinutes = date.getTimezoneOffset();
|
|
12
|
+
const offsetSign = offsetMinutes <= 0 ? "+" : "-";
|
|
13
|
+
const absoluteOffset = Math.abs(offsetMinutes);
|
|
14
|
+
const offsetHours = Math.floor(absoluteOffset / 60);
|
|
15
|
+
const offsetRemainderMinutes = absoluteOffset % 60;
|
|
16
|
+
const pad2 = (value: number): string => String(value).padStart(2, "0");
|
|
17
|
+
return `${formatLocalCalendarDate(date)} ${pad2(date.getHours())}:${pad2(date.getMinutes())} ${offsetSign}${pad2(
|
|
18
|
+
offsetHours,
|
|
19
|
+
)}:${pad2(offsetRemainderMinutes)}`;
|
|
20
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { $which,
|
|
4
|
+
import { $which, getToolsDir, logger, ptree, TempDir, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { extractArchive } from "./zip";
|
|
6
6
|
|
|
7
7
|
const TOOLS_DIR = getToolsDir();
|
|
@@ -174,7 +174,7 @@ async function getLatestVersion(repo: string, signal?: AbortSignal): Promise<str
|
|
|
174
174
|
let response: Response;
|
|
175
175
|
try {
|
|
176
176
|
response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
|
|
177
|
-
headers: { "User-Agent":
|
|
177
|
+
headers: { "User-Agent": USER_AGENT },
|
|
178
178
|
signal: ptree.combineSignals(signal, TOOL_METADATA_TIMEOUT_MS),
|
|
179
179
|
});
|
|
180
180
|
} catch (err) {
|
package/src/vibe/runtime.ts
CHANGED
|
@@ -18,7 +18,7 @@ import * as os from "node:os";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { logger, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import type { AsyncJob, AsyncJobManager } from "../async/job-manager";
|
|
21
|
-
import {
|
|
21
|
+
import { resolveAgentModelSelection } from "../config/model-resolver";
|
|
22
22
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
23
23
|
import { registerArtifactsDir } from "../internal-urls/registry-helpers";
|
|
24
24
|
import { MCPManager } from "../mcp/manager";
|
|
@@ -43,7 +43,7 @@ export type VibeCli = "fast" | "good";
|
|
|
43
43
|
* CLI flavor → bundled agent type. This IS the model-tier mapping: `sonic`
|
|
44
44
|
* carries `model: "@smol"` (the configured fast/low-latency role) and `task`
|
|
45
45
|
* carries `model: "@task"` (inherits the session's strong model).
|
|
46
|
-
* Resolution goes through {@link
|
|
46
|
+
* Resolution goes through {@link resolveAgentModelSelection} exactly like a
|
|
47
47
|
* `task` spawn, so `task.agentModelOverrides` and model-role settings apply.
|
|
48
48
|
*/
|
|
49
49
|
export const VIBE_CLI_AGENT: Record<VibeCli, string> = {
|
|
@@ -144,6 +144,8 @@ interface VibeRestoreCandidate {
|
|
|
144
144
|
interface ResolvedVibeWorker {
|
|
145
145
|
agent: AgentDefinition;
|
|
146
146
|
modelOverride?: string | string[];
|
|
147
|
+
/** Pre-expansion role alias behind {@link modelOverride}, when the worker agent named one. */
|
|
148
|
+
modelRole?: string;
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
interface VibeTurn {
|
|
@@ -165,6 +167,8 @@ interface VibeRecord {
|
|
|
165
167
|
childSessionFile?: string;
|
|
166
168
|
agent: AgentDefinition;
|
|
167
169
|
modelOverride?: string | string[];
|
|
170
|
+
/** Pre-expansion role alias behind {@link modelOverride}, when the worker agent named one. */
|
|
171
|
+
modelRole?: string;
|
|
168
172
|
state: VibeSessionState;
|
|
169
173
|
createdAt: number;
|
|
170
174
|
lastActivityAt: number;
|
|
@@ -490,16 +494,17 @@ export class VibeSessionRegistry {
|
|
|
490
494
|
throw new ToolError(`Bundled agent "${agentName}" for vibe cli "${cli}" is unavailable.`);
|
|
491
495
|
}
|
|
492
496
|
const agentModelOverrides = session.settings.get("task.agentModelOverrides");
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
};
|
|
497
|
+
// Same contract as the task spawn path: the expansion discards the role
|
|
498
|
+
// alias (`@task`, `@smol`), so patterns and role identity come from one
|
|
499
|
+
// call — the child's inherited retry-fallback chain is keyed off the role.
|
|
500
|
+
const { patterns, role } = resolveAgentModelSelection({
|
|
501
|
+
settingsOverride: agentModelOverrides[agentName],
|
|
502
|
+
agentModel: agent.model,
|
|
503
|
+
settings: session.settings,
|
|
504
|
+
activeModelPattern: session.getActiveModelString?.(),
|
|
505
|
+
fallbackModelPattern: session.getModelString?.(),
|
|
506
|
+
});
|
|
507
|
+
return { agent, modelOverride: patterns, modelRole: role };
|
|
503
508
|
}
|
|
504
509
|
|
|
505
510
|
async #appendLifecycleEvent(
|
|
@@ -890,7 +895,7 @@ export class VibeSessionRegistry {
|
|
|
890
895
|
existing.sessionFile === childSessionFile &&
|
|
891
896
|
(existing.status === "idle" || existing.status === "parked");
|
|
892
897
|
const blockedByCollision = Boolean(existing && !existingIsResumable);
|
|
893
|
-
const { agent, modelOverride } = this.#resolveWorker(session, spawn.cli);
|
|
898
|
+
const { agent, modelOverride, modelRole } = this.#resolveWorker(session, spawn.cli);
|
|
894
899
|
if (!existing) {
|
|
895
900
|
AgentRegistry.global().register({
|
|
896
901
|
id: spawn.id,
|
|
@@ -911,6 +916,7 @@ export class VibeSessionRegistry {
|
|
|
911
916
|
childSessionFile,
|
|
912
917
|
agent,
|
|
913
918
|
modelOverride,
|
|
919
|
+
modelRole,
|
|
914
920
|
state: "idle",
|
|
915
921
|
createdAt: spawn.createdAt,
|
|
916
922
|
lastActivityAt: candidate.lastActivityAt,
|
|
@@ -945,7 +951,7 @@ export class VibeSessionRegistry {
|
|
|
945
951
|
throw new ToolError("Vibe mode has exited; enter Vibe mode again before spawning a worker.");
|
|
946
952
|
}
|
|
947
953
|
const manager = this.#manager(session);
|
|
948
|
-
const { agent, modelOverride } = this.#resolveWorker(session, args.cli);
|
|
954
|
+
const { agent, modelOverride, modelRole } = this.#resolveWorker(session, args.cli);
|
|
949
955
|
if (!session.agentOutputManager) {
|
|
950
956
|
session.agentOutputManager = new AgentOutputManager(session.getArtifactsDir ?? (() => null));
|
|
951
957
|
}
|
|
@@ -969,6 +975,7 @@ export class VibeSessionRegistry {
|
|
|
969
975
|
childSessionFile,
|
|
970
976
|
agent,
|
|
971
977
|
modelOverride,
|
|
978
|
+
modelRole,
|
|
972
979
|
state: "starting",
|
|
973
980
|
createdAt,
|
|
974
981
|
lastActivityAt: createdAt,
|
|
@@ -1418,6 +1425,7 @@ export class VibeSessionRegistry {
|
|
|
1418
1425
|
taskDepth: session.taskDepth ?? 0,
|
|
1419
1426
|
detached: true,
|
|
1420
1427
|
modelOverride: record.modelOverride,
|
|
1428
|
+
modelRole: record.modelRole,
|
|
1421
1429
|
parentActiveModelPattern: session.getActiveModelString?.(),
|
|
1422
1430
|
thinkingLevel: record.agent.thinkingLevel,
|
|
1423
1431
|
sessionFile,
|
package/src/web/kagi.ts
CHANGED
|
@@ -98,7 +98,7 @@ export class KagiApiError extends Error {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function extractKagiErrorMessage(payload: unknown): string | null {
|
|
101
|
-
if (!payload || typeof payload !== "object") return null;
|
|
101
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
102
102
|
const record = payload as Record<string, unknown>;
|
|
103
103
|
|
|
104
104
|
for (const value of [record.message, record.detail]) {
|
|
@@ -107,17 +107,20 @@ function extractKagiErrorMessage(payload: unknown): string | null {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const entry of
|
|
110
|
+
for (const errors of [record.error, record.errors]) {
|
|
111
|
+
if (typeof errors === "string" && errors.trim().length > 0) {
|
|
112
|
+
return errors.trim();
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(errors)) continue;
|
|
115
|
+
for (const entry of errors) {
|
|
116
116
|
if (!entry || typeof entry !== "object") continue;
|
|
117
117
|
const e = entry as Record<string, unknown>;
|
|
118
|
-
for (const value of [e.message, e.msg]) {
|
|
119
|
-
if (
|
|
120
|
-
|
|
118
|
+
for (const value of [e.message, e.msg, e.code]) {
|
|
119
|
+
if (
|
|
120
|
+
(typeof value === "string" && value.trim().length > 0) ||
|
|
121
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
122
|
+
) {
|
|
123
|
+
return String(value).trim();
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
}
|
|
@@ -147,6 +150,34 @@ function parseKagiErrorResponse(statusCode: number, responseText: string): KagiA
|
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
function parseKagiSuccessResponse(statusCode: number, responseText: string): KagiSearchResponse {
|
|
154
|
+
let payload: unknown;
|
|
155
|
+
try {
|
|
156
|
+
payload = JSON.parse(responseText);
|
|
157
|
+
} catch {
|
|
158
|
+
throw new KagiApiError("Kagi API returned an invalid response: invalid JSON", statusCode);
|
|
159
|
+
}
|
|
160
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
161
|
+
throw new KagiApiError("Kagi API returned an invalid response: expected an object envelope", statusCode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const record = payload as Record<string, unknown>;
|
|
165
|
+
const errorMessage = extractKagiErrorMessage(payload);
|
|
166
|
+
if (errorMessage && (record.error !== undefined || record.errors !== undefined)) {
|
|
167
|
+
const errors = Array.isArray(record.error) ? record.error : Array.isArray(record.errors) ? record.errors : [];
|
|
168
|
+
const first = errors[0];
|
|
169
|
+
const code =
|
|
170
|
+
first && typeof first === "object" && typeof (first as Record<string, unknown>).code === "number"
|
|
171
|
+
? ((first as Record<string, unknown>).code as number)
|
|
172
|
+
: statusCode;
|
|
173
|
+
throw createKagiApiError(code, errorMessage);
|
|
174
|
+
}
|
|
175
|
+
if (record.data !== undefined && (!record.data || typeof record.data !== "object" || Array.isArray(record.data))) {
|
|
176
|
+
throw new KagiApiError("Kagi API returned an invalid response: expected data to be an object", statusCode);
|
|
177
|
+
}
|
|
178
|
+
return payload as KagiSearchResponse;
|
|
179
|
+
}
|
|
180
|
+
|
|
150
181
|
// ---------------------------------------------------------------------------
|
|
151
182
|
// Public API
|
|
152
183
|
// ---------------------------------------------------------------------------
|
|
@@ -216,23 +247,40 @@ function buildRequestBody(query: string, options: KagiSearchOptions): KagiSearch
|
|
|
216
247
|
return req;
|
|
217
248
|
}
|
|
218
249
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
250
|
+
function firstNonEmptyString(...values: unknown[]): string | undefined {
|
|
251
|
+
for (const value of values) {
|
|
252
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Push every valid item in a result bucket as a source, with an optional title tag. */
|
|
258
|
+
function collectSources(sources: KagiSearchSource[], items: unknown, tag?: string): void {
|
|
259
|
+
if (!Array.isArray(items)) return;
|
|
260
|
+
for (const value of items) {
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
262
|
+
const item = value as Record<string, unknown>;
|
|
263
|
+
const url = firstNonEmptyString(item.url, item.href, item.link);
|
|
264
|
+
if (!url) continue;
|
|
265
|
+
const title = firstNonEmptyString(item.title, item.name) ?? url;
|
|
223
266
|
sources.push({
|
|
224
|
-
title: tag ? `${tag} ${
|
|
225
|
-
url
|
|
226
|
-
snippet: item.snippet,
|
|
227
|
-
publishedDate: item.time,
|
|
267
|
+
title: tag ? `${tag} ${title}` : title,
|
|
268
|
+
url,
|
|
269
|
+
snippet: firstNonEmptyString(item.snippet, item.description, item.summary),
|
|
270
|
+
publishedDate: firstNonEmptyString(item.time),
|
|
228
271
|
});
|
|
229
272
|
}
|
|
230
273
|
}
|
|
231
274
|
|
|
232
275
|
/** Pull a related/adjacent question from an item's props or fall back to title. */
|
|
233
|
-
function questionOf(
|
|
234
|
-
|
|
235
|
-
|
|
276
|
+
function questionOf(value: unknown): string | undefined {
|
|
277
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
278
|
+
const item = value as Record<string, unknown>;
|
|
279
|
+
const props =
|
|
280
|
+
item.props && typeof item.props === "object" && !Array.isArray(item.props)
|
|
281
|
+
? (item.props as Record<string, unknown>)
|
|
282
|
+
: undefined;
|
|
283
|
+
return firstNonEmptyString(props?.question, props?.query, item.title);
|
|
236
284
|
}
|
|
237
285
|
|
|
238
286
|
export async function searchWithKagi(
|
|
@@ -269,11 +317,7 @@ export async function searchWithKagi(
|
|
|
269
317
|
},
|
|
270
318
|
);
|
|
271
319
|
|
|
272
|
-
const payload = (await response.
|
|
273
|
-
if (payload.error && payload.error.length > 0) {
|
|
274
|
-
const first = payload.error[0];
|
|
275
|
-
throw createKagiApiError(first.code ?? response.status, extractKagiErrorMessage(payload) ?? first.message);
|
|
276
|
-
}
|
|
320
|
+
const payload = parseKagiSuccessResponse(response.status, await response.text());
|
|
277
321
|
|
|
278
322
|
const data = payload.data;
|
|
279
323
|
const sources: KagiSearchSource[] = [];
|
|
@@ -284,17 +328,30 @@ export async function searchWithKagi(
|
|
|
284
328
|
collectSources(sources, data?.news, "[News]");
|
|
285
329
|
collectSources(sources, data?.infobox, "[Info]");
|
|
286
330
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
331
|
+
const adjacentQuestions: unknown = data?.adjacent_question;
|
|
332
|
+
if (Array.isArray(adjacentQuestions)) {
|
|
333
|
+
for (const item of adjacentQuestions) {
|
|
334
|
+
const question = questionOf(item);
|
|
335
|
+
if (question) relatedQuestions.push(question);
|
|
336
|
+
}
|
|
290
337
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
338
|
+
const relatedSearches: unknown = data?.related_search;
|
|
339
|
+
if (Array.isArray(relatedSearches)) {
|
|
340
|
+
for (const item of relatedSearches) {
|
|
341
|
+
const question = questionOf(item);
|
|
342
|
+
if (question) relatedQuestions.push(question);
|
|
343
|
+
}
|
|
294
344
|
}
|
|
295
345
|
|
|
296
|
-
const
|
|
297
|
-
const
|
|
346
|
+
const directAnswers: unknown = data?.direct_answer;
|
|
347
|
+
const directAnswer = Array.isArray(directAnswers) ? directAnswers[0] : undefined;
|
|
348
|
+
const answer =
|
|
349
|
+
directAnswer && typeof directAnswer === "object" && !Array.isArray(directAnswer)
|
|
350
|
+
? firstNonEmptyString(
|
|
351
|
+
(directAnswer as Record<string, unknown>).snippet,
|
|
352
|
+
(directAnswer as Record<string, unknown>).title,
|
|
353
|
+
)
|
|
354
|
+
: undefined;
|
|
298
355
|
|
|
299
356
|
return {
|
|
300
357
|
requestId: payload.meta?.trace ?? payload.meta?.id ?? "",
|
package/src/web/parallel.ts
CHANGED
|
@@ -146,6 +146,15 @@ export function parseParallelErrorResponse(statusCode: number, responseText: str
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export async function parseParallelJsonResponse(response: Response, operation: "search" | "extract"): Promise<unknown> {
|
|
150
|
+
try {
|
|
151
|
+
return await response.json();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
154
|
+
throw new ParallelApiError(`Parallel ${operation} returned invalid JSON: ${detail}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
function getAuthHeaders(apiKey: string): {
|
|
150
159
|
Accept: string;
|
|
151
160
|
"Content-Type": string;
|
|
@@ -316,7 +325,7 @@ export async function searchWithParallel(
|
|
|
316
325
|
throw parseParallelErrorResponse(response.status, await response.text());
|
|
317
326
|
}
|
|
318
327
|
|
|
319
|
-
const payload
|
|
328
|
+
const payload = await parseParallelJsonResponse(response, "search");
|
|
320
329
|
return parseParallelSearchPayload(payload);
|
|
321
330
|
}
|
|
322
331
|
|
|
@@ -349,6 +358,6 @@ export async function extractWithParallel(
|
|
|
349
358
|
throw parseParallelErrorResponse(response.status, await response.text());
|
|
350
359
|
}
|
|
351
360
|
|
|
352
|
-
const payload
|
|
361
|
+
const payload = await parseParallelJsonResponse(response, "extract");
|
|
353
362
|
return parseExtractPayload(payload);
|
|
354
363
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, formatNumber, loadPage, looksLikeHtml } from "./types";
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ export const handleCratesIo: SpecialHandler = async (
|
|
|
26
26
|
const result = await loadPage(apiUrl, {
|
|
27
27
|
timeout,
|
|
28
28
|
signal,
|
|
29
|
-
headers: { "User-Agent":
|
|
29
|
+
headers: { "User-Agent": USER_AGENT },
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
if (!result.ok) return null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* API docs: https://www.discogs.com/developers
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
10
10
|
import { buildResult, loadPage } from "./types";
|
|
11
11
|
|
|
@@ -277,7 +277,7 @@ export const handleDiscogs: SpecialHandler = async (
|
|
|
277
277
|
signal,
|
|
278
278
|
headers: {
|
|
279
279
|
Accept: "application/json",
|
|
280
|
-
"User-Agent":
|
|
280
|
+
"User-Agent": USER_AGENT,
|
|
281
281
|
},
|
|
282
282
|
});
|
|
283
283
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { gunzipSync } from "node:zlib";
|
|
4
|
-
import { getDocsRsCacheDir, isEnoent, logger, ptree, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { getDocsRsCacheDir, isEnoent, logger, ptree, tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
6
6
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
7
7
|
import { buildResult, MAX_BYTES } from "./types";
|
|
@@ -388,7 +388,7 @@ export const handleDocsRs: SpecialHandler = async (
|
|
|
388
388
|
const requestSignal = ptree.combineSignals(signal, timeout * 1000);
|
|
389
389
|
const response = await fetch(jsonUrl, {
|
|
390
390
|
signal: requestSignal,
|
|
391
|
-
headers: { "User-Agent":
|
|
391
|
+
headers: { "User-Agent": USER_AGENT, Accept: "application/gzip" },
|
|
392
392
|
redirect: "follow",
|
|
393
393
|
});
|
|
394
394
|
if (!response.ok) return null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, ptree } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $env, ptree, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, formatMediaDuration, loadPage } from "./types";
|
|
4
4
|
|
|
@@ -121,7 +121,7 @@ export async function fetchGitHubApi(
|
|
|
121
121
|
|
|
122
122
|
const headers: Record<string, string> = {
|
|
123
123
|
Accept: "application/vnd.github.v3+json",
|
|
124
|
-
"User-Agent":
|
|
124
|
+
"User-Agent": USER_AGENT,
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
// Use GITHUB_TOKEN if available
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MusicBrainz URL handler for artists, releases, and recordings
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
7
7
|
import { buildResult, formatMediaDuration, loadPage } from "./types";
|
|
8
8
|
|
|
@@ -64,7 +64,6 @@ interface MusicBrainzRelease {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const MUSICBRAINZ_HOSTS = new Set(["musicbrainz.org", "www.musicbrainz.org"]);
|
|
67
|
-
const USER_AGENT = "omp-web-fetch/1.0 (https://github.com/anthropics)";
|
|
68
67
|
const MAX_TRACKS = 50;
|
|
69
68
|
|
|
70
69
|
function parseEntity(url: URL): { entity: MusicBrainzEntity; mbid: string } | null {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PubMed handler for web-fetch
|
|
3
3
|
*/
|
|
4
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { buildResult, loadPage, type RenderResult, type SpecialHandler } from "./types";
|
|
6
6
|
|
|
7
7
|
const NCBI_HEADERS = {
|
|
8
8
|
Accept: "application/json, text/plain;q=0.9, */*;q=0.8",
|
|
9
|
-
"User-Agent":
|
|
9
|
+
"User-Agent": USER_AGENT,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, loadPage } from "./types";
|
|
4
4
|
|
|
@@ -178,7 +178,7 @@ export const handleSecEdgar: SpecialHandler = async (
|
|
|
178
178
|
timeout,
|
|
179
179
|
signal,
|
|
180
180
|
headers: {
|
|
181
|
-
"User-Agent":
|
|
181
|
+
"User-Agent": USER_AGENT,
|
|
182
182
|
Accept: "application/json",
|
|
183
183
|
},
|
|
184
184
|
});
|