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.
Files changed (228) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +74 -7
  3. package/dist/headless.d.ts +25 -0
  4. package/dist/headless.js +454 -0
  5. package/dist/help-text.js +47 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resource-loader.js +64 -9
  11. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  12. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  13. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  14. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  15. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  16. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  17. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  18. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  19. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  20. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  21. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  22. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  23. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  24. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  25. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  26. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  27. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  28. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  29. package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
  30. package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
  31. package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
  32. package/dist/resources/extensions/gsd/auto.ts +560 -52
  33. package/dist/resources/extensions/gsd/captures.ts +49 -0
  34. package/dist/resources/extensions/gsd/commands.ts +194 -11
  35. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  36. package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  37. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  38. package/dist/resources/extensions/gsd/doctor.ts +76 -12
  39. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  40. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  41. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  42. package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
  43. package/dist/resources/extensions/gsd/index.ts +34 -1
  44. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  45. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  46. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  47. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  48. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  49. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  50. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  51. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  52. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  54. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  55. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  56. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  57. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  58. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  59. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  60. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  61. package/dist/resources/extensions/gsd/state.ts +72 -30
  62. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  63. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  64. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  65. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  66. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  67. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  68. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  69. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  70. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  71. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  72. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  73. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  74. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  75. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  76. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  77. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  78. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  79. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  80. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  81. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  82. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  83. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  84. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  85. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  86. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  87. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  88. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  89. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  90. package/dist/resources/extensions/gsd/types.ts +15 -1
  91. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  92. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  93. package/dist/resources/extensions/subagent/index.ts +5 -0
  94. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  95. package/dist/update-check.d.ts +9 -0
  96. package/dist/update-check.js +97 -0
  97. package/package.json +6 -1
  98. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  99. package/packages/pi-ai/dist/providers/anthropic.js +16 -7
  100. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  101. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  103. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  106. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  112. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +21 -8
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  117. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  118. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  119. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  123. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  125. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  127. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  129. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  133. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  135. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/index.js +1 -1
  137. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  138. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  139. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  140. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  141. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  142. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  143. package/packages/pi-coding-agent/src/index.ts +1 -0
  144. package/scripts/postinstall.js +7 -109
  145. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  146. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  147. package/src/resources/extensions/bg-shell/types.ts +33 -1
  148. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  149. package/src/resources/extensions/browser-tools/index.ts +20 -0
  150. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  151. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  152. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  153. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  154. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  155. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  156. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  157. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  158. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  159. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  160. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  161. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  163. package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
  164. package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
  165. package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
  166. package/src/resources/extensions/gsd/auto.ts +560 -52
  167. package/src/resources/extensions/gsd/captures.ts +49 -0
  168. package/src/resources/extensions/gsd/commands.ts +194 -11
  169. package/src/resources/extensions/gsd/complexity.ts +1 -0
  170. package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  171. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  172. package/src/resources/extensions/gsd/doctor.ts +76 -12
  173. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  174. package/src/resources/extensions/gsd/forensics.ts +95 -52
  175. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  176. package/src/resources/extensions/gsd/guided-flow.ts +85 -5
  177. package/src/resources/extensions/gsd/index.ts +34 -1
  178. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  179. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  180. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  181. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  182. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  183. package/src/resources/extensions/gsd/preferences.ts +65 -1
  184. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  185. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  186. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  187. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  188. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  190. package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  191. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  192. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  193. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  194. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  195. package/src/resources/extensions/gsd/state.ts +72 -30
  196. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  197. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  198. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  199. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  200. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  201. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  202. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  203. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  204. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  205. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  206. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  207. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  208. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  209. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  210. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  211. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  212. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  213. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  214. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  215. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  216. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  217. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  218. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  219. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  220. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  221. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  222. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  223. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  224. package/src/resources/extensions/gsd/types.ts +15 -1
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  226. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
  227. package/src/resources/extensions/subagent/index.ts +5 -0
  228. 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
+ }