opengstack 0.13.10 → 0.14.2

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 (189) hide show
  1. package/AGENTS.md +4 -4
  2. package/CLAUDE.md +127 -110
  3. package/README.md +10 -5
  4. package/SKILL.md +500 -70
  5. package/bin/opengstack.js +69 -69
  6. package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +7 -25
  7. package/{skills/benchmark/SKILL.md → commands/benchmark.md} +84 -108
  8. package/{skills/browse/SKILL.md → commands/browse.md} +60 -81
  9. package/{skills/ship/SKILL.md → commands/canary.md} +7 -27
  10. package/{skills/careful/SKILL.md → commands/careful.md} +2 -22
  11. package/{skills/canary/SKILL.md → commands/codex.md} +7 -26
  12. package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +7 -24
  13. package/commands/cso.md +70 -0
  14. package/commands/design-consultation.md +70 -0
  15. package/commands/design-review.md +70 -0
  16. package/commands/design-shotgun.md +70 -0
  17. package/commands/document-release.md +70 -0
  18. package/{skills/freeze/SKILL.md → commands/freeze.md} +3 -29
  19. package/{skills/guard/SKILL.md → commands/guard.md} +4 -35
  20. package/commands/investigate.md +70 -0
  21. package/commands/land-and-deploy.md +70 -0
  22. package/commands/office-hours.md +70 -0
  23. package/{skills/gstack-upgrade/SKILL.md → commands/opengstack-upgrade.md} +64 -79
  24. package/commands/plan-ceo-review.md +70 -0
  25. package/commands/plan-design-review.md +70 -0
  26. package/commands/plan-eng-review.md +70 -0
  27. package/commands/qa-only.md +70 -0
  28. package/commands/qa.md +70 -0
  29. package/commands/retro.md +70 -0
  30. package/commands/review.md +70 -0
  31. package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +22 -40
  32. package/commands/setup-deploy.md +70 -0
  33. package/commands/ship.md +70 -0
  34. package/commands/unfreeze.md +25 -0
  35. package/docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md +9 -9
  36. package/docs/designs/CONDUCTOR_CHROME_SIDEBAR_INTEGRATION.md +2 -2
  37. package/docs/designs/CONDUCTOR_SESSION_API.md +16 -16
  38. package/docs/designs/DESIGN_SHOTGUN.md +74 -74
  39. package/docs/designs/DESIGN_TOOLS_V1.md +111 -111
  40. package/docs/skills.md +483 -202
  41. package/package.json +42 -43
  42. package/scripts/analytics.ts +188 -0
  43. package/scripts/dev-skill.ts +83 -0
  44. package/scripts/discover-skills.ts +39 -0
  45. package/scripts/eval-compare.ts +97 -0
  46. package/scripts/eval-list.ts +117 -0
  47. package/scripts/eval-select.ts +86 -0
  48. package/scripts/eval-summary.ts +188 -0
  49. package/scripts/eval-watch.ts +172 -0
  50. package/scripts/gen-skill-docs.ts +473 -0
  51. package/scripts/resolvers/browse.ts +129 -0
  52. package/scripts/resolvers/codex-helpers.ts +133 -0
  53. package/scripts/resolvers/composition.ts +48 -0
  54. package/scripts/resolvers/confidence.ts +37 -0
  55. package/scripts/resolvers/constants.ts +50 -0
  56. package/scripts/resolvers/design.ts +950 -0
  57. package/scripts/resolvers/index.ts +59 -0
  58. package/scripts/resolvers/learnings.ts +96 -0
  59. package/scripts/resolvers/preamble.ts +505 -0
  60. package/scripts/resolvers/review.ts +884 -0
  61. package/scripts/resolvers/testing.ts +573 -0
  62. package/scripts/resolvers/types.ts +45 -0
  63. package/scripts/resolvers/utility.ts +421 -0
  64. package/scripts/skill-check.ts +190 -0
  65. package/scripts/cleanup.py +0 -100
  66. package/scripts/filter-skills.sh +0 -114
  67. package/scripts/filter_skills.py +0 -164
  68. package/scripts/install-skills.js +0 -60
  69. package/skills/autoplan/SKILL.md +0 -96
  70. package/skills/autoplan/SKILL.md.tmpl +0 -694
  71. package/skills/benchmark/SKILL.md.tmpl +0 -222
  72. package/skills/browse/SKILL.md.tmpl +0 -131
  73. package/skills/browse/bin/find-browse +0 -21
  74. package/skills/browse/bin/remote-slug +0 -14
  75. package/skills/browse/scripts/build-node-server.sh +0 -48
  76. package/skills/browse/src/activity.ts +0 -208
  77. package/skills/browse/src/browser-manager.ts +0 -959
  78. package/skills/browse/src/buffers.ts +0 -137
  79. package/skills/browse/src/bun-polyfill.cjs +0 -109
  80. package/skills/browse/src/cli.ts +0 -678
  81. package/skills/browse/src/commands.ts +0 -128
  82. package/skills/browse/src/config.ts +0 -150
  83. package/skills/browse/src/cookie-import-browser.ts +0 -625
  84. package/skills/browse/src/cookie-picker-routes.ts +0 -230
  85. package/skills/browse/src/cookie-picker-ui.ts +0 -688
  86. package/skills/browse/src/find-browse.ts +0 -61
  87. package/skills/browse/src/meta-commands.ts +0 -550
  88. package/skills/browse/src/platform.ts +0 -17
  89. package/skills/browse/src/read-commands.ts +0 -358
  90. package/skills/browse/src/server.ts +0 -1192
  91. package/skills/browse/src/sidebar-agent.ts +0 -280
  92. package/skills/browse/src/sidebar-utils.ts +0 -21
  93. package/skills/browse/src/snapshot.ts +0 -407
  94. package/skills/browse/src/url-validation.ts +0 -95
  95. package/skills/browse/src/write-commands.ts +0 -364
  96. package/skills/browse/test/activity.test.ts +0 -120
  97. package/skills/browse/test/adversarial-security.test.ts +0 -32
  98. package/skills/browse/test/browser-manager-unit.test.ts +0 -17
  99. package/skills/browse/test/bun-polyfill.test.ts +0 -72
  100. package/skills/browse/test/commands.test.ts +0 -2075
  101. package/skills/browse/test/compare-board.test.ts +0 -342
  102. package/skills/browse/test/config.test.ts +0 -316
  103. package/skills/browse/test/cookie-import-browser.test.ts +0 -519
  104. package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
  105. package/skills/browse/test/file-drop.test.ts +0 -271
  106. package/skills/browse/test/find-browse.test.ts +0 -50
  107. package/skills/browse/test/findport.test.ts +0 -191
  108. package/skills/browse/test/fixtures/basic.html +0 -33
  109. package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
  110. package/skills/browse/test/fixtures/dialog.html +0 -15
  111. package/skills/browse/test/fixtures/empty.html +0 -2
  112. package/skills/browse/test/fixtures/forms.html +0 -55
  113. package/skills/browse/test/fixtures/iframe.html +0 -30
  114. package/skills/browse/test/fixtures/network-idle.html +0 -30
  115. package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
  116. package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
  117. package/skills/browse/test/fixtures/qa-eval.html +0 -51
  118. package/skills/browse/test/fixtures/responsive.html +0 -49
  119. package/skills/browse/test/fixtures/snapshot.html +0 -55
  120. package/skills/browse/test/fixtures/spa.html +0 -24
  121. package/skills/browse/test/fixtures/states.html +0 -17
  122. package/skills/browse/test/fixtures/upload.html +0 -25
  123. package/skills/browse/test/gstack-config.test.ts +0 -138
  124. package/skills/browse/test/gstack-update-check.test.ts +0 -514
  125. package/skills/browse/test/handoff.test.ts +0 -235
  126. package/skills/browse/test/path-validation.test.ts +0 -91
  127. package/skills/browse/test/platform.test.ts +0 -37
  128. package/skills/browse/test/server-auth.test.ts +0 -65
  129. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
  130. package/skills/browse/test/sidebar-agent.test.ts +0 -199
  131. package/skills/browse/test/sidebar-integration.test.ts +0 -320
  132. package/skills/browse/test/sidebar-unit.test.ts +0 -96
  133. package/skills/browse/test/snapshot.test.ts +0 -467
  134. package/skills/browse/test/state-ttl.test.ts +0 -35
  135. package/skills/browse/test/test-server.ts +0 -57
  136. package/skills/browse/test/url-validation.test.ts +0 -72
  137. package/skills/browse/test/watch.test.ts +0 -129
  138. package/skills/canary/SKILL.md.tmpl +0 -212
  139. package/skills/careful/SKILL.md.tmpl +0 -56
  140. package/skills/careful/bin/check-careful.sh +0 -112
  141. package/skills/codex/SKILL.md +0 -90
  142. package/skills/codex/SKILL.md.tmpl +0 -417
  143. package/skills/connect-chrome/SKILL.md.tmpl +0 -195
  144. package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
  145. package/skills/cso/SKILL.md +0 -93
  146. package/skills/cso/SKILL.md.tmpl +0 -606
  147. package/skills/design-consultation/SKILL.md +0 -94
  148. package/skills/design-consultation/SKILL.md.tmpl +0 -415
  149. package/skills/design-review/SKILL.md +0 -94
  150. package/skills/design-review/SKILL.md.tmpl +0 -290
  151. package/skills/design-shotgun/SKILL.md +0 -91
  152. package/skills/design-shotgun/SKILL.md.tmpl +0 -285
  153. package/skills/document-release/SKILL.md +0 -91
  154. package/skills/document-release/SKILL.md.tmpl +0 -359
  155. package/skills/freeze/SKILL.md.tmpl +0 -77
  156. package/skills/freeze/bin/check-freeze.sh +0 -79
  157. package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
  158. package/skills/guard/SKILL.md.tmpl +0 -77
  159. package/skills/investigate/SKILL.md +0 -105
  160. package/skills/investigate/SKILL.md.tmpl +0 -194
  161. package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
  162. package/skills/office-hours/SKILL.md +0 -96
  163. package/skills/office-hours/SKILL.md.tmpl +0 -645
  164. package/skills/plan-ceo-review/SKILL.md +0 -94
  165. package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
  166. package/skills/plan-design-review/SKILL.md +0 -92
  167. package/skills/plan-design-review/SKILL.md.tmpl +0 -446
  168. package/skills/plan-eng-review/SKILL.md +0 -93
  169. package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
  170. package/skills/qa/SKILL.md +0 -95
  171. package/skills/qa/SKILL.md.tmpl +0 -316
  172. package/skills/qa/references/issue-taxonomy.md +0 -85
  173. package/skills/qa/templates/qa-report-template.md +0 -126
  174. package/skills/qa-only/SKILL.md +0 -89
  175. package/skills/qa-only/SKILL.md.tmpl +0 -101
  176. package/skills/retro/SKILL.md +0 -89
  177. package/skills/retro/SKILL.md.tmpl +0 -820
  178. package/skills/review/SKILL.md +0 -92
  179. package/skills/review/SKILL.md.tmpl +0 -281
  180. package/skills/review/TODOS-format.md +0 -62
  181. package/skills/review/checklist.md +0 -220
  182. package/skills/review/design-checklist.md +0 -132
  183. package/skills/review/greptile-triage.md +0 -220
  184. package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
  185. package/skills/setup-deploy/SKILL.md +0 -92
  186. package/skills/setup-deploy/SKILL.md.tmpl +0 -215
  187. package/skills/ship/SKILL.md.tmpl +0 -636
  188. package/skills/unfreeze/SKILL.md +0 -37
  189. package/skills/unfreeze/SKILL.md.tmpl +0 -36
@@ -1,358 +0,0 @@
1
- /**
2
- * Read commands — extract data from pages without side effects
3
- *
4
- * text, html, links, forms, accessibility, js, eval, css, attrs,
5
- * console, network, cookies, storage, perf
6
- */
7
-
8
- import type { BrowserManager } from './browser-manager';
9
- import { consoleBuffer, networkBuffer, dialogBuffer } from './buffers';
10
- import type { Page, Frame } from 'playwright';
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import { TEMP_DIR, isPathWithin } from './platform';
14
-
15
- /** Detect await keyword, ignoring comments. Accepted risk: await in string literals triggers wrapping (harmless). */
16
- function hasAwait(code: string): boolean {
17
- const stripped = code.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
18
- return /\bawait\b/.test(stripped);
19
- }
20
-
21
- /** Detect whether code needs a block wrapper {…} vs expression wrapper (…) inside an async IIFE. */
22
- function needsBlockWrapper(code: string): boolean {
23
- const trimmed = code.trim();
24
- if (trimmed.split('\n').length > 1) return true;
25
- if (/\b(const|let|var|function|class|return|throw|if|for|while|switch|try)\b/.test(trimmed)) return true;
26
- if (trimmed.includes(';')) return true;
27
- return false;
28
- }
29
-
30
- /** Wrap code for page.evaluate(), using async IIFE with block or expression body as needed. */
31
- function wrapForEvaluate(code: string): string {
32
- if (!hasAwait(code)) return code;
33
- const trimmed = code.trim();
34
- return needsBlockWrapper(trimmed)
35
- ? `(async()=>{\n${code}\n})()`
36
- : `(async()=>(${trimmed}))()`;
37
- }
38
-
39
- // Security: Path validation to prevent path traversal attacks
40
- // Resolve safe directories through realpathSync to handle symlinks (e.g., macOS /tmp → /private/tmp)
41
- const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()].map(d => {
42
- try { return fs.realpathSync(d); } catch { return d; }
43
- });
44
-
45
- export function validateReadPath(filePath: string): void {
46
- // Always resolve to absolute first (fixes relative path symlink bypass)
47
- const resolved = path.resolve(filePath);
48
- // Resolve symlinks — throw on non-ENOENT errors
49
- let realPath: string;
50
- try {
51
- realPath = fs.realpathSync(resolved);
52
- } catch (err: any) {
53
- if (err.code === 'ENOENT') {
54
- // File doesn't exist — resolve directory part for symlinks (e.g., /tmp → /private/tmp)
55
- try {
56
- const dir = fs.realpathSync(path.dirname(resolved));
57
- realPath = path.join(dir, path.basename(resolved));
58
- } catch {
59
- realPath = resolved;
60
- }
61
- } else {
62
- throw new Error(`Cannot resolve real path: ${filePath} (${err.code})`);
63
- }
64
- }
65
- const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(realPath, dir));
66
- if (!isSafe) {
67
- throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
68
- }
69
- }
70
-
71
- /**
72
- * Extract clean text from a page (strips script/style/noscript/svg).
73
- * Exported for DRY reuse in meta-commands (diff).
74
- */
75
- export async function getCleanText(page: Page | Frame): Promise<string> {
76
- return await page.evaluate(() => {
77
- const body = document.body;
78
- if (!body) return '';
79
- const clone = body.cloneNode(true) as HTMLElement;
80
- clone.querySelectorAll('script, style, noscript, svg').forEach(el => el.remove());
81
- return clone.innerText
82
- .split('\n')
83
- .map(line => line.trim())
84
- .filter(line => line.length > 0)
85
- .join('\n');
86
- });
87
- }
88
-
89
- export async function handleReadCommand(
90
- command: string,
91
- args: string[],
92
- bm: BrowserManager
93
- ): Promise<string> {
94
- const page = bm.getPage();
95
- // Frame-aware target for content extraction
96
- const target = bm.getActiveFrameOrPage();
97
-
98
- switch (command) {
99
- case 'text': {
100
- return await getCleanText(target);
101
- }
102
-
103
- case 'html': {
104
- const selector = args[0];
105
- if (selector) {
106
- const resolved = await bm.resolveRef(selector);
107
- if ('locator' in resolved) {
108
- return await resolved.locator.innerHTML({ timeout: 5000 });
109
- }
110
- return await target.locator(resolved.selector).innerHTML({ timeout: 5000 });
111
- }
112
- // page.content() is page-only; use evaluate for frame compat
113
- const doctype = await target.evaluate(() => {
114
- const dt = document.doctype;
115
- return dt ? `<!DOCTYPE ${dt.name}>` : '';
116
- });
117
- const html = await target.evaluate(() => document.documentElement.outerHTML);
118
- return doctype ? `${doctype}\n${html}` : html;
119
- }
120
-
121
- case 'links': {
122
- const links = await target.evaluate(() =>
123
- [...document.querySelectorAll('a[href]')].map(a => ({
124
- text: a.textContent?.trim().slice(0, 120) || '',
125
- href: (a as HTMLAnchorElement).href,
126
- })).filter(l => l.text && l.href)
127
- );
128
- return links.map(l => `${l.text} → ${l.href}`).join('\n');
129
- }
130
-
131
- case 'forms': {
132
- const forms = await target.evaluate(() => {
133
- return [...document.querySelectorAll('form')].map((form, i) => {
134
- const fields = [...form.querySelectorAll('input, select, textarea')].map(el => {
135
- const input = el as HTMLInputElement;
136
- return {
137
- tag: el.tagName.toLowerCase(),
138
- type: input.type || undefined,
139
- name: input.name || undefined,
140
- id: input.id || undefined,
141
- placeholder: input.placeholder || undefined,
142
- required: input.required || undefined,
143
- value: input.type === 'password' ? '[redacted]' : (input.value || undefined),
144
- options: el.tagName === 'SELECT'
145
- ? [...(el as HTMLSelectElement).options].map(o => ({ value: o.value, text: o.text }))
146
- : undefined,
147
- };
148
- });
149
- return {
150
- index: i,
151
- action: form.action || undefined,
152
- method: form.method || 'get',
153
- id: form.id || undefined,
154
- fields,
155
- };
156
- });
157
- });
158
- return JSON.stringify(forms, null, 2);
159
- }
160
-
161
- case 'accessibility': {
162
- const snapshot = await target.locator("body").ariaSnapshot();
163
- return snapshot;
164
- }
165
-
166
- case 'js': {
167
- const expr = args[0];
168
- if (!expr) throw new Error('Usage: browse js <expression>');
169
- const wrapped = wrapForEvaluate(expr);
170
- const result = await target.evaluate(wrapped);
171
- return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
172
- }
173
-
174
- case 'eval': {
175
- const filePath = args[0];
176
- if (!filePath) throw new Error('Usage: browse eval <js-file>');
177
- validateReadPath(filePath);
178
- if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
179
- const code = fs.readFileSync(filePath, 'utf-8');
180
- const wrapped = wrapForEvaluate(code);
181
- const result = await target.evaluate(wrapped);
182
- return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
183
- }
184
-
185
- case 'css': {
186
- const [selector, property] = args;
187
- if (!selector || !property) throw new Error('Usage: browse css <selector> <property>');
188
- const resolved = await bm.resolveRef(selector);
189
- if ('locator' in resolved) {
190
- const value = await resolved.locator.evaluate(
191
- (el, prop) => getComputedStyle(el).getPropertyValue(prop),
192
- property
193
- );
194
- return value;
195
- }
196
- const value = await target.evaluate(
197
- ([sel, prop]) => {
198
- const el = document.querySelector(sel);
199
- if (!el) return `Element not found: ${sel}`;
200
- return getComputedStyle(el).getPropertyValue(prop);
201
- },
202
- [resolved.selector, property]
203
- );
204
- return value;
205
- }
206
-
207
- case 'attrs': {
208
- const selector = args[0];
209
- if (!selector) throw new Error('Usage: browse attrs <selector>');
210
- const resolved = await bm.resolveRef(selector);
211
- if ('locator' in resolved) {
212
- const attrs = await resolved.locator.evaluate((el) => {
213
- const result: Record<string, string> = {};
214
- for (const attr of el.attributes) {
215
- result[attr.name] = attr.value;
216
- }
217
- return result;
218
- });
219
- return JSON.stringify(attrs, null, 2);
220
- }
221
- const attrs = await target.evaluate((sel: string) => {
222
- const el = document.querySelector(sel);
223
- if (!el) return `Element not found: ${sel}`;
224
- const result: Record<string, string> = {};
225
- for (const attr of el.attributes) {
226
- result[attr.name] = attr.value;
227
- }
228
- return result;
229
- }, resolved.selector);
230
- return typeof attrs === 'string' ? attrs : JSON.stringify(attrs, null, 2);
231
- }
232
-
233
- case 'console': {
234
- if (args[0] === '--clear') {
235
- consoleBuffer.clear();
236
- return 'Console buffer cleared.';
237
- }
238
- const entries = args[0] === '--errors'
239
- ? consoleBuffer.toArray().filter(e => e.level === 'error' || e.level === 'warning')
240
- : consoleBuffer.toArray();
241
- if (entries.length === 0) return args[0] === '--errors' ? '(no console errors)' : '(no console messages)';
242
- return entries.map(e =>
243
- `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
244
- ).join('\n');
245
- }
246
-
247
- case 'network': {
248
- if (args[0] === '--clear') {
249
- networkBuffer.clear();
250
- return 'Network buffer cleared.';
251
- }
252
- if (networkBuffer.length === 0) return '(no network requests)';
253
- return networkBuffer.toArray().map(e =>
254
- `${e.method} ${e.url} → ${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)`
255
- ).join('\n');
256
- }
257
-
258
- case 'dialog': {
259
- if (args[0] === '--clear') {
260
- dialogBuffer.clear();
261
- return 'Dialog buffer cleared.';
262
- }
263
- if (dialogBuffer.length === 0) return '(no dialogs captured)';
264
- return dialogBuffer.toArray().map(e =>
265
- `[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ''}`
266
- ).join('\n');
267
- }
268
-
269
- case 'is': {
270
- const property = args[0];
271
- const selector = args[1];
272
- if (!property || !selector) throw new Error('Usage: browse is <property> <selector>\nProperties: visible, hidden, enabled, disabled, checked, editable, focused');
273
-
274
- const resolved = await bm.resolveRef(selector);
275
- let locator;
276
- if ('locator' in resolved) {
277
- locator = resolved.locator;
278
- } else {
279
- locator = target.locator(resolved.selector);
280
- }
281
-
282
- switch (property) {
283
- case 'visible': return String(await locator.isVisible());
284
- case 'hidden': return String(await locator.isHidden());
285
- case 'enabled': return String(await locator.isEnabled());
286
- case 'disabled': return String(await locator.isDisabled());
287
- case 'checked': return String(await locator.isChecked());
288
- case 'editable': return String(await locator.isEditable());
289
- case 'focused': {
290
- const isFocused = await locator.evaluate(
291
- (el) => el === document.activeElement
292
- );
293
- return String(isFocused);
294
- }
295
- default:
296
- throw new Error(`Unknown property: ${property}. Use: visible, hidden, enabled, disabled, checked, editable, focused`);
297
- }
298
- }
299
-
300
- case 'cookies': {
301
- const cookies = await page.context().cookies();
302
- return JSON.stringify(cookies, null, 2);
303
- }
304
-
305
- case 'storage': {
306
- if (args[0] === 'set' && args[1]) {
307
- const key = args[1];
308
- const value = args[2] || '';
309
- await target.evaluate(([k, v]: string[]) => localStorage.setItem(k, v), [key, value]);
310
- return `Set localStorage["${key}"]`;
311
- }
312
- const storage = await target.evaluate(() => ({
313
- localStorage: { ...localStorage },
314
- sessionStorage: { ...sessionStorage },
315
- }));
316
- // Redact values that look like secrets (tokens, keys, passwords, JWTs)
317
- const SENSITIVE_KEY = /(^|[_.-])(token|secret|key|password|credential|auth|jwt|session|csrf)($|[_.-])|api.?key/i;
318
- const SENSITIVE_VALUE = /^(eyJ|sk-|sk_live_|sk_test_|pk_live_|pk_test_|rk_live_|sk-ant-|ghp_|gho_|github_pat_|xox[bpsa]-|AKIA[A-Z0-9]{16}|AIza|SG\.|Bearer\s|sbp_)/;
319
- const redacted = JSON.parse(JSON.stringify(storage));
320
- for (const storeType of ['localStorage', 'sessionStorage'] as const) {
321
- const store = redacted[storeType];
322
- if (!store) continue;
323
- for (const [key, value] of Object.entries(store)) {
324
- if (typeof value !== 'string') continue;
325
- if (SENSITIVE_KEY.test(key) || SENSITIVE_VALUE.test(value)) {
326
- store[key] = `[REDACTED — ${value.length} chars]`;
327
- }
328
- }
329
- }
330
- return JSON.stringify(redacted, null, 2);
331
- }
332
-
333
- case 'perf': {
334
- const timings = await page.evaluate(() => {
335
- const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
336
- if (!nav) return 'No navigation timing data available.';
337
- return {
338
- dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart),
339
- tcp: Math.round(nav.connectEnd - nav.connectStart),
340
- ssl: Math.round(nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0),
341
- ttfb: Math.round(nav.responseStart - nav.requestStart),
342
- download: Math.round(nav.responseEnd - nav.responseStart),
343
- domParse: Math.round(nav.domInteractive - nav.responseEnd),
344
- domReady: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
345
- load: Math.round(nav.loadEventEnd - nav.startTime),
346
- total: Math.round(nav.loadEventEnd - nav.startTime),
347
- };
348
- });
349
- if (typeof timings === 'string') return timings;
350
- return Object.entries(timings)
351
- .map(([k, v]) => `${k.padEnd(12)} ${v}ms`)
352
- .join('\n');
353
- }
354
-
355
- default:
356
- throw new Error(`Unknown read command: ${command}`);
357
- }
358
- }