@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.
Files changed (156) hide show
  1. package/dist/agents/architect.md +4 -3
  2. package/dist/agents/builder.md +5 -4
  3. package/dist/agents/commentator.md +3 -2
  4. package/dist/agents/explorer.md +4 -4
  5. package/dist/agents/recapper.md +4 -3
  6. package/dist/config.d.ts +6 -7
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +7 -9
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +3 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts.map +1 -1
  14. package/dist/core/compaction/compaction.js +18 -24
  15. package/dist/core/compaction/compaction.js.map +1 -1
  16. package/dist/core/extensions/loader.d.ts.map +1 -1
  17. package/dist/core/extensions/loader.js +2 -0
  18. package/dist/core/extensions/loader.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +8 -1
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/skills.d.ts.map +1 -1
  23. package/dist/core/skills.js +25 -11
  24. package/dist/core/skills.js.map +1 -1
  25. package/dist/defaults/settings.json +2 -29
  26. package/dist/extensions/package.json +5 -4
  27. package/dist/extensions/pi-intercom/index.ts +12 -5
  28. package/dist/extensions/pi-powerline-footer/index.ts +4 -1
  29. package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
  30. package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
  31. package/dist/extensions/pi-subagents/README.md +12 -5
  32. package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
  33. package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
  34. package/dist/extensions/pi-subagents/package.json +8 -5
  35. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  36. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  37. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
  38. package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
  39. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
  40. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  41. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
  42. package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
  43. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
  44. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
  45. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
  46. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
  47. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
  48. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
  49. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
  51. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
  52. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
  53. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
  56. package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
  57. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
  59. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
  61. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
  62. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
  63. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  64. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
  65. package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
  66. package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
  67. package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
  68. package/dist/extensions/pi-web-agent/package.json +31 -0
  69. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
  70. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  71. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
  72. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
  73. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
  74. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
  75. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
  76. package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
  77. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
  78. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
  79. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
  80. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
  81. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
  82. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
  83. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
  84. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
  85. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
  86. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
  87. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
  88. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
  89. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
  90. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
  91. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
  92. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
  93. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
  94. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
  95. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
  96. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
  97. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
  98. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
  99. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
  100. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
  101. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
  102. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
  103. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
  104. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
  105. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
  106. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
  107. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
  108. package/dist/extensions/pi-web-agent/src/types.ts +88 -0
  109. package/dist/extensions/ponytail/index.js +189 -0
  110. package/dist/extensions/ponytail/package.json +8 -0
  111. package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
  112. package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
  113. package/dist/extensions/ponytail/test/extension.test.js +167 -0
  114. package/dist/extensions/ponytail/test/helpers.test.js +92 -0
  115. package/dist/extensions/question/constants.ts +38 -0
  116. package/dist/extensions/question/dialog-adapter.ts +74 -0
  117. package/dist/extensions/question/helpers.ts +102 -0
  118. package/dist/extensions/question/index.ts +671 -0
  119. package/dist/extensions/question/navigation.ts +14 -0
  120. package/dist/extensions/question/package.json +18 -0
  121. package/dist/extensions/question/question-list.ts +498 -0
  122. package/dist/extensions/question/row-layout.ts +162 -0
  123. package/dist/extensions/question/schemas.ts +69 -0
  124. package/dist/extensions/question/selection-mode.ts +49 -0
  125. package/dist/extensions/question/shortcuts.ts +53 -0
  126. package/dist/extensions/question/tests/helpers.test.ts +178 -0
  127. package/dist/extensions/question/tests/question-list.test.ts +230 -0
  128. package/dist/extensions/question/tests/row-layout.test.ts +136 -0
  129. package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
  130. package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
  131. package/dist/extensions/question/tui-adapter.ts +33 -0
  132. package/dist/extensions/question/types.ts +102 -0
  133. package/dist/extensions/question/ui-protocol.ts +46 -0
  134. package/dist/extensions/tokenin-onboarding.ts +265 -2
  135. package/dist/extensions/web-agent-onboarding.ts +222 -0
  136. package/dist/extensions/workflow/adapter.ts +588 -0
  137. package/dist/extensions/workflow/extension.ts +24 -0
  138. package/dist/extensions/workflow/modes/prototype.ts +156 -0
  139. package/dist/extensions/workflow/modes/quick.ts +138 -0
  140. package/dist/extensions/workflow/package.json +17 -0
  141. package/dist/extensions/workflow/state-machine.ts +386 -0
  142. package/dist/index.d.ts +1 -1
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +1 -1
  145. package/dist/index.js.map +1 -1
  146. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  147. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  148. package/dist/modes/interactive/interactive-mode.js +57 -13
  149. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  150. package/dist/utils/mime.d.ts.map +1 -1
  151. package/dist/utils/mime.js +24 -1
  152. package/dist/utils/mime.js.map +1 -1
  153. package/docs/docs.json +4 -0
  154. package/docs/workflows.md +206 -0
  155. package/package.json +5 -1
  156. 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 "@earendil-works/pi-coding-agent";
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.PI_SUBAGENT_MAX_DEPTH)
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.PI_SUBAGENT_DEPTH ?? "0");
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.PI_SUBAGENT_DEPTH ?? "0");
1004
+ const parentDepth = Number(process.env.SELESAI_SUBAGENT_DEPTH ?? "0");
1005
1005
  const nextDepth = Number.isFinite(parentDepth) ? parentDepth + 1 : 1;
1006
1006
  return {
1007
- PI_SUBAGENT_DEPTH: String(nextDepth),
1008
- PI_SUBAGENT_MAX_DEPTH: String(normalizeMaxSubagentDepth(maxDepth) ?? resolveCurrentMaxSubagentDepth()),
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 "@earendil-works/pi-coding-agent";
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 = ".pi";
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.PI_CODING_AGENT_DIR;
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 "@earendil-works/pi-coding-agent";
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 "@earendil-works/pi-coding-agent";
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 {
@@ -1,4 +1,4 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
1
+ import type { Theme } from "@selesai/code";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
 
4
4
  function fuzzyScore(query: string, text: string): number {
@@ -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 "@earendil-works/pi-coding-agent";
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
+ }