@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.
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli-bin.mjs +7 -1
- package/dist/cli.mjs +42 -9
- package/dist/pdf-extract-worker.mjs +11 -2
- package/dist/thread-worker.mjs +35 -6
- package/docs/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/cli-bin.mjs +7 -1
- package/src/cli.ts +15 -2
- package/src/infrastructure/browser/thread-worker.mjs +35 -6
- package/src/knowledge/store.ts +1 -1
- package/src/utils/log-utils.ts +18 -3
- package/src/utils/research-export.ts +1 -1
- package/src/web-research/pdf-extract-worker.mjs +11 -2
- package/src/web-research/scraper-utils.ts +31 -8
|
@@ -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.
|
|
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 = [
|
|
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(
|
|
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*["']?
|
|
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)))
|
|
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
|
|
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
|
|
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*["']?
|
|
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(
|
|
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]");
|
package/dist/thread-worker.mjs
CHANGED
|
@@ -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(
|
|
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*["']?
|
|
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)))
|
|
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
|
|
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
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 = [
|
|
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
|
|
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(
|
|
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*["']?
|
|
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)))
|
|
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
|
|
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/src/knowledge/store.ts
CHANGED
|
@@ -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
|
|
package/src/utils/log-utils.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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*["']?
|
|
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(
|
|
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
|
-
|
|
556
|
-
|
|
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
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
586
|
+
return collapseBlankLines(
|
|
587
|
+
markdown
|
|
588
|
+
.replace(MARKDOWN_IMAGE_PATTERN, '')
|
|
589
|
+
.replace(IMAGE_LINK_PATTERN, '$1'),
|
|
590
|
+
).trim();
|
|
568
591
|
}
|
|
569
592
|
|
|
570
593
|
/**
|