harnery 0.0.1 → 0.2.1
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.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/bin/agent-coord +42 -0
- package/bin/agent-hook +44 -0
- package/bin/harn +40 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/commander.d.ts +128 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +126 -0
- package/dist/commands/agents.d.ts +18 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +3946 -0
- package/dist/commands/backup.d.ts +22 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +262 -0
- package/dist/commands/browse-ai.d.ts +4 -0
- package/dist/commands/browse-ai.d.ts.map +1 -0
- package/dist/commands/browse-ai.js +156 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.d.ts.map +1 -0
- package/dist/commands/browse.js +590 -0
- package/dist/commands/callers.d.ts +4 -0
- package/dist/commands/callers.d.ts.map +1 -0
- package/dist/commands/callers.js +276 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +158 -0
- package/dist/commands/config-get.d.ts +4 -0
- package/dist/commands/config-get.d.ts.map +1 -0
- package/dist/commands/config-get.js +131 -0
- package/dist/commands/context.d.ts +11 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +185 -0
- package/dist/commands/cookies.d.ts +4 -0
- package/dist/commands/cookies.d.ts.map +1 -0
- package/dist/commands/cookies.js +140 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +137 -0
- package/dist/commands/doctor.d.ts +25 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +200 -0
- package/dist/commands/edit-batch.d.ts +18 -0
- package/dist/commands/edit-batch.d.ts.map +1 -0
- package/dist/commands/edit-batch.js +172 -0
- package/dist/commands/eml.d.ts +4 -0
- package/dist/commands/eml.d.ts.map +1 -0
- package/dist/commands/eml.js +428 -0
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +201 -0
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +99 -0
- package/dist/commands/file-history.d.ts +4 -0
- package/dist/commands/file-history.d.ts.map +1 -0
- package/dist/commands/file-history.js +152 -0
- package/dist/commands/grep.d.ts +4 -0
- package/dist/commands/grep.d.ts.map +1 -0
- package/dist/commands/grep.js +317 -0
- package/dist/commands/init.d.ts +82 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +288 -0
- package/dist/commands/outline.d.ts +4 -0
- package/dist/commands/outline.d.ts.map +1 -0
- package/dist/commands/outline.js +509 -0
- package/dist/commands/presence.d.ts +12 -0
- package/dist/commands/presence.d.ts.map +1 -0
- package/dist/commands/presence.js +123 -0
- package/dist/commands/read.d.ts +7 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +46 -0
- package/dist/commands/scratch.d.ts +4 -0
- package/dist/commands/scratch.d.ts.map +1 -0
- package/dist/commands/scratch.js +426 -0
- package/dist/commands/session.d.ts +4 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +162 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +275 -0
- package/dist/commands/toc.d.ts +5 -0
- package/dist/commands/toc.d.ts.map +1 -0
- package/dist/commands/toc.js +153 -0
- package/dist/commands/tokens.d.ts +4 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +48 -0
- package/dist/commands/tunnel.d.ts +4 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +513 -0
- package/dist/commands/uninstall.d.ts +22 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +126 -0
- package/dist/commands/web.d.ts +4 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +165 -0
- package/dist/core/agents/canonical-emit.d.ts +27 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -0
- package/dist/core/agents/canonical-emit.js +72 -0
- package/dist/core/agents/cli-emit.d.ts +27 -0
- package/dist/core/agents/cli-emit.d.ts.map +1 -0
- package/dist/core/agents/cli-emit.js +57 -0
- package/dist/core/agents/cli.d.ts +10 -0
- package/dist/core/agents/cli.d.ts.map +1 -0
- package/dist/core/agents/cli.js +757 -0
- package/dist/core/agents/codex-replay.d.ts +29 -0
- package/dist/core/agents/codex-replay.d.ts.map +1 -0
- package/dist/core/agents/codex-replay.js +138 -0
- package/dist/core/agents/coord-client.d.ts +98 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -0
- package/dist/core/agents/coord-client.js +212 -0
- package/dist/core/agents/events/consume.d.ts +59 -0
- package/dist/core/agents/events/consume.d.ts.map +1 -0
- package/dist/core/agents/events/consume.js +147 -0
- package/dist/core/agents/events/emit.d.ts +42 -0
- package/dist/core/agents/events/emit.d.ts.map +1 -0
- package/dist/core/agents/events/emit.js +70 -0
- package/dist/core/agents/events/ulid.d.ts +11 -0
- package/dist/core/agents/events/ulid.d.ts.map +1 -0
- package/dist/core/agents/events/ulid.js +47 -0
- package/dist/core/agents/index.d.ts +14 -0
- package/dist/core/agents/index.d.ts.map +1 -0
- package/dist/core/agents/index.js +13 -0
- package/dist/core/agents/paths.d.ts +6 -0
- package/dist/core/agents/paths.d.ts.map +1 -0
- package/dist/core/agents/paths.js +17 -0
- package/dist/core/agents/render/prompt-context.d.ts +43 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
- package/dist/core/agents/render/prompt-context.js +335 -0
- package/dist/core/agents/render/session-context.d.ts +39 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -0
- package/dist/core/agents/render/session-context.js +283 -0
- package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/claim-conflict.js +244 -0
- package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/commit-conflict.js +244 -0
- package/dist/core/agents/rules/stop-hook.d.ts +44 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
- package/dist/core/agents/rules/stop-hook.js +161 -0
- package/dist/core/agents/session-events.d.ts +41 -0
- package/dist/core/agents/session-events.d.ts.map +1 -0
- package/dist/core/agents/session-events.js +205 -0
- package/dist/core/agents/state/activity-log.d.ts +18 -0
- package/dist/core/agents/state/activity-log.d.ts.map +1 -0
- package/dist/core/agents/state/activity-log.js +34 -0
- package/dist/core/agents/state/council.d.ts +39 -0
- package/dist/core/agents/state/council.d.ts.map +1 -0
- package/dist/core/agents/state/council.js +216 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-projector.js +436 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-writer.js +271 -0
- package/dist/core/agents/state/names.d.ts +35 -0
- package/dist/core/agents/state/names.d.ts.map +1 -0
- package/dist/core/agents/state/names.js +376 -0
- package/dist/core/agents/state/pidmap.d.ts +11 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -0
- package/dist/core/agents/state/pidmap.js +32 -0
- package/dist/core/agents/state/scratch.d.ts +27 -0
- package/dist/core/agents/state/scratch.d.ts.map +1 -0
- package/dist/core/agents/state/scratch.js +90 -0
- package/dist/core/agents/state/shell-mutation.d.ts +17 -0
- package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
- package/dist/core/agents/state/shell-mutation.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts +16 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
- package/dist/core/agents/state/stale-sweep.js +166 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +108 -0
- package/dist/core/hooks/cli.d.ts +21 -0
- package/dist/core/hooks/cli.d.ts.map +1 -0
- package/dist/core/hooks/cli.js +1123 -0
- package/dist/core/hooks/effects/image-capture.d.ts +43 -0
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
- package/dist/core/hooks/effects/image-capture.js +288 -0
- package/dist/core/hooks/effects/index.d.ts +64 -0
- package/dist/core/hooks/effects/index.d.ts.map +1 -0
- package/dist/core/hooks/effects/index.js +197 -0
- package/dist/core/hooks/events/emit.d.ts +31 -0
- package/dist/core/hooks/events/emit.d.ts.map +1 -0
- package/dist/core/hooks/events/emit.js +89 -0
- package/dist/core/hooks/events/schema.d.ts +235 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -0
- package/dist/core/hooks/events/schema.js +12 -0
- package/dist/core/hooks/events/ulid.d.ts +10 -0
- package/dist/core/hooks/events/ulid.d.ts.map +1 -0
- package/dist/core/hooks/events/ulid.js +47 -0
- package/dist/core/hooks/harness/detect.d.ts +9 -0
- package/dist/core/hooks/harness/detect.d.ts.map +1 -0
- package/dist/core/hooks/harness/detect.js +29 -0
- package/dist/core/hooks/harness/events.d.ts +45 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -0
- package/dist/core/hooks/harness/events.js +71 -0
- package/dist/core/hooks/harness/output.d.ts +46 -0
- package/dist/core/hooks/harness/output.d.ts.map +1 -0
- package/dist/core/hooks/harness/output.js +87 -0
- package/dist/core/hooks/harness/parse.d.ts +67 -0
- package/dist/core/hooks/harness/parse.d.ts.map +1 -0
- package/dist/core/hooks/harness/parse.js +132 -0
- package/dist/core/hooks/index.d.ts +8 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +7 -0
- package/dist/core/hooks/resolve/anchor.d.ts +37 -0
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
- package/dist/core/hooks/resolve/anchor.js +48 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
- package/dist/core/hooks/resolve/coord-root.js +27 -0
- package/dist/core/hooks/resolve/intent.d.ts +33 -0
- package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
- package/dist/core/hooks/resolve/intent.js +79 -0
- package/dist/core/hooks/resolve/owner.d.ts +42 -0
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
- package/dist/core/hooks/resolve/owner.js +140 -0
- package/dist/core/hooks/resolve/transcript.d.ts +26 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
- package/dist/core/hooks/resolve/transcript.js +73 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/lib/agent-browser/client.d.ts +99 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -0
- package/dist/lib/agent-browser/client.js +177 -0
- package/dist/lib/agent-browser/index.d.ts +2 -0
- package/dist/lib/agent-browser/index.d.ts.map +1 -0
- package/dist/lib/agent-browser/index.js +1 -0
- package/dist/lib/browser/client.d.ts +193 -0
- package/dist/lib/browser/client.d.ts.map +1 -0
- package/dist/lib/browser/client.js +325 -0
- package/dist/lib/browser/dev-overlay.d.ts +23 -0
- package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
- package/dist/lib/browser/dev-overlay.js +153 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.d.ts.map +1 -0
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/layout.d.ts +79 -0
- package/dist/lib/browser/layout.d.ts.map +1 -0
- package/dist/lib/browser/layout.js +220 -0
- package/dist/lib/browser/visibility.d.ts +86 -0
- package/dist/lib/browser/visibility.d.ts.map +1 -0
- package/dist/lib/browser/visibility.js +333 -0
- package/dist/lib/browser/visual-diff.d.ts +38 -0
- package/dist/lib/browser/visual-diff.d.ts.map +1 -0
- package/dist/lib/browser/visual-diff.js +107 -0
- package/dist/lib/completion/bash.d.ts +25 -0
- package/dist/lib/completion/bash.d.ts.map +1 -0
- package/dist/lib/completion/bash.js +284 -0
- package/dist/lib/completion/fish.d.ts +16 -0
- package/dist/lib/completion/fish.d.ts.map +1 -0
- package/dist/lib/completion/fish.js +118 -0
- package/dist/lib/completion/index.d.ts +5 -0
- package/dist/lib/completion/index.d.ts.map +1 -0
- package/dist/lib/completion/index.js +4 -0
- package/dist/lib/completion/walk.d.ts +68 -0
- package/dist/lib/completion/walk.d.ts.map +1 -0
- package/dist/lib/completion/walk.js +102 -0
- package/dist/lib/completion/zsh.d.ts +13 -0
- package/dist/lib/completion/zsh.d.ts.map +1 -0
- package/dist/lib/completion/zsh.js +249 -0
- package/dist/lib/context/index.d.ts +107 -0
- package/dist/lib/context/index.d.ts.map +1 -0
- package/dist/lib/context/index.js +275 -0
- package/dist/lib/cookies/client.d.ts +131 -0
- package/dist/lib/cookies/client.d.ts.map +1 -0
- package/dist/lib/cookies/client.js +239 -0
- package/dist/lib/cookies/index.d.ts +2 -0
- package/dist/lib/cookies/index.d.ts.map +1 -0
- package/dist/lib/cookies/index.js +1 -0
- package/dist/lib/council/index.d.ts +266 -0
- package/dist/lib/council/index.d.ts.map +1 -0
- package/dist/lib/council/index.js +674 -0
- package/dist/lib/docs-index.d.ts +28 -0
- package/dist/lib/docs-index.d.ts.map +1 -0
- package/dist/lib/docs-index.js +169 -0
- package/dist/lib/docs-lint.d.ts +26 -0
- package/dist/lib/docs-lint.d.ts.map +1 -0
- package/dist/lib/docs-lint.js +378 -0
- package/dist/lib/docs-sweep.d.ts +34 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -0
- package/dist/lib/docs-sweep.js +304 -0
- package/dist/lib/docs.d.ts +27 -0
- package/dist/lib/docs.d.ts.map +1 -0
- package/dist/lib/docs.js +142 -0
- package/dist/lib/env.d.ts +11 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +12 -0
- package/dist/lib/exec.d.ts +32 -0
- package/dist/lib/exec.d.ts.map +1 -0
- package/dist/lib/exec.js +54 -0
- package/dist/lib/format.d.ts +29 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +139 -0
- package/dist/lib/http/client.d.ts +56 -0
- package/dist/lib/http/client.d.ts.map +1 -0
- package/dist/lib/http/client.js +160 -0
- package/dist/lib/http/index.d.ts +2 -0
- package/dist/lib/http/index.d.ts.map +1 -0
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/identities/index.d.ts +77 -0
- package/dist/lib/identities/index.d.ts.map +1 -0
- package/dist/lib/identities/index.js +190 -0
- package/dist/lib/machine.d.ts +19 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/machine.js +61 -0
- package/dist/lib/presence.d.ts +48 -0
- package/dist/lib/presence.d.ts.map +1 -0
- package/dist/lib/presence.js +123 -0
- package/dist/lib/readability/client.d.ts +39 -0
- package/dist/lib/readability/client.d.ts.map +1 -0
- package/dist/lib/readability/client.js +121 -0
- package/dist/lib/readability/index.d.ts +2 -0
- package/dist/lib/readability/index.d.ts.map +1 -0
- package/dist/lib/readability/index.js +1 -0
- package/dist/lib/scratch/index.d.ts +74 -0
- package/dist/lib/scratch/index.d.ts.map +1 -0
- package/dist/lib/scratch/index.js +393 -0
- package/dist/lib/tunnel/gate.d.ts +12 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -0
- package/dist/lib/tunnel/gate.js +101 -0
- package/dist/lib/tunnel/state.d.ts +34 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -0
- package/dist/lib/tunnel/state.js +132 -0
- package/package.json +160 -8
- package/schemas/.gitkeep +0 -0
- package/schemas/config.schema.json +109 -0
- package/src/cli.ts +22 -0
- package/src/commander.ts +242 -0
- package/src/commands/.gitkeep +0 -0
- package/src/commands/agents.ts +4567 -0
- package/src/commands/backup.ts +305 -0
- package/src/commands/browse-ai.ts +198 -0
- package/src/commands/browse.ts +849 -0
- package/src/commands/callers.ts +363 -0
- package/src/commands/completion.ts +193 -0
- package/src/commands/config-get.ts +161 -0
- package/src/commands/context.ts +209 -0
- package/src/commands/cookies.ts +198 -0
- package/src/commands/docs.ts +174 -0
- package/src/commands/doctor.ts +231 -0
- package/src/commands/edit-batch.ts +233 -0
- package/src/commands/eml.ts +519 -0
- package/src/commands/env.ts +254 -0
- package/src/commands/fetch.ts +136 -0
- package/src/commands/file-history.ts +202 -0
- package/src/commands/grep.ts +371 -0
- package/src/commands/init.ts +335 -0
- package/src/commands/outline.ts +583 -0
- package/src/commands/presence.ts +152 -0
- package/src/commands/read.ts +64 -0
- package/src/commands/scratch.ts +445 -0
- package/src/commands/session.ts +187 -0
- package/src/commands/sync.ts +306 -0
- package/src/commands/toc.ts +218 -0
- package/src/commands/tokens.ts +79 -0
- package/src/commands/tunnel.ts +633 -0
- package/src/commands/uninstall.ts +144 -0
- package/src/commands/web.ts +193 -0
- package/src/core/agents/canonical-emit.ts +77 -0
- package/src/core/agents/cli-emit.ts +64 -0
- package/src/core/agents/cli.ts +838 -0
- package/src/core/agents/codex-replay.ts +163 -0
- package/src/core/agents/coord-client.ts +249 -0
- package/src/core/agents/events/consume.ts +196 -0
- package/src/core/agents/events/emit.ts +108 -0
- package/src/core/agents/events/ulid.ts +51 -0
- package/src/core/agents/index.ts +14 -0
- package/src/core/agents/paths.ts +16 -0
- package/src/core/agents/render/prompt-context.ts +401 -0
- package/src/core/agents/render/session-context.ts +341 -0
- package/src/core/agents/rules/claim-conflict.ts +282 -0
- package/src/core/agents/rules/commit-conflict.ts +303 -0
- package/src/core/agents/rules/stop-hook.ts +229 -0
- package/src/core/agents/session-events.ts +228 -0
- package/src/core/agents/state/activity-log.ts +33 -0
- package/src/core/agents/state/council.ts +265 -0
- package/src/core/agents/state/heartbeat-projector.ts +488 -0
- package/src/core/agents/state/heartbeat-writer.ts +333 -0
- package/src/core/agents/state/names.ts +399 -0
- package/src/core/agents/state/pidmap.ts +38 -0
- package/src/core/agents/state/scratch.ts +121 -0
- package/src/core/agents/state/shell-mutation.ts +44 -0
- package/src/core/agents/state/stale-sweep.ts +190 -0
- package/src/core/config.ts +111 -0
- package/src/core/hooks/cli.ts +1247 -0
- package/src/core/hooks/effects/image-capture.ts +330 -0
- package/src/core/hooks/effects/index.ts +210 -0
- package/src/core/hooks/events/emit.ts +120 -0
- package/src/core/hooks/events/schema.ts +430 -0
- package/src/core/hooks/events/ulid.ts +51 -0
- package/src/core/hooks/harness/detect.ts +30 -0
- package/src/core/hooks/harness/events.ts +102 -0
- package/src/core/hooks/harness/output.ts +100 -0
- package/src/core/hooks/harness/parse.ts +180 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/resolve/anchor.ts +51 -0
- package/src/core/hooks/resolve/coord-root.ts +25 -0
- package/src/core/hooks/resolve/intent.ts +89 -0
- package/src/core/hooks/resolve/owner.ts +140 -0
- package/src/core/hooks/resolve/transcript.ts +72 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/index.ts +15 -0
- package/src/lib/agent-browser/client.ts +239 -0
- package/src/lib/agent-browser/index.ts +1 -0
- package/src/lib/browser/client.ts +449 -0
- package/src/lib/browser/dev-overlay.ts +207 -0
- package/src/lib/browser/index.ts +24 -0
- package/src/lib/browser/layout.ts +288 -0
- package/src/lib/browser/visibility.ts +419 -0
- package/src/lib/browser/visual-diff.ts +150 -0
- package/src/lib/completion/bash.ts +291 -0
- package/src/lib/completion/fish.ts +134 -0
- package/src/lib/completion/index.ts +10 -0
- package/src/lib/completion/walk.ts +184 -0
- package/src/lib/completion/zsh.ts +262 -0
- package/src/lib/context/index.ts +386 -0
- package/src/lib/cookies/client.ts +301 -0
- package/src/lib/cookies/index.ts +13 -0
- package/src/lib/council/index.ts +803 -0
- package/src/lib/docs-index.ts +216 -0
- package/src/lib/docs-lint.ts +413 -0
- package/src/lib/docs-sweep.ts +348 -0
- package/src/lib/docs.ts +199 -0
- package/src/lib/env.ts +12 -0
- package/src/lib/exec.ts +74 -0
- package/src/lib/format.ts +147 -0
- package/src/lib/http/client.ts +211 -0
- package/src/lib/http/index.ts +1 -0
- package/src/lib/identities/index.ts +210 -0
- package/src/lib/machine.ts +61 -0
- package/src/lib/presence.ts +154 -0
- package/src/lib/readability/client.ts +169 -0
- package/src/lib/readability/index.ts +5 -0
- package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
- package/src/lib/scratch/index.ts +470 -0
- package/src/lib/tunnel/gate.ts +113 -0
- package/src/lib/tunnel/state.ts +167 -0
- package/src/web/.gitkeep +0 -0
- package/index.js +0 -1
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public library entry. Re-exports the harn-able surface for consumers that
|
|
3
|
+
* import harnery as a library rather than calling the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Examples of intended consumption:
|
|
6
|
+
* import { createHarneryProgram } from 'harnery'; // CLI composition
|
|
7
|
+
* import { evaluateClaim } from 'harnery/core/agents'; // verdict rule directly
|
|
8
|
+
* import type { Heartbeat } from 'harnery/core/agents'; // schema types
|
|
9
|
+
*
|
|
10
|
+
* Heavy modules (web UI, hook scripts) are not re-exported from here; they're
|
|
11
|
+
* reachable via their own subpath entries in package.json#exports.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type { HarneryContextOpts, HarneryProgramContext } from "./commander.ts";
|
|
15
|
+
export { createHarneryProgram } from "./commander.ts";
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { type SpawnSyncOptions, spawnSync } from "node:child_process";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
type CookieJar,
|
|
5
|
+
type CookieStore,
|
|
6
|
+
type Cookie as JarCookie,
|
|
7
|
+
mergeCookies,
|
|
8
|
+
} from "../cookies/index.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thin wrapper over Vercel Labs' `agent-browser` Rust CLI.
|
|
12
|
+
*
|
|
13
|
+
* agent-browser is a process that wraps a managed Chrome for Testing
|
|
14
|
+
* binary and exposes verbs over its own daemon (state persists across
|
|
15
|
+
* invocations within a shell). This wrapper exec's the binary; it does
|
|
16
|
+
* **not** manage the daemon itself.
|
|
17
|
+
*
|
|
18
|
+
* Optional cookie-jar integration: if a CookieJar is passed in the
|
|
19
|
+
* options, the wrapper will write the jar contents to a temp state file
|
|
20
|
+
* and `state load` it before any nav, then `cookies get` and merge the
|
|
21
|
+
* result back into the jar after work is done. Cookies are best-effort:
|
|
22
|
+
* the wrapper never fails because a load/save round-trip blew up.
|
|
23
|
+
*
|
|
24
|
+
* Canonical for `harn browse-ai` (dev side); ready to be the shared core of
|
|
25
|
+
* the agent-side `browse` wrapper (sandbox side) when that wrapper gets refactored.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface AgentBrowserOptions {
|
|
29
|
+
/** Override the binary path. Default: looks up `agent-browser` on PATH. */
|
|
30
|
+
binary?: string;
|
|
31
|
+
/** Cookie jar for cross-tool sharing. Pass `null` to skip. */
|
|
32
|
+
jar?: CookieJar | null;
|
|
33
|
+
/** Default per-call timeout in ms. Default 60000. */
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
/** Extra env vars to merge into every spawn. */
|
|
36
|
+
env?: Record<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* Path used as the `state load` source when seeding cookies into the
|
|
39
|
+
* agent-browser session. Defaults to a tmp file derived from the jar.
|
|
40
|
+
*/
|
|
41
|
+
stateFilePath?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ExecResult {
|
|
45
|
+
stdout: string;
|
|
46
|
+
stderr: string;
|
|
47
|
+
ok: boolean;
|
|
48
|
+
exitCode: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
52
|
+
|
|
53
|
+
export class AgentBrowser {
|
|
54
|
+
private readonly opts: AgentBrowserOptions;
|
|
55
|
+
private cookiesSeeded = false;
|
|
56
|
+
|
|
57
|
+
constructor(opts: AgentBrowserOptions = {}) {
|
|
58
|
+
this.opts = opts;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run an `agent-browser` subcommand. Returns a structured result.
|
|
63
|
+
* Throws only on spawn failure (e.g., binary not found); non-zero exit
|
|
64
|
+
* returns ok=false.
|
|
65
|
+
*/
|
|
66
|
+
exec(args: string[], timeoutMs?: number): ExecResult {
|
|
67
|
+
const binary = this.opts.binary ?? "agent-browser";
|
|
68
|
+
const env = this.scrubEnv();
|
|
69
|
+
const result = spawnSync(binary, args, {
|
|
70
|
+
timeout: timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
env,
|
|
73
|
+
} satisfies SpawnSyncOptions);
|
|
74
|
+
|
|
75
|
+
if (result.error && (result.error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`agent-browser binary not found at "${binary}". Install once with:\n curl -fsSL https://github.com/vercel-labs/agent-browser/releases/download/v0.25.4/agent-browser-linux-x64 \\\n -o ~/.local/bin/agent-browser && chmod +x ~/.local/bin/agent-browser\n agent-browser install`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (result.error) {
|
|
81
|
+
throw result.error;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
stdout: (result.stdout ?? "").toString(),
|
|
85
|
+
stderr: (result.stderr ?? "").toString(),
|
|
86
|
+
ok: result.status === 0,
|
|
87
|
+
exitCode: result.status,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Same as `exec` but throws on non-zero exit code, with stderr in the
|
|
93
|
+
* error message. Use when the caller can't recover from a failure.
|
|
94
|
+
*/
|
|
95
|
+
execOrThrow(args: string[], timeoutMs?: number): ExecResult {
|
|
96
|
+
const result = this.exec(args, timeoutMs);
|
|
97
|
+
if (!result.ok) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`agent-browser ${args.join(" ")} failed (exit ${result.exitCode ?? "?"}): ${result.stderr.trim() || result.stdout.trim()}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// High-level verbs
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Seed the jar's cookies into the agent-browser session via `state load`.
|
|
111
|
+
* Called automatically by `open()`. Safe to call multiple times; only
|
|
112
|
+
* the first call does work.
|
|
113
|
+
*/
|
|
114
|
+
seedCookies(): void {
|
|
115
|
+
if (this.cookiesSeeded) return;
|
|
116
|
+
if (!this.opts.jar) {
|
|
117
|
+
this.cookiesSeeded = true;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const jarStore = this.opts.jar.load();
|
|
122
|
+
if (jarStore.cookies.length === 0) {
|
|
123
|
+
this.cookiesSeeded = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const stateFile =
|
|
127
|
+
this.opts.stateFilePath ?? `/tmp/bp-agent-browser-state-${process.pid}.json`;
|
|
128
|
+
writeFileSync(stateFile, JSON.stringify(jarStore, null, 2));
|
|
129
|
+
this.exec(["state", "load", stateFile], 10_000);
|
|
130
|
+
this.cookiesSeeded = true;
|
|
131
|
+
} catch {
|
|
132
|
+
// Cookie seeding is best-effort; never fail the session because
|
|
133
|
+
// the jar couldn't be written/loaded.
|
|
134
|
+
this.cookiesSeeded = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
open(url: string, timeoutMs?: number): ExecResult {
|
|
139
|
+
this.seedCookies();
|
|
140
|
+
return this.execOrThrow(["open", url], timeoutMs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
snapshot(opts: { interactive?: boolean } = {}): string {
|
|
144
|
+
const args = ["snapshot"];
|
|
145
|
+
if (opts.interactive) args.push("-i");
|
|
146
|
+
return this.execOrThrow(args).stdout.trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
screenshot(path: string, opts: { full?: boolean; annotate?: boolean } = {}): void {
|
|
150
|
+
const args = ["screenshot"];
|
|
151
|
+
if (opts.full) args.push("--full");
|
|
152
|
+
if (opts.annotate) args.push("--annotate");
|
|
153
|
+
args.push(path);
|
|
154
|
+
this.execOrThrow(args);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
click(refOrSelector: string): ExecResult {
|
|
158
|
+
return this.execOrThrow(["click", refOrSelector]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fill(refOrSelector: string, value: string): ExecResult {
|
|
162
|
+
return this.execOrThrow(["fill", refOrSelector, value]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
press(key: string): ExecResult {
|
|
166
|
+
return this.execOrThrow(["press", key]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
wait(selectorOrMs: string): ExecResult {
|
|
170
|
+
return this.execOrThrow(["wait", selectorOrMs]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
evaluate(script: string): string {
|
|
174
|
+
return this.execOrThrow(["eval", script]).stdout.trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Run a semicolon-separated batch of agent-browser sub-commands in a
|
|
179
|
+
* single session. Each step is exec'd with the same daemon, so state
|
|
180
|
+
* persists. Returns one ExecResult per step in order.
|
|
181
|
+
*/
|
|
182
|
+
batch(steps: string[]): ExecResult[] {
|
|
183
|
+
this.seedCookies();
|
|
184
|
+
const results: ExecResult[] = [];
|
|
185
|
+
for (const step of steps) {
|
|
186
|
+
const trimmed = step.trim();
|
|
187
|
+
if (!trimmed) continue;
|
|
188
|
+
// Naive shell-like splitting; agent-browser's own argv parser handles
|
|
189
|
+
// the actual command; we just split on whitespace for the wrapper.
|
|
190
|
+
const args = trimmed.split(/\s+/);
|
|
191
|
+
results.push(this.exec(args));
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Start HAR recording. Pair with `harStop`. */
|
|
197
|
+
harStart(path: string): ExecResult {
|
|
198
|
+
return this.execOrThrow(["network", "har", "start", path]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Stop HAR recording. Returns the same result for symmetry with start. */
|
|
202
|
+
harStop(path: string): ExecResult {
|
|
203
|
+
return this.execOrThrow(["network", "har", "stop", path]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Pull cookies out of the agent-browser session via `cookies get --json`,
|
|
208
|
+
* merge them into the jar (if one was provided), and persist. Best-effort:
|
|
209
|
+
* a parse failure or missing jar returns 0 without raising.
|
|
210
|
+
*/
|
|
211
|
+
syncCookiesToJar(): { saved: number } {
|
|
212
|
+
if (!this.opts.jar) return { saved: 0 };
|
|
213
|
+
try {
|
|
214
|
+
const result = this.exec(["cookies", "get", "--json"], 10_000);
|
|
215
|
+
if (!result.ok) return { saved: 0 };
|
|
216
|
+
const parsed = JSON.parse(result.stdout);
|
|
217
|
+
const cookies: JarCookie[] = parsed.data?.cookies ?? parsed.cookies ?? [];
|
|
218
|
+
if (!Array.isArray(cookies) || cookies.length === 0) return { saved: 0 };
|
|
219
|
+
const jar = this.opts.jar;
|
|
220
|
+
const store: CookieStore = jar.load();
|
|
221
|
+
const merged = mergeCookies(store, cookies);
|
|
222
|
+
jar.save(merged);
|
|
223
|
+
return { saved: cookies.length };
|
|
224
|
+
} catch {
|
|
225
|
+
return { saved: 0 };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Some shell environments leak XDG_CONFIG_HOME into Chrome's crashpad
|
|
231
|
+
* lookup, which can cause SIGTRAP if the path is read-only (e.g. in
|
|
232
|
+
* containerized sandbox environments). Strip it here defensively.
|
|
233
|
+
*/
|
|
234
|
+
private scrubEnv(): NodeJS.ProcessEnv {
|
|
235
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ...this.opts.env };
|
|
236
|
+
env.XDG_CONFIG_HOME = undefined;
|
|
237
|
+
return env;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AgentBrowser, type AgentBrowserOptions, type ExecResult } from "./client.js";
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
type BrowserContext,
|
|
6
|
+
type ConsoleMessage,
|
|
7
|
+
chromium,
|
|
8
|
+
type Page,
|
|
9
|
+
type Cookie as PWCookie,
|
|
10
|
+
type Request,
|
|
11
|
+
} from "playwright";
|
|
12
|
+
import type { CookieJar, Cookie as JarCookie } from "../cookies/index.ts";
|
|
13
|
+
import {
|
|
14
|
+
buildClearLayoutAnnotationsScript,
|
|
15
|
+
buildLayoutAnnotateScript,
|
|
16
|
+
buildOverflowCheck,
|
|
17
|
+
buildWidthCheck,
|
|
18
|
+
type OverflowResult,
|
|
19
|
+
type WidthResult,
|
|
20
|
+
} from "./layout.js";
|
|
21
|
+
import {
|
|
22
|
+
buildAnnotateScript,
|
|
23
|
+
buildClearAnnotationsScript,
|
|
24
|
+
buildVisibilityCheck,
|
|
25
|
+
type CheckVisibilityOptions,
|
|
26
|
+
type VisibilityResult,
|
|
27
|
+
} from "./visibility.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Headless-Chromium wrapper for the `browse` command.
|
|
31
|
+
*
|
|
32
|
+
* Two persistence layers:
|
|
33
|
+
* 1. Persistent profile (Playwright's `launchPersistentContext`) keeps
|
|
34
|
+
* browser state (localStorage, IndexedDB, login session) across runs.
|
|
35
|
+
* 2. Optional cookie jar, shared with `fetch`/`cookies` so a session
|
|
36
|
+
* built up in one tool is visible to the others.
|
|
37
|
+
*
|
|
38
|
+
* Designed to be opened, used for one or more navigations, and closed.
|
|
39
|
+
* It is not a long-lived service. For multi-step workflows, the caller drives
|
|
40
|
+
* `navigate`/`click`/`fill` directly between `open()` and `close()`.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export interface BrowserOptions {
|
|
44
|
+
/** Persistent profile dir. Default `~/.cache/harnery/browser-profile/`. Created if missing. */
|
|
45
|
+
profileDir?: string;
|
|
46
|
+
/** Launch headed (visible window). Default false. */
|
|
47
|
+
headed?: boolean;
|
|
48
|
+
/** Cookie jar to seed/sync with. Pass `null` to skip jar entirely. */
|
|
49
|
+
jar?: CookieJar | null;
|
|
50
|
+
/** Viewport. Default 1280x800. */
|
|
51
|
+
viewport?: { width: number; height: number };
|
|
52
|
+
/** Default navigation timeout in ms. Default 30000. */
|
|
53
|
+
navigationTimeout?: number;
|
|
54
|
+
/**
|
|
55
|
+
* `wait_until` strategy for `navigate`. Default `"load"`.
|
|
56
|
+
* Use `"domcontentloaded"` for sites with long-running analytics scripts
|
|
57
|
+
* that never let `"load"` fire.
|
|
58
|
+
*/
|
|
59
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
|
|
60
|
+
/**
|
|
61
|
+
* If set, record network traffic to a HAR file at this absolute path.
|
|
62
|
+
* The HAR is finalized when `close()` is called.
|
|
63
|
+
*/
|
|
64
|
+
recordHarPath?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Optional callback returning extra headers to attach to every request,
|
|
67
|
+
* keyed by request URL. Consumers can inject extra HTTP headers per-URL
|
|
68
|
+
* via this callback (e.g., a Cloudflare-bypass header for specific zones).
|
|
69
|
+
*/
|
|
70
|
+
extraHeaders?: (url: string) => Record<string, string>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface NavigateResult {
|
|
74
|
+
url: string;
|
|
75
|
+
title: string;
|
|
76
|
+
status: number | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ConsoleEvent {
|
|
80
|
+
type: string; // 'log' | 'error' | 'warning' | ...
|
|
81
|
+
text: string;
|
|
82
|
+
location?: { url: string; lineNumber?: number; columnNumber?: number };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PageErrorEvent {
|
|
86
|
+
message: string;
|
|
87
|
+
stack?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface FailedRequest {
|
|
91
|
+
url: string;
|
|
92
|
+
method: string;
|
|
93
|
+
failure: string;
|
|
94
|
+
resourceType: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Diagnostics {
|
|
98
|
+
consoleEvents: ConsoleEvent[];
|
|
99
|
+
consoleErrors: ConsoleEvent[];
|
|
100
|
+
pageErrors: PageErrorEvent[];
|
|
101
|
+
failedRequests: FailedRequest[];
|
|
102
|
+
viewport: { width: number; height: number } | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const DEFAULT_PROFILE = resolve(homedir(), ".cache", "harnery", "browser-profile");
|
|
106
|
+
|
|
107
|
+
export class Browser {
|
|
108
|
+
private context: BrowserContext | null = null;
|
|
109
|
+
private page: Page | null = null;
|
|
110
|
+
readonly profileDir: string;
|
|
111
|
+
private consoleEvents: ConsoleEvent[] = [];
|
|
112
|
+
private pageErrors: PageErrorEvent[] = [];
|
|
113
|
+
private failedRequests: FailedRequest[] = [];
|
|
114
|
+
|
|
115
|
+
constructor(private opts: BrowserOptions = {}) {
|
|
116
|
+
this.profileDir = opts.profileDir ?? DEFAULT_PROFILE;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Lazy: caller-side helper to find the active page if mid-flow. */
|
|
120
|
+
get currentPage(): Page {
|
|
121
|
+
if (!this.page) {
|
|
122
|
+
throw new Error("Browser not opened. Call open() first.");
|
|
123
|
+
}
|
|
124
|
+
return this.page;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async open(): Promise<void> {
|
|
128
|
+
if (this.context) return;
|
|
129
|
+
mkdirSync(this.profileDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
this.context = await chromium.launchPersistentContext(this.profileDir, {
|
|
132
|
+
headless: !this.opts.headed,
|
|
133
|
+
viewport: this.opts.viewport ?? { width: 1280, height: 800 },
|
|
134
|
+
...(this.opts.recordHarPath
|
|
135
|
+
? { recordHar: { path: this.opts.recordHarPath, mode: "full" as const } }
|
|
136
|
+
: {}),
|
|
137
|
+
});
|
|
138
|
+
this.context.setDefaultNavigationTimeout(this.opts.navigationTimeout ?? 30_000);
|
|
139
|
+
|
|
140
|
+
if (this.opts.jar) {
|
|
141
|
+
const jarCookies = this.opts.jar.list();
|
|
142
|
+
if (jarCookies.length > 0) {
|
|
143
|
+
await this.context.addCookies(jarCookies.map(toPWCookie));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Caller-injected extraHeaders callback (e.g., for Cloudflare-bypass
|
|
148
|
+
// or custom auth headers). Per-request route handler so headers only
|
|
149
|
+
// attach when the callback returns non-empty.
|
|
150
|
+
const headersCb = this.opts.extraHeaders;
|
|
151
|
+
if (headersCb) {
|
|
152
|
+
await this.context.route("**/*", async (route, request) => {
|
|
153
|
+
const extra = headersCb(request.url());
|
|
154
|
+
if (Object.keys(extra).length === 0) return route.continue();
|
|
155
|
+
const headers = { ...request.headers(), ...extra };
|
|
156
|
+
return route.continue({ headers });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const pages = this.context.pages();
|
|
161
|
+
this.page = pages[0] ?? (await this.context.newPage());
|
|
162
|
+
this.attachDiagnosticListeners(this.page);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Hook console + pageerror + requestfailed events. Called on `open()`
|
|
167
|
+
* before any navigation so we don't miss early-fired events.
|
|
168
|
+
*/
|
|
169
|
+
private attachDiagnosticListeners(page: Page): void {
|
|
170
|
+
page.on("console", (msg: ConsoleMessage) => {
|
|
171
|
+
const loc = msg.location();
|
|
172
|
+
this.consoleEvents.push({
|
|
173
|
+
type: msg.type(),
|
|
174
|
+
text: msg.text(),
|
|
175
|
+
location: loc.url
|
|
176
|
+
? { url: loc.url, lineNumber: loc.lineNumber, columnNumber: loc.columnNumber }
|
|
177
|
+
: undefined,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
page.on("pageerror", (err: Error) => {
|
|
181
|
+
this.pageErrors.push({ message: err.message, stack: err.stack });
|
|
182
|
+
});
|
|
183
|
+
page.on("requestfailed", (req: Request) => {
|
|
184
|
+
this.failedRequests.push({
|
|
185
|
+
url: req.url(),
|
|
186
|
+
method: req.method(),
|
|
187
|
+
failure: req.failure()?.errorText ?? "unknown",
|
|
188
|
+
resourceType: req.resourceType(),
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Snapshot of every event captured since `open()`. Returned objects are
|
|
195
|
+
* copies, so callers can safely store them after `close()`.
|
|
196
|
+
*/
|
|
197
|
+
diagnostics(): Diagnostics {
|
|
198
|
+
return {
|
|
199
|
+
consoleEvents: [...this.consoleEvents],
|
|
200
|
+
consoleErrors: this.consoleEvents.filter((e) => e.type === "error"),
|
|
201
|
+
pageErrors: [...this.pageErrors],
|
|
202
|
+
failedRequests: [...this.failedRequests],
|
|
203
|
+
viewport: this.page?.viewportSize() ?? null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async navigate(url: string): Promise<NavigateResult> {
|
|
208
|
+
const page = this.currentPage;
|
|
209
|
+
const response = await page.goto(url, { waitUntil: this.opts.waitUntil ?? "load" });
|
|
210
|
+
return {
|
|
211
|
+
url: page.url(),
|
|
212
|
+
title: await page.title(),
|
|
213
|
+
status: response?.status() ?? null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Reload the current page. Preserves cookies + sessionStorage so callers can
|
|
219
|
+
* reproduce sessionStorage-restored UI state (e.g. drawers/modals that open
|
|
220
|
+
* automatically on reload, where Dialog auto-focus + Tooltip-on-focus may
|
|
221
|
+
* interact differently than the click-to-open path).
|
|
222
|
+
*/
|
|
223
|
+
async reload(): Promise<NavigateResult> {
|
|
224
|
+
const page = this.currentPage;
|
|
225
|
+
const response = await page.reload({ waitUntil: this.opts.waitUntil ?? "load" });
|
|
226
|
+
return {
|
|
227
|
+
url: page.url(),
|
|
228
|
+
title: await page.title(),
|
|
229
|
+
status: response?.status() ?? null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Full-page PNG screenshot. Returns the byte count written. */
|
|
234
|
+
async screenshot(path: string, opts: { fullPage?: boolean } = {}): Promise<number> {
|
|
235
|
+
const page = this.currentPage;
|
|
236
|
+
const buf = await page.screenshot({ path, fullPage: opts.fullPage ?? true, type: "png" });
|
|
237
|
+
return buf.length;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Plain-text snapshot of the document body. Suitable as a coarse "what's
|
|
242
|
+
* on screen" signal for LLM iteration loops. For richer extraction, use
|
|
243
|
+
* `htmlContent()` and pipe through a readability filter.
|
|
244
|
+
*/
|
|
245
|
+
async textSnapshot(): Promise<string> {
|
|
246
|
+
const page = this.currentPage;
|
|
247
|
+
return await page.evaluate(() => document.body?.innerText ?? "");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Raw outer HTML of the page (or a selector if provided). */
|
|
251
|
+
async htmlContent(selector?: string): Promise<string> {
|
|
252
|
+
const page = this.currentPage;
|
|
253
|
+
if (selector) {
|
|
254
|
+
const el = await page.$(selector);
|
|
255
|
+
if (!el) throw new Error(`Selector matched nothing: ${selector}`);
|
|
256
|
+
return await el.evaluate((node) => (node as Element).outerHTML);
|
|
257
|
+
}
|
|
258
|
+
return await page.content();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async click(selector: string): Promise<void> {
|
|
262
|
+
await this.currentPage.click(selector);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async fill(selector: string, value: string): Promise<void> {
|
|
266
|
+
await this.currentPage.fill(selector, value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async press(key: string): Promise<void> {
|
|
270
|
+
await this.currentPage.keyboard.press(key);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async waitForSelector(selector: string, timeout?: number): Promise<void> {
|
|
274
|
+
await this.currentPage.waitForSelector(selector, timeout ? { timeout } : undefined);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Evaluate JS in the page context. Caller is responsible for safety. */
|
|
278
|
+
async evaluate<T = unknown>(script: string): Promise<T> {
|
|
279
|
+
return await this.currentPage.evaluate(script);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Read the system clipboard via the page context. Grants `clipboard-read`
|
|
284
|
+
* to the page's origin first because Chromium gates `navigator.clipboard
|
|
285
|
+
* .readText()` behind a user-gesture + permission check; in headless
|
|
286
|
+
* Playwright there is no user gesture, so the permission grant is the
|
|
287
|
+
* substitute. Returns an empty string if the read returns nullish or
|
|
288
|
+
* throws (insecure context, focus race). Used by `browse --batch
|
|
289
|
+
* clipboard ...` to verify a UI Copy action end-to-end.
|
|
290
|
+
*/
|
|
291
|
+
async readClipboard(): Promise<string> {
|
|
292
|
+
if (!this.context) throw new Error("Browser not opened. Call open() first.");
|
|
293
|
+
const url = this.currentPage.url();
|
|
294
|
+
try {
|
|
295
|
+
const origin = new URL(url).origin;
|
|
296
|
+
await this.context.grantPermissions(["clipboard-read"], { origin });
|
|
297
|
+
} catch {
|
|
298
|
+
/* about:blank / data: URL, skip permission; evaluate may still work */
|
|
299
|
+
}
|
|
300
|
+
return await this.currentPage.evaluate(async () => {
|
|
301
|
+
try {
|
|
302
|
+
const text = await navigator.clipboard.readText();
|
|
303
|
+
return typeof text === "string" ? text : "";
|
|
304
|
+
} catch {
|
|
305
|
+
return "";
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Run occlusion checks on one or more selectors. For each, samples a grid
|
|
312
|
+
* of points inside the element's bounding rect and uses
|
|
313
|
+
* `document.elementFromPoint` to detect whether the target is the topmost
|
|
314
|
+
* paintable element at each sample. Catches the class of UI bugs where
|
|
315
|
+
* an element's rect IS in-viewport but a higher-z-index sibling is
|
|
316
|
+
* painting over it.
|
|
317
|
+
*/
|
|
318
|
+
async checkVisibility(
|
|
319
|
+
selectors: string[],
|
|
320
|
+
opts: CheckVisibilityOptions = {},
|
|
321
|
+
): Promise<VisibilityResult[]> {
|
|
322
|
+
return await this.currentPage.evaluate(buildVisibilityCheck(), {
|
|
323
|
+
selectors,
|
|
324
|
+
sampleGrid: opts.sampleGrid ?? 3,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Inject annotation overlays for visibility results. Used before screenshot. */
|
|
329
|
+
async annotateVisibility(results: VisibilityResult[]): Promise<void> {
|
|
330
|
+
await this.currentPage.evaluate(buildAnnotateScript(), { results });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Remove visibility annotation overlays. */
|
|
334
|
+
async clearVisibilityAnnotations(): Promise<void> {
|
|
335
|
+
await this.currentPage.evaluate(buildClearAnnotationsScript());
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Measure each selector's bounding rect + viewport-fill + parent-fill
|
|
340
|
+
* ratios. Catches the class of mobile-layout bug where a table sits at
|
|
341
|
+
* (say) 85% viewport fill because of stacked padding: every per-element
|
|
342
|
+
* check passes, but the user sees too-narrow content.
|
|
343
|
+
*/
|
|
344
|
+
async checkWidth(selectors: string[]): Promise<WidthResult[]> {
|
|
345
|
+
return await this.currentPage.evaluate(buildWidthCheck(), { selectors });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Detect horizontal overflow at the document level. Returns viewport size,
|
|
350
|
+
* `document.scrollWidth`, and the top N elements protruding past the
|
|
351
|
+
* viewport's right edge. Catches the class of bug where a nav/table is
|
|
352
|
+
* wider than the viewport, forcing horizontal scroll on mobile.
|
|
353
|
+
*/
|
|
354
|
+
async checkOverflow(opts: { sampleLimit?: number } = {}): Promise<OverflowResult> {
|
|
355
|
+
return await this.currentPage.evaluate(buildOverflowCheck(), {
|
|
356
|
+
sampleLimit: opts.sampleLimit ?? 5,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Inject annotation overlays for width + overflow results. Used before screenshot. */
|
|
361
|
+
async annotateLayout(args: {
|
|
362
|
+
widths: WidthResult[];
|
|
363
|
+
overflow: OverflowResult | null;
|
|
364
|
+
widthThreshold: number;
|
|
365
|
+
}): Promise<void> {
|
|
366
|
+
await this.currentPage.evaluate(buildLayoutAnnotateScript(), args);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Remove layout annotation overlays. */
|
|
370
|
+
async clearLayoutAnnotations(): Promise<void> {
|
|
371
|
+
await this.currentPage.evaluate(buildClearLayoutAnnotationsScript());
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Inject a script that runs in every page context before page scripts
|
|
376
|
+
* execute. Useful for seeding localStorage before an SSR/CSR comparison;
|
|
377
|
+
* without this, state-dependent hydration mismatches are invisible to a
|
|
378
|
+
* clean-profile probe. Must be called after `open()` and before
|
|
379
|
+
* `navigate()`.
|
|
380
|
+
*/
|
|
381
|
+
async addInitScript(script: string): Promise<void> {
|
|
382
|
+
if (!this.context) throw new Error("Browser not opened. Call open() first.");
|
|
383
|
+
await this.context.addInitScript(script);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Sync cookies from the live context back into the jar (if one was provided),
|
|
388
|
+
* then tear everything down. Safe to call multiple times.
|
|
389
|
+
*/
|
|
390
|
+
async close(): Promise<void> {
|
|
391
|
+
if (!this.context) return;
|
|
392
|
+
if (this.opts.jar) {
|
|
393
|
+
try {
|
|
394
|
+
const live = await this.context.cookies();
|
|
395
|
+
for (const c of live) {
|
|
396
|
+
this.opts.jar.set(toJarCookie(c));
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
// Cookie persist is best-effort; never block close on it.
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
await this.context.close().catch(() => {});
|
|
403
|
+
this.context = null;
|
|
404
|
+
this.page = null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Cookie shape conversion
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
function toPWCookie(c: JarCookie): PWCookie {
|
|
413
|
+
// Playwright's PWCookie type requires sameSite to be a literal; undefined
|
|
414
|
+
// is not allowed. Default to "Lax" (matches Chromium's modern default).
|
|
415
|
+
return {
|
|
416
|
+
name: c.name,
|
|
417
|
+
value: c.value,
|
|
418
|
+
domain: c.domain,
|
|
419
|
+
path: c.path,
|
|
420
|
+
expires: c.expires,
|
|
421
|
+
httpOnly: c.httpOnly,
|
|
422
|
+
secure: c.secure,
|
|
423
|
+
sameSite: normalizeSameSite(c.sameSite) ?? "Lax",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function toJarCookie(c: PWCookie): JarCookie {
|
|
428
|
+
return {
|
|
429
|
+
name: c.name,
|
|
430
|
+
value: c.value,
|
|
431
|
+
domain: c.domain,
|
|
432
|
+
path: c.path,
|
|
433
|
+
expires: c.expires,
|
|
434
|
+
httpOnly: c.httpOnly,
|
|
435
|
+
secure: c.secure,
|
|
436
|
+
sameSite: c.sameSite ?? undefined,
|
|
437
|
+
session: c.expires <= 0,
|
|
438
|
+
size: c.name.length + c.value.length,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizeSameSite(s: string | undefined): "Strict" | "Lax" | "None" | undefined {
|
|
443
|
+
if (!s) return undefined;
|
|
444
|
+
const lower = s.toLowerCase();
|
|
445
|
+
if (lower === "strict") return "Strict";
|
|
446
|
+
if (lower === "lax") return "Lax";
|
|
447
|
+
if (lower === "none") return "None";
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|