@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/cli.ts
ADDED
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-research — standalone CLI
|
|
3
|
+
*
|
|
4
|
+
* A thin, dependency-free command line over the pi-research programmatic SDK
|
|
5
|
+
* (src/sdk.ts). It lets any tool — Claude, Cursor, a shell script, CI — run
|
|
6
|
+
* multi-agent web research without the pi CLI or TUI.
|
|
7
|
+
*
|
|
8
|
+
* pi-research research "<query>" [--depth N] [--model provider/id]
|
|
9
|
+
* pi-research knowledge "<q>" ["<q2>" ...]
|
|
10
|
+
* pi-research knowledge-config [set <none|project|global>]
|
|
11
|
+
* pi-research status [--json]
|
|
12
|
+
* pi-research help
|
|
13
|
+
*
|
|
14
|
+
* Design goals: simple, robust, zero unconfigured-runtime surprises. It fails
|
|
15
|
+
* FAST with an actionable message (and the exact config locations) when the
|
|
16
|
+
* package or a model/API key is missing, instead of spinning up the browser pool
|
|
17
|
+
* and embedding model only to die on the first LLM call.
|
|
18
|
+
*
|
|
19
|
+
* Built to a single plain-JS bundle (dist/cli.mjs) via esbuild --packages=external,
|
|
20
|
+
* so it runs under plain `node` with no TypeScript loader. Exposed as the
|
|
21
|
+
* `pi-research` bin in package.json.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
// pi-research internals (bundled; native/npm deps stay external).
|
|
29
|
+
import {
|
|
30
|
+
initResearchSDK,
|
|
31
|
+
runDeepResearch,
|
|
32
|
+
searchKnowledge,
|
|
33
|
+
shutdownResearchSDK,
|
|
34
|
+
type KnowledgeSearchResult,
|
|
35
|
+
} from './sdk.ts';
|
|
36
|
+
import type { HeadlessObserverOptions } from './orchestration/headless-observer.ts';
|
|
37
|
+
import { validateAndSanitizeQuery } from './utils/input-validation.ts';
|
|
38
|
+
import { validateInitialLink, MAX_INITIAL_LINKS } from './utils/url-utils.ts';
|
|
39
|
+
import { exportResearchReport, appendExportMessage } from './utils/research-export.ts';
|
|
40
|
+
import { getConfig, getGlobalConfigDir, getGlobalEnvFilePath, getInterfaceEnvFilePath, saveConfig, resetConfig, describeKnowledgeStoreMode, isProjectScopedKey, parseDotEnv } from './config.ts';
|
|
41
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Constants
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read the package version from package.json at runtime so `--version`/`status`
|
|
49
|
+
* never drift from package.json. `../package.json` relative to import.meta.url
|
|
50
|
+
* resolves correctly both in dev (src/cli.ts → repo root) and when bundled
|
|
51
|
+
* (dist/cli.mjs → installed package root).
|
|
52
|
+
*/
|
|
53
|
+
function readPkgVersion(): string {
|
|
54
|
+
try {
|
|
55
|
+
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string };
|
|
57
|
+
return typeof pkg.version === 'string' && pkg.version ? pkg.version : '0.0.0';
|
|
58
|
+
} catch {
|
|
59
|
+
return '0.0.0';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const PKG_VERSION = readPkgVersion();
|
|
64
|
+
const BINARY_NAME = 'pi-research';
|
|
65
|
+
|
|
66
|
+
/** sysexits-style exit codes (kept distinct so callers/agents can branch). */
|
|
67
|
+
export const EXIT = {
|
|
68
|
+
OK: 0,
|
|
69
|
+
/** Misuse / bad arguments. */
|
|
70
|
+
USAGE: 64,
|
|
71
|
+
/** Missing setup: package, model, or API key not configured. */
|
|
72
|
+
CONFIG: 78,
|
|
73
|
+
/** Something went wrong at runtime (network, provider, internal). */
|
|
74
|
+
SOFTWARE: 70,
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Output helpers
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function toStderr(s: string): void {
|
|
82
|
+
process.stderr.write(s);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toStdout(s: string): void {
|
|
86
|
+
process.stdout.write(s);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A compact human-readable summary of a value for --json status output. */
|
|
90
|
+
function pretty(obj: unknown): string {
|
|
91
|
+
return JSON.stringify(obj, null, 2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a headless observer that writes a single concise progress line per
|
|
96
|
+
* lifecycle event to stderr. Keeps a long research run visible without spamming
|
|
97
|
+
* stdout (the report goes to stdout, untouched).
|
|
98
|
+
*/
|
|
99
|
+
function makeProgressObserver(): HeadlessObserverOptions {
|
|
100
|
+
return {
|
|
101
|
+
enableLogging: false,
|
|
102
|
+
onProgress: (event: string, data?: any) => {
|
|
103
|
+
switch (event) {
|
|
104
|
+
case 'planning_success':
|
|
105
|
+
toStderr(` • plan: ${data?.plan?.researchers?.length ?? '?'} researcher(s)\n`);
|
|
106
|
+
break;
|
|
107
|
+
case 'round_start':
|
|
108
|
+
toStderr(` • round ${data?.round}\n`);
|
|
109
|
+
break;
|
|
110
|
+
case 'search_start':
|
|
111
|
+
toStderr(` • search burst: ${data?.queries?.length ?? 0} query/${(data?.queries?.length ?? 0) === 1 ? '' : 's'}\n`);
|
|
112
|
+
break;
|
|
113
|
+
case 'researcher_start':
|
|
114
|
+
toStderr(` • researcher ${data?.id}: ${data?.name}\n`);
|
|
115
|
+
break;
|
|
116
|
+
case 'researcher_failure':
|
|
117
|
+
toStderr(` • researcher ${data?.id} failed: ${data?.error}\n`);
|
|
118
|
+
break;
|
|
119
|
+
case 'evaluation_decision':
|
|
120
|
+
toStderr(` • evaluator: ${data?.action}\n`);
|
|
121
|
+
break;
|
|
122
|
+
case 'tokens_consumed':
|
|
123
|
+
if (data?.cost) toStderr(` • tokens: ${data?.tokens ?? 0} ($${Number(data.cost).toFixed(4)})\n`);
|
|
124
|
+
break;
|
|
125
|
+
case 'complete':
|
|
126
|
+
toStderr(` • complete (${data?.result?.length ?? 0} chars)\n`);
|
|
127
|
+
break;
|
|
128
|
+
case 'error':
|
|
129
|
+
toStderr(` • error: ${data?.message}\n`);
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
// Other granular events are intentionally silent to keep output clean.
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Configuration bootstrap
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Bridge the CLI's config files into process.env for keys not already set in the
|
|
146
|
+
* real environment. Layers the base `~/.pi/research/config.env` then the CLI's
|
|
147
|
+
* own `~/.pi/research/cli.env` overlay (overlay wins), or a `--config <path>`
|
|
148
|
+
* override in place of the base.
|
|
149
|
+
*
|
|
150
|
+
* Why: auth values (PI_RESEARCH_API_KEY / _PROVIDER) are read directly from
|
|
151
|
+
* process.env, NOT from the typed Config, so a key placed only in a config file
|
|
152
|
+
* would otherwise be silently ignored. We bridge only when a key is absent from
|
|
153
|
+
* the real environment, so real shell exports always win.
|
|
154
|
+
*/
|
|
155
|
+
export function bridgeConfigEnv(explicitConfigPath?: string): { path: string; loaded: boolean } {
|
|
156
|
+
const basePath = explicitConfigPath ?? getGlobalEnvFilePath();
|
|
157
|
+
const cliOverlayPath = getInterfaceEnvFilePath('cli');
|
|
158
|
+
let loaded = false;
|
|
159
|
+
// Merge base then overlay into one map FIRST (overlay wins), and only then
|
|
160
|
+
// promote into process.env. Promoting per-file would invert the documented
|
|
161
|
+
// precedence: the base file's key would land in process.env on the first
|
|
162
|
+
// iteration and the "don't clobber the real environment" guard would then
|
|
163
|
+
// block the overlay's value — cli.env could never override config.env.
|
|
164
|
+
const merged: Record<string, string> = {};
|
|
165
|
+
for (const filePath of [basePath, cliOverlayPath]) {
|
|
166
|
+
try {
|
|
167
|
+
if (!fs.existsSync(filePath)) continue;
|
|
168
|
+
Object.assign(merged, parseDotEnv(fs.readFileSync(filePath, 'utf-8')));
|
|
169
|
+
loaded = true;
|
|
170
|
+
} catch {
|
|
171
|
+
// ignore unreadable file; fall through to the next
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
175
|
+
// Skip per-directory (project-scoped) keys: promoting them into process.env would make
|
|
176
|
+
// config.env out-rank the per-directory registry, defeating a per-cwd override. They are
|
|
177
|
+
// still applied as a lower-precedence file layer by getConfig()/loadEnvFiles().
|
|
178
|
+
if (isProjectScopedKey(k)) continue;
|
|
179
|
+
if (process.env[k] === undefined && (k.startsWith('PI_RESEARCH_') || k === 'STACKEXCHANGE_API_KEY' || k === 'GITHUB_TOKEN' || k === 'NVD_API_KEY')) {
|
|
180
|
+
process.env[k] = v;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { path: basePath, loaded };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Credential / model detection (no SDK init — cheap & safe for `status`)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
export interface ResolvedConfigPaths {
|
|
191
|
+
/** Global config file (~/.pi/research/config.env). */
|
|
192
|
+
configEnv: string;
|
|
193
|
+
/** Global config dir (~/.pi/research). */
|
|
194
|
+
configDir: string;
|
|
195
|
+
/** Per-interface overlay for THIS standalone CLI / agent skill (~/.pi/research/cli.env). */
|
|
196
|
+
cliIfaceEnv: string;
|
|
197
|
+
/** Per-interface overlay for the pi extension (~/.pi/research/pi.env). */
|
|
198
|
+
piIfaceEnv: string;
|
|
199
|
+
/** pi auth storage (~/.pi/agent/auth.json). */
|
|
200
|
+
piAuth: string;
|
|
201
|
+
/** pi model definitions (~/.pi/agent/models.json). */
|
|
202
|
+
piModels: string;
|
|
203
|
+
/** pi-research state dir (~/.pi/research/state). */
|
|
204
|
+
piState: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolvedConfigPaths(): ResolvedConfigPaths {
|
|
208
|
+
const agentDir = getAgentDir();
|
|
209
|
+
const configDir = getGlobalConfigDir();
|
|
210
|
+
return {
|
|
211
|
+
configEnv: getGlobalEnvFilePath(),
|
|
212
|
+
configDir,
|
|
213
|
+
cliIfaceEnv: getInterfaceEnvFilePath('cli'),
|
|
214
|
+
piIfaceEnv: getInterfaceEnvFilePath('pi'),
|
|
215
|
+
piAuth: path.join(agentDir, 'auth.json'),
|
|
216
|
+
piModels: path.join(agentDir, 'models.json'),
|
|
217
|
+
piState: process.env['PI_RESEARCH_STATE_DIR'] ?? path.join(getGlobalConfigDir(), 'state'),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function fileExists(p: string): boolean {
|
|
222
|
+
try {
|
|
223
|
+
return fs.existsSync(p);
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface CredentialDetection {
|
|
230
|
+
/** Where credentials will come from once initResearchSDK runs. */
|
|
231
|
+
source: 'explicit-env' | 'pi-config' | 'none';
|
|
232
|
+
apiKeyConfigured: boolean;
|
|
233
|
+
provider?: string;
|
|
234
|
+
model?: string;
|
|
235
|
+
modelFrom: 'PI_RESEARCH_MODEL' | 'config.env' | 'unset';
|
|
236
|
+
piAuthPresent: boolean;
|
|
237
|
+
piModelsPresent: boolean;
|
|
238
|
+
/** A short, human-readable problem string, or undefined when ready. */
|
|
239
|
+
problem?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Detect how the SDK will authenticate WITHOUT initializing it. Used both for
|
|
244
|
+
* the `status` command and for the fast-fail pre-flight before every run.
|
|
245
|
+
*/
|
|
246
|
+
function detectCredentials(): CredentialDetection {
|
|
247
|
+
const paths = resolvedConfigPaths();
|
|
248
|
+
const piAuthPresent = fileExists(paths.piAuth);
|
|
249
|
+
const piModelsPresent = fileExists(paths.piModels);
|
|
250
|
+
|
|
251
|
+
const apiKey = process.env['PI_RESEARCH_API_KEY'];
|
|
252
|
+
const providerEnv = process.env['PI_RESEARCH_PROVIDER'];
|
|
253
|
+
const modelEnv = process.env['PI_RESEARCH_MODEL'];
|
|
254
|
+
const modelConfig = getConfig(process.cwd(), 'cli').RESEARCH_MODEL;
|
|
255
|
+
|
|
256
|
+
const model = modelEnv ?? modelConfig;
|
|
257
|
+
const modelFrom = modelEnv ? 'PI_RESEARCH_MODEL' : modelConfig ? 'config.env' : 'unset';
|
|
258
|
+
// Provider is either explicit, or inferred from a "provider/id" model string.
|
|
259
|
+
const provider =
|
|
260
|
+
providerEnv ?? (model && model.includes('/') ? model.slice(0, model.indexOf('/')) : undefined);
|
|
261
|
+
|
|
262
|
+
// --- Explicit API-key path -------------------------------------------------
|
|
263
|
+
if (apiKey) {
|
|
264
|
+
if (!provider) {
|
|
265
|
+
return {
|
|
266
|
+
source: 'explicit-env',
|
|
267
|
+
apiKeyConfigured: true,
|
|
268
|
+
provider,
|
|
269
|
+
model,
|
|
270
|
+
modelFrom,
|
|
271
|
+
piAuthPresent,
|
|
272
|
+
piModelsPresent,
|
|
273
|
+
problem:
|
|
274
|
+
'PI_RESEARCH_API_KEY is set but no provider is configured. Set PI_RESEARCH_PROVIDER ' +
|
|
275
|
+
'(e.g. openai) or use a "provider/model-id" value for PI_RESEARCH_MODEL.',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (!model) {
|
|
279
|
+
return {
|
|
280
|
+
source: 'explicit-env',
|
|
281
|
+
apiKeyConfigured: true,
|
|
282
|
+
provider,
|
|
283
|
+
model,
|
|
284
|
+
modelFrom,
|
|
285
|
+
piAuthPresent,
|
|
286
|
+
piModelsPresent,
|
|
287
|
+
problem:
|
|
288
|
+
'PI_RESEARCH_API_KEY is set but no model is configured. Set PI_RESEARCH_MODEL to a ' +
|
|
289
|
+
'"provider/model-id" (e.g. openai/gpt-4o).',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
source: 'explicit-env',
|
|
294
|
+
apiKeyConfigured: true,
|
|
295
|
+
provider,
|
|
296
|
+
model,
|
|
297
|
+
modelFrom,
|
|
298
|
+
piAuthPresent,
|
|
299
|
+
piModelsPresent,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- pi auth storage path --------------------------------------------------
|
|
304
|
+
if (piAuthPresent) {
|
|
305
|
+
// The SDK will resolve a model from the registry (models.json + PI_RESEARCH_MODEL).
|
|
306
|
+
return {
|
|
307
|
+
source: 'pi-config',
|
|
308
|
+
apiKeyConfigured: true, // key lives in auth.json; SDK reads it
|
|
309
|
+
provider,
|
|
310
|
+
model,
|
|
311
|
+
modelFrom,
|
|
312
|
+
piAuthPresent,
|
|
313
|
+
piModelsPresent,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Nothing configured ----------------------------------------------------
|
|
318
|
+
return {
|
|
319
|
+
source: 'none',
|
|
320
|
+
apiKeyConfigured: false,
|
|
321
|
+
provider,
|
|
322
|
+
model,
|
|
323
|
+
modelFrom,
|
|
324
|
+
piAuthPresent,
|
|
325
|
+
piModelsPresent,
|
|
326
|
+
problem:
|
|
327
|
+
'No model or API key is configured for pi-research. Provide credentials via environment ' +
|
|
328
|
+
'variables (PI_RESEARCH_API_KEY + PI_RESEARCH_PROVIDER + PI_RESEARCH_MODEL) or, if you use ' +
|
|
329
|
+
'pi, via ~/.pi/agent/auth.json.',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build a ready-to-run summary block (config locations + current detection) used
|
|
335
|
+
* in error messages and the `status` command so users always know where things
|
|
336
|
+
* are configured.
|
|
337
|
+
*/
|
|
338
|
+
function configBlock(det: CredentialDetection, extraNote?: string): string {
|
|
339
|
+
const paths = resolvedConfigPaths();
|
|
340
|
+
const lines: string[] = [];
|
|
341
|
+
lines.push('Configuration locations:');
|
|
342
|
+
lines.push(` • base config file: ${paths.configEnv}`);
|
|
343
|
+
lines.push(` • cli overlay: ${paths.cliIfaceEnv} (optional; overrides base for this CLI / agent skill)`);
|
|
344
|
+
lines.push(` • env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`);
|
|
345
|
+
lines.push(` • pi auth storage: ${paths.piAuth}`);
|
|
346
|
+
lines.push(` • pi models: ${paths.piModels}`);
|
|
347
|
+
lines.push('');
|
|
348
|
+
lines.push('Detected:');
|
|
349
|
+
lines.push(` • credential source: ${det.source}`);
|
|
350
|
+
lines.push(` • api key configured: ${det.apiKeyConfigured}`);
|
|
351
|
+
lines.push(` • provider: ${det.provider ?? '(unset)'}`);
|
|
352
|
+
lines.push(` • model: ${det.model ?? '(unset)'} [from: ${det.modelFrom}]`);
|
|
353
|
+
lines.push(` • pi auth.json: ${det.piAuthPresent ? 'present' : 'absent'}`);
|
|
354
|
+
lines.push(` • pi models.json: ${det.piModelsPresent ? 'present' : 'absent'}`);
|
|
355
|
+
if (extraNote) lines.push(` • note: ${extraNote}`);
|
|
356
|
+
return lines.join('\n');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// Subcommands
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
interface ResearchArgs {
|
|
364
|
+
query: string;
|
|
365
|
+
depth?: number;
|
|
366
|
+
model?: string;
|
|
367
|
+
excludeTools?: string[];
|
|
368
|
+
initialLinks?: string[];
|
|
369
|
+
json?: boolean;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Research subcommand. Initializes the SDK, runs deep/quick research, prints the
|
|
374
|
+
* markdown report to stdout, and always shuts down (even on failure).
|
|
375
|
+
*/
|
|
376
|
+
async function cmdResearch(args: ResearchArgs): Promise<number> {
|
|
377
|
+
const det = detectCredentials();
|
|
378
|
+
if (det.problem) {
|
|
379
|
+
if (args.json) toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
|
|
380
|
+
else toStderr(`\nError: ${det.problem}\n\n${configBlock(det)}\n`);
|
|
381
|
+
return EXIT.CONFIG;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Apply the same length/whitespace/dangerous-content gate the pi tool path enforces.
|
|
385
|
+
// The CLI (and the skill launcher that forwards to it) otherwise reached the orchestrator
|
|
386
|
+
// with an unbounded or whitespace-only query — parseArgs only rejects a fully empty one.
|
|
387
|
+
let query: string;
|
|
388
|
+
try {
|
|
389
|
+
query = validateAndSanitizeQuery(args.query);
|
|
390
|
+
} catch (e) {
|
|
391
|
+
const vmsg = e instanceof Error ? e.message : String(e);
|
|
392
|
+
if (args.json) toStdout(pretty({ ok: false, error: vmsg, exitCode: EXIT.USAGE }));
|
|
393
|
+
else toStderr(`\nError: ${vmsg}\n`);
|
|
394
|
+
return EXIT.USAGE;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const depth: 0 | 1 | 2 | 3 =
|
|
398
|
+
(args.depth ?? getConfig(process.cwd(), 'cli').DEFAULT_RESEARCH_DEPTH) as 0 | 1 | 2 | 3;
|
|
399
|
+
// An explicit --model wins over configured/PI_RESEARCH_MODEL for this run.
|
|
400
|
+
const runModel = args.model ?? det.model;
|
|
401
|
+
toStderr(`[pi-research] starting research (depth ${depth})${runModel ? ` with ${runModel}` : ''}…\n`);
|
|
402
|
+
|
|
403
|
+
// Stream concise lifecycle progress to stderr so a long run is never silent.
|
|
404
|
+
// Mirrors the headless observer pattern used by the pi-research SDK consumers.
|
|
405
|
+
const observer: HeadlessObserverOptions = makeProgressObserver();
|
|
406
|
+
|
|
407
|
+
let exit: number = EXIT.OK;
|
|
408
|
+
try {
|
|
409
|
+
await initResearchSDK({
|
|
410
|
+
model: runModel,
|
|
411
|
+
// Provider/key are read from process.env by the SDK; pass-through for clarity.
|
|
412
|
+
apiKey: process.env['PI_RESEARCH_API_KEY'],
|
|
413
|
+
provider: process.env['PI_RESEARCH_PROVIDER'],
|
|
414
|
+
// Drive the SDK with the CLI's own resolved config (base config.env +
|
|
415
|
+
// cli.env overlay), not the SDK's default base read.
|
|
416
|
+
config: getConfig(process.cwd(), 'cli'),
|
|
417
|
+
ignoreGlobalConfig: true,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
let report = await runDeepResearch(query, {
|
|
421
|
+
depth,
|
|
422
|
+
observer,
|
|
423
|
+
...(args.excludeTools ? { excludeTools: args.excludeTools } : {}),
|
|
424
|
+
...(args.initialLinks ? { initialLinks: args.initialLinks } : {}),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Optionally persist the report to a file (opt-in via
|
|
428
|
+
// PI_RESEARCH_REPORT_EXPORT_ENABLED). The SDK is a pure library and does not
|
|
429
|
+
// write files; the CLI front-end does, mirroring the pi tool path.
|
|
430
|
+
// The saved path is surfaced in BOTH the report text and the JSON output so
|
|
431
|
+
// the calling agent can tell the user where the file is.
|
|
432
|
+
let reportPath: string | null = null;
|
|
433
|
+
const exportCfg = getConfig(process.cwd(), 'cli');
|
|
434
|
+
if (exportCfg.RESEARCH_REPORT_EXPORT_ENABLED) {
|
|
435
|
+
reportPath = await exportResearchReport(args.query, report, depth <= 1 ? 'quick' : 'deep', process.cwd(), exportCfg.RESEARCH_REPORT_EXPORT_DIR);
|
|
436
|
+
if (reportPath) {
|
|
437
|
+
report = appendExportMessage(report, reportPath);
|
|
438
|
+
toStderr(`[pi-research] report saved to: ${reportPath}\n`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (args.json) {
|
|
443
|
+
toStdout(pretty({ ok: true, depth, report, ...(reportPath ? { reportPath } : {}) }));
|
|
444
|
+
} else {
|
|
445
|
+
toStdout(report.endsWith('\n') ? report : report + '\n');
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
exit = reportError(err, 'research', args.json);
|
|
449
|
+
} finally {
|
|
450
|
+
await safeShutdown();
|
|
451
|
+
}
|
|
452
|
+
return exit;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Knowledge-search subcommand (the second research tool). Returns a tri-state
|
|
457
|
+
* result; `found: 'no'`/`'maybe'` tells the caller to follow up with live
|
|
458
|
+
* research.
|
|
459
|
+
*/
|
|
460
|
+
async function cmdKnowledge(queries: string[], json?: boolean): Promise<number> {
|
|
461
|
+
const det = detectCredentials();
|
|
462
|
+
if (det.problem) {
|
|
463
|
+
if (json) { toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG })); return EXIT.CONFIG; }
|
|
464
|
+
toStderr(`\nError: ${det.problem}\n\n${configBlock(det)}\n`);
|
|
465
|
+
return EXIT.CONFIG;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const mode = getConfig(process.cwd(), 'cli').KNOWLEDGE_STORE_MODE;
|
|
469
|
+
if (mode === 'none') {
|
|
470
|
+
const paths = resolvedConfigPaths();
|
|
471
|
+
const msg =
|
|
472
|
+
'Knowledge store is disabled (PI_RESEARCH_KNOWLEDGE_STORE_MODE=none). ' +
|
|
473
|
+
"Set it to 'project' or 'global' to enable local knowledge search.\n\n" +
|
|
474
|
+
`Configure in:\n ${paths.configEnv}\n ${paths.cliIfaceEnv} (cli overlay)\n or via PI_RESEARCH_KNOWLEDGE_STORE_MODE env var`;
|
|
475
|
+
if (json) toStdout(pretty({ found: 'no', text: msg, configured: false }));
|
|
476
|
+
else toStderr(`\n[pi-research] ${msg}\n`);
|
|
477
|
+
return EXIT.CONFIG;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let result: KnowledgeSearchResult;
|
|
481
|
+
let exit: number = EXIT.OK;
|
|
482
|
+
try {
|
|
483
|
+
await initResearchSDK({
|
|
484
|
+
model: det.model,
|
|
485
|
+
apiKey: process.env['PI_RESEARCH_API_KEY'],
|
|
486
|
+
provider: process.env['PI_RESEARCH_PROVIDER'],
|
|
487
|
+
config: getConfig(process.cwd(), 'cli'),
|
|
488
|
+
ignoreGlobalConfig: true,
|
|
489
|
+
});
|
|
490
|
+
toStderr(`[pi-research] searching knowledge store (${queries.length} query/${queries.length === 1 ? '' : 's'})…\n`);
|
|
491
|
+
result = await searchKnowledge(queries);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
return reportError(err, 'knowledge search', json);
|
|
494
|
+
} finally {
|
|
495
|
+
await safeShutdown();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (json) {
|
|
499
|
+
toStdout(pretty(result));
|
|
500
|
+
} else {
|
|
501
|
+
toStdout((result.text.endsWith('\n') ? result.text : result.text + '\n'));
|
|
502
|
+
}
|
|
503
|
+
return exit;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Status subcommand: print where things are and whether the skill is ready to
|
|
508
|
+
* run, without initializing the SDK. Machine-readable with --json.
|
|
509
|
+
*/
|
|
510
|
+
async function cmdStatus(json?: boolean): Promise<number> {
|
|
511
|
+
const det = detectCredentials();
|
|
512
|
+
const paths = resolvedConfigPaths();
|
|
513
|
+
const cfg = getConfig(process.cwd(), 'cli');
|
|
514
|
+
const summary = {
|
|
515
|
+
package: '@lincoln504/pi-research',
|
|
516
|
+
version: PKG_VERSION,
|
|
517
|
+
cli: `node ${process.argv[1]}`,
|
|
518
|
+
ready: !det.problem,
|
|
519
|
+
credentials: {
|
|
520
|
+
source: det.source,
|
|
521
|
+
apiKeyConfigured: det.apiKeyConfigured,
|
|
522
|
+
provider: det.provider ?? null,
|
|
523
|
+
model: det.model ?? null,
|
|
524
|
+
modelFrom: det.modelFrom,
|
|
525
|
+
problem: det.problem ?? null,
|
|
526
|
+
},
|
|
527
|
+
knowledgeStoreMode: cfg.KNOWLEDGE_STORE_MODE,
|
|
528
|
+
defaultDepth: cfg.DEFAULT_RESEARCH_DEPTH,
|
|
529
|
+
paths,
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
if (json) {
|
|
533
|
+
toStdout(pretty(summary));
|
|
534
|
+
return EXIT.OK;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
toStdout(`pi-research ${PKG_VERSION}\n`);
|
|
538
|
+
toStdout(`ready: ${summary.ready ? 'yes' : 'no'}\n\n`);
|
|
539
|
+
toStdout(configBlock(det, summary.ready ? undefined : '— fix the problem above, then re-run.') + '\n');
|
|
540
|
+
toStdout(`\nknowledge store mode: ${cfg.KNOWLEDGE_STORE_MODE} (default depth: ${cfg.DEFAULT_RESEARCH_DEPTH})\n`);
|
|
541
|
+
return EXIT.OK;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* knowledge-config subcommand: show or set the per-directory knowledge-store mode.
|
|
546
|
+
*
|
|
547
|
+
* `set` persists the mode for the CURRENT working directory via the same registry writer the
|
|
548
|
+
* interactive /research-config menu uses (saveConfig scope 'local' → ~/.pi/research/state/
|
|
549
|
+
* project-settings.json keyed by the normalized cwd), then resetConfig() so the new value is
|
|
550
|
+
* what the very next getConfig() returns. This is the supported way for an agent to change the
|
|
551
|
+
* setting on the user's behalf without hand-editing the locked registry JSON. A machine-wide
|
|
552
|
+
* default is set instead via the PI_RESEARCH_KNOWLEDGE_STORE_MODE env var (per-run) or config.env.
|
|
553
|
+
*/
|
|
554
|
+
async function cmdKnowledgeConfig(kc: NonNullable<ParsedArgs['knowledgeConfig']>): Promise<number> {
|
|
555
|
+
const cwd = process.cwd();
|
|
556
|
+
|
|
557
|
+
if (kc.action === 'set') {
|
|
558
|
+
const before = getConfig(cwd, 'cli').KNOWLEDGE_STORE_MODE;
|
|
559
|
+
const cfg = { ...getConfig(cwd, 'cli'), KNOWLEDGE_STORE_MODE: kc.mode! };
|
|
560
|
+
// Persist ONLY the knowledge-store mode for this directory — not the whole local-key set —
|
|
561
|
+
// so an unrelated per-directory DEFAULT_RESEARCH_DEPTH isn't frozen as a side effect.
|
|
562
|
+
saveConfig(cfg, 'local', cwd, ['PI_RESEARCH_KNOWLEDGE_STORE_MODE']); // per-directory registry write
|
|
563
|
+
resetConfig(); // drop the cached config so `after` re-resolves from disk
|
|
564
|
+
const info = describeKnowledgeStoreMode(cwd, 'cli');
|
|
565
|
+
// The write always persists to the registry, but a real env var out-ranks the registry, so
|
|
566
|
+
// the EFFECTIVE mode can still differ from what was requested. Surface that instead of
|
|
567
|
+
// silently reporting a no-op.
|
|
568
|
+
const overridden = info.mode !== kc.mode;
|
|
569
|
+
if (kc.json) {
|
|
570
|
+
toStdout(pretty({ command: 'knowledge-config', action: 'set', requested: kc.mode, previous: before, saved: true, effectiveOverriddenBy: overridden ? info.origin : null, ...info, cwd }));
|
|
571
|
+
return EXIT.OK;
|
|
572
|
+
}
|
|
573
|
+
toStdout(
|
|
574
|
+
`knowledge store mode for this directory: ${before} -> ${kc.mode} (saved per-directory)\n` +
|
|
575
|
+
` scope: ${kc.mode === 'none' ? 'disabled here' : kc.mode === 'project' ? 'this directory only' : 'shared across all directories'}\n` +
|
|
576
|
+
` saved: per-directory (${cwd})\n` +
|
|
577
|
+
` db dir: ${info.dbDir}\n` +
|
|
578
|
+
(overridden
|
|
579
|
+
? `\nNOTE: the effective mode here is still '${info.mode}' because a higher-precedence ${info.origin} overrides it. Clear that source for this setting to take effect.\n`
|
|
580
|
+
: ''),
|
|
581
|
+
);
|
|
582
|
+
return EXIT.OK;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// show
|
|
586
|
+
const info = describeKnowledgeStoreMode(cwd, 'cli');
|
|
587
|
+
if (kc.json) {
|
|
588
|
+
toStdout(pretty({ command: 'knowledge-config', action: 'show', ...info, cwd }));
|
|
589
|
+
return EXIT.OK;
|
|
590
|
+
}
|
|
591
|
+
toStdout(
|
|
592
|
+
`knowledge store mode: ${info.mode} (source: ${info.origin})\n` +
|
|
593
|
+
` scope: ${info.mode === 'none' ? 'disabled here' : info.mode === 'project' ? 'this directory only' : 'shared across all directories'}\n` +
|
|
594
|
+
` db dir: ${info.dbDir}\n` +
|
|
595
|
+
`\nchange it for THIS directory: ${BINARY_NAME} knowledge-config set <none|project|global>\n` +
|
|
596
|
+
`change the machine-wide default: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> in ${resolvedConfigPaths().configEnv}\n`,
|
|
597
|
+
);
|
|
598
|
+
return EXIT.OK;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// Error reporting & shutdown
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
/** Distinguish setup errors from runtime errors and print a clean, located message. */
|
|
606
|
+
function reportError(err: unknown, what: string, json?: boolean): number {
|
|
607
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
608
|
+
const lower = msg.toLowerCase();
|
|
609
|
+
|
|
610
|
+
// Auth / model / config-shaped errors → CONFIG exit + config block.
|
|
611
|
+
const isConfigError =
|
|
612
|
+
lower.includes('no llm model available') ||
|
|
613
|
+
lower.includes('not found in pi') ||
|
|
614
|
+
lower.includes('no api key') ||
|
|
615
|
+
lower.includes('provider must be specified') ||
|
|
616
|
+
lower.includes('invalid model string') ||
|
|
617
|
+
lower.includes('authentication') ||
|
|
618
|
+
lower.includes('unauthorized') ||
|
|
619
|
+
lower.includes('api key');
|
|
620
|
+
|
|
621
|
+
const isRateLimit = lower.includes('429') || lower.includes('rate limit') || lower.includes('too many requests');
|
|
622
|
+
const exitCode = isConfigError ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
623
|
+
|
|
624
|
+
// --json: emit a structured error on stdout so a machine consumer never has to
|
|
625
|
+
// special-case plain-text stderr. Mirrors the { ok: true, ... } success shape.
|
|
626
|
+
if (json) {
|
|
627
|
+
const payload: Record<string, unknown> = { ok: false, error: msg, exitCode };
|
|
628
|
+
if (process.env['PI_RESEARCH_DEBUG'] === 'true' && err instanceof Error && err.stack) {
|
|
629
|
+
payload['stack'] = err.stack;
|
|
630
|
+
}
|
|
631
|
+
toStdout(pretty(payload));
|
|
632
|
+
return exitCode;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (isConfigError) {
|
|
636
|
+
toStderr(`\nError: pi-research ${what} failed: ${msg}\n\n${configBlock(detectCredentials())}\n`);
|
|
637
|
+
return exitCode;
|
|
638
|
+
}
|
|
639
|
+
// Rate limits are operational, not fatal setup problems.
|
|
640
|
+
if (isRateLimit) {
|
|
641
|
+
toStderr(`\nError: pi-research ${what} halted: provider rate limit reached. Wait a moment and retry.\n ${msg}\n`);
|
|
642
|
+
return exitCode;
|
|
643
|
+
}
|
|
644
|
+
toStderr(`\nError: pi-research ${what} failed: ${msg}\n`);
|
|
645
|
+
if (process.env['PI_RESEARCH_DEBUG'] === 'true' && err instanceof Error && err.stack) {
|
|
646
|
+
toStderr('\n' + err.stack + '\n');
|
|
647
|
+
}
|
|
648
|
+
return exitCode;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Best-effort SDK shutdown that never throws.
|
|
653
|
+
* Bounded by SHUTDOWN_TIMEOUT_MS: WASM threads (ONNX/embedding model), LanceDB
|
|
654
|
+
* connections, and the browser orphan sweep can all outlast their own internal
|
|
655
|
+
* timeouts. We must not let the CLI hang forever on exit.
|
|
656
|
+
*/
|
|
657
|
+
const SHUTDOWN_TIMEOUT_MS = 8_000;
|
|
658
|
+
|
|
659
|
+
async function safeShutdown(): Promise<void> {
|
|
660
|
+
try {
|
|
661
|
+
await Promise.race([
|
|
662
|
+
shutdownResearchSDK(),
|
|
663
|
+
new Promise<void>((resolve) =>
|
|
664
|
+
setTimeout(() => {
|
|
665
|
+
toStderr('[pi-research] shutdown timeout — forcing exit\n');
|
|
666
|
+
resolve();
|
|
667
|
+
}, SHUTDOWN_TIMEOUT_MS),
|
|
668
|
+
),
|
|
669
|
+
]);
|
|
670
|
+
} catch {
|
|
671
|
+
// Swallow — nothing actionable for the caller, and we must not mask the real result.
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ---------------------------------------------------------------------------
|
|
676
|
+
// Argument parsing
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
|
|
679
|
+
interface ParsedArgs {
|
|
680
|
+
command?: string;
|
|
681
|
+
research?: ResearchArgs;
|
|
682
|
+
knowledge?: { queries: string[]; json?: boolean };
|
|
683
|
+
knowledgeConfig?: { action: 'show' | 'set'; mode?: 'none' | 'project' | 'global'; json?: boolean };
|
|
684
|
+
status?: { json?: boolean };
|
|
685
|
+
configPath?: string;
|
|
686
|
+
json?: boolean;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function buildHelp(): string {
|
|
690
|
+
const p = resolvedConfigPaths();
|
|
691
|
+
return `${BINARY_NAME} v${PKG_VERSION} — multi-agent web research (pi-research SDK)
|
|
692
|
+
|
|
693
|
+
USAGE
|
|
694
|
+
${BINARY_NAME} <command> [options]
|
|
695
|
+
|
|
696
|
+
COMMANDS
|
|
697
|
+
research "<query>" Run multi-agent web research (default depth: 1).
|
|
698
|
+
--depth <0-3> 0 = quick, 1 = normal (default), 2 = deep, 3 = ultra.
|
|
699
|
+
--model <provider/id> Override the model for this run.
|
|
700
|
+
--exclude-tools <a,b> Disable internal tools (e.g. security_search,stackexchange).
|
|
701
|
+
--initial-links <url ...> Seed URLs to investigate first; requires a query (-- ends options).
|
|
702
|
+
--config <path> Use this config file instead of the base config.env.
|
|
703
|
+
--json Emit a JSON object instead of a markdown report.
|
|
704
|
+
|
|
705
|
+
knowledge "<q1>" ["<q2>" ...] Search local knowledge store before live research (up to 5 queries).
|
|
706
|
+
--config <path> Use this config file instead of the base config.env.
|
|
707
|
+
--json Emit a JSON object.
|
|
708
|
+
|
|
709
|
+
knowledge-config [show] Show the knowledge-store mode for this directory + its source.
|
|
710
|
+
--config <path> Use this config file instead of the base config.env.
|
|
711
|
+
--json Emit a JSON object.
|
|
712
|
+
knowledge-config set <mode> Set the mode for THIS directory (none | project | global);
|
|
713
|
+
persisted per-directory. Takes effect on the next run.
|
|
714
|
+
|
|
715
|
+
status Show detected config, model/key, and readiness.
|
|
716
|
+
--config <path> Use this config file instead of the base config.env.
|
|
717
|
+
--json Emit a JSON object.
|
|
718
|
+
|
|
719
|
+
help, --help, -h Show this help.
|
|
720
|
+
|
|
721
|
+
CONFIGURE
|
|
722
|
+
Credentials are resolved in this order (first match wins):
|
|
723
|
+
|
|
724
|
+
env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
|
|
725
|
+
cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
|
|
726
|
+
base config: ${p.configEnv}
|
|
727
|
+
pi auth: ${p.piAuth}
|
|
728
|
+
pi models: ${p.piModels}
|
|
729
|
+
|
|
730
|
+
The pi extension has its own optional overlay file:
|
|
731
|
+
pi extension: ${p.piIfaceEnv}
|
|
732
|
+
|
|
733
|
+
Knowledge store — ON by default in every directory (mode 'global', one shared store).
|
|
734
|
+
per-directory: ${BINARY_NAME} knowledge-config set <none|project|global>
|
|
735
|
+
machine-wide: PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> in ${p.configEnv}
|
|
736
|
+
'project' scopes the store to the current directory; 'none' disables it here.
|
|
737
|
+
|
|
738
|
+
Run \`${BINARY_NAME} status\` to see exactly what is detected.
|
|
739
|
+
|
|
740
|
+
EXIT CODES
|
|
741
|
+
0 success
|
|
742
|
+
64 bad arguments
|
|
743
|
+
78 not configured — message prints the exact locations to fix
|
|
744
|
+
70 runtime error (network, provider, internal)
|
|
745
|
+
`;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
749
|
+
const out: ParsedArgs = {};
|
|
750
|
+
const args = argv.slice(2);
|
|
751
|
+
|
|
752
|
+
if (args.length === 0) {
|
|
753
|
+
out.command = 'help';
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const cmd = args[0];
|
|
758
|
+
if (cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
759
|
+
out.command = 'help';
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
762
|
+
if (cmd === '-v' || cmd === '--version') {
|
|
763
|
+
out.command = 'version';
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const rest = args.slice(1);
|
|
768
|
+
|
|
769
|
+
if (cmd === 'status') {
|
|
770
|
+
const json = rest.includes('--json');
|
|
771
|
+
let configPath: string | undefined;
|
|
772
|
+
const ci = rest.indexOf('--config');
|
|
773
|
+
if (ci !== -1 && rest[ci + 1]) configPath = rest[ci + 1];
|
|
774
|
+
out.command = 'status';
|
|
775
|
+
out.status = { json };
|
|
776
|
+
out.configPath = configPath;
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (cmd === 'knowledge') {
|
|
781
|
+
const positional: string[] = [];
|
|
782
|
+
let json = false;
|
|
783
|
+
let configPath: string | undefined;
|
|
784
|
+
for (let i = 0; i < rest.length; i++) {
|
|
785
|
+
const a = rest[i];
|
|
786
|
+
if (a === '--json') json = true;
|
|
787
|
+
else if (a === '--config' && rest[i + 1]) configPath = rest[++i];
|
|
788
|
+
else if (a?.startsWith('--')) {
|
|
789
|
+
throw new UsageError(`unknown option for knowledge: ${a}`);
|
|
790
|
+
} else if (a !== undefined) {
|
|
791
|
+
positional.push(a);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (positional.length === 0) throw new UsageError('knowledge requires at least one query.');
|
|
795
|
+
if (positional.length > 5) throw new UsageError('knowledge accepts at most 5 queries.');
|
|
796
|
+
out.command = 'knowledge';
|
|
797
|
+
out.knowledge = { queries: positional, json };
|
|
798
|
+
out.configPath = configPath;
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (cmd === 'knowledge-config') {
|
|
803
|
+
let json = false;
|
|
804
|
+
let configPath: string | undefined;
|
|
805
|
+
const positional: string[] = [];
|
|
806
|
+
for (let i = 0; i < rest.length; i++) {
|
|
807
|
+
const a = rest[i];
|
|
808
|
+
if (a === '--json') json = true;
|
|
809
|
+
else if (a === '--config' && rest[i + 1]) configPath = rest[++i];
|
|
810
|
+
else if (a?.startsWith('--')) {
|
|
811
|
+
throw new UsageError(`unknown option for knowledge-config: ${a}`);
|
|
812
|
+
} else if (a !== undefined) {
|
|
813
|
+
positional.push(a);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const action = positional[0] ?? 'show';
|
|
817
|
+
if (action === 'show') {
|
|
818
|
+
out.command = 'knowledge-config';
|
|
819
|
+
out.knowledgeConfig = { action: 'show', json };
|
|
820
|
+
out.configPath = configPath;
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
if (action === 'set') {
|
|
824
|
+
const mode = positional[1];
|
|
825
|
+
if (mode === undefined) {
|
|
826
|
+
throw new UsageError('knowledge-config set requires a mode: none | project | global.');
|
|
827
|
+
}
|
|
828
|
+
if (mode !== 'none' && mode !== 'project' && mode !== 'global') {
|
|
829
|
+
throw new UsageError(`invalid knowledge store mode "${mode}". Use one of: none | project | global.`);
|
|
830
|
+
}
|
|
831
|
+
if (positional.length > 2) {
|
|
832
|
+
throw new UsageError(`unexpected argument "${positional[2]}" after "knowledge-config set ${mode}".`);
|
|
833
|
+
}
|
|
834
|
+
out.command = 'knowledge-config';
|
|
835
|
+
out.knowledgeConfig = { action: 'set', mode, json };
|
|
836
|
+
out.configPath = configPath;
|
|
837
|
+
return out;
|
|
838
|
+
}
|
|
839
|
+
throw new UsageError(`unknown knowledge-config action "${action}". Use: show | set <none|project|global>.`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (cmd === 'research') {
|
|
843
|
+
const r: ResearchArgs = { query: '' };
|
|
844
|
+
const initialLinks: string[] = [];
|
|
845
|
+
let depth: number | undefined;
|
|
846
|
+
let model: string | undefined;
|
|
847
|
+
let excludeTools: string[] | undefined;
|
|
848
|
+
let json = false;
|
|
849
|
+
let configPath: string | undefined;
|
|
850
|
+
const positional: string[] = [];
|
|
851
|
+
|
|
852
|
+
for (let i = 0; i < rest.length; i++) {
|
|
853
|
+
const a = rest[i];
|
|
854
|
+
if (a === '--') {
|
|
855
|
+
// Everything after -- is the query / positional.
|
|
856
|
+
positional.push(...rest.slice(i + 1).filter((x): x is string => x !== undefined));
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
if (a === '--depth') {
|
|
860
|
+
const v = rest[++i];
|
|
861
|
+
const n = v === undefined ? NaN : parseInt(v, 10);
|
|
862
|
+
if (isNaN(n) || n < 0 || n > 3) throw new UsageError('--depth must be an integer 0–3.');
|
|
863
|
+
depth = n;
|
|
864
|
+
} else if (a === '--model') {
|
|
865
|
+
model = rest[++i];
|
|
866
|
+
if (!model) throw new UsageError('--model requires a value.');
|
|
867
|
+
} else if (a === '--exclude-tools') {
|
|
868
|
+
const v = rest[++i];
|
|
869
|
+
excludeTools = v ? v.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
|
|
870
|
+
} else if (a === '--initial-links') {
|
|
871
|
+
// Consume until the next flag or end. Validate each entry: these are templated
|
|
872
|
+
// verbatim into the researcher's "investigate these first" evidence block, so
|
|
873
|
+
// (like the query) they must be bounded and shape-checked — reject non-http(s)
|
|
874
|
+
// or oversized tokens and cap the count so a caller can't inject arbitrary
|
|
875
|
+
// instructions framed as trusted seed evidence or blow the prompt budget.
|
|
876
|
+
// Shares validateInitialLink with the research tool's initialLinks
|
|
877
|
+
// parameter (research-tool-definition.ts) so the two paths cannot drift.
|
|
878
|
+
let j = i + 1;
|
|
879
|
+
while (j < rest.length && !rest[j]?.startsWith('--')) {
|
|
880
|
+
const link = rest[j];
|
|
881
|
+
if (link) {
|
|
882
|
+
const linkError = validateInitialLink(link);
|
|
883
|
+
if (linkError) throw new UsageError(`--initial-links: ${linkError}.`);
|
|
884
|
+
if (initialLinks.length >= MAX_INITIAL_LINKS) throw new UsageError(`--initial-links: at most ${MAX_INITIAL_LINKS} URLs may be provided.`);
|
|
885
|
+
initialLinks.push(link);
|
|
886
|
+
}
|
|
887
|
+
j++;
|
|
888
|
+
}
|
|
889
|
+
i = j - 1;
|
|
890
|
+
} else if (a === '--config') {
|
|
891
|
+
configPath = rest[++i];
|
|
892
|
+
if (!configPath) throw new UsageError('--config requires a path.');
|
|
893
|
+
} else if (a === '--json') {
|
|
894
|
+
json = true;
|
|
895
|
+
} else if (a?.startsWith('--')) {
|
|
896
|
+
throw new UsageError(`unknown option: ${a}`);
|
|
897
|
+
} else if (a !== undefined) {
|
|
898
|
+
positional.push(a);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// A links-only invocation is a dead path: cmdResearch feeds the (empty) query
|
|
903
|
+
// through validateAndSanitizeQuery, which rejects it anyway — but with a
|
|
904
|
+
// confusing "empty query" error at exit 64. Reject it up front with one
|
|
905
|
+
// clear message instead: --initial-links SEEDS a query, it cannot replace one.
|
|
906
|
+
if (positional.length === 0) {
|
|
907
|
+
throw new UsageError(
|
|
908
|
+
initialLinks.length > 0
|
|
909
|
+
? 'research requires a query; --initial-links seeds URLs for a query but cannot replace it.'
|
|
910
|
+
: 'research requires a query.',
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
r.query = positional.join(' ').trim();
|
|
914
|
+
r.depth = depth;
|
|
915
|
+
r.model = model;
|
|
916
|
+
r.excludeTools = excludeTools;
|
|
917
|
+
r.initialLinks = initialLinks.length ? initialLinks : undefined;
|
|
918
|
+
r.json = json;
|
|
919
|
+
out.command = 'research';
|
|
920
|
+
out.research = r;
|
|
921
|
+
out.configPath = configPath;
|
|
922
|
+
return out;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
throw new UsageError(`unknown command "${cmd}". Run \`${BINARY_NAME} help\`.`);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export class UsageError extends Error {}
|
|
929
|
+
|
|
930
|
+
// ---------------------------------------------------------------------------
|
|
931
|
+
// Main
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
|
|
934
|
+
// Flush stdout+stderr before exiting so buffered output (e.g. --json into a pipe) is not
|
|
935
|
+
// truncated. Uses process.exit() (not exitCode) so native handles (WASM threads, LanceDB)
|
|
936
|
+
// don't keep the loop alive after work is done. Module-scoped so BOTH the entry-point and
|
|
937
|
+
// the signal handler drain the same way — the signal path previously called process.exit()
|
|
938
|
+
// directly and could truncate output on Ctrl-C mid-write.
|
|
939
|
+
const flushAndExit = (code: number): void => {
|
|
940
|
+
process.stdout.write('', () => {
|
|
941
|
+
process.stderr.write('', () => {
|
|
942
|
+
process.exit(code);
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
async function main(argv: string[]): Promise<number> {
|
|
948
|
+
// Register signal handlers so SIGINT/SIGTERM always trigger SDK shutdown
|
|
949
|
+
// before the process exits. safeShutdown() is idempotent — safe to call
|
|
950
|
+
// before SDK init (it no-ops) and safe to call from multiple signals.
|
|
951
|
+
let _signalCleanupDone = false;
|
|
952
|
+
const onSignal = (sig: string) => {
|
|
953
|
+
if (_signalCleanupDone) return;
|
|
954
|
+
_signalCleanupDone = true;
|
|
955
|
+
toStderr(`\n[pi-research] ${sig} — cleaning up…\n`);
|
|
956
|
+
// Fire-and-forget: we can't await here, but safeShutdown swallows errors
|
|
957
|
+
// and the finally blocks in each command handler also call it. Drain via
|
|
958
|
+
// flushAndExit (not a bare process.exit) so buffered stdout isn't truncated.
|
|
959
|
+
void safeShutdown().finally(() => flushAndExit(EXIT.SOFTWARE));
|
|
960
|
+
};
|
|
961
|
+
process.once('SIGINT', () => onSignal('SIGINT'));
|
|
962
|
+
process.once('SIGTERM', () => onSignal('SIGTERM'));
|
|
963
|
+
|
|
964
|
+
let parsed: ParsedArgs;
|
|
965
|
+
try {
|
|
966
|
+
parsed = parseArgs(argv);
|
|
967
|
+
} catch (err) {
|
|
968
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
969
|
+
toStderr(`\nError: ${msg}\n\n${buildHelp()}`);
|
|
970
|
+
return EXIT.USAGE;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Bridge config.env → process.env for commands that touch the engine.
|
|
974
|
+
// help/version are pure and need no credentials/config.
|
|
975
|
+
const noBridge = new Set(['help', 'version']);
|
|
976
|
+
if (!noBridge.has(parsed.command ?? '')) {
|
|
977
|
+
bridgeConfigEnv(parsed.configPath);
|
|
978
|
+
// Self-heal cross-harness skill installs on every engine-touching CLI run. The
|
|
979
|
+
// CLI — not the interactive pi extension — is the surface that skill invocations
|
|
980
|
+
// from Claude Code / Codex / OpenClaw actually drive, so without this an update
|
|
981
|
+
// that relocates the package leaves a sibling skill symlink dangling until the
|
|
982
|
+
// user happens to open an interactive pi session. Best-effort and idempotent;
|
|
983
|
+
// it early-returns when no skills are installed and a filesystem hiccup here must
|
|
984
|
+
// never block the command. Mirrors the reconcile in the extension's activation.
|
|
985
|
+
try {
|
|
986
|
+
const { reconcileSkillInstalls } = await import('./skill-install/skill-installer.ts');
|
|
987
|
+
reconcileSkillInstalls();
|
|
988
|
+
} catch { /* best-effort self-heal; never block the CLI */ }
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
switch (parsed.command) {
|
|
992
|
+
case 'help':
|
|
993
|
+
toStdout(buildHelp());
|
|
994
|
+
return EXIT.OK;
|
|
995
|
+
case 'version':
|
|
996
|
+
toStdout(`${BINARY_NAME} ${PKG_VERSION}\n`);
|
|
997
|
+
return EXIT.OK;
|
|
998
|
+
case 'status':
|
|
999
|
+
return cmdStatus(parsed.status?.json);
|
|
1000
|
+
case 'knowledge':
|
|
1001
|
+
return cmdKnowledge(parsed.knowledge!.queries, parsed.knowledge!.json);
|
|
1002
|
+
case 'knowledge-config':
|
|
1003
|
+
return cmdKnowledgeConfig(parsed.knowledgeConfig!);
|
|
1004
|
+
case 'research':
|
|
1005
|
+
return cmdResearch(parsed.research!);
|
|
1006
|
+
default:
|
|
1007
|
+
toStdout(buildHelp());
|
|
1008
|
+
return EXIT.OK;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// ---------------------------------------------------------------------------
|
|
1013
|
+
// Entry-point guard
|
|
1014
|
+
// ---------------------------------------------------------------------------
|
|
1015
|
+
|
|
1016
|
+
// Only run when this module is the direct entry point (node cli.mjs <args>),
|
|
1017
|
+
// not when it is imported by a test or another module. We compare the
|
|
1018
|
+
// resolved on-disk path of this module's URL against process.argv[1].
|
|
1019
|
+
const _isMain = (() => {
|
|
1020
|
+
try {
|
|
1021
|
+
if (!process.argv[1]) return false;
|
|
1022
|
+
// realpath BOTH sides: when installed via an npm `bin` symlink (the primary global-install
|
|
1023
|
+
// path), argv[1] is the symlink while import.meta.url is realpath-resolved by Node. A plain
|
|
1024
|
+
// path.resolve does not follow symlinks, so the two never matched and main() was silently
|
|
1025
|
+
// skipped — the CLI exited 0 with empty output. Resolving symlinks on both sides fixes it.
|
|
1026
|
+
const modulePath = fs.realpathSync(path.resolve(fileURLToPath(import.meta.url)));
|
|
1027
|
+
const invokedPath = fs.realpathSync(path.resolve(process.argv[1]));
|
|
1028
|
+
return modulePath === invokedPath;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
})();
|
|
1033
|
+
|
|
1034
|
+
if (_isMain) {
|
|
1035
|
+
// Top-level entry. Never let an unhandled rejection escape — print a clean error.
|
|
1036
|
+
// flushAndExit (module-scoped above) drains both streams before process.exit() so
|
|
1037
|
+
// buffered output isn't truncated and native handles don't keep the loop alive.
|
|
1038
|
+
main(process.argv)
|
|
1039
|
+
.then(flushAndExit)
|
|
1040
|
+
.catch((err) => {
|
|
1041
|
+
toStderr(`\nError: fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1042
|
+
flushAndExit(EXIT.SOFTWARE);
|
|
1043
|
+
});
|
|
1044
|
+
}
|