@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
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Research Panel Color Utilities
3
+ *
4
+ * Color parsing, conversion, and gradient generation for wave animation.
5
+ */
6
+
7
+ /**
8
+ * Parse ANSI foreground color escape code to extract color index or RGB values.
9
+ *
10
+ * Returns:
11
+ * - { type: '256', index: N } for \x1b[38;5;Nm
12
+ * - { type: 'rgb', r, g, b } for \x1b[38;2;r;g;bm
13
+ * - { type: 'basic', index: N } for \x1b[30-37m
14
+ * - null if parse fails
15
+ */
16
+ export function parseAnsiFgColor(
17
+ ansiCode: string
18
+ ): { type: '256' | 'rgb' | 'basic'; index?: number; r?: number; g?: number; b?: number } | null {
19
+ // Match \x1b[38;5;N... (256-color)
20
+ // eslint-disable-next-line no-control-regex
21
+ const match256 = ansiCode.match(/\x1b\[38;5;(\d+)/);
22
+ if (match256 && match256[1] !== undefined) {
23
+ return { type: '256', index: parseInt(match256[1], 10) };
24
+ }
25
+
26
+ // Match \x1b[38;2;r;g;b... (truecolor)
27
+ // eslint-disable-next-line no-control-regex
28
+ const matchRgb = ansiCode.match(/\x1b\[38;2;(\d+);(\d+);(\d+)/);
29
+ if (matchRgb && matchRgb[1] !== undefined && matchRgb[2] !== undefined && matchRgb[3] !== undefined) {
30
+ return {
31
+ type: 'rgb',
32
+ r: parseInt(matchRgb[1], 10),
33
+ g: parseInt(matchRgb[2], 10),
34
+ b: parseInt(matchRgb[3], 10),
35
+ };
36
+ }
37
+
38
+ // Match basic colors \x1b[30-37...
39
+ // eslint-disable-next-line no-control-regex
40
+ const matchBasic = ansiCode.match(/\x1b\[3([0-7])/);
41
+ if (matchBasic && matchBasic[1] !== undefined) {
42
+ return { type: 'basic', index: parseInt(matchBasic[1], 10) };
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Convert RGB to nearest 256-color cube or grayscale index.
50
+ *
51
+ * Algorithm:
52
+ * 1. Convert RGB to 6x6x6 cube coordinates (0-5 each)
53
+ * 2. Find closest grayscale (232-255)
54
+ * 3. Return whichever is closer (with preference for cube if saturation > threshold)
55
+ */
56
+ export function rgbTo256(r: number, g: number, b: number): number {
57
+ // 6x6x6 cube values
58
+ const CUBE_VALUES = [0, 95, 135, 175, 215, 255];
59
+
60
+ // Find closest cube index
61
+ const findClosest = (value: number) => {
62
+ let minDist = Infinity;
63
+ let minIdx = 0;
64
+ for (let i = 0; i < CUBE_VALUES.length; i++) {
65
+ const cubeValue = CUBE_VALUES[i];
66
+ if (cubeValue === undefined) continue;
67
+ const dist = Math.abs(value - cubeValue);
68
+ if (dist < minDist) {
69
+ minDist = dist;
70
+ minIdx = i;
71
+ }
72
+ }
73
+ return minIdx;
74
+ };
75
+
76
+ const rIdx = findClosest(r);
77
+ const gIdx = findClosest(g);
78
+ const bIdx = findClosest(b);
79
+
80
+ // Cube color
81
+ const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;
82
+ const cubeR = CUBE_VALUES[rIdx] ?? 0;
83
+ const cubeG = CUBE_VALUES[gIdx] ?? 0;
84
+ const cubeB = CUBE_VALUES[bIdx] ?? 0;
85
+
86
+ // Calculate distances
87
+ const colorDistance = (r1: number, g1: number, b1: number, r2: number, g2: number, b2: number) => {
88
+ const dr = r1 - r2;
89
+ const dg = g1 - g2;
90
+ const db = b1 - b2;
91
+ // Weighted Euclidean distance (human eye more sensitive to green)
92
+ return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;
93
+ };
94
+
95
+ const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);
96
+
97
+ // Find closest grayscale (232-255)
98
+ const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
99
+ const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);
100
+ let minGrayDist = Infinity;
101
+ let minGrayIdx = 0;
102
+
103
+ for (let i = 0; i < GRAY_VALUES.length; i++) {
104
+ const dist = Math.abs(gray - (GRAY_VALUES[i] ?? 0));
105
+ if (dist < minGrayDist) {
106
+ minGrayDist = dist;
107
+ minGrayIdx = i;
108
+ }
109
+ }
110
+
111
+ const grayIndex = 232 + minGrayIdx;
112
+ const grayR = GRAY_VALUES[minGrayIdx] ?? 0;
113
+ const grayG = GRAY_VALUES[minGrayIdx] ?? 0;
114
+ const grayB = GRAY_VALUES[minGrayIdx] ?? 0;
115
+ const grayDist = colorDistance(r, g, b, grayR, grayG, grayB);
116
+
117
+ // Prefer cube if not essentially grayscale (low saturation)
118
+ const maxDist = Math.max(cubeDist, grayDist);
119
+ if (maxDist === 0) return cubeIndex;
120
+ const saturation = cubeDist / maxDist;
121
+
122
+ // If very low saturation, prefer grayscale; otherwise prefer cube
123
+ return saturation < 0.15 ? grayIndex : cubeIndex;
124
+ }
125
+
126
+ /**
127
+ * Convert 256-color index to RGB values.
128
+ *
129
+ * @param index - 256-color index (0-255)
130
+ * @returns RGB values or null if index is invalid
131
+ */
132
+ export function indexToRgb(index: number): { r: number; g: number; b: number } | null {
133
+ if (index < 0 || index > 255) return null;
134
+
135
+ // Basic colors (0-7)
136
+ if (index < 8) {
137
+ const BASIC_COLORS: [number, number, number][] = [
138
+ [0, 0, 0], // 0: black
139
+ [128, 0, 0], // 1: red
140
+ [0, 128, 0], // 2: green
141
+ [128, 128, 0], // 3: yellow
142
+ [0, 0, 128], // 4: blue
143
+ [128, 0, 128], // 5: magenta
144
+ [0, 128, 128], // 6: cyan
145
+ [192, 192, 192], // 7: white
146
+ ];
147
+ const color = BASIC_COLORS[index];
148
+ return color ? { r: color[0], g: color[1], b: color[2] } : null;
149
+ }
150
+
151
+ // High-intensity colors (8-15)
152
+ if (index < 16) {
153
+ const HIGH_COLORS: [number, number, number][] = [
154
+ [128, 128, 128], // 8: bright black (gray)
155
+ [255, 0, 0], // 9: bright red
156
+ [0, 255, 0], // 10: bright green
157
+ [255, 255, 0], // 11: bright yellow
158
+ [0, 0, 255], // 12: bright blue
159
+ [255, 0, 255], // 13: bright magenta
160
+ [0, 255, 255], // 14: bright cyan
161
+ [255, 255, 255], // 15: bright white
162
+ ];
163
+ const highIndex = index - 8;
164
+ const color = HIGH_COLORS[highIndex];
165
+ return color ? { r: color[0], g: color[1], b: color[2] } : null;
166
+ }
167
+
168
+ // 6x6x6 color cube (16-231)
169
+ if (index < 232) {
170
+ const cubeIndex = index - 16;
171
+ const rIdx = Math.floor(cubeIndex / 36) % 6;
172
+ const gIdx = Math.floor(cubeIndex / 6) % 6;
173
+ const bIdx = cubeIndex % 6;
174
+
175
+ const CUBE_VALUES = [0, 95, 135, 175, 215, 255];
176
+ return {
177
+ r: CUBE_VALUES[rIdx] ?? 0,
178
+ g: CUBE_VALUES[gIdx] ?? 0,
179
+ b: CUBE_VALUES[bIdx] ?? 0,
180
+ };
181
+ }
182
+
183
+ // Grayscale ramp (232-255)
184
+ const grayIndex = index - 232;
185
+ const grayValue = 8 + grayIndex * 10;
186
+ return { r: grayValue, g: grayValue, b: grayValue };
187
+ }
188
+
189
+ /**
190
+ * Convert RGB to HSL
191
+ */
192
+ export function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
193
+ r /= 255;
194
+ g /= 255;
195
+ b /= 255;
196
+
197
+ const max = Math.max(r, g, b);
198
+ const min = Math.min(r, g, b);
199
+ let h = 0;
200
+ let s = 0;
201
+ const l = (max + min) / 2;
202
+
203
+ if (max !== min) {
204
+ const d = max - min;
205
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
206
+
207
+ switch (max) {
208
+ case r:
209
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
210
+ break;
211
+ case g:
212
+ h = ((b - r) / d + 2) / 6;
213
+ break;
214
+ case b:
215
+ h = ((r - g) / d + 4) / 6;
216
+ break;
217
+ }
218
+ }
219
+
220
+ return { h: h * 360, s, l };
221
+ }
222
+
223
+ /**
224
+ * Convert HSL to RGB
225
+ */
226
+ export function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
227
+ h /= 360;
228
+
229
+ let r: number;
230
+ let g: number;
231
+ let b: number;
232
+
233
+ if (s === 0) {
234
+ r = g = b = l;
235
+ } else {
236
+ const hue2rgb = (p: number, q: number, t: number) => {
237
+ if (t < 0) t += 1;
238
+ if (t > 1) t -= 1;
239
+ if (t < 1/6) return p + (q - p) * 6 * t;
240
+ if (t < 1/2) return q;
241
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
242
+ return p;
243
+ };
244
+
245
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
246
+ const p = 2 * l - q;
247
+ r = hue2rgb(p, q, h + 1/3);
248
+ g = hue2rgb(p, q, h);
249
+ b = hue2rgb(p, q, h - 1/3);
250
+ }
251
+
252
+ return {
253
+ r: Math.round(r * 255),
254
+ g: Math.round(g * 255),
255
+ b: Math.round(b * 255),
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Cycle HSL saturation and lightness for gradient generation.
261
+ *
262
+ * Sawtooth wave: stepIndex=0 → full brightness/saturation → dark, desaturated end.
263
+ * An easeIn curve (power 2.2) spreads frames perceptually: bright drops fast,
264
+ * mid-dark range gets more dwell time.
265
+ */
266
+ export function cycleHslSaturationLightness(
267
+ baseR: number,
268
+ baseG: number,
269
+ baseB: number,
270
+ stepIndex: number,
271
+ totalSteps: number
272
+ ): { r: number; g: number; b: number } {
273
+ const hsl = rgbToHsl(baseR, baseG, baseB);
274
+
275
+ // Sawtooth: progress 0→1 over the cycle, then snaps back
276
+ const progress = (stepIndex % totalSteps) / totalSteps;
277
+ const linearFactor = 1 - progress; // 1.0 (bright) → 0.0 (dark)
278
+
279
+ // easeIn (2.2): fast initial brightness drop, slow approach to dark end —
280
+ // perceptually even spacing across the brightness range
281
+ const curvedFactor = Math.pow(linearFactor, 2.2);
282
+
283
+ // Lightness: 100% of base (bright) → 45% of base (dark)
284
+ const newL = hsl.l * (0.45 + 0.55 * curvedFactor);
285
+ // Saturation: 100% → 30% of base (strongly desaturated at dark end)
286
+ const newS = hsl.s * (0.30 + 0.70 * curvedFactor);
287
+
288
+ return hslToRgb(hsl.h, newS, newL);
289
+ }
290
+
291
+ /**
292
+ * Derive a 256-color gradient from a base color index.
293
+ *
294
+ * This is the main entry point for generating wave animation colors.
295
+ */
296
+ export function derive256Gradient(baseIndex: number, steps: number): string[] {
297
+ const colors: string[] = [];
298
+
299
+ // Parse base color to RGB
300
+ const baseRgb = indexToRgb(baseIndex);
301
+ if (!baseRgb) {
302
+ // Fallback to a simple gradient if base color is invalid
303
+ for (let i = 0; i < steps; i++) {
304
+ colors.push(`\x1b[38;5;${Math.floor((i / steps) * 255)}m`);
305
+ }
306
+ return colors;
307
+ }
308
+
309
+ // Generate gradient by cycling through HSL variations
310
+ for (let i = 0; i < steps; i++) {
311
+ const rgb = cycleHslSaturationLightness(baseRgb.r, baseRgb.g, baseRgb.b, i, steps);
312
+ const index256 = rgbTo256(rgb.r, rgb.g, rgb.b);
313
+ colors.push(`\x1b[38;5;${index256}m`);
314
+ }
315
+
316
+ return colors;
317
+ }
318
+
319
+ /**
320
+ * Derive an RGB color gradient from base RGB values.
321
+ */
322
+ export function deriveRgbGradient(r: number, g: number, b: number, steps: number): string[] {
323
+ const colors: string[] = [];
324
+
325
+ for (let i = 0; i < steps; i++) {
326
+ const rgb = cycleHslSaturationLightness(r, g, b, i, steps);
327
+ colors.push(`\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m`);
328
+ }
329
+
330
+ return colors;
331
+ }
332
+
333
+ // Export internal functions for testing (with _ prefix for internal use)
334
+ export const _rgbTo256 = rgbTo256;
335
+ export const _indexToRgb = indexToRgb;
336
+ export const _rgbToHsl = rgbToHsl;
337
+ export const _hslToRgb = hslToRgb;
338
+ export const _parseAnsiFgColor = parseAnsiFgColor;
339
+ export const _derive256Gradient = derive256Gradient;
340
+ export const _deriveRgbGradient = deriveRgbGradient;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Research Panel Formatting Utilities
3
+ *
4
+ * Functions for formatting tokens, costs, and progress.
5
+ */
6
+
7
+ import type { ResearchProgress } from '../types/research-panel-types.ts';
8
+
9
+ /**
10
+ * Format tokens for display
11
+ */
12
+ export function formatTokens(tokens: number): string {
13
+ if (tokens < 1000) return tokens.toString();
14
+ if (tokens < 10000) return `${(tokens / 1000).toFixed(1)}k`;
15
+ if (tokens < 1_000_000) return `${Math.round(tokens / 1000)}k`;
16
+ return `${(tokens / 1_000_000).toFixed(1)}M`;
17
+ }
18
+
19
+ /**
20
+ * Render a percentage string for the header.
21
+ * Returns empty string when progress data is absent or not yet initialised.
22
+ * Rounds to nearest 10% increment (10, 20, 30, ..., 90, 100).
23
+ */
24
+ export function renderProgressPct(progress: ResearchProgress | undefined): string {
25
+ if (!progress || progress.expected <= 0) return '';
26
+ const pct = Math.min(1, progress.made / progress.expected) * 100;
27
+ const rounded = Math.round(pct / 10) * 10;
28
+ return `${Math.min(100, rounded)}%`;
29
+ }
30
+
31
+ /**
32
+ * Format cost for display
33
+ */
34
+ export function formatCost(cost: number): string {
35
+ if (cost === 0) return '$0.00';
36
+ if (cost < 0.0001) return '<$0.0001';
37
+ if (cost < 0.01) return `$${cost.toFixed(4)}`;
38
+ if (cost < 1) return `$${cost.toFixed(3)}`;
39
+ if (cost < 100) return `$${cost.toFixed(2)}`;
40
+ return `$${Math.round(cost)}`;
41
+ }
42
+
43
+ // Export internal functions for testing
44
+ export function _formatTokens(tokens: number): string {
45
+ return formatTokens(tokens);
46
+ }
47
+
48
+ export function _renderProgressPct(progress: ResearchProgress | undefined): string {
49
+ return renderProgressPct(progress);
50
+ }
51
+
52
+ export function _formatCost(cost: number): string {
53
+ return formatCost(cost);
54
+ }