@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
package/src/tools/hub/launch.ts
CHANGED
|
@@ -74,6 +74,9 @@ const KEY_INPUT: Record<string, string> = {
|
|
|
74
74
|
LEFT: "\u001b[D",
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
/** Terminal daemon lifecycle states — the process is no longer running. */
|
|
78
|
+
const TERMINAL_STATES: Partial<Record<DaemonState, true>> = { exited: true, failed: true };
|
|
79
|
+
|
|
77
80
|
/** Structured launch state retained for compact TUI rendering. */
|
|
78
81
|
export interface LaunchToolDetails {
|
|
79
82
|
op: LaunchParams["op"];
|
|
@@ -229,6 +232,8 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string {
|
|
|
229
232
|
lines.push(
|
|
230
233
|
`NOT ready — readiness timed out after ${params.ready?.timeout ?? 30}s${cause}. The process is still running (state: ${daemon.state}); follow its logs or stop it.`,
|
|
231
234
|
);
|
|
235
|
+
} else if (params.ready && daemon.readyAt === undefined && TERMINAL_STATES[daemon.state]) {
|
|
236
|
+
lines.push("Process exited before readiness was observed.");
|
|
232
237
|
}
|
|
233
238
|
return lines.join("\n");
|
|
234
239
|
}
|
|
@@ -437,6 +442,8 @@ export function launchRenderResult(
|
|
|
437
442
|
: "Readiness timed out; the process is still running.",
|
|
438
443
|
),
|
|
439
444
|
);
|
|
445
|
+
} else if (params.ready && daemon && daemon.readyAt === undefined && TERMINAL_STATES[daemon.state]) {
|
|
446
|
+
body.push(theme.fg("warning", "Process exited before readiness was observed."));
|
|
440
447
|
}
|
|
441
448
|
break;
|
|
442
449
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { LspTool } from "../lsp";
|
|
|
20
20
|
import type { MCPManager } from "../mcp";
|
|
21
21
|
import type { MnemopiSessionState } from "../mnemopi/state";
|
|
22
22
|
import type { PlanModeState } from "../plan-mode/state";
|
|
23
|
+
import type { AgentLifecycleManager } from "../registry/agent-lifecycle";
|
|
23
24
|
import type { AgentRegistry } from "../registry/agent-registry";
|
|
24
25
|
import type { ArtifactManager } from "../session/artifacts";
|
|
25
26
|
import type { ClientBridge } from "../session/client-bridge";
|
|
@@ -238,6 +239,8 @@ export interface ToolSession {
|
|
|
238
239
|
xdevRegistry?: XdevRegistry;
|
|
239
240
|
/** Agent registry for IRC routing across live sessions. */
|
|
240
241
|
agentRegistry?: AgentRegistry;
|
|
242
|
+
/** Idle→parked→revive lifecycle owner; lets the hub kill a non-job-backed agent registration. Default: AgentLifecycleManager.global(). */
|
|
243
|
+
agentLifecycle?: () => AgentLifecycleManager;
|
|
241
244
|
/** Get artifacts directory for artifact:// URLs */
|
|
242
245
|
getArtifactsDir?: () => string | null;
|
|
243
246
|
/** Get the ArtifactManager backing this session (shared across parent + subagents). */
|
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
* `xd://report_issue`, and the system prompt tells the model to write
|
|
6
6
|
* `<tool>: <concise description>` there when auto-QA is enabled.
|
|
7
7
|
*
|
|
8
|
-
* Enabled by default
|
|
9
|
-
*
|
|
8
|
+
* Enabled by default (`dev.autoqa` defaults to true); `PI_AUTO_QA=0` or an
|
|
9
|
+
* explicit `dev.autoqa: false` short-circuits injection entirely. When the
|
|
10
|
+
* user is only enabled by default (never configured `dev.autoqa` themselves),
|
|
11
|
+
* a persisted `dev.autoqaConsent: "denied"` also disables injection so a "No"
|
|
12
|
+
* in the consent dialog fully turns the feature off.
|
|
10
13
|
* Records grievances to a local SQLite database; never throws from the device
|
|
11
14
|
* dispatch path.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Nothing is written until consent resolves. If the user has never been asked
|
|
17
|
+
* (`dev.autoqaConsent === "unset"`) the process-global consent handler —
|
|
18
|
+
* wired by `InteractiveMode` to a Yes/No popup — is invoked exactly once and
|
|
19
|
+
* the decision is persisted; a denial (or dismissal) drops the pending report
|
|
20
|
+
* without touching the database. Subsequent calls (including from subagents)
|
|
21
|
+
* read the cached decision without prompting. `PI_AUTO_QA_PUSH=1` bypasses
|
|
22
|
+
* the dialog for headless environments.
|
|
18
23
|
*
|
|
19
24
|
* When the user grants consent, push is automatically active against the
|
|
20
25
|
* bundled endpoint (`dev.autoqaPush.endpoint`, default `qa.omp.sh`). Each
|
|
@@ -24,11 +29,13 @@
|
|
|
24
29
|
* the network and never throws.
|
|
25
30
|
*/
|
|
26
31
|
import { Database } from "bun:sqlite";
|
|
32
|
+
import * as fs from "node:fs";
|
|
33
|
+
import * as path from "node:path";
|
|
27
34
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
28
35
|
import type { FetchImpl } from "@oh-my-pi/pi-ai";
|
|
29
36
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
30
37
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
31
|
-
import { $env, $flag,
|
|
38
|
+
import { $env, $flag, getAutoQaDbPath, getInstallId, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
32
39
|
import type { Settings } from "..";
|
|
33
40
|
import type { Theme } from "../modes/theme/theme";
|
|
34
41
|
import { renderStatusLine, truncateToWidth } from "../tui";
|
|
@@ -88,8 +95,23 @@ function parseReportIssueBody(text: string): { tool: string; report: string } {
|
|
|
88
95
|
throw new ToolError(`Invalid report format. ${reportIssueDeviceUsage()}`);
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Whether Auto-QA is active for this session.
|
|
100
|
+
*
|
|
101
|
+
* Precedence: `PI_AUTO_QA` env flag > explicit `dev.autoqa` setting >
|
|
102
|
+
* default-on unless the user previously denied consent. The denial veto only
|
|
103
|
+
* applies to the default: explicitly configuring `dev.autoqa: true` re-enables
|
|
104
|
+
* injection (recording still no-ops until consent is granted).
|
|
105
|
+
*/
|
|
91
106
|
export function isAutoQaEnabled(settings?: Settings): boolean {
|
|
92
|
-
|
|
107
|
+
let fallback = false;
|
|
108
|
+
if (settings) {
|
|
109
|
+
const enabled = !!settings.get("dev.autoqa");
|
|
110
|
+
fallback = settings.isConfigured("dev.autoqa")
|
|
111
|
+
? enabled
|
|
112
|
+
: enabled && settings.get("dev.autoqaConsent") !== "denied";
|
|
113
|
+
}
|
|
114
|
+
return $flag("PI_AUTO_QA", fallback);
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -233,15 +255,18 @@ let cachedDb: Database | null = null;
|
|
|
233
255
|
|
|
234
256
|
/**
|
|
235
257
|
* Open (or return the cached handle for) the auto-QA SQLite database at
|
|
236
|
-
* `~/.omp/
|
|
237
|
-
*
|
|
258
|
+
* `~/.omp/autoqa.db` (XDG: `$XDG_DATA_HOME/omp/autoqa.db`), creating the
|
|
259
|
+
* schema lazily. Returns `null` when the path cannot be resolved or opened.
|
|
238
260
|
*/
|
|
239
261
|
export function openAutoQaDb(): Database | null {
|
|
240
262
|
if (cachedDb) return cachedDb;
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
263
|
+
const dbPath = getAutoQaDbPath();
|
|
264
|
+
if (!dbPath) return null;
|
|
243
265
|
try {
|
|
244
|
-
|
|
266
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
267
|
+
const db = new Database(dbPath, { create: true });
|
|
268
|
+
// Install the busy handler BEFORE any lock-taking statement. See #2421.
|
|
269
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
245
270
|
db.exec(`
|
|
246
271
|
CREATE TABLE IF NOT EXISTS grievances (
|
|
247
272
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -252,6 +277,17 @@ export function openAutoQaDb(): Database | null {
|
|
|
252
277
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
253
278
|
pushed INTEGER NOT NULL DEFAULT 0
|
|
254
279
|
);
|
|
280
|
+
`);
|
|
281
|
+
// Legacy DBs (May 2026) predate `created_at`. ALTER TABLE only accepts
|
|
282
|
+
// constant defaults, so add it empty and backfill before the index below.
|
|
283
|
+
const hasCreatedAt = db.prepare("SELECT 1 FROM pragma_table_info('grievances') WHERE name = 'created_at'").get();
|
|
284
|
+
if (!hasCreatedAt) {
|
|
285
|
+
db.exec(`
|
|
286
|
+
ALTER TABLE grievances ADD COLUMN created_at TEXT NOT NULL DEFAULT '';
|
|
287
|
+
UPDATE grievances SET created_at = CURRENT_TIMESTAMP WHERE created_at = '';
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
db.exec(`
|
|
255
291
|
CREATE INDEX IF NOT EXISTS grievances_pushed_created_at_idx
|
|
256
292
|
ON grievances (pushed, created_at, id);
|
|
257
293
|
`);
|
|
@@ -471,23 +507,38 @@ export async function flushGrievances(
|
|
|
471
507
|
}
|
|
472
508
|
}
|
|
473
509
|
|
|
474
|
-
/**
|
|
475
|
-
|
|
510
|
+
/**
|
|
511
|
+
* Most recently scheduled record pipeline. Never rejects (the pipeline
|
|
512
|
+
* swallows its own errors); retained so tests can await the fire-and-forget
|
|
513
|
+
* work deterministically via {@link __awaitAutoQaRecordPipelineForTests}.
|
|
514
|
+
*/
|
|
515
|
+
let lastRecordPipeline: Promise<void> = Promise.resolve();
|
|
516
|
+
|
|
517
|
+
/** Test-only: await the last consent → insert → flush pipeline. */
|
|
518
|
+
export function __awaitAutoQaRecordPipelineForTests(): Promise<void> {
|
|
519
|
+
return lastRecordPipeline;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Queue a grievance for recording. The consent → insert → flush pipeline is
|
|
524
|
+
* fire-and-forget: nothing is written until the user grants consent (or
|
|
525
|
+
* `PI_AUTO_QA_PUSH=1` forces headless recording), and the device result
|
|
526
|
+
* returns immediately so the model never waits on the dialog or the network.
|
|
527
|
+
*/
|
|
528
|
+
function recordToolIssue(session: ToolSession, tool: string, report: string): void {
|
|
476
529
|
const canonicalTool = tool.startsWith("proxy_") ? tool.slice("proxy_".length) : tool;
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
db.prepare("INSERT INTO grievances (model, version, tool, report) VALUES (?, ?, ?, ?)").run(
|
|
480
|
-
session.getActiveModelString?.() ?? "unknown",
|
|
481
|
-
VERSION,
|
|
482
|
-
canonicalTool,
|
|
483
|
-
report,
|
|
484
|
-
);
|
|
485
|
-
void (async () => {
|
|
530
|
+
const model = session.getActiveModelString?.() ?? "unknown";
|
|
531
|
+
lastRecordPipeline = (async () => {
|
|
486
532
|
try {
|
|
487
|
-
await resolveAutoQaConsent(session.settings);
|
|
533
|
+
if (!$flag("PI_AUTO_QA_PUSH") && !(await resolveAutoQaConsent(session.settings))) return;
|
|
534
|
+
const db = openAutoQaDb();
|
|
535
|
+
if (!db) return;
|
|
536
|
+
db.prepare(
|
|
537
|
+
"INSERT INTO grievances (model, version, tool, report, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)",
|
|
538
|
+
).run(model, VERSION, canonicalTool, report);
|
|
488
539
|
await flushGrievances(db, session.settings);
|
|
489
540
|
} catch (error) {
|
|
490
|
-
logger.debug("autoqa
|
|
541
|
+
logger.debug("autoqa consent pipeline failed", { error: String(error) });
|
|
491
542
|
}
|
|
492
543
|
})();
|
|
493
544
|
}
|
|
@@ -504,7 +555,7 @@ export async function dispatchReportIssueDevice(
|
|
|
504
555
|
try {
|
|
505
556
|
if (isAutoQaEnabled(session.settings)) {
|
|
506
557
|
const { tool, report } = parseReportIssueBody(text);
|
|
507
|
-
|
|
558
|
+
recordToolIssue(session, tool, report);
|
|
508
559
|
}
|
|
509
560
|
} catch (error) {
|
|
510
561
|
if (error instanceof ToolError) throw error;
|
|
@@ -21,10 +21,17 @@ export type ToolWithTimeout = keyof typeof TOOL_TIMEOUTS;
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Clamp a raw timeout to the allowed range for a tool.
|
|
24
|
-
*
|
|
24
|
+
*
|
|
25
|
+
* When `rawTimeout` is undefined the tool's `default` is used. A positive
|
|
26
|
+
* `maxTimeout` (the `tools.maxTimeout` global ceiling) caps the *resolved*
|
|
27
|
+
* value — including the default-fallback path — before the per-tool `min`/`max`
|
|
28
|
+
* floor and ceiling apply, so a configured global cap governs calls where the
|
|
29
|
+
* agent omits `timeout`, not only explicitly-passed values. `maxTimeout <= 0`
|
|
30
|
+
* means no global cap.
|
|
25
31
|
*/
|
|
26
|
-
export function clampTimeout(tool: ToolWithTimeout, rawTimeout?: number): number {
|
|
32
|
+
export function clampTimeout(tool: ToolWithTimeout, rawTimeout?: number, maxTimeout?: number): number {
|
|
27
33
|
const config = TOOL_TIMEOUTS[tool];
|
|
28
34
|
const timeout = rawTimeout ?? config.default;
|
|
29
|
-
|
|
35
|
+
const capped = maxTimeout !== undefined && maxTimeout > 0 ? Math.min(timeout, maxTimeout) : timeout;
|
|
36
|
+
return Math.max(config.min, Math.min(config.max, capped));
|
|
30
37
|
}
|
package/src/tools/write.ts
CHANGED
|
@@ -85,6 +85,36 @@ import { renderXdevCall, renderXdevResult, type XdevDispatch } from "./xdev";
|
|
|
85
85
|
|
|
86
86
|
const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
|
|
87
87
|
const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]";
|
|
88
|
+
const URI_LIKE_WRITE_PATH_RE = /^([a-z][a-z0-9+.-]*):\/{1,2}(.*)$/i;
|
|
89
|
+
const XD_MISSING_DELIMITER_RE = /^xd\/+(.*)$/i;
|
|
90
|
+
const XD_SCHEME_NEAR_MISSES: Record<string, true> = { dx: true, xdd: true, xdt: true };
|
|
91
|
+
|
|
92
|
+
function assertWriteTargetAddressable(target: string, router: InternalUrlRouter): void {
|
|
93
|
+
const trimmed = target.trim();
|
|
94
|
+
if (path.win32.isAbsolute(trimmed) || router.canHandle(trimmed)) return;
|
|
95
|
+
|
|
96
|
+
const missingDelimiter = trimmed.match(XD_MISSING_DELIMITER_RE);
|
|
97
|
+
if (missingDelimiter) {
|
|
98
|
+
throw new ToolError(
|
|
99
|
+
`Unknown URI-like write target '${trimmed}'. Did you mean 'xd://${missingDelimiter[1]}'? Prefix the path with './' to write it as a filesystem path.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const uriLike = trimmed.match(URI_LIKE_WRITE_PATH_RE);
|
|
104
|
+
if (!uriLike) return;
|
|
105
|
+
|
|
106
|
+
const scheme = uriLike[1]!.toLowerCase();
|
|
107
|
+
// conflict:// has no router handler but is spliced downstream by
|
|
108
|
+
// parseConflictUri (which emits its own precise id/scope errors); let it pass.
|
|
109
|
+
if (scheme === "conflict") return;
|
|
110
|
+
const canonicalScheme = router.getHandler(scheme) ? scheme : XD_SCHEME_NEAR_MISSES[scheme] ? "xd" : undefined;
|
|
111
|
+
const suggestion = canonicalScheme
|
|
112
|
+
? ` Did you mean '${canonicalScheme}://${uriLike[2]}'?`
|
|
113
|
+
: " Tool devices use 'xd://<tool>'.";
|
|
114
|
+
throw new ToolError(
|
|
115
|
+
`Unknown URI-like write target '${trimmed}'.${suggestion} Prefix the path with './' to write it as a filesystem path.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
88
118
|
|
|
89
119
|
const BULK_DIRECTIVE_RE = /^#?(\d+)\s*[:=]\s*(@ours|@theirs|@base|@both)$/;
|
|
90
120
|
/**
|
|
@@ -987,6 +1017,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
987
1017
|
// Strip hashline display prefixes ([PATH#HASH] + LINE:) if the model copied them from read output
|
|
988
1018
|
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
|
|
989
1019
|
const internalRouter = InternalUrlRouter.instance();
|
|
1020
|
+
assertWriteTargetAddressable(path, internalRouter);
|
|
990
1021
|
if (internalRouter.canHandle(path)) {
|
|
991
1022
|
const parsed = parseInternalUrl(path);
|
|
992
1023
|
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
package/src/utils/changelog.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getLastChangelogVersionPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import bundledChangelog from "../../CHANGELOG.md" with { type: "text" };
|
|
2
3
|
|
|
3
4
|
export interface ChangelogEntry {
|
|
4
5
|
major: number;
|
|
@@ -29,73 +30,68 @@ export interface StartupChangelogSelection {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
* Parse changelog entries from
|
|
33
|
-
*
|
|
33
|
+
* Parse changelog entries from omp's package asset when available, falling back
|
|
34
|
+
* to the copy embedded in compiled binaries.
|
|
34
35
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* synthesize a fallback path from the host project's cwd; doing so caused issue
|
|
38
|
-
* #1423 (the host project's `CHANGELOG.md` was rendered as omp's).
|
|
36
|
+
* The embedded fallback keeps standalone binaries self-contained without
|
|
37
|
+
* resolving relative to the host project's cwd, which caused issue #1423.
|
|
39
38
|
*/
|
|
40
39
|
export async function parseChangelog(changelogPath: string | undefined): Promise<ChangelogEntry[]> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let currentLines: string[] = [];
|
|
50
|
-
let currentVersion: { major: number; minor: number; patch: number } | null = null;
|
|
51
|
-
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
// Check if this is a version header (## [x.y.z] ...)
|
|
54
|
-
if (line.startsWith("## ")) {
|
|
55
|
-
// Save previous entry if exists
|
|
56
|
-
if (currentVersion && currentLines.length > 0) {
|
|
57
|
-
entries.push({
|
|
58
|
-
...currentVersion,
|
|
59
|
-
content: currentLines.join("\n").trim(),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Try to parse version from this line
|
|
64
|
-
const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
|
|
65
|
-
if (versionMatch) {
|
|
66
|
-
currentVersion = {
|
|
67
|
-
major: Number.parseInt(versionMatch[1], 10),
|
|
68
|
-
minor: Number.parseInt(versionMatch[2], 10),
|
|
69
|
-
patch: Number.parseInt(versionMatch[3], 10),
|
|
70
|
-
};
|
|
71
|
-
currentLines = [line];
|
|
72
|
-
} else {
|
|
73
|
-
// Reset if we can't parse version
|
|
74
|
-
currentVersion = null;
|
|
75
|
-
currentLines = [];
|
|
76
|
-
}
|
|
77
|
-
} else if (currentVersion) {
|
|
78
|
-
// Collect lines for current version
|
|
79
|
-
currentLines.push(line);
|
|
40
|
+
let content = bundledChangelog;
|
|
41
|
+
if (changelogPath) {
|
|
42
|
+
try {
|
|
43
|
+
content = await Bun.file(changelogPath).text();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!isEnoent(error)) {
|
|
46
|
+
logger.error(`Warning: Could not parse changelog: ${error}`);
|
|
80
47
|
}
|
|
81
48
|
}
|
|
49
|
+
}
|
|
82
50
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
entries.push({
|
|
86
|
-
...currentVersion,
|
|
87
|
-
content: currentLines.join("\n").trim(),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
51
|
+
return parseChangelogContent(content);
|
|
52
|
+
}
|
|
90
53
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
54
|
+
function parseChangelogContent(content: string): ChangelogEntry[] {
|
|
55
|
+
const lines = content.split("\n");
|
|
56
|
+
const entries: ChangelogEntry[] = [];
|
|
57
|
+
|
|
58
|
+
let currentLines: string[] = [];
|
|
59
|
+
let currentVersion: { major: number; minor: number; patch: number } | null = null;
|
|
60
|
+
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (line.startsWith("## ")) {
|
|
63
|
+
if (currentVersion && currentLines.length > 0) {
|
|
64
|
+
entries.push({
|
|
65
|
+
...currentVersion,
|
|
66
|
+
content: currentLines.join("\n").trim(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
|
|
71
|
+
if (versionMatch) {
|
|
72
|
+
currentVersion = {
|
|
73
|
+
major: Number.parseInt(versionMatch[1], 10),
|
|
74
|
+
minor: Number.parseInt(versionMatch[2], 10),
|
|
75
|
+
patch: Number.parseInt(versionMatch[3], 10),
|
|
76
|
+
};
|
|
77
|
+
currentLines = [line];
|
|
78
|
+
} else {
|
|
79
|
+
currentVersion = null;
|
|
80
|
+
currentLines = [];
|
|
81
|
+
}
|
|
82
|
+
} else if (currentVersion) {
|
|
83
|
+
currentLines.push(line);
|
|
95
84
|
}
|
|
96
|
-
logger.error(`Warning: Could not parse changelog: ${error}`);
|
|
97
|
-
return [];
|
|
98
85
|
}
|
|
86
|
+
|
|
87
|
+
if (currentVersion && currentLines.length > 0) {
|
|
88
|
+
entries.push({
|
|
89
|
+
...currentVersion,
|
|
90
|
+
content: currentLines.join("\n").trim(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return entries;
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
/**
|
package/src/utils/git.ts
CHANGED
|
@@ -217,6 +217,12 @@ export const GIT_NETWORK_TIMEOUT_MS = 30 * 60 * 1000;
|
|
|
217
217
|
export const GIT_COMMAND_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
218
218
|
|
|
219
219
|
const GIT_COMMAND_TIMEOUT_EXIT_CODE = 124;
|
|
220
|
+
// Exit code returned when the `git` binary cannot be launched at all (spawn
|
|
221
|
+
// ENOENT). Mirrors the POSIX "command not found" code so read-only callers that
|
|
222
|
+
// degrade on any non-zero exit treat a missing git the same as a failed
|
|
223
|
+
// invocation instead of letting the raw spawn ENOENT escape as an unhandled
|
|
224
|
+
// rejection.
|
|
225
|
+
const GIT_SPAWN_ENOENT_EXIT_CODE = 127;
|
|
220
226
|
const GIT_OUTPUT_TRUNCATED_MARKER = "\n[git subprocess output truncated after 8 MiB]\n";
|
|
221
227
|
const GIT_COMMAND_TERMINATE_GRACE_MS = 5_000;
|
|
222
228
|
|
|
@@ -388,6 +394,29 @@ function ensureAvailable(): void {
|
|
|
388
394
|
}
|
|
389
395
|
}
|
|
390
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Launch a `git` plumbing command synchronously and decode stdout. Returns the
|
|
399
|
+
* exit code plus trimmed stdout; a missing `git` binary (spawn ENOENT) is
|
|
400
|
+
* reported as {@link GIT_SPAWN_ENOENT_EXIT_CODE} so sync read-only callers
|
|
401
|
+
* degrade to `null` instead of throwing an uncaught error during rendering.
|
|
402
|
+
*/
|
|
403
|
+
function gitSpawnSyncText(cwd: string, args: readonly string[]): { exitCode: number; stdout: string } {
|
|
404
|
+
const commandArgs = withShortLivedGitConfig(withNoOptionalLocks(args));
|
|
405
|
+
try {
|
|
406
|
+
const result = Bun.spawnSync(["git", ...commandArgs], {
|
|
407
|
+
cwd,
|
|
408
|
+
env: buildGitEnv(),
|
|
409
|
+
stdout: "pipe",
|
|
410
|
+
stderr: "pipe",
|
|
411
|
+
windowsHide: true,
|
|
412
|
+
});
|
|
413
|
+
return { exitCode: result.exitCode ?? 0, stdout: new TextDecoder().decode(result.stdout).trim() };
|
|
414
|
+
} catch (err) {
|
|
415
|
+
if (isEnoent(err)) return { exitCode: GIT_SPAWN_ENOENT_EXIT_CODE, stdout: "" };
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
391
420
|
function formatCommandFailure(
|
|
392
421
|
args: readonly string[],
|
|
393
422
|
result: Pick<GitCommandResult, "exitCode" | "stdout" | "stderr">,
|
|
@@ -401,15 +430,26 @@ function formatCommandFailure(
|
|
|
401
430
|
|
|
402
431
|
async function git(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<GitCommandResult> {
|
|
403
432
|
const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
433
|
+
let child: Subprocess;
|
|
434
|
+
try {
|
|
435
|
+
child = Bun.spawn(["git", ...commandArgs], {
|
|
436
|
+
cwd,
|
|
437
|
+
env: buildGitEnv(options.env),
|
|
438
|
+
signal: options.signal,
|
|
439
|
+
stdin: normalizeStdin(options.stdin),
|
|
440
|
+
stdout: "pipe",
|
|
441
|
+
stderr: "pipe",
|
|
442
|
+
windowsHide: true,
|
|
443
|
+
});
|
|
444
|
+
} catch (err) {
|
|
445
|
+
if (isEnoent(err)) {
|
|
446
|
+
// A deleted/nonexistent cwd also surfaces as a spawn ENOENT; only blame
|
|
447
|
+
// the binary when the working directory actually exists.
|
|
448
|
+
const stderr = fs.existsSync(cwd) ? "git is not installed." : `working directory does not exist: ${cwd}`;
|
|
449
|
+
return { exitCode: GIT_SPAWN_ENOENT_EXIT_CODE, stdout: "", stderr };
|
|
450
|
+
}
|
|
451
|
+
throw err;
|
|
452
|
+
}
|
|
413
453
|
|
|
414
454
|
return await collectSubprocessResult("git", commandArgs, child, options);
|
|
415
455
|
}
|
|
@@ -890,28 +930,12 @@ async function resolveHeadStateReftable(repository: GitRepository, signal?: Abor
|
|
|
890
930
|
}
|
|
891
931
|
|
|
892
932
|
function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState | null {
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
const
|
|
896
|
-
cwd: repository.repoRoot,
|
|
897
|
-
env: buildGitEnv(),
|
|
898
|
-
stdout: "pipe",
|
|
899
|
-
stderr: "pipe",
|
|
900
|
-
windowsHide: true,
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", "HEAD"]));
|
|
904
|
-
const revResult = Bun.spawnSync(["git", ...revArgs], {
|
|
905
|
-
cwd: repository.repoRoot,
|
|
906
|
-
env: buildGitEnv(),
|
|
907
|
-
stdout: "pipe",
|
|
908
|
-
stderr: "pipe",
|
|
909
|
-
windowsHide: true,
|
|
910
|
-
});
|
|
911
|
-
const commit = revResult.exitCode === 0 ? new TextDecoder().decode(revResult.stdout).trim() || null : null;
|
|
933
|
+
const symResult = gitSpawnSyncText(repository.repoRoot, ["symbolic-ref", "HEAD"]);
|
|
934
|
+
const revResult = gitSpawnSyncText(repository.repoRoot, ["rev-parse", "--verify", "HEAD"]);
|
|
935
|
+
const commit = revResult.exitCode === 0 ? revResult.stdout || null : null;
|
|
912
936
|
|
|
913
937
|
if (symResult.exitCode === 0) {
|
|
914
|
-
const ref =
|
|
938
|
+
const ref = symResult.stdout;
|
|
915
939
|
const branchName = ref.startsWith(LOCAL_BRANCH_PREFIX) ? ref.slice(LOCAL_BRANCH_PREFIX.length) : null;
|
|
916
940
|
return {
|
|
917
941
|
...repository,
|
|
@@ -933,29 +957,13 @@ function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState |
|
|
|
933
957
|
|
|
934
958
|
function readRefSync(repository: GitRepository, targetRef: string): string | null {
|
|
935
959
|
if (isReftableRepoSync(repository)) {
|
|
936
|
-
|
|
937
|
-
const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", targetRef]));
|
|
938
|
-
const symResult = Bun.spawnSync(["git", ...symArgs], {
|
|
939
|
-
cwd: repository.repoRoot,
|
|
940
|
-
env: buildGitEnv(),
|
|
941
|
-
stdout: "pipe",
|
|
942
|
-
stderr: "pipe",
|
|
943
|
-
windowsHide: true,
|
|
944
|
-
});
|
|
960
|
+
const symResult = gitSpawnSyncText(repository.repoRoot, ["symbolic-ref", targetRef]);
|
|
945
961
|
if (symResult.exitCode === 0) {
|
|
946
|
-
|
|
947
|
-
return `${HEAD_REF_PREFIX} ${stdoutText}`;
|
|
962
|
+
return `${HEAD_REF_PREFIX} ${symResult.stdout}`;
|
|
948
963
|
}
|
|
949
|
-
const
|
|
950
|
-
const revResult = Bun.spawnSync(["git", ...revArgs], {
|
|
951
|
-
cwd: repository.repoRoot,
|
|
952
|
-
env: buildGitEnv(),
|
|
953
|
-
stdout: "pipe",
|
|
954
|
-
stderr: "pipe",
|
|
955
|
-
windowsHide: true,
|
|
956
|
-
});
|
|
964
|
+
const revResult = gitSpawnSyncText(repository.repoRoot, ["rev-parse", "--verify", targetRef]);
|
|
957
965
|
if (revResult.exitCode === 0) {
|
|
958
|
-
return
|
|
966
|
+
return revResult.stdout || null;
|
|
959
967
|
}
|
|
960
968
|
return null;
|
|
961
969
|
}
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* Calls Firecrawl's search API and maps web results into the unified
|
|
5
5
|
* SearchResponse shape used by the web search tool.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type AuthStorage,
|
|
9
|
+
type FetchImpl,
|
|
10
|
+
getEnvApiKey,
|
|
11
|
+
resolveApiKeyOnce,
|
|
12
|
+
seedApiKeyResolver,
|
|
13
|
+
withAuth,
|
|
14
|
+
} from "@oh-my-pi/pi-ai";
|
|
8
15
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
9
16
|
import { SearchProviderError } from "../../../web/search/types";
|
|
10
17
|
import { clampNumResults } from "../utils";
|
|
@@ -66,13 +73,19 @@ function buildRequestBody(params: FirecrawlSearchParams): Record<string, unknown
|
|
|
66
73
|
return body;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
async function callFirecrawlSearch(
|
|
76
|
+
async function callFirecrawlSearch(
|
|
77
|
+
apiKey: string | undefined,
|
|
78
|
+
params: FirecrawlSearchParams,
|
|
79
|
+
): Promise<FirecrawlSearchResponse> {
|
|
80
|
+
const headers: Record<string, string> = {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
};
|
|
83
|
+
if (apiKey) {
|
|
84
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
85
|
+
}
|
|
70
86
|
const response = await (params.fetch ?? fetch)(FIRECRAWL_SEARCH_URL, {
|
|
71
87
|
method: "POST",
|
|
72
|
-
headers
|
|
73
|
-
"Content-Type": "application/json",
|
|
74
|
-
Authorization: `Bearer ${apiKey}`,
|
|
75
|
-
},
|
|
88
|
+
headers,
|
|
76
89
|
body: JSON.stringify(buildRequestBody(params)),
|
|
77
90
|
signal: withHardTimeout(params.signal),
|
|
78
91
|
});
|
|
@@ -100,16 +113,24 @@ export async function searchFirecrawl(params: SearchParams): Promise<SearchRespo
|
|
|
100
113
|
signal: params.signal,
|
|
101
114
|
fetch: params.fetch,
|
|
102
115
|
};
|
|
103
|
-
const
|
|
116
|
+
const keyResolver = params.authStorage.resolver("firecrawl", {
|
|
104
117
|
sessionId: params.sessionId,
|
|
105
118
|
});
|
|
106
119
|
const numResults = clampNumResults(firecrawlParams.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
107
120
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
const resolvedKey = await resolveApiKeyOnce(keyResolver, params.signal);
|
|
122
|
+
let data: FirecrawlSearchResponse;
|
|
123
|
+
if (resolvedKey) {
|
|
124
|
+
// Reuse the preflight credential for the initial authenticated attempt.
|
|
125
|
+
const seededResolver = seedApiKeyResolver(resolvedKey, keyResolver);
|
|
126
|
+
data = await withAuth(seededResolver, key => callFirecrawlSearch(key, firecrawlParams), {
|
|
127
|
+
signal: params.signal,
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
// Keyless mode — omit Authorization header
|
|
131
|
+
data = await callFirecrawlSearch(undefined, firecrawlParams);
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
const sources: SearchSource[] = [];
|
|
114
135
|
|
|
115
136
|
for (const result of data.data?.web ?? []) {
|
|
@@ -125,7 +146,7 @@ export async function searchFirecrawl(params: SearchParams): Promise<SearchRespo
|
|
|
125
146
|
provider: "firecrawl",
|
|
126
147
|
sources: sources.slice(0, numResults),
|
|
127
148
|
requestId: data.id ?? undefined,
|
|
128
|
-
authMode: "api_key",
|
|
149
|
+
authMode: resolvedKey ? "api_key" : "keyless",
|
|
129
150
|
};
|
|
130
151
|
}
|
|
131
152
|
|
|
@@ -134,10 +155,22 @@ export class FirecrawlProvider extends SearchProvider {
|
|
|
134
155
|
readonly id = "firecrawl";
|
|
135
156
|
readonly label = "Firecrawl";
|
|
136
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Auto-chain admission: requires a credential so an unconfigured Firecrawl
|
|
160
|
+
* doesn't displace other providers that the user has set up with API keys.
|
|
161
|
+
*/
|
|
137
162
|
isAvailable(authStorage: AuthStorage): boolean {
|
|
138
163
|
return authStorage.hasAuth("firecrawl") || !!getEnvApiKey("firecrawl");
|
|
139
164
|
}
|
|
140
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Firecrawl supports keyless mode, so an explicit user selection
|
|
168
|
+
* (`webSearch: firecrawl`) works without any credential configured.
|
|
169
|
+
*/
|
|
170
|
+
isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
141
174
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
142
175
|
return searchFirecrawl(params);
|
|
143
176
|
}
|