@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
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research Configuration Command
|
|
3
|
+
*
|
|
4
|
+
* Consolidated interactive TUI menu for managing pi-research.
|
|
5
|
+
* Mirrored after the official Pi settings experience:
|
|
6
|
+
* - Single flat scrollable list
|
|
7
|
+
* - Active item centered with description
|
|
8
|
+
* - Enter/Space to cycle values
|
|
9
|
+
* - Automatic saving on change
|
|
10
|
+
* - Search support
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* - /research-config - Opens interactive TUI menu
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
17
|
+
import type { TUI } from '@earendil-works/pi-tui';
|
|
18
|
+
import {
|
|
19
|
+
SettingsList,
|
|
20
|
+
type SettingItem,
|
|
21
|
+
visibleWidth,
|
|
22
|
+
truncateToWidth,
|
|
23
|
+
} from '@earendil-works/pi-tui';
|
|
24
|
+
import { setInteractiveTuiActive, initGlobalTuiController } from './tui/tui-controller.ts';
|
|
25
|
+
import { normalizeSessionId } from './orchestration/session-state.ts';
|
|
26
|
+
import { getConfig, saveConfig, resetConfig, getDbDir } from './config.ts';
|
|
27
|
+
import { healthRegistry } from './healthcheck/index.ts';
|
|
28
|
+
import { getService, clearService, tryGetServiceContainerFromCtx } from './core/service-registry.ts';
|
|
29
|
+
import { ServiceNames, IKnowledgeStoreService } from './core/service-interfaces.ts';
|
|
30
|
+
// Type-only: avoids statically loading the native ML/vector stack into the
|
|
31
|
+
// extension load path. The value (clearEmbeddingInstance) is dynamically
|
|
32
|
+
// imported at its call sites, only when a knowledge command actually runs.
|
|
33
|
+
import type { KnowledgeStoreService } from './infrastructure/knowledge-store-service.ts';
|
|
34
|
+
import type { Theme } from './types/research-panel-types.ts';
|
|
35
|
+
// Import the constant directly from its source module rather than the knowledge
|
|
36
|
+
// barrel (./knowledge/index.ts), which would transitively load lancedb.
|
|
37
|
+
import { SUPPORTED_MODELS } from './knowledge/model-config.ts';
|
|
38
|
+
import { metrics } from './utils/metrics.ts';
|
|
39
|
+
import {
|
|
40
|
+
extractRunStats,
|
|
41
|
+
aggregateSessionStats,
|
|
42
|
+
buildSessionOverview,
|
|
43
|
+
buildRunCompactLine,
|
|
44
|
+
} from './utils/metrics-summary.ts';
|
|
45
|
+
import { logger } from './logger.ts';
|
|
46
|
+
import { formatTimeAgo, formatDuration } from './utils/text-utils.ts';
|
|
47
|
+
import { buildDefaultDebugLogPath } from './utils/log-utils.ts';
|
|
48
|
+
import {
|
|
49
|
+
HARNESSES,
|
|
50
|
+
installSkill,
|
|
51
|
+
uninstallSkill,
|
|
52
|
+
skillInstallCandidates,
|
|
53
|
+
skillUninstallCandidates,
|
|
54
|
+
SKILL_AGENT_TARGETS,
|
|
55
|
+
} from './skill-install/skill-installer.ts';
|
|
56
|
+
import * as path from 'node:path';
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Utilities
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* TUI setting id → canonical env-var key written to config.env / the project
|
|
64
|
+
* registry. Passed to saveConfig as `changedKeys` so a save persists ONLY the
|
|
65
|
+
* setting the user actually changed — never a snapshot of the whole resolved
|
|
66
|
+
* config (which would freeze env overrides and current defaults into the file).
|
|
67
|
+
* Several ids do not map 1:1 (e.g. RESEARCHER_TIMEOUT_MS → PI_RESEARCH_TIMEOUT_MS),
|
|
68
|
+
* hence an explicit map instead of `PI_RESEARCH_${id}`.
|
|
69
|
+
*/
|
|
70
|
+
const ENV_KEY_BY_SETTING_ID: Record<string, string> = {
|
|
71
|
+
DEFAULT_RESEARCH_DEPTH: 'PI_RESEARCH_DEFAULT_RESEARCH_DEPTH',
|
|
72
|
+
KNOWLEDGE_STORE_MODE: 'PI_RESEARCH_KNOWLEDGE_STORE_MODE',
|
|
73
|
+
RESEARCHER_TIMEOUT_MS: 'PI_RESEARCH_TIMEOUT_MS',
|
|
74
|
+
MAX_CONCURRENT_RESEARCHERS: 'PI_RESEARCH_MAX_RESEARCHERS',
|
|
75
|
+
MAX_SCRAPE_BATCHES: 'PI_RESEARCH_MAX_SCRAPE_BATCHES',
|
|
76
|
+
RESEARCH_REPORT_EXPORT_ENABLED: 'PI_RESEARCH_REPORT_EXPORT_ENABLED',
|
|
77
|
+
DEBUG: 'PI_RESEARCH_DEBUG',
|
|
78
|
+
EMBEDDING_MODEL: 'PI_RESEARCH_EMBEDDING_MODEL',
|
|
79
|
+
EMBEDDING_DEVICE: 'PI_RESEARCH_EMBEDDING_DEVICE',
|
|
80
|
+
KNOWLEDGE_STORE_CACHE_TTL_DAYS: 'PI_RESEARCH_CACHE_TTL_DAYS',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Command Handler
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Main command handler for /research-config
|
|
89
|
+
*/
|
|
90
|
+
export async function handleResearchConfigCommand(
|
|
91
|
+
_args: string,
|
|
92
|
+
ctx: ExtensionContext,
|
|
93
|
+
pi: ExtensionAPI
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (!ctx.hasUI) {
|
|
96
|
+
ctx.ui.notify('Interactive menu requires UI mode', 'error');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await showInteractiveMenu(ctx, pi);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Interactive TUI Menu
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Show interactive TUI menu for research configuration
|
|
108
|
+
*/
|
|
109
|
+
async function showInteractiveMenu(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
110
|
+
// Ensure the global TUI controller is initialized. Normalize the session id (→ 'default'
|
|
111
|
+
// when unresolved) like every other call site: initGlobalTuiController unconditionally sets
|
|
112
|
+
// state.piSessionId, and an undefined value there makes setInteractiveTuiActive skip
|
|
113
|
+
// hideMasterWidget/refreshAllSessions — leaving the live panel stacked under the menu.
|
|
114
|
+
const piSessionId = normalizeSessionId((ctx as any).sessionId || (ctx as any).sessionManager?.getSessionId());
|
|
115
|
+
initGlobalTuiController(ctx.ui, piSessionId);
|
|
116
|
+
|
|
117
|
+
const cwd = ctx.cwd || process.cwd();
|
|
118
|
+
const initialConfig = { ...getConfig(cwd) };
|
|
119
|
+
const config = { ...getConfig(cwd) };
|
|
120
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
121
|
+
const depthLabels: Record<number, string> = { 1: 'normal', 2: 'deep', 3: 'ultra' };
|
|
122
|
+
|
|
123
|
+
const anyKnowledgeStore = config.KNOWLEDGE_STORE_MODE !== 'none';
|
|
124
|
+
|
|
125
|
+
// Coding-agent skill install/uninstall gating, computed once from disk:
|
|
126
|
+
// • Install is offered only when a target agent's ROOT config folder already
|
|
127
|
+
// exists (~/.claude or ~/.codex) — we never create an agent's home dir.
|
|
128
|
+
// • Uninstall is offered only when the skill is actually installed (an owned
|
|
129
|
+
// symlink/copy at ~/.<agent>/skills/pi-research) — a deeper-level check.
|
|
130
|
+
const anyAgentRootPresent = skillInstallCandidates().length > 0;
|
|
131
|
+
const skillInstalledAnywhere = skillUninstallCandidates().length > 0;
|
|
132
|
+
|
|
133
|
+
const initialItems: SettingItem[] = [
|
|
134
|
+
// ── Project-scoped settings (saved per-directory) ──
|
|
135
|
+
{
|
|
136
|
+
id: 'DEFAULT_RESEARCH_DEPTH',
|
|
137
|
+
label: '/research depth [project]',
|
|
138
|
+
description: 'Default depth for the /research command (normal, deep, ultra).\n[project] means configured independently per directory.',
|
|
139
|
+
currentValue: depthLabels[config.DEFAULT_RESEARCH_DEPTH] || String(config.DEFAULT_RESEARCH_DEPTH),
|
|
140
|
+
values: ['normal', 'deep', 'ultra'],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'KNOWLEDGE_STORE_MODE',
|
|
144
|
+
label: 'Knowledge Mode [project]',
|
|
145
|
+
description: 'Knowledge store scope — none (disabled), project (scope knowledge per this directory), or global (scope knowledge globally, shared across all projects).\n[project] means configured independently per directory.',
|
|
146
|
+
currentValue: config.KNOWLEDGE_STORE_MODE,
|
|
147
|
+
values: ['none', 'project', 'global'],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'RESEARCHER_TIMEOUT_MS',
|
|
151
|
+
label: 'Researcher Timeout',
|
|
152
|
+
description: 'Minutes before a stalled research track is forcefully cancelled.',
|
|
153
|
+
currentValue: String(Math.round(config.RESEARCHER_TIMEOUT_MS / 60000)),
|
|
154
|
+
values: ['3', '5', '10', '15', '20', '30'],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'MAX_CONCURRENT_RESEARCHERS',
|
|
158
|
+
label: 'Max Concurrency',
|
|
159
|
+
description: 'Parallel researcher threads. Reduce this if you hit API rate-limit errors.',
|
|
160
|
+
currentValue: String(config.MAX_CONCURRENT_RESEARCHERS),
|
|
161
|
+
values: ['1', '2', '3', '4', '5'],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'MAX_SCRAPE_BATCHES',
|
|
165
|
+
label: 'Scrape Batches',
|
|
166
|
+
description: 'Max URL batches a researcher can fetch (0 = unlimited). Lower values conserve API credit.',
|
|
167
|
+
currentValue: config.MAX_SCRAPE_BATCHES === 0 ? 'unlimited' : String(config.MAX_SCRAPE_BATCHES),
|
|
168
|
+
values: ['unlimited', '1', '2', '3', '5', '10', '15'],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'RESEARCH_REPORT_EXPORT_ENABLED',
|
|
172
|
+
label: 'Auto-export Report',
|
|
173
|
+
description: 'Write a markdown report to disk when each research run completes.',
|
|
174
|
+
currentValue: config.RESEARCH_REPORT_EXPORT_ENABLED ? 'true' : 'false',
|
|
175
|
+
values: ['true', 'false'],
|
|
176
|
+
},
|
|
177
|
+
// Browser Workers is intentionally NOT exposed here. It is an
|
|
178
|
+
// environment-only setting (PI_RESEARCH_WORKER_THREADS, default 4) so that
|
|
179
|
+
// CPU/RAM-sensitive concurrency is not casually changed from the menu.
|
|
180
|
+
...(anyKnowledgeStore ? [
|
|
181
|
+
{
|
|
182
|
+
id: 'EMBEDDING_MODEL',
|
|
183
|
+
label: 'Embedding Model',
|
|
184
|
+
description: 'Vector model for semantic search. Changing it clears the current knowledge store and starts fresh.',
|
|
185
|
+
currentValue: config.EMBEDDING_MODEL.split('/').pop()!,
|
|
186
|
+
values: SUPPORTED_MODELS.map(m => m.id.split('/').pop()!),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'EMBEDDING_DEVICE',
|
|
190
|
+
label: 'Embedding Device',
|
|
191
|
+
description: 'Hardware backend for the embedding model. GPU probes whether WebGPU actually runs on this machine and automatically falls back to CPU if it does not (safe everywhere). CPU forces CPU-only inference. (Raw forced-GPU is available via the PI_RESEARCH_EMBEDDING_DEVICE=webgpu env var for benchmarking.)',
|
|
192
|
+
currentValue: config.EMBEDDING_DEVICE === 'cpu' ? 'CPU' : 'GPU',
|
|
193
|
+
values: ['GPU', 'CPU'],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'KNOWLEDGE_STORE_CACHE_TTL_DAYS',
|
|
197
|
+
label: 'Cache Retention',
|
|
198
|
+
description: 'Days to retain findings before automated eviction. Longer = more disk, shorter = fresher data.',
|
|
199
|
+
currentValue: String(config.KNOWLEDGE_STORE_CACHE_TTL_DAYS),
|
|
200
|
+
values: ['7', '14', '30', '60', '90', '180', '365'],
|
|
201
|
+
},
|
|
202
|
+
] as SettingItem[] : []),
|
|
203
|
+
{
|
|
204
|
+
id: 'DEBUG',
|
|
205
|
+
label: 'Debug Logging',
|
|
206
|
+
description: `Write verbose diagnostics to ${buildDefaultDebugLogPath()} for troubleshooting.`,
|
|
207
|
+
currentValue: config.DEBUG ? 'true' : 'false',
|
|
208
|
+
values: ['true', 'false'],
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// ── Actions ──
|
|
212
|
+
{
|
|
213
|
+
id: 'ACTION_HEALTH',
|
|
214
|
+
label: 'Run Health Check',
|
|
215
|
+
description: 'Test browser pool, GPU, and knowledge store connectivity.',
|
|
216
|
+
currentValue: 'run',
|
|
217
|
+
values: ['run'],
|
|
218
|
+
},
|
|
219
|
+
...(anyKnowledgeStore ? [
|
|
220
|
+
{
|
|
221
|
+
id: 'ACTION_KNOWLEDGE_STATUS',
|
|
222
|
+
label: 'Store Status',
|
|
223
|
+
description: 'Show entry counts, disk usage, and the active embedding model.',
|
|
224
|
+
currentValue: 'run',
|
|
225
|
+
values: ['run'],
|
|
226
|
+
},
|
|
227
|
+
] as SettingItem[] : []),
|
|
228
|
+
...(config.KNOWLEDGE_STORE_MODE === 'project' ? [
|
|
229
|
+
{
|
|
230
|
+
id: 'ACTION_KNOWLEDGE_CLEAR_LOCAL',
|
|
231
|
+
label: 'Clear Project Store',
|
|
232
|
+
description: 'Permanently delete all entries tied to this project directory.',
|
|
233
|
+
currentValue: 'run',
|
|
234
|
+
values: ['run'],
|
|
235
|
+
},
|
|
236
|
+
] as SettingItem[] : []),
|
|
237
|
+
...(config.KNOWLEDGE_STORE_MODE === 'global' ? [
|
|
238
|
+
{
|
|
239
|
+
id: 'ACTION_KNOWLEDGE_CLEAR_GLOBAL',
|
|
240
|
+
label: 'Clear User Store',
|
|
241
|
+
description: 'Permanently delete all globally shared store entries.',
|
|
242
|
+
currentValue: 'run',
|
|
243
|
+
values: ['run'],
|
|
244
|
+
},
|
|
245
|
+
] as SettingItem[] : []),
|
|
246
|
+
{
|
|
247
|
+
id: 'ACTION_METRICS',
|
|
248
|
+
label: 'Session Metrics',
|
|
249
|
+
description: 'Show token usage, API cost estimates, and success rates. You will be offered the option to reset counters after viewing.',
|
|
250
|
+
currentValue: 'run',
|
|
251
|
+
values: ['run'],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'ACTION_LOGS_CLEAR',
|
|
255
|
+
label: 'Clear Debug Logs',
|
|
256
|
+
description: 'Delete the diagnostic log file and all archived rotation files.',
|
|
257
|
+
currentValue: 'run',
|
|
258
|
+
values: ['run'],
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// ── Coding-agent skill ──
|
|
262
|
+
// Install only shows when a target agent's root config dir exists; uninstall
|
|
263
|
+
// only shows when the skill is actually installed somewhere.
|
|
264
|
+
...(anyAgentRootPresent ? [
|
|
265
|
+
{
|
|
266
|
+
id: 'ACTION_SKILL_INSTALL',
|
|
267
|
+
label: 'Install in External Agents',
|
|
268
|
+
description: 'Symlink the pi-research skill into your other coding agents (Claude, Codex, OpenClaw) so they can run web research too. Only agents already set up on this machine are touched.',
|
|
269
|
+
currentValue: 'run',
|
|
270
|
+
values: ['run'],
|
|
271
|
+
},
|
|
272
|
+
] as SettingItem[] : []),
|
|
273
|
+
...(skillInstalledAnywhere ? [
|
|
274
|
+
{
|
|
275
|
+
id: 'ACTION_SKILL_UNINSTALL',
|
|
276
|
+
label: 'Remove from External Agents',
|
|
277
|
+
description: 'Remove the pi-research skill from your other coding agents (Claude, Codex, OpenClaw). Only the symlinks this extension created are removed; unrelated skills are left untouched.',
|
|
278
|
+
currentValue: 'run',
|
|
279
|
+
values: ['run'],
|
|
280
|
+
},
|
|
281
|
+
] as SettingItem[] : []),
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
setInteractiveTuiActive(true);
|
|
285
|
+
try {
|
|
286
|
+
await ctx.ui.custom(
|
|
287
|
+
(_tui: TUI, theme: Theme, _kb: any, done: (val: any) => void) => {
|
|
288
|
+
// The "[project]" tag marks settings that are saved independently per
|
|
289
|
+
// directory. It is rendered in the warning (yellow) color so it stands
|
|
290
|
+
// out, while the rest of the label keeps its normal/accent color. The
|
|
291
|
+
// tag is embedded in the label text (SettingsList pads it for alignment),
|
|
292
|
+
// so we split on it here rather than recoloring the whole padded string.
|
|
293
|
+
const PROJECT_TAG = '[project]';
|
|
294
|
+
const colorizeLabel = (text: string, selected: boolean): string => {
|
|
295
|
+
const idx = text.indexOf(PROJECT_TAG);
|
|
296
|
+
if (idx === -1) {
|
|
297
|
+
return selected ? theme.fg('accent', text) : text;
|
|
298
|
+
}
|
|
299
|
+
const before = text.slice(0, idx);
|
|
300
|
+
const after = text.slice(idx + PROJECT_TAG.length); // trailing alignment padding
|
|
301
|
+
const beforeColored = before ? (selected ? theme.fg('accent', before) : before) : '';
|
|
302
|
+
return `${beforeColored}${theme.fg('warning', PROJECT_TAG)}${after}`;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const listTheme = {
|
|
306
|
+
label: colorizeLabel,
|
|
307
|
+
value: (text: string, selected: boolean) => selected ? theme.fg('accent', text) : theme.fg('muted', text),
|
|
308
|
+
description: (text: string) => {
|
|
309
|
+
// SettingsList calls this once per wrapped description line, so we
|
|
310
|
+
// can't inspect the line alone — we look up the currently selected
|
|
311
|
+
// item (respecting the active search filter) and color the whole
|
|
312
|
+
// description yellow when it describes a destructive action.
|
|
313
|
+
const list = settingsList as unknown as {
|
|
314
|
+
searchEnabled?: boolean;
|
|
315
|
+
filteredItems?: SettingItem[];
|
|
316
|
+
items?: SettingItem[];
|
|
317
|
+
selectedIndex: number;
|
|
318
|
+
};
|
|
319
|
+
const displayItems = list.searchEnabled ? list.filteredItems : list.items;
|
|
320
|
+
const selectedItem = displayItems?.[list.selectedIndex];
|
|
321
|
+
const desc = selectedItem?.description?.toLowerCase() || '';
|
|
322
|
+
if (desc.includes('delete') || desc.includes('clear')) {
|
|
323
|
+
return theme.fg('warning', text);
|
|
324
|
+
}
|
|
325
|
+
return theme.fg('dim', text);
|
|
326
|
+
},
|
|
327
|
+
cursor: theme.fg('accent', '→ '),
|
|
328
|
+
hint: (text: string) => theme.fg('dim', text),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
let settingsList: SettingsList;
|
|
332
|
+
|
|
333
|
+
const wrappedDone = (val: any) => {
|
|
334
|
+
done(val);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
settingsList = new SettingsList(
|
|
338
|
+
initialItems,
|
|
339
|
+
10,
|
|
340
|
+
listTheme,
|
|
341
|
+
async (id, newValue) => {
|
|
342
|
+
// 1. Handle Settings Changes (Auto-save)
|
|
343
|
+
let changed = true;
|
|
344
|
+
let scope: 'local' | 'user' = 'local';
|
|
345
|
+
|
|
346
|
+
if (id === 'DEFAULT_RESEARCH_DEPTH') {
|
|
347
|
+
const depthMap: Record<string, number> = { 'normal': 1, 'deep': 2, 'ultra': 3 };
|
|
348
|
+
config.DEFAULT_RESEARCH_DEPTH = depthMap[newValue] || 1;
|
|
349
|
+
} else if (id === 'MAX_CONCURRENT_RESEARCHERS') {
|
|
350
|
+
config.MAX_CONCURRENT_RESEARCHERS = parseInt(newValue, 10);
|
|
351
|
+
scope = 'user';
|
|
352
|
+
} else if (id === 'MAX_SCRAPE_BATCHES') {
|
|
353
|
+
config.MAX_SCRAPE_BATCHES = newValue === 'unlimited' ? 0 : parseInt(newValue, 10);
|
|
354
|
+
scope = 'user';
|
|
355
|
+
} else if (id === 'RESEARCHER_TIMEOUT_MS') {
|
|
356
|
+
config.RESEARCHER_TIMEOUT_MS = parseInt(newValue, 10) * 60000;
|
|
357
|
+
scope = 'user';
|
|
358
|
+
} else if (id === 'DEBUG') {
|
|
359
|
+
config.DEBUG = newValue === 'true';
|
|
360
|
+
scope = 'user';
|
|
361
|
+
} else if (id === 'RESEARCH_REPORT_EXPORT_ENABLED') {
|
|
362
|
+
config.RESEARCH_REPORT_EXPORT_ENABLED = newValue === 'true';
|
|
363
|
+
scope = 'user';
|
|
364
|
+
} else if (id === 'KNOWLEDGE_STORE_MODE') {
|
|
365
|
+
config.KNOWLEDGE_STORE_MODE = newValue as 'none' | 'project' | 'global';
|
|
366
|
+
} else if (id === 'EMBEDDING_MODEL') {
|
|
367
|
+
config.EMBEDDING_MODEL = SUPPORTED_MODELS.find(m => m.id.split('/').pop() === newValue)?.id ?? newValue;
|
|
368
|
+
// DESTRUCTIVE + DEFERRED: changing the model clears the knowledge store, so it is
|
|
369
|
+
// NOT auto-saved on cycle like the other settings. Persist + clear happen in the
|
|
370
|
+
// close handler, gated on an explicit ctx.ui.confirm — and an Esc/cancel close
|
|
371
|
+
// reverts the change entirely (nothing was written here, so there is nothing to
|
|
372
|
+
// roll back on disk).
|
|
373
|
+
changed = false;
|
|
374
|
+
} else if (id === 'EMBEDDING_DEVICE') {
|
|
375
|
+
// TUI offers only GPU (= safe auto/probe-then-fallback) and CPU.
|
|
376
|
+
// Raw forced 'webgpu' (no probe) stays reachable via env var only.
|
|
377
|
+
config.EMBEDDING_DEVICE = newValue === 'CPU' ? 'cpu' : 'auto';
|
|
378
|
+
scope = 'user';
|
|
379
|
+
} else if (id === 'KNOWLEDGE_STORE_CACHE_TTL_DAYS') {
|
|
380
|
+
config.KNOWLEDGE_STORE_CACHE_TTL_DAYS = parseInt(newValue, 10);
|
|
381
|
+
scope = 'user';
|
|
382
|
+
} else {
|
|
383
|
+
changed = false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (changed) {
|
|
387
|
+
try {
|
|
388
|
+
// Persist ONLY the key that changed — for BOTH scopes. A local write scoped to
|
|
389
|
+
// all keys would freeze the sibling local key per-directory; a user write scoped
|
|
390
|
+
// to all keys would snapshot the entire resolved config (env overrides included)
|
|
391
|
+
// into config.env, freezing values the user never chose.
|
|
392
|
+
saveConfig(config, scope, cwd, [ENV_KEY_BY_SETTING_ID[id] ?? `PI_RESEARCH_${id}`]);
|
|
393
|
+
resetConfig();
|
|
394
|
+
} catch (e: unknown) {
|
|
395
|
+
ctx.ui.notify(`Failed to save: ${e instanceof Error ? e.message : String(e)}`, 'error');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// If Knowledge Mode was just turned on, warm the store in the background so the first
|
|
400
|
+
// search after enabling doesn't block on a cold init (native embed stack + init lock,
|
|
401
|
+
// a few seconds). Fire-and-forget: getStore() drives initialize() to completion. The
|
|
402
|
+
// search tool also awaits readiness, so this is a latency optimisation, not a
|
|
403
|
+
// correctness dependency — the store works without it, just slower on the first query.
|
|
404
|
+
if (id === 'KNOWLEDGE_STORE_MODE' && newValue !== 'none') {
|
|
405
|
+
const kmContainer = tryGetServiceContainerFromCtx(ctx);
|
|
406
|
+
void getService<IKnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, ctx, kmContainer)
|
|
407
|
+
.then((s) => s.getStore())
|
|
408
|
+
.catch((e) => logger.debug('[research-config] knowledge-store warm-up failed:', e));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 2. Handle Actions
|
|
412
|
+
if (id === 'ACTION_HEALTH') {
|
|
413
|
+
wrappedDone({ type: 'action', action: 'health' });
|
|
414
|
+
} else if (id === 'ACTION_KNOWLEDGE_STATUS') {
|
|
415
|
+
wrappedDone({ type: 'action', action: 'knowledge_status' });
|
|
416
|
+
} else if (id === 'ACTION_KNOWLEDGE_CLEAR_GLOBAL') {
|
|
417
|
+
wrappedDone({ type: 'action', action: 'knowledge_clear_global' });
|
|
418
|
+
} else if (id === 'ACTION_KNOWLEDGE_CLEAR_LOCAL') {
|
|
419
|
+
wrappedDone({ type: 'action', action: 'knowledge_clear_local' });
|
|
420
|
+
} else if (id === 'ACTION_METRICS') {
|
|
421
|
+
wrappedDone({ type: 'action', action: 'metrics' });
|
|
422
|
+
} else if (id === 'ACTION_LOGS_CLEAR') {
|
|
423
|
+
wrappedDone({ type: 'action', action: 'logs_clear' });
|
|
424
|
+
} else if (id === 'ACTION_SKILL_INSTALL') {
|
|
425
|
+
wrappedDone({ type: 'action', action: 'skill_install' });
|
|
426
|
+
} else if (id === 'ACTION_SKILL_UNINSTALL') {
|
|
427
|
+
wrappedDone({ type: 'action', action: 'skill_uninstall' });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
},
|
|
431
|
+
() => wrappedDone({ type: 'cancel' }),
|
|
432
|
+
{ enableSearch: true }
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
render: (width: number) => {
|
|
437
|
+
if (width < 4) return [];
|
|
438
|
+
const border = theme.fg('muted', '─'.repeat(width));
|
|
439
|
+
// Drop the blank separator lines the SettingsList widget inserts (before the
|
|
440
|
+
// description and before the hint) so the menu renders as one gap-free block.
|
|
441
|
+
const listLines = settingsList.render(width).filter(line => visibleWidth(line) > 0);
|
|
442
|
+
|
|
443
|
+
// Header with title and search hint
|
|
444
|
+
const titleText = 'Research Configuration';
|
|
445
|
+
const hintText = 'Type to search';
|
|
446
|
+
const header = `${titleText} ${theme.fg('dim', `(${hintText})`)}`;
|
|
447
|
+
|
|
448
|
+
// Footer: which directory the [project]-scoped settings apply to.
|
|
449
|
+
// "[project]" is yellow (matching the per-row tag); the rest is dim.
|
|
450
|
+
// Kept on a single line so the outer truncate clips it cleanly when
|
|
451
|
+
// the terminal is narrow.
|
|
452
|
+
const dirName = path.basename(cwd);
|
|
453
|
+
// Two-space indent so the footer's "[project]" lines up with the list rows
|
|
454
|
+
// and the "[project]" in the description (both use a 2-space left margin).
|
|
455
|
+
const pathInfoLine = ` ${theme.fg('warning', '[project]')}${theme.fg('dim', ` directory: ${dirName}`)}`;
|
|
456
|
+
|
|
457
|
+
// No blank line before the footer: keep it flush under the hint line.
|
|
458
|
+
const lines = [border, header, border, ...listLines, pathInfoLine];
|
|
459
|
+
return lines.map(line => {
|
|
460
|
+
if (visibleWidth(line) > width) {
|
|
461
|
+
return truncateToWidth(line, width);
|
|
462
|
+
}
|
|
463
|
+
return line;
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
handleInput: (data: string) => settingsList.handleInput(data),
|
|
467
|
+
invalidate: () => settingsList.invalidate(),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
// Rendered inline (default): the menu replaces the editor and takes
|
|
471
|
+
// keyboard focus. While a research run is active, the live research panel
|
|
472
|
+
// is removed from the screen for the duration of the menu (see
|
|
473
|
+
// setInteractiveTuiActive → hideMasterWidget), so the inline menu gets the
|
|
474
|
+
// full screen and leaves no ghost rows on close — without an overlay popup.
|
|
475
|
+
)
|
|
476
|
+
.then(async (result: any) => {
|
|
477
|
+
// 1. Handle Critical Config Changes (Model/Device)
|
|
478
|
+
// We do this after the TUI closes to avoid race conditions and redundant clears
|
|
479
|
+
let modelChangeConfirmed = false;
|
|
480
|
+
if (config.EMBEDDING_MODEL !== initialConfig.EMBEDDING_MODEL) {
|
|
481
|
+
// The model change was deliberately NOT auto-saved on cycle (see the change handler):
|
|
482
|
+
// it clears the knowledge store, so it needs the same explicit confirm as the other
|
|
483
|
+
// destructive actions — and an Esc/cancel close must abandon it without prompting.
|
|
484
|
+
const cancelled = result?.type === 'cancel';
|
|
485
|
+
modelChangeConfirmed = !cancelled && await ctx.ui.confirm(
|
|
486
|
+
'Change Embedding Model',
|
|
487
|
+
`Switch the embedding model to ${config.EMBEDDING_MODEL.split('/').pop()}? This permanently clears the current knowledge store.`,
|
|
488
|
+
);
|
|
489
|
+
if (modelChangeConfirmed) {
|
|
490
|
+
logger.info(`[research-config] Embedding model changed from ${initialConfig.EMBEDDING_MODEL} to ${config.EMBEDDING_MODEL}. Clearing knowledge store.`);
|
|
491
|
+
try {
|
|
492
|
+
saveConfig(config, 'user', cwd, ['PI_RESEARCH_EMBEDDING_MODEL']);
|
|
493
|
+
resetConfig();
|
|
494
|
+
} catch (e: unknown) {
|
|
495
|
+
modelChangeConfirmed = false;
|
|
496
|
+
ctx.ui.notify(`Failed to save: ${e instanceof Error ? e.message : String(e)}`, 'error');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (modelChangeConfirmed) {
|
|
500
|
+
try {
|
|
501
|
+
const service = await getService<KnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, ctx, container);
|
|
502
|
+
await service.clear();
|
|
503
|
+
await clearService(ServiceNames.KNOWLEDGE_STORE, container);
|
|
504
|
+
const { clearEmbeddingInstance } = await import('./infrastructure/embedding/embedding-factory.ts');
|
|
505
|
+
clearEmbeddingInstance();
|
|
506
|
+
ctx.ui.notify('Model updated. Store cleared.', 'info');
|
|
507
|
+
} catch (e: unknown) {
|
|
508
|
+
logger.warn('[research-config] Failed to clear knowledge store on model change:', e);
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
// Declined, cancelled, or the save failed: revert the in-memory value so nothing
|
|
512
|
+
// downstream acts on a model that was never persisted.
|
|
513
|
+
config.EMBEDDING_MODEL = initialConfig.EMBEDDING_MODEL;
|
|
514
|
+
if (!cancelled) ctx.ui.notify('Embedding model unchanged.', 'info');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!modelChangeConfirmed && config.EMBEDDING_DEVICE !== initialConfig.EMBEDDING_DEVICE) {
|
|
518
|
+
logger.info('[research-config] Device changed. Resetting service.');
|
|
519
|
+
try {
|
|
520
|
+
await clearService(ServiceNames.KNOWLEDGE_STORE, container);
|
|
521
|
+
const { clearEmbeddingInstance } = await import('./infrastructure/embedding/embedding-factory.ts');
|
|
522
|
+
clearEmbeddingInstance();
|
|
523
|
+
ctx.ui.notify('Device updated. Service refreshed.', 'info');
|
|
524
|
+
|
|
525
|
+
} catch (e: unknown) {
|
|
526
|
+
logger.warn('[research-config] Failed to refresh service on device change:', e);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 2. Handle Actions after the TUI closes to avoid UI conflicts
|
|
531
|
+
if (result?.type === 'action') {
|
|
532
|
+
switch (result.action) {
|
|
533
|
+
case 'health':
|
|
534
|
+
await runHealthCheckAction(ctx, pi);
|
|
535
|
+
break;
|
|
536
|
+
case 'knowledge_status':
|
|
537
|
+
await showKnowledgeStatusAction(ctx, pi);
|
|
538
|
+
break;
|
|
539
|
+
case 'knowledge_clear_global': {
|
|
540
|
+
const confirmed = await ctx.ui.confirm('Clear User Store', 'Permanently delete all user-scoped store data?');
|
|
541
|
+
if (confirmed) {
|
|
542
|
+
try {
|
|
543
|
+
const service = await getService<IKnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, ctx, container);
|
|
544
|
+
await service.clearGlobal();
|
|
545
|
+
ctx.ui.notify('User store cleared.', 'info');
|
|
546
|
+
} catch (e: unknown) {
|
|
547
|
+
ctx.ui.notify(`Clear failed: ${e instanceof Error ? e.message : String(e)}`, 'error');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case 'knowledge_clear_local': {
|
|
553
|
+
const confirmed = await ctx.ui.confirm('Clear Project Store', 'Permanently delete all project-scoped store data?');
|
|
554
|
+
if (confirmed) {
|
|
555
|
+
try {
|
|
556
|
+
const service = await getService<IKnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, ctx, container);
|
|
557
|
+
await service.clearLocal();
|
|
558
|
+
ctx.ui.notify('Project store cleared.', 'info');
|
|
559
|
+
} catch (e: unknown) {
|
|
560
|
+
ctx.ui.notify(`Clear failed: ${e instanceof Error ? e.message : String(e)}`, 'error');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case 'metrics':
|
|
566
|
+
await showMetricsAction(ctx, pi);
|
|
567
|
+
if (await ctx.ui.confirm('Session Metrics', 'Reset all session counters?')) {
|
|
568
|
+
metrics.clearSession();
|
|
569
|
+
ctx.ui.notify('Session metrics reset.', 'info');
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
case 'logs_clear': {
|
|
573
|
+
const confirmed = await ctx.ui.confirm('Clear Logs', 'Delete the main diagnostic log file and all archived rotation files?');
|
|
574
|
+
if (confirmed) {
|
|
575
|
+
logger.clear();
|
|
576
|
+
ctx.ui.notify('Diagnostic logs cleared.', 'info');
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case 'skill_install':
|
|
581
|
+
await installSkillAction(ctx, pi);
|
|
582
|
+
break;
|
|
583
|
+
case 'skill_uninstall':
|
|
584
|
+
await uninstallSkillAction(ctx, pi);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
} catch (error: unknown) {
|
|
590
|
+
logger.error(`[Config] Menu error: ${error instanceof Error ? error.message : String(error)}`);
|
|
591
|
+
} finally {
|
|
592
|
+
setInteractiveTuiActive(false);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ============================================================================
|
|
597
|
+
// Action Handlers (Internal)
|
|
598
|
+
// ============================================================================
|
|
599
|
+
|
|
600
|
+
/** id → human label for the coding agents the installer targets. */
|
|
601
|
+
const HARNESS_LABEL: Record<string, string> = Object.fromEntries(
|
|
602
|
+
HARNESSES.map(h => [h.id, h.label]),
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Symlink the pi-research skill into each target coding agent (Claude, Codex)
|
|
607
|
+
* whose ROOT config folder already exists. Agents without a config folder are
|
|
608
|
+
* skipped — we never create an agent's home dir. An agent slot already occupied
|
|
609
|
+
* by an unrelated skill is left untouched.
|
|
610
|
+
*/
|
|
611
|
+
async function installSkillAction(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
612
|
+
try {
|
|
613
|
+
const presentById = new Map(skillInstallCandidates().map(d => [d.id, true] as const));
|
|
614
|
+
const targets = SKILL_AGENT_TARGETS.filter(id => presentById.get(id));
|
|
615
|
+
|
|
616
|
+
// resolveSkillSourceDir (inside installSkill) throws if the bundled SKILL.md
|
|
617
|
+
// cannot be located — caught below and surfaced to the user.
|
|
618
|
+
const results = targets.length ? installSkill([...targets]) : [];
|
|
619
|
+
const resById = new Map(results.map(r => [r.tool, r]));
|
|
620
|
+
|
|
621
|
+
const lines: string[] = ['## pi-research skill — install', ''];
|
|
622
|
+
let ok = 0;
|
|
623
|
+
for (const id of SKILL_AGENT_TARGETS) {
|
|
624
|
+
const label = HARNESS_LABEL[id] ?? id;
|
|
625
|
+
if (!presentById.get(id)) {
|
|
626
|
+
lines.push(`- **${label}** — not detected on this machine; skipped.`);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const r = resById.get(id);
|
|
630
|
+
switch (r?.status) {
|
|
631
|
+
case 'installed':
|
|
632
|
+
ok++;
|
|
633
|
+
lines.push(`- **${label}** — ${r.type === 'copy' ? 'copied (symlink unavailable)' : 'symlinked'} → \`${r.path}\``);
|
|
634
|
+
break;
|
|
635
|
+
case 'already-installed':
|
|
636
|
+
ok++;
|
|
637
|
+
lines.push(`- **${label}** — already installed → \`${r.path}\``);
|
|
638
|
+
break;
|
|
639
|
+
case 'skipped-foreign':
|
|
640
|
+
lines.push(`- **${label}** — a different skill already occupies this slot; left untouched.`);
|
|
641
|
+
break;
|
|
642
|
+
case 'error':
|
|
643
|
+
lines.push(`- **${label}** — failed: ${r.message ?? 'unknown error'}`);
|
|
644
|
+
break;
|
|
645
|
+
default:
|
|
646
|
+
lines.push(`- **${label}** — ${r?.status ?? 'no result'}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
lines.push('', 'The skill activates automatically — just ask the agent to pi-research something.');
|
|
650
|
+
|
|
651
|
+
pi.sendMessage({ customType: 'skill-install-result', content: lines.join('\n'), display: true });
|
|
652
|
+
if (ctx.hasUI) {
|
|
653
|
+
if (ok > 0) ctx.ui.notify(`Skill installed for ${ok} coding agent${ok === 1 ? '' : 's'}.`, 'info');
|
|
654
|
+
else if (targets.length === 0) ctx.ui.notify('No coding agents detected (Claude, Codex).', 'warning');
|
|
655
|
+
else ctx.ui.notify('No agents installed — see the message for details.', 'warning');
|
|
656
|
+
}
|
|
657
|
+
} catch (e: unknown) {
|
|
658
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
659
|
+
logger.error(`[research-config] Skill install failed: ${msg}`);
|
|
660
|
+
if (ctx.hasUI) ctx.ui.notify(`Skill install failed: ${msg}`, 'error');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Remove the pi-research skill symlinks this extension created from Claude and
|
|
666
|
+
* Codex. Only installs we own are removed; foreign skills are kept.
|
|
667
|
+
*/
|
|
668
|
+
async function uninstallSkillAction(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
669
|
+
// Deeper-level gate than install: only act on agents where the skill is
|
|
670
|
+
// actually installed (an owned symlink/copy present on disk).
|
|
671
|
+
const installedTargets = skillUninstallCandidates();
|
|
672
|
+
if (installedTargets.length === 0) {
|
|
673
|
+
if (ctx.hasUI) ctx.ui.notify('The skill is not installed in any coding agent.', 'info');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const names = installedTargets.map(d => HARNESS_LABEL[d.id] ?? d.id).join(' and ');
|
|
678
|
+
const confirmed = await ctx.ui.confirm(
|
|
679
|
+
'Uninstall Skill',
|
|
680
|
+
`Remove the pi-research skill from ${names}? (Only symlinks this extension created are removed.)`,
|
|
681
|
+
);
|
|
682
|
+
if (!confirmed) return;
|
|
683
|
+
try {
|
|
684
|
+
const results = uninstallSkill(installedTargets.map(d => d.id));
|
|
685
|
+
|
|
686
|
+
const lines: string[] = ['## pi-research skill — uninstall', ''];
|
|
687
|
+
let removed = 0;
|
|
688
|
+
if (results.length === 0) {
|
|
689
|
+
lines.push('- Nothing to remove — the skill was not installed in any coding agent.');
|
|
690
|
+
} else {
|
|
691
|
+
for (const r of results) {
|
|
692
|
+
const label = HARNESS_LABEL[r.tool] ?? r.tool;
|
|
693
|
+
switch (r.status) {
|
|
694
|
+
case 'removed':
|
|
695
|
+
removed++;
|
|
696
|
+
lines.push(`- **${label}** — removed → \`${r.path}\``);
|
|
697
|
+
break;
|
|
698
|
+
case 'not-present':
|
|
699
|
+
lines.push(`- **${label}** — was not installed.`);
|
|
700
|
+
break;
|
|
701
|
+
case 'skipped-foreign':
|
|
702
|
+
lines.push(`- **${label}** — not ours; left untouched.`);
|
|
703
|
+
break;
|
|
704
|
+
case 'error':
|
|
705
|
+
lines.push(`- **${label}** — failed: ${r.message ?? 'unknown error'}`);
|
|
706
|
+
break;
|
|
707
|
+
default:
|
|
708
|
+
lines.push(`- **${label}** — ${r.status}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
pi.sendMessage({ customType: 'skill-uninstall-result', content: lines.join('\n'), display: true });
|
|
714
|
+
if (ctx.hasUI) {
|
|
715
|
+
ctx.ui.notify(removed > 0 ? `Skill removed from ${removed} coding agent${removed === 1 ? '' : 's'}.` : 'No installs were removed.', 'info');
|
|
716
|
+
}
|
|
717
|
+
} catch (e: unknown) {
|
|
718
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
719
|
+
logger.error(`[research-config] Skill uninstall failed: ${msg}`);
|
|
720
|
+
if (ctx.hasUI) ctx.ui.notify(`Skill uninstall failed: ${msg}`, 'error');
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function runHealthCheckAction(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
725
|
+
if (ctx.hasUI) {
|
|
726
|
+
ctx.ui.notify('Running health checks.', 'info');
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
const systemHealth = await healthRegistry.runAll({ force: true });
|
|
730
|
+
const outputLines: string[] = [];
|
|
731
|
+
outputLines.push('## System Health Status');
|
|
732
|
+
outputLines.push('');
|
|
733
|
+
|
|
734
|
+
const statusIcon = systemHealth.status === 'healthy' ? '[OK]' :
|
|
735
|
+
systemHealth.status === 'degraded' ? '[WARN]' : '[ERROR]';
|
|
736
|
+
outputLines.push(`**${statusIcon} Status: ${systemHealth.status.toUpperCase()}**`);
|
|
737
|
+
outputLines.push('');
|
|
738
|
+
|
|
739
|
+
for (const component of systemHealth.components) {
|
|
740
|
+
const icon = component.healthy ? '[OK]' : '[FAIL]';
|
|
741
|
+
outputLines.push(`${icon} **${component.component}**`);
|
|
742
|
+
if (component.error) outputLines.push(` - Error: ${component.error}`);
|
|
743
|
+
outputLines.push(` - Duration: ${component.durationMs.toFixed(1)}ms`);
|
|
744
|
+
outputLines.push('');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
pi.sendMessage({
|
|
748
|
+
customType: 'health-result',
|
|
749
|
+
content: outputLines.join('\n'),
|
|
750
|
+
display: true,
|
|
751
|
+
details: { health: systemHealth },
|
|
752
|
+
});
|
|
753
|
+
if (ctx.hasUI) {
|
|
754
|
+
ctx.ui.notify(`Health check complete: ${systemHealth.status}.`, 'info');
|
|
755
|
+
}
|
|
756
|
+
} catch (error: unknown) {
|
|
757
|
+
if (ctx.hasUI) {
|
|
758
|
+
ctx.ui.notify(`Health check failed: ${error instanceof Error ? error.message : String(error)}`, 'error');
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function showKnowledgeStatusAction(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
764
|
+
try {
|
|
765
|
+
const cwd = ctx.cwd || process.cwd();
|
|
766
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
767
|
+
// Reset config to ensure we read the latest from file (in case TUI changed it)
|
|
768
|
+
resetConfig();
|
|
769
|
+
const config = getConfig(cwd);
|
|
770
|
+
|
|
771
|
+
const service = await getService<KnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, ctx, container);
|
|
772
|
+
const store = await service.getStore();
|
|
773
|
+
if (!store) {
|
|
774
|
+
throw new Error('Knowledge store is disabled or not initialized');
|
|
775
|
+
}
|
|
776
|
+
const counts = await store.countScoped(cwd);
|
|
777
|
+
const dbDir = getDbDir(config, cwd);
|
|
778
|
+
|
|
779
|
+
pi.sendMessage({
|
|
780
|
+
customType: 'knowledge-status',
|
|
781
|
+
content: `## Knowledge Store\n\n- Status: Operational\n- Project Entries: ${counts.local}\n- User Entries: ${counts.global}\n- Total Projects: ${counts.projects}\n- Model: ${config.EMBEDDING_MODEL}\n- Device: ${config.EMBEDDING_DEVICE}\n- Path: \`${dbDir}\``,
|
|
782
|
+
display: true,
|
|
783
|
+
});
|
|
784
|
+
} catch (error: unknown) {
|
|
785
|
+
if (ctx.hasUI) {
|
|
786
|
+
ctx.ui.notify(`Failed to get knowledge status: ${error instanceof Error ? error.message : String(error)}`, 'error');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function showMetricsAction(_ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
792
|
+
const runHistory = metrics.getRunHistory();
|
|
793
|
+
const sessionStart = metrics.getSessionStartedAt();
|
|
794
|
+
|
|
795
|
+
const lines: string[] = ['## Session Metrics', ''];
|
|
796
|
+
lines.push(`Session started ${formatTimeAgo(new Date(sessionStart).toISOString())}`);
|
|
797
|
+
lines.push('');
|
|
798
|
+
|
|
799
|
+
// ── Session Overview ──────────────────────────────────────────────────
|
|
800
|
+
const sessionStats = aggregateSessionStats(runHistory);
|
|
801
|
+
sessionStats.sessionStartedAt = sessionStart;
|
|
802
|
+
|
|
803
|
+
if (runHistory.length === 0) {
|
|
804
|
+
lines.push('_No research runs in this session._');
|
|
805
|
+
} else {
|
|
806
|
+
lines.push(buildSessionOverview(sessionStats));
|
|
807
|
+
lines.push('');
|
|
808
|
+
|
|
809
|
+
// ── Last Run Detail ──────────────────────────────────────────────────
|
|
810
|
+
const lastRun = runHistory[runHistory.length - 1]!;
|
|
811
|
+
const lastStats = extractRunStats(lastRun.snapshot);
|
|
812
|
+
|
|
813
|
+
lines.push(`### Last Run`);
|
|
814
|
+
const statusIcon = lastRun.status === 'success' ? '[ok]'
|
|
815
|
+
: lastRun.status === 'cancelled' ? '[cancelled]'
|
|
816
|
+
: '[failed]';
|
|
817
|
+
lines.push(`${statusIcon} \`${lastRun.runId.slice(0, 8)}\` — ${formatDuration(lastRun.durationMs)} — completed ${formatTimeAgo(new Date(lastRun.completedAt).toISOString())}`);
|
|
818
|
+
lines.push('');
|
|
819
|
+
|
|
820
|
+
if (lastStats) {
|
|
821
|
+
const activityParts: string[] = [];
|
|
822
|
+
if (lastStats.researchersLaunched > 0) activityParts.push(`**${lastStats.researchersLaunched}** researchers`);
|
|
823
|
+
if (lastStats.roundsCompleted > 0) activityParts.push(`**${lastStats.roundsCompleted}** rounds`);
|
|
824
|
+
if (activityParts.length > 0) lines.push(activityParts.join(' · '));
|
|
825
|
+
|
|
826
|
+
const discoveryParts: string[] = [];
|
|
827
|
+
if (lastStats.searchQueries > 0) discoveryParts.push(`**${lastStats.searchQueries}** searches`);
|
|
828
|
+
if (lastStats.urlsDiscovered > 0) discoveryParts.push(`**${lastStats.urlsDiscovered}** discovered`);
|
|
829
|
+
if (lastStats.urlsAnalyzed > 0) {
|
|
830
|
+
const layerParts: string[] = [`${lastStats.urlsAnalyzed} analyzed`];
|
|
831
|
+
if (lastStats.fetchSuccess > 0 && lastStats.browserSuccess > 0) {
|
|
832
|
+
layerParts.push(`${lastStats.fetchSuccess} fetch`);
|
|
833
|
+
layerParts.push(`${lastStats.browserSuccess} browser`);
|
|
834
|
+
}
|
|
835
|
+
discoveryParts.push(`**${layerParts.join(', ')}**`);
|
|
836
|
+
}
|
|
837
|
+
if (discoveryParts.length > 0) lines.push(discoveryParts.join(' · '));
|
|
838
|
+
|
|
839
|
+
const toolParts: string[] = [];
|
|
840
|
+
if (lastStats.toolUsage.searches > 0) toolParts.push(`${lastStats.toolUsage.searches} searches`);
|
|
841
|
+
if (lastStats.toolUsage.scrapes > 0) toolParts.push(`${lastStats.toolUsage.scrapes} scrapes`);
|
|
842
|
+
if (lastStats.toolUsage.securitySearches > 0) toolParts.push(`${lastStats.toolUsage.securitySearches} security`);
|
|
843
|
+
if (lastStats.toolUsage.stackexchangeQueries > 0) toolParts.push(`${lastStats.toolUsage.stackexchangeQueries} StackExchange`);
|
|
844
|
+
if (lastStats.toolUsage.knowledgeLookups > 0) toolParts.push(`${lastStats.toolUsage.knowledgeLookups} knowledge`);
|
|
845
|
+
if (toolParts.length > 0) lines.push(`Tools: ${toolParts.join(' · ')}`);
|
|
846
|
+
|
|
847
|
+
const resourceParts: string[] = [];
|
|
848
|
+
if (lastStats.tokens > 0) resourceParts.push(`**${lastStats.tokens.toLocaleString('en-US')}** tokens`);
|
|
849
|
+
if (lastStats.urlsFailed > 0) resourceParts.push(`**${lastStats.urlsFailed}** failed`);
|
|
850
|
+
if (lastStats.errors > 0) resourceParts.push(`**${lastStats.errors}** errors`);
|
|
851
|
+
if (resourceParts.length > 0) lines.push(resourceParts.join(' · '));
|
|
852
|
+
|
|
853
|
+
lines.push('');
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ── Prior Runs ─────────────────────────────────────────────────────
|
|
857
|
+
if (runHistory.length > 1) {
|
|
858
|
+
const prior = runHistory.slice(0, -1).slice().reverse();
|
|
859
|
+
lines.push(`### Prior Runs`);
|
|
860
|
+
for (const run of prior) {
|
|
861
|
+
lines.push(buildRunCompactLine(run as any));
|
|
862
|
+
}
|
|
863
|
+
lines.push('');
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
pi.sendMessage({
|
|
868
|
+
customType: 'metrics-result',
|
|
869
|
+
content: lines.join('\n'),
|
|
870
|
+
display: true,
|
|
871
|
+
});
|
|
872
|
+
}
|