@lincoln504/pi-research 0.1.12 → 1.0.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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -115
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1036 -147
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +264 -7
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -4,10 +4,12 @@
4
4
  * Search and retrieve data from Stack Exchange network via REST API v2.3.
5
5
  */
6
6
 
7
- import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@mariozechner/pi-coding-agent';
8
- import { Type } from '@sinclair/typebox';
7
+ import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@earendil-works/pi-coding-agent';
8
+ import { Type, type Static } from 'typebox';
9
+ import { Value } from 'typebox/value';
9
10
  import { stackexchangeCommand } from '../stackexchange/index.ts';
10
11
  import type { ToolUsageTracker } from '../utils/tool-usage-tracker.ts';
12
+ import { getMaxGatheringCalls } from '../constants.ts';
11
13
 
12
14
  export function createStackexchangeTool(options: {
13
15
  ctx: ExtensionContext;
@@ -15,44 +17,56 @@ export function createStackexchangeTool(options: {
15
17
  }): ToolDefinition {
16
18
  const { tracker } = options;
17
19
 
20
+ const StackExchangeParamsSchema = Type.Object({
21
+ command: Type.String({
22
+ description: 'Command: search, get, user, or site',
23
+ }),
24
+ query: Type.Optional(Type.String({
25
+ description: 'Search query (for search command)',
26
+ })),
27
+ id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
28
+ site: Type.Optional(Type.String({
29
+ description: 'Stack Exchange site (default: stackoverflow.com)',
30
+ })),
31
+ limit: Type.Optional(Type.Number({
32
+ description: 'Results count (1-100, default: 10)',
33
+ default: 10,
34
+ minimum: 1,
35
+ maximum: 100,
36
+ })),
37
+ maxPages: Type.Optional(Type.Number({
38
+ description: 'Maximum pages to fetch for pagination (1-10, default: 5)',
39
+ default: 5,
40
+ minimum: 1,
41
+ maximum: 10,
42
+ })),
43
+ format: Type.Optional(Type.String({
44
+ description: 'Output format: table, json, or compact (default: table)',
45
+ default: 'table',
46
+ })),
47
+ tags: Type.Optional(Type.String({
48
+ description: 'Filter by tags (comma-separated)',
49
+ })),
50
+ });
51
+
52
+ type StackExchangeParams = Static<typeof StackExchangeParamsSchema>;
53
+
18
54
  return {
19
55
  name: 'stackexchange',
20
56
  label: 'Stack Exchange Search',
21
- description: 'Search and retrieve data from Stack Exchange network via REST API v2.3 (anonymous: 300 requests/day, with key: 10,000 requests/day)',
57
+ description: 'Search and retrieve data from Stack Exchange network via REST API v2.3 with pagination support (anonymous: 300 requests/day, with key: 10,000 requests/day)',
22
58
  promptSnippet: 'Search Stack Overflow and other Stack Exchange sites for questions, answers, and user information',
23
59
  promptGuidelines: [
24
- 'Use stackexchange to find technical answers on Stack Overflow',
60
+ 'Available for finding technical answers on Stack Overflow',
25
61
  'Great for finding code solutions, debugging help, and best practices',
26
62
  'Works with any Stack Exchange site (Stack Overflow, SuperUser, AskUbuntu, etc.)',
27
63
  'Anonymous access: 300 requests/day. Set STACKEXCHANGE_API_KEY env var for 10,000/day.',
28
64
  'Use tags to filter by specific topics.',
29
- 'CRITICAL: You are allowed a maximum of 4 gathering calls total across ALL tools. Use them for breadth.',
65
+ 'Use maxPages parameter to control pagination for search results (default: 5 pages).',
66
+ `CRITICAL: You are allowed a maximum of ${getMaxGatheringCalls()} gathering calls across your web tools (search, security_search, stackexchange). Use them for breadth.`,
30
67
  ],
31
- parameters: Type.Object({
32
- command: Type.String({
33
- description: 'Command: search, get, user, or site',
34
- }),
35
- query: Type.Optional(Type.String({
36
- description: 'Search query (for search command)',
37
- })),
38
- id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
39
- site: Type.Optional(Type.String({
40
- description: 'Stack Exchange site (default: stackoverflow.com)',
41
- })),
42
- limit: Type.Optional(Type.Number({
43
- description: 'Results count (1-100, default: 10)',
44
- default: 10,
45
- minimum: 1,
46
- maximum: 100,
47
- })),
48
- format: Type.Optional(Type.String({
49
- description: 'Output format: table, json, or compact (default: table)',
50
- default: 'table',
51
- })),
52
- tags: Type.Optional(Type.String({
53
- description: 'Filter by tags (comma-separated)',
54
- })),
55
- }),
68
+ parameters: StackExchangeParamsSchema,
69
+ executionMode: 'parallel',
56
70
  async execute(
57
71
  _toolCallId,
58
72
  params,
@@ -63,12 +77,21 @@ export function createStackexchangeTool(options: {
63
77
  // Record call in tracker - returns false if limit reached
64
78
  const allowed = tracker.recordCall('stackexchange');
65
79
  if (!allowed) {
66
- // THROW to prevent researcher from calling again
67
- throw new Error(tracker.getLimitMessage('stackexchange'));
80
+ return {
81
+ content: [{ type: 'text', text: tracker.getLimitMessage('stackexchange') }],
82
+ details: { blocked: true, reason: 'limit_reached' },
83
+ };
84
+ }
85
+
86
+ if (!Value.Check(StackExchangeParamsSchema, params)) {
87
+ return {
88
+ content: [{ type: 'text', text: 'Invalid parameters for stackexchange tool.' }],
89
+ details: { error: 'invalid_parameters' },
90
+ };
68
91
  }
69
92
 
70
- const paramsRecord = params as Record<string, unknown>;
71
- const command = paramsRecord['command'] as string;
93
+ const p = params as StackExchangeParams;
94
+ const command = p.command;
72
95
 
73
96
  if (!command || typeof command !== 'string') {
74
97
  throw new Error('Stack Exchange command is required and must be a string');
@@ -77,7 +100,7 @@ export function createStackexchangeTool(options: {
77
100
  try {
78
101
  return await stackexchangeCommand({
79
102
  command,
80
- params: paramsRecord,
103
+ params: p as Record<string, unknown>,
81
104
  ctx: extensionCtx,
82
105
  signal,
83
106
  });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * youtube_transcript Tool
3
+ *
4
+ * Fetches transcripts for YouTube videos the researcher selected from its link
5
+ * pool. Used like the scrape tool: the agent picks which YouTube links to read,
6
+ * and this tool reads up to a few of them in one batch.
7
+ *
8
+ * Constraints (enforced here):
9
+ * - One call per researcher (hard limit youtube_transcript: 1 in the tracker).
10
+ * - Up to YOUTUBE_TRANSCRIPT_MAX_VIDEOS (default 3) videos per call.
11
+ *
12
+ * Robustness comes from the backend (PoToken minting, empty-body guard, per-video
13
+ * isolation, circuit breaker, timeouts); this wrapper handles validation, rate
14
+ * limiting, metrics, and never throws to the agent.
15
+ */
16
+
17
+ import type { ToolDefinition, AgentToolResult, ExtensionContext, AgentToolUpdateCallback } from '@earendil-works/pi-coding-agent';
18
+ import { Type, type Static } from 'typebox';
19
+ import { Value } from 'typebox/value';
20
+ import { youtubeTranscriptCommand } from '../youtube/index.ts';
21
+ import type { ToolUsageTracker } from '../utils/tool-usage-tracker.ts';
22
+ import type { SystemResearchState } from '../orchestration/deep-research-types.ts';
23
+ import { cacheScrapedContent, registerScrapedLinks, registerResearcherScrapes } from '../utils/shared-links.ts';
24
+ import { type Config, getConfig } from '../config.ts';
25
+ import { logger } from '../logger.ts';
26
+ import { metrics } from '../utils/metrics.ts';
27
+
28
+ export function createYoutubeTranscriptTool(options: {
29
+ ctx: ExtensionContext;
30
+ tracker?: ToolUsageTracker;
31
+ /** Fires for each video as it completes (success or failure) — used for TUI flash. */
32
+ onUrlScrapeResult?: (url: string, success: boolean) => void;
33
+ /** Provides the research session id for knowledge-store ingestion of transcripts. */
34
+ getGlobalState?: () => SystemResearchState;
35
+ /** Registers transcribed URLs in the global pool (mirrors scrape). */
36
+ updateGlobalLinks?: (links: string[]) => void;
37
+ /** Researcher id for per-researcher source tracking. */
38
+ researcherId?: string;
39
+ config?: Config;
40
+ }): ToolDefinition {
41
+ const { tracker } = options;
42
+ const config = options.config || getConfig(options.ctx.cwd);
43
+ const maxVideos = config.YOUTUBE_TRANSCRIPT_MAX_VIDEOS;
44
+
45
+ const YoutubeParamsSchema = Type.Object({
46
+ urls: Type.Array(
47
+ Type.String({ description: 'YouTube watch/share/shorts/embed links (or bare 11-char video IDs)' }),
48
+ { minItems: 1, maxItems: maxVideos, description: `Up to ${maxVideos} YouTube video links to transcribe` },
49
+ ),
50
+ });
51
+
52
+ type YoutubeParams = Static<typeof YoutubeParamsSchema>;
53
+
54
+ return {
55
+ name: 'youtube_transcript',
56
+ label: 'YouTube Transcripts',
57
+ description:
58
+ `Fetch the transcript (captions) of up to ${maxVideos} YouTube videos in one batch. ` +
59
+ `Use it like the scrape tool, but for YouTube links from your search results: select the ` +
60
+ `most relevant videos and read what they actually say. One call per researcher.`,
61
+ promptSnippet: `Read transcripts of up to ${maxVideos} YouTube videos from your link pool (one call only)`,
62
+ promptGuidelines: [
63
+ `Select up to ${maxVideos} of the most relevant YouTube links from your search results — be judicious, exactly like choosing what to scrape.`,
64
+ 'You may call this tool only ONCE, so choose the videos carefully before calling.',
65
+ 'Pass full watch URLs, youtu.be links, or shorts/embed/live links. Non-YouTube links are ignored.',
66
+ 'Videos without captions, or that are private/age-restricted, are reported as unavailable — the rest still succeed.',
67
+ "When you cite a YouTube source in CITED LINKS, use the video's ACTUAL title and channel (both shown in this tool's output) as the description — e.g. \"'<exact title>' by <channel>\". Do NOT paraphrase or invent a title; the watch URL alone is not human-identifiable.",
68
+ ],
69
+ parameters: YoutubeParamsSchema,
70
+ executionMode: 'parallel',
71
+ async execute(
72
+ _callId: string,
73
+ params: unknown,
74
+ signal: AbortSignal | undefined,
75
+ _onUpdate: AgentToolUpdateCallback<unknown>,
76
+ extensionCtx: ExtensionContext,
77
+ ): Promise<AgentToolResult<unknown>> {
78
+ // Rate-limit enforcement (one call per researcher).
79
+ if (tracker) {
80
+ const allowed = tracker.recordCall('youtube_transcript');
81
+ if (!allowed) {
82
+ metrics.increment('tool_youtube_transcript_calls_total', 1, { status: 'rate_limited' });
83
+ return {
84
+ content: [{ type: 'text', text: tracker.getLimitMessage('youtube_transcript') }],
85
+ details: { blocked: true, reason: 'limit_reached' },
86
+ };
87
+ }
88
+ }
89
+
90
+ if (!Value.Check(YoutubeParamsSchema, params)) {
91
+ metrics.increment('tool_youtube_transcript_calls_total', 1, { status: 'invalid_params' });
92
+ return {
93
+ content: [{ type: 'text', text: `Invalid parameters for youtube_transcript tool. Expected up to ${maxVideos} YouTube URLs.` }],
94
+ details: { error: 'invalid_params' },
95
+ };
96
+ }
97
+
98
+ const p = params as YoutubeParams;
99
+ const urls = p.urls.map((u) => u.trim()).filter((u) => u.length > 0);
100
+ if (urls.length === 0) {
101
+ metrics.increment('tool_youtube_transcript_calls_total', 1, { status: 'no_urls' });
102
+ return {
103
+ content: [{ type: 'text', text: 'No YouTube links provided.' }],
104
+ details: { error: 'no_urls' },
105
+ };
106
+ }
107
+
108
+ // Resolve config against the live cwd for timeout/lang. The video cap stays
109
+ // `maxVideos` (the factory value the schema's maxItems was built from) so the
110
+ // schema gate and the command-level cap can never disagree. The PoToken
111
+ // request key is intentionally not a tool parameter — it is read from the
112
+ // env var by the minter (SDK callers may pass it through the command).
113
+ const liveConfig = options.config || getConfig(extensionCtx.cwd);
114
+
115
+ // Session id for knowledge-store ingestion: caching each transcript under
116
+ // its watch URL lets the orchestrator's post-round writer pick it up as the
117
+ // document `content` and embed the FULL transcript (gated by the writer
118
+ // queue, i.e. only when the knowledge store is enabled). Also feeds in-run
119
+ // dedup so a sibling never re-fetches the same video.
120
+ const researchId = options.getGlobalState?.().researchId;
121
+ const cachedUrls: string[] = [];
122
+
123
+ try {
124
+ const result = await youtubeTranscriptCommand({
125
+ urls,
126
+ maxVideos,
127
+ timeoutMs: liveConfig.YOUTUBE_TRANSCRIPT_TIMEOUT_MS,
128
+ lang: liveConfig.YOUTUBE_TRANSCRIPT_LANG,
129
+ signal,
130
+ onVideoComplete: (videoId, success) =>
131
+ options.onUrlScrapeResult?.(`https://www.youtube.com/watch?v=${videoId}`, success),
132
+ onTranscriptDocument: (url, document) => {
133
+ if (researchId) {
134
+ cacheScrapedContent(researchId, url, document);
135
+ cachedUrls.push(url);
136
+ }
137
+ },
138
+ });
139
+
140
+ if (researchId && cachedUrls.length > 0) {
141
+ registerScrapedLinks(researchId, cachedUrls);
142
+ options.updateGlobalLinks?.(cachedUrls);
143
+ if (options.researcherId) {
144
+ registerResearcherScrapes(researchId, options.researcherId, cachedUrls);
145
+ }
146
+ }
147
+
148
+ return result;
149
+ } catch (error) {
150
+ const errorMsg = error instanceof Error ? error.message : String(error);
151
+ logger.error(`[youtube_transcript tool] Failed: ${errorMsg}`);
152
+ metrics.increment('tool_youtube_transcript_calls_total', 1, { status: 'error' });
153
+ return {
154
+ content: [{
155
+ type: 'text',
156
+ text: `# YouTube Transcript Failed\n\n**Error:** ${errorMsg}\n\nThis may be due to network issues or YouTube bot-protection. Continue with the rest of your research.`,
157
+ }],
158
+ details: { error: errorMsg },
159
+ };
160
+ }
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Knowledge Search TUI Panel
3
+ *
4
+ * A minimal, static bordered box displayed while the knowledge store
5
+ * search tool is executing. One line of text inside a box, no header,
6
+ * no animations — just "searching knowledge store" in accent-colored borders.
7
+ *
8
+ * Visual layout (width = terminal width):
9
+ *
10
+ * ┌──────────────────────────────────────────┐
11
+ * │ searching knowledge store │
12
+ * └──────────────────────────────────────────┘
13
+ *
14
+ * Design principles:
15
+ * - Same architectural pattern as the research panel (widget factory,
16
+ * Component interface, theme usage)
17
+ * - No header line above the box (unlike the research panel)
18
+ * - 3 rows total (top border + content + bottom border) vs research panel's 4+
19
+ * - Static text, no wave animation or progress tracking
20
+ * - Message is horizontally centered within the box
21
+ * - Narrow terminals are handled the same way as the rest of the project:
22
+ * the message is fit with truncateToWidth (ellipsis) and measured with
23
+ * visibleWidth, so multibyte/zero-width content can never overflow the border
24
+ * - Safe rendering: never crashes, graceful fallback on error
25
+ */
26
+
27
+ import { type Component, visibleWidth, truncateToWidth } from '@earendil-works/pi-tui';
28
+ import type { Theme } from '../types/research-panel-types.ts';
29
+
30
+ /**
31
+ * The static message displayed while the knowledge store is being searched.
32
+ */
33
+ const SEARCHING_MESSAGE = 'searching knowledge store';
34
+
35
+ /**
36
+ * Create the knowledge search panel component.
37
+ *
38
+ * Returns a Component factory compatible with pi's setWidget() API.
39
+ * The component renders a simple bordered box with the searching message.
40
+ */
41
+ export function createKnowledgeSearchPanel(): (tui: unknown, theme: Theme) => Component {
42
+ return (_tui: unknown, theme: Theme) => {
43
+ const component: Component = {
44
+ render(width: number): string[] {
45
+ if (width < 4) return [];
46
+
47
+ const innerWidth = Math.max(0, width - 2); // subtract left + right border
48
+
49
+ // Fit the message to the inner width, adding an ellipsis when too
50
+ // narrow — the same truncation helper the research panel uses. Measure
51
+ // with visibleWidth so the padding maths is correct even if the message
52
+ // ever contains wide/zero-width characters.
53
+ const displayText = truncateToWidth(SEARCHING_MESSAGE, innerWidth);
54
+ const textWidth = visibleWidth(displayText);
55
+
56
+ // Center horizontally within the box.
57
+ const totalPad = Math.max(0, innerWidth - textWidth);
58
+ const leftPad = Math.floor(totalPad / 2);
59
+ const rightPad = totalPad - leftPad;
60
+ const paddedContent = ' '.repeat(leftPad) + displayText + ' '.repeat(rightPad);
61
+
62
+ // Row 0: top border ┌───...───┐
63
+ const topBorder = theme.fg('accent', '┌' + '─'.repeat(innerWidth) + '┐');
64
+
65
+ // Row 1: content │ text │
66
+ const contentLine = theme.fg('accent', '│') +
67
+ theme.fg('muted', paddedContent) +
68
+ theme.fg('accent', '│');
69
+
70
+ // Row 2: bottom border └───...───┘
71
+ const bottomBorder = theme.fg('accent', '└' + '─'.repeat(innerWidth) + '┘');
72
+
73
+ return [topBorder, contentLine, bottomBorder];
74
+ },
75
+
76
+ invalidate(): void {
77
+ const comp = component as Component & { parent?: { invalidate: () => void } };
78
+ if (comp.parent && typeof comp.parent.invalidate === 'function') {
79
+ comp.parent.invalidate();
80
+ }
81
+ },
82
+ };
83
+
84
+ return component;
85
+ };
86
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * TUI Animation Pulse
3
+ *
4
+ * Centralized clock for TUI animations to ensure visual synchronization
5
+ * across multiple widgets and panels.
6
+ */
7
+
8
+ import { logger } from '../logger.ts';
9
+
10
+ // ~30 FPS. Deliberately kept ABOVE pi-tui's render cap
11
+ // (MIN_RENDER_INTERVAL_MS = 16). Matching the cap (16ms) makes the pulse and
12
+ // the renderer beat against each other — render delay is max(0, 16 - elapsed),
13
+ // so two timers of the same period produce irregular spacing (visible jitter)
14
+ // for no gain: the wave head is one discrete cell wide, so it only steps a cell
15
+ // roughly every other 60fps frame anyway. 33ms gives a steady, even cadence
16
+ // where every pulse renders once. The real smoothness fix is renderImmediate()
17
+ // in the observer (bypassing the 100ms state-refresh debounce), not raw FPS.
18
+ const FRAME_INTERVAL_MS = 33;
19
+
20
+ interface PulseSubscriber {
21
+ (frame: number): void;
22
+ }
23
+
24
+ class TuiPulse {
25
+ private frame = 0;
26
+ private timer: NodeJS.Timeout | null = null;
27
+ private subscribers = new Set<PulseSubscriber>();
28
+
29
+ /**
30
+ * Start the pulse if not already running
31
+ */
32
+ public start(): void {
33
+ if (this.timer) return;
34
+
35
+ logger.debug('[Pulse] Starting global TUI pulse');
36
+ this.timer = setInterval(() => {
37
+ this.frame++;
38
+ for (const subscriber of this.subscribers) {
39
+ try {
40
+ subscriber(this.frame);
41
+ } catch (err) {
42
+ logger.error('[Pulse] Error in subscriber:', err);
43
+ }
44
+ }
45
+ }, FRAME_INTERVAL_MS);
46
+
47
+ if (this.timer.unref) {
48
+ this.timer.unref();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Stop the pulse if no subscribers remain
54
+ */
55
+ public stop(): void {
56
+ if (this.timer && this.subscribers.size === 0) {
57
+ logger.debug('[Pulse] Stopping global TUI pulse (no subscribers)');
58
+ clearInterval(this.timer);
59
+ this.timer = null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Subscribe to the pulse. Returns an unsubscribe function.
65
+ */
66
+ public subscribe(callback: PulseSubscriber): () => void {
67
+ this.subscribers.add(callback);
68
+ this.start();
69
+
70
+ return () => {
71
+ this.subscribers.delete(callback);
72
+ if (this.subscribers.size === 0) {
73
+ this.stop();
74
+ }
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Get the current global frame count
80
+ */
81
+ public getFrame(): number {
82
+ return this.frame;
83
+ }
84
+ }
85
+
86
+ export const tuiPulse = new TuiPulse();
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Research Health Helpers
3
+ *
4
+ * Helper functions for health checking during research
5
+ */
6
+
7
+ import { runHealthCheck, healthRegistry, isBusyPoolHealthFailure } from '../healthcheck/index.ts';
8
+ import { logger } from '../logger.ts';
9
+ import { safeUnref } from '../utils/safe-unref.ts';
10
+ import {
11
+ addSlice,
12
+ activateSlice,
13
+ removeSlice,
14
+ type ResearchPanelState,
15
+ } from './research-panel.ts';
16
+
17
+ export interface HealthCheckContext {
18
+ panelState: ResearchPanelState;
19
+ onUpdate: () => void;
20
+ }
21
+
22
+ export interface HealthCheckResult {
23
+ success: boolean;
24
+ error?: string;
25
+ }
26
+
27
+ /**
28
+ * Ensure functional health before starting research
29
+ * Displays a health check slice and runs verification
30
+ */
31
+ export async function ensureFunctionalHealth(
32
+ ctx: HealthCheckContext
33
+ ): Promise<void> {
34
+ const { panelState, onUpdate } = ctx;
35
+
36
+ const sliceLabel = 'health check ...';
37
+ addSlice(panelState, sliceLabel, sliceLabel, false);
38
+ activateSlice(panelState, sliceLabel);
39
+ onUpdate();
40
+
41
+ try {
42
+ const health = await runHealthCheck();
43
+ if (!health.success) {
44
+ // Distinguish a DEAD/uninitialized pool (must abort) from one that is merely
45
+ // BUSY: under concurrent load the BrowserRuntime readiness probe queues behind
46
+ // in-flight scrapes and times out even though the pool is operational. Mirror
47
+ // the quick orchestrator and proceed-with-warning rather than aborting — the
48
+ // per-task timeouts bound the work. Without this the TUI aborts the exact
49
+ // contention case formatHealthError now (correctly) describes as non-fatal.
50
+ if (isBusyPoolHealthFailure(health)) {
51
+ logger.warn('[ResearchHealth] Browser pool busy — readiness probe queued out under load, but the pool is operational. Proceeding; per-task timeouts bound the work.');
52
+ } else {
53
+ const raw = health.error || '';
54
+ const msg = formatHealthError(raw);
55
+ throw new Error(msg);
56
+ }
57
+ }
58
+ } finally {
59
+ removeSlice(panelState, sliceLabel);
60
+ onUpdate();
61
+ }
62
+ }
63
+
64
+
65
+ /**
66
+ * Format health check error into user-friendly message
67
+ */
68
+ export function formatHealthError(raw: string): string {
69
+ if (raw.includes('Xvfb') || raw.includes('xvfb') || raw.includes('virtual display') || raw.includes('display server') || raw.includes('DISPLAY not set')) {
70
+ return 'No display server found on Linux. Install Xvfb for TTY/Wayland use: sudo apt install xvfb';
71
+ } else if (raw.includes('not found') || raw.includes('not installed') || raw.includes('binaries')) {
72
+ return 'Browser engine not installed. Run `npx camoufox-js fetch` to install it.';
73
+ } else if (raw.includes('Timeout') || raw.includes('timeout') || raw.includes('timed out')) {
74
+ // The readiness probes check browser/state/knowledge availability — they do NOT perform a
75
+ // web search, so a timeout here is NOT the search engine blocking us. It is almost always
76
+ // slow browser startup, resource contention (several research runs sharing the pool/GPU at
77
+ // once, which makes these probes time out together), or a teardown race when a run is
78
+ // cancelled mid-flight. Keep this message honest so a contention timeout is not misread as
79
+ // bot-blocking.
80
+ return 'A readiness check timed out before the browser/services were ready. This is usually slow browser startup, resource contention (e.g. multiple research runs at once sharing the pool), or a cancelled run. It is not a sign the search engine is refusing automated traffic — these readiness probes do not run a search. Retry; if it persists, reduce concurrency and check system resources.';
81
+ } else if (raw.includes('net::ERR') || raw.includes('ECONNREFUSED') || raw.includes('ENOTFOUND')) {
82
+ return 'The browser hit a network error reaching the web. Check your internet connection.';
83
+ } else {
84
+ return `Browser readiness check failed. Enable debug logging (PI_RESEARCH_DEBUG=true) for details. (${raw || 'unknown error'})`;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Periodic health monitoring for long-running research
90
+ */
91
+ export interface HealthMonitor {
92
+ start: () => void;
93
+ stop: () => void;
94
+ }
95
+
96
+ export function createHealthMonitor(): HealthMonitor {
97
+ let timer: NodeJS.Timeout | null = null;
98
+
99
+ const start = () => {
100
+ if (timer) return;
101
+
102
+ timer = setInterval(async () => {
103
+ try {
104
+ const health = await healthRegistry.runAll();
105
+ const failedNonCritical = health.components.filter(c => !c.healthy && !healthRegistry.isCritical(c.component));
106
+ if (failedNonCritical.length > 0) {
107
+ logger.warn(`[research] Periodic health check: non-critical components degraded: ${failedNonCritical.map(c => c.component).join(', ')}`);
108
+ }
109
+ } catch (error) {
110
+ logger.debug('[research] Periodic health check failed (non-blocking):', error);
111
+ }
112
+ }, 30000); // Every 30 seconds
113
+ safeUnref(timer);
114
+ };
115
+
116
+ const stop = () => {
117
+ if (timer) {
118
+ clearInterval(timer);
119
+ timer = null;
120
+ }
121
+ };
122
+
123
+ return { start, stop };
124
+ }