@selesai/code 0.1.19 → 0.2.0
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/dist/agents/architect.md +4 -3
- package/dist/agents/builder.md +5 -4
- package/dist/agents/commentator.md +3 -2
- package/dist/agents/explorer.md +4 -4
- package/dist/agents/recapper.md +4 -3
- package/dist/config.d.ts +6 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -9
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +18 -24
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +8 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +25 -11
- package/dist/core/skills.js.map +1 -1
- package/dist/defaults/settings.json +2 -29
- package/dist/extensions/package.json +5 -4
- package/dist/extensions/pi-intercom/index.ts +12 -5
- package/dist/extensions/pi-powerline-footer/index.ts +4 -1
- package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
- package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
- package/dist/extensions/pi-subagents/README.md +12 -5
- package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
- package/dist/extensions/pi-subagents/package.json +8 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
- package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
- package/dist/extensions/pi-web-agent/package.json +31 -0
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
- package/dist/extensions/pi-web-agent/src/types.ts +88 -0
- package/dist/extensions/ponytail/index.js +189 -0
- package/dist/extensions/ponytail/package.json +8 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
- package/dist/extensions/ponytail/test/extension.test.js +167 -0
- package/dist/extensions/ponytail/test/helpers.test.js +92 -0
- package/dist/extensions/question/constants.ts +38 -0
- package/dist/extensions/question/dialog-adapter.ts +74 -0
- package/dist/extensions/question/helpers.ts +102 -0
- package/dist/extensions/question/index.ts +671 -0
- package/dist/extensions/question/navigation.ts +14 -0
- package/dist/extensions/question/package.json +18 -0
- package/dist/extensions/question/question-list.ts +498 -0
- package/dist/extensions/question/row-layout.ts +162 -0
- package/dist/extensions/question/schemas.ts +69 -0
- package/dist/extensions/question/selection-mode.ts +49 -0
- package/dist/extensions/question/shortcuts.ts +53 -0
- package/dist/extensions/question/tests/helpers.test.ts +178 -0
- package/dist/extensions/question/tests/question-list.test.ts +230 -0
- package/dist/extensions/question/tests/row-layout.test.ts +136 -0
- package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
- package/dist/extensions/question/tui-adapter.ts +33 -0
- package/dist/extensions/question/types.ts +102 -0
- package/dist/extensions/question/ui-protocol.ts +46 -0
- package/dist/extensions/tokenin-onboarding.ts +265 -2
- package/dist/extensions/web-agent-onboarding.ts +222 -0
- package/dist/extensions/workflow/adapter.ts +588 -0
- package/dist/extensions/workflow/extension.ts +24 -0
- package/dist/extensions/workflow/modes/prototype.ts +156 -0
- package/dist/extensions/workflow/modes/quick.ts +138 -0
- package/dist/extensions/workflow/package.json +17 -0
- package/dist/extensions/workflow/state-machine.ts +386 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +57 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/mime.d.ts.map +1 -1
- package/dist/utils/mime.js +24 -1
- package/dist/utils/mime.js.map +1 -1
- package/docs/docs.json +4 -0
- package/docs/workflows.md +206 -0
- package/package.json +5 -1
- package/README.md +0 -198
|
@@ -6,7 +6,7 @@ import * as os from "node:os";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
8
|
import type { FSWatcher } from "node:fs";
|
|
9
|
-
import type { ExtensionContext } from "@
|
|
9
|
+
import type { ExtensionContext } from "@selesai/code";
|
|
10
10
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Basic Types
|
|
@@ -982,7 +982,7 @@ export function normalizeMaxSubagentDepth(value: unknown): number | undefined {
|
|
|
982
982
|
}
|
|
983
983
|
|
|
984
984
|
export function resolveCurrentMaxSubagentDepth(configMaxDepth?: number): number {
|
|
985
|
-
return normalizeMaxSubagentDepth(process.env.
|
|
985
|
+
return normalizeMaxSubagentDepth(process.env.SELESAI_SUBAGENT_MAX_DEPTH)
|
|
986
986
|
?? normalizeMaxSubagentDepth(configMaxDepth)
|
|
987
987
|
?? DEFAULT_SUBAGENT_MAX_DEPTH;
|
|
988
988
|
}
|
|
@@ -994,18 +994,18 @@ export function resolveChildMaxSubagentDepth(parentMaxDepth: number, agentMaxDep
|
|
|
994
994
|
}
|
|
995
995
|
|
|
996
996
|
export function checkSubagentDepth(configMaxDepth?: number): { blocked: boolean; depth: number; maxDepth: number } {
|
|
997
|
-
const depth = Number(process.env.
|
|
997
|
+
const depth = Number(process.env.SELESAI_SUBAGENT_DEPTH ?? "0");
|
|
998
998
|
const maxDepth = resolveCurrentMaxSubagentDepth(configMaxDepth);
|
|
999
999
|
const blocked = Number.isFinite(depth) && depth >= maxDepth;
|
|
1000
1000
|
return { blocked, depth, maxDepth };
|
|
1001
1001
|
}
|
|
1002
1002
|
|
|
1003
1003
|
export function getSubagentDepthEnv(maxDepth?: number): Record<string, string> {
|
|
1004
|
-
const parentDepth = Number(process.env.
|
|
1004
|
+
const parentDepth = Number(process.env.SELESAI_SUBAGENT_DEPTH ?? "0");
|
|
1005
1005
|
const nextDepth = Number.isFinite(parentDepth) ? parentDepth + 1 : 1;
|
|
1006
1006
|
return {
|
|
1007
|
-
|
|
1008
|
-
|
|
1007
|
+
SELESAI_SUBAGENT_DEPTH: String(nextDepth),
|
|
1008
|
+
SELESAI_SUBAGENT_MAX_DEPTH: String(normalizeMaxSubagentDepth(maxDepth) ?? resolveCurrentMaxSubagentDepth()),
|
|
1009
1009
|
};
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import * as piCodingAgent from "@
|
|
8
|
+
import * as piCodingAgent from "@selesai/code";
|
|
9
9
|
import type { Message } from "@earendil-works/pi-ai";
|
|
10
10
|
import { formatToolCall } from "./formatters.ts";
|
|
11
11
|
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, SingleResult, ToolCallSummary } from "./types.ts";
|
|
@@ -14,7 +14,7 @@ import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, Singl
|
|
|
14
14
|
// File System Utilities
|
|
15
15
|
// ============================================================================
|
|
16
16
|
|
|
17
|
-
const DEFAULT_CONFIG_DIR_NAME = ".
|
|
17
|
+
const DEFAULT_CONFIG_DIR_NAME = ".selesai";
|
|
18
18
|
|
|
19
19
|
export function resolveConfigDirName(codingAgentModule: unknown = piCodingAgent): string {
|
|
20
20
|
const value = codingAgentModule && typeof codingAgentModule === "object"
|
|
@@ -32,7 +32,7 @@ export function getProjectConfigDir(projectRoot: string): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function getAgentDir(): string {
|
|
35
|
-
const configured = process.env.
|
|
35
|
+
const configured = process.env.SELESAI_CODING_AGENT_DIR;
|
|
36
36
|
if (configured === "~") return os.homedir();
|
|
37
37
|
if (configured?.startsWith("~/")) return path.join(os.homedir(), configured.slice(2));
|
|
38
38
|
return configured || path.join(os.homedir(), getConfigDirName(), "agent");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { ExtensionContext } from "@
|
|
2
|
+
import type { ExtensionContext } from "@selesai/code";
|
|
3
3
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
4
4
|
import {
|
|
5
5
|
SLASH_SUBAGENT_CANCEL_EVENT,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@selesai/code";
|
|
5
5
|
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
6
6
|
import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
|
|
7
7
|
import {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
7
|
-
import { getMarkdownTheme, type ExtensionContext } from "@
|
|
7
|
+
import { getMarkdownTheme, type ExtensionContext } from "@selesai/code";
|
|
8
8
|
import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
9
9
|
import {
|
|
10
10
|
type AgentProgress,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import * as assert from "node:assert";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { createSpawnTimingMarkers, emitSpawnTiming } from "../../src/runs/shared/spawn-timing.ts";
|
|
7
|
+
|
|
8
|
+
describe("spawn-timing", () => {
|
|
9
|
+
it("creates markers with a start time", () => {
|
|
10
|
+
const markers = createSpawnTimingMarkers();
|
|
11
|
+
assert.ok(markers.spawnStart > 0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("emits a timing line when an eventsPath is provided", () => {
|
|
15
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-timing-"));
|
|
16
|
+
const eventsPath = path.join(dir, "events.jsonl");
|
|
17
|
+
const markers = createSpawnTimingMarkers();
|
|
18
|
+
emitSpawnTiming({
|
|
19
|
+
eventsPath,
|
|
20
|
+
marker: "spawn_start",
|
|
21
|
+
markers,
|
|
22
|
+
runId: "run-1",
|
|
23
|
+
stepIndex: 2,
|
|
24
|
+
agent: "scout",
|
|
25
|
+
});
|
|
26
|
+
const lines = fs.readFileSync(eventsPath, "utf-8").trim().split("\n");
|
|
27
|
+
assert.equal(lines.length, 1);
|
|
28
|
+
const event = JSON.parse(lines[0]!);
|
|
29
|
+
assert.equal(event.type, "subagent.spawn.timing");
|
|
30
|
+
assert.equal(event.runId, "run-1");
|
|
31
|
+
assert.equal(event.stepIndex, 2);
|
|
32
|
+
assert.equal(event.agent, "scout");
|
|
33
|
+
assert.equal(event.marker, "spawn_start");
|
|
34
|
+
assert.ok(typeof event.elapsedMs === "number");
|
|
35
|
+
assert.ok(event.ts > 0);
|
|
36
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("skips emission when eventsPath is missing", () => {
|
|
40
|
+
const markers = createSpawnTimingMarkers();
|
|
41
|
+
assert.doesNotThrow(() => {
|
|
42
|
+
emitSpawnTiming({ eventsPath: undefined, marker: "spawn_start", markers });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist/runner",
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"target": "ES2022",
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"noCheck": true,
|
|
10
|
+
"noEmitOnError": false,
|
|
11
|
+
"declaration": false,
|
|
12
|
+
"declarationMap": false,
|
|
13
|
+
"inlineSources": false
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["test", "**/*.test.ts"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@selesai/bundled-pi-web-agent",
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Bundled pi-web-agent extension for reliable web access with explicit search, fetch, and headless boundaries. Vendored source-first from @demigodmode/pi-web-agent.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi",
|
|
10
|
+
"extension",
|
|
11
|
+
"web-search",
|
|
12
|
+
"web-fetch"
|
|
13
|
+
],
|
|
14
|
+
"license": "AGPL-3.0-only",
|
|
15
|
+
"pi": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"./src/extension.ts"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@mozilla/readability": "^0.6.0",
|
|
22
|
+
"cheerio": "^1.1.0",
|
|
23
|
+
"jsdom": "^26.0.0",
|
|
24
|
+
"playwright": "^1.60.0",
|
|
25
|
+
"typebox": "^1.1.37"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
29
|
+
"@earendil-works/pi-tui": "*"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export type SearxngOptions = {
|
|
2
|
+
categories?: string[];
|
|
3
|
+
language?: string;
|
|
4
|
+
safesearch?: 0 | 1 | 2;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type FirecrawlOptions = {
|
|
8
|
+
formats?: string[];
|
|
9
|
+
onlyMainContent?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SearchBackendConfig = {
|
|
13
|
+
provider: 'duckduckgo' | 'searxng' | 'brave';
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
fallback?: 'duckduckgo';
|
|
16
|
+
options?: SearxngOptions;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type FetchBackendConfig = {
|
|
20
|
+
provider: 'http' | 'firecrawl';
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
fallback?: 'http';
|
|
24
|
+
options?: FirecrawlOptions;
|
|
25
|
+
};
|
|
26
|
+
export type HeadlessBackendConfig = { provider: 'local-browser' };
|
|
27
|
+
|
|
28
|
+
export type BackendConfig = {
|
|
29
|
+
search: SearchBackendConfig;
|
|
30
|
+
fetch: FetchBackendConfig;
|
|
31
|
+
headless: HeadlessBackendConfig;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type BackendConfigOverride = {
|
|
35
|
+
search?: Partial<SearchBackendConfig>;
|
|
36
|
+
fetch?: Partial<FetchBackendConfig>;
|
|
37
|
+
headless?: Partial<HeadlessBackendConfig>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type BackendConfigFile = {
|
|
41
|
+
backends?: {
|
|
42
|
+
search?: { provider?: unknown; baseUrl?: unknown; fallback?: unknown; options?: unknown };
|
|
43
|
+
fetch?: { provider?: unknown; baseUrl?: unknown; apiKey?: unknown; fallback?: unknown; options?: unknown };
|
|
44
|
+
headless?: { provider?: unknown };
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_BACKEND_CONFIG: BackendConfig = {
|
|
49
|
+
search: { provider: 'duckduckgo' },
|
|
50
|
+
fetch: { provider: 'http' },
|
|
51
|
+
headless: { provider: 'local-browser' }
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function extractStringArray(value: unknown): string[] | undefined {
|
|
55
|
+
if (!Array.isArray(value)) return undefined;
|
|
56
|
+
const strings = value.filter((item): item is string => typeof item === 'string');
|
|
57
|
+
return strings.length === value.length ? strings : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractSearxngOptions(value: unknown): SearxngOptions | undefined {
|
|
61
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
62
|
+
const raw = value as { categories?: unknown; language?: unknown; safesearch?: unknown };
|
|
63
|
+
const options: SearxngOptions = {};
|
|
64
|
+
const categories = extractStringArray(raw.categories);
|
|
65
|
+
if (categories) options.categories = categories;
|
|
66
|
+
if (typeof raw.language === 'string') options.language = raw.language;
|
|
67
|
+
if (raw.safesearch === 0 || raw.safesearch === 1 || raw.safesearch === 2) options.safesearch = raw.safesearch;
|
|
68
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractFirecrawlOptions(value: unknown): FirecrawlOptions | undefined {
|
|
72
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
73
|
+
const raw = value as { formats?: unknown; onlyMainContent?: unknown };
|
|
74
|
+
const options: FirecrawlOptions = {};
|
|
75
|
+
const formats = extractStringArray(raw.formats);
|
|
76
|
+
if (formats) options.formats = formats;
|
|
77
|
+
if (typeof raw.onlyMainContent === 'boolean') options.onlyMainContent = raw.onlyMainContent;
|
|
78
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function extractBackendConfigOverride(
|
|
82
|
+
file: BackendConfigFile | null | undefined
|
|
83
|
+
): BackendConfigOverride {
|
|
84
|
+
const backends = file?.backends;
|
|
85
|
+
const override: BackendConfigOverride = {};
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
backends?.search?.provider === 'duckduckgo' ||
|
|
89
|
+
backends?.search?.provider === 'searxng' ||
|
|
90
|
+
backends?.search?.provider === 'brave'
|
|
91
|
+
) {
|
|
92
|
+
override.search = { provider: backends.search.provider };
|
|
93
|
+
if (backends.search.provider === 'searxng' && typeof backends.search.baseUrl === 'string') {
|
|
94
|
+
override.search.baseUrl = backends.search.baseUrl;
|
|
95
|
+
}
|
|
96
|
+
if (backends.search.fallback === 'duckduckgo') {
|
|
97
|
+
override.search.fallback = 'duckduckgo';
|
|
98
|
+
}
|
|
99
|
+
if (backends.search.provider === 'searxng') {
|
|
100
|
+
const options = extractSearxngOptions(backends.search.options);
|
|
101
|
+
if (options) {
|
|
102
|
+
override.search.options = options;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (backends?.fetch?.provider === 'http' || backends?.fetch?.provider === 'firecrawl') {
|
|
108
|
+
override.fetch = { provider: backends.fetch.provider };
|
|
109
|
+
if (typeof backends.fetch.baseUrl === 'string') {
|
|
110
|
+
override.fetch.baseUrl = backends.fetch.baseUrl;
|
|
111
|
+
}
|
|
112
|
+
if (typeof backends.fetch.apiKey === 'string') {
|
|
113
|
+
override.fetch.apiKey = backends.fetch.apiKey;
|
|
114
|
+
}
|
|
115
|
+
if (backends.fetch.fallback === 'http') {
|
|
116
|
+
override.fetch.fallback = 'http';
|
|
117
|
+
}
|
|
118
|
+
const options = extractFirecrawlOptions(backends.fetch.options);
|
|
119
|
+
if (options) {
|
|
120
|
+
override.fetch.options = options;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (backends?.headless?.provider === 'local-browser') {
|
|
125
|
+
override.headless = { provider: 'local-browser' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return override;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function validateBackendConfig(config: BackendConfig): string[] {
|
|
132
|
+
const issues: string[] = [];
|
|
133
|
+
|
|
134
|
+
if (config.search.provider === 'searxng' && !config.search.baseUrl) {
|
|
135
|
+
issues.push('search provider searxng requires backends.search.baseUrl');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
139
|
+
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng' && config.search.provider !== 'brave') {
|
|
143
|
+
issues.push('search fallback duckduckgo is only supported when search provider is searxng or brave');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (config.fetch.fallback === 'http' && config.fetch.provider !== 'firecrawl') {
|
|
147
|
+
issues.push('fetch fallback http is only supported when fetch provider is firecrawl');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (config.search.options?.categories && config.search.options.categories.length === 0) {
|
|
151
|
+
issues.push('search options.categories must contain at least one category when provided');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (config.search.options?.language !== undefined && !config.search.options.language.trim()) {
|
|
155
|
+
issues.push('search options.language must not be empty when provided');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
config.search.options?.safesearch !== undefined &&
|
|
160
|
+
![0, 1, 2].includes(config.search.options.safesearch)
|
|
161
|
+
) {
|
|
162
|
+
issues.push('search options.safesearch must be 0, 1, or 2 when provided');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (config.fetch.options?.formats && config.fetch.options.formats.length === 0) {
|
|
166
|
+
issues.push('fetch options.formats must contain at least one format when provided');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return issues;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function mergeSearchConfig(
|
|
173
|
+
current: SearchBackendConfig,
|
|
174
|
+
override: Partial<SearchBackendConfig> | undefined
|
|
175
|
+
): SearchBackendConfig {
|
|
176
|
+
if (!override) return current;
|
|
177
|
+
if (override.provider && override.provider !== current.provider) {
|
|
178
|
+
return { ...override, provider: override.provider };
|
|
179
|
+
}
|
|
180
|
+
return { ...current, ...override };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mergeFetchConfig(
|
|
184
|
+
current: FetchBackendConfig,
|
|
185
|
+
override: Partial<FetchBackendConfig> | undefined
|
|
186
|
+
): FetchBackendConfig {
|
|
187
|
+
if (!override) return current;
|
|
188
|
+
if (override.provider && override.provider !== current.provider) {
|
|
189
|
+
return { ...override, provider: override.provider };
|
|
190
|
+
}
|
|
191
|
+
return { ...current, ...override };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function mergeBackendConfigLayers(
|
|
195
|
+
...layers: Array<BackendConfig | BackendConfigOverride | undefined>
|
|
196
|
+
): BackendConfig {
|
|
197
|
+
return layers.reduce<BackendConfig>(
|
|
198
|
+
(merged, layer) => ({
|
|
199
|
+
search: mergeSearchConfig(merged.search, layer?.search),
|
|
200
|
+
fetch: mergeFetchConfig(merged.fetch, layer?.fetch),
|
|
201
|
+
headless: { ...merged.headless, ...layer?.headless }
|
|
202
|
+
}),
|
|
203
|
+
DEFAULT_BACKEND_CONFIG
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { BackendConfig, FirecrawlOptions, SearxngOptions } from './config.js';
|
|
2
|
+
import { readBraveKeyFromSettings } from './settings-reader.js';
|
|
3
|
+
|
|
4
|
+
function withTimeout(timeoutMs: number) {
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
7
|
+
return { signal: controller.signal, done: () => clearTimeout(timeout) };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function message(error: unknown) {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function braveDoctorUrl() {
|
|
15
|
+
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
|
16
|
+
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
17
|
+
url.searchParams.set('count', '1');
|
|
18
|
+
return url.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function searxngDoctorUrl(baseUrl: string, options: SearxngOptions = {}) {
|
|
22
|
+
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
23
|
+
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
24
|
+
url.searchParams.set('format', 'json');
|
|
25
|
+
if (options.categories?.length) url.searchParams.set('categories', options.categories.join(','));
|
|
26
|
+
if (options.language) url.searchParams.set('language', options.language);
|
|
27
|
+
if (options.safesearch !== undefined) url.searchParams.set('safesearch', String(options.safesearch));
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function firecrawlDoctorBody(options: FirecrawlOptions = {}) {
|
|
32
|
+
return {
|
|
33
|
+
url: 'https://example.com',
|
|
34
|
+
formats: options.formats ?? ['markdown'],
|
|
35
|
+
...(options.onlyMainContent !== undefined ? { onlyMainContent: options.onlyMainContent } : {})
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function checkBackendHealth(
|
|
40
|
+
config: BackendConfig,
|
|
41
|
+
{
|
|
42
|
+
fetchImpl = fetch,
|
|
43
|
+
timeoutMs = 3_000
|
|
44
|
+
}: {
|
|
45
|
+
fetchImpl?: typeof fetch;
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
} = {}
|
|
48
|
+
): Promise<string[]> {
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
|
|
51
|
+
if (config.search.provider === 'duckduckgo') {
|
|
52
|
+
lines.push('search backend: duckduckgo');
|
|
53
|
+
} else if (config.search.provider === 'brave') {
|
|
54
|
+
const apiKey = process.env.PI_WEB_AGENT_BRAVE_API_KEY ?? readBraveKeyFromSettings();
|
|
55
|
+
if (!apiKey?.trim()) {
|
|
56
|
+
lines.push('search backend: brave warning (missing Brave API key — set webAgent.braveApiKey in settings.json or PI_WEB_AGENT_BRAVE_API_KEY env var)');
|
|
57
|
+
} else {
|
|
58
|
+
const timeout = withTimeout(timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetchImpl(braveDoctorUrl(), {
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: 'application/json',
|
|
63
|
+
'X-Subscription-Token': apiKey
|
|
64
|
+
},
|
|
65
|
+
signal: timeout.signal
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
lines.push(`search backend: brave warning (HTTP ${response.status})`);
|
|
69
|
+
} else {
|
|
70
|
+
const json = (await response.json()) as { web?: { results?: unknown } };
|
|
71
|
+
lines.push(Array.isArray(json.web?.results)
|
|
72
|
+
? 'search backend: brave ok'
|
|
73
|
+
: 'search backend: brave warning (unexpected response)');
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lines.push(`search backend: brave warning (${message(error)})`);
|
|
77
|
+
} finally {
|
|
78
|
+
timeout.done();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else if (!config.search.baseUrl) {
|
|
82
|
+
lines.push('search backend: searxng warning (missing baseUrl)');
|
|
83
|
+
} else {
|
|
84
|
+
const timeout = withTimeout(timeoutMs);
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetchImpl(searxngDoctorUrl(config.search.baseUrl, config.search.options), { signal: timeout.signal });
|
|
87
|
+
const json = (await response.json()) as { results?: unknown };
|
|
88
|
+
lines.push(response.ok && Array.isArray(json.results)
|
|
89
|
+
? 'search backend: searxng ok'
|
|
90
|
+
: 'search backend: searxng warning (unexpected response)');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
lines.push(`search backend: searxng warning (${message(error)})`);
|
|
93
|
+
} finally {
|
|
94
|
+
timeout.done();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (config.search.fallback) {
|
|
99
|
+
lines.push(`search fallback: ${config.search.fallback}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (config.fetch.provider === 'http') {
|
|
103
|
+
lines.push('fetch backend: http');
|
|
104
|
+
} else if (!config.fetch.baseUrl) {
|
|
105
|
+
lines.push('fetch backend: firecrawl warning (missing baseUrl)');
|
|
106
|
+
} else {
|
|
107
|
+
const timeout = withTimeout(timeoutMs);
|
|
108
|
+
try {
|
|
109
|
+
const headers: Record<string, string> = { 'content-type': 'application/json' };
|
|
110
|
+
if (config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY) {
|
|
111
|
+
headers.Authorization = `Bearer ${config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY}`;
|
|
112
|
+
}
|
|
113
|
+
const response = await fetchImpl(new URL('/v1/scrape', config.fetch.baseUrl).toString(), {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers,
|
|
116
|
+
body: JSON.stringify(firecrawlDoctorBody(config.fetch.options)),
|
|
117
|
+
signal: timeout.signal
|
|
118
|
+
});
|
|
119
|
+
lines.push(response.ok ? 'fetch backend: firecrawl ok' : `fetch backend: firecrawl warning (HTTP ${response.status})`);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
lines.push(`fetch backend: firecrawl warning (${message(error)})`);
|
|
122
|
+
} finally {
|
|
123
|
+
timeout.done();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (config.fetch.fallback) {
|
|
128
|
+
lines.push(`fetch fallback: ${config.fetch.fallback}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (config.headless.provider === 'local-browser') {
|
|
132
|
+
lines.push('headless backend: local-browser (managed Chromium fallback configured)');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return lines;
|
|
136
|
+
}
|