@lincoln504/pi-research 0.1.13 → 1.0.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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -138
  3. package/dist/cli.mjs +23375 -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 +130 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +211 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +122 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +66 -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 +1195 -0
  30. package/src/config.ts +1032 -155
  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 +258 -11
  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 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  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,407 @@
1
+ /**
2
+ * Stderr/Stdout Capture
3
+ *
4
+ * Captures stderr/stdout during task execution to redirect native logs.
5
+ */
6
+
7
+ // Use createRequire to get a mutable reference to 'fs'.
8
+ // ESM namespace imports (import * as fs from 'node:fs') create bindings that are
9
+ // writable at Node.js runtime but flagged as immutable by esbuild's static analysis.
10
+ // Since this module needs to monkey-patch fs.writeSync to capture native addon output
11
+ // on FD 1/2, we use createRequire — the official Node.js CJS↔ESM interop mechanism
12
+ // — to obtain a fully mutable module object.
13
+ import { createRequire } from 'node:module';
14
+ import type * as FsType from 'node:fs';
15
+ import type { Stats } from 'node:fs';
16
+ import { TextDecoder } from 'node:util';
17
+ import type { LogContext } from './log-utils.ts';
18
+ import { getLogContext, formatArg, safeJsonStringify, redactSecrets } from './log-utils.ts';
19
+
20
+ const fs: typeof FsType = createRequire(import.meta.url)('node:fs');
21
+
22
+ /**
23
+ * Session-scoped capture flags to support concurrent research runs.
24
+ * Maps session IDs to capture state, with a global flag as fallback.
25
+ */
26
+ const sessionCaptureStates = new Map<string, boolean>();
27
+ // Active UNKEYED captures (captureStdio called without a sessionId — e.g. the outer
28
+ // research run's runCapturingStderr). Tracked as a depth counter separate from the keyed
29
+ // map so that ending a keyed capture (which recomputes the global flag) cannot clear the
30
+ // flag while an unkeyed capture is still active — otherwise a later capture would treat
31
+ // stdout/stderr as un-patched, save the still-patched writers as "originals", and on a
32
+ // LIFO-violating restore leave them permanently diverted.
33
+ let unkeyedCaptureDepth = 0;
34
+ let isAnyLoggerCapturingOutput = false;
35
+
36
+ /**
37
+ * Recompute the global "is anything capturing" flag from BOTH the keyed sessions and
38
+ * the unkeyed-capture depth, so neither scheme can clobber the other's state.
39
+ */
40
+ function recomputeGlobalCapturing(): void {
41
+ isAnyLoggerCapturingOutput =
42
+ unkeyedCaptureDepth > 0 || Array.from(sessionCaptureStates.values()).some(v => v);
43
+ }
44
+
45
+ /**
46
+ * Check if a session is currently capturing output
47
+ */
48
+ export function isSessionCapturing(sessionId?: string): boolean {
49
+ if (sessionId) {
50
+ return sessionCaptureStates.get(sessionId) ?? false;
51
+ }
52
+ return isAnyLoggerCapturingOutput;
53
+ }
54
+
55
+ /**
56
+ * Mark a session as capturing or not capturing
57
+ */
58
+ export function setSessionCapturing(sessionId: string, capturing: boolean): void {
59
+ sessionCaptureStates.set(sessionId, capturing);
60
+ if (capturing) {
61
+ isAnyLoggerCapturingOutput = true;
62
+ } else {
63
+ // Recompute from all sessions AND active unkeyed captures.
64
+ recomputeGlobalCapturing();
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Native log patterns that should be captured
70
+ */
71
+ const NATIVE_LOG_PATTERNS = [
72
+ 'Warning:',
73
+ 'Error:',
74
+ 'Dawn',
75
+ 'ONNX',
76
+ 'ort',
77
+ 'maxDynamicStorageBuffersPerPipelineLayout',
78
+ 'maxComputeWorkgroupStorageSize',
79
+ 'allocation limit',
80
+ 'artificially',
81
+ 'reduced from',
82
+ 'dynamic offset allocation limit',
83
+ ];
84
+
85
+ /**
86
+ * Complex TUI ANSI escape sequences
87
+ */
88
+ const COMPLEX_TUI_PATTERNS = [
89
+ '\x1b[H', // Home
90
+ '\x1b[2J', // Clear screen
91
+ '\x1b[J', // Clear to end
92
+ '\x1b[K', // Clear line
93
+ '\x1b[?25', // Cursor visibility
94
+ '\x1b[?1000', // Mouse tracking
95
+ '\x1b[?1002',
96
+ '\x1b[?1003',
97
+ '\x1b[?1006',
98
+ '\x1b[?2004', // Bracketed paste
99
+ '\x1b[<u', // Kitty protocol
100
+ '\x1b[>4;0m', // xterm modifyOtherKeys
101
+ ];
102
+
103
+ /**
104
+ * Box-drawing characters - definitive TUI marker
105
+ */
106
+ const BOX_DRAWING_CHARS = /[─│┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬╴╵╶╷╭╮╯╰╱╲╳]/;
107
+
108
+ /**
109
+ * Check if a message contains a complex TUI escape sequence
110
+ */
111
+ function isComplexTui(message: string): boolean {
112
+ if (!message.includes('\x1b[')) {
113
+ return false;
114
+ }
115
+
116
+ for (const pattern of COMPLEX_TUI_PATTERNS) {
117
+ if (message.includes(pattern)) {
118
+ return true;
119
+ }
120
+ }
121
+
122
+ // Check for cursor position patterns (ANSI ESC sequences — control chars intentional)
123
+ // eslint-disable-next-line no-control-regex
124
+ if (/\u001b\[\d+;\d+H/.test(message) || /\u001b\[\d+[ABCD]/.test(message)) {
125
+ return true;
126
+ }
127
+
128
+ return false;
129
+ }
130
+
131
+ /**
132
+ * Check if a message is a native log (should be captured)
133
+ */
134
+ function isNativeLog(message: string): boolean {
135
+ return NATIVE_LOG_PATTERNS.some(pattern => message.includes(pattern));
136
+ }
137
+
138
+ /**
139
+ * Check if a message is box drawing (TUI)
140
+ */
141
+ function isBoxDrawing(message: string): boolean {
142
+ return BOX_DRAWING_CHARS.test(message);
143
+ }
144
+
145
+ /**
146
+ * Check if stderr should be redirected
147
+ */
148
+ function shouldRedirectStderr(stat: Stats): boolean {
149
+ // Only redirect when FD 2 is a tty or regular file, not a pipe/socket (MCP mode)
150
+ return !stat.isFIFO() && !stat.isSocket();
151
+ }
152
+
153
+ /**
154
+ * Create a console patch function
155
+ */
156
+ function createConsolePatch(level: string, logFile: string, hasSufficientDiskSpace: () => boolean) {
157
+ return (...args: unknown[]) => {
158
+ // Check disk space before writing
159
+ if (!hasSufficientDiskSpace()) {
160
+ return;
161
+ }
162
+
163
+ const timestamp = new Date().toISOString();
164
+ const entry: LogContext = {
165
+ timestamp,
166
+ level: `CONSOLE_${level.toUpperCase()}`,
167
+ ...getLogContext(),
168
+ // Redact secrets + bound size, matching Logger.emit(); captured console
169
+ // output can echo auth headers / tokens from dependencies.
170
+ message: redactSecrets(args.map(formatArg).join(' ')),
171
+ };
172
+ try {
173
+ fs.appendFileSync(logFile, `${safeJsonStringify(entry)}\n`);
174
+ } catch { /* ignore */ }
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Run a task while capturing its stderr and stdout
180
+ */
181
+ export async function captureStdio<T>(
182
+ logFile: string,
183
+ hasSufficientDiskSpace: () => boolean,
184
+ task: () => Promise<T>,
185
+ sessionId?: string
186
+ ): Promise<T> {
187
+ // Check if this specific session or any session is already capturing
188
+ const sessionCapturing = sessionId ? isSessionCapturing(sessionId) : false;
189
+ if (!logFile || sessionCapturing || (!sessionId && isAnyLoggerCapturingOutput)) {
190
+ return await task();
191
+ }
192
+
193
+ if (sessionId) {
194
+ setSessionCapturing(sessionId, true);
195
+ } else {
196
+ unkeyedCaptureDepth++;
197
+ isAnyLoggerCapturingOutput = true;
198
+ }
199
+
200
+ const decoder = new TextDecoder();
201
+
202
+ // Save originals
203
+ const originalStderrWrite = process.stderr.write;
204
+ const originalStdoutWrite = process.stdout.write;
205
+ const originalFsWriteSync = fs.writeSync;
206
+ const originalConsole = {
207
+ log: console.log,
208
+ info: console.info,
209
+ warn: console.warn,
210
+ error: console.error,
211
+ debug: console.debug,
212
+ };
213
+
214
+ // Redirect FD 2 at the OS level (Linux + macOS only)
215
+ let savedFd2: number = -1;
216
+ let fd2Redirected = false;
217
+ if (process.platform !== 'win32') {
218
+ try {
219
+ const stat = fs.fstatSync(2);
220
+ if (shouldRedirectStderr(stat)) {
221
+ savedFd2 = fs.openSync('/dev/fd/2', 'a');
222
+ fs.closeSync(2);
223
+ const newFd = fs.openSync(logFile, 'a');
224
+ if (newFd === 2) {
225
+ fd2Redirected = true;
226
+ } else {
227
+ // Didn't get FD 2 — undo
228
+ if (newFd >= 0) fs.closeSync(newFd);
229
+ try {
230
+ const r = fs.openSync(`/dev/fd/${savedFd2}`, 'a');
231
+ if (r !== 2 && r >= 0) fs.closeSync(r);
232
+ } catch { /* best-effort restore */ }
233
+ try { fs.closeSync(savedFd2); } catch { /* ignore */ }
234
+ savedFd2 = -1;
235
+ }
236
+ }
237
+ } catch { /* fstat failed or /dev/fd unavailable */ }
238
+ }
239
+
240
+ // Patch console methods
241
+ console.log = createConsolePatch('log', logFile, hasSufficientDiskSpace);
242
+ console.info = createConsolePatch('info', logFile, hasSufficientDiskSpace);
243
+ console.warn = createConsolePatch('warn', logFile, hasSufficientDiskSpace);
244
+ console.error = createConsolePatch('error', logFile, hasSufficientDiskSpace);
245
+ console.debug = createConsolePatch('debug', logFile, hasSufficientDiskSpace);
246
+
247
+ // Patch stderr.write
248
+ (process.stderr.write as any) = (chunk: string | Uint8Array, encodingOrCb?: any, callback?: any) => {
249
+ const cb = typeof encodingOrCb === 'function' ? encodingOrCb : callback;
250
+ const message = typeof chunk === 'string' ? chunk : decoder.decode(chunk);
251
+
252
+ // Check disk space before writing
253
+ if (!hasSufficientDiskSpace()) {
254
+ if (typeof cb === 'function') cb();
255
+ return true;
256
+ }
257
+
258
+ const timestamp = new Date().toISOString();
259
+ const entry: LogContext = {
260
+ timestamp,
261
+ level: 'STDERR',
262
+ ...getLogContext(),
263
+ message: redactSecrets(message.trim()),
264
+ };
265
+ try {
266
+ fs.appendFileSync(logFile, `${safeJsonStringify(entry)}\n`);
267
+ } catch { /* ignore */ }
268
+
269
+ if (typeof cb === 'function') cb();
270
+ return true;
271
+ };
272
+
273
+ // Patch stdout.write
274
+ (process.stdout.write as any) = (chunk: string | Uint8Array, encodingOrCb?: any, callback?: any) => {
275
+ const message = typeof chunk === 'string' ? chunk : decoder.decode(chunk);
276
+
277
+ // Native log patterns or plain text that we want to divert from TUI
278
+ const isNative = isNativeLog(message);
279
+ const complexTui = isComplexTui(message);
280
+ const boxDrawing = isBoxDrawing(message);
281
+ const hasAnsi = message.includes('\x1b[');
282
+
283
+ // If it's a known native log, divert it regardless of colors/TUI markers
284
+ if (isNative && !boxDrawing) {
285
+ const timestamp = new Date().toISOString();
286
+ const entry: LogContext = {
287
+ timestamp,
288
+ level: 'STDOUT_NATIVE',
289
+ ...getLogContext(),
290
+ message: redactSecrets(message.trim()),
291
+ };
292
+ try {
293
+ fs.appendFileSync(logFile, `${safeJsonStringify(entry)}\n`);
294
+ } catch { /* ignore */ }
295
+
296
+ const cb = typeof encodingOrCb === 'function' ? encodingOrCb : callback;
297
+ if (typeof cb === 'function') cb();
298
+ return true;
299
+ }
300
+
301
+ // If it's complex TUI or box-drawing, PASS THROUGH
302
+ if (complexTui || boxDrawing) {
303
+ return originalStdoutWrite.call(process.stdout, chunk, encodingOrCb, callback);
304
+ }
305
+
306
+ // If it's plain text (no ANSI) and non-empty, divert and log
307
+ if (!hasAnsi && message.trim().length > 0) {
308
+ if (!hasSufficientDiskSpace()) {
309
+ const cb = typeof encodingOrCb === 'function' ? encodingOrCb : callback;
310
+ if (typeof cb === 'function') cb();
311
+ return true;
312
+ }
313
+
314
+ const timestamp = new Date().toISOString();
315
+ const entry: LogContext = {
316
+ timestamp,
317
+ level: 'STDOUT_PLAIN',
318
+ ...getLogContext(),
319
+ message: redactSecrets(message.trim()),
320
+ };
321
+ try {
322
+ fs.appendFileSync(logFile, `${safeJsonStringify(entry)}\n`);
323
+ } catch { /* ignore */ }
324
+
325
+ const cb = typeof encodingOrCb === 'function' ? encodingOrCb : callback;
326
+ if (typeof cb === 'function') cb();
327
+ return true;
328
+ }
329
+
330
+ // Everything else goes to stdout
331
+ return originalStdoutWrite.call(process.stdout, chunk, encodingOrCb, callback);
332
+ };
333
+
334
+ // Patch fs.writeSync for FD 1 and 2
335
+ try {
336
+ const descriptor = Object.getOwnPropertyDescriptor(fs, 'writeSync');
337
+ if (!descriptor || (descriptor.writable || descriptor.set)) {
338
+ fs.writeSync = (fd: number, chunk: any, ...args: any[]) => {
339
+ if (fd === 1 || fd === 2) {
340
+ const message = typeof chunk === 'string' ? chunk : decoder.decode(chunk);
341
+
342
+ const isNative = isNativeLog(message);
343
+ const boxDrawing = isBoxDrawing(message);
344
+ const complexTui = isComplexTui(message);
345
+ const hasAnsi = message.includes('\x1b[');
346
+
347
+ const shouldDivert = fd === 2 ||
348
+ (isNative && !boxDrawing && !complexTui) ||
349
+ (!hasAnsi && !boxDrawing && message.trim().length > 0);
350
+
351
+ if (shouldDivert) {
352
+ if (!hasSufficientDiskSpace()) {
353
+ return (typeof chunk === 'string' ? Buffer.from(chunk).length : (chunk as any).length);
354
+ }
355
+
356
+ const timestamp = new Date().toISOString();
357
+ const entry: LogContext = {
358
+ timestamp,
359
+ level: fd === 1 ? 'FS_WRITE_SYNC_STDOUT' : 'FS_WRITE_SYNC_STDERR',
360
+ ...getLogContext(),
361
+ message: redactSecrets(message.trim()),
362
+ };
363
+ try {
364
+ fs.appendFileSync(logFile, `${safeJsonStringify(entry)}\n`);
365
+ } catch { /* ignore */ }
366
+ return (typeof chunk === 'string' ? Buffer.from(chunk).length : (chunk as any).length);
367
+ }
368
+ }
369
+ return (originalFsWriteSync as any).apply(fs, [fd, chunk, ...args]);
370
+ };
371
+ }
372
+ } catch (_e) { /* ignore */ }
373
+
374
+ try {
375
+ return await task();
376
+ } finally {
377
+ // Restore everything
378
+ process.stderr.write = originalStderrWrite;
379
+ process.stdout.write = originalStdoutWrite;
380
+ try {
381
+ fs.writeSync = originalFsWriteSync;
382
+ } catch { /* ignore */ }
383
+ console.log = originalConsole.log;
384
+ console.info = originalConsole.info;
385
+ console.warn = originalConsole.warn;
386
+ console.error = originalConsole.error;
387
+ console.debug = originalConsole.debug;
388
+ if (sessionId) {
389
+ setSessionCapturing(sessionId, false);
390
+ } else {
391
+ unkeyedCaptureDepth = Math.max(0, unkeyedCaptureDepth - 1);
392
+ recomputeGlobalCapturing();
393
+ }
394
+
395
+ // Restore FD 2
396
+ if (fd2Redirected && savedFd2 >= 0) {
397
+ try {
398
+ fs.closeSync(2);
399
+ const r = fs.openSync(`/dev/fd/${savedFd2}`, 'a');
400
+ if (r !== 2 && r >= 0) {
401
+ // Didn't recover FD 2 — leave restored FD open
402
+ }
403
+ try { fs.closeSync(savedFd2); } catch { /* ignore */ }
404
+ } catch { /* ignore restore errors */ }
405
+ }
406
+ }
407
+ }