@lincoln504/pi-research 1.6.11 → 1.6.12
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 +3 -0
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli.mjs +26 -8
- package/docs/CHANGELOG.md +8 -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
|
---
|
|
@@ -86,6 +88,7 @@ Three depth levels (normal, deep, ultra) set the team size and number of rounds.
|
|
|
86
88
|
|
|
87
89
|
- Node.js >= 22.19.0
|
|
88
90
|
- An LLM with a 100k+ context window (an API key or a local model)
|
|
91
|
+
- On OpenRouter, `inclusionai/ling-3.0-flash` is a proven low-cost workhorse for driving runs: $0.021 input / $0.063 output per Mtok with a 262k context window — the cheapest capable model on the platform at time of writing — with reliable strict-JSON planning output and fast synthesis (verified over extended production use). The `:free` variants (`ling-3.0-flash-fin:free`) work for light usage but are subject to free-tier rate limits. Note: `ling-3.0-tiny` currently has no serving endpoints on OpenRouter and cannot be called.
|
|
89
92
|
- Internet access on a residential IP, since search, scraping, and YouTube transcripts get bot-blocked from datacenter/VPS/cloud IPs
|
|
90
93
|
- pi. The pi extension uses the host's copy while the standalone CLI and agent skill install it as a dependency.
|
|
91
94
|
- Local knowledge-store embeddings need `@huggingface/transformers`, which is an **optional dependency**: if its native image chain (sharp) cannot install on your machine, npm skips it with a warning, the install still succeeds, and everything except local embeddings works. The store disables embeddings gracefully with a message telling you how to restore them.
|
|
@@ -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.12", "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,14 @@ 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
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
8
16
|
## [1.6.11] - 2026-09-04
|
|
9
17
|
|
|
10
18
|
### 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
|
+
}
|