@lincoln504/pi-research 1.7.7 → 1.7.8

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.
@@ -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.7.7", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.7.8", "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-bin.mjs CHANGED
@@ -37,7 +37,13 @@
37
37
  import { existsSync } from 'node:fs';
38
38
  import { fileURLToPath } from 'node:url';
39
39
 
40
- const REQUIRED_PEERS = ['@earendil-works/pi-ai', '@earendil-works/pi-coding-agent'];
40
+ const REQUIRED_PEERS = [
41
+ '@earendil-works/pi-ai',
42
+ '@earendil-works/pi-coding-agent',
43
+ // dist/cli.mjs imports it directly (TUI width helpers), so an install that skips
44
+ // peers can lack it even though pi-coding-agent normally brings it in.
45
+ '@earendil-works/pi-tui',
46
+ ];
41
47
 
42
48
  /**
43
49
  * Probe one peer dependency.
package/dist/cli.mjs CHANGED
@@ -230,7 +230,12 @@ function redactSecrets(message) {
230
230
  out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
231
231
  out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
232
232
  out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep3) => `${key}${sep3}[REDACTED]`);
233
- out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep3) => `${key}${sep3}[REDACTED]`);
233
+ out = out.replace(AUTH_SCHEME_PATTERN, (_m, key, sep3, scheme) => `${key}${sep3}${scheme} [REDACTED]`);
234
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep3, val) => {
235
+ const q = val[0] === '"' || val[0] === "'" ? val[0] : "";
236
+ const closed = q !== "" && val.length > 1 && val.endsWith(q);
237
+ return `${key}${sep3}${q}[REDACTED]${closed ? q : ""}`;
238
+ });
234
239
  out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
235
240
  out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
236
241
  out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
@@ -249,7 +254,7 @@ function redactSecrets(message) {
249
254
  function stripTerminalEscapes(message) {
250
255
  return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
251
256
  }
252
- var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
257
+ var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, AUTH_SCHEME_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
253
258
  var init_log_utils = __esm({
254
259
  "src/utils/log-utils.ts"() {
255
260
  "use strict";
@@ -264,7 +269,11 @@ var init_log_utils = __esm({
264
269
  "gi"
265
270
  );
266
271
  SENSITIVE_KV_PATTERN = new RegExp(
267
- `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
272
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|token|key)${B_RIGHT}(["']?\\s*[:=]\\s*)("[^"\\r\\n]{0,1024}"|'[^'\\r\\n]{0,1024}'|["']?[^\\s"',&)]+)`,
273
+ "gi"
274
+ );
275
+ AUTH_SCHEME_PATTERN = new RegExp(
276
+ `${B_LEFT}(authorization)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([A-Za-z][A-Za-z0-9_-]{0,31})[ \\t]+[^\\s"',&)]+`,
268
277
  "gi"
269
278
  );
270
279
  KNOWN_TOKEN_PATTERN = new RegExp(
@@ -7959,7 +7968,7 @@ var init_store = __esm({
7959
7968
  if (this.db) return;
7960
7969
  try {
7961
7970
  if (!fs13.existsSync(this.options.dbDir)) {
7962
- fs13.mkdirSync(this.options.dbDir, { recursive: true });
7971
+ fs13.mkdirSync(this.options.dbDir, { recursive: true, mode: 448 });
7963
7972
  }
7964
7973
  this.db = await (await getLancedb()).connect(this.options.dbDir);
7965
7974
  const tableNames = await this.db.tableNames();
@@ -19003,10 +19012,29 @@ ${kept.join("\n")}
19003
19012
  ${rest}` : rest;
19004
19013
  }
19005
19014
  function cleanConvertedMarkdown(markdown) {
19006
- return stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown))).replace(/\n{3,}/g, "\n\n").trim();
19015
+ return collapseBlankLines(stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown)))).trim();
19016
+ }
19017
+ var CODE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
19018
+ function collapseBlankLines(markdown) {
19019
+ if (!markdown.includes("\n\n\n")) return markdown;
19020
+ const out = [];
19021
+ let fence = null;
19022
+ for (const line of markdown.split("\n")) {
19023
+ const m = CODE_FENCE.exec(line);
19024
+ if (fence === null) {
19025
+ if (m) fence = m[1];
19026
+ } else if (m && m[1][0] === fence[0] && m[1].length >= fence.length && line.slice(m[0].length).trim() === "") {
19027
+ fence = null;
19028
+ }
19029
+ if (fence === null && line === "" && out.length > 0 && out[out.length - 1] === "") continue;
19030
+ out.push(line);
19031
+ }
19032
+ return out.join("\n");
19007
19033
  }
19008
19034
  function stripImageLinks(markdown) {
19009
- return markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1").replace(/\n{3,}/g, "\n\n").trim();
19035
+ return collapseBlankLines(
19036
+ markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1")
19037
+ ).trim();
19010
19038
  }
19011
19039
  function createNativeMarkdownConverter(nativeModule) {
19012
19040
  return async (html) => {
@@ -27194,7 +27222,7 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
27194
27222
  const filename = `${baseFilename}-${hash}.md`;
27195
27223
  const filepath = join21(targetDir, filename);
27196
27224
  try {
27197
- await fs15.writeFile(filepath, result, { flag: "wx" });
27225
+ await fs15.writeFile(filepath, result, { flag: "wx", mode: 384 });
27198
27226
  logger.log(`[export] Research report saved to: ${filepath}`);
27199
27227
  return filepath;
27200
27228
  } catch (error) {
@@ -27523,6 +27551,10 @@ function configBlock(det, extraNote) {
27523
27551
  if (extraNote) lines.push(` \u2022 note: ${extraNote}`);
27524
27552
  return lines.join("\n");
27525
27553
  }
27554
+ function resolveCliDepth(explicit, cfg) {
27555
+ const d = explicit ?? (cfg.QUICK_RESEARCH ? cfg.DEFAULT_RESEARCH_DEPTH : Math.max(1, cfg.DEFAULT_RESEARCH_DEPTH));
27556
+ return d;
27557
+ }
27526
27558
  async function cmdResearch(args) {
27527
27559
  const det = await detectCredentials(args.model);
27528
27560
  if (det.problem) {
@@ -27545,7 +27577,7 @@ Error: ${vmsg}
27545
27577
  `);
27546
27578
  return EXIT.USAGE;
27547
27579
  }
27548
- const depth = args.depth ?? getConfig(process.cwd(), "cli").DEFAULT_RESEARCH_DEPTH;
27580
+ const depth = resolveCliDepth(args.depth, getConfig(process.cwd(), "cli"));
27549
27581
  const runModel = det.model;
27550
27582
  toStderr(`[pi-research] starting research (depth ${depth})${runModel ? ` with ${runModel}` : ""}\u2026
27551
27583
  `);
@@ -28518,5 +28550,6 @@ export {
28518
28550
  exitCodeForSignal,
28519
28551
  makeProgressObserver,
28520
28552
  parseArgs,
28521
- reportError
28553
+ reportError,
28554
+ resolveCliDepth
28522
28555
  };
@@ -213,7 +213,11 @@ var COOKIE_HEADER_PATTERN = new RegExp(
213
213
  "gi"
214
214
  );
215
215
  var SENSITIVE_KV_PATTERN = new RegExp(
216
- `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
216
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|token|key)${B_RIGHT}(["']?\\s*[:=]\\s*)("[^"\\r\\n]{0,1024}"|'[^'\\r\\n]{0,1024}'|["']?[^\\s"',&)]+)`,
217
+ "gi"
218
+ );
219
+ var AUTH_SCHEME_PATTERN = new RegExp(
220
+ `${B_LEFT}(authorization)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([A-Za-z][A-Za-z0-9_-]{0,31})[ \\t]+[^\\s"',&)]+`,
217
221
  "gi"
218
222
  );
219
223
  var KNOWN_TOKEN_PATTERN = new RegExp(
@@ -251,7 +255,12 @@ function redactSecrets(message) {
251
255
  out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
252
256
  out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
253
257
  out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
254
- out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
258
+ out = out.replace(AUTH_SCHEME_PATTERN, (_m, key, sep, scheme) => `${key}${sep}${scheme} [REDACTED]`);
259
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep, val) => {
260
+ const q = val[0] === '"' || val[0] === "'" ? val[0] : "";
261
+ const closed = q !== "" && val.length > 1 && val.endsWith(q);
262
+ return `${key}${sep}${q}[REDACTED]${closed ? q : ""}`;
263
+ });
255
264
  out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
256
265
  out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
257
266
  out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
@@ -60,7 +60,12 @@ function redactSecrets(message) {
60
60
  out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
61
61
  out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
62
62
  out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
63
- out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
63
+ out = out.replace(AUTH_SCHEME_PATTERN, (_m, key, sep, scheme) => `${key}${sep}${scheme} [REDACTED]`);
64
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep, val) => {
65
+ const q = val[0] === '"' || val[0] === "'" ? val[0] : "";
66
+ const closed = q !== "" && val.length > 1 && val.endsWith(q);
67
+ return `${key}${sep}${q}[REDACTED]${closed ? q : ""}`;
68
+ });
64
69
  out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
65
70
  out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
66
71
  out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
@@ -79,7 +84,7 @@ function redactSecrets(message) {
79
84
  function stripTerminalEscapes(message) {
80
85
  return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
81
86
  }
82
- var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
87
+ var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, AUTH_SCHEME_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
83
88
  var init_log_utils = __esm({
84
89
  "src/utils/log-utils.ts"() {
85
90
  "use strict";
@@ -94,7 +99,11 @@ var init_log_utils = __esm({
94
99
  "gi"
95
100
  );
96
101
  SENSITIVE_KV_PATTERN = new RegExp(
97
- `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
102
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|token|key)${B_RIGHT}(["']?\\s*[:=]\\s*)("[^"\\r\\n]{0,1024}"|'[^'\\r\\n]{0,1024}'|["']?[^\\s"',&)]+)`,
103
+ "gi"
104
+ );
105
+ AUTH_SCHEME_PATTERN = new RegExp(
106
+ `${B_LEFT}(authorization)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([A-Za-z][A-Za-z0-9_-]{0,31})[ \\t]+[^\\s"',&)]+`,
98
107
  "gi"
99
108
  );
100
109
  KNOWN_TOKEN_PATTERN = new RegExp(
@@ -1561,6 +1570,7 @@ var init_browser_error_utils = __esm({
1561
1570
  var scraper_utils_exports = {};
1562
1571
  __export(scraper_utils_exports, {
1563
1572
  cleanConvertedMarkdown: () => cleanConvertedMarkdown,
1573
+ collapseBlankLines: () => collapseBlankLines,
1564
1574
  createJsMarkdownConverter: () => createJsMarkdownConverter,
1565
1575
  createNativeMarkdownConverter: () => createNativeMarkdownConverter,
1566
1576
  disposeSsrfSafeFetcher: () => disposeSsrfSafeFetcher,
@@ -1815,10 +1825,28 @@ ${kept.join("\n")}
1815
1825
  ${rest}` : rest;
1816
1826
  }
1817
1827
  function cleanConvertedMarkdown(markdown) {
1818
- return stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown))).replace(/\n{3,}/g, "\n\n").trim();
1828
+ return collapseBlankLines(stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown)))).trim();
1829
+ }
1830
+ function collapseBlankLines(markdown) {
1831
+ if (!markdown.includes("\n\n\n")) return markdown;
1832
+ const out = [];
1833
+ let fence = null;
1834
+ for (const line of markdown.split("\n")) {
1835
+ const m = CODE_FENCE.exec(line);
1836
+ if (fence === null) {
1837
+ if (m) fence = m[1];
1838
+ } else if (m && m[1][0] === fence[0] && m[1].length >= fence.length && line.slice(m[0].length).trim() === "") {
1839
+ fence = null;
1840
+ }
1841
+ if (fence === null && line === "" && out.length > 0 && out[out.length - 1] === "") continue;
1842
+ out.push(line);
1843
+ }
1844
+ return out.join("\n");
1819
1845
  }
1820
1846
  function stripImageLinks(markdown) {
1821
- return markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1").replace(/\n{3,}/g, "\n\n").trim();
1847
+ return collapseBlankLines(
1848
+ markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1")
1849
+ ).trim();
1822
1850
  }
1823
1851
  function createNativeMarkdownConverter(nativeModule) {
1824
1852
  return async (html) => {
@@ -1880,7 +1908,7 @@ function validateContent(html, markdown, url) {
1880
1908
  throw error;
1881
1909
  }
1882
1910
  }
1883
- var cachedSsrfFetcher, FRONT_MATTER_SLOTS, MAX_FRONT_MATTER_CHARS, FRONT_MATTER_KEY;
1911
+ var cachedSsrfFetcher, FRONT_MATTER_SLOTS, MAX_FRONT_MATTER_CHARS, FRONT_MATTER_KEY, CODE_FENCE;
1884
1912
  var init_scraper_utils = __esm({
1885
1913
  "src/web-research/scraper-utils.ts"() {
1886
1914
  "use strict";
@@ -1907,6 +1935,7 @@ var init_scraper_utils = __esm({
1907
1935
  ];
1908
1936
  MAX_FRONT_MATTER_CHARS = 2e4;
1909
1937
  FRONT_MATTER_KEY = /^([\w.:-]+):(?:\s|$)/;
1938
+ CODE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
1910
1939
  }
1911
1940
  });
1912
1941
 
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ 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.7.8] - 2026-10-01
9
+
10
+ ### Fixed
11
+
12
+ - **Scraped code blocks keep their blank lines.** `cleanConvertedMarkdown` and `stripImageLinks` collapsed every run of blank lines across the whole document, fenced code included, so a PEP 8 two-blank-line gap, a Makefile or preformatted ASCII was altered in what was stored and cited. The collapse now skips fenced blocks (backtick or tilde, closing fence at least as long as the opening one, an unclosed fence running to the end as in CommonMark) and still tidies prose. Empty-anchor removal still applies inside code on purpose: that is where Quarto/nbdev emit them.
13
+ - **`pi-research research` honours the quick-mode gate for a config-derived depth.** `PI_RESEARCH_DEFAULT_RESEARCH_DEPTH=0` with `QUICK_RESEARCH` off ran quick mode from the CLI, while the documented rule, and the pi tool path, resolve it to 1. An explicit `--depth 0` still runs quick mode, as the skill documents. The rule now lives in one tested function, `resolveCliDepth`.
14
+ - **Log redaction covers more credential shapes.** A bare `token` key (`token=…`, `GITHUB_TOKEN=…`, `?token=…`) was not masked, a quoted value was cut at its first space, comma or ampersand (`password="a b"` left ` b"` in the log), and `Authorization: Token <credential>` (any scheme other than Bearer/Basic) masked only the scheme word. Quoted values are now consumed whole under a 1024-character bound, so the scan stays linear (measured on adversarial 40k inputs: single-digit milliseconds). Plural counters such as `max_tokens=4096` are deliberately not matched.
15
+ - **The `pi-research` bin shim also checks `@earendil-works/pi-tui`.** `dist/cli.mjs` imports it directly, but the preflight only listed `pi-ai` and `pi-coding-agent`, so a peer-skipping install lacking only `pi-tui` still died with a raw `ERR_MODULE_NOT_FOUND`.
16
+ - **Local files that hold research content are no longer group/world readable by default.** The knowledge store directory is created `0700` and exported research reports are written `0600` (previously the process umask decided, typically `0775`/`0644`, and exports can fall back to the shared temp directory). An existing directory is not re-chmodded.
17
+
8
18
  ## [1.7.7] - 2026-10-01
9
19
 
10
20
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.7.7",
3
+ "version": "1.7.8",
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-bin.mjs CHANGED
@@ -37,7 +37,13 @@
37
37
  import { existsSync } from 'node:fs';
38
38
  import { fileURLToPath } from 'node:url';
39
39
 
40
- const REQUIRED_PEERS = ['@earendil-works/pi-ai', '@earendil-works/pi-coding-agent'];
40
+ const REQUIRED_PEERS = [
41
+ '@earendil-works/pi-ai',
42
+ '@earendil-works/pi-coding-agent',
43
+ // dist/cli.mjs imports it directly (TUI width helpers), so an install that skips
44
+ // peers can lack it even though pi-coding-agent normally brings it in.
45
+ '@earendil-works/pi-tui',
46
+ ];
41
47
 
42
48
  /**
43
49
  * Probe one peer dependency.
package/src/cli.ts CHANGED
@@ -580,6 +580,20 @@ interface ResearchArgs {
580
580
  json?: boolean;
581
581
  }
582
582
 
583
+
584
+ /**
585
+ * Depth for a CLI research run. An explicit --depth 0 always runs quick mode (the
586
+ * documented CLI/skill entry). A CONFIG-derived default of 0 only does when
587
+ * QUICK_RESEARCH is on; otherwise it resolves to 1, as on the pi tool path.
588
+ */
589
+ export function resolveCliDepth(
590
+ explicit: number | undefined,
591
+ cfg: { QUICK_RESEARCH: boolean; DEFAULT_RESEARCH_DEPTH: number },
592
+ ): 0 | 1 | 2 | 3 {
593
+ const d = explicit ?? (cfg.QUICK_RESEARCH ? cfg.DEFAULT_RESEARCH_DEPTH : Math.max(1, cfg.DEFAULT_RESEARCH_DEPTH));
594
+ return d as 0 | 1 | 2 | 3;
595
+ }
596
+
583
597
  /**
584
598
  * Research subcommand. Initializes the SDK, runs deep/quick research, prints the
585
599
  * markdown report to stdout, and always shuts down (even on failure).
@@ -607,8 +621,7 @@ export async function cmdResearch(args: ResearchArgs): Promise<number> {
607
621
  return EXIT.USAGE;
608
622
  }
609
623
 
610
- const depth: 0 | 1 | 2 | 3 =
611
- (args.depth ?? getConfig(process.cwd(), 'cli').DEFAULT_RESEARCH_DEPTH) as 0 | 1 | 2 | 3;
624
+ const depth = resolveCliDepth(args.depth, getConfig(process.cwd(), 'cli'));
612
625
  // det.model already reflects --model-first precedence (detectCredentials above).
613
626
  const runModel = det.model;
614
627
  toStderr(`[pi-research] starting research (depth ${depth})${runModel ? ` with ${runModel}` : ''}…\n`);
@@ -60,7 +60,12 @@ function redactSecrets(message) {
60
60
  out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
61
61
  out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
62
62
  out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
63
- out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
63
+ out = out.replace(AUTH_SCHEME_PATTERN, (_m, key, sep, scheme) => `${key}${sep}${scheme} [REDACTED]`);
64
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep, val) => {
65
+ const q = val[0] === '"' || val[0] === "'" ? val[0] : "";
66
+ const closed = q !== "" && val.length > 1 && val.endsWith(q);
67
+ return `${key}${sep}${q}[REDACTED]${closed ? q : ""}`;
68
+ });
64
69
  out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
65
70
  out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
66
71
  out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
@@ -79,7 +84,7 @@ function redactSecrets(message) {
79
84
  function stripTerminalEscapes(message) {
80
85
  return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
81
86
  }
82
- var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
87
+ var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, AUTH_SCHEME_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
83
88
  var init_log_utils = __esm({
84
89
  "src/utils/log-utils.ts"() {
85
90
  "use strict";
@@ -94,7 +99,11 @@ var init_log_utils = __esm({
94
99
  "gi"
95
100
  );
96
101
  SENSITIVE_KV_PATTERN = new RegExp(
97
- `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
102
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|token|key)${B_RIGHT}(["']?\\s*[:=]\\s*)("[^"\\r\\n]{0,1024}"|'[^'\\r\\n]{0,1024}'|["']?[^\\s"',&)]+)`,
103
+ "gi"
104
+ );
105
+ AUTH_SCHEME_PATTERN = new RegExp(
106
+ `${B_LEFT}(authorization)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([A-Za-z][A-Za-z0-9_-]{0,31})[ \\t]+[^\\s"',&)]+`,
98
107
  "gi"
99
108
  );
100
109
  KNOWN_TOKEN_PATTERN = new RegExp(
@@ -1561,6 +1570,7 @@ var init_browser_error_utils = __esm({
1561
1570
  var scraper_utils_exports = {};
1562
1571
  __export(scraper_utils_exports, {
1563
1572
  cleanConvertedMarkdown: () => cleanConvertedMarkdown,
1573
+ collapseBlankLines: () => collapseBlankLines,
1564
1574
  createJsMarkdownConverter: () => createJsMarkdownConverter,
1565
1575
  createNativeMarkdownConverter: () => createNativeMarkdownConverter,
1566
1576
  disposeSsrfSafeFetcher: () => disposeSsrfSafeFetcher,
@@ -1815,10 +1825,28 @@ ${kept.join("\n")}
1815
1825
  ${rest}` : rest;
1816
1826
  }
1817
1827
  function cleanConvertedMarkdown(markdown) {
1818
- return stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown))).replace(/\n{3,}/g, "\n\n").trim();
1828
+ return collapseBlankLines(stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown)))).trim();
1829
+ }
1830
+ function collapseBlankLines(markdown) {
1831
+ if (!markdown.includes("\n\n\n")) return markdown;
1832
+ const out = [];
1833
+ let fence = null;
1834
+ for (const line of markdown.split("\n")) {
1835
+ const m = CODE_FENCE.exec(line);
1836
+ if (fence === null) {
1837
+ if (m) fence = m[1];
1838
+ } else if (m && m[1][0] === fence[0] && m[1].length >= fence.length && line.slice(m[0].length).trim() === "") {
1839
+ fence = null;
1840
+ }
1841
+ if (fence === null && line === "" && out.length > 0 && out[out.length - 1] === "") continue;
1842
+ out.push(line);
1843
+ }
1844
+ return out.join("\n");
1819
1845
  }
1820
1846
  function stripImageLinks(markdown) {
1821
- return markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1").replace(/\n{3,}/g, "\n\n").trim();
1847
+ return collapseBlankLines(
1848
+ markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1")
1849
+ ).trim();
1822
1850
  }
1823
1851
  function createNativeMarkdownConverter(nativeModule) {
1824
1852
  return async (html) => {
@@ -1880,7 +1908,7 @@ function validateContent(html, markdown, url) {
1880
1908
  throw error;
1881
1909
  }
1882
1910
  }
1883
- var cachedSsrfFetcher, FRONT_MATTER_SLOTS, MAX_FRONT_MATTER_CHARS, FRONT_MATTER_KEY;
1911
+ var cachedSsrfFetcher, FRONT_MATTER_SLOTS, MAX_FRONT_MATTER_CHARS, FRONT_MATTER_KEY, CODE_FENCE;
1884
1912
  var init_scraper_utils = __esm({
1885
1913
  "src/web-research/scraper-utils.ts"() {
1886
1914
  "use strict";
@@ -1907,6 +1935,7 @@ var init_scraper_utils = __esm({
1907
1935
  ];
1908
1936
  MAX_FRONT_MATTER_CHARS = 2e4;
1909
1937
  FRONT_MATTER_KEY = /^([\w.:-]+):(?:\s|$)/;
1938
+ CODE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
1910
1939
  }
1911
1940
  });
1912
1941
 
@@ -279,7 +279,7 @@ export class KnowledgeStore implements IKnowledgeStore {
279
279
 
280
280
  try {
281
281
  if (!fs.existsSync(this.options.dbDir)) {
282
- fs.mkdirSync(this.options.dbDir, { recursive: true });
282
+ fs.mkdirSync(this.options.dbDir, { recursive: true, mode: 0o700 });
283
283
  }
284
284
  this.db = await (await getLancedb()).connect(this.options.dbDir);
285
285
 
@@ -126,8 +126,18 @@ const COOKIE_HEADER_PATTERN = new RegExp(
126
126
  // sort key, a map key) is the accepted trade-off — same one already made for
127
127
  // the equally generic bare `session`/`cookie` entries below.
128
128
  const SENSITIVE_KV_PATTERN = new RegExp(
129
- `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}` +
130
- `(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
129
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|token|key)${B_RIGHT}` +
130
+ // A quoted value is consumed whole (bounded, so no backtracking blow-up): the
131
+ // old unquoted class stopped at the first space/comma/&, so `password="a b"`
132
+ // left ` b"` in the clear. An unterminated quote falls through to that class.
133
+ `(["']?\\s*[:=]\\s*)("[^"\\r\\n]{0,1024}"|'[^'\\r\\n]{0,1024}'|["']?[^\\s"',&)]+)`,
134
+ 'gi',
135
+ );
136
+ // `Authorization: <scheme> <credential>` for schemes the Bearer/Basic patterns do
137
+ // not know (Token, Digest, ApiKey, ...). The KV pass alone would mask only the
138
+ // scheme word and leave the credential after the space.
139
+ const AUTH_SCHEME_PATTERN = new RegExp(
140
+ `${B_LEFT}(authorization)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([A-Za-z][A-Za-z0-9_-]{0,31})[ \\t]+[^\\s"',&)]+`,
131
141
  'gi',
132
142
  );
133
143
  // Well-known opaque credential formats (OpenAI, GitHub, AWS, Slack, Anthropic).
@@ -237,7 +247,12 @@ export function redactSecrets(message: string): string {
237
247
  // Whole-header cookie masking before the KV pass, which would otherwise stop
238
248
  // at the first whitespace and leave every subsequent pair in the clear.
239
249
  out = out.replace(COOKIE_HEADER_PATTERN, (_m, key: string, sep: string) => `${key}${sep}[REDACTED]`);
240
- out = out.replace(SENSITIVE_KV_PATTERN, (_m, key: string, sep: string) => `${key}${sep}[REDACTED]`);
250
+ out = out.replace(AUTH_SCHEME_PATTERN, (_m, key: string, sep: string, scheme: string) => `${key}${sep}${scheme} [REDACTED]`);
251
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key: string, sep: string, val: string) => {
252
+ const q = val[0] === '"' || val[0] === "'" ? val[0] : '';
253
+ const closed = q !== '' && val.length > 1 && val.endsWith(q);
254
+ return `${key}${sep}${q}[REDACTED]${closed ? q : ''}`;
255
+ });
241
256
  out = out.replace(KNOWN_TOKEN_PATTERN, '[REDACTED]');
242
257
  out = out.replace(PROVIDER_TOKEN_PATTERN, '[REDACTED]');
243
258
  out = out.replace(LONG_HEX_SECRET_PATTERN, '[REDACTED]');
@@ -188,7 +188,7 @@ export async function exportResearchReport(
188
188
  const filepath = join(targetDir, filename);
189
189
 
190
190
  try {
191
- await fs.writeFile(filepath, result, { flag: 'wx' });
191
+ await fs.writeFile(filepath, result, { flag: 'wx', mode: 0o600 });
192
192
  logger.log(`[export] Research report saved to: ${filepath}`);
193
193
  return filepath;
194
194
  } catch (error) {
@@ -213,7 +213,11 @@ var COOKIE_HEADER_PATTERN = new RegExp(
213
213
  "gi"
214
214
  );
215
215
  var SENSITIVE_KV_PATTERN = new RegExp(
216
- `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
216
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|token|key)${B_RIGHT}(["']?\\s*[:=]\\s*)("[^"\\r\\n]{0,1024}"|'[^'\\r\\n]{0,1024}'|["']?[^\\s"',&)]+)`,
217
+ "gi"
218
+ );
219
+ var AUTH_SCHEME_PATTERN = new RegExp(
220
+ `${B_LEFT}(authorization)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([A-Za-z][A-Za-z0-9_-]{0,31})[ \\t]+[^\\s"',&)]+`,
217
221
  "gi"
218
222
  );
219
223
  var KNOWN_TOKEN_PATTERN = new RegExp(
@@ -251,7 +255,12 @@ function redactSecrets(message) {
251
255
  out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
252
256
  out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
253
257
  out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
254
- out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
258
+ out = out.replace(AUTH_SCHEME_PATTERN, (_m, key, sep, scheme) => `${key}${sep}${scheme} [REDACTED]`);
259
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep, val) => {
260
+ const q = val[0] === '"' || val[0] === "'" ? val[0] : "";
261
+ const closed = q !== "" && val.length > 1 && val.endsWith(q);
262
+ return `${key}${sep}${q}[REDACTED]${closed ? q : ""}`;
263
+ });
255
264
  out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
256
265
  out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
257
266
  out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
@@ -551,20 +551,43 @@ export function trimFrontMatter(markdown: string): string {
551
551
  * leaves extra blank lines, so they are collapsed again.
552
552
  */
553
553
  export function cleanConvertedMarkdown(markdown: string): string {
554
- return stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown)))
555
- .replace(/\n{3,}/g, '\n\n')
556
- .trim();
554
+ return collapseBlankLines(stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown)))).trim();
555
+ }
556
+
557
+ /** Opening/closing fence of a fenced code block (CommonMark: up to 3 spaces, 3+ backticks or tildes). */
558
+ const CODE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
559
+
560
+ /**
561
+ * Collapse runs of blank lines to one, EXCEPT inside fenced code blocks, where
562
+ * blank lines are content (PEP 8 spacing, Makefiles, ASCII art). An unclosed
563
+ * fence runs to the end of the document, as in CommonMark.
564
+ */
565
+ export function collapseBlankLines(markdown: string): string {
566
+ if (!markdown.includes('\n\n\n')) return markdown;
567
+ const out: string[] = [];
568
+ let fence: string | null = null;
569
+ for (const line of markdown.split('\n')) {
570
+ const m = CODE_FENCE.exec(line);
571
+ if (fence === null) {
572
+ if (m) fence = m[1]!;
573
+ } else if (m && m[1]![0] === fence[0] && m[1]!.length >= fence.length && line.slice(m[0].length).trim() === '') {
574
+ fence = null;
575
+ }
576
+ if (fence === null && line === '' && out.length > 0 && out[out.length - 1] === '') continue;
577
+ out.push(line);
578
+ }
579
+ return out.join('\n');
557
580
  }
558
581
 
559
582
  /**
560
583
  * Strip image links from markdown
561
584
  */
562
585
  export function stripImageLinks(markdown: string): string {
563
- return markdown
564
- .replace(MARKDOWN_IMAGE_PATTERN, '')
565
- .replace(IMAGE_LINK_PATTERN, '$1')
566
- .replace(/\n{3,}/g, '\n\n')
567
- .trim();
586
+ return collapseBlankLines(
587
+ markdown
588
+ .replace(MARKDOWN_IMAGE_PATTERN, '')
589
+ .replace(IMAGE_LINK_PATTERN, '$1'),
590
+ ).trim();
568
591
  }
569
592
 
570
593
  /**