gsd-pi 2.22.0 → 2.24.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.
- package/README.md +25 -1
- package/dist/cli.js +74 -7
- package/dist/headless.d.ts +25 -0
- package/dist/headless.js +454 -0
- package/dist/help-text.js +47 -0
- package/dist/mcp-server.d.ts +20 -3
- package/dist/mcp-server.js +21 -1
- package/dist/models-resolver.d.ts +32 -0
- package/dist/models-resolver.js +50 -0
- package/dist/resource-loader.js +64 -9
- package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/dist/resources/extensions/bg-shell/types.ts +33 -1
- package/dist/resources/extensions/browser-tools/capture.ts +18 -16
- package/dist/resources/extensions/browser-tools/index.ts +20 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/dist/resources/extensions/gsd/auto.ts +560 -52
- package/dist/resources/extensions/gsd/captures.ts +49 -0
- package/dist/resources/extensions/gsd/commands.ts +194 -11
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/dist/resources/extensions/gsd/diff-context.ts +73 -80
- package/dist/resources/extensions/gsd/doctor.ts +76 -12
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/forensics.ts +95 -52
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
- package/dist/resources/extensions/gsd/index.ts +34 -1
- package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +16 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +21 -8
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/scripts/postinstall.js +7 -109
- package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/src/resources/extensions/bg-shell/types.ts +33 -1
- package/src/resources/extensions/browser-tools/capture.ts +18 -16
- package/src/resources/extensions/browser-tools/index.ts +20 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/src/resources/extensions/gsd/auto.ts +560 -52
- package/src/resources/extensions/gsd/captures.ts +49 -0
- package/src/resources/extensions/gsd/commands.ts +194 -11
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/src/resources/extensions/gsd/diff-context.ts +73 -80
- package/src/resources/extensions/gsd/doctor.ts +76 -12
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/forensics.ts +95 -52
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +85 -5
- package/src/resources/extensions/gsd/index.ts +34 -1
- package/src/resources/extensions/gsd/mcp-server.ts +33 -12
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/src/resources/extensions/gsd/session-forensics.ts +36 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/src/resources/extensions/gsd/workspace-index.ts +34 -6
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { ToolDeps } from "../state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Structured data extraction with JSON Schema validation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function registerExtractTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
10
|
+
pi.registerTool({
|
|
11
|
+
name: "browser_extract",
|
|
12
|
+
label: "Browser Extract",
|
|
13
|
+
description:
|
|
14
|
+
"Extract structured data from the current page using CSS selectors and validate against a JSON Schema. " +
|
|
15
|
+
"Provide a schema describing the shape of data you want. The tool extracts data by evaluating " +
|
|
16
|
+
"CSS selectors in the page context, then validates the result against your schema. " +
|
|
17
|
+
"Supports extracting single objects or arrays of items. Waits for network idle before extraction.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
schema: Type.Record(Type.String(), Type.Unknown(), {
|
|
20
|
+
description:
|
|
21
|
+
"JSON Schema describing the data shape to extract. Properties should include " +
|
|
22
|
+
"'_selector' (CSS selector) and '_attribute' (attribute to read, default: 'textContent') hints. " +
|
|
23
|
+
"Example: { type: 'object', properties: { title: { _selector: 'h1', _attribute: 'textContent' }, price: { _selector: '.price', _attribute: 'textContent' } } }",
|
|
24
|
+
}),
|
|
25
|
+
selector: Type.Optional(
|
|
26
|
+
Type.String({ description: "CSS selector to scope extraction to a specific container element." }),
|
|
27
|
+
),
|
|
28
|
+
multiple: Type.Optional(
|
|
29
|
+
Type.Boolean({
|
|
30
|
+
description:
|
|
31
|
+
"If true, extract an array of items. The 'selector' parameter becomes the item container selector, " +
|
|
32
|
+
"and schema properties are extracted relative to each matched container.",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
38
|
+
try {
|
|
39
|
+
const { page: p } = await deps.ensureBrowser();
|
|
40
|
+
|
|
41
|
+
// Wait for network idle before extraction
|
|
42
|
+
await p.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => {});
|
|
43
|
+
|
|
44
|
+
const schema = params.schema as any;
|
|
45
|
+
const scopeSelector = params.selector;
|
|
46
|
+
const multiple = params.multiple ?? false;
|
|
47
|
+
|
|
48
|
+
// Build extraction plan from schema
|
|
49
|
+
const extractionPlan = buildExtractionPlan(schema);
|
|
50
|
+
|
|
51
|
+
// Execute extraction in page context
|
|
52
|
+
const rawData = await p.evaluate(
|
|
53
|
+
({ plan, scope, multi }: { plan: ExtractionField[]; scope: string | undefined; multi: boolean }) => {
|
|
54
|
+
function extractFromContainer(container: Element, fields: typeof plan): Record<string, unknown> {
|
|
55
|
+
const result: Record<string, unknown> = {};
|
|
56
|
+
for (const field of fields) {
|
|
57
|
+
const el = container.querySelector(field.selector);
|
|
58
|
+
if (!el) {
|
|
59
|
+
result[field.name] = null;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let value: unknown;
|
|
63
|
+
switch (field.attribute) {
|
|
64
|
+
case "textContent":
|
|
65
|
+
value = (el.textContent ?? "").trim();
|
|
66
|
+
break;
|
|
67
|
+
case "innerText":
|
|
68
|
+
value = ((el as HTMLElement).innerText ?? "").trim();
|
|
69
|
+
break;
|
|
70
|
+
case "innerHTML":
|
|
71
|
+
value = el.innerHTML;
|
|
72
|
+
break;
|
|
73
|
+
case "href":
|
|
74
|
+
value = (el as HTMLAnchorElement).href ?? el.getAttribute("href");
|
|
75
|
+
break;
|
|
76
|
+
case "src":
|
|
77
|
+
value = (el as HTMLImageElement).src ?? el.getAttribute("src");
|
|
78
|
+
break;
|
|
79
|
+
case "value":
|
|
80
|
+
value = (el as HTMLInputElement).value;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
value = el.getAttribute(field.attribute) ?? (el.textContent ?? "").trim();
|
|
84
|
+
}
|
|
85
|
+
// Type coercion
|
|
86
|
+
if (field.type === "number" && typeof value === "string") {
|
|
87
|
+
const num = parseFloat(value.replace(/[^0-9.-]/g, ""));
|
|
88
|
+
value = isNaN(num) ? value : num;
|
|
89
|
+
} else if (field.type === "boolean" && typeof value === "string") {
|
|
90
|
+
value = value.toLowerCase() === "true" || value === "1";
|
|
91
|
+
}
|
|
92
|
+
result[field.name] = value;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const root = scope ? document.querySelector(scope) : document.body;
|
|
98
|
+
if (!root) return { data: null, error: `Scope selector "${scope}" not found` };
|
|
99
|
+
|
|
100
|
+
if (multi) {
|
|
101
|
+
// For multiple items, scope is the item selector
|
|
102
|
+
const containers = scope
|
|
103
|
+
? document.querySelectorAll(scope)
|
|
104
|
+
: [document.body];
|
|
105
|
+
const items = Array.from(containers).map((container) =>
|
|
106
|
+
extractFromContainer(container, plan),
|
|
107
|
+
);
|
|
108
|
+
return { data: items, error: null };
|
|
109
|
+
} else {
|
|
110
|
+
return { data: extractFromContainer(root, plan), error: null };
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{ plan: extractionPlan, scope: scopeSelector, multi: multiple },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (rawData.error) {
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: `Extraction failed: ${rawData.error}` }],
|
|
119
|
+
details: { error: rawData.error },
|
|
120
|
+
isError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate against schema using ajv
|
|
125
|
+
const validationErrors = await validateData(rawData.data, schema, multiple);
|
|
126
|
+
|
|
127
|
+
const resultText = JSON.stringify(rawData.data, null, 2);
|
|
128
|
+
const truncated = resultText.length > 4000 ? resultText.slice(0, 4000) + "\n...(truncated)" : resultText;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: validationErrors.length > 0
|
|
134
|
+
? `Extracted data (with ${validationErrors.length} validation warning(s)):\n${truncated}\n\nValidation warnings:\n${validationErrors.join("\n")}`
|
|
135
|
+
: `Extracted data:\n${truncated}`,
|
|
136
|
+
}],
|
|
137
|
+
details: {
|
|
138
|
+
data: rawData.data,
|
|
139
|
+
validationErrors: validationErrors.length > 0 ? validationErrors : undefined,
|
|
140
|
+
fieldCount: extractionPlan.length,
|
|
141
|
+
itemCount: multiple ? (rawData.data as any[])?.length ?? 0 : 1,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: "text", text: `Extraction failed: ${err.message}` }],
|
|
147
|
+
details: { error: err.message },
|
|
148
|
+
isError: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface ExtractionField {
|
|
156
|
+
name: string;
|
|
157
|
+
selector: string;
|
|
158
|
+
attribute: string;
|
|
159
|
+
type: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function buildExtractionPlan(schema: any): ExtractionField[] {
|
|
163
|
+
const fields: ExtractionField[] = [];
|
|
164
|
+
|
|
165
|
+
if (!schema || typeof schema !== "object") return fields;
|
|
166
|
+
|
|
167
|
+
const properties = schema.properties ?? schema;
|
|
168
|
+
|
|
169
|
+
for (const [name, propSchema] of Object.entries(properties)) {
|
|
170
|
+
const prop = propSchema as any;
|
|
171
|
+
if (!prop || typeof prop !== "object") continue;
|
|
172
|
+
|
|
173
|
+
// Skip meta fields
|
|
174
|
+
if (name === "type" || name === "required" || name === "properties" || name === "$schema") continue;
|
|
175
|
+
|
|
176
|
+
const selector = prop._selector ?? prop.selector ?? `[data-field="${name}"], .${name}, #${name}`;
|
|
177
|
+
const attribute = prop._attribute ?? prop.attribute ?? "textContent";
|
|
178
|
+
const type = prop.type ?? "string";
|
|
179
|
+
|
|
180
|
+
fields.push({ name, selector, attribute, type });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return fields;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function validateData(data: unknown, schema: any, isArray: boolean): Promise<string[]> {
|
|
187
|
+
const errors: string[] = [];
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const ajvModule = await import("ajv");
|
|
191
|
+
const Ajv = ajvModule.default ?? ajvModule;
|
|
192
|
+
const ajv = new (Ajv as any)({ allErrors: true, strict: false });
|
|
193
|
+
|
|
194
|
+
// Clean schema — remove our custom _selector/_attribute hints before validation
|
|
195
|
+
const cleanSchema = cleanSchemaForValidation(schema);
|
|
196
|
+
|
|
197
|
+
// Wrap in array schema if multiple
|
|
198
|
+
const validationSchema = isArray
|
|
199
|
+
? { type: "array", items: cleanSchema }
|
|
200
|
+
: cleanSchema;
|
|
201
|
+
|
|
202
|
+
const validate = ajv.compile(validationSchema);
|
|
203
|
+
const valid = validate(data);
|
|
204
|
+
|
|
205
|
+
if (!valid && validate.errors) {
|
|
206
|
+
for (const err of validate.errors) {
|
|
207
|
+
errors.push(`${err.instancePath || "/"}: ${err.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch (err: any) {
|
|
211
|
+
errors.push(`Schema validation setup failed: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return errors;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function cleanSchemaForValidation(schema: any): any {
|
|
218
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
219
|
+
if (Array.isArray(schema)) return schema.map(cleanSchemaForValidation);
|
|
220
|
+
|
|
221
|
+
const cleaned: any = {};
|
|
222
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
223
|
+
if (key.startsWith("_")) continue; // Remove our custom hints
|
|
224
|
+
if (key === "selector" && typeof value === "string") continue; // Also remove plain 'selector'
|
|
225
|
+
if (key === "attribute" && typeof value === "string") continue; // Also remove plain 'attribute'
|
|
226
|
+
cleaned[key] = cleanSchemaForValidation(value);
|
|
227
|
+
}
|
|
228
|
+
return cleaned;
|
|
229
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { ToolDeps } from "../state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prompt injection detection — scan page content for text attempting to hijack the agent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Known injection patterns — regex patterns that match common prompt injection attempts
|
|
10
|
+
const INJECTION_PATTERNS: Array<{ pattern: RegExp; category: string; severity: "high" | "medium" | "low" }> = [
|
|
11
|
+
// Direct instruction override attempts
|
|
12
|
+
{ pattern: /ignore\s+(all\s+)?previous\s+(instructions?|prompts?)/i, category: "instruction_override", severity: "high" },
|
|
13
|
+
{ pattern: /disregard\s+(all\s+)?previous\s+(instructions?|prompts?)/i, category: "instruction_override", severity: "high" },
|
|
14
|
+
{ pattern: /forget\s+(all\s+)?previous\s+(instructions?|prompts?)/i, category: "instruction_override", severity: "high" },
|
|
15
|
+
{ pattern: /override\s+(all\s+)?previous\s+(instructions?|prompts?)/i, category: "instruction_override", severity: "high" },
|
|
16
|
+
|
|
17
|
+
// System prompt extraction
|
|
18
|
+
{ pattern: /(?:what|show|reveal|display|repeat|tell)\s+(?:me\s+)?(?:your|the)\s+system\s+prompt/i, category: "prompt_extraction", severity: "high" },
|
|
19
|
+
{ pattern: /print\s+(?:your|the)\s+(?:system\s+)?(?:prompt|instructions)/i, category: "prompt_extraction", severity: "high" },
|
|
20
|
+
|
|
21
|
+
// Role reassignment
|
|
22
|
+
{ pattern: /you\s+are\s+now\s+(?:a\s+)?(?:DAN|jailbreak|unrestricted|unfiltered)/i, category: "role_reassignment", severity: "high" },
|
|
23
|
+
{ pattern: /act\s+as\s+(?:a\s+)?(?:DAN|jailbreak|unrestricted|unfiltered)/i, category: "role_reassignment", severity: "high" },
|
|
24
|
+
{ pattern: /entering\s+(?:a\s+)?(?:developer|admin|root|sudo)\s+mode/i, category: "role_reassignment", severity: "high" },
|
|
25
|
+
|
|
26
|
+
// Hidden instruction injection
|
|
27
|
+
{ pattern: /\[SYSTEM\]\s*:/i, category: "fake_system_message", severity: "high" },
|
|
28
|
+
{ pattern: /\[INST\]\s*:/i, category: "fake_system_message", severity: "medium" },
|
|
29
|
+
{ pattern: /<\/?system>/i, category: "fake_system_message", severity: "high" },
|
|
30
|
+
|
|
31
|
+
// Tool/action manipulation
|
|
32
|
+
{ pattern: /execute\s+(?:the\s+following\s+)?(?:command|code|script)/i, category: "command_injection", severity: "medium" },
|
|
33
|
+
{ pattern: /run\s+(?:this|the\s+following)\s+(?:command|code|script)/i, category: "command_injection", severity: "medium" },
|
|
34
|
+
|
|
35
|
+
// Invisible text / social engineering
|
|
36
|
+
{ pattern: /do\s+not\s+(?:read|process|show)\s+(?:the\s+)?(?:following|rest)/i, category: "social_engineering", severity: "low" },
|
|
37
|
+
{ pattern: /(?:this|the\s+following)\s+(?:is|are)\s+(?:your\s+)?new\s+instructions/i, category: "instruction_override", severity: "high" },
|
|
38
|
+
|
|
39
|
+
// Base64/encoded content markers
|
|
40
|
+
{ pattern: /base64\s*:\s*[A-Za-z0-9+\/=]{50,}/i, category: "encoded_payload", severity: "medium" },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export function registerInjectionDetectionTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
44
|
+
pi.registerTool({
|
|
45
|
+
name: "browser_check_injection",
|
|
46
|
+
label: "Browser Check Injection",
|
|
47
|
+
description:
|
|
48
|
+
"Scan current page content for potential prompt injection attempts. " +
|
|
49
|
+
"Checks visible text and hidden elements for patterns that might hijack the agent. " +
|
|
50
|
+
"Returns findings with severity levels. Use after navigating to untrusted pages.",
|
|
51
|
+
parameters: Type.Object({
|
|
52
|
+
includeHidden: Type.Optional(
|
|
53
|
+
Type.Boolean({
|
|
54
|
+
description:
|
|
55
|
+
"Also scan hidden/invisible text (default: true). " +
|
|
56
|
+
"Hidden text is a common vector for injection attacks.",
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
62
|
+
try {
|
|
63
|
+
const { page: p } = await deps.ensureBrowser();
|
|
64
|
+
const includeHidden = params.includeHidden ?? true;
|
|
65
|
+
|
|
66
|
+
// Extract text content from the page
|
|
67
|
+
const pageContent = await p.evaluate((scanHidden: boolean) => {
|
|
68
|
+
const results: Array<{ text: string; source: string; visible: boolean }> = [];
|
|
69
|
+
|
|
70
|
+
// 1. Visible text content
|
|
71
|
+
const bodyText = document.body?.innerText ?? "";
|
|
72
|
+
results.push({ text: bodyText, source: "body_visible_text", visible: true });
|
|
73
|
+
|
|
74
|
+
// 2. Title and meta
|
|
75
|
+
results.push({ text: document.title, source: "page_title", visible: true });
|
|
76
|
+
|
|
77
|
+
// Meta descriptions and keywords
|
|
78
|
+
const metas = document.querySelectorAll("meta[name], meta[property]");
|
|
79
|
+
for (const meta of metas) {
|
|
80
|
+
const content = meta.getAttribute("content");
|
|
81
|
+
if (content) {
|
|
82
|
+
results.push({
|
|
83
|
+
text: content,
|
|
84
|
+
source: `meta:${meta.getAttribute("name") || meta.getAttribute("property")}`,
|
|
85
|
+
visible: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (scanHidden) {
|
|
91
|
+
// 3. Hidden elements (display:none, visibility:hidden, opacity:0, off-screen, aria-hidden)
|
|
92
|
+
const allElements = document.querySelectorAll("*");
|
|
93
|
+
for (const el of allElements) {
|
|
94
|
+
const htmlEl = el as HTMLElement;
|
|
95
|
+
const style = window.getComputedStyle(htmlEl);
|
|
96
|
+
const isHidden =
|
|
97
|
+
style.display === "none" ||
|
|
98
|
+
style.visibility === "hidden" ||
|
|
99
|
+
style.opacity === "0" ||
|
|
100
|
+
htmlEl.getAttribute("aria-hidden") === "true" ||
|
|
101
|
+
(htmlEl.offsetWidth === 0 && htmlEl.offsetHeight === 0);
|
|
102
|
+
|
|
103
|
+
if (isHidden && htmlEl.textContent?.trim()) {
|
|
104
|
+
const text = htmlEl.textContent.trim();
|
|
105
|
+
if (text.length > 5 && text.length < 5000) {
|
|
106
|
+
results.push({ text, source: "hidden_element", visible: false });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 4. HTML comments
|
|
112
|
+
const walker = document.createTreeWalker(
|
|
113
|
+
document.documentElement,
|
|
114
|
+
NodeFilter.SHOW_COMMENT,
|
|
115
|
+
);
|
|
116
|
+
let node;
|
|
117
|
+
while ((node = walker.nextNode())) {
|
|
118
|
+
const text = (node as Comment).textContent?.trim() ?? "";
|
|
119
|
+
if (text.length > 10) {
|
|
120
|
+
results.push({ text, source: "html_comment", visible: false });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 5. Data attributes with text content
|
|
125
|
+
const dataElements = document.querySelectorAll("[data-prompt], [data-instruction], [data-system]");
|
|
126
|
+
for (const el of dataElements) {
|
|
127
|
+
for (const attr of el.attributes) {
|
|
128
|
+
if (attr.name.startsWith("data-") && attr.value.length > 10) {
|
|
129
|
+
results.push({
|
|
130
|
+
text: attr.value,
|
|
131
|
+
source: `data_attribute:${attr.name}`,
|
|
132
|
+
visible: false,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return results;
|
|
140
|
+
}, includeHidden);
|
|
141
|
+
|
|
142
|
+
// Scan all extracted text against injection patterns
|
|
143
|
+
const findings: Array<{
|
|
144
|
+
pattern: string;
|
|
145
|
+
category: string;
|
|
146
|
+
severity: string;
|
|
147
|
+
source: string;
|
|
148
|
+
visible: boolean;
|
|
149
|
+
matchedText: string;
|
|
150
|
+
}> = [];
|
|
151
|
+
|
|
152
|
+
for (const { text, source, visible } of pageContent) {
|
|
153
|
+
for (const { pattern, category, severity } of INJECTION_PATTERNS) {
|
|
154
|
+
const match = text.match(pattern);
|
|
155
|
+
if (match) {
|
|
156
|
+
findings.push({
|
|
157
|
+
pattern: pattern.source.slice(0, 60),
|
|
158
|
+
category,
|
|
159
|
+
severity,
|
|
160
|
+
source,
|
|
161
|
+
visible,
|
|
162
|
+
matchedText: match[0].slice(0, 100),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Deduplicate findings by category + source
|
|
169
|
+
const seen = new Set<string>();
|
|
170
|
+
const uniqueFindings = findings.filter((f) => {
|
|
171
|
+
const key = `${f.category}|${f.source}|${f.matchedText}`;
|
|
172
|
+
if (seen.has(key)) return false;
|
|
173
|
+
seen.add(key);
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const highCount = uniqueFindings.filter((f) => f.severity === "high").length;
|
|
178
|
+
const medCount = uniqueFindings.filter((f) => f.severity === "medium").length;
|
|
179
|
+
const lowCount = uniqueFindings.filter((f) => f.severity === "low").length;
|
|
180
|
+
|
|
181
|
+
if (uniqueFindings.length === 0) {
|
|
182
|
+
return {
|
|
183
|
+
content: [{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: `No prompt injection patterns detected.\nScanned: ${pageContent.length} text regions (hidden: ${includeHidden})`,
|
|
186
|
+
}],
|
|
187
|
+
details: {
|
|
188
|
+
clean: true,
|
|
189
|
+
scannedRegions: pageContent.length,
|
|
190
|
+
includeHidden,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const findingLines = uniqueFindings.map((f) =>
|
|
196
|
+
` [${f.severity.toUpperCase()}] ${f.category} in ${f.source}${!f.visible ? " (HIDDEN)" : ""}: "${f.matchedText}"`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
content: [{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `⚠️ Prompt injection patterns detected: ${uniqueFindings.length} finding(s)\nHigh: ${highCount} | Medium: ${medCount} | Low: ${lowCount}\n\n${findingLines.join("\n")}\n\n⚠️ This page may be attempting to manipulate the agent. Proceed with caution.`,
|
|
203
|
+
}],
|
|
204
|
+
details: {
|
|
205
|
+
clean: false,
|
|
206
|
+
findings: uniqueFindings,
|
|
207
|
+
counts: { high: highCount, medium: medCount, low: lowCount, total: uniqueFindings.length },
|
|
208
|
+
scannedRegions: pageContent.length,
|
|
209
|
+
includeHidden,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
} catch (err: any) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: `Injection check failed: ${err.message}` }],
|
|
215
|
+
details: { error: err.message },
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { ToolDeps } from "../state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Network interception & mocking tools — mock API responses, block URLs, simulate errors.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface ActiveRoute {
|
|
10
|
+
id: number;
|
|
11
|
+
pattern: string;
|
|
12
|
+
type: "mock" | "block";
|
|
13
|
+
status?: number;
|
|
14
|
+
delay?: number;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let nextRouteId = 1;
|
|
19
|
+
const activeRoutes: ActiveRoute[] = [];
|
|
20
|
+
const routeCleanups: Map<number, () => Promise<void>> = new Map();
|
|
21
|
+
|
|
22
|
+
export function registerNetworkMockTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
23
|
+
// -------------------------------------------------------------------------
|
|
24
|
+
// browser_mock_route
|
|
25
|
+
// -------------------------------------------------------------------------
|
|
26
|
+
pi.registerTool({
|
|
27
|
+
name: "browser_mock_route",
|
|
28
|
+
label: "Browser Mock Route",
|
|
29
|
+
description:
|
|
30
|
+
"Intercept network requests matching a URL pattern and respond with custom status, body, and headers. " +
|
|
31
|
+
"Supports simulating slow responses via delay parameter. " +
|
|
32
|
+
"Routes survive page navigation within the same context. Use browser_clear_routes to remove all mocks.",
|
|
33
|
+
parameters: Type.Object({
|
|
34
|
+
url: Type.String({
|
|
35
|
+
description: "URL pattern to intercept. Supports glob patterns (e.g., '**/api/users*') or exact URLs.",
|
|
36
|
+
}),
|
|
37
|
+
status: Type.Optional(
|
|
38
|
+
Type.Number({ description: "HTTP status code for the mock response (default: 200)." }),
|
|
39
|
+
),
|
|
40
|
+
body: Type.Optional(
|
|
41
|
+
Type.String({ description: "Response body string. For JSON responses, pass a JSON string." }),
|
|
42
|
+
),
|
|
43
|
+
contentType: Type.Optional(
|
|
44
|
+
Type.String({ description: "Content-Type header (default: 'application/json' if body looks like JSON, else 'text/plain')." }),
|
|
45
|
+
),
|
|
46
|
+
headers: Type.Optional(
|
|
47
|
+
Type.Record(Type.String(), Type.String(), {
|
|
48
|
+
description: "Additional response headers as key-value pairs.",
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
delay: Type.Optional(
|
|
52
|
+
Type.Number({ description: "Delay in milliseconds before sending the response. Simulates slow responses." }),
|
|
53
|
+
),
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
57
|
+
try {
|
|
58
|
+
const { page: p } = await deps.ensureBrowser();
|
|
59
|
+
const routeId = nextRouteId++;
|
|
60
|
+
|
|
61
|
+
const status = params.status ?? 200;
|
|
62
|
+
const body = params.body ?? "";
|
|
63
|
+
const delay = params.delay ?? 0;
|
|
64
|
+
|
|
65
|
+
// Auto-detect content type
|
|
66
|
+
let contentType = params.contentType;
|
|
67
|
+
if (!contentType) {
|
|
68
|
+
try {
|
|
69
|
+
JSON.parse(body);
|
|
70
|
+
contentType = "application/json";
|
|
71
|
+
} catch {
|
|
72
|
+
contentType = "text/plain";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const headers: Record<string, string> = {
|
|
77
|
+
"content-type": contentType,
|
|
78
|
+
"access-control-allow-origin": "*",
|
|
79
|
+
...(params.headers ?? {}),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handler = async (route: any) => {
|
|
83
|
+
if (delay > 0) {
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
85
|
+
}
|
|
86
|
+
await route.fulfill({
|
|
87
|
+
status,
|
|
88
|
+
body,
|
|
89
|
+
headers,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
await p.route(params.url, handler);
|
|
94
|
+
|
|
95
|
+
const cleanup = async () => {
|
|
96
|
+
try {
|
|
97
|
+
await p.unroute(params.url, handler);
|
|
98
|
+
} catch {
|
|
99
|
+
// Page may be closed
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const routeInfo: ActiveRoute = {
|
|
104
|
+
id: routeId,
|
|
105
|
+
pattern: params.url,
|
|
106
|
+
type: "mock",
|
|
107
|
+
status,
|
|
108
|
+
delay: delay > 0 ? delay : undefined,
|
|
109
|
+
description: `Mock ${params.url} → ${status}${delay > 0 ? ` (${delay}ms delay)` : ""}`,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
activeRoutes.push(routeInfo);
|
|
113
|
+
routeCleanups.set(routeId, cleanup);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `Route mocked: ${routeInfo.description}\nRoute ID: ${routeId}\nActive routes: ${activeRoutes.length}`,
|
|
119
|
+
}],
|
|
120
|
+
details: { routeId, ...routeInfo, activeRouteCount: activeRoutes.length },
|
|
121
|
+
};
|
|
122
|
+
} catch (err: any) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: `Mock route failed: ${err.message}` }],
|
|
125
|
+
details: { error: err.message },
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// -------------------------------------------------------------------------
|
|
133
|
+
// browser_block_urls
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
pi.registerTool({
|
|
136
|
+
name: "browser_block_urls",
|
|
137
|
+
label: "Browser Block URLs",
|
|
138
|
+
description:
|
|
139
|
+
"Block network requests matching URL patterns. Useful for blocking analytics, ads, or third-party scripts. " +
|
|
140
|
+
"Accepts glob patterns. Routes survive page navigation.",
|
|
141
|
+
parameters: Type.Object({
|
|
142
|
+
patterns: Type.Array(Type.String(), {
|
|
143
|
+
description: "URL patterns to block (glob syntax, e.g., ['**/analytics*', '**/ads*']).",
|
|
144
|
+
}),
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
148
|
+
try {
|
|
149
|
+
const { page: p } = await deps.ensureBrowser();
|
|
150
|
+
const results: ActiveRoute[] = [];
|
|
151
|
+
|
|
152
|
+
for (const pattern of params.patterns) {
|
|
153
|
+
const routeId = nextRouteId++;
|
|
154
|
+
|
|
155
|
+
const handler = async (route: any) => {
|
|
156
|
+
await route.abort("blockedbyclient");
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await p.route(pattern, handler);
|
|
160
|
+
|
|
161
|
+
const cleanup = async () => {
|
|
162
|
+
try {
|
|
163
|
+
await p.unroute(pattern, handler);
|
|
164
|
+
} catch {}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const routeInfo: ActiveRoute = {
|
|
168
|
+
id: routeId,
|
|
169
|
+
pattern,
|
|
170
|
+
type: "block",
|
|
171
|
+
description: `Block ${pattern}`,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
activeRoutes.push(routeInfo);
|
|
175
|
+
routeCleanups.set(routeId, cleanup);
|
|
176
|
+
results.push(routeInfo);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
content: [{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: `Blocked ${results.length} URL pattern(s):\n${results.map((r) => ` - ${r.description} (ID: ${r.id})`).join("\n")}\nActive routes: ${activeRoutes.length}`,
|
|
183
|
+
}],
|
|
184
|
+
details: { blocked: results, activeRouteCount: activeRoutes.length },
|
|
185
|
+
};
|
|
186
|
+
} catch (err: any) {
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: `Block URLs failed: ${err.message}` }],
|
|
189
|
+
details: { error: err.message },
|
|
190
|
+
isError: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// -------------------------------------------------------------------------
|
|
197
|
+
// browser_clear_routes
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
pi.registerTool({
|
|
200
|
+
name: "browser_clear_routes",
|
|
201
|
+
label: "Browser Clear Routes",
|
|
202
|
+
description:
|
|
203
|
+
"Remove all active route mocks and URL blocks. Also lists currently active routes if called with no routes active.",
|
|
204
|
+
parameters: Type.Object({}),
|
|
205
|
+
|
|
206
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
207
|
+
try {
|
|
208
|
+
await deps.ensureBrowser();
|
|
209
|
+
const count = activeRoutes.length;
|
|
210
|
+
|
|
211
|
+
if (count === 0) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: "No active routes to clear." }],
|
|
214
|
+
details: { cleared: 0 },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const routeDescriptions = activeRoutes.map((r) => r.description);
|
|
219
|
+
|
|
220
|
+
// Clean up all routes
|
|
221
|
+
for (const [id, cleanup] of routeCleanups) {
|
|
222
|
+
await cleanup();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
activeRoutes.length = 0;
|
|
226
|
+
routeCleanups.clear();
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
content: [{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: `Cleared ${count} route(s):\n${routeDescriptions.map((d) => ` - ${d}`).join("\n")}`,
|
|
232
|
+
}],
|
|
233
|
+
details: { cleared: count, routes: routeDescriptions },
|
|
234
|
+
};
|
|
235
|
+
} catch (err: any) {
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: "text", text: `Clear routes failed: ${err.message}` }],
|
|
238
|
+
details: { error: err.message },
|
|
239
|
+
isError: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|