feed-the-machine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/bin/generate-manifest.mjs +210 -0
  4. package/bin/install.mjs +114 -0
  5. package/ftm/SKILL.md +88 -0
  6. package/ftm-audit/SKILL.md +146 -0
  7. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -0
  8. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -0
  9. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -0
  10. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -0
  11. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -0
  12. package/ftm-audit/scripts/run-knip.sh +23 -0
  13. package/ftm-audit.yml +2 -0
  14. package/ftm-brainstorm/SKILL.md +379 -0
  15. package/ftm-brainstorm/evals/evals.json +100 -0
  16. package/ftm-brainstorm/evals/promptfoo.yaml +109 -0
  17. package/ftm-brainstorm/references/agent-prompts.md +224 -0
  18. package/ftm-brainstorm/references/plan-template.md +121 -0
  19. package/ftm-brainstorm.yml +2 -0
  20. package/ftm-browse/SKILL.md +415 -0
  21. package/ftm-browse/daemon/browser-manager.ts +206 -0
  22. package/ftm-browse/daemon/bun.lock +30 -0
  23. package/ftm-browse/daemon/cli.ts +347 -0
  24. package/ftm-browse/daemon/commands.ts +410 -0
  25. package/ftm-browse/daemon/main.ts +357 -0
  26. package/ftm-browse/daemon/package.json +17 -0
  27. package/ftm-browse/daemon/server.ts +189 -0
  28. package/ftm-browse/daemon/snapshot.ts +519 -0
  29. package/ftm-browse/daemon/tsconfig.json +22 -0
  30. package/ftm-browse.yml +4 -0
  31. package/ftm-codex-gate/SKILL.md +302 -0
  32. package/ftm-codex-gate.yml +2 -0
  33. package/ftm-config/SKILL.md +310 -0
  34. package/ftm-config.default.yml +80 -0
  35. package/ftm-config.yml +2 -0
  36. package/ftm-council/SKILL.md +132 -0
  37. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -0
  38. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -0
  39. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -0
  40. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -0
  41. package/ftm-council/references/protocols/PREREQUISITES.md +47 -0
  42. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -0
  43. package/ftm-council.yml +2 -0
  44. package/ftm-dashboard.yml +4 -0
  45. package/ftm-debug/SKILL.md +146 -0
  46. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -0
  47. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -0
  48. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -0
  49. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -0
  50. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -0
  51. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -0
  52. package/ftm-debug.yml +2 -0
  53. package/ftm-diagram/SKILL.md +233 -0
  54. package/ftm-diagram.yml +2 -0
  55. package/ftm-executor/SKILL.md +657 -0
  56. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -0
  57. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -0
  58. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -0
  59. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -0
  60. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -0
  61. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -0
  62. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -0
  63. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -0
  64. package/ftm-executor/references/protocols/MODEL-PROFILE.md +44 -0
  65. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -0
  66. package/ftm-executor/runtime/ftm-runtime.mjs +252 -0
  67. package/ftm-executor/runtime/package.json +8 -0
  68. package/ftm-executor.yml +2 -0
  69. package/ftm-git/SKILL.md +195 -0
  70. package/ftm-git/evals/evals.json +26 -0
  71. package/ftm-git/evals/promptfoo.yaml +75 -0
  72. package/ftm-git/hooks/post-commit-experience.sh +92 -0
  73. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -0
  74. package/ftm-git/references/protocols/REMEDIATION.md +139 -0
  75. package/ftm-git/scripts/pre-commit-secrets.sh +110 -0
  76. package/ftm-git.yml +2 -0
  77. package/ftm-intent/SKILL.md +198 -0
  78. package/ftm-intent.yml +2 -0
  79. package/ftm-map.yml +2 -0
  80. package/ftm-mind/SKILL.md +986 -0
  81. package/ftm-mind/evals/promptfoo.yaml +142 -0
  82. package/ftm-mind/references/blackboard-schema.md +328 -0
  83. package/ftm-mind/references/complexity-guide.md +110 -0
  84. package/ftm-mind/references/event-registry.md +299 -0
  85. package/ftm-mind/references/mcp-inventory.md +296 -0
  86. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -0
  87. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -0
  88. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -0
  89. package/ftm-mind/references/reflexion-protocol.md +249 -0
  90. package/ftm-mind/references/routing/SCENARIOS.md +22 -0
  91. package/ftm-mind/references/routing-scenarios.md +35 -0
  92. package/ftm-mind.yml +2 -0
  93. package/ftm-pause/SKILL.md +133 -0
  94. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -0
  95. package/ftm-pause/references/protocols/VALIDATION.md +80 -0
  96. package/ftm-pause.yml +2 -0
  97. package/ftm-researcher.yml +2 -0
  98. package/ftm-resume/SKILL.md +166 -0
  99. package/ftm-resume/references/protocols/VALIDATION.md +172 -0
  100. package/ftm-resume.yml +2 -0
  101. package/ftm-retro/SKILL.md +189 -0
  102. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -0
  103. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -0
  104. package/ftm-retro.yml +2 -0
  105. package/ftm-routine.yml +4 -0
  106. package/ftm-state/blackboard/context.json +23 -0
  107. package/ftm-state/blackboard/experiences/index.json +9 -0
  108. package/ftm-state/blackboard/patterns.json +6 -0
  109. package/ftm-state/schemas/context.schema.json +130 -0
  110. package/ftm-state/schemas/experience-index.schema.json +77 -0
  111. package/ftm-state/schemas/experience.schema.json +78 -0
  112. package/ftm-state/schemas/patterns.schema.json +44 -0
  113. package/ftm-upgrade/SKILL.md +153 -0
  114. package/ftm-upgrade/scripts/check-version.sh +76 -0
  115. package/ftm-upgrade/scripts/upgrade.sh +143 -0
  116. package/ftm-upgrade.yml +2 -0
  117. package/ftm.yml +2 -0
  118. package/install.sh +102 -0
  119. package/package.json +74 -0
  120. package/uninstall.sh +25 -0
@@ -0,0 +1,415 @@
1
+ ---
2
+ name: ftm-browse
3
+ description: Headless browser daemon for visual verification and web interaction. Gives agents the ability to navigate, screenshot, click, fill forms, and inspect ARIA trees via CLI commands. Use when user says "browse", "screenshot", "visual", "look at the app", "open browser", "check the page", "navigate to", "take a screenshot", "visual verification".
4
+ ---
5
+
6
+ ## Events
7
+
8
+ ### Emits
9
+ - `task_completed` — when a visual verification or interaction workflow finishes successfully
10
+
11
+ ### Listens To
12
+ (none — ftm-browse is invoked on demand and does not respond to events)
13
+
14
+ # ftm-browse
15
+
16
+ ftm-browse is a persistent headless Chromium daemon controlled via a CLI binary at `~/.claude/skills/ftm-browse/bin/ftm-browse`. Each CLI invocation communicates with the daemon over a local HTTP server (bearer-auth, random port), so the browser stays alive across commands without the per-invocation startup penalty. The daemon auto-starts on first use and shuts itself down after 30 minutes of idle. This CLI-to-HTTP model is 4x more token-efficient than driving Playwright MCP directly, because tool calls remain terse and outputs are structured JSON rather than raw browser protocol noise.
17
+
18
+ ---
19
+
20
+ ## Setup
21
+
22
+ **First run — install the browser engine:**
23
+
24
+ ```bash
25
+ npx playwright install chromium
26
+ ```
27
+
28
+ **Define the alias in any shell session before use:**
29
+
30
+ ```bash
31
+ PB="$HOME/.claude/skills/ftm-browse/bin/ftm-browse"
32
+ ```
33
+
34
+ **Verify the installation:**
35
+
36
+ ```bash
37
+ $PB goto https://example.com && $PB screenshot
38
+ ```
39
+
40
+ The first `goto` command will start the daemon (up to 10 seconds for cold start). All subsequent commands respond in ~100ms because the browser process stays alive.
41
+
42
+ ---
43
+
44
+ ## Command Reference
45
+
46
+ ### WRITE commands — state-mutating
47
+
48
+ These commands change browser state. Never retry blindly; check the returned `success` field.
49
+
50
+ **`goto <url>`** — Navigate to a URL. Waits for `domcontentloaded`.
51
+
52
+ ```bash
53
+ $PB goto https://example.com
54
+ $PB goto http://localhost:3000/dashboard
55
+ ```
56
+
57
+ Returns: `{ success, data: { url, title, status } }`
58
+
59
+ **`click <@ref>`** — Click an interactive element by its `@e` ref. Performs a 5ms staleness check before clicking; fails immediately if the ref is stale rather than waiting.
60
+
61
+ ```bash
62
+ $PB click @e3
63
+ $PB click @e12
64
+ ```
65
+
66
+ Returns: `{ success, data: { url, title } }` — includes the URL after any resulting navigation.
67
+
68
+ **`fill <@ref> <value>`** — Fill a text input, textarea, or other fillable element. Values with spaces do not need quoting — remaining CLI args are joined.
69
+
70
+ ```bash
71
+ $PB fill @e2 hello world
72
+ $PB fill @e5 user@example.com
73
+ ```
74
+
75
+ Returns: `{ success, data: { ref, value } }`
76
+
77
+ **`press <key>`** — Send a keyboard key to the active page. Accepts any Playwright key name.
78
+
79
+ ```bash
80
+ $PB press Enter
81
+ $PB press Tab
82
+ $PB press Escape
83
+ $PB press ArrowDown
84
+ ```
85
+
86
+ Returns: `{ success, data: { key, url } }`
87
+
88
+ ---
89
+
90
+ ### READ commands — safe to retry
91
+
92
+ These commands do not change browser state. Safe to call multiple times.
93
+
94
+ **`text`** — Get visible page text via `document.body.innerText`.
95
+
96
+ ```bash
97
+ $PB text
98
+ ```
99
+
100
+ Returns: `{ success, data: { text } }`
101
+
102
+ **`html`** — Get full page HTML via `page.content()`.
103
+
104
+ ```bash
105
+ $PB html
106
+ ```
107
+
108
+ Returns: `{ success, data: { html } }`
109
+
110
+ **`eval <js-expression>`** — Execute arbitrary JavaScript in the page context. Returns the result as a JSON-serializable value. DOM elements are returned as `{ type: "element", tagName, id, text }`. Functions are returned as `{ type: "function", name }`. Errors inside the expression are caught and returned as `{ error: "..." }` rather than crashing the daemon.
111
+
112
+ ```bash
113
+ $PB eval document.title
114
+ $PB eval "document.querySelector('input[name=email]').value"
115
+ $PB eval "Array.from(document.querySelectorAll('td')).map(td => td.textContent)"
116
+ $PB eval window.location.href
117
+ $PB eval "document.cookie"
118
+ ```
119
+
120
+ Returns: `{ success, data: { result } }` — `result` is whatever the expression evaluated to.
121
+
122
+ Use cases:
123
+ - Reading hidden or programmatically-set form field values not visible in the ARIA tree
124
+ - Extracting data from dynamic tables or rendered lists
125
+ - Checking feature toggle states or global JS variables (`window.featureFlags`)
126
+ - Inspecting `localStorage` or `sessionStorage` values
127
+ - Querying computed DOM state not exposed via accessibility roles
128
+
129
+ ---
130
+
131
+ ### META commands
132
+
133
+ **`snapshot`** — Full ARIA accessibility tree of the current page. Includes both interactive and structural elements (headings, nav, main, etc.).
134
+
135
+ ```bash
136
+ $PB snapshot
137
+ ```
138
+
139
+ Returns: `{ success, data: { url, title, interactive_only: false, tree, refs, aria_text? } }`
140
+
141
+ **`snapshot -i`** — Interactive elements only, each labeled with an `@e1`, `@e2`... ref. Use this before clicking or filling — never guess a ref.
142
+
143
+ ```bash
144
+ $PB snapshot -i
145
+ ```
146
+
147
+ Returns: same shape as `snapshot` with `interactive_only: true`; the `refs` map contains the locator entries for each `@eN`.
148
+
149
+ **`screenshot`** — Capture a viewport screenshot (1280x800). Saves to `~/.ftm-browse/screenshots/screenshot-<timestamp>.png` by default and returns the path.
150
+
151
+ ```bash
152
+ $PB screenshot
153
+ $PB screenshot --path /tmp/before.png
154
+ $PB screenshot --path /tmp/after.png
155
+ ```
156
+
157
+ Returns: `{ success, data: { path, url, title } }`
158
+
159
+ **`tabs`** — List all open browser tabs.
160
+
161
+ ```bash
162
+ $PB tabs
163
+ ```
164
+
165
+ Returns: `{ success, data: { tabs: [{ index, url, title, active }] } }`
166
+
167
+ **`chain '<json-array>'`** — Execute multiple commands in sequence in a single CLI invocation. The chain stops at the first failure. Use this to reduce round-trips for multi-step operations.
168
+
169
+ ```bash
170
+ $PB chain '[
171
+ {"command":"goto","args":{"url":"https://example.com"}},
172
+ {"command":"snapshot","args":{"interactive_only":true}},
173
+ {"command":"screenshot","args":{}}
174
+ ]'
175
+ ```
176
+
177
+ Returns: `{ success, data: { results: [{ command, result }] } }`. On failure: adds `failed_at` field.
178
+
179
+ **`health`** — Check that the daemon is alive and responding.
180
+
181
+ ```bash
182
+ $PB health
183
+ ```
184
+
185
+ Returns: `{ status: "ok", pid }` (wrapped in standard result envelope from the daemon's health handler — note this endpoint bypasses `executeCommand` and returns directly).
186
+
187
+ **`stop`** (alias: `shutdown`) — Send SIGTERM to the daemon. The daemon cleans up its state file and exits.
188
+
189
+ ```bash
190
+ $PB stop
191
+ ```
192
+
193
+ ---
194
+
195
+ ## The @e Ref System
196
+
197
+ Refs are short handles (`@e1`, `@e2`, ...) that identify interactive elements. They are assigned fresh on each `snapshot` call and map to stable Playwright locator strategies (by label, by role+name, by placeholder, by name attribute, or by nth-position CSS fallback).
198
+
199
+ **Getting refs:**
200
+
201
+ ```bash
202
+ $PB snapshot -i
203
+ # Output includes tree nodes like:
204
+ # { "ref": "@e3", "role": "button", "name": "Submit", "interactive": true }
205
+ # { "ref": "@e5", "role": "textbox", "name": "Email", "interactive": true }
206
+ ```
207
+
208
+ **Using refs:**
209
+
210
+ ```bash
211
+ $PB fill @e5 user@example.com
212
+ $PB click @e3
213
+ ```
214
+
215
+ **Staleness rule:** After any navigation event — whether from `goto`, a `click` that follows a link, or `press Enter` submitting a form — the current ref map is invalidated. The daemon detects stale refs in ~5ms and returns an error asking you to re-snapshot. Always re-run `snapshot -i` after navigation before using refs again.
216
+
217
+ **Typical interaction workflow:**
218
+
219
+ ```bash
220
+ # 1. Navigate to the page
221
+ $PB goto http://localhost:3000/login
222
+
223
+ # 2. Get interactive refs
224
+ $PB snapshot -i
225
+
226
+ # 3. Identify target elements from the output, then interact
227
+ $PB fill @e2 admin@example.com
228
+ $PB fill @e3 password123
229
+ $PB click @e4 # "Sign in" button
230
+
231
+ # 4. Page navigated — refs are stale; re-snapshot
232
+ $PB snapshot -i
233
+
234
+ # 5. Continue on the new page
235
+ $PB screenshot
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Common Workflows
241
+
242
+ ### Visual smoke test
243
+
244
+ ```bash
245
+ PB="$HOME/.claude/skills/ftm-browse/bin/ftm-browse"
246
+ $PB goto http://localhost:3000
247
+ $PB screenshot --path /tmp/smoke.png
248
+ # Read /tmp/smoke.png to verify layout
249
+ ```
250
+
251
+ ### Form filling
252
+
253
+ ```bash
254
+ $PB goto http://localhost:3000/signup
255
+ $PB snapshot -i
256
+ # Identify: @e1=name input, @e2=email input, @e3=password, @e4=submit button
257
+ $PB fill @e1 Jane Doe
258
+ $PB fill @e2 jane@example.com
259
+ $PB fill @e3 s3cret!
260
+ $PB click @e4
261
+ $PB screenshot --path /tmp/after-signup.png
262
+ ```
263
+
264
+ ### Navigation verification
265
+
266
+ ```bash
267
+ $PB goto http://localhost:3000
268
+ $PB snapshot -i
269
+ # Find nav links
270
+ $PB click @e7 # "Dashboard" link
271
+ $PB text # Verify content changed
272
+ $PB screenshot
273
+ ```
274
+
275
+ ### Before/after comparison
276
+
277
+ ```bash
278
+ $PB goto http://localhost:3000/widget
279
+ $PB screenshot --path /tmp/before.png
280
+ # ... make changes in code ...
281
+ $PB goto http://localhost:3000/widget # reload after change
282
+ $PB screenshot --path /tmp/after.png
283
+ # Compare /tmp/before.png and /tmp/after.png visually
284
+ ```
285
+
286
+ ### Multi-step with chain (fewer round-trips)
287
+
288
+ ```bash
289
+ $PB chain '[
290
+ {"command":"goto","args":{"url":"http://localhost:3000/login"}},
291
+ {"command":"fill","args":{"ref":"@e2","value":"admin@example.com"}},
292
+ {"command":"fill","args":{"ref":"@e3","value":"password"}},
293
+ {"command":"click","args":{"ref":"@e4"}},
294
+ {"command":"screenshot","args":{}}
295
+ ]'
296
+ ```
297
+
298
+ Note: When using `chain` with refs, you must have called `snapshot -i` first in a separate command to populate the ref map. Refs set by a `snapshot` inside the same chain are available to subsequent steps in that chain.
299
+
300
+ ---
301
+
302
+ ## Integration with Other FTM Skills
303
+
304
+ **ftm-debug** — Use ftm-browse to visually verify bug fixes. Take a screenshot before applying a fix, apply the fix, reload, screenshot again. Compare before/after to confirm the fix is visible. Also use `snapshot` to inspect DOM state when debugging rendering issues — the ARIA tree reveals whether components have mounted and populated correctly.
305
+
306
+ **ftm-audit** — Use ftm-browse to verify runtime wiring. Navigate to each route the audit is checking, call `snapshot` to confirm the component appears in the ARIA tree with the correct role and name, and screenshot for documentation. This catches hydration failures, missing route registrations, and components that render blank.
307
+
308
+ **ftm-executor** — After completing a task that touches frontend code, use ftm-browse as the post-task smoke test harness. If the project has a dev server running, `goto` the affected route, take a screenshot, and verify the page renders without errors. Include the screenshot path in the task completion report.
309
+
310
+ ---
311
+
312
+ ## Supervised Execution Mode
313
+
314
+ When ftm-browse is executing browser steps within an approved plan (dispatched by ftm-executor or ftm-mind), activate supervised mode. This mode adds verification guardrails after every navigation action.
315
+
316
+ ### Activation
317
+
318
+ Supervised mode activates automatically when:
319
+ - The browse command is part of a plan step (context includes plan step reference)
320
+ - The caller provides expected page state (title pattern, URL pattern, or element selector)
321
+
322
+ ### Post-Navigation Verification
323
+
324
+ After every `goto`, `click` that triggers navigation, or `fill` + `submit` sequence:
325
+
326
+ 1. **Wait for page load** — wait for `networkidle` or 5-second timeout, whichever comes first
327
+ 2. **Check URL** — if expected URL pattern was provided, verify current URL matches
328
+ 3. **Check title** — if expected title pattern was provided, verify page title matches
329
+ 4. **Check for error indicators** — scan for common error patterns:
330
+ - HTTP error codes in title (403, 404, 500, 502, 503)
331
+ - Error modals or alert dialogs
332
+ - "Access Denied", "Unauthorized", "Session Expired" text in page
333
+ 5. **Check for auth redirects** — if current URL contains `/login`, `/signin`, `/sso`, `/oauth`, or `/saml` when it wasn't the intended destination, flag as auth redirect
334
+
335
+ ### On Unexpected State
336
+
337
+ When verification detects a mismatch or error:
338
+
339
+ 1. **Stop execution immediately** — do not proceed to the next browser step
340
+ 2. **Take a screenshot** — capture the current page state
341
+ 3. **Present the situation** to the user:
342
+
343
+ ```
344
+ ⚠ Unexpected browser state during plan step [N]:
345
+
346
+ Expected: [expected URL/title/state]
347
+ Actual: [current URL/title]
348
+ Issue: [description — wrong page / error page / auth redirect / modal detected]
349
+
350
+ Screenshot: [path to screenshot]
351
+
352
+ Options:
353
+ 1. retry — navigate again
354
+ 2. skip — skip this browser step, continue plan
355
+ 3. abort — stop plan execution entirely
356
+ 4. manual — open interactive browser for manual intervention
357
+ ```
358
+
359
+ 4. **Wait for user choice** — do not proceed without explicit selection
360
+
361
+ ### Auth Redirect Detection
362
+
363
+ Authentication redirects are especially dangerous because silently following them can:
364
+ - Leak credentials to unexpected domains
365
+ - Complete OAuth flows the user didn't intend
366
+ - Grant permissions the user didn't approve
367
+
368
+ When an auth redirect is detected:
369
+ - NEVER automatically follow it
370
+ - Flag it prominently: "Auth redirect detected — redirected to [domain]"
371
+ - The user must explicitly choose to proceed
372
+
373
+ ### Audit Trail
374
+
375
+ Every browser step within a plan produces:
376
+ - **Before screenshot** — taken just before the action
377
+ - **After screenshot** — taken after the action completes (or after error)
378
+ - **Timing** — how long the action took
379
+ - **Verification result** — PASS or FAIL with details
380
+
381
+ Screenshots saved to a temp directory, paths included in the step report.
382
+
383
+ ### Non-Plan Usage
384
+
385
+ When ftm-browse is used directly (not within a plan), supervised mode is OFF by default. The user gets the raw browse experience. They can enable it manually with `--supervised` flag.
386
+
387
+ ---
388
+
389
+ ## Error Handling
390
+
391
+ | Symptom | Cause | Fix |
392
+ |---|---|---|
393
+ | First command hangs up to 10s | Daemon cold start | Normal — wait for it |
394
+ | `Ref @eN not found. The page may have changed` | Stale ref after navigation | Re-run `snapshot -i` |
395
+ | `Ref @eN no longer exists on the page` | Element removed from DOM | Re-run `snapshot -i` |
396
+ | `Timeout` on goto | Page slow to load or wrong URL | Check URL, verify server is running |
397
+ | `Browser not installed` or Chromium launch error | Playwright Chromium missing | Run `npx playwright install chromium` |
398
+ | `Daemon failed to start within 10 seconds` | Bun or binary issue | Check `~/.ftm-browse/` for logs; verify binary is executable |
399
+ | Connection refused | Daemon died (idle timeout or crash) | Next command will auto-restart it |
400
+ | `commands must be an array` | Bad JSON passed to chain | Validate JSON before passing to chain |
401
+ | `Evaluation failed: ...` | Playwright could not serialize or run the expression | Check for syntax errors; wrap complex expressions in quotes |
402
+
403
+ ---
404
+
405
+ ## Tips
406
+
407
+ - Always run `snapshot -i` before `click` or `fill` — never guess or hardcode a ref number.
408
+ - Use `chain` for multi-step flows to reduce round-trip overhead; each step result is available in the returned array.
409
+ - Screenshots are cheap — take them liberally at key points (before interaction, after submit, after navigation) as a natural audit trail.
410
+ - The daemon persists across all commands in a session. Cold start only happens once per 30-minute idle window.
411
+ - `$PB text` is the fastest way to assert page content without parsing HTML.
412
+ - `$PB html` is useful when you need to inspect the raw DOM, check for hidden elements, or verify server-rendered markup.
413
+ - The daemon uses a 1280x800 headless Chromium viewport with a standard Mac Chrome user-agent, so most sites render predictably.
414
+ - To stop the daemon explicitly: `$PB stop`. It will auto-restart on next use.
415
+ - `$PB eval` is the escape hatch for anything the ARIA tree doesn't expose — hidden inputs, JS globals, localStorage, computed values.
@@ -0,0 +1,206 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
4
+
5
+ const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
6
+ const DEFAULT_USER_DATA_DIR = path.join(process.env.HOME || "~", ".ftm-browse", "user-data");
7
+
8
+ class BrowserManager {
9
+ private browser: Browser | null = null;
10
+ private context: BrowserContext | null = null;
11
+ private page: Page | null = null;
12
+ private idleTimer: ReturnType<typeof setTimeout> | null = null;
13
+ private shutdownCallback: (() => void) | null = null;
14
+ private initPromise: Promise<void> | null = null;
15
+ private userDataDir: string | null = null;
16
+
17
+ setUserDataDir(dir: string | null): void {
18
+ this.userDataDir = dir;
19
+ }
20
+
21
+ setShutdownCallback(cb: () => void): void {
22
+ this.shutdownCallback = cb;
23
+ }
24
+
25
+ resetIdleTimer(): void {
26
+ if (this.idleTimer) {
27
+ clearTimeout(this.idleTimer);
28
+ }
29
+ this.idleTimer = setTimeout(() => {
30
+ console.log("[browser-manager] 30min idle timeout reached, shutting down");
31
+ void this.shutdown();
32
+ }, IDLE_TIMEOUT_MS);
33
+ }
34
+
35
+ async initialize(): Promise<void> {
36
+ // Short-circuit if already running (handles both ephemeral and persistent context modes)
37
+ if (this.isRunning()) return;
38
+
39
+ // Deduplicate concurrent init calls
40
+ if (this.initPromise) {
41
+ return this.initPromise;
42
+ }
43
+
44
+ this.initPromise = this._doInit().finally(() => {
45
+ this.initPromise = null;
46
+ });
47
+ return this.initPromise;
48
+ }
49
+
50
+ private async _doInit(): Promise<void> {
51
+ const launchArgs = [
52
+ "--no-sandbox",
53
+ "--disable-setuid-sandbox",
54
+ "--disable-dev-shm-usage",
55
+ "--disable-gpu",
56
+ "--disable-background-networking",
57
+ "--disable-default-apps",
58
+ "--disable-extensions",
59
+ "--disable-sync",
60
+ "--disable-translate",
61
+ "--metrics-recording-only",
62
+ "--mute-audio",
63
+ "--no-first-run",
64
+ "--safebrowsing-disable-auto-update",
65
+ ];
66
+
67
+ const contextOptions = {
68
+ viewport: { width: 1280, height: 800 } as { width: number; height: number } | null,
69
+ userAgent:
70
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
71
+ };
72
+
73
+ if (this.userDataDir) {
74
+ // Persistent context — cookies and session data survive daemon restarts
75
+ const resolvedDir = this.userDataDir === "default" ? DEFAULT_USER_DATA_DIR : this.userDataDir;
76
+ console.log(`[browser-manager] Launching Chromium with persistent context at: ${resolvedDir}`);
77
+
78
+ if (!fs.existsSync(resolvedDir)) {
79
+ fs.mkdirSync(resolvedDir, { recursive: true, mode: 0o700 });
80
+ }
81
+
82
+ // launchPersistentContext returns a BrowserContext directly (no separate Browser)
83
+ this.context = await chromium.launchPersistentContext(resolvedDir, {
84
+ headless: true,
85
+ args: launchArgs,
86
+ ...contextOptions,
87
+ });
88
+
89
+ // Reuse existing page if available, otherwise open a new one
90
+ const existingPages = this.context.pages();
91
+ this.page = existingPages.length > 0 ? existingPages[0] : await this.context.newPage();
92
+ } else {
93
+ // Ephemeral context — no persistence (default, safe)
94
+ console.log("[browser-manager] Launching Chromium (ephemeral)...");
95
+
96
+ this.browser = await chromium.launch({
97
+ headless: true,
98
+ args: launchArgs,
99
+ });
100
+
101
+ this.context = await this.browser.newContext(contextOptions);
102
+ this.page = await this.context.newPage();
103
+ }
104
+
105
+ // Start idle timer
106
+ this.resetIdleTimer();
107
+
108
+ console.log("[browser-manager] Browser ready");
109
+ }
110
+
111
+ async getPage(): Promise<Page> {
112
+ await this.initialize();
113
+ this.resetIdleTimer();
114
+
115
+ if (!this.page || this.page.isClosed()) {
116
+ console.log("[browser-manager] Page was closed, creating new page");
117
+ this.page = await this.context!.newPage();
118
+ }
119
+
120
+ return this.page;
121
+ }
122
+
123
+ async getOrCreatePage(tabIndex?: number): Promise<Page> {
124
+ await this.initialize();
125
+ this.resetIdleTimer();
126
+
127
+ const pages = this.context!.pages();
128
+
129
+ if (tabIndex !== undefined && tabIndex < pages.length) {
130
+ this.page = pages[tabIndex];
131
+ return this.page;
132
+ }
133
+
134
+ if (!this.page || this.page.isClosed()) {
135
+ this.page = await this.context!.newPage();
136
+ }
137
+
138
+ return this.page;
139
+ }
140
+
141
+ async getAllPages(): Promise<Page[]> {
142
+ if (!this.context) return [];
143
+ return this.context.pages();
144
+ }
145
+
146
+ async setActivePage(index: number): Promise<Page | null> {
147
+ if (!this.context) return null;
148
+ const pages = this.context.pages();
149
+ if (index < 0 || index >= pages.length) return null;
150
+ this.page = pages[index];
151
+ this.resetIdleTimer();
152
+ return this.page;
153
+ }
154
+
155
+ async shutdown(): Promise<void> {
156
+ if (this.idleTimer) {
157
+ clearTimeout(this.idleTimer);
158
+ this.idleTimer = null;
159
+ }
160
+
161
+ try {
162
+ if (this.page && !this.page.isClosed()) {
163
+ await this.page.close();
164
+ }
165
+ } catch {
166
+ // Ignore close errors
167
+ }
168
+
169
+ try {
170
+ if (this.context) {
171
+ await this.context.close();
172
+ }
173
+ } catch {
174
+ // Ignore close errors
175
+ }
176
+
177
+ try {
178
+ if (this.browser) {
179
+ await this.browser.close();
180
+ }
181
+ } catch {
182
+ // Ignore close errors
183
+ }
184
+
185
+ this.page = null;
186
+ this.context = null;
187
+ this.browser = null;
188
+
189
+ console.log("[browser-manager] Browser shut down");
190
+
191
+ if (this.shutdownCallback) {
192
+ this.shutdownCallback();
193
+ }
194
+ }
195
+
196
+ isRunning(): boolean {
197
+ // Persistent context mode: browser is null, check context instead
198
+ if (this.userDataDir) {
199
+ return this.context !== null;
200
+ }
201
+ return this.browser !== null && this.browser.isConnected();
202
+ }
203
+ }
204
+
205
+ // Singleton instance
206
+ export const browserManager = new BrowserManager();
@@ -0,0 +1,30 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "ftm-browse",
7
+ "dependencies": {
8
+ "playwright": "^1.49.0",
9
+ },
10
+ "devDependencies": {
11
+ "@types/bun": "latest",
12
+ },
13
+ },
14
+ },
15
+ "packages": {
16
+ "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
17
+
18
+ "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
19
+
20
+ "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
21
+
22
+ "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
23
+
24
+ "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
25
+
26
+ "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
27
+
28
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
29
+ }
30
+ }