@lincoln504/pi-research 1.7.6 → 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/README.md +1 -1
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli-bin.mjs +7 -1
- package/dist/cli.mjs +64 -9
- package/dist/pdf-extract-worker.mjs +11 -2
- package/dist/thread-worker.mjs +35 -6
- package/docs/ARCHITECTURE.md +2 -1
- package/docs/CHANGELOG.md +22 -1
- package/package.json +7 -7
- package/src/cli-bin.mjs +7 -1
- package/src/cli.ts +15 -2
- package/src/core/pi-version.ts +25 -5
- package/src/infrastructure/browser/thread-worker.mjs +35 -6
- package/src/knowledge/store.ts +1 -1
- package/src/tools/health-tool-definition.ts +3 -0
- package/src/tools/research-knowledge-search.ts +3 -0
- package/src/tools/research-tool-definition.ts +3 -0
- package/src/tools/tool-metadata.ts +80 -0
- 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
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ Three depth levels (normal, deep, ultra) set the team size and number of rounds.
|
|
|
92
92
|
- Node.js >= 22.22.2
|
|
93
93
|
- An LLM with a 100k+ context window (an API key or a local model)
|
|
94
94
|
- Internet access on a residential IP, since search, scraping, and YouTube transcripts get bot-blocked from datacenter/VPS/cloud IPs
|
|
95
|
-
- pi. The pi extension uses the host's copy while the standalone CLI and agent skill install it as a dependency.
|
|
95
|
+
- pi (`@earendil-works/pi-coding-agent`) 0.99.0 or newer, including the 1.x line. The pi extension uses the host's copy while the standalone CLI and agent skill install it as a dependency. A host newer than the last version this release was tested against still runs, with a one-line notice; the tested ceiling is in `src/core/pi-version.ts`.
|
|
96
96
|
- Local knowledge-store embeddings need `@huggingface/transformers`, an optional dependency. If its native image chain (sharp) cannot install, npm skips it with a warning and the install still succeeds, so everything works except local embeddings. The store tells you how to restore them. Prefer no embedding model at all? Set `PI_RESEARCH_KNOWLEDGE_STORE_RETRIEVAL=bm25` for a pure lexical (BM25) knowledge store — see [KNOWLEDGE-STORE.md](docs/KNOWLEDGE-STORE.md).
|
|
97
97
|
- Cloudflare Turnstile and similar systems block scraping on some sites. A run compensates with a wide pool of search results to scrape.
|
|
98
98
|
|
|
@@ -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();
|
|
@@ -9928,6 +9937,25 @@ var init_cancellation = __esm({
|
|
|
9928
9937
|
}
|
|
9929
9938
|
});
|
|
9930
9939
|
|
|
9940
|
+
// src/tools/tool-metadata.ts
|
|
9941
|
+
var PI_RESEARCH_TOOL_NAMESPACE, KNOWLEDGE_SEARCH_TOOL_ANNOTATIONS;
|
|
9942
|
+
var init_tool_metadata = __esm({
|
|
9943
|
+
"src/tools/tool-metadata.ts"() {
|
|
9944
|
+
"use strict";
|
|
9945
|
+
PI_RESEARCH_TOOL_NAMESPACE = {
|
|
9946
|
+
name: "pi-research",
|
|
9947
|
+
description: "Web research, knowledge-store search, and system health checks.",
|
|
9948
|
+
instructions: "research_knowledge_search is a local lookup and should be tried before research: a complete hit answers the question without any live work. research runs a multi-agent web investigation and takes minutes. health reports local service status and is the cheapest of the three."
|
|
9949
|
+
};
|
|
9950
|
+
KNOWLEDGE_SEARCH_TOOL_ANNOTATIONS = {
|
|
9951
|
+
readOnlyHint: false,
|
|
9952
|
+
destructiveHint: false,
|
|
9953
|
+
idempotentHint: false,
|
|
9954
|
+
openWorldHint: true
|
|
9955
|
+
};
|
|
9956
|
+
}
|
|
9957
|
+
});
|
|
9958
|
+
|
|
9931
9959
|
// src/tools/research-knowledge-types.ts
|
|
9932
9960
|
import { Type as Type9 } from "typebox";
|
|
9933
9961
|
var AnswerStatusEnum, ResearchKnowledgeSynthesisResponseSchema, ResearchKnowledgeSynthesisResponseSchemaAsTSchema, KnowledgeRelevanceTriageSchema;
|
|
@@ -10473,6 +10501,8 @@ function createResearchKnowledgeSearchTool(iface) {
|
|
|
10473
10501
|
"Do not call knowledge search and research for the same query in the same turn \u2014 wait for the result."
|
|
10474
10502
|
],
|
|
10475
10503
|
parameters: ResearchKnowledgeSearchParams,
|
|
10504
|
+
annotations: KNOWLEDGE_SEARCH_TOOL_ANNOTATIONS,
|
|
10505
|
+
namespace: PI_RESEARCH_TOOL_NAMESPACE,
|
|
10476
10506
|
executionMode: "parallel",
|
|
10477
10507
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
10478
10508
|
const startTime = Date.now();
|
|
@@ -10599,6 +10629,7 @@ var init_research_knowledge_search = __esm({
|
|
|
10599
10629
|
init_llm_utils();
|
|
10600
10630
|
init_service_registry();
|
|
10601
10631
|
init_llm_timeout();
|
|
10632
|
+
init_tool_metadata();
|
|
10602
10633
|
init_retry_utils();
|
|
10603
10634
|
init_service_interfaces();
|
|
10604
10635
|
init_research_knowledge_types();
|
|
@@ -18981,10 +19012,29 @@ ${kept.join("\n")}
|
|
|
18981
19012
|
${rest}` : rest;
|
|
18982
19013
|
}
|
|
18983
19014
|
function cleanConvertedMarkdown(markdown) {
|
|
18984
|
-
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");
|
|
18985
19033
|
}
|
|
18986
19034
|
function stripImageLinks(markdown) {
|
|
18987
|
-
return
|
|
19035
|
+
return collapseBlankLines(
|
|
19036
|
+
markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1")
|
|
19037
|
+
).trim();
|
|
18988
19038
|
}
|
|
18989
19039
|
function createNativeMarkdownConverter(nativeModule) {
|
|
18990
19040
|
return async (html) => {
|
|
@@ -27172,7 +27222,7 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
|
|
|
27172
27222
|
const filename = `${baseFilename}-${hash}.md`;
|
|
27173
27223
|
const filepath = join21(targetDir, filename);
|
|
27174
27224
|
try {
|
|
27175
|
-
await fs15.writeFile(filepath, result, { flag: "wx" });
|
|
27225
|
+
await fs15.writeFile(filepath, result, { flag: "wx", mode: 384 });
|
|
27176
27226
|
logger.log(`[export] Research report saved to: ${filepath}`);
|
|
27177
27227
|
return filepath;
|
|
27178
27228
|
} catch (error) {
|
|
@@ -27501,6 +27551,10 @@ function configBlock(det, extraNote) {
|
|
|
27501
27551
|
if (extraNote) lines.push(` \u2022 note: ${extraNote}`);
|
|
27502
27552
|
return lines.join("\n");
|
|
27503
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
|
+
}
|
|
27504
27558
|
async function cmdResearch(args) {
|
|
27505
27559
|
const det = await detectCredentials(args.model);
|
|
27506
27560
|
if (det.problem) {
|
|
@@ -27523,7 +27577,7 @@ Error: ${vmsg}
|
|
|
27523
27577
|
`);
|
|
27524
27578
|
return EXIT.USAGE;
|
|
27525
27579
|
}
|
|
27526
|
-
const depth = args.depth
|
|
27580
|
+
const depth = resolveCliDepth(args.depth, getConfig(process.cwd(), "cli"));
|
|
27527
27581
|
const runModel = det.model;
|
|
27528
27582
|
toStderr(`[pi-research] starting research (depth ${depth})${runModel ? ` with ${runModel}` : ""}\u2026
|
|
27529
27583
|
`);
|
|
@@ -28496,5 +28550,6 @@ export {
|
|
|
28496
28550
|
exitCodeForSignal,
|
|
28497
28551
|
makeProgressObserver,
|
|
28498
28552
|
parseArgs,
|
|
28499
|
-
reportError
|
|
28553
|
+
reportError,
|
|
28554
|
+
resolveCliDepth
|
|
28500
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/ARCHITECTURE.md
CHANGED
|
@@ -372,7 +372,8 @@ changes which pairing needs re-validating.
|
|
|
372
372
|
|
|
373
373
|
Pinned validation library — `typebox` (TypeBox 1.x, unscoped package name) is pinned to the
|
|
374
374
|
exact version the pi host packages depend on (`@earendil-works/pi-ai` /
|
|
375
|
-
`@earendil-works/pi-coding-agent` 0.
|
|
375
|
+
`@earendil-works/pi-coding-agent` 1.0.0 both pin `1.3.27` — verified 2026-10-01 against 1.0.0
|
|
376
|
+
(and against 0.99.2 before it), after an
|
|
376
377
|
undocumented drift was caught in the 0.84/0.85 era and realigned; that drift was to `1.3.26`,
|
|
377
378
|
and the version now pinned is `1.3.27`, both re-checked directly in the installed host
|
|
378
379
|
packages' `package.json`). Every
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,13 +5,34 @@ 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
|
+
|
|
18
|
+
## [1.7.7] - 2026-10-01
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **pi 1.0.0 support.** The peer ranges said `>=0.99.0 <1`, which excluded the current pi release: a standalone install resolved 0.99.x while the user's pi was 1.0.0, and the extension printed an untested-host notice on every session. The ranges are now `>=0.99.0 <2` and the tested ceiling is 1.0.0. The whole public surface was diffed 0.99.2 -> 1.0.0: `@earendil-works/pi-ai` is identical in its declarations, the extension API (`core/extensions/index.d.ts`, the file every tool, event and context type comes from) is unchanged, and `docs/extensions.md` is unchanged. `pi-coding-agent` adds `ModelRegistry.generateImages()` and widens `quietStartup` to `boolean | "header"`; `pi-tui` adds `isAppleTerminalSession`, `flattenLines()` and `TUI.getScreenLines()`. Nothing was removed or re-signed, so the 0.99.0 floor still describes the oldest host those APIs exist on, and the version number is a milestone rather than a break for this package. Verified by installing 1.0.0 into this repo, type-checking both projects against it, running the unit and integration suites (254 files / 3207 tests, 57 + 103 integration), and calling the `health` tool through a real pi 1.0.0 session. Two 1.0.0 behaviour changes are worth knowing and change nothing here: the TUI now defaults to fullscreen (this extension renders through `ctx.ui.*` widgets, which work in both modes) and built-in extensions and tools are named `builtin:<name>` in errors and diagnostics.
|
|
23
|
+
- **The three tools declare MCP-style `annotations` and one shared `namespace`.** Both fields arrived in pi 0.99 and are surfaced by 1.0.0: `pi.getAllTools()` reports them, the documented permission pattern reads the annotations to decide which calls to confirm, and codemode lists the tools of one namespace under a single heading. `research` and `research_knowledge_search` are declared `openWorldHint: true` because they reach the network and spend provider tokens, `health` is `idempotentHint: true` and closed-world, and all three are `readOnlyHint: false` with `destructiveHint: false`: they append to the knowledge store, and `health` with `probe: true` starts the browser pool. The hints are author-declared and unverified, so they under-claim deliberately, because `readOnlyHint` is the one hint that can suppress a confirmation a user should have been asked for.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Host-version references in `docs/ARCHITECTURE.md` and the ES/JA/ZH docs said the typebox pin was verified against 0.99.2. It is still `1.3.27` in both host packages, re-verified against 1.0.0, and the docs now say so. The README requirements state the supported host range and that a host newer than the tested ceiling still runs, with a one-line notice.
|
|
28
|
+
|
|
8
29
|
## [1.7.6] - 2026-10-01
|
|
9
30
|
|
|
10
31
|
### Fixed
|
|
11
32
|
|
|
12
33
|
- **The missing-peer remedy names the package that is actually missing.** The 1.7.5 shim's bullet list correctly enumerated every absent `@earendil-works/pi-*` package, but the command underneath it was hardcoded to `npm install -g @earendil-works/pi-coding-agent` with the comment "add just the missing host package". When `pi-ai` (and not `pi-coding-agent`) was the absent one, the instruction named a package the user already had and following it changed nothing. The remedy is now built from the detected set, so both peers are named when both are missing and only the absent one is named otherwise. Caught in review; pinned by a test that asserts the present package is never the one the remedy suggests installing.
|
|
13
34
|
- **The bin preflight detects an install whose files are incomplete, not just an absent package.** It resolved each peer and treated a successful resolve as healthy. Resolving is not loading: an interrupted install, a pruned `node_modules` or a bad package-cache restore leaves `package.json` and its `exports` map intact with the file they point at gone, so the preflight passed it through and the bundle died on that import with the raw `ERR_MODULE_NOT_FOUND` the shim exists to replace. The preflight now also checks that the resolved file exists and reports `installed, but its files are incomplete` with the same exit 78. Checked with `existsSync` rather than by importing the module, which would run the host packages' side effects before the CLI has decided to start. Verified by reverting each half in isolation: without the `argv[1]` rewrite the stub engine exits 97, with a resolve-only preflight a deliberately truncated peer produces `ERR_MODULE_NOT_FOUND`, and the fixed shim produces exit 78.
|
|
14
|
-
- **`dist/cli-bin.mjs` ships executable.** It
|
|
35
|
+
- **`dist/cli-bin.mjs` ships executable.** It went out at mode 644 because the build copies it with `copyFileSync`, which preserves the committed source mode. No install was broken by that: npm, pnpm and bun each chmod the declared `bin` target when they link it, which is how 1.7.5 worked. The point is not to depend on it, since the bin is the one file a shell may execute directly. The build now chmods 755 after the copy and the source is committed executable. (Measured correction, 2026-10-01: this entry first said `dist/cli.mjs` shipped at 755 for contrast. It does not. npm publishes every file in this package at 644, `dist/cli.mjs` included, in both the 1.7.5 and 1.7.6 tarballs. That file is never executed directly, only via `node` or a manager-generated shim, so its mode does not matter.)
|
|
15
36
|
|
|
16
37
|
## [1.7.5] - 2026-10-01
|
|
17
38
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "1.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,
|
|
@@ -133,9 +133,9 @@
|
|
|
133
133
|
"youtubei.js": "^18.0.0"
|
|
134
134
|
},
|
|
135
135
|
"peerDependencies": {
|
|
136
|
-
"@earendil-works/pi-ai": ">=0.99.0 <
|
|
137
|
-
"@earendil-works/pi-coding-agent": ">=0.99.0 <
|
|
138
|
-
"@earendil-works/pi-tui": ">=0.99.0 <
|
|
136
|
+
"@earendil-works/pi-ai": ">=0.99.0 <2",
|
|
137
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0 <2",
|
|
138
|
+
"@earendil-works/pi-tui": ">=0.99.0 <2"
|
|
139
139
|
},
|
|
140
140
|
"optionalDependencies": {
|
|
141
141
|
"@huggingface/transformers": "4.3.0",
|
|
@@ -159,9 +159,9 @@
|
|
|
159
159
|
"sharp": "^0.35.0"
|
|
160
160
|
},
|
|
161
161
|
"devDependencies": {
|
|
162
|
-
"@earendil-works/pi-ai": ">=0.99.0 <
|
|
163
|
-
"@earendil-works/pi-coding-agent": ">=0.99.0 <
|
|
164
|
-
"@earendil-works/pi-tui": ">=0.99.0 <
|
|
162
|
+
"@earendil-works/pi-ai": ">=0.99.0 <2",
|
|
163
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0 <2",
|
|
164
|
+
"@earendil-works/pi-tui": ">=0.99.0 <2",
|
|
165
165
|
"@eslint/js": "^10.0.1",
|
|
166
166
|
"@types/jsdom": "^30.0.0",
|
|
167
167
|
"@typescript-eslint/eslint-plugin": "8.71.0",
|
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`);
|
package/src/core/pi-version.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - As a pi EXTENSION, the host supplies `@earendil-works/*`. Our package.json
|
|
9
9
|
* range never constrains it, so an in-process check is the only enforcement.
|
|
10
|
-
* - As a standalone CLI/SDK, the range is `>=0.99.0 <
|
|
11
|
-
* carry no lockfile, so every fresh install resolves the newest
|
|
12
|
-
* instant.
|
|
13
|
-
* already has (0.83.0 extended the ResourceLoader
|
|
10
|
+
* - As a standalone CLI/SDK, the range is `>=0.99.0 <2` and published tarballs
|
|
11
|
+
* carry no lockfile, so every fresh install resolves the newest release in
|
|
12
|
+
* that window at that instant. The host is young: a minor bump may break
|
|
13
|
+
* anything under semver, and already has (0.83.0 extended the ResourceLoader
|
|
14
|
+
* contract). The upper bound is `<2` rather than `<1` because 1.0.0 changed
|
|
15
|
+
* no API this package uses — see the 1.0.0 note on the tested ceiling — and
|
|
16
|
+
* `<1` would exclude the current release from a fresh install.
|
|
14
17
|
*
|
|
15
18
|
* Hence two thresholds, not one. Below the FLOOR we throw — those APIs are gone
|
|
16
19
|
* and nothing will work. Above the TESTED ceiling we warn once and continue: a
|
|
@@ -102,8 +105,25 @@ export const PI_MIN_VERSION: SemverParts = { major: 0, minor: 99, patch: 0 };
|
|
|
102
105
|
* Bump this — deliberately — when a new pi is verified, not automatically.
|
|
103
106
|
* Compared on MAJOR.MINOR only: a patch bump within a tested line is not a new
|
|
104
107
|
* surface, and warning on it would be noise.
|
|
108
|
+
*
|
|
109
|
+
* 1.0.0 is a major version number, not a breaking change for this package. The
|
|
110
|
+
* full public surface was diffed against 0.99.2 (2026-10-01): `@earendil-works/pi-ai`
|
|
111
|
+
* is byte-identical in its declarations, and the extension API
|
|
112
|
+
* (`core/extensions/index.d.ts`, the file every tool, event and context type
|
|
113
|
+
* comes from) is unchanged, as is `docs/extensions.md`. `pi-coding-agent` adds
|
|
114
|
+
* one method (`ModelRegistry.generateImages()`), widens `quietStartup` to
|
|
115
|
+
* `boolean | "header"`, and adds internal fields; `pi-tui` adds
|
|
116
|
+
* `isAppleTerminalSession`, `flattenLines()` and `TUI.getScreenLines()`.
|
|
117
|
+
* Nothing was removed or re-signed, so the 0.99.0 floor still describes the
|
|
118
|
+
* oldest host these APIs exist on.
|
|
119
|
+
*
|
|
120
|
+
* The behavioural changes in 1.0.0 that a user can notice are the fullscreen TUI
|
|
121
|
+
* default (this extension renders through `ctx.ui.*` widgets, which work in both
|
|
122
|
+
* modes), the `builtin:<name>` naming for built-in extensions and tools, and
|
|
123
|
+
* `--no-extensions` now also disabling the built-in extensions. None of them
|
|
124
|
+
* change what this package does.
|
|
105
125
|
*/
|
|
106
|
-
export const PI_TESTED_MAX_VERSION: SemverParts = { major:
|
|
126
|
+
export const PI_TESTED_MAX_VERSION: SemverParts = { major: 1, minor: 0, patch: 0 };
|
|
107
127
|
|
|
108
128
|
export type PiCompatibilityLevel = 'ok' | 'unparseable' | 'too-old' | 'untested';
|
|
109
129
|
|
|
@@ -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
|
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import { Type, type Static } from 'typebox';
|
|
14
14
|
import { Value } from 'typebox/value';
|
|
15
15
|
import { healthRegistry } from '../healthcheck/index.ts';
|
|
16
|
+
import { HEALTH_TOOL_ANNOTATIONS, PI_RESEARCH_TOOL_NAMESPACE } from './tool-metadata.ts';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Create the health check tool definition
|
|
@@ -37,6 +38,8 @@ export function createHealthTool(): ToolDefinition {
|
|
|
37
38
|
description: 'Verify system status (browser pool, knowledge store, GPU lock).',
|
|
38
39
|
promptSnippet: 'Run system health checks',
|
|
39
40
|
parameters,
|
|
41
|
+
annotations: HEALTH_TOOL_ANNOTATIONS,
|
|
42
|
+
namespace: PI_RESEARCH_TOOL_NAMESPACE,
|
|
40
43
|
renderShell: 'self',
|
|
41
44
|
executionMode: 'parallel',
|
|
42
45
|
async execute(
|
|
@@ -37,6 +37,7 @@ import { recordLlmUsage } from '../utils/llm-usage.ts';
|
|
|
37
37
|
import { buildSafeOptions, validateAndExtractText, buildConstrainedSubmitTool, completeSimpleStructured } from '../core/llm/llm-utils.ts';
|
|
38
38
|
import { getService, tryGetServiceContainerFromCtx } from '../core/service-registry.ts';
|
|
39
39
|
import { withTimeout } from '../core/llm/llm-timeout.ts';
|
|
40
|
+
import { KNOWLEDGE_SEARCH_TOOL_ANNOTATIONS, PI_RESEARCH_TOOL_NAMESPACE } from './tool-metadata.ts';
|
|
40
41
|
import { abortableDelay } from '../web-research/retry-utils.ts';
|
|
41
42
|
import { ServiceNames } from '../core/service-interfaces.ts';
|
|
42
43
|
import type { IKnowledgeStoreService } from '../core/service-interfaces.ts';
|
|
@@ -894,6 +895,8 @@ export function createResearchKnowledgeSearchTool(iface?: ConfigInterface): Tool
|
|
|
894
895
|
'Do not call knowledge search and research for the same query in the same turn — wait for the result.',
|
|
895
896
|
],
|
|
896
897
|
parameters: ResearchKnowledgeSearchParams,
|
|
898
|
+
annotations: KNOWLEDGE_SEARCH_TOOL_ANNOTATIONS,
|
|
899
|
+
namespace: PI_RESEARCH_TOOL_NAMESPACE,
|
|
897
900
|
executionMode: 'parallel',
|
|
898
901
|
async execute(
|
|
899
902
|
_toolCallId: string,
|
|
@@ -19,6 +19,7 @@ import type { ResearchDepth, CleanupContext } from '../types/index.ts';
|
|
|
19
19
|
import { Type, type Static } from 'typebox';
|
|
20
20
|
import { Value } from 'typebox/value';
|
|
21
21
|
import { validateConfig, getConfig, type ConfigInterface } from '../config.ts';
|
|
22
|
+
import { RESEARCH_TOOL_ANNOTATIONS, PI_RESEARCH_TOOL_NAMESPACE } from './tool-metadata.ts';
|
|
22
23
|
import { tryGetServiceContainerFromCtx, getService } from '../core/service-registry.ts';
|
|
23
24
|
|
|
24
25
|
import { ServiceNames } from '../core/service-interfaces.ts';
|
|
@@ -159,6 +160,8 @@ export function createResearchTool(iface?: ConfigInterface): ToolDefinition {
|
|
|
159
160
|
'Perform multi-source web research using search, scraping, and specialized databases.',
|
|
160
161
|
promptSnippet: 'Execute comprehensive web research',
|
|
161
162
|
parameters,
|
|
163
|
+
annotations: RESEARCH_TOOL_ANNOTATIONS,
|
|
164
|
+
namespace: PI_RESEARCH_TOOL_NAMESPACE,
|
|
162
165
|
renderShell: 'self',
|
|
163
166
|
executionMode: 'parallel',
|
|
164
167
|
prepareArguments: (args: unknown) => {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared metadata for the tools this extension registers.
|
|
3
|
+
*
|
|
4
|
+
* pi 0.99 added two MCP-shaped fields to `ToolDefinition`: `annotations` and
|
|
5
|
+
* `namespace`. Neither changes how a tool runs. They are what pi reports and what
|
|
6
|
+
* other extensions read:
|
|
7
|
+
*
|
|
8
|
+
* - `pi.getAllTools()` reports each tool's `exposure`, `namespace` and
|
|
9
|
+
* `annotations`, and the documented permission pattern reads the annotations
|
|
10
|
+
* to decide which calls to confirm.
|
|
11
|
+
* - codemode lists the tools of one namespace under a single heading with the
|
|
12
|
+
* namespace `description`; `instructions` is returned on request by
|
|
13
|
+
* `describeNamespace(name)` and is not part of any tool listing.
|
|
14
|
+
*
|
|
15
|
+
* The hints come from the tool's author and are NOT verified by pi, so the rule
|
|
16
|
+
* here is to under-claim. `readOnlyHint` is true only for a tool that cannot
|
|
17
|
+
* change anything the user would have to undo, and `openWorldHint` is true for
|
|
18
|
+
* anything that can reach the network. A permission extension that trusts these
|
|
19
|
+
* hints to skip a confirmation must not be able to skip one for a call that
|
|
20
|
+
* starts a browser, spends money on a provider, or writes to the knowledge store.
|
|
21
|
+
*
|
|
22
|
+
* MCP defaults for absent hints are "not read-only, may be destructive, may reach
|
|
23
|
+
* an open world", so declaring a hint can only ever narrow what a permission
|
|
24
|
+
* extension confirms — with one exception: `readOnlyHint: true` on a tool that
|
|
25
|
+
* does write would silence a confirmation it should have asked for. That is the
|
|
26
|
+
* claim to be most careful about.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ToolAnnotations, ToolNamespace } from '@earendil-works/pi-coding-agent';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One namespace for every tool this extension registers, so codemode lists them
|
|
33
|
+
* together instead of scattering three unrelated entries through its tool list.
|
|
34
|
+
*/
|
|
35
|
+
export const PI_RESEARCH_TOOL_NAMESPACE: ToolNamespace = {
|
|
36
|
+
name: 'pi-research',
|
|
37
|
+
description: 'Web research, knowledge-store search, and system health checks.',
|
|
38
|
+
instructions:
|
|
39
|
+
'research_knowledge_search is a local lookup and should be tried before research: ' +
|
|
40
|
+
'a complete hit answers the question without any live work. research runs a ' +
|
|
41
|
+
'multi-agent web investigation and takes minutes. health reports local service ' +
|
|
42
|
+
'status and is the cheapest of the three.',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `research`: reaches the open web, spawns sub-agents, spends provider tokens, and
|
|
47
|
+
* appends to the knowledge store and the session report. Additive, so not
|
|
48
|
+
* destructive, and every run is a new investigation, so not idempotent.
|
|
49
|
+
*/
|
|
50
|
+
export const RESEARCH_TOOL_ANNOTATIONS: ToolAnnotations = {
|
|
51
|
+
readOnlyHint: false,
|
|
52
|
+
destructiveHint: false,
|
|
53
|
+
idempotentHint: false,
|
|
54
|
+
openWorldHint: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `research_knowledge_search`: reads the local store, and on a miss can go live
|
|
59
|
+
* (network, provider tokens) and write what it learned back. Not read-only for the
|
|
60
|
+
* same reason as `research`.
|
|
61
|
+
*/
|
|
62
|
+
export const KNOWLEDGE_SEARCH_TOOL_ANNOTATIONS: ToolAnnotations = {
|
|
63
|
+
readOnlyHint: false,
|
|
64
|
+
destructiveHint: false,
|
|
65
|
+
idempotentHint: false,
|
|
66
|
+
openWorldHint: true,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `health`: reports local status. Not read-only because `probe: true` forces
|
|
71
|
+
* liveness checks that can start the browser pool and load models, and repeating a
|
|
72
|
+
* probe has no further effect, so it is idempotent. It reaches the local machine,
|
|
73
|
+
* not the open web.
|
|
74
|
+
*/
|
|
75
|
+
export const HEALTH_TOOL_ANNOTATIONS: ToolAnnotations = {
|
|
76
|
+
readOnlyHint: false,
|
|
77
|
+
destructiveHint: false,
|
|
78
|
+
idempotentHint: true,
|
|
79
|
+
openWorldHint: false,
|
|
80
|
+
};
|
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
|
/**
|