@lincoln504/pi-research 1.6.11 → 1.6.13
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 +5 -1
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli.mjs +26 -8
- package/docs/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/src/stackexchange/output/compact.ts +2 -1
- package/src/stackexchange/output/table.ts +17 -6
- package/src/tools/stackexchange.ts +1 -0
- package/src/utils/plain-url.ts +44 -0
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
<a href="docs/DOCUMENTATION-ES.md">ES</a>
|
|
23
23
|
·
|
|
24
24
|
<a href="docs/DOCUMENTATION-ZH.md">中文研究</a>
|
|
25
|
+
·
|
|
26
|
+
<a href="docs/DOCUMENTATION-JA.md">日本語研究</a>
|
|
25
27
|
</p>
|
|
26
28
|
|
|
27
29
|
---
|
|
@@ -32,6 +34,8 @@
|
|
|
32
34
|
|
|
33
35
|
Search and scraping run locally through a stealth browser with no search provider and no monthly cap. The only cost is LLM tokens.
|
|
34
36
|
|
|
37
|
+
**Current recommended model:** [`inclusionai/ling-3.0-flash`](https://openrouter.ai/inclusionai/ling-3.0-flash) on OpenRouter — cheap, intelligent, and fast.
|
|
38
|
+
|
|
35
39
|
### Install
|
|
36
40
|
|
|
37
41
|
As a [Pi](https://github.com/earendil-works/pi) Coding Agent extension:
|
|
@@ -66,7 +70,7 @@ A research run loops through agent teams: a coordinator plans and starts with a
|
|
|
66
70
|
|
|
67
71
|
Three depth levels (normal, deep, ultra) set the team size and number of rounds. A natural-language request is enough; the tool picks the right one:
|
|
68
72
|
|
|
69
|
-

|
|
70
74
|
|
|
71
75
|
### Use cases
|
|
72
76
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pi-research
|
|
3
3
|
description: "Multi-agent web research for any coding agent. Use for anything requiring current information from the internet — news, trends, product/service/company facts, statistics, people, places, events, APIs, library behavior, or 'what is X / how does X work' questions. Also use to search a local knowledge store of previously researched findings before doing live research. Powered by the pi-research engine (search, stealth scraping, security databases, Stack Exchange). Not for reading local files, running project commands, or analyzing this repository's own source."
|
|
4
4
|
license: MIT
|
|
5
|
-
metadata: { "author": "Lincoln Deen", "version": "1.6.
|
|
5
|
+
metadata: { "author": "Lincoln Deen", "version": "1.6.13", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
|
|
6
6
|
allowed-tools: Bash(node:*)
|
|
7
7
|
---
|
|
8
8
|
|
package/dist/cli.mjs
CHANGED
|
@@ -21804,6 +21804,21 @@ var Filters = {
|
|
|
21804
21804
|
ANSWERS_WITH_BODY: "!9_bDE(fI5"
|
|
21805
21805
|
};
|
|
21806
21806
|
|
|
21807
|
+
// src/utils/plain-url.ts
|
|
21808
|
+
var MIN_FENCE = 3;
|
|
21809
|
+
function plainUrl(url) {
|
|
21810
|
+
const safe = url.replace(/`/g, "%60");
|
|
21811
|
+
return `\`${safe}\``;
|
|
21812
|
+
}
|
|
21813
|
+
function fenceBlock(content) {
|
|
21814
|
+
const runs = content.match(/`+/g) ?? [];
|
|
21815
|
+
const longest = runs.reduce((max, run) => Math.max(max, run.length), 0);
|
|
21816
|
+
const fence = "`".repeat(Math.max(MIN_FENCE, longest + 1));
|
|
21817
|
+
return `${fence}text
|
|
21818
|
+
${content}
|
|
21819
|
+
${fence}`;
|
|
21820
|
+
}
|
|
21821
|
+
|
|
21807
21822
|
// src/stackexchange/output/table.ts
|
|
21808
21823
|
function formatQuestionsTable(questions) {
|
|
21809
21824
|
if (questions.length === 0) {
|
|
@@ -21829,13 +21844,15 @@ function formatQuestionsTable(questions) {
|
|
|
21829
21844
|
output += `- **Author:** ${q.owner.display_name} (rep: ${q.owner.reputation})
|
|
21830
21845
|
`;
|
|
21831
21846
|
}
|
|
21832
|
-
output += `- **Link:** ${q.link}
|
|
21847
|
+
output += `- **Link:** ${plainUrl(q.link)}
|
|
21833
21848
|
`;
|
|
21834
21849
|
output += `- **Created:** ${new Date(q.creation_date * 1e3).toLocaleString()}
|
|
21835
21850
|
`;
|
|
21836
21851
|
if (q.body) {
|
|
21837
21852
|
const body = q.body.length > 500 ? `${q.body.substring(0, 500)}...` : q.body;
|
|
21838
|
-
output += `- **Body:**
|
|
21853
|
+
output += `- **Body:**
|
|
21854
|
+
|
|
21855
|
+
${fenceBlock(body)}
|
|
21839
21856
|
`;
|
|
21840
21857
|
}
|
|
21841
21858
|
output += "\n---\n\n";
|
|
@@ -21870,7 +21887,7 @@ function formatAnswersTable(answers) {
|
|
|
21870
21887
|
output += `
|
|
21871
21888
|
### Answer Body
|
|
21872
21889
|
|
|
21873
|
-
${body}
|
|
21890
|
+
${fenceBlock(body)}
|
|
21874
21891
|
`;
|
|
21875
21892
|
}
|
|
21876
21893
|
output += "\n---\n\n";
|
|
@@ -21902,10 +21919,10 @@ function formatUsersTable(users) {
|
|
|
21902
21919
|
`;
|
|
21903
21920
|
}
|
|
21904
21921
|
if (u.website_url) {
|
|
21905
|
-
output += `- **Website:** ${u.website_url}
|
|
21922
|
+
output += `- **Website:** ${plainUrl(u.website_url)}
|
|
21906
21923
|
`;
|
|
21907
21924
|
}
|
|
21908
|
-
output += `- **Profile:** ${u.link}
|
|
21925
|
+
output += `- **Profile:** ${plainUrl(u.link)}
|
|
21909
21926
|
`;
|
|
21910
21927
|
output += "\n---\n\n";
|
|
21911
21928
|
}
|
|
@@ -21936,7 +21953,7 @@ function formatCompactQuestions(questions) {
|
|
|
21936
21953
|
let index = 1;
|
|
21937
21954
|
for (const q of questions) {
|
|
21938
21955
|
const accepted = q.accepted_answer_id ? "[accepted]" : "";
|
|
21939
|
-
output += `${index}.
|
|
21956
|
+
output += `${index}. ${q.title} ${accepted} (score: ${q.score}, answers: ${q.answer_count}) ${plainUrl(q.link)}
|
|
21940
21957
|
`;
|
|
21941
21958
|
index++;
|
|
21942
21959
|
}
|
|
@@ -21952,7 +21969,7 @@ function formatQuestionsCompact(questions) {
|
|
|
21952
21969
|
let index = 1;
|
|
21953
21970
|
for (const q of questions) {
|
|
21954
21971
|
const accepted = q.accepted_answer_id ? "[accepted]" : " ";
|
|
21955
|
-
lines.push(`${index}. ${accepted}
|
|
21972
|
+
lines.push(`${index}. ${accepted} ${q.title} (${q.score} pts, ${q.answer_count} ans, ${q.view_count} views) ${plainUrl(q.link)}`);
|
|
21956
21973
|
index++;
|
|
21957
21974
|
}
|
|
21958
21975
|
return lines.join("\n");
|
|
@@ -22324,7 +22341,8 @@ function createStackexchangeTool(options) {
|
|
|
22324
22341
|
"Anonymous access: 300 requests/day. Set STACKEXCHANGE_API_KEY env var for 10,000/day.",
|
|
22325
22342
|
"Use tags to filter by specific topics.",
|
|
22326
22343
|
"Use maxPages parameter to control pagination for search results (default: 5 pages).",
|
|
22327
|
-
`CRITICAL: You are allowed a maximum of ${getMaxGatheringCalls(options.config)} gathering calls across your web tools (search, security_search, stackexchange). Use them for breadth
|
|
22344
|
+
`CRITICAL: You are allowed a maximum of ${getMaxGatheringCalls(options.config)} gathering calls across your web tools (search, security_search, stackexchange). Use them for breadth.`,
|
|
22345
|
+
"URLs in results are wrapped in backticks on purpose: they are data, not clickable links. Never reformat them as markdown links \u2014 a click in the terminal opens a real browser tab."
|
|
22328
22346
|
],
|
|
22329
22347
|
parameters: StackExchangeParamsSchema,
|
|
22330
22348
|
executionMode: "parallel",
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.6.13] - 2026-09-08
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **README: the model recommendation moved to the top and shrank to one sentence.** The long RESEARCH_MODEL paragraph that sat inside Requirements/limitations since the 2026-09-07 commit is now a bold "**Current recommended model:** [`inclusionai/ling-3.0-flash`](https://openrouter.ai/inclusionai/ling-3.0-flash) on OpenRouter — cheap, intelligent, and fast." line above Install; the pricing/rate-limit detail was dropped.
|
|
15
|
+
|
|
16
|
+
- **README: the prompt-research GIF was replaced by a static frame.** `02-prompt-research.gif` (2.8 MB) is removed from the repo; `docs/media/prompt-research.png` (23 KB, the frame showing the "Research in depth the history of apples" prompt and the run kicking off) takes its place. The GIF was referenced only by README.md, so no other docs change; the tarball gets ~2.8 MB lighter.
|
|
17
|
+
|
|
18
|
+
- **Dependency refresh, lockfile-only: `@earendil-works/*` 0.85.0→0.85.1 and `@typescript-eslint/*` 8.69→8.70.** Both are within the existing package.json ranges, so consumer installs are unaffected by the lockfile change itself — a fresh install already resolves 0.85.1 (verified by packing the tarball and installing it into a clean project: pi 0.85.1 resolves, the documented dual apache-arrow layout reproduces, `pi-research --help` boots). `PI_TESTED_MAX_VERSION` compares on major.minor only, so the patch stays inside the tested window with no warning. Deliberate holds unchanged: apache-arrow 21.1.0 (LanceDB IPC), playwright-core 1.60.0 (camoufox < 1.61), typescript < 6.1.0 (eslint peer ceiling).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`npm ci` under npm 12 rejected the lockfile an `npm update` had rewritten** (this release cycle's own dep refresh): the rewrite dropped the `node-domexception@1.0.28` alias entry backing the `@nolyfill/domexception` override while leaving a stale real `node-domexception@1.0.0` copy nested under `@earendil-works/pi-coding-agent`. npm 11 (the local dev npm) accepted the tree; CI's npm 12 is stricter and failed every `npm ci --legacy-peer-deps` leg. The lockfile was regenerated with npm 12 itself, which collapses the stale nested copy so the override is the only resolution; `npm ci --legacy-peer-deps` now validates clean under both npm 11 and npm 12, and CI is green cross-OS.
|
|
23
|
+
|
|
24
|
+
- **Stack Exchange tool output can no longer open a real browser tab when clicked.** The tool's formatters emitted markdown links (`[title](link)`) and bare URLs, and question/answer bodies carried third-party markup — any markdown-rendering surface (assistant replies, research reports, the terminal UI, which turns markdown links AND bare URLs into OSC 8 terminal hyperlinks and opens them in the system browser on mouse press+release) turned these into click-to-open-tab targets in the middle of scrolling tool output. New `src/utils/plain-url.ts` fixes this at the source, cross-platform and renderer-independent: `plainUrl()` wraps every emitted URL in backticks (code spans tokenize before autolink/link parsing in every CommonMark renderer, so a code-spanned URL can never become a link but stays byte-exact and copyable for the model), and `fenceBlock()` wraps third-party body content in a fenced code block whose length grows past any backtick run in the content (embedded links, headings and tables render inert; content containing its own ``` fences cannot close the fence early). All stackexchange formatters (questions, answers, users, sites, compact variants, and the `get`+compact path) now emit code-spanned URLs and own-line-fenced bodies — fence placement matters: an opening fence inline after `- **Body:**` is NOT recognized as a code block by CommonMark and was verified (against pi's bundled `marked`) to still render embedded links clickable, so the fence starts on its own line. The tool's prompt guidelines now also instruct the model never to re-emit result URLs as markdown links, covering the assistant-echo path. Pinned by `test/unit/stackexchange/output/no-clickable-links.test.ts`, which scans each formatter's clickable surface (post code-span/fence stripping) for link syntax and — when `marked` is resolvable — renders the actual output and asserts zero anchor elements.
|
|
25
|
+
|
|
26
|
+
- **CI: declare `vite` as an explicit devDependency.** CI installs with `--legacy-peer-deps`, which skips peer dependency resolution — `vite` was only reachable as a transitive peer and the install could not resolve it. Now declared directly.
|
|
27
|
+
|
|
28
|
+
### Verified
|
|
29
|
+
|
|
30
|
+
- 3,064 unit tests over 242 files, ESLint clean, all four type-checks (TS 6 src + tests, TS 7 native src + tests), dependency-cruiser and madge clean (190 modules, no circular dependencies), build, and `npm ci --dry-run` manifest verification; `npm audit` reports 0 vulnerabilities. The packed tarball was installed into a clean project: pi 0.85.1 resolves fresh, the documented dual apache-arrow layout (21.1.0 nested / 18.1.0 hoisted for LanceDB) reproduces, and the installed CLI boots. CI green cross-OS (ubuntu ×2, macOS, Windows) including the consumer fresh-install and Arrow/LanceDB interop legs.
|
|
31
|
+
|
|
8
32
|
## [1.6.11] - 2026-09-04
|
|
9
33
|
|
|
10
34
|
### Changed
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Question, Answer, User, Site } from '../types.ts';
|
|
6
|
+
import { plainUrl } from '../../utils/plain-url.ts';
|
|
6
7
|
|
|
7
8
|
export function formatQuestionsCompact(questions: Question[]): string {
|
|
8
9
|
if (questions.length === 0) {
|
|
@@ -13,7 +14,7 @@ export function formatQuestionsCompact(questions: Question[]): string {
|
|
|
13
14
|
let index = 1;
|
|
14
15
|
for (const q of questions) {
|
|
15
16
|
const accepted = q.accepted_answer_id ? '[accepted]' : ' ';
|
|
16
|
-
lines.push(`${index}. ${accepted}
|
|
17
|
+
lines.push(`${index}. ${accepted} ${q.title} (${q.score} pts, ${q.answer_count} ans, ${q.view_count} views) ${plainUrl(q.link)}`);
|
|
17
18
|
index++;
|
|
18
19
|
}
|
|
19
20
|
return lines.join('\n');
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Question, Answer, User, Site } from '../types.ts';
|
|
6
|
+
import { plainUrl, fenceBlock } from '../../utils/plain-url.ts';
|
|
6
7
|
|
|
7
8
|
export function formatQuestionsTable(questions: Question[]): string {
|
|
8
9
|
if (questions.length === 0) {
|
|
@@ -21,7 +22,10 @@ export function formatQuestionsTable(questions: Question[]): string {
|
|
|
21
22
|
if (q.owner) {
|
|
22
23
|
output += `- **Author:** ${q.owner.display_name} (rep: ${q.owner.reputation})\n`;
|
|
23
24
|
}
|
|
24
|
-
output
|
|
25
|
+
// plainUrl: URLs in tool output are data, not interactive links — see
|
|
26
|
+
// plain-url.ts. Bare URLs would be GFM-autolinked into clickable OSC 8
|
|
27
|
+
// hyperlinks by markdown renderers (accidental browser-tab opens).
|
|
28
|
+
output += `- **Link:** ${plainUrl(q.link)}\n`;
|
|
25
29
|
output += `- **Created:** ${new Date(q.creation_date * 1000).toLocaleString()}\n`;
|
|
26
30
|
|
|
27
31
|
if (q.body) {
|
|
@@ -29,7 +33,13 @@ export function formatQuestionsTable(questions: Question[]): string {
|
|
|
29
33
|
// previous `length < 1000` guard omitted the Body line for the longest
|
|
30
34
|
// questions (the ones most in need of truncation).
|
|
31
35
|
const body = q.body.length > 500 ? `${q.body.substring(0, 500)}...` : q.body;
|
|
32
|
-
|
|
36
|
+
// Third-party body text must never be rendered as markdown — it can
|
|
37
|
+
// carry arbitrary links (markdown injection → clickable browser tabs).
|
|
38
|
+
// The fence MUST start on its own line: CommonMark only recognizes a
|
|
39
|
+
// fenced code block when the opening fence begins the line; inline
|
|
40
|
+
// after "**Body:**" it degrades to paragraph text and marked renders
|
|
41
|
+
// embedded links clickable (verified against pi's bundled marked).
|
|
42
|
+
output += `- **Body:**\n\n${fenceBlock(body)}\n`;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
output += '\n---\n\n';
|
|
@@ -62,7 +72,8 @@ export function formatAnswersTable(answers: Answer[]): string {
|
|
|
62
72
|
// answers (same class as the question-body fix above; answers get a more
|
|
63
73
|
// generous bound since the body is the whole point of an answer).
|
|
64
74
|
const body = a.body.length > 2000 ? `${a.body.substring(0, 2000)}...` : a.body;
|
|
65
|
-
|
|
75
|
+
// Same third-party-content rule as the question body above.
|
|
76
|
+
output += `\n### Answer Body\n\n${fenceBlock(body)}\n`;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
output += '\n---\n\n';
|
|
@@ -89,9 +100,9 @@ export function formatUsersTable(users: User[]): string {
|
|
|
89
100
|
output += `- **Location:** ${u.location}\n`;
|
|
90
101
|
}
|
|
91
102
|
if (u.website_url) {
|
|
92
|
-
output += `- **Website:** ${u.website_url}\n`;
|
|
103
|
+
output += `- **Website:** ${plainUrl(u.website_url)}\n`;
|
|
93
104
|
}
|
|
94
|
-
output += `- **Profile:** ${u.link}\n`;
|
|
105
|
+
output += `- **Profile:** ${plainUrl(u.link)}\n`;
|
|
95
106
|
output += '\n---\n\n';
|
|
96
107
|
}
|
|
97
108
|
|
|
@@ -125,7 +136,7 @@ export function formatCompactQuestions(questions: Question[]): string {
|
|
|
125
136
|
let index = 1;
|
|
126
137
|
for (const q of questions) {
|
|
127
138
|
const accepted = q.accepted_answer_id ? '[accepted]' : '';
|
|
128
|
-
output += `${index}.
|
|
139
|
+
output += `${index}. ${q.title} ${accepted} (score: ${q.score}, answers: ${q.answer_count}) ${plainUrl(q.link)}\n`;
|
|
129
140
|
index++;
|
|
130
141
|
}
|
|
131
142
|
return output;
|
|
@@ -70,6 +70,7 @@ export function createStackexchangeTool(options: {
|
|
|
70
70
|
'Use tags to filter by specific topics.',
|
|
71
71
|
'Use maxPages parameter to control pagination for search results (default: 5 pages).',
|
|
72
72
|
`CRITICAL: You are allowed a maximum of ${getMaxGatheringCalls(options.config)} gathering calls across your web tools (search, security_search, stackexchange). Use them for breadth.`,
|
|
73
|
+
'URLs in results are wrapped in backticks on purpose: they are data, not clickable links. Never reformat them as markdown links — a click in the terminal opens a real browser tab.',
|
|
73
74
|
],
|
|
74
75
|
parameters: StackExchangeParamsSchema,
|
|
75
76
|
executionMode: 'parallel',
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutralize external URLs in tool output so terminal-UI markdown renderers
|
|
3
|
+
* cannot turn them into clickable hyperlinks.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: pi's TUI (and most markdown-rendering terminals) convert
|
|
6
|
+
* markdown links AND bare URLs (GFM autolink) into OSC 8 terminal hyperlinks,
|
|
7
|
+
* and a mouse press+release on such a span opens the URL in the user's real
|
|
8
|
+
* browser. Tool results are agent-facing data, not interactive UI — a URL
|
|
9
|
+
* appearing there should never be one accidental click away from opening a
|
|
10
|
+
* tab, and third-party content (question bodies, API fields) must not be able
|
|
11
|
+
* to inject clickable links into the user's terminal at all.
|
|
12
|
+
*
|
|
13
|
+
* Mechanism: wrapping a URL in backticks makes it a markdown code span. Code
|
|
14
|
+
* spans are tokenized before autolink in every CommonMark/GFM tokenizer
|
|
15
|
+
* (verified against the `marked` version pi ships), so the URL renders as
|
|
16
|
+
* literal text — copyable by humans, trivially extractable by the model, and
|
|
17
|
+
* impossible to click.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Longest run of backticks that can safely fence arbitrary content. */
|
|
21
|
+
const MIN_FENCE = 3;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Return `url` as a markdown code span: renders as plain text everywhere,
|
|
25
|
+
* never becomes a clickable hyperlink, and stays byte-exact for the model.
|
|
26
|
+
*/
|
|
27
|
+
export function plainUrl(url: string): string {
|
|
28
|
+
const safe = url.replace(/`/g, '%60');
|
|
29
|
+
return `\`${safe}\``;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wrap arbitrary third-party text (question bodies, API-provided markup) in a
|
|
34
|
+
* fenced code block so no markdown inside it — links, headings, tables — is
|
|
35
|
+
* ever rendered or autolinked. The fence length grows past any backtick run
|
|
36
|
+
* found in the content, so content containing ``` fences cannot close it
|
|
37
|
+
* early.
|
|
38
|
+
*/
|
|
39
|
+
export function fenceBlock(content: string): string {
|
|
40
|
+
const runs = content.match(/`+/g) ?? [];
|
|
41
|
+
const longest = runs.reduce((max, run) => Math.max(max, run.length), 0);
|
|
42
|
+
const fence = '`'.repeat(Math.max(MIN_FENCE, longest + 1));
|
|
43
|
+
return `${fence}text\n${content}\n${fence}`;
|
|
44
|
+
}
|