@demigodmode/pi-web-agent 0.3.1 → 0.4.0

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.
@@ -1,87 +1,199 @@
1
- function sourceRank(sourceKind) {
2
- switch (sourceKind) {
3
- case 'official-docs':
4
- return 0;
5
- case 'official-api':
6
- return 1;
7
- case 'official-discussion':
8
- return 2;
9
- case 'issue-thread':
10
- return 3;
11
- case 'community':
12
- return 4;
13
- case 'package-page':
14
- return 5;
15
- default:
16
- return 6;
17
- }
1
+ import { rankEvidence } from './evidence-ranker.js';
2
+ import { planSearchQueries } from './query-planner.js';
3
+ import { decideNextResearchStep } from './stop-decider.js';
4
+ const DEFAULT_MAX_PASSES = 3;
5
+ const DEFAULT_MAX_FETCHES_PER_PASS = 4;
6
+ const DEFAULT_MAX_HEADLESS_ATTEMPTS = 2;
7
+ function classifyEvidenceUrl(url) {
8
+ if (url.includes('/docs/api/') || url.includes('/config/'))
9
+ return 'official-api';
10
+ if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide/'))
11
+ return 'official-docs';
12
+ if (url.includes('github.com/vitest-dev/vitest') && url.includes('/docs/'))
13
+ return 'official-docs';
14
+ if (url.includes('learn.microsoft.com'))
15
+ return 'official-docs';
16
+ if (url.includes('github.com/') && url.includes('/issues/'))
17
+ return 'issue-thread';
18
+ if (url.includes('npmjs.com/package/'))
19
+ return 'package-page';
20
+ return 'community';
18
21
  }
19
- function sortEvidence(evidence) {
20
- return [...evidence].sort((left, right) => sourceRank(left.sourceKind) - sourceRank(right.sourceKind));
22
+ function summarizeText(text, maxLength = 180) {
23
+ return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
21
24
  }
22
- function strongEvidence(evidence) {
23
- return evidence.filter((item) => item.sourceKind === 'official-docs' ||
24
- item.sourceKind === 'official-api' ||
25
- item.sourceKind === 'official-discussion');
25
+ function isBotCheckContent({ title = '', text }) {
26
+ return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(`${title}\n${text}`);
26
27
  }
27
- function hasOfficialDocsOrApi(evidence) {
28
- return evidence.some((item) => item.sourceKind === 'official-docs' || item.sourceKind === 'official-api');
28
+ function evidenceFromHeadless(result) {
29
+ if (result.status !== 'ok' || !result.content?.text.trim())
30
+ return null;
31
+ if (isBotCheckContent({ title: result.content.title, text: result.content.text }))
32
+ return null;
33
+ return {
34
+ title: result.content.title ?? result.url,
35
+ url: result.url,
36
+ sourceKind: classifyEvidenceUrl(result.url),
37
+ method: 'headless',
38
+ summary: summarizeText(result.content.text),
39
+ supports: [summarizeText(result.content.text, 120)]
40
+ };
29
41
  }
30
- function hasBotCheck(outcomes) {
31
- return outcomes.some((outcome) => outcome.kind === 'bot-check');
42
+ function fallbackWorkerPass({ previousQueries, allGaps, allLowValueOutcomes, exhaustedBudget }) {
43
+ return {
44
+ searchQueries: previousQueries,
45
+ evidence: [],
46
+ gaps: allGaps,
47
+ lowValueOutcomes: allLowValueOutcomes,
48
+ exhaustedBudget
49
+ };
32
50
  }
33
- function isHeadlessWorthTrying(pass, approvedEvidence) {
34
- if (!pass.suggestedHeadlessUrl)
35
- return false;
36
- if (hasBotCheck(pass.lowValueOutcomes))
37
- return false;
38
- if (approvedEvidence.length >= 2 && hasOfficialDocsOrApi(approvedEvidence))
39
- return false;
40
- const candidate = pass.suggestedHeadlessUrl;
41
- return !candidate.includes('npmjs.com/package/');
51
+ function buildMetadata({ previousQueries, allEvidence, allGaps, allLowValueOutcomes, headlessAttempts, exhaustedBudget }) {
52
+ return {
53
+ searchPasses: previousQueries.length,
54
+ fetchedPages: allEvidence.length + allGaps.length + allLowValueOutcomes.length,
55
+ headlessAttempts,
56
+ exhaustedBudget
57
+ };
58
+ }
59
+ function decisionForAnswer(action, query, ranked) {
60
+ if (action === 'answer') {
61
+ return {
62
+ action: 'answer',
63
+ rationale: 'Adaptive research gathered enough strong evidence.',
64
+ approvedEvidence: ranked
65
+ };
66
+ }
67
+ return {
68
+ action: 'research-again',
69
+ rationale: 'Research budget exhausted; answer with caveat.',
70
+ followupQuery: query
71
+ };
42
72
  }
43
73
  export function createResearchOrchestrator({ worker, headlessFetch }) {
44
74
  return {
45
75
  async run({ query }) {
46
- const pass = await worker.run({ query, maxSearchRounds: 1, maxFetches: 3 });
47
- const approvedEvidence = sortEvidence(pass.evidence.filter((item) => item.sourceKind !== 'package-page'));
48
- const strong = strongEvidence(approvedEvidence);
49
- const enoughEvidence = strong.length >= 2 && hasOfficialDocsOrApi(approvedEvidence);
50
- if (enoughEvidence) {
51
- const decision = {
52
- action: 'answer',
53
- rationale: 'Two strong sources with official support are enough to answer safely.',
54
- approvedEvidence
55
- };
56
- return { decision, evidence: approvedEvidence, workerPass: pass };
57
- }
58
- if (isHeadlessWorthTrying(pass, approvedEvidence)) {
59
- const url = pass.suggestedHeadlessUrl;
60
- await headlessFetch({ url });
61
- const decision = {
62
- action: 'escalate-headless',
63
- rationale: 'One high-value page is worth a single orchestrator-approved headless retry.',
64
- url,
65
- approvedEvidence
66
- };
67
- return { decision, evidence: approvedEvidence, workerPass: pass };
68
- }
69
- const hasConcreteGap = pass.gaps.length > 0;
70
- const onlyLowValueOutcomes = pass.lowValueOutcomes.length > 0 && pass.evidence.length === 0;
71
- if (!hasConcreteGap || onlyLowValueOutcomes) {
72
- const decision = {
73
- action: 'research-again',
74
- rationale: 'Current results did not justify more escalation; continue only with a more targeted pass.',
75
- followupQuery: query
76
- };
77
- return { decision, evidence: approvedEvidence, workerPass: pass };
76
+ const allEvidence = [];
77
+ const allGaps = [];
78
+ const allLowValueOutcomes = [];
79
+ const previousQueries = [];
80
+ const suggestedHeadlessUrls = [];
81
+ let headlessAttempts = 0;
82
+ let lastPass;
83
+ for (let passIndex = 0; passIndex < DEFAULT_MAX_PASSES; passIndex++) {
84
+ const queries = planSearchQueries({
85
+ originalQuery: query,
86
+ passIndex,
87
+ previousQueries,
88
+ gaps: allGaps.map((gap) => gap.message)
89
+ });
90
+ for (const plannedQuery of queries) {
91
+ previousQueries.push(plannedQuery);
92
+ const pass = await worker.run({
93
+ query: plannedQuery,
94
+ maxSearchRounds: 1,
95
+ maxFetches: DEFAULT_MAX_FETCHES_PER_PASS
96
+ });
97
+ lastPass = pass;
98
+ allEvidence.push(...pass.evidence);
99
+ allGaps.push(...pass.gaps);
100
+ allLowValueOutcomes.push(...pass.lowValueOutcomes);
101
+ if (pass.suggestedHeadlessUrl)
102
+ suggestedHeadlessUrls.push(pass.suggestedHeadlessUrl);
103
+ const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
104
+ const decision = decideNextResearchStep({
105
+ evidence: ranked,
106
+ suggestedHeadlessUrls,
107
+ passIndex,
108
+ maxPasses: DEFAULT_MAX_PASSES,
109
+ headlessAttempts,
110
+ maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS
111
+ });
112
+ if (decision.action === 'headless') {
113
+ headlessAttempts++;
114
+ const headlessResult = await headlessFetch({ url: decision.url });
115
+ const headlessEvidence = evidenceFromHeadless(headlessResult);
116
+ if (headlessEvidence) {
117
+ allEvidence.push(headlessEvidence);
118
+ const updatedRanked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
119
+ const updatedDecision = decideNextResearchStep({
120
+ evidence: updatedRanked,
121
+ suggestedHeadlessUrls: [],
122
+ passIndex,
123
+ maxPasses: DEFAULT_MAX_PASSES,
124
+ headlessAttempts,
125
+ maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS
126
+ });
127
+ return {
128
+ decision: decisionForAnswer(updatedDecision.action === 'answer' ? 'answer' : 'answer-with-caveat', query, updatedRanked),
129
+ evidence: updatedRanked,
130
+ workerPass: lastPass,
131
+ metadata: buildMetadata({
132
+ previousQueries,
133
+ allEvidence,
134
+ allGaps,
135
+ allLowValueOutcomes,
136
+ headlessAttempts,
137
+ exhaustedBudget: updatedDecision.action !== 'answer'
138
+ })
139
+ };
140
+ }
141
+ return {
142
+ decision: {
143
+ action: 'escalate-headless',
144
+ rationale: 'One high-value page is worth a single orchestrator-approved headless retry.',
145
+ url: decision.url,
146
+ approvedEvidence: ranked
147
+ },
148
+ evidence: ranked,
149
+ workerPass: lastPass,
150
+ metadata: buildMetadata({
151
+ previousQueries,
152
+ allEvidence,
153
+ allGaps,
154
+ allLowValueOutcomes,
155
+ headlessAttempts,
156
+ exhaustedBudget: false
157
+ })
158
+ };
159
+ }
160
+ if (decision.action === 'answer' || decision.action === 'answer-with-caveat') {
161
+ return {
162
+ decision: decisionForAnswer(decision.action, query, ranked),
163
+ evidence: ranked,
164
+ workerPass: lastPass,
165
+ metadata: buildMetadata({
166
+ previousQueries,
167
+ allEvidence,
168
+ allGaps,
169
+ allLowValueOutcomes,
170
+ headlessAttempts,
171
+ exhaustedBudget: decision.action === 'answer-with-caveat'
172
+ })
173
+ };
174
+ }
175
+ }
78
176
  }
79
- const decision = {
80
- action: 'research-again',
81
- rationale: 'The first pass did not gather enough strong evidence to answer safely.',
82
- followupQuery: query
177
+ const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
178
+ return {
179
+ decision: decisionForAnswer('answer-with-caveat', query, ranked),
180
+ evidence: ranked,
181
+ workerPass: lastPass ??
182
+ fallbackWorkerPass({
183
+ previousQueries,
184
+ allGaps,
185
+ allLowValueOutcomes,
186
+ exhaustedBudget: true
187
+ }),
188
+ metadata: buildMetadata({
189
+ previousQueries,
190
+ allEvidence,
191
+ allGaps,
192
+ allLowValueOutcomes,
193
+ headlessAttempts,
194
+ exhaustedBudget: true
195
+ })
83
196
  };
84
- return { decision, evidence: approvedEvidence, workerPass: pass };
85
197
  }
86
198
  };
87
199
  }
@@ -25,6 +25,12 @@ export type ResearchWorkerResult = {
25
25
  suggestedHeadlessUrl?: string;
26
26
  exhaustedBudget: boolean;
27
27
  };
28
+ export type ResearchRunMetadata = {
29
+ searchPasses: number;
30
+ fetchedPages: number;
31
+ headlessAttempts: number;
32
+ exhaustedBudget: boolean;
33
+ };
28
34
  export type ResearchOrchestratorDecision = {
29
35
  action: 'answer';
30
36
  rationale: string;
@@ -1,8 +1,11 @@
1
+ import { selectCandidates } from './candidate-selector.js';
1
2
  function classifySource(url) {
2
3
  if (url.includes('/docs/api/') || url.includes('/config/'))
3
4
  return 'official-api';
4
5
  if (url.includes('playwright.dev/docs') || url.includes('vitest.dev/guide/'))
5
6
  return 'official-docs';
7
+ if (url.includes('github.com/vitest-dev/vitest') && url.includes('/docs/'))
8
+ return 'official-docs';
6
9
  if (url.includes('learn.microsoft.com'))
7
10
  return 'official-docs';
8
11
  if (url.includes('github.com/') && url.includes('/issues/'))
@@ -91,7 +94,11 @@ export function createResearchWorker({ search, fetchPage }) {
91
94
  exhaustedBudget: false
92
95
  };
93
96
  }
94
- const candidates = searchResult.results.slice(0, maxFetches);
97
+ const candidates = selectCandidates({
98
+ results: searchResult.results,
99
+ seenUrls: new Set(evidence.map((item) => item.url)),
100
+ maxCandidates: maxFetches
101
+ });
95
102
  for (const candidate of candidates) {
96
103
  const fetched = await fetchPage({ url: candidate.url });
97
104
  if (fetched.status === 'ok') {
@@ -0,0 +1,19 @@
1
+ import type { ResearchEvidence } from './research-types.js';
2
+ export type ResearchStepDecision = {
3
+ action: 'answer';
4
+ } | {
5
+ action: 'answer-with-caveat';
6
+ } | {
7
+ action: 'search-again';
8
+ } | {
9
+ action: 'headless';
10
+ url: string;
11
+ };
12
+ export declare function decideNextResearchStep({ evidence, suggestedHeadlessUrls, passIndex, maxPasses, headlessAttempts, maxHeadlessAttempts }: {
13
+ evidence: ResearchEvidence[];
14
+ suggestedHeadlessUrls: string[];
15
+ passIndex: number;
16
+ maxPasses: number;
17
+ headlessAttempts: number;
18
+ maxHeadlessAttempts: number;
19
+ }): ResearchStepDecision;
@@ -0,0 +1,14 @@
1
+ import { hasOfficialEvidence, strongEvidenceCount } from './evidence-ranker.js';
2
+ export function decideNextResearchStep({ evidence, suggestedHeadlessUrls, passIndex, maxPasses, headlessAttempts, maxHeadlessAttempts }) {
3
+ if (strongEvidenceCount(evidence) >= 2 && hasOfficialEvidence(evidence)) {
4
+ return { action: 'answer' };
5
+ }
6
+ const headlessUrl = suggestedHeadlessUrls.find((url) => !url.includes('npmjs.com/package/'));
7
+ if (headlessUrl && headlessAttempts < maxHeadlessAttempts) {
8
+ return { action: 'headless', url: headlessUrl };
9
+ }
10
+ if (passIndex + 1 < maxPasses) {
11
+ return { action: 'search-again' };
12
+ }
13
+ return { action: 'answer-with-caveat' };
14
+ }
@@ -1,3 +1,10 @@
1
+ function internalReaderLabel(method) {
2
+ if (method === 'headless')
3
+ return 'web_fetch_headless';
4
+ if (method === 'http')
5
+ return 'web_fetch';
6
+ return 'web_explore';
7
+ }
1
8
  export function buildExplorePresentation(result) {
2
9
  if (result.status === 'error') {
3
10
  return {
@@ -7,13 +14,26 @@ export function buildExplorePresentation(result) {
7
14
  }
8
15
  };
9
16
  }
10
- const preview = result.findings.map((finding) => `- ${finding}`).join('\n');
17
+ const internalSummary = result.metadata
18
+ ? `Internal research: web_search ×${result.metadata.searchPasses}, web_fetch ×${result.metadata.fetchedPages}, web_fetch_headless ×${result.metadata.headlessAttempts}`
19
+ : undefined;
20
+ const hasEvidence = result.findings.length > 0 || result.sources.length > 0;
21
+ const evidenceLines = hasEvidence
22
+ ? result.findings.map((finding, index) => `- [${internalReaderLabel(result.sources[index]?.method)}] ${finding}`)
23
+ : ['No usable evidence found.'];
24
+ const preview = [
25
+ ...evidenceLines,
26
+ internalSummary ? `\n${internalSummary}` : undefined
27
+ ]
28
+ .filter((line) => line !== undefined)
29
+ .join('\n');
11
30
  const verbose = [
12
31
  'Findings',
13
- ...result.findings.map((finding) => `- ${finding}`),
32
+ ...evidenceLines,
14
33
  '',
15
34
  'Sources',
16
- ...result.sources.map((source) => `- ${source.title}: ${source.url}`),
35
+ ...result.sources.map((source) => `- [${internalReaderLabel(source.method)}] ${source.title}: ${source.url}`),
36
+ internalSummary ? `\nInternal tools\n${internalSummary}` : undefined,
17
37
  result.caveat ? `\nCaveat\n${result.caveat}` : undefined
18
38
  ]
19
39
  .filter((line) => line !== undefined)
@@ -21,7 +41,9 @@ export function buildExplorePresentation(result) {
21
41
  return {
22
42
  mode: 'compact',
23
43
  views: {
24
- compact: `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`,
44
+ compact: hasEvidence
45
+ ? `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`
46
+ : 'No usable evidence found',
25
47
  preview,
26
48
  verbose
27
49
  },
@@ -1,4 +1,5 @@
1
1
  import type { ResearchEvidence } from '../orchestration/research-types.js';
2
+ import type { WebExploreResponse } from '../types.js';
2
3
  export declare function createWebExploreTool({ explore }?: {
3
4
  explore?: {
4
5
  run: (input: {
@@ -9,6 +10,7 @@ export declare function createWebExploreTool({ explore }?: {
9
10
  };
10
11
  evidence: ResearchEvidence[];
11
12
  workerPass: unknown;
13
+ metadata?: WebExploreResponse['metadata'];
12
14
  }>;
13
15
  } | ((input: {
14
16
  query: string;
@@ -18,6 +20,7 @@ export declare function createWebExploreTool({ explore }?: {
18
20
  };
19
21
  evidence: ResearchEvidence[];
20
22
  workerPass: unknown;
23
+ metadata?: WebExploreResponse['metadata'];
21
24
  }>);
22
25
  }): ({ query }: {
23
26
  query: string;
@@ -28,7 +31,14 @@ export declare function createWebExploreTool({ explore }?: {
28
31
  sources: Array<{
29
32
  title: string;
30
33
  url: string;
34
+ method?: "http" | "headless";
31
35
  }>;
32
36
  caveat?: string;
37
+ metadata?: {
38
+ searchPasses: number;
39
+ fetchedPages: number;
40
+ headlessAttempts: number;
41
+ exhaustedBudget: boolean;
42
+ };
33
43
  error?: import("../types.js").ToolError;
34
44
  }>;
@@ -1,18 +1,6 @@
1
1
  import { createResearchWorkflow } from '../orchestration/index.js';
2
+ import { synthesizeAnswer } from '../orchestration/answer-synthesizer.js';
2
3
  import { buildExplorePresentation } from '../presentation/explore-presentation.js';
3
- function findingFromEvidence(evidence, index) {
4
- if (evidence.summary.includes('Use channel')) {
5
- return 'Use channel for branded Chrome or Edge when possible.';
6
- }
7
- if (evidence.summary.includes('use at your own risk') || evidence.summary.includes('risky')) {
8
- return 'Treat executablePath as a fallback because Playwright documents it as use-at-your-own-risk.';
9
- }
10
- if (evidence.summary.includes('coverage.provider to v8') ||
11
- evidence.summary.includes('@vitest/coverage-v8')) {
12
- return 'Vitest coverage docs say to set coverage.provider to v8 and install @vitest/coverage-v8.';
13
- }
14
- return evidence.summary || `Finding ${index + 1}`;
15
- }
16
4
  export function createWebExploreTool({ explore = createResearchWorkflow() } = {}) {
17
5
  const runExplore = typeof explore === 'function' ? explore : explore.run.bind(explore);
18
6
  return async function webExplore({ query }) {
@@ -30,19 +18,21 @@ export function createWebExploreTool({ explore = createResearchWorkflow() } = {}
30
18
  };
31
19
  }
32
20
  const result = await runExplore({ query: normalizedQuery });
33
- const findings = result.evidence.slice(0, 5).map(findingFromEvidence);
34
21
  const sources = result.evidence.slice(0, 4).map((item) => ({
35
22
  title: item.title,
36
- url: item.url
23
+ url: item.url,
24
+ method: item.method
37
25
  }));
38
- const caveat = result.decision.action === 'answer'
39
- ? undefined
40
- : 'Evidence is partial, so this answer is based on the strongest source found so far.';
26
+ const synthesized = synthesizeAnswer({
27
+ evidence: result.evidence,
28
+ partial: result.decision.action !== 'answer'
29
+ });
41
30
  const shaped = {
42
31
  status: 'ok',
43
- findings,
32
+ findings: synthesized.findings,
44
33
  sources,
45
- caveat
34
+ caveat: synthesized.caveat,
35
+ metadata: result.metadata
46
36
  };
47
37
  return {
48
38
  ...shaped,
package/dist/types.d.ts CHANGED
@@ -56,8 +56,15 @@ export type WebExploreResponse = {
56
56
  sources: Array<{
57
57
  title: string;
58
58
  url: string;
59
+ method?: 'http' | 'headless';
59
60
  }>;
60
61
  caveat?: string;
62
+ metadata?: {
63
+ searchPasses: number;
64
+ fetchedPages: number;
65
+ headlessAttempts: number;
66
+ exhaustedBudget: boolean;
67
+ };
61
68
  presentation?: PresentationEnvelope;
62
69
  error?: ToolError;
63
70
  };
package/package.json CHANGED
@@ -1,75 +1,75 @@
1
- {
2
- "name": "@demigodmode/pi-web-agent",
3
- "version": "0.3.1",
4
- "description": "Pi package for reliable web access with explicit search, fetch, and headless boundaries.",
5
- "type": "module",
6
- "main": "./dist/extension.js",
7
- "types": "./dist/extension.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/extension.d.ts",
11
- "import": "./dist/extension.js"
12
- }
13
- },
14
- "files": [
15
- "dist",
16
- "README.md"
17
- ],
18
- "keywords": [
19
- "pi-package",
20
- "pi",
21
- "extension",
22
- "web-search",
23
- "web-fetch"
24
- ],
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/demigodmode/pi-web-agent.git"
28
- },
29
- "homepage": "https://github.com/demigodmode/pi-web-agent#readme",
30
- "bugs": {
31
- "url": "https://github.com/demigodmode/pi-web-agent/issues"
32
- },
33
- "license": "AGPL-3.0-only",
34
- "publishConfig": {
35
- "access": "public"
36
- },
37
- "scripts": {
38
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
39
- "build:dev": "tsc -p tsconfig.json",
40
- "test": "vitest run --coverage",
41
- "test:watch": "vitest",
42
- "lint": "tsc -p tsconfig.json --noEmit",
43
- "docs:dev": "vitepress dev docs",
44
- "docs:build": "vitepress build docs",
45
- "docs:preview": "vitepress preview docs",
46
- "eval:live": "npm run build:dev && node dist/scripts/live-web-eval.js",
47
- "release:dry-run": "node scripts/release.mjs --dry-run",
48
- "release": "node scripts/release.mjs"
49
- },
50
- "pi": {
51
- "extensions": [
52
- "./dist/extension.js"
53
- ]
54
- },
55
- "dependencies": {
56
- "@mozilla/readability": "^0.6.0",
57
- "cheerio": "^1.1.0",
58
- "jsdom": "^26.0.0",
59
- "playwright-core": "^1.54.0"
60
- },
61
- "devDependencies": {
62
- "@mariozechner/pi-coding-agent": "^0.67.2",
63
- "@sinclair/typebox": "^0.34.41",
64
- "@types/jsdom": "^21.1.7",
65
- "@types/node": "^24.0.0",
66
- "@vitest/coverage-v8": "^3.2.4",
67
- "typescript": "^5.8.0",
68
- "vitepress": "^1.6.4",
69
- "vitest": "^3.2.0"
70
- },
71
- "peerDependencies": {
72
- "@mariozechner/pi-coding-agent": "*",
73
- "@sinclair/typebox": "*"
74
- }
75
- }
1
+ {
2
+ "name": "@demigodmode/pi-web-agent",
3
+ "version": "0.4.0",
4
+ "description": "Pi package for reliable web access with explicit search, fetch, and headless boundaries.",
5
+ "type": "module",
6
+ "main": "./dist/extension.js",
7
+ "types": "./dist/extension.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/extension.d.ts",
11
+ "import": "./dist/extension.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "keywords": [
19
+ "pi-package",
20
+ "pi",
21
+ "extension",
22
+ "web-search",
23
+ "web-fetch"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/demigodmode/pi-web-agent.git"
28
+ },
29
+ "homepage": "https://github.com/demigodmode/pi-web-agent#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/demigodmode/pi-web-agent/issues"
32
+ },
33
+ "license": "AGPL-3.0-only",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
39
+ "build:dev": "tsc -p tsconfig.json",
40
+ "test": "vitest run --coverage",
41
+ "test:watch": "vitest",
42
+ "lint": "tsc -p tsconfig.json --noEmit",
43
+ "docs:dev": "vitepress dev docs",
44
+ "docs:build": "vitepress build docs",
45
+ "docs:preview": "vitepress preview docs",
46
+ "eval:live": "npm run build:dev && node dist/scripts/live-web-eval.js",
47
+ "release:dry-run": "node scripts/release.mjs --dry-run",
48
+ "release": "node scripts/release.mjs"
49
+ },
50
+ "pi": {
51
+ "extensions": [
52
+ "./dist/extension.js"
53
+ ]
54
+ },
55
+ "dependencies": {
56
+ "@mozilla/readability": "^0.6.0",
57
+ "cheerio": "^1.1.0",
58
+ "jsdom": "^26.0.0",
59
+ "playwright-core": "^1.54.0"
60
+ },
61
+ "devDependencies": {
62
+ "@mariozechner/pi-coding-agent": "^0.67.2",
63
+ "@sinclair/typebox": "^0.34.41",
64
+ "@types/jsdom": "^21.1.7",
65
+ "@types/node": "^24.0.0",
66
+ "@vitest/coverage-v8": "^3.2.4",
67
+ "typescript": "^5.8.0",
68
+ "vitepress": "^1.6.4",
69
+ "vitest": "^3.2.0"
70
+ },
71
+ "peerDependencies": {
72
+ "@mariozechner/pi-coding-agent": "*",
73
+ "@sinclair/typebox": "*"
74
+ }
75
+ }