@lincoln504/pi-research 1.6.8 → 1.6.10

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/README.md CHANGED
@@ -53,6 +53,10 @@ tries to compile from source; upgrading npm fixes it.
53
53
 
54
54
  In pi it works out of the box on the session's model and pi's configuration. Standalone use ([agent skill](docs/AGENT-SKILL.md) or [SDK](docs/SDK.md)) needs a model configured. See [Configuration](docs/CONFIGURATION.md).
55
55
 
56
+ ### Uninstall
57
+
58
+ `pi remove npm:@lincoln504/pi-research` removes the extension, and `npm uninstall -g @lincoln504/pi-research` the standalone engine. npm 7+ no longer runs `preuninstall`, so **nothing else is removed on its own**: skill links into other agents (Claude Code, Codex, …), the state directory (`~/.pi/research/state`), and the cache (`~/.cache/pi-research`, including any downloaded embedding models) stay in place. Remove the skill links first with `pi-research skill uninstall` (or `/research-config` → Remove from External Agents); the shared stealth-browser cache (`~/.cache/camoufox`) is preserved unless `PI_RESEARCH_PURGE_BROWSERS=1`. See [AGENT-SKILL.md](docs/AGENT-SKILL.md#installation-flow) for the full picture.
59
+
56
60
  ### How it works
57
61
 
58
62
  A research run loops through agent teams: a coordinator plans and starts with a search, researcher agents scrape and read in parallel, and a research lead decides whether to go another round, then writes the report from every report collected. The result is one cited Markdown report with findings optionally saved to the knowledge store.
@@ -2,7 +2,7 @@
2
2
  name: pi-research
3
3
  description: "Multi-agent web research for any coding agent. Use for anything requiring current information from the internet — news, trends, product/service/company facts, statistics, people, places, events, APIs, library behavior, or 'what is X / how does X work' questions. Also use to search a local knowledge store of previously researched findings before doing live research. Powered by the pi-research engine (search, stealth scraping, security databases, Stack Exchange). Not for reading local files, running project commands, or analyzing this repository's own source."
4
4
  license: MIT
5
- metadata: { "author": "Lincoln Deen", "version": "1.6.8", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.6.10", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
6
6
  allowed-tools: Bash(node:*)
7
7
  ---
8
8
 
package/dist/cli.mjs CHANGED
@@ -2387,6 +2387,46 @@ var init_atomic_replace = __esm({
2387
2387
  }
2388
2388
  });
2389
2389
 
2390
+ // src/knowledge/availability.ts
2391
+ import { createRequire as createRequire3 } from "node:module";
2392
+ function probeKnowledgeStoreAvailability(resolver = defaultResolver) {
2393
+ if (cachedIsValidFor(resolver)) return _cached;
2394
+ const missing = [];
2395
+ for (const pkg of REQUIRED_PACKAGES) {
2396
+ try {
2397
+ resolver(pkg);
2398
+ } catch {
2399
+ missing.push(pkg);
2400
+ }
2401
+ }
2402
+ const result = { available: missing.length === 0, missing };
2403
+ if (resolver === defaultResolver) _cached = result;
2404
+ return result;
2405
+ }
2406
+ function cachedIsValidFor(resolver) {
2407
+ return _cached !== null && resolver === defaultResolver;
2408
+ }
2409
+ function describeKnowledgeStoreUnavailability(availability) {
2410
+ if (availability.available) return "";
2411
+ const parts = availability.missing.map(
2412
+ (pkg) => pkg === "@huggingface/transformers" ? `'${pkg}' (optional embedding dependency \u2014 likely skipped at install time)` : `'${pkg}' (broken install \u2014 native vector store missing)`
2413
+ );
2414
+ return `knowledge store is OFF: required package${parts.length > 1 ? "s" : ""} not resolvable \u2014 ${parts.join("; ")}. Repair with a full install of optional dependencies (e.g. 'npm install' including optional deps, or pi install npm:@lincoln504/pi-research without --omit=optional).`;
2415
+ }
2416
+ var REQUIRED_PACKAGES, _cached, defaultResolver;
2417
+ var init_availability = __esm({
2418
+ "src/knowledge/availability.ts"() {
2419
+ "use strict";
2420
+ REQUIRED_PACKAGES = ["@lancedb/lancedb", "@huggingface/transformers"];
2421
+ _cached = null;
2422
+ defaultResolver = (specifier) => (
2423
+ // import.meta.url survives esbuild's ESM output; createRequire only builds a
2424
+ // resolver — it never evaluates the specifier.
2425
+ createRequire3(import.meta.url).resolve(specifier)
2426
+ );
2427
+ }
2428
+ });
2429
+
2390
2430
  // src/config.ts
2391
2431
  import * as fs5 from "node:fs";
2392
2432
  import * as path5 from "node:path";
@@ -2593,7 +2633,19 @@ function describeKnowledgeStoreMode(cwd = process.cwd(), iface) {
2593
2633
  else if (iface && fileHasKey(getInterfaceEnvFilePath(iface))) origin = `${iface}.env overlay`;
2594
2634
  else if (fileHasKey(getGlobalEnvFilePath())) origin = "config.env (machine-wide default)";
2595
2635
  }
2596
- return { mode: cfg.KNOWLEDGE_STORE_MODE, origin, dbDir: getDbDir(cfg, cwd) };
2636
+ return {
2637
+ mode: cfg.KNOWLEDGE_STORE_MODE,
2638
+ origin,
2639
+ dbDir: getDbDir(cfg, cwd),
2640
+ ...availabilityFields()
2641
+ };
2642
+ }
2643
+ function availabilityFields() {
2644
+ const availability = probeKnowledgeStoreAvailability();
2645
+ return {
2646
+ available: availability.available,
2647
+ unavailableReason: describeKnowledgeStoreUnavailability(availability)
2648
+ };
2597
2649
  }
2598
2650
  function parseDotEnv(content) {
2599
2651
  const out = {};
@@ -2744,6 +2796,7 @@ function saveConfig(config, scope = "local", cwd = process.cwd(), changedKeys) {
2744
2796
  PI_RESEARCH_MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING: String(config.MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING),
2745
2797
  PI_RESEARCH_AVG_TOKENS_PER_SCRAPE: String(config.AVG_TOKENS_PER_SCRAPE),
2746
2798
  PI_RESEARCH_MAX_CONCURRENT_SCRAPES: String(config.MAX_CONCURRENT_SCRAPES),
2799
+ PI_RESEARCH_MAX_SCRAPE_URLS: String(config.MAX_SCRAPE_URLS),
2747
2800
  PI_RESEARCH_YOUTUBE_TRANSCRIPT_MAX_VIDEOS: String(config.YOUTUBE_TRANSCRIPT_MAX_VIDEOS),
2748
2801
  PI_RESEARCH_YOUTUBE_TRANSCRIPT_TIMEOUT_MS: String(config.YOUTUBE_TRANSCRIPT_TIMEOUT_MS),
2749
2802
  PI_RESEARCH_YOUTUBE_TRANSCRIPT_LANG: config.YOUTUBE_TRANSCRIPT_LANG,
@@ -2905,6 +2958,7 @@ function createConfig(env, processEnv) {
2905
2958
  MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING: parseEnvNumber(e, "PI_RESEARCH_MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING", DEFAULTS.MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING, 0.05, 1),
2906
2959
  AVG_TOKENS_PER_SCRAPE: parseEnvNumber(e, "PI_RESEARCH_AVG_TOKENS_PER_SCRAPE", DEFAULTS.AVG_TOKENS_PER_SCRAPE, 500, 1e4),
2907
2960
  MAX_CONCURRENT_SCRAPES: parseEnvNumber(e, "PI_RESEARCH_MAX_CONCURRENT_SCRAPES", DEFAULTS.MAX_CONCURRENT_SCRAPES, 1, 20, true),
2961
+ MAX_SCRAPE_URLS: parseEnvNumber(e, "PI_RESEARCH_MAX_SCRAPE_URLS", DEFAULTS.MAX_SCRAPE_URLS, 1, 20, true),
2908
2962
  YOUTUBE_TRANSCRIPT_MAX_VIDEOS: parseEnvNumber(e, "PI_RESEARCH_YOUTUBE_TRANSCRIPT_MAX_VIDEOS", DEFAULTS.YOUTUBE_TRANSCRIPT_MAX_VIDEOS, 1, 5, true),
2909
2963
  YOUTUBE_TRANSCRIPT_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_YOUTUBE_TRANSCRIPT_TIMEOUT_MS", DEFAULTS.YOUTUBE_TRANSCRIPT_TIMEOUT_MS, 5e3, 12e4),
2910
2964
  YOUTUBE_TRANSCRIPT_LANG: parseEnvString(e, "PI_RESEARCH_YOUTUBE_TRANSCRIPT_LANG", DEFAULTS.YOUTUBE_TRANSCRIPT_LANG),
@@ -3002,6 +3056,7 @@ var init_config = __esm({
3002
3056
  init_host_config();
3003
3057
  init_text_utils();
3004
3058
  init_atomic_replace();
3059
+ init_availability();
3005
3060
  ConfigSchema = Type.Object({
3006
3061
  /** Per-researcher timeout in milliseconds (default: 300000, range: 3-30 min) */
3007
3062
  RESEARCHER_TIMEOUT_MS: Type.Number({ minimum: 18e4, maximum: 18e5, default: 3e5 }),
@@ -3046,8 +3101,10 @@ var init_config = __esm({
3046
3101
  MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING: Type.Number({ minimum: 0.05, maximum: 1, default: 0.15 }),
3047
3102
  /** Estimated tokens per scrape result for planning (default: 2500) */
3048
3103
  AVG_TOKENS_PER_SCRAPE: Type.Number({ minimum: 500, maximum: 1e4, default: 2500 }),
3049
- /** Maximum number of concurrent scrapes (default: 3) */
3104
+ /** Maximum concurrent scrapes (default: 3) */
3050
3105
  MAX_CONCURRENT_SCRAPES: Type.Number({ minimum: 1, maximum: 20, default: 3 }),
3106
+ /** Maximum URLs fetched per scrape batch; extras are listed under "Not Fetched — Over Batch Cap" (default: 8, range: 1-20) */
3107
+ MAX_SCRAPE_URLS: Type.Number({ minimum: 1, maximum: 20, default: 8 }),
3051
3108
  /** Max YouTube videos transcribed per youtube_transcript call (default: 3, range 1-5) */
3052
3109
  YOUTUBE_TRANSCRIPT_MAX_VIDEOS: Type.Number({ minimum: 1, maximum: 5, default: 3 }),
3053
3110
  /** Per-video timeout for the youtube_transcript tool in ms (default: 20000, range 5-120s) */
@@ -3184,6 +3241,7 @@ var init_config = __esm({
3184
3241
  "PI_RESEARCH_MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING",
3185
3242
  "PI_RESEARCH_AVG_TOKENS_PER_SCRAPE",
3186
3243
  "PI_RESEARCH_MAX_CONCURRENT_SCRAPES",
3244
+ "PI_RESEARCH_MAX_SCRAPE_URLS",
3187
3245
  "PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS",
3188
3246
  "PI_RESEARCH_SEARCH_TIMEOUT_MS",
3189
3247
  "PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS",
@@ -3240,6 +3298,13 @@ function getMaxScrapeBatches(config, cachingActive = false) {
3240
3298
  return 2;
3241
3299
  }
3242
3300
  }
3301
+ function getMaxScrapeUrls(config) {
3302
+ try {
3303
+ return (config || getConfig()).MAX_SCRAPE_URLS;
3304
+ } catch {
3305
+ return MAX_SCRAPE_URLS;
3306
+ }
3307
+ }
3243
3308
  function resolveExcludedTools(callerExclusions, configDisabled = []) {
3244
3309
  return [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_TOOLS, ...callerExclusions ?? [], ...configDisabled])];
3245
3310
  }
@@ -3250,7 +3315,7 @@ var init_constants = __esm({
3250
3315
  init_config();
3251
3316
  MAX_GATHERING_CALLS = 12;
3252
3317
  MAX_GREP_CALLS = 30;
3253
- MAX_SCRAPE_URLS = 6;
3318
+ MAX_SCRAPE_URLS = 8;
3254
3319
  MAX_TEAM_SIZE_LEVEL_1 = 2;
3255
3320
  MAX_TEAM_SIZE_LEVEL_2 = 3;
3256
3321
  MAX_TEAM_SIZE_LEVEL_3 = 5;
@@ -9007,11 +9072,15 @@ function getMigrationStrategy(config) {
9007
9072
  logger.warn(`[knowledge] Invalid migration strategy '${strategy}'. Valid options: drop, re-embed, backup. Falling back to default (backup).`);
9008
9073
  return void 0;
9009
9074
  }
9010
- async function createKnowledgeStoreComponents(embedderFactory, reconnectFactory, withLock, config = getConfig(), workspace = process.cwd()) {
9075
+ async function createKnowledgeStoreComponents(embedderFactory, reconnectFactory, withLock, config = getConfig(), workspace = process.cwd(), availability = probeKnowledgeStoreAvailability()) {
9011
9076
  if (config.KNOWLEDGE_STORE_MODE === "none") {
9012
9077
  logger.debug("[knowledge] Knowledge store is disabled in configuration");
9013
9078
  return null;
9014
9079
  }
9080
+ if (!availability.available) {
9081
+ logger.warn(`[knowledge] ${describeKnowledgeStoreUnavailability(availability)}`);
9082
+ return null;
9083
+ }
9015
9084
  validateConfig(config);
9016
9085
  for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
9017
9086
  let store;
@@ -9089,6 +9158,7 @@ var init_knowledge = __esm({
9089
9158
  init_config();
9090
9159
  init_logger();
9091
9160
  init_model_config();
9161
+ init_availability();
9092
9162
  init_model_config();
9093
9163
  MAX_INIT_RETRIES = 5;
9094
9164
  BASE_RETRY_DELAY_MS = 1e3;
@@ -9115,6 +9185,7 @@ var init_knowledge_store_service = __esm({
9115
9185
  init_embedding_factory();
9116
9186
  init_config();
9117
9187
  init_embedder_utils();
9188
+ init_availability();
9118
9189
  KnowledgeStoreService = class {
9119
9190
  name = ServiceNames.KNOWLEDGE_STORE;
9120
9191
  lifecycle = "uninitialized" /* UNINITIALIZED */;
@@ -9255,9 +9326,10 @@ var init_knowledge_store_service = __esm({
9255
9326
  }
9256
9327
  }
9257
9328
  if (!components) {
9258
- logger.debug("[KnowledgeStoreService] Knowledge store is disabled. Setting lifecycle to DISABLED.");
9329
+ const depsMissing = !probeKnowledgeStoreAvailability().available;
9330
+ logger.debug(`[KnowledgeStoreService] Knowledge store is disabled (${depsMissing ? "dependencies missing" : "mode = none"}). Setting lifecycle to DISABLED.`);
9259
9331
  this.lifecycle = "disabled" /* DISABLED */;
9260
- this._disabledReason = "mode";
9332
+ this._disabledReason = depsMissing ? "native" : "mode";
9261
9333
  this._embedder = null;
9262
9334
  this._store = null;
9263
9335
  this._writerQueue = null;
@@ -10104,6 +10176,9 @@ function missResult(reason) {
10104
10176
  case "store_disabled":
10105
10177
  text = "No results found (knowledge store is disabled in settings). Live research can get the info.";
10106
10178
  break;
10179
+ case "store_packages_missing":
10180
+ text = "No results found (knowledge store is off \u2014 its required packages are not installed on this host, so it stays off regardless of settings). Live research can get the info.";
10181
+ break;
10107
10182
  case "store_not_ready":
10108
10183
  text = "No results found (knowledge store is initializing). Live research can get the info.";
10109
10184
  break;
@@ -10166,8 +10241,10 @@ function createResearchKnowledgeSearchTool(iface) {
10166
10241
  }
10167
10242
  const store = await storeService.getStore();
10168
10243
  if (!store) {
10169
- const lifecycle = storeService.lifecycle;
10170
- return missResult(lifecycle === "disabled" ? "store_disabled" : "store_not_ready");
10244
+ const disabledReason = storeService.getDisabledReason();
10245
+ if (disabledReason === "native") return missResult("store_packages_missing");
10246
+ if (disabledReason === "mode") return missResult("store_disabled");
10247
+ return missResult("store_not_ready");
10171
10248
  }
10172
10249
  const count = await store.count();
10173
10250
  if (count === 0) {
@@ -12537,7 +12614,7 @@ import * as crypto4 from "node:crypto";
12537
12614
  import { join as join8 } from "node:path";
12538
12615
  import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
12539
12616
  import { platform, homedir as homedir3 } from "node:os";
12540
- import { createRequire as createRequire3 } from "node:module";
12617
+ import { createRequire as createRequire4 } from "node:module";
12541
12618
  function getWindowsCamoufoxDir() {
12542
12619
  return join8(homedir3(), "AppData", "Local", "camoufox", "camoufox", "Cache");
12543
12620
  }
@@ -12655,7 +12732,7 @@ function probeBrowserNativeDeps(load = defaultNativeDepLoad) {
12655
12732
  }
12656
12733
  }
12657
12734
  function defaultNativeDepLoad() {
12658
- const require2 = createRequire3(import.meta.url);
12735
+ const require2 = createRequire4(import.meta.url);
12659
12736
  const Database = require2("better-sqlite3");
12660
12737
  const db = new Database(":memory:");
12661
12738
  db.close();
@@ -15988,7 +16065,7 @@ import { FixedClusterPool, WorkerChoiceStrategies } from "poolifier";
15988
16065
  // src/infrastructure/browser/ensure-browser.ts
15989
16066
  init_logger();
15990
16067
  import { spawn as spawn2 } from "node:child_process";
15991
- import { createRequire as createRequire4 } from "node:module";
16068
+ import { createRequire as createRequire5 } from "node:module";
15992
16069
  import { createHash as createHash2 } from "node:crypto";
15993
16070
  import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3, fstatSync, openSync as openSync4, closeSync as closeSync4, rmSync } from "node:fs";
15994
16071
  import { join as join11, dirname as dirname8 } from "node:path";
@@ -16015,7 +16092,7 @@ function isBrowserBinaryPresent() {
16015
16092
  }
16016
16093
  function resolveCamoufoxBin() {
16017
16094
  try {
16018
- const require2 = createRequire4(import.meta.url);
16095
+ const require2 = createRequire5(import.meta.url);
16019
16096
  const pkgJson = require2.resolve("camoufox-js/package.json");
16020
16097
  const bin = join11(dirname8(pkgJson), "..", ".bin", process.platform === "win32" ? "camoufox-js.cmd" : "camoufox-js");
16021
16098
  if (existsSync7(bin)) return bin;
@@ -17939,6 +18016,7 @@ healthRegistry.initialize().catch(() => {
17939
18016
 
17940
18017
  // src/healthcheck/index.ts
17941
18018
  init_embedder_utils();
18019
+ init_availability();
17942
18020
  async function checkBrowserCapability() {
17943
18021
  const mockMode = process.env["PI_RESEARCH_MOCK_SEARCH"] === "true" && process.env["PI_RESEARCH_MOCK_SCRAPE"] === "true";
17944
18022
  if (isBrowserAvailable() || mockMode) {
@@ -17957,6 +18035,11 @@ async function checkBrowserCapability() {
17957
18035
  return { healthy: false, error: 'Camoufox (browser) not found. Run "npx camoufox-js fetch" to install the browser.' };
17958
18036
  }
17959
18037
  }
18038
+ function knowledgeStoreDisabledStatus() {
18039
+ const availability = probeKnowledgeStoreAvailability();
18040
+ const detail = availability.available ? "native embedding/vector stack unavailable on this platform" : `required packages not resolvable \u2014 ${availability.missing.join(", ")}`;
18041
+ return `disabled (${detail})`;
18042
+ }
17960
18043
  function registerHealthChecks(registry, container = getServiceContainer()) {
17961
18044
  const config = getConfig();
17962
18045
  const healthTimeoutMs = config.HEALTH_CHECK_TIMEOUT_MS;
@@ -17988,7 +18071,7 @@ function registerHealthChecks(registry, container = getServiceContainer()) {
17988
18071
  if (existing?.lifecycle === "disabled" /* DISABLED */) {
17989
18072
  const reason = existing.getDisabledReason();
17990
18073
  return { healthy: true, diagnostic: {
17991
- status: reason === "native" ? "disabled (native embedding/vector stack unavailable on this platform)" : "disabled"
18074
+ status: reason === "native" ? knowledgeStoreDisabledStatus() : "disabled"
17992
18075
  } };
17993
18076
  }
17994
18077
  const status = existing?.lifecycle === "initializing" /* INITIALIZING */ ? "initializing" : existing?.lifecycle === "disposed" /* DISPOSED */ || existing?.lifecycle === "disposing" /* DISPOSING */ ? "disposed (not running)" : "ready (idle)";
@@ -18007,12 +18090,12 @@ function registerHealthChecks(registry, container = getServiceContainer()) {
18007
18090
  return { healthy: true, diagnostic: { status: "disabled in config" } };
18008
18091
  }
18009
18092
  if (isNativeStackUnavailableError(initError)) {
18010
- return { healthy: true, diagnostic: { status: "disabled (native embedding/vector stack unavailable on this platform)" } };
18093
+ return { healthy: true, diagnostic: { status: knowledgeStoreDisabledStatus() } };
18011
18094
  }
18012
18095
  if (service?.lifecycle === "disabled" /* DISABLED */) {
18013
18096
  const reason = service.getDisabledReason();
18014
18097
  return { healthy: true, diagnostic: {
18015
- status: reason === "native" ? "disabled (native embedding/vector stack unavailable on this platform)" : "disabled"
18098
+ status: reason === "native" ? knowledgeStoreDisabledStatus() : "disabled"
18016
18099
  } };
18017
18100
  }
18018
18101
  try {
@@ -18061,7 +18144,7 @@ function registerHealthChecks(registry, container = getServiceContainer()) {
18061
18144
  };
18062
18145
  } catch (e) {
18063
18146
  if (isNativeStackUnavailableError(e)) {
18064
- return { healthy: true, diagnostic: { status: "disabled (native embedding/vector stack unavailable on this platform)" } };
18147
+ return { healthy: true, diagnostic: { status: knowledgeStoreDisabledStatus() } };
18065
18148
  }
18066
18149
  return { healthy: false, error: `Knowledge store healthcheck failed: ${e instanceof Error ? e.message : String(e)}` };
18067
18150
  }
@@ -19261,6 +19344,7 @@ init_cancellation();
19261
19344
  function createScrapeTool(options) {
19262
19345
  const config = options.config || getConfig(options.ctx.cwd);
19263
19346
  const maxScrapeBatches = options.tracker?.getToolLimit("scrape") ?? getMaxScrapeBatches(config);
19347
+ const maxScrapeUrls = getMaxScrapeUrls(config);
19264
19348
  const container = tryGetServiceContainerFromCtx(options.ctx);
19265
19349
  const fallbackState = {
19266
19350
  version: 1,
@@ -19279,7 +19363,7 @@ function createScrapeTool(options) {
19279
19363
  fallbackState.allScrapedLinks = [.../* @__PURE__ */ new Set([...fallbackState.allScrapedLinks, ...links])];
19280
19364
  });
19281
19365
  const ScrapeParamsSchema = Type5.Object({
19282
- urls: Type5.Array(Type5.String({ description: `The URLs to scrape (only the first ${MAX_SCRAPE_URLS} are fetched per call; extras are reported under "Not Fetched" rather than silently dropped)` }), { minItems: 1, maxItems: 20 }),
19366
+ urls: Type5.Array(Type5.String({ description: `The URLs to scrape (only the first ${maxScrapeUrls} are fetched per call; extras are reported under "Not Fetched" rather than silently dropped)` }), { minItems: 1, maxItems: 20 }),
19283
19367
  maxConcurrency: Type5.Optional(Type5.Number({ default: config.MAX_CONCURRENT_SCRAPES, minimum: 1, maximum: 20 }))
19284
19368
  });
19285
19369
  const trackerLimit = options.tracker?.getToolLimit("scrape");
@@ -19289,7 +19373,7 @@ function createScrapeTool(options) {
19289
19373
  batchProtocolText = `PROTOCOL: Batch 1 \u2192 Batch 2 \u2192 ... (up to ${effectiveLimit} batches)`;
19290
19374
  } else {
19291
19375
  const batchNumbers = Array.from({ length: effectiveLimit }, (_, i) => `Batch ${i + 1}`).join(" \u2192 ");
19292
- batchProtocolText = `PROTOCOL: ${batchNumbers} (up to ${MAX_SCRAPE_URLS} URLs each).`;
19376
+ batchProtocolText = `PROTOCOL: ${batchNumbers} (up to ${maxScrapeUrls} URLs each).`;
19293
19377
  }
19294
19378
  return {
19295
19379
  name: "scrape",
@@ -19298,7 +19382,7 @@ function createScrapeTool(options) {
19298
19382
  promptSnippet: `Read full content of web pages or PDFs (up to ${maxScrapeBatches} batches)`,
19299
19383
  promptGuidelines: [
19300
19384
  batchProtocolText,
19301
- `Up to ${MAX_SCRAPE_URLS} URLs per batch.`,
19385
+ `Up to ${maxScrapeUrls} URLs per batch.`,
19302
19386
  "Handshake is ELIMINATED. Start scraping immediately.",
19303
19387
  "PDFs are auto-detected and extracted with high fidelity."
19304
19388
  ],
@@ -19344,8 +19428,8 @@ function createScrapeTool(options) {
19344
19428
  }
19345
19429
  }
19346
19430
  const fetchCandidates = [...dedupedUrls, ...evictedDuplicates];
19347
- const finalUrls = fetchCandidates.slice(0, MAX_SCRAPE_URLS);
19348
- const skippedUrls = fetchCandidates.slice(MAX_SCRAPE_URLS);
19431
+ const finalUrls = fetchCandidates.slice(0, maxScrapeUrls);
19432
+ const skippedUrls = fetchCandidates.slice(maxScrapeUrls);
19349
19433
  if (options.getTokensUsed) {
19350
19434
  const ctxWindow = options.contextWindowSize ?? DEFAULT_MODEL_CONTEXT_WINDOW;
19351
19435
  const tokensUsed = options.getTokensUsed();
@@ -19512,7 +19596,7 @@ ${dedupNote}`;
19512
19596
  markdown += `## Not Fetched \u2014 Over Batch Cap (${skippedUrls.length})
19513
19597
 
19514
19598
  `;
19515
- markdown += `Only the first ${MAX_SCRAPE_URLS} URLs of a batch are fetched. The following URLs were NOT fetched \u2014 no content for them appears above. Request them in a later scrape batch if still needed:
19599
+ markdown += `Only the first ${maxScrapeUrls} URLs of a batch are fetched. The following URLs were NOT fetched \u2014 no content for them appears above. Request them in a later scrape batch if still needed:
19516
19600
  `;
19517
19601
  for (const url of skippedUrls) {
19518
19602
  markdown += `- ${url}
@@ -23685,7 +23769,7 @@ ${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
23685
23769
  let lastError;
23686
23770
  const researcherExecutionStartMs = Date.now();
23687
23771
  const deliveredSteeringIds = /* @__PURE__ */ new Set();
23688
- const researcherSystemPrompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{extra_tool_guidelines}}", "").replace("{{digest_section}}", () => RESEARCHER_DIGEST_SECTION).trim();
23772
+ const researcherSystemPrompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{extra_tool_guidelines}}", "").replaceAll("{{max_scrape_urls}}", String(config.MAX_SCRAPE_URLS)).replace("{{digest_section}}", () => RESEARCHER_DIGEST_SECTION).trim();
23689
23773
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
23690
23774
  if (signal?.aborted) throw new Error("Aborted");
23691
23775
  if (stopRequested()) {
@@ -24215,7 +24299,7 @@ For roughly one in ${this.config.YOUTUBE_QUERY_EVERY_N} of your queries, append
24215
24299
  Your goal is to gather a focused, high-quality pool of initial links.
24216
24300
 
24217
24301
  ## Scrape
24218
- After searching, scrape the best sources using the \`scrape\` tool (up to ${maxScrapeBatchesDisplay} batches, up to 4 URLs each).
24302
+ After searching, scrape the best sources using the \`scrape\` tool (up to ${maxScrapeBatchesDisplay} batches, up to ${this.config.MAX_SCRAPE_URLS} URLs each).
24219
24303
  Prioritize primary sources and authoritative data.`;
24220
24304
  consumeQueuedMessages(this.options.sessionId);
24221
24305
  const steeringMessages = getSteeringMessages(this.options.sessionId);
@@ -24228,7 +24312,7 @@ Prioritize primary sources and authoritative data.`;
24228
24312
  evidenceLines.push("## Initial Links\nInvestigate these provided URLs first:\n" + this.options.initialLinks.map((l) => `- ${l}`).join("\n"));
24229
24313
  }
24230
24314
  evidenceLines.push(quickEvidenceSection);
24231
- const prompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{extra_tool_guidelines}}", "- `search`: Perform broad web searches (Round 1 only).").replace("{{digest_section}}", "").trim();
24315
+ const prompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{extra_tool_guidelines}}", "- `search`: Perform broad web searches (Round 1 only).").replaceAll("{{max_scrape_urls}}", String(this.config.MAX_SCRAPE_URLS)).replace("{{digest_section}}", "").trim();
24232
24316
  const userMessage = [
24233
24317
  `Goal: ${query}`,
24234
24318
  storeSection.trim(),
@@ -27278,7 +27362,8 @@ async function cmdKnowledgeConfig(kc) {
27278
27362
  scope: ${kc.mode === "none" ? "disabled here" : kc.mode === "project" ? "this directory only" : "shared across all directories"}
27279
27363
  saved: per-directory (${cwd})
27280
27364
  db dir: ${info2.dbDir}
27281
- ` + (overridden ? `
27365
+ ` + (info2.available ? "" : ` state: ${info2.unavailableReason}
27366
+ `) + (overridden ? `
27282
27367
  NOTE: the effective mode here is still '${info2.mode}' because a higher-precedence ${info2.origin} overrides it. Clear that source for this setting to take effect.
27283
27368
  ` : "")
27284
27369
  );
@@ -27293,7 +27378,8 @@ NOTE: the effective mode here is still '${info2.mode}' because a higher-preceden
27293
27378
  `knowledge store mode: ${info.mode} (source: ${info.origin})
27294
27379
  scope: ${info.mode === "none" ? "disabled here" : info.mode === "project" ? "this directory only" : "shared across all directories"}
27295
27380
  db dir: ${info.dbDir}
27296
-
27381
+ ` + (info.available ? "" : ` state: ${info.unavailableReason}
27382
+ `) + `
27297
27383
  change it for THIS directory: ${BINARY_NAME} knowledge-config set <none|project|global>
27298
27384
  change the machine-wide default: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> in ${resolvedConfigPaths().configEnv}
27299
27385
  `
@@ -29,10 +29,10 @@ Do not treat any source as inherently objective. Justify its inclusion through t
29
29
  Combine historical URLs and search results into a unified pool. Use previous session summaries as a guide for what to expect. Any knowledge-store results in your user message were retrieved automatically for your goal from the **Project Knowledge Store** and/or **User Knowledge Store** — cite them with that store name as their `Source:`.
30
30
 
31
31
  ### Step 2: Scrape Round 1
32
- Identify the 4 most promising URLs and scrape them in one batch (the scrape tool accepts up to 6 per call; 4 is your target per round). Prioritize primary sources, authoritative references, and dense documentation; all else being equal, lean toward more recent / current sources and topics.
32
+ Identify the most promising URLs and scrape them in one batch. The scrape tool accepts up to {{max_scrape_urls}} URLs per call — {{max_scrape_urls}} is your target per round, so plan each batch at the full budget rather than stopping early. Prioritize primary sources, authoritative references, and dense documentation; all else being equal, lean toward more recent / current sources and topics.
33
33
 
34
34
  ### Step 3: Scrape Round 2 (if needed)
35
- If your first batch did not yield enough material, scrape up to 4 additional URLs from **your own source list** that you haven't read yet. Before choosing, review the **Session URL Pool** at the bottom of each scrape response — it shows what topics and domains your sibling researchers are already covering. Use this to steer your remaining scrapes from your own list:
35
+ If your first batch did not yield enough material, scrape up to {{max_scrape_urls}} additional URLs from **your own source list** that you haven't read yet. Before choosing, review the **Session URL Pool** at the bottom of each scrape response — it shows what topics and domains your sibling researchers are already covering. Use this to steer your remaining scrapes from your own list:
36
36
  - **Complement** your siblings: if they're covering area X, pick your own URLs that go deeper on X or fill adjacent gaps.
37
37
  - **Diversify**: if you have sources on an angle no one else is touching, prioritize those to maximize session coverage.
38
38
  - **Stay on mission**: regardless of what siblings are doing, make sure you have enough material for your assigned goal.
@@ -2665,8 +2665,10 @@ var ConfigSchema = Type.Object({
2665
2665
  MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING: Type.Number({ minimum: 0.05, maximum: 1, default: 0.15 }),
2666
2666
  /** Estimated tokens per scrape result for planning (default: 2500) */
2667
2667
  AVG_TOKENS_PER_SCRAPE: Type.Number({ minimum: 500, maximum: 1e4, default: 2500 }),
2668
- /** Maximum number of concurrent scrapes (default: 3) */
2668
+ /** Maximum concurrent scrapes (default: 3) */
2669
2669
  MAX_CONCURRENT_SCRAPES: Type.Number({ minimum: 1, maximum: 20, default: 3 }),
2670
+ /** Maximum URLs fetched per scrape batch; extras are listed under "Not Fetched — Over Batch Cap" (default: 8, range: 1-20) */
2671
+ MAX_SCRAPE_URLS: Type.Number({ minimum: 1, maximum: 20, default: 8 }),
2670
2672
  /** Max YouTube videos transcribed per youtube_transcript call (default: 3, range 1-5) */
2671
2673
  YOUTUBE_TRANSCRIPT_MAX_VIDEOS: Type.Number({ minimum: 1, maximum: 5, default: 3 }),
2672
2674
  /** Per-video timeout for the youtube_transcript tool in ms (default: 20000, range 5-120s) */
@@ -113,7 +113,7 @@ agent to invoke:
113
113
  | Tool | Quick | Deep | Backend |
114
114
  |------|-------|------|---------|
115
115
  | `search` | ✓ | — | DuckDuckGo Lite via the stealth browser |
116
- | `scrape` | ✓ | ✓ | Batch page fetch → Markdown via the stealth browser (up to 6 URLs per call) |
116
+ | `scrape` | ✓ | ✓ | Batch page fetch → Markdown via the stealth browser (up to MAX_SCRAPE_URLS URLs per call, default 8) |
117
117
  | `youtube_transcript` | ✓ | ✓ | YouTube captions via youtubei.js + BotGuard PoToken (≤3 videos by default, configurable 1–5; one call per researcher) |
118
118
  | `security_search` | ✓ | ✓ | NVD, CISA KEV, GitHub Advisories, OSV |
119
119
  | `stackexchange` | ✓ | ✓ | Stack Exchange network |
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.6.10] - 2026-09-02
9
+
10
+ ### Changed
11
+
12
+ - **`/research-config` now behaves properly on every pi host — TUI, RPC/web-hub GUIs, print/json, and the SDK (`guis` is a family of frontends, not one client).** The interactive settings menu is now gated on `ctx.mode === 'tui'` (not just `ctx.hasUI`, which is true in RPC too): on non-TUI hosts the command replies with a clear non-interactive guidance message instead of silently entering a menu that cannot render. Those hosts get working headless diagnostics instead — `/research-config health` and `/research-config knowledge-status` now run on any host (previously they were reachable only through the menu, even though the tool failure text points users at health). Every destructive `ctx.ui.confirm` now carries a 60s timeout, so no host can wedge on a dialog whose client never responds.
13
+
14
+ - **The knowledge store's Arrow/LanceDB boundary is now verified in the actual consumer layout on every CI run.** The dev tree resolves a single apache-arrow 21.1.0 (forced by `overrides`), but a fresh npm consumer install resolves two copies — 21.1.0 nested under the package for the direct pin and 18.1.0 hoisted for `@lancedb/lancedb`'s declared peer ceiling. docs/ARCHITECTURE.md pins 21.1.0 for a measured reason and warns mismatched Arrow copies may not interoperate, and this divergence is invisible to the dev-tree tests. The fresh-install CI job now runs `scripts/consumer-interop-probe.cjs`, which replicates `createStoreTable` (same fields, FTS/BTree index set, reopen-from-disk) with the consumer's exact resolution. README gains an Uninstall section documenting that npm 7+ runs no `preuninstall`, so skill links / `~/.pi/research/state` / caches persist unless removed first.
15
+
16
+ ### Fixed
17
+
18
+ - **Ctrl+C during a research run no longer cancels the run while the editor still holds typed text — it clears the editor first, and only a subsequent Ctrl+C (editor now empty) cancels the research.** The global TUI controller (`src/tui/tui-controller.ts`) treated Ctrl+C and Esc identically as cancel keys, aborting the session's active research runs on every press of either. That fought pi's own keybinding contract: Ctrl+C is `app.clear` ("Clear editor" — pi's `handleCtrlC()` clears the editor text and never interrupts the agent, doubling as the two-press-within-500ms exit), while Esc is `app.interrupt` ("Cancel or abort"), so a user discarding a drafted follow-up mid-run got the whole run killed instead of an empty editor. The controller now reads the editor via `ctx.ui.getEditorText()`: with text present, Ctrl+C falls through untouched (pi clears the text; the run keeps going); with an empty editor it is an explicit cancel gesture and aborts, still scoped to the current pi session as before; Esc keeps its unconditional panic-cancel role; and a throwing `getEditorText()` (ctx gone stale mid-run) is treated as empty — cancel — the safe default. The two gestures coexist with pi's native double-press exit in practice: the clear-then-decide rhythm is naturally slower than the 500ms window, so the human cancel path never trips pi's shutdown. Seven new tests pin the policy (`test/unit/tui/tui-controller.test.ts`).
19
+
20
+ - **`/research-config` could silently do nothing on RPC hosts.** `ctx.hasUI` is true in RPC, so the old guard passed and `ctx.ui.custom(...)` was entered even though no non-TUI host can render or answer the menu. The mode gate in the change above makes the menu reachable only on a real TUI and hands every other host the guidance message plus the headless `health` / `knowledge-status` diagnostics.
21
+
22
+ ### Verified
23
+
24
+ - 3,027 unit tests over 240 files (14 new across this Unreleased cycle: 7 Ctrl+C-policy + 7 host-mode), ESLint clean on the touched source, all four type-checks (TS 6 src + tests, TS 7 native src + tests), and dependency-cruiser clean (238 modules / 1,218 dependencies).
25
+
26
+
27
+ ## [1.6.9] - 2026-09-01
28
+
29
+ ### Changed
30
+
31
+ - **`MAX_SCRAPE_URLS` is now a real config setting, raised to 8 (was a hardcoded constant capped at 6).** The per-batch page cap joined every other scrape knob in the Config schema (`PI_RESEARCH_MAX_SCRAPE_URLS`, range 1–20, default 8): env-var parsing, the config.env bridge, persistence, and the bundled thread-worker schema mirror all follow, and `src/tools/scrape.ts` resolves one `getMaxScrapeUrls(config)` value that feeds the tool-schema description, the batch-protocol text, the per-call slice, and the "Not Fetched — Over Batch Cap" report, so the enforced cap and every string the model sees are the same number by construction (previously the constant was 6 while the researcher prompt separately steered a lower target). `src/constants.ts` keeps `MAX_SCRAPE_URLS` as a documented schema-default mirror plus the accessor, mirroring `MAX_GATHERING_CALLS`. The researcher prompt no longer steers below the cap: a new {{max_scrape_urls}} placeholder — substituted from config in BOTH call sites (deep researcher-executor and the quick orchestrator, each of which maintains its own replace chain) — states the per-call maximum as the batch target ("{{max_scrape_urls}} is your target per round, so plan each batch at the full budget"), so the prompt can never drift from the enforced cap the way the old hardcoded targets (4, then 6) already had from the constants-derived tool text. The shipped-render tests for both paths pin the substitution. Context-gate note: batches of 8 bill 8 × `AVG_TOKENS_PER_SCRAPE` against the gate fraction, so installs pinning a high per-URL estimate with a small window (the code comment records an 8-URL batch blocked on a 128k model) may see more `context_limit` blocks — tune `PI_RESEARCH_AVG_TOKENS_PER_SCRAPE` if so. `MAX_CONCURRENT_SCRAPES` (fetch parallelism) is untouched at its long-standing default of 3.
32
+
33
+ ### Fixed
34
+
35
+ - **A knowledge store whose packages are not even resolvable is now cleanly OFF — at the settings layer, not buried in runtime degrade.** Since 1.6.7 the store correctly memoized DISABLED when `@huggingface/transformers` (optional dep) was skipped at install or the `@lancedb/lancedb` install was broken — but only AFTER running the full init: `MAX_INIT_RETRIES` (5) attempts of exponential backoff (~31s) per fresh process, spent discovering what `require.resolve` already knows, and every settings surface kept advertising a `KNOWLEDGE_STORE_MODE` that could never run. Now a new execution-free probe (`src/knowledge/availability.ts` — `require.resolve` only, never loads the native modules) is consulted BEFORE the retry loop: `createKnowledgeStoreComponents` returns null immediately (the same clean OFF verdict mode=`none` produces), and `KnowledgeStoreService` records it as DISABLED(`native`) — permanent for the process, never revived by a mode flip, mirroring the existing catch-path semantics. The settings surfaces now tell the truth about it everywhere: `describeKnowledgeStoreMode` gained `available`/`unavailableReason` (flows into `knowledge-config --json` automatically), the `knowledge-config` text output and the `/research-config` TUI's Knowledge Mode item say what is missing and how to repair it, and the healthcheck's 'disabled' verdict names the actual missing packages instead of the generic platform line. One more surface joined them: the `research_knowledge_search` miss message no longer tells a package-gap host the store is "disabled in settings" — a 'native' service disable now reports `store_packages_missing` (the repair is installing the package, not flipping a Knowledge Mode toggle that cannot help), while a true mode disable keeps `store_disabled` and an in-flight/failed init keeps `store_not_ready`. Unit tests pin the probe, the fail-fast (embedder factory never invoked), the native-vs-mode disable distinction, and the miss-reason split.
36
+
8
37
  ## [1.6.8] - 2026-08-31
9
38
 
10
39
  ### Changed
@@ -71,6 +71,7 @@ Research
71
71
  | `PI_RESEARCH_MAX_SCRAPE_BATCHES` (TUI) | `2` | 0–99 | Scrape batches per researcher (0 = unlimited). When prompt caching is known to be active for the resolved research model (Anthropic API models, or a provider route explicitly configured for Anthropic-style cache control), the effective limit is this value plus one — the cached prompt prefix makes the extra batch cheap. |
72
72
  | `PI_RESEARCH_MAX_GATHERING_CALLS` | `12` | 1–100 | Shared web-gathering calls per researcher (`search` + `security_search` + `stackexchange` + `youtube_transcript`). |
73
73
  | `PI_RESEARCH_MAX_CONCURRENT_SCRAPES` | `3` | 1–20 | Concurrent URLs fetched per scrape batch. |
74
+ | `PI_RESEARCH_MAX_SCRAPE_URLS` | `8` | 1–20 | Maximum URLs fetched per scrape batch. URLs past the cap are listed under "Not Fetched — Over Batch Cap" and must be requested in a later batch. (Promoted from a hardcoded constant so it is env/config-file tunable like every other scrape knob.) |
74
75
  | `PI_RESEARCH_MAX_RETRIES` | `2` | 0–5 | Retries per researcher request. |
75
76
  | `PI_RESEARCH_RETRY_DELAY_MS` | `2000` | 100–10000 | Base delay between retries. |
76
77
  | `PI_RESEARCH_MAX_FAILED_RESEARCHERS` | `2` | 1–10 | Unique researcher failures that abort the whole run. Raise to let slower, still-in-flight researchers finish before giving up. |
@@ -121,7 +122,7 @@ See the [knowledge store doc](KNOWLEDGE-STORE.md) for what each value does.
121
122
 
122
123
  | Variable | Default | Range | Description |
123
124
  |----------|---------|-------|-------------|
124
- | `PI_RESEARCH_KNOWLEDGE_STORE_MODE` (TUI) `[project]` | `global` | none · project · global | Store scope: one shared store across every directory (`global`), scoped to the current directory (`project`), or disabled (`none`). |
125
+ | `PI_RESEARCH_KNOWLEDGE_STORE_MODE` (TUI) `[project]` | `global` | none · project · global | Store scope: one shared store across every directory (`global`), scoped to the current directory (`project`), or disabled (`none`). Independently of this setting, the store is cleanly OFF when its required packages are not installed (optional `@huggingface/transformers` skipped at install, broken `@lancedb/lancedb`): init fails fast instead of retry-storming, and `pi-research knowledge-config`, the `/research-config` menu, and the healthcheck all name the missing package and the repair. |
125
126
  | `PI_RESEARCH_EMBEDDING_MODEL` (TUI) | `onnx-community/granite-embedding-small-english-r2-ONNX` | — | Embedding model. Changing it clears the store and starts fresh. |
126
127
  | `PI_RESEARCH_EMBEDDING_DEVICE` (TUI) | `auto` | auto · webgpu · cpu | Inference backend. `auto` probes WebGPU viability out-of-process and falls back to CPU; `cpu` forces CPU; `webgpu` forces the GPU path with no probe (advanced — can hard-crash on a software GPU). The TUI exposes only `auto` (as "GPU") and `cpu`. |
127
128
  | `PI_RESEARCH_CACHE_TTL_DAYS` (TUI) | `30` | 1–365 | How long cached findings are kept before eviction. |
@@ -144,17 +144,25 @@ platform/architecture pairs:
144
144
  | Windows | x64 / arm64 | Supported |
145
145
 
146
146
  Intel Macs (`darwin-x64`) have no prebuilt binary for either component, so the
147
- knowledge store cannot run there. The degradation is automatic:
147
+ knowledge store cannot run there. The same clean OFF applies on any platform when a
148
+ required package is not installed at all — optional `@huggingface/transformers` is
149
+ skipped by `npm install --omit=optional` (and by an npm optional-dependency bug), or
150
+ the `@lancedb/lancedb` install is broken. The degradation is automatic and fast:
148
151
 
149
152
  - Research still works. Search, scraping, YouTube transcripts, the security databases,
150
153
  Stack Exchange, planning, and synthesis are unaffected — only the store is missing.
151
- - The health check reports the store as disabled, not unhealthy: its health component
152
- returns disabled (native embedding/vector stack unavailable on this platform) with a
153
- healthy status, so the missing component does not drag overall health to "unhealthy"
154
- or block a quick (depth-0) run.
155
- - `research_knowledge_search` does no caching or retrieval; every run goes to the live web.
156
-
157
- No configuration is needed on an Intel Mac.
154
+ - The store fails fast. Missing packages are detected by resolution before any
155
+ initialization is attempted, so there is no retry storm — the store simply starts OFF.
156
+ - The settings surfaces say why. `pi-research knowledge-config`, the `/research-config`
157
+ menu, and the health check name the missing package and the repair (install the
158
+ optional dependencies) instead of advertising a mode the store cannot honor. A
159
+ `research_knowledge_search` miss reports the package gap rather than pointing at a
160
+ settings toggle that cannot help.
161
+ - The health check reports the store as disabled, not unhealthy, so the missing
162
+ component does not drag overall health to "unhealthy" or block a quick (depth-0) run.
163
+
164
+ No configuration is needed: install the package (a full install including optional
165
+ dependencies) and the store comes up; a mode flip alone cannot revive it mid-process.
158
166
 
159
167
  ### Retention and eviction
160
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.6.8",
3
+ "version": "1.6.10",
4
4
  "description": "Web research for your agents with smart and safe tooling + knowledge store",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli.ts CHANGED
@@ -916,6 +916,7 @@ export async function cmdKnowledgeConfig(kc: NonNullable<ParsedArgs['knowledgeCo
916
916
  ` scope: ${kc.mode === 'none' ? 'disabled here' : kc.mode === 'project' ? 'this directory only' : 'shared across all directories'}\n` +
917
917
  ` saved: per-directory (${cwd})\n` +
918
918
  ` db dir: ${info.dbDir}\n` +
919
+ (info.available ? '' : ` state: ${info.unavailableReason}\n`) +
919
920
  (overridden
920
921
  ? `\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`
921
922
  : ''),
@@ -933,6 +934,7 @@ export async function cmdKnowledgeConfig(kc: NonNullable<ParsedArgs['knowledgeCo
933
934
  `knowledge store mode: ${info.mode} (source: ${info.origin})\n` +
934
935
  ` scope: ${info.mode === 'none' ? 'disabled here' : info.mode === 'project' ? 'this directory only' : 'shared across all directories'}\n` +
935
936
  ` db dir: ${info.dbDir}\n` +
937
+ (info.available ? '' : ` state: ${info.unavailableReason}\n`) +
936
938
  `\nchange it for THIS directory: ${BINARY_NAME} knowledge-config set <none|project|global>\n` +
937
939
  `change the machine-wide default: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> in ${resolvedConfigPaths().configEnv}\n`,
938
940
  );