@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.
- package/LICENSE +1 -1
- package/README.md +64 -115
- package/dist/cli.mjs +23211 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1044 -0
- package/src/config.ts +1036 -147
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +264 -7
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
package/src/logger.ts
CHANGED
|
@@ -1,23 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logger — scoped file-based diagnostics
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Writes newline-delimited JSON records to a single consolidated log file. The
|
|
5
|
+
* path defaults to {os.tmpdir()}/pi-research.log and is overridable via the
|
|
6
|
+
* PI_RESEARCH_LOG_PATH env var (see buildDefaultDebugLogPath / log-utils). The
|
|
7
|
+
* file rotates at 10MB, keeping the last 10 archives.
|
|
8
|
+
* ERROR and WARN levels are always logged.
|
|
9
|
+
* INFO and DEBUG levels are only logged when PI_RESEARCH_DEBUG=true (or config.DEBUG=true).
|
|
7
10
|
*
|
|
8
|
-
* When NOT verbose: logFile is null, preventing all temp-dir writes.
|
|
9
11
|
* This module never patches process-global console.* methods.
|
|
12
|
+
*
|
|
13
|
+
* Write durability / performance contract (see LogWriter):
|
|
14
|
+
* - WARN/ERROR are written SYNCHRONOUSLY. They are the primary forensic signal
|
|
15
|
+
* and must survive an immediate crash, so they are never buffered.
|
|
16
|
+
* - INFO/DEBUG (only emitted when verbose) are buffered and flushed off the
|
|
17
|
+
* event loop via fs.promises.appendFile, so high-frequency debug logging
|
|
18
|
+
* never blocks the event loop or the TUI render path — regardless of whether
|
|
19
|
+
* the log file lives on a fast (tmpfs/RAM) or slow (disk) filesystem. The
|
|
20
|
+
* buffer is drained synchronously before every WARN/ERROR write (preserving
|
|
21
|
+
* order) and on process 'exit', so graceful shutdowns lose nothing. Only a
|
|
22
|
+
* hard SIGKILL can drop the sub-millisecond tail of buffered INFO/DEBUG,
|
|
23
|
+
* which is acceptable for non-critical diagnostics.
|
|
10
24
|
*/
|
|
11
25
|
|
|
12
|
-
import { appendFileSync } from 'node:fs';
|
|
26
|
+
import { appendFileSync, existsSync, mkdirSync, openSync, closeSync } from 'node:fs';
|
|
27
|
+
import { appendFile } from 'node:fs/promises';
|
|
13
28
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
|
-
import { randomBytes } from 'node:crypto';
|
|
15
|
-
import * as os from 'node:os';
|
|
16
29
|
import * as path from 'node:path';
|
|
30
|
+
import { errorTracker, type ErrorContext } from './utils/error-tracker.ts';
|
|
31
|
+
import {
|
|
32
|
+
buildDefaultDebugLogPath,
|
|
33
|
+
isVerboseFromEnv,
|
|
34
|
+
getLogContext,
|
|
35
|
+
safeJsonStringify,
|
|
36
|
+
formatArg,
|
|
37
|
+
redactSecrets,
|
|
38
|
+
type LogContext,
|
|
39
|
+
} from './utils/log-utils.ts';
|
|
40
|
+
import { LogRotation } from './utils/log-rotation.ts';
|
|
41
|
+
import { DiskSpaceChecker } from './utils/disk-space-checker.ts';
|
|
42
|
+
import { captureStdio } from './utils/stdio-capture.ts';
|
|
17
43
|
|
|
18
|
-
/**
|
|
19
|
-
* Log level enum
|
|
20
|
-
*/
|
|
21
44
|
export enum LogLevel {
|
|
22
45
|
INFO = 'INFO',
|
|
23
46
|
ERROR = 'ERROR',
|
|
@@ -25,142 +48,259 @@ export enum LogLevel {
|
|
|
25
48
|
DEBUG = 'DEBUG',
|
|
26
49
|
}
|
|
27
50
|
|
|
28
|
-
/**
|
|
29
|
-
* Logger interface for dependency injection
|
|
30
|
-
*/
|
|
31
51
|
export interface ILogger {
|
|
32
52
|
log(...args: unknown[]): void;
|
|
33
53
|
info(...args: unknown[]): void;
|
|
34
54
|
error(...args: unknown[]): void;
|
|
35
55
|
warn(...args: unknown[]): void;
|
|
36
56
|
debug(...args: unknown[]): void;
|
|
57
|
+
clear(): void;
|
|
58
|
+
flushSync(): void;
|
|
59
|
+
runCapturingStderr<T>(task: () => Promise<T>): Promise<T>;
|
|
37
60
|
}
|
|
38
61
|
|
|
39
|
-
/**
|
|
40
|
-
* Logger configuration options
|
|
41
|
-
*/
|
|
42
62
|
export interface LoggerOptions {
|
|
43
63
|
verbose: boolean;
|
|
44
64
|
logFilePath?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface LogContext {
|
|
48
|
-
sessionId?: string;
|
|
49
|
-
sessionFile?: string;
|
|
50
|
-
cwd?: string;
|
|
51
65
|
researchRunId?: string;
|
|
52
|
-
|
|
53
|
-
phase?: string;
|
|
54
|
-
eventName?: string;
|
|
66
|
+
consoleLog?: boolean;
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
const logContextStorage = new AsyncLocalStorage<LogContext>();
|
|
58
|
-
|
|
59
69
|
/**
|
|
60
|
-
*
|
|
70
|
+
* Per-file log writer, shared across every Logger instance that targets the
|
|
71
|
+
* same path (logging is consolidated to one file, so a single shared buffer is
|
|
72
|
+
* required to preserve global write order).
|
|
73
|
+
*
|
|
74
|
+
* INFO/DEBUG go through enqueue() — buffered, flushed asynchronously off the
|
|
75
|
+
* event loop. WARN/ERROR go through writeSync() — which first drains the buffer
|
|
76
|
+
* (keeping order) and then writes synchronously, so critical lines are durable.
|
|
61
77
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
class LogWriter {
|
|
79
|
+
private buffer: string[] = [];
|
|
80
|
+
private bufferBytes = 0;
|
|
81
|
+
private flushScheduled = false;
|
|
82
|
+
private flushing = false;
|
|
83
|
+
// Flush eagerly once buffered output gets large, to bound memory and latency
|
|
84
|
+
// during sustained bursts instead of waiting for the scheduled tick.
|
|
85
|
+
private static readonly MAX_BUFFER_BYTES = 256 * 1024;
|
|
86
|
+
|
|
87
|
+
constructor(private readonly filePath: string) {}
|
|
88
|
+
|
|
89
|
+
/** Queue a line for asynchronous, non-blocking write (INFO/DEBUG path). */
|
|
90
|
+
enqueue(line: string): void {
|
|
91
|
+
this.buffer.push(line);
|
|
92
|
+
this.bufferBytes += line.length;
|
|
93
|
+
if (this.bufferBytes >= LogWriter.MAX_BUFFER_BYTES) {
|
|
94
|
+
void this.flush();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.scheduleFlush();
|
|
67
98
|
}
|
|
68
|
-
return hash;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function buildDefaultDebugLogPath(hash: string): string {
|
|
72
|
-
return path.join(os.tmpdir(), `pi-research-debug-${hash}.log`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function getDefaultDebugLogPathTemplate(): string {
|
|
76
|
-
return buildDefaultDebugLogPath('{hash}');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Check if verbose mode is enabled from environment
|
|
81
|
-
*/
|
|
82
|
-
export function isVerboseFromEnv(): boolean {
|
|
83
|
-
return process.argv.includes('--verbose') || process.env['PI_RESEARCH_VERBOSE'] === '1';
|
|
84
|
-
}
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
private scheduleFlush(): void {
|
|
101
|
+
if (this.flushScheduled || this.flushing) return;
|
|
102
|
+
this.flushScheduled = true;
|
|
103
|
+
// setTimeout(0) runs the flush on the next event-loop turn — off the
|
|
104
|
+
// synchronous caller path — and coalesces a burst of lines into one write.
|
|
105
|
+
setTimeout(() => { void this.flush(); }, 0);
|
|
106
|
+
}
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
/** Asynchronously write buffered lines. Never blocks the event loop. */
|
|
109
|
+
private async flush(): Promise<void> {
|
|
110
|
+
this.flushScheduled = false;
|
|
111
|
+
if (this.flushing || this.buffer.length === 0) return;
|
|
112
|
+
this.flushing = true;
|
|
113
|
+
try {
|
|
114
|
+
while (this.buffer.length > 0) {
|
|
115
|
+
const batch = this.buffer.join('');
|
|
116
|
+
this.buffer.length = 0;
|
|
117
|
+
this.bufferBytes = 0;
|
|
118
|
+
try {
|
|
119
|
+
await appendFile(this.filePath, batch);
|
|
120
|
+
} catch {
|
|
121
|
+
// Silently ignore file write errors (matches the synchronous path).
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
this.flushing = false;
|
|
126
|
+
// A line enqueued after the loop drained but before this point would
|
|
127
|
+
// otherwise linger with no scheduled flush — reschedule defensively.
|
|
128
|
+
if (this.buffer.length > 0) this.scheduleFlush();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
93
131
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Drain buffered lines, then write `line`, all synchronously and in order.
|
|
134
|
+
* Used for WARN/ERROR so the critical line (and all lines queued in the buffer
|
|
135
|
+
* before it) are on disk before this call returns.
|
|
136
|
+
*
|
|
137
|
+
* Ordering caveat: if a large INFO/DEBUG burst already cleared the buffer into
|
|
138
|
+
* an in-flight async appendFile, drainSync() will find the buffer empty and
|
|
139
|
+
* the WARN line will be appended before that in-flight batch completes. In
|
|
140
|
+
* practice this means WARN/ERROR lines may appear slightly before the last
|
|
141
|
+
* flush of the preceding INFO/DEBUG burst. Acceptable for a diagnostic log.
|
|
142
|
+
*/
|
|
143
|
+
writeSync(line: string): void {
|
|
144
|
+
this.drainSync();
|
|
145
|
+
try {
|
|
146
|
+
appendFileSync(this.filePath, line);
|
|
147
|
+
} catch {
|
|
148
|
+
// Silently ignore file write errors.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
98
151
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
152
|
+
/** Synchronously flush all buffered lines (graceful exit / tests / pre-sync-write). */
|
|
153
|
+
drainSync(): void {
|
|
154
|
+
if (this.buffer.length === 0) return;
|
|
155
|
+
const batch = this.buffer.join('');
|
|
156
|
+
this.buffer.length = 0;
|
|
157
|
+
this.bufferBytes = 0;
|
|
158
|
+
try {
|
|
159
|
+
appendFileSync(this.filePath, batch);
|
|
160
|
+
} catch {
|
|
161
|
+
// Silently ignore file write errors.
|
|
162
|
+
}
|
|
104
163
|
}
|
|
105
164
|
}
|
|
106
165
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
// One writer per target path → shared, ordered buffer for consolidated logging.
|
|
167
|
+
const logWriters = new Map<string, LogWriter>();
|
|
168
|
+
|
|
169
|
+
// Drain every writer synchronously on graceful process exit so no buffered
|
|
170
|
+
// INFO/DEBUG line is lost. Registered exactly once, on first writer creation.
|
|
171
|
+
let exitDrainRegistered = false;
|
|
172
|
+
function getLogWriter(filePath: string): LogWriter {
|
|
173
|
+
let writer = logWriters.get(filePath);
|
|
174
|
+
if (!writer) {
|
|
175
|
+
writer = new LogWriter(filePath);
|
|
176
|
+
logWriters.set(filePath, writer);
|
|
110
177
|
}
|
|
111
|
-
if (
|
|
112
|
-
|
|
178
|
+
if (!exitDrainRegistered) {
|
|
179
|
+
exitDrainRegistered = true;
|
|
180
|
+
process.on('exit', () => {
|
|
181
|
+
for (const w of logWriters.values()) {
|
|
182
|
+
try { w.drainSync(); } catch { /* best-effort on exit */ }
|
|
183
|
+
}
|
|
184
|
+
});
|
|
113
185
|
}
|
|
114
|
-
return
|
|
186
|
+
return writer;
|
|
115
187
|
}
|
|
116
188
|
|
|
117
189
|
/**
|
|
118
|
-
*
|
|
190
|
+
* Symbol used to identify real Logger instances
|
|
119
191
|
*/
|
|
192
|
+
const LOGGER_BRAND = Symbol.for('pi-research.Logger');
|
|
193
|
+
|
|
120
194
|
export class Logger implements ILogger {
|
|
121
195
|
private verbose: boolean;
|
|
122
|
-
private
|
|
196
|
+
private consoleLog: boolean;
|
|
197
|
+
private logFile: string;
|
|
198
|
+
private logDir: string;
|
|
199
|
+
private readonly sessionId: string | undefined;
|
|
200
|
+
readonly [LOGGER_BRAND] = true;
|
|
201
|
+
|
|
202
|
+
private readonly rotation: LogRotation;
|
|
203
|
+
private readonly diskSpaceChecker: DiskSpaceChecker;
|
|
123
204
|
|
|
124
205
|
constructor(options: Partial<LoggerOptions> = {}) {
|
|
125
206
|
this.verbose = options.verbose ?? isVerboseFromEnv();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
207
|
+
this.consoleLog = options.consoleLog ?? (process.env['PI_RESEARCH_CONSOLE_LOG'] === 'true');
|
|
208
|
+
// Consolidated logging: always use the same file regardless of run ID
|
|
209
|
+
this.logFile = options.logFilePath ?? buildDefaultDebugLogPath();
|
|
210
|
+
this.logDir = path.dirname(this.logFile);
|
|
211
|
+
this.sessionId = options.researchRunId;
|
|
212
|
+
|
|
213
|
+
this.rotation = new LogRotation(this);
|
|
214
|
+
this.diskSpaceChecker = new DiskSpaceChecker();
|
|
215
|
+
|
|
216
|
+
// Ensure parent directory exists, and pre-create the log file owner-only.
|
|
217
|
+
// The default path lives in a world-traversable tmpdir; appendFile would create
|
|
218
|
+
// it at the umask default (often world-readable). Logs are redacted, but the
|
|
219
|
+
// file should still match the 0o600 posture used for state/lock/knowledge files.
|
|
220
|
+
try {
|
|
221
|
+
if (!existsSync(this.logDir)) {
|
|
222
|
+
mkdirSync(this.logDir, { recursive: true, mode: 0o700 });
|
|
223
|
+
}
|
|
224
|
+
// Atomically create the log file owner-only if it's absent. 'wx' = O_CREAT|
|
|
225
|
+
// O_EXCL: it either creates the file at 0o600 or fails with EEXIST when one
|
|
226
|
+
// already exists — no check-then-create window for another user to win.
|
|
227
|
+
try {
|
|
228
|
+
closeSync(openSync(this.logFile, 'wx', 0o600));
|
|
229
|
+
} catch {
|
|
230
|
+
// Already exists (or not creatable) — appendFile uses the existing file.
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore if we can't create the dir or file
|
|
132
234
|
}
|
|
133
235
|
}
|
|
134
236
|
|
|
135
|
-
/**
|
|
136
|
-
* Emit a log message to file (if verbose) or nowhere (if silent)
|
|
137
|
-
* @param level - Log level (INFO, ERROR, WARN, DEBUG)
|
|
138
|
-
* @param args - Arguments to log
|
|
139
|
-
*/
|
|
140
237
|
private emit(level: string, ...args: unknown[]): void {
|
|
141
|
-
|
|
238
|
+
// Only filter INFO and DEBUG if not verbose
|
|
239
|
+
if (!this.verbose && (level === LogLevel.INFO || level === LogLevel.DEBUG)) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check disk space before writing
|
|
244
|
+
if (!this.diskSpaceChecker.checkDiskSpace(this.logDir)) {
|
|
142
245
|
return;
|
|
143
246
|
}
|
|
144
247
|
|
|
248
|
+
// Rotate log file if needed (throttled to one stat per 60s — see LogRotation).
|
|
249
|
+
this.rotation.checkAndRotate(this.logFile, this.logDir);
|
|
250
|
+
|
|
145
251
|
const timestamp = new Date().toISOString();
|
|
146
252
|
const firstError = args.find((arg): arg is Error => arg instanceof Error);
|
|
253
|
+
// Redact credentials and bound size before the message reaches ANY sink, so
|
|
254
|
+
// neither the log file nor the console persists clear-text secrets or
|
|
255
|
+
// unbounded network/scraped payloads.
|
|
256
|
+
const message = redactSecrets(args.map(formatArg).join(' '));
|
|
147
257
|
const entry = {
|
|
148
258
|
timestamp,
|
|
149
259
|
level,
|
|
150
260
|
...getLogContext(),
|
|
151
|
-
message
|
|
261
|
+
message,
|
|
152
262
|
...(firstError
|
|
153
|
-
? { errorMessage: firstError.message, errorStack: firstError.stack }
|
|
263
|
+
? { errorMessage: redactSecrets(firstError.message), errorStack: redactSecrets(firstError.stack ?? '') }
|
|
154
264
|
: {}),
|
|
155
265
|
};
|
|
156
266
|
const line = `${safeJsonStringify(entry)}\n`;
|
|
157
267
|
|
|
158
|
-
// Write to file
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
268
|
+
// Write to file. WARN/ERROR are written synchronously for crash-durability;
|
|
269
|
+
// INFO/DEBUG are buffered and flushed off the event loop so high-frequency
|
|
270
|
+
// logging never blocks the loop or the TUI render path (see LogWriter).
|
|
271
|
+
const writer = getLogWriter(this.logFile);
|
|
272
|
+
if (level === LogLevel.ERROR || level === LogLevel.WARN) {
|
|
273
|
+
writer.writeSync(line);
|
|
274
|
+
} else {
|
|
275
|
+
writer.enqueue(line);
|
|
163
276
|
}
|
|
277
|
+
|
|
278
|
+
// Optional console output
|
|
279
|
+
if (this.consoleLog) {
|
|
280
|
+
const color = level === LogLevel.ERROR ? '\x1b[31m' :
|
|
281
|
+
level === LogLevel.WARN ? '\x1b[33m' :
|
|
282
|
+
level === LogLevel.DEBUG ? '\x1b[90m' : '\x1b[36m';
|
|
283
|
+
const reset = '\x1b[0m';
|
|
284
|
+
// Reuse the already-redacted message; additionally strip CR/LF so untrusted
|
|
285
|
+
// content cannot forge log lines (log-injection defense). The JSON file sink
|
|
286
|
+
// escapes control chars on its own. CodeQL (js/log-injection) StringReplaceSanitizer
|
|
287
|
+
// requires the replacement to be "" (empty string) — not ' ' — per its QL definition:
|
|
288
|
+
// replaces(s, "") and s.regexpMatch("\\n")
|
|
289
|
+
const msg = message.replace(/[\r\n]/g, '');
|
|
290
|
+
const rawSid = this.sessionId ?? '';
|
|
291
|
+
const sid = rawSid.replace(/[\r\n]/g, '');
|
|
292
|
+
const prefix = sid ? `[${sid}] ` : '';
|
|
293
|
+
console.log(`${color}${timestamp} ${level} ${prefix}${reset}${msg}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async runCapturingStderr<T>(task: () => Promise<T>): Promise<T> {
|
|
298
|
+
return captureStdio(
|
|
299
|
+
this.logFile,
|
|
300
|
+
() => this.diskSpaceChecker.checkDiskSpace(this.logDir),
|
|
301
|
+
task,
|
|
302
|
+
this.sessionId
|
|
303
|
+
);
|
|
164
304
|
}
|
|
165
305
|
|
|
166
306
|
log(...args: unknown[]): void {
|
|
@@ -172,6 +312,11 @@ export class Logger implements ILogger {
|
|
|
172
312
|
}
|
|
173
313
|
|
|
174
314
|
error(...args: unknown[]): void {
|
|
315
|
+
const errArg = args.find((arg): arg is Error => arg instanceof Error);
|
|
316
|
+
if (errArg) {
|
|
317
|
+
const context: ErrorContext = getLogContext();
|
|
318
|
+
errorTracker.trackError(errArg, context);
|
|
319
|
+
}
|
|
175
320
|
this.emit(LogLevel.ERROR, ...args);
|
|
176
321
|
}
|
|
177
322
|
|
|
@@ -183,64 +328,113 @@ export class Logger implements ILogger {
|
|
|
183
328
|
this.emit(LogLevel.DEBUG, ...args);
|
|
184
329
|
}
|
|
185
330
|
|
|
331
|
+
clear(): void {
|
|
332
|
+
this.flushSync();
|
|
333
|
+
this.rotation.clearLogs(this.logFile, this.logDir);
|
|
334
|
+
}
|
|
335
|
+
|
|
186
336
|
/**
|
|
187
|
-
*
|
|
337
|
+
* Synchronously flush this logger's buffered INFO/DEBUG output to disk.
|
|
338
|
+
* Use before reading the log file directly (tests) or during shutdown.
|
|
188
339
|
*/
|
|
340
|
+
flushSync(): void {
|
|
341
|
+
getLogWriter(this.logFile).drainSync();
|
|
342
|
+
}
|
|
343
|
+
|
|
189
344
|
getLogFilePath(): string | null {
|
|
190
345
|
return this.logFile;
|
|
191
346
|
}
|
|
192
347
|
|
|
193
|
-
/**
|
|
194
|
-
* Check if verbose mode is enabled
|
|
195
|
-
*/
|
|
196
348
|
isVerbose(): boolean {
|
|
197
349
|
return this.verbose;
|
|
198
350
|
}
|
|
199
351
|
}
|
|
200
352
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*/
|
|
204
|
-
let globalLogger: Logger | null = null;
|
|
353
|
+
// AsyncLocalStorage context for concurrent run isolation
|
|
354
|
+
const loggerContext = new AsyncLocalStorage<Logger>();
|
|
205
355
|
|
|
206
356
|
/**
|
|
207
|
-
*
|
|
357
|
+
* Run a function with a specific logger bound to the async context.
|
|
358
|
+
* All calls to getLogger() within fn (and any async work it spawns) will
|
|
359
|
+
* return this logger instance, providing proper isolation for concurrent runs.
|
|
208
360
|
*/
|
|
209
|
-
export function
|
|
210
|
-
return
|
|
361
|
+
export function runWithLogger<T>(loggerInstance: Logger, fn: () => Promise<T>): Promise<T> {
|
|
362
|
+
return loggerContext.run(loggerInstance, fn);
|
|
211
363
|
}
|
|
212
364
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
365
|
+
// Session-scoped logger storage to support concurrent research runs
|
|
366
|
+
// Each session gets its own logger instance to prevent log bleeding
|
|
367
|
+
const sessionLoggers = new Map<string, Logger>();
|
|
368
|
+
let _globalLogger: Logger | null = null;
|
|
369
|
+
|
|
370
|
+
export function getLogger(sessionId?: string): Logger {
|
|
371
|
+
// Check async context first (used for concurrent run isolation via runWithLogger)
|
|
372
|
+
const contextLogger = loggerContext.getStore();
|
|
373
|
+
if (contextLogger) return contextLogger;
|
|
374
|
+
|
|
375
|
+
// Return session-scoped logger if sessionId is provided
|
|
376
|
+
if (sessionId) {
|
|
377
|
+
let lg = sessionLoggers.get(sessionId);
|
|
378
|
+
if (!lg) {
|
|
379
|
+
lg = new Logger({
|
|
380
|
+
verbose: isVerboseFromEnv(),
|
|
381
|
+
researchRunId: sessionId,
|
|
382
|
+
consoleLog: process.env['PI_RESEARCH_CONSOLE_LOG'] === 'true'
|
|
383
|
+
});
|
|
384
|
+
sessionLoggers.set(sessionId, lg);
|
|
385
|
+
}
|
|
386
|
+
return lg;
|
|
387
|
+
}
|
|
388
|
+
// Fall back to creating a global logger if no context-bound logger is available
|
|
389
|
+
if (!_globalLogger) {
|
|
390
|
+
_globalLogger = new Logger({
|
|
391
|
+
verbose: isVerboseFromEnv(),
|
|
392
|
+
consoleLog: process.env['PI_RESEARCH_CONSOLE_LOG'] === 'true'
|
|
393
|
+
});
|
|
219
394
|
}
|
|
220
|
-
return
|
|
395
|
+
return _globalLogger;
|
|
221
396
|
}
|
|
222
397
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
398
|
+
export function setLogger(loggerInstance: Logger, sessionId?: string): void {
|
|
399
|
+
if (loggerInstance && !(loggerInstance as any)[LOGGER_BRAND]) {
|
|
400
|
+
throw new Error('setLogger must be called with a Logger instance, not the wrapper.');
|
|
401
|
+
}
|
|
402
|
+
if (sessionId) {
|
|
403
|
+
sessionLoggers.set(sessionId, loggerInstance);
|
|
404
|
+
} else {
|
|
405
|
+
_globalLogger = loggerInstance;
|
|
406
|
+
}
|
|
228
407
|
}
|
|
229
408
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
409
|
+
export function resetLogger(sessionId?: string): void {
|
|
410
|
+
if (sessionId) {
|
|
411
|
+
sessionLoggers.delete(sessionId);
|
|
412
|
+
} else {
|
|
413
|
+
_globalLogger = null;
|
|
414
|
+
sessionLoggers.clear();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function createLogger(options: Partial<LoggerOptions> = {}): Logger {
|
|
419
|
+
if (options.consoleLog === undefined) {
|
|
420
|
+
options.consoleLog = process.env['PI_RESEARCH_CONSOLE_LOG'] === 'true';
|
|
421
|
+
}
|
|
422
|
+
return new Logger(options);
|
|
235
423
|
}
|
|
236
424
|
|
|
237
425
|
/**
|
|
238
|
-
*
|
|
426
|
+
* Global default logger proxy
|
|
239
427
|
*/
|
|
240
|
-
export const logger = {
|
|
428
|
+
export const logger: ILogger = {
|
|
241
429
|
log: (...args: unknown[]) => getLogger().log(...args),
|
|
242
430
|
info: (...args: unknown[]) => getLogger().info(...args),
|
|
243
431
|
error: (...args: unknown[]) => getLogger().error(...args),
|
|
244
432
|
warn: (...args: unknown[]) => getLogger().warn(...args),
|
|
245
433
|
debug: (...args: unknown[]) => getLogger().debug(...args),
|
|
434
|
+
clear: () => getLogger().clear(),
|
|
435
|
+
flushSync: () => getLogger().flushSync(),
|
|
436
|
+
runCapturingStderr: <T>(task: () => Promise<T>) => getLogger().runCapturingStderr(task),
|
|
246
437
|
};
|
|
438
|
+
|
|
439
|
+
export { type LogContext };
|
|
440
|
+
export { buildDefaultDebugLogPath, isVerboseFromEnv, createResearchRunId, getLogContext, runWithLogContext } from './utils/log-utils.ts';
|