@runchr/gstack-antigravity 0.1.0 → 0.1.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.

Potentially problematic release.


This version of @runchr/gstack-antigravity might be problematic. Click here for more details.

Files changed (231) hide show
  1. package/.agents/skills/gstack/.agents/skills/gstack/SKILL.md +651 -0
  2. package/.agents/skills/gstack/.agents/skills/gstack-autoplan/SKILL.md +678 -0
  3. package/.agents/skills/gstack/.agents/skills/gstack-benchmark/SKILL.md +482 -0
  4. package/.agents/skills/gstack/.agents/skills/gstack-browse/SKILL.md +511 -0
  5. package/.agents/skills/gstack/.agents/skills/gstack-canary/SKILL.md +486 -0
  6. package/.agents/skills/gstack/.agents/skills/gstack-careful/SKILL.md +50 -0
  7. package/.agents/skills/gstack/.agents/skills/gstack-cso/SKILL.md +607 -0
  8. package/.agents/skills/gstack/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
  9. package/.agents/skills/gstack/.agents/skills/gstack-design-review/SKILL.md +988 -0
  10. package/.agents/skills/gstack/.agents/skills/gstack-document-release/SKILL.md +604 -0
  11. package/.agents/skills/gstack/.agents/skills/gstack-freeze/SKILL.md +67 -0
  12. package/.agents/skills/gstack/.agents/skills/gstack-guard/SKILL.md +62 -0
  13. package/.agents/skills/gstack/.agents/skills/gstack-investigate/SKILL.md +415 -0
  14. package/.agents/skills/gstack/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
  15. package/.agents/skills/gstack/.agents/skills/gstack-office-hours/SKILL.md +986 -0
  16. package/.agents/skills/gstack/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
  17. package/.agents/skills/gstack/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
  18. package/.agents/skills/gstack/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
  19. package/.agents/skills/gstack/.agents/skills/gstack-qa/SKILL.md +1006 -0
  20. package/.agents/skills/gstack/.agents/skills/gstack-qa-only/SKILL.md +626 -0
  21. package/.agents/skills/gstack/.agents/skills/gstack-retro/SKILL.md +1065 -0
  22. package/.agents/skills/gstack/.agents/skills/gstack-review/SKILL.md +704 -0
  23. package/.agents/skills/gstack/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
  24. package/.agents/skills/gstack/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
  25. package/.agents/skills/gstack/.agents/skills/gstack-ship/SKILL.md +1312 -0
  26. package/.agents/skills/gstack/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
  27. package/.agents/skills/gstack/.agents/skills/gstack-upgrade/SKILL.md +220 -0
  28. package/.agents/skills/gstack/.env.example +5 -0
  29. package/.agents/skills/gstack/.github/workflows/skill-docs.yml +17 -0
  30. package/.agents/skills/gstack/AGENTS.md +49 -0
  31. package/.agents/skills/gstack/ARCHITECTURE.md +359 -0
  32. package/.agents/skills/gstack/BROWSER.md +271 -0
  33. package/.agents/skills/gstack/CHANGELOG.md +800 -0
  34. package/.agents/skills/gstack/CLAUDE.md +284 -0
  35. package/.agents/skills/gstack/CONTRIBUTING.md +370 -0
  36. package/.agents/skills/gstack/ETHOS.md +129 -0
  37. package/.agents/skills/gstack/LICENSE +21 -0
  38. package/.agents/skills/gstack/README.md +228 -0
  39. package/.agents/skills/gstack/SKILL.md +657 -0
  40. package/.agents/skills/gstack/SKILL.md.tmpl +281 -0
  41. package/.agents/skills/gstack/TODOS.md +564 -0
  42. package/.agents/skills/gstack/VERSION +1 -0
  43. package/.agents/skills/gstack/autoplan/SKILL.md +689 -0
  44. package/.agents/skills/gstack/autoplan/SKILL.md.tmpl +416 -0
  45. package/.agents/skills/gstack/benchmark/SKILL.md +489 -0
  46. package/.agents/skills/gstack/benchmark/SKILL.md.tmpl +233 -0
  47. package/.agents/skills/gstack/bin/dev-setup +68 -0
  48. package/.agents/skills/gstack/bin/dev-teardown +56 -0
  49. package/.agents/skills/gstack/bin/gstack-analytics +191 -0
  50. package/.agents/skills/gstack/bin/gstack-community-dashboard +113 -0
  51. package/.agents/skills/gstack/bin/gstack-config +38 -0
  52. package/.agents/skills/gstack/bin/gstack-diff-scope +71 -0
  53. package/.agents/skills/gstack/bin/gstack-global-discover.ts +591 -0
  54. package/.agents/skills/gstack/bin/gstack-repo-mode +93 -0
  55. package/.agents/skills/gstack/bin/gstack-review-log +9 -0
  56. package/.agents/skills/gstack/bin/gstack-review-read +12 -0
  57. package/.agents/skills/gstack/bin/gstack-slug +15 -0
  58. package/.agents/skills/gstack/bin/gstack-telemetry-log +158 -0
  59. package/.agents/skills/gstack/bin/gstack-telemetry-sync +127 -0
  60. package/.agents/skills/gstack/bin/gstack-update-check +196 -0
  61. package/.agents/skills/gstack/browse/SKILL.md +517 -0
  62. package/.agents/skills/gstack/browse/SKILL.md.tmpl +141 -0
  63. package/.agents/skills/gstack/browse/bin/find-browse +21 -0
  64. package/.agents/skills/gstack/browse/bin/remote-slug +14 -0
  65. package/.agents/skills/gstack/browse/scripts/build-node-server.sh +48 -0
  66. package/.agents/skills/gstack/browse/src/browser-manager.ts +634 -0
  67. package/.agents/skills/gstack/browse/src/buffers.ts +137 -0
  68. package/.agents/skills/gstack/browse/src/bun-polyfill.cjs +109 -0
  69. package/.agents/skills/gstack/browse/src/cli.ts +420 -0
  70. package/.agents/skills/gstack/browse/src/commands.ts +111 -0
  71. package/.agents/skills/gstack/browse/src/config.ts +150 -0
  72. package/.agents/skills/gstack/browse/src/cookie-import-browser.ts +417 -0
  73. package/.agents/skills/gstack/browse/src/cookie-picker-routes.ts +207 -0
  74. package/.agents/skills/gstack/browse/src/cookie-picker-ui.ts +541 -0
  75. package/.agents/skills/gstack/browse/src/find-browse.ts +61 -0
  76. package/.agents/skills/gstack/browse/src/meta-commands.ts +269 -0
  77. package/.agents/skills/gstack/browse/src/platform.ts +17 -0
  78. package/.agents/skills/gstack/browse/src/read-commands.ts +335 -0
  79. package/.agents/skills/gstack/browse/src/server.ts +369 -0
  80. package/.agents/skills/gstack/browse/src/snapshot.ts +398 -0
  81. package/.agents/skills/gstack/browse/src/url-validation.ts +91 -0
  82. package/.agents/skills/gstack/browse/src/write-commands.ts +352 -0
  83. package/.agents/skills/gstack/browse/test/bun-polyfill.test.ts +72 -0
  84. package/.agents/skills/gstack/browse/test/commands.test.ts +1836 -0
  85. package/.agents/skills/gstack/browse/test/config.test.ts +250 -0
  86. package/.agents/skills/gstack/browse/test/cookie-import-browser.test.ts +397 -0
  87. package/.agents/skills/gstack/browse/test/cookie-picker-routes.test.ts +205 -0
  88. package/.agents/skills/gstack/browse/test/find-browse.test.ts +50 -0
  89. package/.agents/skills/gstack/browse/test/fixtures/basic.html +33 -0
  90. package/.agents/skills/gstack/browse/test/fixtures/cursor-interactive.html +22 -0
  91. package/.agents/skills/gstack/browse/test/fixtures/dialog.html +15 -0
  92. package/.agents/skills/gstack/browse/test/fixtures/empty.html +2 -0
  93. package/.agents/skills/gstack/browse/test/fixtures/forms.html +55 -0
  94. package/.agents/skills/gstack/browse/test/fixtures/qa-eval-checkout.html +108 -0
  95. package/.agents/skills/gstack/browse/test/fixtures/qa-eval-spa.html +98 -0
  96. package/.agents/skills/gstack/browse/test/fixtures/qa-eval.html +51 -0
  97. package/.agents/skills/gstack/browse/test/fixtures/responsive.html +49 -0
  98. package/.agents/skills/gstack/browse/test/fixtures/snapshot.html +55 -0
  99. package/.agents/skills/gstack/browse/test/fixtures/spa.html +24 -0
  100. package/.agents/skills/gstack/browse/test/fixtures/states.html +17 -0
  101. package/.agents/skills/gstack/browse/test/fixtures/upload.html +25 -0
  102. package/.agents/skills/gstack/browse/test/gstack-config.test.ts +125 -0
  103. package/.agents/skills/gstack/browse/test/gstack-update-check.test.ts +467 -0
  104. package/.agents/skills/gstack/browse/test/handoff.test.ts +235 -0
  105. package/.agents/skills/gstack/browse/test/path-validation.test.ts +63 -0
  106. package/.agents/skills/gstack/browse/test/platform.test.ts +37 -0
  107. package/.agents/skills/gstack/browse/test/snapshot.test.ts +467 -0
  108. package/.agents/skills/gstack/browse/test/test-server.ts +57 -0
  109. package/.agents/skills/gstack/browse/test/url-validation.test.ts +72 -0
  110. package/.agents/skills/gstack/canary/SKILL.md +493 -0
  111. package/.agents/skills/gstack/canary/SKILL.md.tmpl +220 -0
  112. package/.agents/skills/gstack/careful/SKILL.md +59 -0
  113. package/.agents/skills/gstack/careful/SKILL.md.tmpl +57 -0
  114. package/.agents/skills/gstack/careful/bin/check-careful.sh +112 -0
  115. package/.agents/skills/gstack/codex/SKILL.md +677 -0
  116. package/.agents/skills/gstack/codex/SKILL.md.tmpl +356 -0
  117. package/.agents/skills/gstack/conductor.json +6 -0
  118. package/.agents/skills/gstack/cso/SKILL.md +615 -0
  119. package/.agents/skills/gstack/cso/SKILL.md.tmpl +376 -0
  120. package/.agents/skills/gstack/design-consultation/SKILL.md +625 -0
  121. package/.agents/skills/gstack/design-consultation/SKILL.md.tmpl +369 -0
  122. package/.agents/skills/gstack/design-review/SKILL.md +998 -0
  123. package/.agents/skills/gstack/design-review/SKILL.md.tmpl +262 -0
  124. package/.agents/skills/gstack/docs/images/github-2013.png +0 -0
  125. package/.agents/skills/gstack/docs/images/github-2026.png +0 -0
  126. package/.agents/skills/gstack/docs/skills.md +877 -0
  127. package/.agents/skills/gstack/document-release/SKILL.md +613 -0
  128. package/.agents/skills/gstack/document-release/SKILL.md.tmpl +357 -0
  129. package/.agents/skills/gstack/freeze/SKILL.md +82 -0
  130. package/.agents/skills/gstack/freeze/SKILL.md.tmpl +80 -0
  131. package/.agents/skills/gstack/freeze/bin/check-freeze.sh +68 -0
  132. package/.agents/skills/gstack/gstack-upgrade/SKILL.md +226 -0
  133. package/.agents/skills/gstack/gstack-upgrade/SKILL.md.tmpl +224 -0
  134. package/.agents/skills/gstack/guard/SKILL.md +82 -0
  135. package/.agents/skills/gstack/guard/SKILL.md.tmpl +80 -0
  136. package/.agents/skills/gstack/investigate/SKILL.md +435 -0
  137. package/.agents/skills/gstack/investigate/SKILL.md.tmpl +196 -0
  138. package/.agents/skills/gstack/land-and-deploy/SKILL.md +880 -0
  139. package/.agents/skills/gstack/land-and-deploy/SKILL.md.tmpl +575 -0
  140. package/.agents/skills/gstack/office-hours/SKILL.md +996 -0
  141. package/.agents/skills/gstack/office-hours/SKILL.md.tmpl +624 -0
  142. package/.agents/skills/gstack/package.json +55 -0
  143. package/.agents/skills/gstack/plan-ceo-review/SKILL.md +1277 -0
  144. package/.agents/skills/gstack/plan-ceo-review/SKILL.md.tmpl +838 -0
  145. package/.agents/skills/gstack/plan-design-review/SKILL.md +676 -0
  146. package/.agents/skills/gstack/plan-design-review/SKILL.md.tmpl +314 -0
  147. package/.agents/skills/gstack/plan-eng-review/SKILL.md +836 -0
  148. package/.agents/skills/gstack/plan-eng-review/SKILL.md.tmpl +279 -0
  149. package/.agents/skills/gstack/qa/SKILL.md +1016 -0
  150. package/.agents/skills/gstack/qa/SKILL.md.tmpl +316 -0
  151. package/.agents/skills/gstack/qa/references/issue-taxonomy.md +85 -0
  152. package/.agents/skills/gstack/qa/templates/qa-report-template.md +126 -0
  153. package/.agents/skills/gstack/qa-only/SKILL.md +633 -0
  154. package/.agents/skills/gstack/qa-only/SKILL.md.tmpl +101 -0
  155. package/.agents/skills/gstack/retro/SKILL.md +1072 -0
  156. package/.agents/skills/gstack/retro/SKILL.md.tmpl +833 -0
  157. package/.agents/skills/gstack/review/SKILL.md +849 -0
  158. package/.agents/skills/gstack/review/SKILL.md.tmpl +259 -0
  159. package/.agents/skills/gstack/review/TODOS-format.md +62 -0
  160. package/.agents/skills/gstack/review/checklist.md +190 -0
  161. package/.agents/skills/gstack/review/design-checklist.md +132 -0
  162. package/.agents/skills/gstack/review/greptile-triage.md +220 -0
  163. package/.agents/skills/gstack/scripts/analytics.ts +190 -0
  164. package/.agents/skills/gstack/scripts/dev-skill.ts +82 -0
  165. package/.agents/skills/gstack/scripts/eval-compare.ts +96 -0
  166. package/.agents/skills/gstack/scripts/eval-list.ts +116 -0
  167. package/.agents/skills/gstack/scripts/eval-select.ts +86 -0
  168. package/.agents/skills/gstack/scripts/eval-summary.ts +187 -0
  169. package/.agents/skills/gstack/scripts/eval-watch.ts +172 -0
  170. package/.agents/skills/gstack/scripts/gen-skill-docs.ts +2414 -0
  171. package/.agents/skills/gstack/scripts/skill-check.ts +167 -0
  172. package/.agents/skills/gstack/setup +269 -0
  173. package/.agents/skills/gstack/setup-browser-cookies/SKILL.md +330 -0
  174. package/.agents/skills/gstack/setup-browser-cookies/SKILL.md.tmpl +74 -0
  175. package/.agents/skills/gstack/setup-deploy/SKILL.md +459 -0
  176. package/.agents/skills/gstack/setup-deploy/SKILL.md.tmpl +220 -0
  177. package/.agents/skills/gstack/ship/SKILL.md +1457 -0
  178. package/.agents/skills/gstack/ship/SKILL.md.tmpl +528 -0
  179. package/.agents/skills/gstack/supabase/config.sh +10 -0
  180. package/.agents/skills/gstack/supabase/functions/community-pulse/index.ts +59 -0
  181. package/.agents/skills/gstack/supabase/functions/telemetry-ingest/index.ts +135 -0
  182. package/.agents/skills/gstack/supabase/functions/update-check/index.ts +37 -0
  183. package/.agents/skills/gstack/supabase/migrations/001_telemetry.sql +89 -0
  184. package/.agents/skills/gstack/test/analytics.test.ts +277 -0
  185. package/.agents/skills/gstack/test/codex-e2e.test.ts +197 -0
  186. package/.agents/skills/gstack/test/fixtures/coverage-audit-fixture.ts +76 -0
  187. package/.agents/skills/gstack/test/fixtures/eval-baselines.json +7 -0
  188. package/.agents/skills/gstack/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
  189. package/.agents/skills/gstack/test/fixtures/qa-eval-ground-truth.json +43 -0
  190. package/.agents/skills/gstack/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
  191. package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.css +86 -0
  192. package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.html +41 -0
  193. package/.agents/skills/gstack/test/fixtures/review-eval-enum-diff.rb +30 -0
  194. package/.agents/skills/gstack/test/fixtures/review-eval-enum.rb +27 -0
  195. package/.agents/skills/gstack/test/fixtures/review-eval-vuln.rb +14 -0
  196. package/.agents/skills/gstack/test/gemini-e2e.test.ts +173 -0
  197. package/.agents/skills/gstack/test/gen-skill-docs.test.ts +1049 -0
  198. package/.agents/skills/gstack/test/global-discover.test.ts +187 -0
  199. package/.agents/skills/gstack/test/helpers/codex-session-runner.ts +282 -0
  200. package/.agents/skills/gstack/test/helpers/e2e-helpers.ts +239 -0
  201. package/.agents/skills/gstack/test/helpers/eval-store.test.ts +548 -0
  202. package/.agents/skills/gstack/test/helpers/eval-store.ts +689 -0
  203. package/.agents/skills/gstack/test/helpers/gemini-session-runner.test.ts +104 -0
  204. package/.agents/skills/gstack/test/helpers/gemini-session-runner.ts +201 -0
  205. package/.agents/skills/gstack/test/helpers/llm-judge.ts +130 -0
  206. package/.agents/skills/gstack/test/helpers/observability.test.ts +283 -0
  207. package/.agents/skills/gstack/test/helpers/session-runner.test.ts +96 -0
  208. package/.agents/skills/gstack/test/helpers/session-runner.ts +357 -0
  209. package/.agents/skills/gstack/test/helpers/skill-parser.ts +206 -0
  210. package/.agents/skills/gstack/test/helpers/touchfiles.ts +260 -0
  211. package/.agents/skills/gstack/test/hook-scripts.test.ts +373 -0
  212. package/.agents/skills/gstack/test/skill-e2e-browse.test.ts +293 -0
  213. package/.agents/skills/gstack/test/skill-e2e-deploy.test.ts +279 -0
  214. package/.agents/skills/gstack/test/skill-e2e-design.test.ts +614 -0
  215. package/.agents/skills/gstack/test/skill-e2e-plan.test.ts +538 -0
  216. package/.agents/skills/gstack/test/skill-e2e-qa-bugs.test.ts +194 -0
  217. package/.agents/skills/gstack/test/skill-e2e-qa-workflow.test.ts +412 -0
  218. package/.agents/skills/gstack/test/skill-e2e-review.test.ts +535 -0
  219. package/.agents/skills/gstack/test/skill-e2e-workflow.test.ts +586 -0
  220. package/.agents/skills/gstack/test/skill-e2e.test.ts +3325 -0
  221. package/.agents/skills/gstack/test/skill-llm-eval.test.ts +787 -0
  222. package/.agents/skills/gstack/test/skill-parser.test.ts +179 -0
  223. package/.agents/skills/gstack/test/skill-routing-e2e.test.ts +605 -0
  224. package/.agents/skills/gstack/test/skill-validation.test.ts +1520 -0
  225. package/.agents/skills/gstack/test/telemetry.test.ts +278 -0
  226. package/.agents/skills/gstack/test/touchfiles.test.ts +262 -0
  227. package/.agents/skills/gstack/unfreeze/SKILL.md +40 -0
  228. package/.agents/skills/gstack/unfreeze/SKILL.md.tmpl +38 -0
  229. package/README.md +12 -7
  230. package/README_KO.md +12 -6
  231. package/package.json +3 -2
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Meta commands — tabs, server control, screenshots, chain, diff, snapshot
3
+ */
4
+
5
+ import type { BrowserManager } from './browser-manager';
6
+ import { handleSnapshot } from './snapshot';
7
+ import { getCleanText } from './read-commands';
8
+ import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
9
+ import { validateNavigationUrl } from './url-validation';
10
+ import * as Diff from 'diff';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { TEMP_DIR, isPathWithin } from './platform';
14
+
15
+ // Security: Path validation to prevent path traversal attacks
16
+ const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()];
17
+
18
+ export function validateOutputPath(filePath: string): void {
19
+ const resolved = path.resolve(filePath);
20
+ const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(resolved, dir));
21
+ if (!isSafe) {
22
+ throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
23
+ }
24
+ }
25
+
26
+ export async function handleMetaCommand(
27
+ command: string,
28
+ args: string[],
29
+ bm: BrowserManager,
30
+ shutdown: () => Promise<void> | void
31
+ ): Promise<string> {
32
+ switch (command) {
33
+ // ─── Tabs ──────────────────────────────────────────
34
+ case 'tabs': {
35
+ const tabs = await bm.getTabListWithTitles();
36
+ return tabs.map(t =>
37
+ `${t.active ? '→ ' : ' '}[${t.id}] ${t.title || '(untitled)'} — ${t.url}`
38
+ ).join('\n');
39
+ }
40
+
41
+ case 'tab': {
42
+ const id = parseInt(args[0], 10);
43
+ if (isNaN(id)) throw new Error('Usage: browse tab <id>');
44
+ bm.switchTab(id);
45
+ return `Switched to tab ${id}`;
46
+ }
47
+
48
+ case 'newtab': {
49
+ const url = args[0];
50
+ const id = await bm.newTab(url);
51
+ return `Opened tab ${id}${url ? ` → ${url}` : ''}`;
52
+ }
53
+
54
+ case 'closetab': {
55
+ const id = args[0] ? parseInt(args[0], 10) : undefined;
56
+ await bm.closeTab(id);
57
+ return `Closed tab${id ? ` ${id}` : ''}`;
58
+ }
59
+
60
+ // ─── Server Control ────────────────────────────────
61
+ case 'status': {
62
+ const page = bm.getPage();
63
+ const tabs = bm.getTabCount();
64
+ return [
65
+ `Status: healthy`,
66
+ `URL: ${page.url()}`,
67
+ `Tabs: ${tabs}`,
68
+ `PID: ${process.pid}`,
69
+ ].join('\n');
70
+ }
71
+
72
+ case 'url': {
73
+ return bm.getCurrentUrl();
74
+ }
75
+
76
+ case 'stop': {
77
+ await shutdown();
78
+ return 'Server stopped';
79
+ }
80
+
81
+ case 'restart': {
82
+ // Signal that we want a restart — the CLI will detect exit and restart
83
+ console.log('[browse] Restart requested. Exiting for CLI to restart.');
84
+ await shutdown();
85
+ return 'Restarting...';
86
+ }
87
+
88
+ // ─── Visual ────────────────────────────────────────
89
+ case 'screenshot': {
90
+ // Parse priority: flags (--viewport, --clip) → selector (@ref, CSS) → output path
91
+ const page = bm.getPage();
92
+ let outputPath = `${TEMP_DIR}/browse-screenshot.png`;
93
+ let clipRect: { x: number; y: number; width: number; height: number } | undefined;
94
+ let targetSelector: string | undefined;
95
+ let viewportOnly = false;
96
+
97
+ const remaining: string[] = [];
98
+ for (let i = 0; i < args.length; i++) {
99
+ if (args[i] === '--viewport') {
100
+ viewportOnly = true;
101
+ } else if (args[i] === '--clip') {
102
+ const coords = args[++i];
103
+ if (!coords) throw new Error('Usage: screenshot --clip x,y,w,h [path]');
104
+ const parts = coords.split(',').map(Number);
105
+ if (parts.length !== 4 || parts.some(isNaN))
106
+ throw new Error('Usage: screenshot --clip x,y,width,height — all must be numbers');
107
+ clipRect = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
108
+ } else if (args[i].startsWith('--')) {
109
+ throw new Error(`Unknown screenshot flag: ${args[i]}`);
110
+ } else {
111
+ remaining.push(args[i]);
112
+ }
113
+ }
114
+
115
+ // Separate target (selector/@ref) from output path
116
+ for (const arg of remaining) {
117
+ if (arg.startsWith('@e') || arg.startsWith('@c') || arg.startsWith('.') || arg.startsWith('#') || arg.includes('[')) {
118
+ targetSelector = arg;
119
+ } else {
120
+ outputPath = arg;
121
+ }
122
+ }
123
+
124
+ validateOutputPath(outputPath);
125
+
126
+ if (clipRect && targetSelector) {
127
+ throw new Error('Cannot use --clip with a selector/ref — choose one');
128
+ }
129
+ if (viewportOnly && clipRect) {
130
+ throw new Error('Cannot use --viewport with --clip — choose one');
131
+ }
132
+
133
+ if (targetSelector) {
134
+ const resolved = await bm.resolveRef(targetSelector);
135
+ const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
136
+ await locator.screenshot({ path: outputPath, timeout: 5000 });
137
+ return `Screenshot saved (element): ${outputPath}`;
138
+ }
139
+
140
+ if (clipRect) {
141
+ await page.screenshot({ path: outputPath, clip: clipRect });
142
+ return `Screenshot saved (clip ${clipRect.x},${clipRect.y},${clipRect.width},${clipRect.height}): ${outputPath}`;
143
+ }
144
+
145
+ await page.screenshot({ path: outputPath, fullPage: !viewportOnly });
146
+ return `Screenshot saved${viewportOnly ? ' (viewport)' : ''}: ${outputPath}`;
147
+ }
148
+
149
+ case 'pdf': {
150
+ const page = bm.getPage();
151
+ const pdfPath = args[0] || `${TEMP_DIR}/browse-page.pdf`;
152
+ validateOutputPath(pdfPath);
153
+ await page.pdf({ path: pdfPath, format: 'A4' });
154
+ return `PDF saved: ${pdfPath}`;
155
+ }
156
+
157
+ case 'responsive': {
158
+ const page = bm.getPage();
159
+ const prefix = args[0] || `${TEMP_DIR}/browse-responsive`;
160
+ validateOutputPath(prefix);
161
+ const viewports = [
162
+ { name: 'mobile', width: 375, height: 812 },
163
+ { name: 'tablet', width: 768, height: 1024 },
164
+ { name: 'desktop', width: 1280, height: 720 },
165
+ ];
166
+ const originalViewport = page.viewportSize();
167
+ const results: string[] = [];
168
+
169
+ for (const vp of viewports) {
170
+ await page.setViewportSize({ width: vp.width, height: vp.height });
171
+ const path = `${prefix}-${vp.name}.png`;
172
+ await page.screenshot({ path, fullPage: true });
173
+ results.push(`${vp.name} (${vp.width}x${vp.height}): ${path}`);
174
+ }
175
+
176
+ // Restore original viewport
177
+ if (originalViewport) {
178
+ await page.setViewportSize(originalViewport);
179
+ }
180
+
181
+ return results.join('\n');
182
+ }
183
+
184
+ // ─── Chain ─────────────────────────────────────────
185
+ case 'chain': {
186
+ // Read JSON array from args[0] (if provided) or expect it was passed as body
187
+ const jsonStr = args[0];
188
+ if (!jsonStr) throw new Error('Usage: echo \'[["goto","url"],["text"]]\' | browse chain');
189
+
190
+ let commands: string[][];
191
+ try {
192
+ commands = JSON.parse(jsonStr);
193
+ } catch {
194
+ throw new Error('Invalid JSON. Expected: [["command", "arg1", "arg2"], ...]');
195
+ }
196
+
197
+ if (!Array.isArray(commands)) throw new Error('Expected JSON array of commands');
198
+
199
+ const results: string[] = [];
200
+ const { handleReadCommand } = await import('./read-commands');
201
+ const { handleWriteCommand } = await import('./write-commands');
202
+
203
+ for (const cmd of commands) {
204
+ const [name, ...cmdArgs] = cmd;
205
+ try {
206
+ let result: string;
207
+ if (WRITE_COMMANDS.has(name)) result = await handleWriteCommand(name, cmdArgs, bm);
208
+ else if (READ_COMMANDS.has(name)) result = await handleReadCommand(name, cmdArgs, bm);
209
+ else if (META_COMMANDS.has(name)) result = await handleMetaCommand(name, cmdArgs, bm, shutdown);
210
+ else throw new Error(`Unknown command: ${name}`);
211
+ results.push(`[${name}] ${result}`);
212
+ } catch (err: any) {
213
+ results.push(`[${name}] ERROR: ${err.message}`);
214
+ }
215
+ }
216
+
217
+ return results.join('\n\n');
218
+ }
219
+
220
+ // ─── Diff ──────────────────────────────────────────
221
+ case 'diff': {
222
+ const [url1, url2] = args;
223
+ if (!url1 || !url2) throw new Error('Usage: browse diff <url1> <url2>');
224
+
225
+ const page = bm.getPage();
226
+ await validateNavigationUrl(url1);
227
+ await page.goto(url1, { waitUntil: 'domcontentloaded', timeout: 15000 });
228
+ const text1 = await getCleanText(page);
229
+
230
+ await validateNavigationUrl(url2);
231
+ await page.goto(url2, { waitUntil: 'domcontentloaded', timeout: 15000 });
232
+ const text2 = await getCleanText(page);
233
+
234
+ const changes = Diff.diffLines(text1, text2);
235
+ const output: string[] = [`--- ${url1}`, `+++ ${url2}`, ''];
236
+
237
+ for (const part of changes) {
238
+ const prefix = part.added ? '+' : part.removed ? '-' : ' ';
239
+ const lines = part.value.split('\n').filter(l => l.length > 0);
240
+ for (const line of lines) {
241
+ output.push(`${prefix} ${line}`);
242
+ }
243
+ }
244
+
245
+ return output.join('\n');
246
+ }
247
+
248
+ // ─── Snapshot ─────────────────────────────────────
249
+ case 'snapshot': {
250
+ return await handleSnapshot(args, bm);
251
+ }
252
+
253
+ // ─── Handoff ────────────────────────────────────
254
+ case 'handoff': {
255
+ const message = args.join(' ') || 'User takeover requested';
256
+ return await bm.handoff(message);
257
+ }
258
+
259
+ case 'resume': {
260
+ bm.resume();
261
+ // Re-snapshot to capture current page state after human interaction
262
+ const snapshot = await handleSnapshot(['-i'], bm);
263
+ return `RESUMED\n${snapshot}`;
264
+ }
265
+
266
+ default:
267
+ throw new Error(`Unknown meta command: ${command}`);
268
+ }
269
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Cross-platform constants for gstack browse.
3
+ *
4
+ * On macOS/Linux: TEMP_DIR = '/tmp', path.sep = '/' — identical to hardcoded values.
5
+ * On Windows: TEMP_DIR = os.tmpdir(), path.sep = '\\' — correct Windows behavior.
6
+ */
7
+
8
+ import * as os from 'os';
9
+ import * as path from 'path';
10
+
11
+ export const IS_WINDOWS = process.platform === 'win32';
12
+ export const TEMP_DIR = IS_WINDOWS ? os.tmpdir() : '/tmp';
13
+
14
+ /** Check if resolvedPath is within dir, using platform-aware separators. */
15
+ export function isPathWithin(resolvedPath: string, dir: string): boolean {
16
+ return resolvedPath === dir || resolvedPath.startsWith(dir + path.sep);
17
+ }
@@ -0,0 +1,335 @@
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 } 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
+ const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()];
41
+
42
+ export function validateReadPath(filePath: string): void {
43
+ if (path.isAbsolute(filePath)) {
44
+ const resolved = path.resolve(filePath);
45
+ const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(resolved, dir));
46
+ if (!isSafe) {
47
+ throw new Error(`Absolute path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
48
+ }
49
+ }
50
+ const normalized = path.normalize(filePath);
51
+ if (normalized.includes('..')) {
52
+ throw new Error('Path traversal sequences (..) are not allowed');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Extract clean text from a page (strips script/style/noscript/svg).
58
+ * Exported for DRY reuse in meta-commands (diff).
59
+ */
60
+ export async function getCleanText(page: Page): Promise<string> {
61
+ return await page.evaluate(() => {
62
+ const body = document.body;
63
+ if (!body) return '';
64
+ const clone = body.cloneNode(true) as HTMLElement;
65
+ clone.querySelectorAll('script, style, noscript, svg').forEach(el => el.remove());
66
+ return clone.innerText
67
+ .split('\n')
68
+ .map(line => line.trim())
69
+ .filter(line => line.length > 0)
70
+ .join('\n');
71
+ });
72
+ }
73
+
74
+ export async function handleReadCommand(
75
+ command: string,
76
+ args: string[],
77
+ bm: BrowserManager
78
+ ): Promise<string> {
79
+ const page = bm.getPage();
80
+
81
+ switch (command) {
82
+ case 'text': {
83
+ return await getCleanText(page);
84
+ }
85
+
86
+ case 'html': {
87
+ const selector = args[0];
88
+ if (selector) {
89
+ const resolved = await bm.resolveRef(selector);
90
+ if ('locator' in resolved) {
91
+ return await resolved.locator.innerHTML({ timeout: 5000 });
92
+ }
93
+ return await page.innerHTML(resolved.selector);
94
+ }
95
+ return await page.content();
96
+ }
97
+
98
+ case 'links': {
99
+ const links = await page.evaluate(() =>
100
+ [...document.querySelectorAll('a[href]')].map(a => ({
101
+ text: a.textContent?.trim().slice(0, 120) || '',
102
+ href: (a as HTMLAnchorElement).href,
103
+ })).filter(l => l.text && l.href)
104
+ );
105
+ return links.map(l => `${l.text} → ${l.href}`).join('\n');
106
+ }
107
+
108
+ case 'forms': {
109
+ const forms = await page.evaluate(() => {
110
+ return [...document.querySelectorAll('form')].map((form, i) => {
111
+ const fields = [...form.querySelectorAll('input, select, textarea')].map(el => {
112
+ const input = el as HTMLInputElement;
113
+ return {
114
+ tag: el.tagName.toLowerCase(),
115
+ type: input.type || undefined,
116
+ name: input.name || undefined,
117
+ id: input.id || undefined,
118
+ placeholder: input.placeholder || undefined,
119
+ required: input.required || undefined,
120
+ value: input.type === 'password' ? '[redacted]' : (input.value || undefined),
121
+ options: el.tagName === 'SELECT'
122
+ ? [...(el as HTMLSelectElement).options].map(o => ({ value: o.value, text: o.text }))
123
+ : undefined,
124
+ };
125
+ });
126
+ return {
127
+ index: i,
128
+ action: form.action || undefined,
129
+ method: form.method || 'get',
130
+ id: form.id || undefined,
131
+ fields,
132
+ };
133
+ });
134
+ });
135
+ return JSON.stringify(forms, null, 2);
136
+ }
137
+
138
+ case 'accessibility': {
139
+ const snapshot = await page.locator("body").ariaSnapshot();
140
+ return snapshot;
141
+ }
142
+
143
+ case 'js': {
144
+ const expr = args[0];
145
+ if (!expr) throw new Error('Usage: browse js <expression>');
146
+ const wrapped = wrapForEvaluate(expr);
147
+ const result = await page.evaluate(wrapped);
148
+ return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
149
+ }
150
+
151
+ case 'eval': {
152
+ const filePath = args[0];
153
+ if (!filePath) throw new Error('Usage: browse eval <js-file>');
154
+ validateReadPath(filePath);
155
+ if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
156
+ const code = fs.readFileSync(filePath, 'utf-8');
157
+ const wrapped = wrapForEvaluate(code);
158
+ const result = await page.evaluate(wrapped);
159
+ return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
160
+ }
161
+
162
+ case 'css': {
163
+ const [selector, property] = args;
164
+ if (!selector || !property) throw new Error('Usage: browse css <selector> <property>');
165
+ const resolved = await bm.resolveRef(selector);
166
+ if ('locator' in resolved) {
167
+ const value = await resolved.locator.evaluate(
168
+ (el, prop) => getComputedStyle(el).getPropertyValue(prop),
169
+ property
170
+ );
171
+ return value;
172
+ }
173
+ const value = await page.evaluate(
174
+ ([sel, prop]) => {
175
+ const el = document.querySelector(sel);
176
+ if (!el) return `Element not found: ${sel}`;
177
+ return getComputedStyle(el).getPropertyValue(prop);
178
+ },
179
+ [resolved.selector, property]
180
+ );
181
+ return value;
182
+ }
183
+
184
+ case 'attrs': {
185
+ const selector = args[0];
186
+ if (!selector) throw new Error('Usage: browse attrs <selector>');
187
+ const resolved = await bm.resolveRef(selector);
188
+ if ('locator' in resolved) {
189
+ const attrs = await resolved.locator.evaluate((el) => {
190
+ const result: Record<string, string> = {};
191
+ for (const attr of el.attributes) {
192
+ result[attr.name] = attr.value;
193
+ }
194
+ return result;
195
+ });
196
+ return JSON.stringify(attrs, null, 2);
197
+ }
198
+ const attrs = await page.evaluate((sel) => {
199
+ const el = document.querySelector(sel);
200
+ if (!el) return `Element not found: ${sel}`;
201
+ const result: Record<string, string> = {};
202
+ for (const attr of el.attributes) {
203
+ result[attr.name] = attr.value;
204
+ }
205
+ return result;
206
+ }, resolved.selector);
207
+ return typeof attrs === 'string' ? attrs : JSON.stringify(attrs, null, 2);
208
+ }
209
+
210
+ case 'console': {
211
+ if (args[0] === '--clear') {
212
+ consoleBuffer.clear();
213
+ return 'Console buffer cleared.';
214
+ }
215
+ const entries = args[0] === '--errors'
216
+ ? consoleBuffer.toArray().filter(e => e.level === 'error' || e.level === 'warning')
217
+ : consoleBuffer.toArray();
218
+ if (entries.length === 0) return args[0] === '--errors' ? '(no console errors)' : '(no console messages)';
219
+ return entries.map(e =>
220
+ `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
221
+ ).join('\n');
222
+ }
223
+
224
+ case 'network': {
225
+ if (args[0] === '--clear') {
226
+ networkBuffer.clear();
227
+ return 'Network buffer cleared.';
228
+ }
229
+ if (networkBuffer.length === 0) return '(no network requests)';
230
+ return networkBuffer.toArray().map(e =>
231
+ `${e.method} ${e.url} → ${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)`
232
+ ).join('\n');
233
+ }
234
+
235
+ case 'dialog': {
236
+ if (args[0] === '--clear') {
237
+ dialogBuffer.clear();
238
+ return 'Dialog buffer cleared.';
239
+ }
240
+ if (dialogBuffer.length === 0) return '(no dialogs captured)';
241
+ return dialogBuffer.toArray().map(e =>
242
+ `[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ''}`
243
+ ).join('\n');
244
+ }
245
+
246
+ case 'is': {
247
+ const property = args[0];
248
+ const selector = args[1];
249
+ if (!property || !selector) throw new Error('Usage: browse is <property> <selector>\nProperties: visible, hidden, enabled, disabled, checked, editable, focused');
250
+
251
+ const resolved = await bm.resolveRef(selector);
252
+ let locator;
253
+ if ('locator' in resolved) {
254
+ locator = resolved.locator;
255
+ } else {
256
+ locator = page.locator(resolved.selector);
257
+ }
258
+
259
+ switch (property) {
260
+ case 'visible': return String(await locator.isVisible());
261
+ case 'hidden': return String(await locator.isHidden());
262
+ case 'enabled': return String(await locator.isEnabled());
263
+ case 'disabled': return String(await locator.isDisabled());
264
+ case 'checked': return String(await locator.isChecked());
265
+ case 'editable': return String(await locator.isEditable());
266
+ case 'focused': {
267
+ const isFocused = await locator.evaluate(
268
+ (el) => el === document.activeElement
269
+ );
270
+ return String(isFocused);
271
+ }
272
+ default:
273
+ throw new Error(`Unknown property: ${property}. Use: visible, hidden, enabled, disabled, checked, editable, focused`);
274
+ }
275
+ }
276
+
277
+ case 'cookies': {
278
+ const cookies = await page.context().cookies();
279
+ return JSON.stringify(cookies, null, 2);
280
+ }
281
+
282
+ case 'storage': {
283
+ if (args[0] === 'set' && args[1]) {
284
+ const key = args[1];
285
+ const value = args[2] || '';
286
+ await page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]);
287
+ return `Set localStorage["${key}"]`;
288
+ }
289
+ const storage = await page.evaluate(() => ({
290
+ localStorage: { ...localStorage },
291
+ sessionStorage: { ...sessionStorage },
292
+ }));
293
+ // Redact values that look like secrets (tokens, keys, passwords, JWTs)
294
+ const SENSITIVE_KEY = /(^|[_.-])(token|secret|key|password|credential|auth|jwt|session|csrf)($|[_.-])|api.?key/i;
295
+ 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_)/;
296
+ const redacted = JSON.parse(JSON.stringify(storage));
297
+ for (const storeType of ['localStorage', 'sessionStorage'] as const) {
298
+ const store = redacted[storeType];
299
+ if (!store) continue;
300
+ for (const [key, value] of Object.entries(store)) {
301
+ if (typeof value !== 'string') continue;
302
+ if (SENSITIVE_KEY.test(key) || SENSITIVE_VALUE.test(value)) {
303
+ store[key] = `[REDACTED — ${value.length} chars]`;
304
+ }
305
+ }
306
+ }
307
+ return JSON.stringify(redacted, null, 2);
308
+ }
309
+
310
+ case 'perf': {
311
+ const timings = await page.evaluate(() => {
312
+ const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
313
+ if (!nav) return 'No navigation timing data available.';
314
+ return {
315
+ dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart),
316
+ tcp: Math.round(nav.connectEnd - nav.connectStart),
317
+ ssl: Math.round(nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0),
318
+ ttfb: Math.round(nav.responseStart - nav.requestStart),
319
+ download: Math.round(nav.responseEnd - nav.responseStart),
320
+ domParse: Math.round(nav.domInteractive - nav.responseEnd),
321
+ domReady: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
322
+ load: Math.round(nav.loadEventEnd - nav.startTime),
323
+ total: Math.round(nav.loadEventEnd - nav.startTime),
324
+ };
325
+ });
326
+ if (typeof timings === 'string') return timings;
327
+ return Object.entries(timings)
328
+ .map(([k, v]) => `${k.padEnd(12)} ${v}ms`)
329
+ .join('\n');
330
+ }
331
+
332
+ default:
333
+ throw new Error(`Unknown read command: ${command}`);
334
+ }
335
+ }