@selesai/code 0.1.19 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) 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/models.json +143 -39
  26. package/dist/defaults/settings.json +2 -29
  27. package/dist/extensions/package.json +5 -4
  28. package/dist/extensions/pi-intercom/index.ts +12 -5
  29. package/dist/extensions/pi-powerline-footer/index.ts +4 -1
  30. package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
  31. package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
  32. package/dist/extensions/pi-subagents/README.md +12 -5
  33. package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
  34. package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
  35. package/dist/extensions/pi-subagents/package.json +8 -5
  36. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  37. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  38. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
  39. package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
  40. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
  41. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  42. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
  43. package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
  44. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
  45. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
  46. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
  47. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
  48. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
  49. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
  51. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
  52. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
  53. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
  54. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
  56. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
  57. package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
  58. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
  59. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
  60. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
  62. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
  63. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
  64. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  65. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
  66. package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
  68. package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
  69. package/dist/extensions/pi-web-agent/package.json +31 -0
  70. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
  71. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  72. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
  73. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
  74. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
  75. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
  76. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
  77. package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
  78. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
  79. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
  80. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
  81. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
  82. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
  83. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
  84. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
  85. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
  86. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
  87. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
  88. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
  89. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
  90. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
  91. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
  92. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
  93. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
  94. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
  95. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
  96. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
  97. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
  98. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
  99. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
  100. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
  101. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
  102. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
  103. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
  104. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
  105. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
  106. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
  107. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
  108. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
  109. package/dist/extensions/pi-web-agent/src/types.ts +88 -0
  110. package/dist/extensions/ponytail/index.js +189 -0
  111. package/dist/extensions/ponytail/package.json +8 -0
  112. package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
  113. package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
  114. package/dist/extensions/ponytail/test/extension.test.js +167 -0
  115. package/dist/extensions/ponytail/test/helpers.test.js +92 -0
  116. package/dist/extensions/question/constants.ts +38 -0
  117. package/dist/extensions/question/dialog-adapter.ts +74 -0
  118. package/dist/extensions/question/helpers.ts +102 -0
  119. package/dist/extensions/question/index.ts +671 -0
  120. package/dist/extensions/question/navigation.ts +14 -0
  121. package/dist/extensions/question/package.json +18 -0
  122. package/dist/extensions/question/question-list.ts +498 -0
  123. package/dist/extensions/question/row-layout.ts +162 -0
  124. package/dist/extensions/question/schemas.ts +69 -0
  125. package/dist/extensions/question/selection-mode.ts +49 -0
  126. package/dist/extensions/question/shortcuts.ts +53 -0
  127. package/dist/extensions/question/tests/helpers.test.ts +178 -0
  128. package/dist/extensions/question/tests/question-list.test.ts +230 -0
  129. package/dist/extensions/question/tests/row-layout.test.ts +136 -0
  130. package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
  131. package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
  132. package/dist/extensions/question/tui-adapter.ts +33 -0
  133. package/dist/extensions/question/types.ts +102 -0
  134. package/dist/extensions/question/ui-protocol.ts +46 -0
  135. package/dist/extensions/tokenin-onboarding.ts +265 -2
  136. package/dist/extensions/web-agent-onboarding.ts +222 -0
  137. package/dist/extensions/workflow/adapter.ts +588 -0
  138. package/dist/extensions/workflow/extension.ts +24 -0
  139. package/dist/extensions/workflow/modes/prototype.ts +156 -0
  140. package/dist/extensions/workflow/modes/quick.ts +138 -0
  141. package/dist/extensions/workflow/package.json +17 -0
  142. package/dist/extensions/workflow/state-machine.ts +386 -0
  143. package/dist/index.d.ts +1 -1
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +1 -1
  146. package/dist/index.js.map +1 -1
  147. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  148. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  149. package/dist/modes/interactive/interactive-mode.js +57 -13
  150. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  151. package/dist/utils/mime.d.ts.map +1 -1
  152. package/dist/utils/mime.js +24 -1
  153. package/dist/utils/mime.js.map +1 -1
  154. package/docs/docs.json +4 -0
  155. package/docs/workflows.md +206 -0
  156. package/package.json +5 -1
  157. package/README.md +0 -198
@@ -0,0 +1,101 @@
1
+ import type { ResearchSourceKind } from './research-types.js';
2
+
3
+ export type SourceProfileKind =
4
+ | 'official-docs'
5
+ | 'official-api'
6
+ | 'issue-thread'
7
+ | 'forum-thread'
8
+ | 'package-page'
9
+ | 'community';
10
+
11
+ export type SourceProfile = {
12
+ kind: SourceProfileKind;
13
+ sourceKind: ResearchSourceKind;
14
+ shouldPreferHeadlessWhenWeak: boolean;
15
+ };
16
+
17
+ const COMMUNITY_FORUM_HOST_RE = /(^|\.)(community|forum|forums|discuss|discourse)\./;
18
+
19
+ function profile(
20
+ kind: SourceProfileKind,
21
+ sourceKind: ResearchSourceKind,
22
+ shouldPreferHeadlessWhenWeak: boolean
23
+ ): SourceProfile {
24
+ return { kind, sourceKind, shouldPreferHeadlessWhenWeak };
25
+ }
26
+
27
+ function parseUrl(rawUrl: string): URL | undefined {
28
+ try {
29
+ return new URL(rawUrl);
30
+ } catch {
31
+ return undefined;
32
+ }
33
+ }
34
+
35
+ function isOfficialApi(host: string, path: string) {
36
+ return (
37
+ (host === 'playwright.dev' && path.startsWith('/docs/api/')) ||
38
+ (host === 'vitest.dev' && path.startsWith('/config/'))
39
+ );
40
+ }
41
+
42
+ function isOfficialDocs(host: string, path: string) {
43
+ return (
44
+ (host === 'playwright.dev' && path.startsWith('/docs/')) ||
45
+ (host === 'vitest.dev' && path.startsWith('/guide/')) ||
46
+ (host === 'github.com' && path.startsWith('/vitest-dev/vitest/') && path.includes('/docs/')) ||
47
+ host === 'learn.microsoft.com'
48
+ );
49
+ }
50
+
51
+ function isIssueThread(host: string, path: string) {
52
+ return host === 'github.com' && (path.includes('/issues/') || path.includes('/discussions/'));
53
+ }
54
+
55
+ function hasForumThreadPath(path: string) {
56
+ return (
57
+ path.includes('/forum/') ||
58
+ path.includes('/forums/') ||
59
+ path.includes('/t/') ||
60
+ path.includes('/topic/') ||
61
+ path.includes('/threads/')
62
+ );
63
+ }
64
+
65
+ function isForumThread(host: string, path: string) {
66
+ return (
67
+ (host === 'reddit.com' && path.includes('/comments/')) ||
68
+ (host === 'stackoverflow.com' && path.startsWith('/questions/')) ||
69
+ (COMMUNITY_FORUM_HOST_RE.test(`${host}.`) && hasForumThreadPath(path))
70
+ );
71
+ }
72
+
73
+ export function classifySourceProfile(rawUrl: string): SourceProfile {
74
+ const parsed = parseUrl(rawUrl);
75
+ if (!parsed) return profile('community', 'community', false);
76
+
77
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
78
+ const path = parsed.pathname.toLowerCase();
79
+
80
+ if (isOfficialApi(host, path)) {
81
+ return profile('official-api', 'official-api', false);
82
+ }
83
+
84
+ if (isOfficialDocs(host, path)) {
85
+ return profile('official-docs', 'official-docs', false);
86
+ }
87
+
88
+ if (isIssueThread(host, path)) {
89
+ return profile('issue-thread', 'issue-thread', true);
90
+ }
91
+
92
+ if (isForumThread(host, path)) {
93
+ return profile('forum-thread', 'community', true);
94
+ }
95
+
96
+ if (host === 'npmjs.com' && path.startsWith('/package/')) {
97
+ return profile('package-page', 'package-page', false);
98
+ }
99
+
100
+ return profile('community', 'community', false);
101
+ }
@@ -0,0 +1,81 @@
1
+ import type { ResearchEvidence } from './research-types.js';
2
+ import type { EvidenceQualityReport } from './evidence-quality.js';
3
+ import { hasOfficialEvidence, strongEvidenceCount } from './evidence-ranker.js';
4
+
5
+ export type ResearchStepDecision =
6
+ | { action: 'answer' }
7
+ | { action: 'answer-with-caveat' }
8
+ | { action: 'search-again' }
9
+ | { action: 'headless'; url: string };
10
+
11
+ function activeCaveatReasons(evidence: ResearchEvidence[], quality?: EvidenceQualityReport) {
12
+ const reasons = quality?.caveatReasons ?? [];
13
+ if (!hasOfficialDocsAndApi(evidence)) return reasons;
14
+ return reasons.filter((reason) => reason !== 'low-diversity');
15
+ }
16
+
17
+ function hasQualityConcern(evidence: ResearchEvidence[], quality?: EvidenceQualityReport) {
18
+ return activeCaveatReasons(evidence, quality).length > 0;
19
+ }
20
+
21
+ function hasOfficialDocsAndApi(evidence: ResearchEvidence[]) {
22
+ return evidence.some((item) => item.sourceKind === 'official-docs') &&
23
+ evidence.some((item) => item.sourceKind === 'official-api');
24
+ }
25
+
26
+ function shouldSearchForBetterQuality({
27
+ evidence,
28
+ quality,
29
+ passIndex,
30
+ maxPasses
31
+ }: {
32
+ evidence: ResearchEvidence[];
33
+ quality?: EvidenceQualityReport;
34
+ passIndex: number;
35
+ maxPasses: number;
36
+ }) {
37
+ if (!quality) return false;
38
+ if (passIndex + 1 >= maxPasses) return false;
39
+ if (quality.flags.hasOnlyCommunityEvidence) return true;
40
+ if (quality.flags.hasLowDiversity && !hasOfficialDocsAndApi(evidence)) return true;
41
+ return false;
42
+ }
43
+
44
+ export function decideNextResearchStep({
45
+ evidence,
46
+ suggestedHeadlessUrls,
47
+ passIndex,
48
+ maxPasses,
49
+ headlessAttempts,
50
+ maxHeadlessAttempts,
51
+ quality
52
+ }: {
53
+ evidence: ResearchEvidence[];
54
+ suggestedHeadlessUrls: string[];
55
+ passIndex: number;
56
+ maxPasses: number;
57
+ headlessAttempts: number;
58
+ maxHeadlessAttempts: number;
59
+ quality?: EvidenceQualityReport;
60
+ }): ResearchStepDecision {
61
+ const strongEnough = strongEvidenceCount(evidence) >= 2 && hasOfficialEvidence(evidence);
62
+
63
+ if (strongEnough && shouldSearchForBetterQuality({ evidence, quality, passIndex, maxPasses })) {
64
+ return { action: 'search-again' };
65
+ }
66
+
67
+ if (strongEnough) {
68
+ return hasQualityConcern(evidence, quality) ? { action: 'answer-with-caveat' } : { action: 'answer' };
69
+ }
70
+
71
+ const headlessUrl = suggestedHeadlessUrls.find((url) => !url.includes('npmjs.com/package/'));
72
+ if (headlessUrl && headlessAttempts < maxHeadlessAttempts) {
73
+ return { action: 'headless', url: headlessUrl };
74
+ }
75
+
76
+ if (passIndex + 1 < maxPasses) {
77
+ return { action: 'search-again' };
78
+ }
79
+
80
+ return { action: 'answer-with-caveat' };
81
+ }
@@ -0,0 +1,208 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getAgentDir } from '@earendil-works/pi-coding-agent';
4
+ import {
5
+ DEFAULT_BACKEND_CONFIG,
6
+ extractBackendConfigOverride,
7
+ mergeBackendConfigLayers,
8
+ type BackendConfig,
9
+ type BackendConfigFile,
10
+ type BackendConfigOverride
11
+ } from '../backends/config.js';
12
+ import {
13
+ DEFAULT_PRESENTATION_CONFIG,
14
+ extractPresentationConfigOverride,
15
+ mergePresentationConfigLayers
16
+ } from './config.js';
17
+ import type {
18
+ PresentationConfig,
19
+ PresentationConfigFile,
20
+ PresentationConfigOverride,
21
+ PresentationScope
22
+ } from './types.js';
23
+
24
+ export type PresentationConfigStoreOptions = {
25
+ homeDir?: string;
26
+ projectDir?: string;
27
+ };
28
+
29
+ export type PresentationConfigLayer = {
30
+ path: string;
31
+ exists: boolean;
32
+ rawConfig?: PresentationConfigOverride;
33
+ rawBackends?: BackendConfigOverride;
34
+ error?: string;
35
+ };
36
+
37
+ export type LoadedPresentationConfig = {
38
+ global: PresentationConfigLayer;
39
+ project: PresentationConfigLayer;
40
+ effectiveConfig: PresentationConfig;
41
+ effectiveBackends: BackendConfig;
42
+ };
43
+
44
+ export function getPresentationConfigPaths(options: PresentationConfigStoreOptions = {}) {
45
+ // ponytail: use selesai's getAgentDir() so the config dir respects the active
46
+ // CONFIG_DIR_NAME (e.g. .selesai) instead of upstream's hardcoded .pi. Ceiling:
47
+ // if selesai changes the extensions config layout, update here; upgrade path =
48
+ // upstream pi-web-agent accepting a config-dir resolver.
49
+ const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? '';
50
+ const projectDir = options.projectDir ?? process.cwd();
51
+ const agentDir = getAgentDir();
52
+ // agentDir is already <homeDir>/<CONFIG_DIR_NAME>/agent; derive the home-relative config dir.
53
+ const globalConfigRoot = path.join(agentDir, 'extensions', 'pi-web-agent', 'config.json');
54
+
55
+ return {
56
+ globalPath: globalConfigRoot,
57
+ projectPath: path.join(projectDir, '.pi', 'extensions', 'pi-web-agent', 'config.json')
58
+ };
59
+ }
60
+
61
+ type AgentConfigFile = PresentationConfigFile & BackendConfigFile;
62
+
63
+ type LegacyPresentationConfigFile = NonNullable<PresentationConfigFile['presentation']>;
64
+
65
+ function hasPresentationRoot(parsed: AgentConfigFile) {
66
+ return parsed.presentation !== undefined;
67
+ }
68
+
69
+ async function readPresentationConfigFile(filePath: string): Promise<PresentationConfigLayer> {
70
+ try {
71
+ const rawText = await readFile(filePath, 'utf8');
72
+ const parsed = JSON.parse(rawText) as AgentConfigFile;
73
+ const presentationFile: PresentationConfigFile = hasPresentationRoot(parsed)
74
+ ? parsed
75
+ : { presentation: parsed as LegacyPresentationConfigFile };
76
+
77
+ return {
78
+ path: filePath,
79
+ exists: true,
80
+ rawConfig: extractPresentationConfigOverride(presentationFile),
81
+ rawBackends: extractBackendConfigOverride(parsed)
82
+ };
83
+ } catch (error) {
84
+ if ((error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
85
+ return { path: filePath, exists: false };
86
+ }
87
+
88
+ const message = error instanceof Error ? error.message : String(error);
89
+ return {
90
+ path: filePath,
91
+ exists: true,
92
+ error: message
93
+ };
94
+ }
95
+ }
96
+
97
+ function serializePresentationConfigOverride(config: PresentationConfigOverride): PresentationConfigFile {
98
+ const presentation: NonNullable<PresentationConfigFile['presentation']> = {};
99
+
100
+ if (config.defaultMode) {
101
+ presentation.defaultMode = config.defaultMode;
102
+ }
103
+
104
+ if (Object.keys(config.tools).length > 0) {
105
+ presentation.tools = config.tools;
106
+ }
107
+
108
+ return { presentation };
109
+ }
110
+
111
+ function serializeBackendConfigOverride(config: BackendConfigOverride): BackendConfigFile {
112
+ const backends: NonNullable<BackendConfigFile['backends']> = {};
113
+
114
+ if (config.search && Object.keys(config.search).length > 0) {
115
+ backends.search = { ...config.search };
116
+ }
117
+
118
+ if (config.fetch && Object.keys(config.fetch).length > 0) {
119
+ const { apiKey: _apiKey, ...fetch } = config.fetch;
120
+ backends.fetch = { ...fetch };
121
+ }
122
+
123
+ if (config.headless && Object.keys(config.headless).length > 0) {
124
+ backends.headless = { ...config.headless };
125
+ }
126
+
127
+ return { backends };
128
+ }
129
+
130
+ async function readConfigFileForWrite(filePath: string): Promise<AgentConfigFile> {
131
+ try {
132
+ return JSON.parse(await readFile(filePath, 'utf8')) as AgentConfigFile;
133
+ } catch (error) {
134
+ if ((error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
135
+ return {};
136
+ }
137
+
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ async function writeConfigFile(filePath: string, config: AgentConfigFile) {
143
+ await mkdir(path.dirname(filePath), { recursive: true });
144
+ await writeFile(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
145
+ }
146
+
147
+ export async function loadPresentationConfigLayers(
148
+ options: PresentationConfigStoreOptions = {}
149
+ ): Promise<LoadedPresentationConfig> {
150
+ const { globalPath, projectPath } = getPresentationConfigPaths(options);
151
+ const global = await readPresentationConfigFile(globalPath);
152
+ const project = await readPresentationConfigFile(projectPath);
153
+
154
+ return {
155
+ global,
156
+ project,
157
+ effectiveConfig: mergePresentationConfigLayers(
158
+ DEFAULT_PRESENTATION_CONFIG,
159
+ global.rawConfig,
160
+ project.rawConfig
161
+ ),
162
+ effectiveBackends: mergeBackendConfigLayers(
163
+ DEFAULT_BACKEND_CONFIG,
164
+ global.rawBackends,
165
+ project.rawBackends
166
+ )
167
+ };
168
+ }
169
+
170
+ export async function savePresentationConfigScope(
171
+ options: PresentationConfigStoreOptions,
172
+ scope: PresentationScope,
173
+ config: PresentationConfigOverride
174
+ ) {
175
+ const { globalPath, projectPath } = getPresentationConfigPaths(options);
176
+ const filePath = scope === 'global' ? globalPath : projectPath;
177
+ const existing = await readConfigFileForWrite(filePath);
178
+
179
+ await writeConfigFile(filePath, {
180
+ ...existing,
181
+ ...serializePresentationConfigOverride(config)
182
+ });
183
+ }
184
+
185
+ export async function saveBackendConfigScope(
186
+ options: PresentationConfigStoreOptions,
187
+ scope: PresentationScope,
188
+ config: BackendConfigOverride
189
+ ) {
190
+ const { globalPath, projectPath } = getPresentationConfigPaths(options);
191
+ const filePath = scope === 'global' ? globalPath : projectPath;
192
+ const existing = await readConfigFileForWrite(filePath);
193
+
194
+ await writeConfigFile(filePath, {
195
+ ...existing,
196
+ ...serializeBackendConfigOverride(config)
197
+ });
198
+ }
199
+
200
+ export async function resetPresentationConfigScope(
201
+ options: PresentationConfigStoreOptions,
202
+ scope: PresentationScope
203
+ ) {
204
+ const { globalPath, projectPath } = getPresentationConfigPaths(options);
205
+ const filePath = scope === 'global' ? globalPath : projectPath;
206
+
207
+ await rm(filePath, { force: true });
208
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ PRESENTATION_MODES,
3
+ type PresentationConfig,
4
+ type PresentationConfigFile,
5
+ type PresentationConfigOverride,
6
+ type PresentationMode,
7
+ type PresentationToolName
8
+ } from './types.js';
9
+
10
+ const PRESENTATION_MODE_SET = new Set<string>(PRESENTATION_MODES);
11
+
12
+ export const DEFAULT_PRESENTATION_CONFIG: PresentationConfig = {
13
+ defaultMode: 'compact',
14
+ tools: {}
15
+ };
16
+
17
+ export function isPresentationMode(value: unknown): value is PresentationMode {
18
+ return typeof value === 'string' && PRESENTATION_MODE_SET.has(value);
19
+ }
20
+
21
+ export function extractPresentationConfigOverride(
22
+ file: PresentationConfigFile | null | undefined
23
+ ): PresentationConfigOverride {
24
+ const presentation = file?.presentation;
25
+ const tools = Object.fromEntries(
26
+ Object.entries(presentation?.tools ?? {}).flatMap(([toolName, value]) => {
27
+ if (!value || !isPresentationMode(value.mode)) {
28
+ return [];
29
+ }
30
+
31
+ return [[toolName, { mode: value.mode }]];
32
+ })
33
+ ) as PresentationConfig['tools'];
34
+
35
+ return {
36
+ defaultMode: isPresentationMode(presentation?.defaultMode)
37
+ ? presentation.defaultMode
38
+ : undefined,
39
+ tools
40
+ };
41
+ }
42
+
43
+ export function normalizePresentationConfigFile(
44
+ file: PresentationConfigFile | null | undefined
45
+ ): PresentationConfig {
46
+ const override = extractPresentationConfigOverride(file);
47
+
48
+ return {
49
+ defaultMode: override.defaultMode ?? DEFAULT_PRESENTATION_CONFIG.defaultMode,
50
+ tools: override.tools
51
+ };
52
+ }
53
+
54
+ export function mergePresentationConfigLayers(
55
+ defaults: PresentationConfig,
56
+ globalConfig?: PresentationConfigOverride,
57
+ projectConfig?: PresentationConfigOverride
58
+ ): PresentationConfig {
59
+ return {
60
+ defaultMode:
61
+ projectConfig?.defaultMode ?? globalConfig?.defaultMode ?? defaults.defaultMode,
62
+ tools: {
63
+ ...defaults.tools,
64
+ ...globalConfig?.tools,
65
+ ...projectConfig?.tools
66
+ }
67
+ };
68
+ }
69
+
70
+ export function resolvePresentationMode(
71
+ toolName: PresentationToolName,
72
+ config: PresentationConfig = DEFAULT_PRESENTATION_CONFIG
73
+ ): PresentationMode {
74
+ return config.tools[toolName]?.mode ?? config.defaultMode;
75
+ }
@@ -0,0 +1,61 @@
1
+ import type { WebExploreResponse } from '../types.js';
2
+ import type { PresentationEnvelope } from './types.js';
3
+
4
+ function internalReaderLabel(method?: 'http' | 'headless' | 'firecrawl') {
5
+ if (method === 'headless') return 'web_fetch_headless';
6
+ if (method === 'firecrawl') return 'firecrawl';
7
+ if (method === 'http') return 'web_fetch';
8
+ return 'web_explore';
9
+ }
10
+
11
+ export function buildExplorePresentation(result: WebExploreResponse): PresentationEnvelope {
12
+ if (result.status === 'error') {
13
+ return {
14
+ mode: 'compact',
15
+ views: {
16
+ compact: `Research failed: ${result.error?.message ?? 'Unknown research failure.'}`
17
+ }
18
+ };
19
+ }
20
+
21
+ const internalSummary = result.metadata
22
+ ? `Internal research: web_search ×${result.metadata.searchPasses}, web_fetch ×${result.metadata.fetchedPages}, web_fetch_headless ×${result.metadata.headlessAttempts}`
23
+ : undefined;
24
+ const hasEvidence = result.findings.length > 0 || result.sources.length > 0;
25
+ const evidenceLines = hasEvidence
26
+ ? result.findings.map((finding, index) => `- [${internalReaderLabel(result.sources[index]?.method)}] ${finding}`)
27
+ : ['No usable evidence found.'];
28
+ const preview = [
29
+ ...evidenceLines,
30
+ internalSummary ? `\n${internalSummary}` : undefined
31
+ ]
32
+ .filter((line) => line !== undefined)
33
+ .join('\n');
34
+ const verbose = [
35
+ 'Findings',
36
+ ...evidenceLines,
37
+ '',
38
+ 'Sources',
39
+ ...result.sources.map((source) => `- [${internalReaderLabel(source.method)}] ${source.title}: ${source.url}`),
40
+ internalSummary ? `\nInternal tools\n${internalSummary}` : undefined,
41
+ result.caveat ? `\nCaveat\n${result.caveat}` : undefined
42
+ ]
43
+ .filter((line) => line !== undefined)
44
+ .join('\n');
45
+
46
+ return {
47
+ mode: 'compact',
48
+ views: {
49
+ compact: hasEvidence
50
+ ? `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`
51
+ : 'No usable evidence found',
52
+ preview,
53
+ verbose
54
+ },
55
+ metrics: {
56
+ sourceCount: result.sources.length,
57
+ resultCount: result.findings.length
58
+ },
59
+ sources: result.sources
60
+ };
61
+ }
@@ -0,0 +1,54 @@
1
+ import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
2
+ import type { PresentationEnvelope } from './types.js';
3
+
4
+ type FetchLike = WebFetchResponse | WebFetchHeadlessResponse;
5
+
6
+ function countWords(text: string | undefined): number | undefined {
7
+ return text?.trim() ? text.trim().split(/\s+/).length : undefined;
8
+ }
9
+
10
+ function firstExcerpt(text: string | undefined, maxChars = 240): string | undefined {
11
+ if (!text) {
12
+ return undefined;
13
+ }
14
+
15
+ return text.length <= maxChars ? text : `${text.slice(0, maxChars).trimEnd()}…`;
16
+ }
17
+
18
+ export function buildFetchPresentation(result: FetchLike): PresentationEnvelope {
19
+ const wordCount = countWords(result.content?.text);
20
+ const fallbackPrefix = result.metadata.fallbackFrom
21
+ ? `${result.metadata.fallbackFrom} failed; used ${result.metadata.method} fallback. `
22
+ : '';
23
+ const compact =
24
+ result.status === 'ok'
25
+ ? `${fallbackPrefix}Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
26
+ : result.status === 'needs_headless'
27
+ ? `${fallbackPrefix}Needs headless rendering: ${result.error?.message ?? 'Headless rendering recommended.'}`
28
+ : `${fallbackPrefix}Fetch failed: ${result.error?.message ?? 'Unknown fetch failure.'}`;
29
+
30
+ return {
31
+ mode: 'compact',
32
+ views: {
33
+ compact,
34
+ preview: result.content?.title
35
+ ? `${result.content.title}\n${firstExcerpt(result.content.text) ?? ''}`.trim()
36
+ : firstExcerpt(result.content?.text),
37
+ verbose:
38
+ result.status === 'ok'
39
+ ? [
40
+ `URL: ${result.url}`,
41
+ result.content?.title ? `Title: ${result.content.title}` : undefined,
42
+ firstExcerpt(result.content?.text, 500)
43
+ ]
44
+ .filter(Boolean)
45
+ .join('\n')
46
+ : undefined
47
+ },
48
+ metrics: {
49
+ wordCount,
50
+ cacheHit: result.metadata.cacheHit
51
+ },
52
+ sources: [{ title: result.content?.title ?? result.url, url: result.url }]
53
+ };
54
+ }
@@ -0,0 +1,41 @@
1
+ import type { WebSearchResponse } from '../types.js';
2
+ import type { PresentationEnvelope } from './types.js';
3
+
4
+ function formatCompact(result: WebSearchResponse): string {
5
+ const fallbackPrefix = result.metadata.fallbackFrom
6
+ ? `${result.metadata.fallbackFrom} failed; used ${result.metadata.backend} fallback. `
7
+ : '';
8
+
9
+ if (result.status === 'error') {
10
+ return `${fallbackPrefix}Search failed: ${result.error?.message ?? 'Unknown search failure.'}`;
11
+ }
12
+
13
+ const suffix = result.results.length === 1 ? 'result' : 'results';
14
+ return `${fallbackPrefix}Found ${result.results.length} ${suffix}`;
15
+ }
16
+
17
+ export function buildSearchPresentation(result: WebSearchResponse): PresentationEnvelope {
18
+ const preview = result.results
19
+ .slice(0, 3)
20
+ .map((item, index) => `${index + 1}. ${item.title}`)
21
+ .join('\n');
22
+
23
+ const verbose = result.results
24
+ .slice(0, 5)
25
+ .map((item, index) => `${index + 1}. ${item.title}\n ${item.url}\n ${item.snippet}`)
26
+ .join('\n');
27
+
28
+ return {
29
+ mode: 'compact',
30
+ views: {
31
+ compact: formatCompact(result),
32
+ preview: preview || undefined,
33
+ verbose: verbose || undefined
34
+ },
35
+ metrics: {
36
+ resultCount: result.results.length,
37
+ cacheHit: result.metadata.cacheHit
38
+ },
39
+ sources: result.results.map((item) => ({ title: item.title, url: item.url }))
40
+ };
41
+ }
@@ -0,0 +1,20 @@
1
+ import type { PresentationEnvelope, PresentationMode } from './types.js';
2
+
3
+ export function selectPresentationView(
4
+ envelope: PresentationEnvelope | undefined,
5
+ requestedMode: PresentationMode
6
+ ): string | undefined {
7
+ if (!envelope) {
8
+ return undefined;
9
+ }
10
+
11
+ if (requestedMode === 'preview' && envelope.views.preview) {
12
+ return envelope.views.preview;
13
+ }
14
+
15
+ if (requestedMode === 'verbose' && envelope.views.verbose) {
16
+ return envelope.views.verbose;
17
+ }
18
+
19
+ return envelope.views.compact;
20
+ }