@runchr/gstack-antigravity 0.1.1 → 0.1.3

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 (229) 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/package.json +2 -1
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # Output the remote slug (owner-repo) for the current git repo.
3
+ # Used by SKILL.md files to derive project-specific paths in ~/.gstack/projects/.
4
+ set -e
5
+ URL=$(git remote get-url origin 2>/dev/null || true)
6
+ if [ -n "$URL" ]; then
7
+ # Strip trailing .git if present, then extract owner/repo
8
+ URL="${URL%.git}"
9
+ # Handle both SSH (git@host:owner/repo) and HTTPS (https://host/owner/repo)
10
+ OWNER_REPO=$(echo "$URL" | sed -E 's#.*[:/]([^/]+)/([^/]+)$#\1-\2#')
11
+ echo "$OWNER_REPO"
12
+ else
13
+ basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
14
+ fi
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Build a Node.js-compatible server bundle for Windows.
3
+ #
4
+ # On Windows, Bun can't launch or connect to Playwright's Chromium
5
+ # (oven-sh/bun#4253, #9911). This script produces a server bundle
6
+ # that runs under Node.js with Bun API polyfills.
7
+
8
+ set -e
9
+
10
+ GSTACK_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
11
+ SRC_DIR="$GSTACK_DIR/browse/src"
12
+ DIST_DIR="$GSTACK_DIR/browse/dist"
13
+
14
+ echo "Building Node-compatible server bundle..."
15
+
16
+ # Step 1: Transpile server.ts to a single .mjs bundle (externalize runtime deps)
17
+ bun build "$SRC_DIR/server.ts" \
18
+ --target=node \
19
+ --outfile "$DIST_DIR/server-node.mjs" \
20
+ --external playwright \
21
+ --external playwright-core \
22
+ --external diff \
23
+ --external "bun:sqlite"
24
+
25
+ # Step 2: Post-process
26
+ # Replace import.meta.dir with a resolvable reference
27
+ perl -pi -e 's/import\.meta\.dir/__browseNodeSrcDir/g' "$DIST_DIR/server-node.mjs"
28
+ # Stub out bun:sqlite (macOS-only cookie import, not needed on Windows)
29
+ perl -pi -e 's|import { Database } from "bun:sqlite";|const Database = null; // bun:sqlite stubbed on Node|g' "$DIST_DIR/server-node.mjs"
30
+
31
+ # Step 3: Create the final file with polyfill header injected after the first line
32
+ {
33
+ head -1 "$DIST_DIR/server-node.mjs"
34
+ echo '// ── Windows Node.js compatibility (auto-generated) ──'
35
+ echo 'import { fileURLToPath as _ftp } from "node:url";'
36
+ echo 'import { dirname as _dn } from "node:path";'
37
+ echo 'const __browseNodeSrcDir = _dn(_dn(_ftp(import.meta.url))) + "/src";'
38
+ echo '{ const _r = createRequire(import.meta.url); _r("./bun-polyfill.cjs"); }'
39
+ echo '// ── end compatibility ──'
40
+ tail -n +2 "$DIST_DIR/server-node.mjs"
41
+ } > "$DIST_DIR/server-node.tmp.mjs"
42
+
43
+ mv "$DIST_DIR/server-node.tmp.mjs" "$DIST_DIR/server-node.mjs"
44
+
45
+ # Step 4: Copy polyfill to dist/
46
+ cp "$SRC_DIR/bun-polyfill.cjs" "$DIST_DIR/bun-polyfill.cjs"
47
+
48
+ echo "Node server bundle ready: $DIST_DIR/server-node.mjs"
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Browser lifecycle manager
3
+ *
4
+ * Chromium crash handling:
5
+ * browser.on('disconnected') → log error → process.exit(1)
6
+ * CLI detects dead server → auto-restarts on next command
7
+ * We do NOT try to self-heal — don't hide failure.
8
+ *
9
+ * Dialog handling:
10
+ * page.on('dialog') → auto-accept by default → store in dialog buffer
11
+ * Prevents browser lockup from alert/confirm/prompt
12
+ *
13
+ * Context recreation (useragent):
14
+ * recreateContext() saves cookies/storage/URLs, creates new context,
15
+ * restores state. Falls back to clean slate on any failure.
16
+ */
17
+
18
+ import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page, type Locator, type Cookie } from 'playwright';
19
+ import { addConsoleEntry, addNetworkEntry, addDialogEntry, networkBuffer, type DialogEntry } from './buffers';
20
+ import { validateNavigationUrl } from './url-validation';
21
+
22
+ export interface RefEntry {
23
+ locator: Locator;
24
+ role: string;
25
+ name: string;
26
+ }
27
+
28
+ export interface BrowserState {
29
+ cookies: Cookie[];
30
+ pages: Array<{
31
+ url: string;
32
+ isActive: boolean;
33
+ storage: { localStorage: Record<string, string>; sessionStorage: Record<string, string> } | null;
34
+ }>;
35
+ }
36
+
37
+ export class BrowserManager {
38
+ private browser: Browser | null = null;
39
+ private context: BrowserContext | null = null;
40
+ private pages: Map<number, Page> = new Map();
41
+ private activeTabId: number = 0;
42
+ private nextTabId: number = 1;
43
+ private extraHeaders: Record<string, string> = {};
44
+ private customUserAgent: string | null = null;
45
+
46
+ /** Server port — set after server starts, used by cookie-import-browser command */
47
+ public serverPort: number = 0;
48
+
49
+ // ─── Ref Map (snapshot → @e1, @e2, @c1, @c2, ...) ────────
50
+ private refMap: Map<string, RefEntry> = new Map();
51
+
52
+ // ─── Snapshot Diffing ─────────────────────────────────────
53
+ // NOT cleared on navigation — it's a text baseline for diffing
54
+ private lastSnapshot: string | null = null;
55
+
56
+ // ─── Dialog Handling ──────────────────────────────────────
57
+ private dialogAutoAccept: boolean = true;
58
+ private dialogPromptText: string | null = null;
59
+
60
+ // ─── Handoff State ─────────────────────────────────────────
61
+ private isHeaded: boolean = false;
62
+ private consecutiveFailures: number = 0;
63
+
64
+ async launch() {
65
+ this.browser = await chromium.launch({ headless: true });
66
+
67
+ // Chromium crash → exit with clear message
68
+ this.browser.on('disconnected', () => {
69
+ console.error('[browse] FATAL: Chromium process crashed or was killed. Server exiting.');
70
+ console.error('[browse] Console/network logs flushed to .gstack/browse-*.log');
71
+ process.exit(1);
72
+ });
73
+
74
+ const contextOptions: BrowserContextOptions = {
75
+ viewport: { width: 1280, height: 720 },
76
+ };
77
+ if (this.customUserAgent) {
78
+ contextOptions.userAgent = this.customUserAgent;
79
+ }
80
+ this.context = await this.browser.newContext(contextOptions);
81
+
82
+ if (Object.keys(this.extraHeaders).length > 0) {
83
+ await this.context.setExtraHTTPHeaders(this.extraHeaders);
84
+ }
85
+
86
+ // Create first tab
87
+ await this.newTab();
88
+ }
89
+
90
+ async close() {
91
+ if (this.browser) {
92
+ // Remove disconnect handler to avoid exit during intentional close
93
+ this.browser.removeAllListeners('disconnected');
94
+ // Timeout: headed browser.close() can hang on macOS
95
+ await Promise.race([
96
+ this.browser.close(),
97
+ new Promise(resolve => setTimeout(resolve, 5000)),
98
+ ]).catch(() => {});
99
+ this.browser = null;
100
+ }
101
+ }
102
+
103
+ /** Health check — verifies Chromium is connected AND responsive */
104
+ async isHealthy(): Promise<boolean> {
105
+ if (!this.browser || !this.browser.isConnected()) return false;
106
+ try {
107
+ const page = this.pages.get(this.activeTabId);
108
+ if (!page) return true; // connected but no pages — still healthy
109
+ await Promise.race([
110
+ page.evaluate('1'),
111
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000)),
112
+ ]);
113
+ return true;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ // ─── Tab Management ────────────────────────────────────────
120
+ async newTab(url?: string): Promise<number> {
121
+ if (!this.context) throw new Error('Browser not launched');
122
+
123
+ // Validate URL before allocating page to avoid zombie tabs on rejection
124
+ if (url) {
125
+ await validateNavigationUrl(url);
126
+ }
127
+
128
+ const page = await this.context.newPage();
129
+ const id = this.nextTabId++;
130
+ this.pages.set(id, page);
131
+ this.activeTabId = id;
132
+
133
+ // Wire up console/network/dialog capture
134
+ this.wirePageEvents(page);
135
+
136
+ if (url) {
137
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
138
+ }
139
+
140
+ return id;
141
+ }
142
+
143
+ async closeTab(id?: number): Promise<void> {
144
+ const tabId = id ?? this.activeTabId;
145
+ const page = this.pages.get(tabId);
146
+ if (!page) throw new Error(`Tab ${tabId} not found`);
147
+
148
+ await page.close();
149
+ this.pages.delete(tabId);
150
+
151
+ // Switch to another tab if we closed the active one
152
+ if (tabId === this.activeTabId) {
153
+ const remaining = [...this.pages.keys()];
154
+ if (remaining.length > 0) {
155
+ this.activeTabId = remaining[remaining.length - 1];
156
+ } else {
157
+ // No tabs left — create a new blank one
158
+ await this.newTab();
159
+ }
160
+ }
161
+ }
162
+
163
+ switchTab(id: number): void {
164
+ if (!this.pages.has(id)) throw new Error(`Tab ${id} not found`);
165
+ this.activeTabId = id;
166
+ }
167
+
168
+ getTabCount(): number {
169
+ return this.pages.size;
170
+ }
171
+
172
+ async getTabListWithTitles(): Promise<Array<{ id: number; url: string; title: string; active: boolean }>> {
173
+ const tabs: Array<{ id: number; url: string; title: string; active: boolean }> = [];
174
+ for (const [id, page] of this.pages) {
175
+ tabs.push({
176
+ id,
177
+ url: page.url(),
178
+ title: await page.title().catch(() => ''),
179
+ active: id === this.activeTabId,
180
+ });
181
+ }
182
+ return tabs;
183
+ }
184
+
185
+ // ─── Page Access ───────────────────────────────────────────
186
+ getPage(): Page {
187
+ const page = this.pages.get(this.activeTabId);
188
+ if (!page) throw new Error('No active page. Use "browse goto <url>" first.');
189
+ return page;
190
+ }
191
+
192
+ getCurrentUrl(): string {
193
+ try {
194
+ return this.getPage().url();
195
+ } catch {
196
+ return 'about:blank';
197
+ }
198
+ }
199
+
200
+ // ─── Ref Map ──────────────────────────────────────────────
201
+ setRefMap(refs: Map<string, RefEntry>) {
202
+ this.refMap = refs;
203
+ }
204
+
205
+ clearRefs() {
206
+ this.refMap.clear();
207
+ }
208
+
209
+ /**
210
+ * Resolve a selector that may be a @ref (e.g., "@e3", "@c1") or a CSS selector.
211
+ * Returns { locator } for refs or { selector } for CSS selectors.
212
+ */
213
+ async resolveRef(selector: string): Promise<{ locator: Locator } | { selector: string }> {
214
+ if (selector.startsWith('@e') || selector.startsWith('@c')) {
215
+ const ref = selector.slice(1); // "e3" or "c1"
216
+ const entry = this.refMap.get(ref);
217
+ if (!entry) {
218
+ throw new Error(
219
+ `Ref ${selector} not found. Run 'snapshot' to get fresh refs.`
220
+ );
221
+ }
222
+ const count = await entry.locator.count();
223
+ if (count === 0) {
224
+ throw new Error(
225
+ `Ref ${selector} (${entry.role} "${entry.name}") is stale — element no longer exists. ` +
226
+ `Run 'snapshot' for fresh refs.`
227
+ );
228
+ }
229
+ return { locator: entry.locator };
230
+ }
231
+ return { selector };
232
+ }
233
+
234
+ /** Get the ARIA role for a ref selector, or null for CSS selectors / unknown refs. */
235
+ getRefRole(selector: string): string | null {
236
+ if (selector.startsWith('@e') || selector.startsWith('@c')) {
237
+ const entry = this.refMap.get(selector.slice(1));
238
+ return entry?.role ?? null;
239
+ }
240
+ return null;
241
+ }
242
+
243
+ getRefCount(): number {
244
+ return this.refMap.size;
245
+ }
246
+
247
+ // ─── Snapshot Diffing ─────────────────────────────────────
248
+ setLastSnapshot(text: string | null) {
249
+ this.lastSnapshot = text;
250
+ }
251
+
252
+ getLastSnapshot(): string | null {
253
+ return this.lastSnapshot;
254
+ }
255
+
256
+ // ─── Dialog Control ───────────────────────────────────────
257
+ setDialogAutoAccept(accept: boolean) {
258
+ this.dialogAutoAccept = accept;
259
+ }
260
+
261
+ getDialogAutoAccept(): boolean {
262
+ return this.dialogAutoAccept;
263
+ }
264
+
265
+ setDialogPromptText(text: string | null) {
266
+ this.dialogPromptText = text;
267
+ }
268
+
269
+ getDialogPromptText(): string | null {
270
+ return this.dialogPromptText;
271
+ }
272
+
273
+ // ─── Viewport ──────────────────────────────────────────────
274
+ async setViewport(width: number, height: number) {
275
+ await this.getPage().setViewportSize({ width, height });
276
+ }
277
+
278
+ // ─── Extra Headers ─────────────────────────────────────────
279
+ async setExtraHeader(name: string, value: string) {
280
+ this.extraHeaders[name] = value;
281
+ if (this.context) {
282
+ await this.context.setExtraHTTPHeaders(this.extraHeaders);
283
+ }
284
+ }
285
+
286
+ // ─── User Agent ────────────────────────────────────────────
287
+ setUserAgent(ua: string) {
288
+ this.customUserAgent = ua;
289
+ }
290
+
291
+ getUserAgent(): string | null {
292
+ return this.customUserAgent;
293
+ }
294
+
295
+ // ─── State Save/Restore (shared by recreateContext + handoff) ─
296
+ /**
297
+ * Capture browser state: cookies, localStorage, sessionStorage, URLs, active tab.
298
+ * Skips pages that fail storage reads (e.g., already closed).
299
+ */
300
+ async saveState(): Promise<BrowserState> {
301
+ if (!this.context) throw new Error('Browser not launched');
302
+
303
+ const cookies = await this.context.cookies();
304
+ const pages: BrowserState['pages'] = [];
305
+
306
+ for (const [id, page] of this.pages) {
307
+ const url = page.url();
308
+ let storage = null;
309
+ try {
310
+ storage = await page.evaluate(() => ({
311
+ localStorage: { ...localStorage },
312
+ sessionStorage: { ...sessionStorage },
313
+ }));
314
+ } catch {}
315
+ pages.push({
316
+ url: url === 'about:blank' ? '' : url,
317
+ isActive: id === this.activeTabId,
318
+ storage,
319
+ });
320
+ }
321
+
322
+ return { cookies, pages };
323
+ }
324
+
325
+ /**
326
+ * Restore browser state into the current context: cookies, pages, storage.
327
+ * Navigates to saved URLs, restores storage, wires page events.
328
+ * Failures on individual pages are swallowed — partial restore is better than none.
329
+ */
330
+ async restoreState(state: BrowserState): Promise<void> {
331
+ if (!this.context) throw new Error('Browser not launched');
332
+
333
+ // Restore cookies
334
+ if (state.cookies.length > 0) {
335
+ await this.context.addCookies(state.cookies);
336
+ }
337
+
338
+ // Re-create pages
339
+ let activeId: number | null = null;
340
+ for (const saved of state.pages) {
341
+ const page = await this.context.newPage();
342
+ const id = this.nextTabId++;
343
+ this.pages.set(id, page);
344
+ this.wirePageEvents(page);
345
+
346
+ if (saved.url) {
347
+ await page.goto(saved.url, { waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => {});
348
+ }
349
+
350
+ if (saved.storage) {
351
+ try {
352
+ await page.evaluate((s: { localStorage: Record<string, string>; sessionStorage: Record<string, string> }) => {
353
+ if (s.localStorage) {
354
+ for (const [k, v] of Object.entries(s.localStorage)) {
355
+ localStorage.setItem(k, v);
356
+ }
357
+ }
358
+ if (s.sessionStorage) {
359
+ for (const [k, v] of Object.entries(s.sessionStorage)) {
360
+ sessionStorage.setItem(k, v);
361
+ }
362
+ }
363
+ }, saved.storage);
364
+ } catch {}
365
+ }
366
+
367
+ if (saved.isActive) activeId = id;
368
+ }
369
+
370
+ // If no pages were saved, create a blank one
371
+ if (this.pages.size === 0) {
372
+ await this.newTab();
373
+ } else {
374
+ this.activeTabId = activeId ?? [...this.pages.keys()][0];
375
+ }
376
+
377
+ // Clear refs — pages are new, locators are stale
378
+ this.clearRefs();
379
+ }
380
+
381
+ /**
382
+ * Recreate the browser context to apply user agent changes.
383
+ * Saves and restores cookies, localStorage, sessionStorage, and open pages.
384
+ * Falls back to a clean slate on any failure.
385
+ */
386
+ async recreateContext(): Promise<string | null> {
387
+ if (!this.browser || !this.context) {
388
+ throw new Error('Browser not launched');
389
+ }
390
+
391
+ try {
392
+ // 1. Save state
393
+ const state = await this.saveState();
394
+
395
+ // 2. Close old pages and context
396
+ for (const page of this.pages.values()) {
397
+ await page.close().catch(() => {});
398
+ }
399
+ this.pages.clear();
400
+ await this.context.close().catch(() => {});
401
+
402
+ // 3. Create new context with updated settings
403
+ const contextOptions: BrowserContextOptions = {
404
+ viewport: { width: 1280, height: 720 },
405
+ };
406
+ if (this.customUserAgent) {
407
+ contextOptions.userAgent = this.customUserAgent;
408
+ }
409
+ this.context = await this.browser.newContext(contextOptions);
410
+
411
+ if (Object.keys(this.extraHeaders).length > 0) {
412
+ await this.context.setExtraHTTPHeaders(this.extraHeaders);
413
+ }
414
+
415
+ // 4. Restore state
416
+ await this.restoreState(state);
417
+
418
+ return null; // success
419
+ } catch (err: unknown) {
420
+ // Fallback: create a clean context + blank tab
421
+ try {
422
+ this.pages.clear();
423
+ if (this.context) await this.context.close().catch(() => {});
424
+
425
+ const contextOptions: BrowserContextOptions = {
426
+ viewport: { width: 1280, height: 720 },
427
+ };
428
+ if (this.customUserAgent) {
429
+ contextOptions.userAgent = this.customUserAgent;
430
+ }
431
+ this.context = await this.browser!.newContext(contextOptions);
432
+ await this.newTab();
433
+ this.clearRefs();
434
+ } catch {
435
+ // If even the fallback fails, we're in trouble — but browser is still alive
436
+ }
437
+ return `Context recreation failed: ${err instanceof Error ? err.message : String(err)}. Browser reset to blank tab.`;
438
+ }
439
+ }
440
+
441
+ // ─── Handoff: Headless → Headed ─────────────────────────────
442
+ /**
443
+ * Hand off browser control to the user by relaunching in headed mode.
444
+ *
445
+ * Flow (launch-first-close-second for safe rollback):
446
+ * 1. Save state from current headless browser
447
+ * 2. Launch NEW headed browser
448
+ * 3. Restore state into new browser
449
+ * 4. Close OLD headless browser
450
+ * If step 2 fails → return error, headless browser untouched
451
+ */
452
+ async handoff(message: string): Promise<string> {
453
+ if (this.isHeaded) {
454
+ return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`;
455
+ }
456
+ if (!this.browser || !this.context) {
457
+ throw new Error('Browser not launched');
458
+ }
459
+
460
+ // 1. Save state from current browser
461
+ const state = await this.saveState();
462
+ const currentUrl = this.getCurrentUrl();
463
+
464
+ // 2. Launch new headed browser (try-catch — if this fails, headless stays running)
465
+ let newBrowser: Browser;
466
+ try {
467
+ newBrowser = await chromium.launch({ headless: false, timeout: 15000 });
468
+ } catch (err: unknown) {
469
+ const msg = err instanceof Error ? err.message : String(err);
470
+ return `ERROR: Cannot open headed browser — ${msg}. Headless browser still running.`;
471
+ }
472
+
473
+ // 3. Create context and restore state into new headed browser
474
+ try {
475
+ const contextOptions: BrowserContextOptions = {
476
+ viewport: { width: 1280, height: 720 },
477
+ };
478
+ if (this.customUserAgent) {
479
+ contextOptions.userAgent = this.customUserAgent;
480
+ }
481
+ const newContext = await newBrowser.newContext(contextOptions);
482
+
483
+ if (Object.keys(this.extraHeaders).length > 0) {
484
+ await newContext.setExtraHTTPHeaders(this.extraHeaders);
485
+ }
486
+
487
+ // Swap to new browser/context before restoreState (it uses this.context)
488
+ const oldBrowser = this.browser;
489
+ const oldContext = this.context;
490
+
491
+ this.browser = newBrowser;
492
+ this.context = newContext;
493
+ this.pages.clear();
494
+
495
+ // Register crash handler on new browser
496
+ this.browser.on('disconnected', () => {
497
+ console.error('[browse] FATAL: Chromium process crashed or was killed. Server exiting.');
498
+ console.error('[browse] Console/network logs flushed to .gstack/browse-*.log');
499
+ process.exit(1);
500
+ });
501
+
502
+ await this.restoreState(state);
503
+ this.isHeaded = true;
504
+
505
+ // 4. Close old headless browser (fire-and-forget — close() can hang
506
+ // when another Playwright instance is active, so we don't await it)
507
+ oldBrowser.removeAllListeners('disconnected');
508
+ oldBrowser.close().catch(() => {});
509
+
510
+ return [
511
+ `HANDOFF: Browser opened at ${currentUrl}`,
512
+ `MESSAGE: ${message}`,
513
+ `STATUS: Waiting for user. Run 'resume' when done.`,
514
+ ].join('\n');
515
+ } catch (err: unknown) {
516
+ // Restore failed — close the new browser, keep old one
517
+ await newBrowser.close().catch(() => {});
518
+ const msg = err instanceof Error ? err.message : String(err);
519
+ return `ERROR: Handoff failed during state restore — ${msg}. Headless browser still running.`;
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Resume AI control after user handoff.
525
+ * Clears stale refs and resets failure counter.
526
+ * The meta-command handler calls handleSnapshot() after this.
527
+ */
528
+ resume(): void {
529
+ this.clearRefs();
530
+ this.resetFailures();
531
+ }
532
+
533
+ getIsHeaded(): boolean {
534
+ return this.isHeaded;
535
+ }
536
+
537
+ // ─── Auto-handoff Hint (consecutive failure tracking) ───────
538
+ incrementFailures(): void {
539
+ this.consecutiveFailures++;
540
+ }
541
+
542
+ resetFailures(): void {
543
+ this.consecutiveFailures = 0;
544
+ }
545
+
546
+ getFailureHint(): string | null {
547
+ if (this.consecutiveFailures >= 3 && !this.isHeaded) {
548
+ return `HINT: ${this.consecutiveFailures} consecutive failures. Consider using 'handoff' to let the user help.`;
549
+ }
550
+ return null;
551
+ }
552
+
553
+ // ─── Console/Network/Dialog/Ref Wiring ────────────────────
554
+ private wirePageEvents(page: Page) {
555
+ // Clear ref map on navigation — refs point to stale elements after page change
556
+ // (lastSnapshot is NOT cleared — it's a text baseline for diffing)
557
+ page.on('framenavigated', (frame) => {
558
+ if (frame === page.mainFrame()) {
559
+ this.clearRefs();
560
+ }
561
+ });
562
+
563
+ // ─── Dialog auto-handling (prevents browser lockup) ─────
564
+ page.on('dialog', async (dialog) => {
565
+ const entry: DialogEntry = {
566
+ timestamp: Date.now(),
567
+ type: dialog.type(),
568
+ message: dialog.message(),
569
+ defaultValue: dialog.defaultValue() || undefined,
570
+ action: this.dialogAutoAccept ? 'accepted' : 'dismissed',
571
+ response: this.dialogAutoAccept ? (this.dialogPromptText ?? undefined) : undefined,
572
+ };
573
+ addDialogEntry(entry);
574
+
575
+ try {
576
+ if (this.dialogAutoAccept) {
577
+ await dialog.accept(this.dialogPromptText ?? undefined);
578
+ } else {
579
+ await dialog.dismiss();
580
+ }
581
+ } catch {
582
+ // Dialog may have been dismissed by navigation — ignore
583
+ }
584
+ });
585
+
586
+ page.on('console', (msg) => {
587
+ addConsoleEntry({
588
+ timestamp: Date.now(),
589
+ level: msg.type(),
590
+ text: msg.text(),
591
+ });
592
+ });
593
+
594
+ page.on('request', (req) => {
595
+ addNetworkEntry({
596
+ timestamp: Date.now(),
597
+ method: req.method(),
598
+ url: req.url(),
599
+ });
600
+ });
601
+
602
+ page.on('response', (res) => {
603
+ // Find matching request entry and update it (backward scan)
604
+ const url = res.url();
605
+ const status = res.status();
606
+ for (let i = networkBuffer.length - 1; i >= 0; i--) {
607
+ const entry = networkBuffer.get(i);
608
+ if (entry && entry.url === url && !entry.status) {
609
+ networkBuffer.set(i, { ...entry, status, duration: Date.now() - entry.timestamp });
610
+ break;
611
+ }
612
+ }
613
+ });
614
+
615
+ // Capture response sizes via response finished
616
+ page.on('requestfinished', async (req) => {
617
+ try {
618
+ const res = await req.response();
619
+ if (res) {
620
+ const url = req.url();
621
+ const body = await res.body().catch(() => null);
622
+ const size = body ? body.length : 0;
623
+ for (let i = networkBuffer.length - 1; i >= 0; i--) {
624
+ const entry = networkBuffer.get(i);
625
+ if (entry && entry.url === url && !entry.size) {
626
+ networkBuffer.set(i, { ...entry, size });
627
+ break;
628
+ }
629
+ }
630
+ }
631
+ } catch {}
632
+ });
633
+ }
634
+ }