@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.6
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/CHANGELOG.md +69 -0
- package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
- package/dist/cli.js +4438 -3962
- package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
- package/dist/types/cli/args.d.ts +4 -1
- package/dist/types/cli/browser-relay-cli.d.ts +15 -0
- package/dist/types/cli/command-help.d.ts +99 -0
- package/dist/types/cli/gc-cli.d.ts +1 -0
- package/dist/types/cli/help-extra.d.ts +2 -0
- package/dist/types/cli/thinking-levels.d.ts +5 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/commands/browser-relay.d.ts +38 -0
- package/dist/types/commands/commit.d.ts +3 -0
- package/dist/types/commands/complete.d.ts +1 -1
- package/dist/types/commands/completions.d.ts +6 -0
- package/dist/types/commands/grep.d.ts +3 -0
- package/dist/types/commands/join.d.ts +4 -0
- package/dist/types/commands/launch-help.d.ts +171 -0
- package/dist/types/commands/launch.d.ts +5 -1
- package/dist/types/commands/models.d.ts +3 -0
- package/dist/types/commands/say.d.ts +10 -0
- package/dist/types/commands/worktree.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-discovery.d.ts +2 -2
- package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/models-config.d.ts +1 -0
- package/dist/types/config/service-tier.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +69 -21
- package/dist/types/cursor-bridge-tools.d.ts +1 -1
- package/dist/types/cursor.d.ts +7 -4
- package/dist/types/edit/index.d.ts +3 -3
- package/dist/types/edit/modes/replace.d.ts +22 -15
- package/dist/types/edit/renderer.d.ts +1 -1
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/spawn-options.d.ts +12 -16
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/launch/broker.d.ts +1 -1
- package/dist/types/launch/client.d.ts +15 -4
- package/dist/types/launch/ensure.d.ts +8 -0
- package/dist/types/launch/paths.d.ts +1 -1
- package/dist/types/launch/protocol.d.ts +19 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/mux/daemon.d.ts +15 -0
- package/dist/types/lsp/mux/protocol.d.ts +57 -0
- package/dist/types/lsp/mux/server.d.ts +18 -0
- package/dist/types/lsp/types.d.ts +22 -2
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mnemopi/embed-client.d.ts +1 -1
- package/dist/types/mnemopi/state.d.ts +0 -23
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/btw-panel.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/workflow.d.ts +2 -1
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/security/contracts/schemas.d.ts +405 -403
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/launch-completion.d.ts +10 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +2 -1
- package/dist/types/session/session-entries.d.ts +15 -1
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +23 -11
- package/dist/types/session/session-stats.d.ts +10 -0
- package/dist/types/session/session-storage.d.ts +15 -3
- package/dist/types/session/yield-queue.d.ts +6 -1
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/spawn-policy.d.ts +5 -0
- package/dist/types/thinking.d.ts +1 -7
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +2 -1
- package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
- package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
- package/dist/types/tools/browser/relay/kind.d.ts +19 -0
- package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
- package/dist/types/tools/browser/relay/server.d.ts +35 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -0
- package/dist/types/tools/computer/exposure.d.ts +9 -7
- package/dist/types/tools/computer/protocol.d.ts +70 -17
- package/dist/types/tools/computer/supervisor.d.ts +23 -10
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +42 -7
- package/dist/types/tools/computer-renderer.d.ts +7 -5
- package/dist/types/tools/computer.d.ts +25 -44
- package/dist/types/tools/context.d.ts +3 -0
- package/dist/types/tools/glob.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +1 -1
- package/dist/types/tools/index.d.ts +9 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/tools/xdev.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +2 -2
- package/dist/types/web/kagi.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/providers/anthropic.d.ts +1 -0
- package/dist/types/web/search/providers/base.d.ts +2 -0
- package/dist/types/web/search/providers/brave.d.ts +1 -0
- package/dist/types/web/search/providers/browser-page.d.ts +1 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -0
- package/dist/types/web/search/providers/exa.d.ts +1 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/dist/types/web/search/providers/jina.d.ts +1 -0
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -0
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +1 -0
- package/dist/types/web/search/providers/searxng.d.ts +1 -0
- package/dist/types/web/search/providers/tavily.d.ts +1 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +6 -9
- package/dist/types/web/search/providers/zai.d.ts +1 -0
- package/dist/types/web/search/types.d.ts +4 -0
- package/package.json +12 -12
- package/src/cli/args.ts +7 -87
- package/src/cli/browser-relay-cli.ts +119 -0
- package/src/cli/command-help.ts +103 -0
- package/src/cli/completion-gen.ts +5 -5
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gc-cli.ts +641 -21
- package/src/cli/help-extra.ts +89 -0
- package/src/cli/setup-cli.ts +13 -21
- package/src/cli/thinking-levels.ts +7 -0
- package/src/cli/update-cli.ts +26 -7
- package/src/cli-commands.ts +170 -33
- package/src/cli.ts +17 -5
- package/src/commands/acp.ts +3 -1
- package/src/commands/agents.ts +3 -2
- package/src/commands/auth-broker.ts +3 -2
- package/src/commands/auth-gateway.ts +3 -2
- package/src/commands/bench.ts +2 -3
- package/src/commands/browser-relay.ts +53 -0
- package/src/commands/cleanse.ts +2 -2
- package/src/commands/commit.ts +3 -2
- package/src/commands/complete.ts +2 -1
- package/src/commands/completions.ts +3 -2
- package/src/commands/config.ts +3 -2
- package/src/commands/dry-balance.ts +2 -2
- package/src/commands/gallery.ts +3 -2
- package/src/commands/gc.ts +3 -2
- package/src/commands/grep.ts +3 -2
- package/src/commands/grievances.ts +3 -2
- package/src/commands/install.ts +2 -2
- package/src/commands/join.ts +3 -2
- package/src/commands/launch-help.ts +116 -0
- package/src/commands/launch.ts +7 -191
- package/src/commands/models.ts +3 -2
- package/src/commands/plugin.ts +3 -2
- package/src/commands/read.ts +3 -2
- package/src/commands/say.ts +3 -2
- package/src/commands/setup.ts +3 -2
- package/src/commands/shell.ts +3 -2
- package/src/commands/ssh.ts +3 -2
- package/src/commands/stats.ts +3 -2
- package/src/commands/tiny-models.ts +2 -2
- package/src/commands/token.ts +2 -2
- package/src/commands/ttsr.ts +2 -2
- package/src/commands/update.ts +3 -2
- package/src/commands/usage.ts +3 -2
- package/src/commands/web-search.ts +3 -2
- package/src/commands/worktree.ts +3 -2
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-discovery.ts +44 -27
- package/src/config/model-registry.ts +19 -13
- package/src/config/models-config-schema-bundle.ts +9 -0
- package/src/config/resolve-config-value.ts +2 -1
- package/src/config/service-tier.ts +5 -0
- package/src/config/settings-schema.ts +72 -20
- package/src/config/settings.ts +1 -1
- package/src/cursor-bridge-tools.ts +1 -1
- package/src/cursor.ts +10 -14
- package/src/edit/diff.ts +17 -8
- package/src/edit/index.ts +20 -7
- package/src/edit/modes/replace.ts +93 -40
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +14 -17
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +24 -29
- package/src/export/html/index.ts +10 -3
- package/src/export/html/template.js +8 -26
- package/src/export/share.ts +4 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
- package/src/extensibility/skills.ts +11 -0
- package/src/extensibility/utils.ts +10 -2
- package/src/launch/broker.ts +223 -7
- package/src/launch/client.ts +192 -21
- package/src/launch/ensure.ts +73 -0
- package/src/launch/paths.ts +1 -1
- package/src/launch/protocol.ts +54 -2
- package/src/lsp/client.ts +23 -2
- package/src/lsp/index.ts +8 -0
- package/src/lsp/mux/daemon.ts +348 -0
- package/src/lsp/mux/protocol.ts +96 -0
- package/src/lsp/mux/server.ts +797 -0
- package/src/lsp/types.ts +28 -2
- package/src/main.ts +20 -8
- package/src/mcp/config-writer.ts +1 -1
- package/src/mcp/oauth-flow.ts +11 -1
- package/src/mnemopi/embed-client.ts +49 -2
- package/src/mnemopi/state.ts +57 -23
- package/src/modes/components/assistant-message.ts +11 -0
- package/src/modes/components/btw-panel.ts +41 -4
- package/src/modes/components/chat-transcript-builder.ts +4 -0
- package/src/modes/components/custom-editor.ts +9 -0
- package/src/modes/components/read-tool-group.ts +10 -0
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
- package/src/modes/components/tool-execution.ts +8 -0
- package/src/modes/components/tree-selector.ts +46 -48
- package/src/modes/controllers/btw-controller.ts +55 -7
- package/src/modes/controllers/command-controller.ts +31 -0
- package/src/modes/controllers/event-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +63 -8
- package/src/modes/controllers/mcp-command-controller.ts +11 -5
- package/src/modes/controllers/selector-controller.ts +20 -0
- package/src/modes/interactive-mode.ts +18 -2
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -10
- package/src/modes/workflow.ts +8 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/session/launch-completion.md +1 -0
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +26 -52
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/computer.md +21 -21
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/replace.md +5 -4
- package/src/prompts/tools/task.md +4 -4
- package/src/sdk.ts +49 -6
- package/src/secrets/index.ts +1 -1
- package/src/security/contracts/schemas.ts +205 -183
- package/src/security/contracts/validation.ts +5 -1
- package/src/security/store.ts +2 -2
- package/src/session/agent-session-types.ts +9 -1
- package/src/session/agent-session.ts +204 -15
- package/src/session/indexed-session-storage.ts +9 -0
- package/src/session/launch-completion.ts +37 -0
- package/src/session/prewalk.ts +24 -2
- package/src/session/session-advisors.ts +2 -2
- package/src/session/session-context.ts +42 -12
- package/src/session/session-dump-format.ts +1 -1
- package/src/session/session-entries.ts +17 -1
- package/src/session/session-maintenance.ts +38 -4
- package/src/session/session-manager.ts +123 -67
- package/src/session/session-paths.ts +118 -55
- package/src/session/session-stats.ts +48 -3
- package/src/session/session-storage.ts +30 -22
- package/src/session/stream-guards.ts +1 -1
- package/src/session/yield-queue.ts +121 -16
- package/src/slash-commands/builtin-registry.ts +37 -24
- package/src/system-prompt.ts +8 -3
- package/src/task/index.ts +23 -8
- package/src/task/spawn-policy.ts +14 -0
- package/src/thinking.ts +2 -8
- package/src/tools/ast-grep.ts +10 -4
- package/src/tools/browser/cmux/cmux-tab.ts +97 -14
- package/src/tools/browser/cmux/rpc.ts +2 -8
- package/src/tools/browser/registry.ts +53 -2
- package/src/tools/browser/relay/bridge.ts +945 -0
- package/src/tools/browser/relay/daemon.ts +117 -0
- package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
- package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
- package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
- package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
- package/src/tools/browser/relay/kind.ts +41 -0
- package/src/tools/browser/relay/protocol.ts +54 -0
- package/src/tools/browser/relay/server.ts +141 -0
- package/src/tools/browser/render.ts +4 -1
- package/src/tools/browser/shared-daemon.ts +7 -58
- package/src/tools/browser/tab-supervisor.ts +5 -4
- package/src/tools/browser/tab-worker.ts +33 -12
- package/src/tools/browser.ts +26 -1
- package/src/tools/computer/exposure.ts +11 -35
- package/src/tools/computer/protocol.ts +51 -10
- package/src/tools/computer/supervisor.ts +227 -124
- package/src/tools/computer/worker-entry.ts +11 -6
- package/src/tools/computer/worker.ts +695 -81
- package/src/tools/computer-renderer.ts +104 -65
- package/src/tools/computer.ts +134 -428
- package/src/tools/context.ts +3 -0
- package/src/tools/glob.ts +9 -2
- package/src/tools/grep.ts +12 -6
- package/src/tools/hub/launch.ts +122 -6
- package/src/tools/index.ts +9 -0
- package/src/tools/inspect-image.ts +32 -6
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/xdev.ts +29 -2
- package/src/utils/clipboard.ts +40 -14
- package/src/utils/command-args.ts +26 -28
- package/src/utils/tool-choice.ts +0 -13
- package/src/web/kagi.ts +2 -1
- package/src/web/search/index.ts +18 -2
- package/src/web/search/providers/anthropic.ts +4 -1
- package/src/web/search/providers/base.ts +2 -0
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/browser-page.ts +6 -4
- package/src/web/search/providers/codex.ts +4 -1
- package/src/web/search/providers/duckduckgo.ts +2 -1
- package/src/web/search/providers/ecosia.ts +2 -1
- package/src/web/search/providers/exa.ts +4 -2
- package/src/web/search/providers/firecrawl.ts +3 -1
- package/src/web/search/providers/gemini.ts +8 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +12 -2
- package/src/web/search/providers/kagi.ts +3 -0
- package/src/web/search/providers/kimi.ts +5 -1
- package/src/web/search/providers/mojeek.ts +2 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +6 -3
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/searxng.ts +11 -5
- package/src/web/search/providers/startpage.ts +10 -4
- package/src/web/search/providers/synthetic.ts +11 -5
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/tinyfish.ts +3 -1
- package/src/web/search/providers/utils.ts +12 -10
- package/src/web/search/providers/xai.ts +2 -2
- package/src/web/search/providers/zai.ts +18 -3
- package/src/web/search/types.ts +6 -0
- package/dist/types/config/file-lock.d.ts +0 -29
- package/src/config/file-lock.ts +0 -164
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker-owned browser relay daemon.
|
|
3
|
+
*
|
|
4
|
+
* The MV3 extension can only dial OUT (service workers cannot listen on
|
|
5
|
+
* sockets), so a native process must own the relay port. Instead of making
|
|
6
|
+
* the user run `omp browser-relay` by hand, the relay kind lazily starts one
|
|
7
|
+
* under a profile-independent, machine-global daemon broker. Every relay
|
|
8
|
+
* consumer holds a connection to that broker, so one project exiting cannot
|
|
9
|
+
* tear down the fixed-port singleton while another project still uses it.
|
|
10
|
+
*
|
|
11
|
+
* A manually started relay may already own the port. Consumers still acquire
|
|
12
|
+
* the global broker lease before probing, then adopt that external server
|
|
13
|
+
* without attempting another bind.
|
|
14
|
+
*/
|
|
15
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { daemonClientForGlobal } from "../../../launch/client";
|
|
17
|
+
import { describeQuietly, stopQuietly, waitReady } from "../../../launch/ensure";
|
|
18
|
+
import { resolveWorkerSpawnCmd } from "../../../subprocess/worker-client";
|
|
19
|
+
import { throwIfAborted } from "../../tool-errors";
|
|
20
|
+
|
|
21
|
+
/** Stable broker daemon name for the relay server. */
|
|
22
|
+
export const RELAY_DAEMON_NAME = "omp.browser.relay";
|
|
23
|
+
const RELAY_BROKER_SCOPE = "browser-relay";
|
|
24
|
+
/** Matches the serve banner (`omp browser relay listening on http://…`). */
|
|
25
|
+
const READY_LOG_PATTERN = String.raw`browser relay listening on http://\S+`;
|
|
26
|
+
const READY_TIMEOUT_MS = 15_000;
|
|
27
|
+
const PROBE_TIMEOUT_MS = 1_500;
|
|
28
|
+
/** probe→describe→start rounds; bounds cross-process races and wedged-relay replacement. */
|
|
29
|
+
const ENSURE_ATTEMPTS = 3;
|
|
30
|
+
|
|
31
|
+
/** True when the relay HTTP server answers /json/version at all (200 = extension connected, 503 = waiting for it). */
|
|
32
|
+
export async function probeRelayServer(cdpUrl: string): Promise<boolean> {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${cdpUrl}/json/version`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
35
|
+
await res.body?.cancel();
|
|
36
|
+
return res.ok || res.status === 503;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Auto-start is only safe for endpoints this machine can own. */
|
|
43
|
+
export function isLoopbackRelayUrl(cdpUrl: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const { hostname } = new URL(cdpUrl);
|
|
46
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ensure a relay server answers at `cdpUrl`, starting the broker-owned daemon
|
|
54
|
+
* when nothing is serving. Returns true once the HTTP endpoint responds — the
|
|
55
|
+
* extension handshake (503 → 200) is the caller's wait. False when the relay
|
|
56
|
+
* could not be started (broker unavailable or start rounds exhausted).
|
|
57
|
+
*/
|
|
58
|
+
export async function ensureRelayDaemon(opts: { cdpUrl: string; signal?: AbortSignal }): Promise<boolean> {
|
|
59
|
+
let port: string;
|
|
60
|
+
try {
|
|
61
|
+
port = String(new URL(opts.cdpUrl).port || 80);
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// Open the lazy client before probing. Merely caching SocketDaemonClient
|
|
66
|
+
// would not create the broker connection (and therefore would hold no lease).
|
|
67
|
+
const client = await daemonClientForGlobal(RELAY_BROKER_SCOPE);
|
|
68
|
+
throwIfAborted(opts.signal);
|
|
69
|
+
await client.request({ op: "ping" }, opts.signal);
|
|
70
|
+
if (await probeRelayServer(opts.cdpUrl)) return true;
|
|
71
|
+
const spawn = resolveWorkerSpawnCmd("browser-relay");
|
|
72
|
+
for (let attempt = 0; attempt < ENSURE_ATTEMPTS; attempt++) {
|
|
73
|
+
throwIfAborted(opts.signal);
|
|
74
|
+
// A manual serve or concurrent global-broker start may have won the
|
|
75
|
+
// port since the last round; adopt it instead of fighting the bind.
|
|
76
|
+
if (await probeRelayServer(opts.cdpUrl)) return true;
|
|
77
|
+
const existing = await describeQuietly(client, RELAY_DAEMON_NAME, "Browser relay", opts.signal);
|
|
78
|
+
if (existing && existing.state !== "exited" && existing.state !== "failed") {
|
|
79
|
+
if (existing.readyAt === undefined) await waitReady(client, RELAY_DAEMON_NAME, "Browser relay", opts.signal);
|
|
80
|
+
if (await probeRelayServer(opts.cdpUrl)) return true;
|
|
81
|
+
// Live record but nothing listening: replace the wedged daemon.
|
|
82
|
+
await stopQuietly(client, RELAY_DAEMON_NAME, "Browser relay", opts.signal);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const started = await client.request(
|
|
87
|
+
{
|
|
88
|
+
op: "start",
|
|
89
|
+
spec: {
|
|
90
|
+
name: RELAY_DAEMON_NAME,
|
|
91
|
+
application: spawn.cmd[0]!,
|
|
92
|
+
args: [...spawn.cmd.slice(1), "--port", port],
|
|
93
|
+
env: {},
|
|
94
|
+
cwd: spawn.cwd ?? client.projectDir,
|
|
95
|
+
pty: false,
|
|
96
|
+
ready: { log: READY_LOG_PATTERN, timeoutMs: READY_TIMEOUT_MS },
|
|
97
|
+
restart: "no",
|
|
98
|
+
persist: false,
|
|
99
|
+
detached: false,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
opts.signal,
|
|
103
|
+
);
|
|
104
|
+
if (started.op !== "start") continue;
|
|
105
|
+
if (await probeRelayServer(opts.cdpUrl)) return true;
|
|
106
|
+
await stopQuietly(client, RELAY_DAEMON_NAME, "Browser relay", opts.signal);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throwIfAborted(opts.signal);
|
|
109
|
+
// Lost a cross-process start race; the next round adopts the winner.
|
|
110
|
+
logger.debug("Browser relay start contention", {
|
|
111
|
+
name: RELAY_DAEMON_NAME,
|
|
112
|
+
error: error instanceof Error ? error.message : String(error),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// extension/background.ts
|
|
2
|
+
var DEFAULT_PORT = 9224;
|
|
3
|
+
var PING_INTERVAL_MS = 20000;
|
|
4
|
+
var RECONNECT_MIN_MS = 1000;
|
|
5
|
+
var RECONNECT_MAX_MS = 1e4;
|
|
6
|
+
var ws = null;
|
|
7
|
+
var reconnectDelay = RECONNECT_MIN_MS;
|
|
8
|
+
var pingTimer = null;
|
|
9
|
+
async function loadSettings() {
|
|
10
|
+
const stored = await chrome.storage.local.get({ port: DEFAULT_PORT, token: "" });
|
|
11
|
+
const port = Number(stored.port);
|
|
12
|
+
return {
|
|
13
|
+
port: Number.isInteger(port) && port > 0 && port <= 65535 ? port : DEFAULT_PORT,
|
|
14
|
+
token: typeof stored.token === "string" ? stored.token : ""
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function snapshot(tab) {
|
|
18
|
+
if (tab.id === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
tabId: tab.id,
|
|
22
|
+
url: tab.url ?? tab.pendingUrl ?? "",
|
|
23
|
+
title: tab.title ?? "",
|
|
24
|
+
active: tab.active,
|
|
25
|
+
windowId: tab.windowId,
|
|
26
|
+
pinned: tab.pinned,
|
|
27
|
+
groupId: tab.groupId
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
var ompGroupTitle = null;
|
|
31
|
+
var groupOps = Promise.resolve();
|
|
32
|
+
function enqueueGroupOp(fn) {
|
|
33
|
+
const result = groupOps.then(fn, fn);
|
|
34
|
+
groupOps = result.catch(() => {});
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
async function groupTabs(tabIds, title, color) {
|
|
38
|
+
ompGroupTitle = title;
|
|
39
|
+
chrome.storage.session.set({ ompGroupTitle: title });
|
|
40
|
+
const byWindow = new Map;
|
|
41
|
+
for (const tabId of tabIds) {
|
|
42
|
+
try {
|
|
43
|
+
const tab = await chrome.tabs.get(tabId);
|
|
44
|
+
if (tab.pinned || tab.id === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
const bucket = byWindow.get(tab.windowId) ?? [];
|
|
47
|
+
bucket.push(tab.id);
|
|
48
|
+
byWindow.set(tab.windowId, bucket);
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
const grouped = {};
|
|
52
|
+
for (const [windowId, ids] of byWindow) {
|
|
53
|
+
const existing = await chrome.tabGroups.query({ title, windowId });
|
|
54
|
+
let groupId;
|
|
55
|
+
if (existing[0]) {
|
|
56
|
+
groupId = existing[0].id;
|
|
57
|
+
for (const dupe of existing.slice(1)) {
|
|
58
|
+
const dupeTabs = await chrome.tabs.query({ groupId: dupe.id });
|
|
59
|
+
const dupeIds = dupeTabs.map((tab) => tab.id).filter((id) => id !== undefined);
|
|
60
|
+
if (dupeIds.length > 0)
|
|
61
|
+
await chrome.tabs.group({ tabIds: dupeIds, groupId });
|
|
62
|
+
}
|
|
63
|
+
await chrome.tabs.group({ tabIds: ids, groupId });
|
|
64
|
+
} else {
|
|
65
|
+
groupId = await chrome.tabs.group({ tabIds: ids });
|
|
66
|
+
}
|
|
67
|
+
await chrome.tabGroups.update(groupId, { title, color });
|
|
68
|
+
for (const id of ids)
|
|
69
|
+
grouped[String(id)] = groupId;
|
|
70
|
+
}
|
|
71
|
+
return { grouped };
|
|
72
|
+
}
|
|
73
|
+
async function restoreGroups() {
|
|
74
|
+
if (!ompGroupTitle) {
|
|
75
|
+
const stored = await chrome.storage.session.get({ ompGroupTitle: "" }).catch(() => ({ ompGroupTitle: "" }));
|
|
76
|
+
ompGroupTitle = typeof stored.ompGroupTitle === "string" && stored.ompGroupTitle ? stored.ompGroupTitle : null;
|
|
77
|
+
}
|
|
78
|
+
if (!ompGroupTitle)
|
|
79
|
+
return;
|
|
80
|
+
const groups = await chrome.tabGroups.query({ title: ompGroupTitle }).catch(() => []);
|
|
81
|
+
for (const group of groups) {
|
|
82
|
+
const tabs = await chrome.tabs.query({ groupId: group.id }).catch(() => []);
|
|
83
|
+
const ids = tabs.map((tab) => tab.id).filter((id) => id !== undefined);
|
|
84
|
+
if (ids.length > 0)
|
|
85
|
+
await chrome.tabs.ungroup(ids).catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function post(msg) {
|
|
89
|
+
if (ws?.readyState === WebSocket.OPEN)
|
|
90
|
+
ws.send(JSON.stringify(msg));
|
|
91
|
+
}
|
|
92
|
+
async function setBadge(connected) {
|
|
93
|
+
try {
|
|
94
|
+
await chrome.action.setBadgeText({ text: connected ? "on" : "off" });
|
|
95
|
+
await chrome.action.setBadgeBackgroundColor({ color: connected ? "#1a7f37" : "#8b8b8b" });
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
async function buildHello() {
|
|
99
|
+
const [tabs, targets] = await Promise.all([chrome.tabs.query({}), chrome.debugger.getTargets()]);
|
|
100
|
+
const snapshots = [];
|
|
101
|
+
for (const tab of tabs) {
|
|
102
|
+
const snap = snapshot(tab);
|
|
103
|
+
if (snap)
|
|
104
|
+
snapshots.push(snap);
|
|
105
|
+
}
|
|
106
|
+
const attachedTabIds = [];
|
|
107
|
+
for (const target of targets) {
|
|
108
|
+
if (target.attached && target.tabId !== undefined)
|
|
109
|
+
attachedTabIds.push(target.tabId);
|
|
110
|
+
}
|
|
111
|
+
const versionMatch = /Chrome\/[\d.]+/.exec(navigator.userAgent);
|
|
112
|
+
return {
|
|
113
|
+
t: "hello",
|
|
114
|
+
userAgent: navigator.userAgent,
|
|
115
|
+
browserVersion: versionMatch?.[0] ?? "Chrome/unknown",
|
|
116
|
+
tabs: snapshots,
|
|
117
|
+
attachedTabIds
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function runRpc(msg) {
|
|
121
|
+
switch (msg.op) {
|
|
122
|
+
case "attach":
|
|
123
|
+
await chrome.debugger.attach({ tabId: msg.tabId }, "1.3");
|
|
124
|
+
return {};
|
|
125
|
+
case "detach":
|
|
126
|
+
await chrome.debugger.detach({ tabId: msg.tabId });
|
|
127
|
+
return {};
|
|
128
|
+
case "send":
|
|
129
|
+
return await chrome.debugger.sendCommand(msg.sessionId ? { tabId: msg.tabId, sessionId: msg.sessionId } : { tabId: msg.tabId }, msg.method, msg.params);
|
|
130
|
+
case "createTab": {
|
|
131
|
+
const tab = await chrome.tabs.create({ url: msg.url });
|
|
132
|
+
const snap = snapshot(tab);
|
|
133
|
+
if (!snap)
|
|
134
|
+
throw new Error("created tab has no id");
|
|
135
|
+
return { tab: snap };
|
|
136
|
+
}
|
|
137
|
+
case "removeTab":
|
|
138
|
+
await chrome.tabs.remove(msg.tabId);
|
|
139
|
+
return {};
|
|
140
|
+
case "activateTab": {
|
|
141
|
+
const tab = await chrome.tabs.get(msg.tabId);
|
|
142
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
143
|
+
await chrome.tabs.update(msg.tabId, { active: true });
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
case "group":
|
|
147
|
+
return await enqueueGroupOp(() => groupTabs(msg.tabIds, msg.title, msg.color));
|
|
148
|
+
case "ungroup":
|
|
149
|
+
await enqueueGroupOp(() => chrome.tabs.ungroup(msg.tabIds).catch(() => {}));
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function handleRelayMessage(raw) {
|
|
154
|
+
let msg;
|
|
155
|
+
try {
|
|
156
|
+
msg = JSON.parse(raw);
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (msg.t === "pong")
|
|
161
|
+
return;
|
|
162
|
+
runRpc(msg).then((result) => post({ t: "rpcResult", id: msg.id, ok: true, result })).catch((err) => {
|
|
163
|
+
post({ t: "rpcResult", id: msg.id, ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function scheduleReconnect() {
|
|
167
|
+
const delay = reconnectDelay;
|
|
168
|
+
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
169
|
+
setTimeout(() => void connect(), delay);
|
|
170
|
+
}
|
|
171
|
+
async function connect() {
|
|
172
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING))
|
|
173
|
+
return;
|
|
174
|
+
const settings = await loadSettings();
|
|
175
|
+
const url = `ws://127.0.0.1:${settings.port}/ext${settings.token ? `?token=${encodeURIComponent(settings.token)}` : ""}`;
|
|
176
|
+
const socket = new WebSocket(url);
|
|
177
|
+
ws = socket;
|
|
178
|
+
socket.onopen = () => {
|
|
179
|
+
reconnectDelay = RECONNECT_MIN_MS;
|
|
180
|
+
setBadge(true);
|
|
181
|
+
buildHello().then((hello) => post(hello));
|
|
182
|
+
clearInterval(pingTimer ?? undefined);
|
|
183
|
+
pingTimer = setInterval(() => post({ t: "ping" }), PING_INTERVAL_MS);
|
|
184
|
+
};
|
|
185
|
+
socket.onmessage = (event) => {
|
|
186
|
+
if (typeof event.data === "string")
|
|
187
|
+
handleRelayMessage(event.data);
|
|
188
|
+
};
|
|
189
|
+
socket.onclose = () => {
|
|
190
|
+
if (ws !== socket)
|
|
191
|
+
return;
|
|
192
|
+
ws = null;
|
|
193
|
+
if (pingTimer !== null) {
|
|
194
|
+
clearInterval(pingTimer);
|
|
195
|
+
pingTimer = null;
|
|
196
|
+
}
|
|
197
|
+
setBadge(false);
|
|
198
|
+
restoreGroups();
|
|
199
|
+
scheduleReconnect();
|
|
200
|
+
};
|
|
201
|
+
socket.onerror = () => {
|
|
202
|
+
socket.close();
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
206
|
+
if (source.tabId === undefined)
|
|
207
|
+
return;
|
|
208
|
+
post({ t: "cdpEvent", tabId: source.tabId, sessionId: source.sessionId, method, params });
|
|
209
|
+
});
|
|
210
|
+
chrome.debugger.onDetach.addListener((source, reason) => {
|
|
211
|
+
if (source.tabId === undefined)
|
|
212
|
+
return;
|
|
213
|
+
post({ t: "detached", tabId: source.tabId, reason });
|
|
214
|
+
});
|
|
215
|
+
chrome.tabs.onCreated.addListener((tab) => {
|
|
216
|
+
const snap = snapshot(tab);
|
|
217
|
+
if (snap)
|
|
218
|
+
post({ t: "tabCreated", tab: snap });
|
|
219
|
+
});
|
|
220
|
+
chrome.tabs.onUpdated.addListener((_tabId, _changeInfo, tab) => {
|
|
221
|
+
const snap = snapshot(tab);
|
|
222
|
+
if (snap)
|
|
223
|
+
post({ t: "tabUpdated", tab: snap });
|
|
224
|
+
});
|
|
225
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
226
|
+
post({ t: "tabRemoved", tabId });
|
|
227
|
+
});
|
|
228
|
+
chrome.alarms.create("omp-relay-keepalive", { periodInMinutes: 0.5 });
|
|
229
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
230
|
+
if (alarm.name === "omp-relay-keepalive")
|
|
231
|
+
connect();
|
|
232
|
+
});
|
|
233
|
+
chrome.storage.onChanged.addListener((_changes, areaName) => {
|
|
234
|
+
if (areaName !== "local")
|
|
235
|
+
return;
|
|
236
|
+
ws?.close();
|
|
237
|
+
connect();
|
|
238
|
+
});
|
|
239
|
+
chrome.action.onClicked.addListener(() => void chrome.runtime.openOptionsPage());
|
|
240
|
+
chrome.runtime.onInstalled.addListener(() => void connect());
|
|
241
|
+
chrome.runtime.onStartup.addListener(() => void connect());
|
|
242
|
+
connect();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "OMP Browser Relay",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Lets the omp coding agent drive your existing tabs through a local CDP relay.",
|
|
6
|
+
"permissions": ["debugger", "tabs", "tabGroups", "storage", "alarms"],
|
|
7
|
+
"background": {
|
|
8
|
+
"service_worker": "background.js"
|
|
9
|
+
},
|
|
10
|
+
"options_page": "options.html",
|
|
11
|
+
"action": {
|
|
12
|
+
"default_title": "OMP Browser Relay (click for settings)"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>OMP Browser Relay settings</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: system-ui, sans-serif;
|
|
9
|
+
max-width: 28rem;
|
|
10
|
+
margin: 2rem auto;
|
|
11
|
+
padding: 0 1rem;
|
|
12
|
+
color: #1f2328;
|
|
13
|
+
}
|
|
14
|
+
label {
|
|
15
|
+
display: block;
|
|
16
|
+
margin: 1rem 0 0.25rem;
|
|
17
|
+
font-weight: 600;
|
|
18
|
+
}
|
|
19
|
+
input {
|
|
20
|
+
width: 100%;
|
|
21
|
+
padding: 0.4rem 0.5rem;
|
|
22
|
+
font: inherit;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
button {
|
|
26
|
+
margin-top: 1.25rem;
|
|
27
|
+
padding: 0.45rem 1.1rem;
|
|
28
|
+
font: inherit;
|
|
29
|
+
}
|
|
30
|
+
#status {
|
|
31
|
+
margin-left: 0.75rem;
|
|
32
|
+
color: #1a7f37;
|
|
33
|
+
}
|
|
34
|
+
p.hint {
|
|
35
|
+
color: #57606a;
|
|
36
|
+
font-size: 0.85rem;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<h1>OMP Browser Relay</h1>
|
|
42
|
+
<p class="hint">
|
|
43
|
+
The extension connects to a local <code>omp-browser-relay</code> process. Values here must match the flags the
|
|
44
|
+
relay was started with.
|
|
45
|
+
</p>
|
|
46
|
+
<label for="port">Relay port</label>
|
|
47
|
+
<input id="port" type="number" min="1" max="65535" />
|
|
48
|
+
<label for="token">Token (optional)</label>
|
|
49
|
+
<input id="token" type="text" autocomplete="off" />
|
|
50
|
+
<button id="save">Save</button><span id="status"></span>
|
|
51
|
+
<script src="options.js"></script>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Options page for the OMP Browser Relay extension (plain JS: shipped as-is).
|
|
2
|
+
const DEFAULT_PORT = 9224;
|
|
3
|
+
const portInput = document.getElementById("port");
|
|
4
|
+
const tokenInput = document.getElementById("token");
|
|
5
|
+
const status = document.getElementById("status");
|
|
6
|
+
|
|
7
|
+
chrome.storage.local.get({ port: DEFAULT_PORT, token: "" }).then(stored => {
|
|
8
|
+
portInput.value = String(stored.port);
|
|
9
|
+
tokenInput.value = String(stored.token);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
document.getElementById("save").addEventListener("click", async () => {
|
|
13
|
+
const port = Number(portInput.value);
|
|
14
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
15
|
+
status.textContent = "invalid port";
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await chrome.storage.local.set({ port, token: tokenInput.value });
|
|
19
|
+
status.textContent = "saved";
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
status.textContent = "";
|
|
22
|
+
}, 1500);
|
|
23
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser relay mode: drive the user's own Chrome tabs through the local CDP
|
|
3
|
+
* relay served by `omp browser-relay` (sibling `server.ts`/`bridge.ts`) plus
|
|
4
|
+
* its companion extension (`packages/browser-relay`, installed via
|
|
5
|
+
* `omp browser-relay install`). The relay impersonates Chrome's CDP discovery
|
|
6
|
+
* endpoint, so beyond kind resolution the entire connected-browser machinery
|
|
7
|
+
* (registry, tab supervisor, tab workers) applies unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import { parseFlag } from "@oh-my-pi/pi-utils";
|
|
10
|
+
|
|
11
|
+
/** Browser kind selecting the omp browser relay. */
|
|
12
|
+
export interface RelayKind {
|
|
13
|
+
kind: "relay";
|
|
14
|
+
cdpUrl: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Default endpoint of the `omp-browser-relay` CLI. */
|
|
18
|
+
export const DEFAULT_RELAY_URL = "http://127.0.0.1:9224";
|
|
19
|
+
|
|
20
|
+
export interface ResolveRelayKindOptions {
|
|
21
|
+
/** `browser.relay` setting; `PI_BROWSER_RELAY=0|1` overrides it. */
|
|
22
|
+
settingEnabled?: boolean;
|
|
23
|
+
/** `browser.relayUrl` setting; falls back to {@link DEFAULT_RELAY_URL}. */
|
|
24
|
+
url?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the relay browser kind, or null when relay mode is disabled.
|
|
29
|
+
* Mirrors `resolveCmuxKind`: the setting opts in, the env var is the final
|
|
30
|
+
* override in both directions.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveRelayKind(
|
|
33
|
+
options?: ResolveRelayKindOptions | null,
|
|
34
|
+
env: Record<string, string | undefined> = process.env,
|
|
35
|
+
): RelayKind | null {
|
|
36
|
+
if (!parseFlag(env.PI_BROWSER_RELAY, options?.settingEnabled ?? false)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const url = options?.url?.trim() || DEFAULT_RELAY_URL;
|
|
40
|
+
return { kind: "relay", cdpUrl: url.replace(/\/+$/, "") };
|
|
41
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol between the relay server and the Chrome extension.
|
|
3
|
+
*
|
|
4
|
+
* The extension dials out to `ws://127.0.0.1:<port>/ext` and exchanges JSON
|
|
5
|
+
* messages. The relay drives the extension with numbered RPCs; the extension
|
|
6
|
+
* pushes tab lifecycle and `chrome.debugger` events as they happen.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Minimal view of a Chrome tab shared between extension and relay. */
|
|
10
|
+
export interface TabSnapshot {
|
|
11
|
+
tabId: number;
|
|
12
|
+
url: string;
|
|
13
|
+
title: string;
|
|
14
|
+
active: boolean;
|
|
15
|
+
windowId: number;
|
|
16
|
+
/** Pinned tabs are never grouped (Chrome would silently unpin them). */
|
|
17
|
+
pinned: boolean;
|
|
18
|
+
/** Chrome tab group id; -1 when ungrouped. */
|
|
19
|
+
groupId: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** RPCs the relay may ask the extension to perform. */
|
|
23
|
+
export type RelayRpcRequest =
|
|
24
|
+
| { op: "attach"; tabId: number }
|
|
25
|
+
| { op: "detach"; tabId: number }
|
|
26
|
+
| { op: "send"; tabId: number; sessionId?: string; method: string; params?: Record<string, unknown> }
|
|
27
|
+
| { op: "createTab"; url: string }
|
|
28
|
+
| { op: "removeTab"; tabId: number }
|
|
29
|
+
| { op: "activateTab"; tabId: number }
|
|
30
|
+
/** Add tabs to the per-window omp group (created/reused by title), remembering prior membership. */
|
|
31
|
+
| { op: "group"; tabIds: number[]; title: string; color: string }
|
|
32
|
+
/** Return tabs to their pre-omp group (or ungroup); no-op for tabs the relay never grouped. */
|
|
33
|
+
| { op: "ungroup"; tabIds: number[] };
|
|
34
|
+
|
|
35
|
+
/** Messages sent relay → extension. */
|
|
36
|
+
export type RelayToExtMessage = ({ t: "rpc"; id: number } & RelayRpcRequest) | { t: "pong" };
|
|
37
|
+
|
|
38
|
+
/** Messages sent extension → relay. */
|
|
39
|
+
export type ExtToRelayMessage =
|
|
40
|
+
| {
|
|
41
|
+
t: "hello";
|
|
42
|
+
userAgent: string;
|
|
43
|
+
browserVersion: string;
|
|
44
|
+
tabs: TabSnapshot[];
|
|
45
|
+
/** Tabs that already have a `chrome.debugger` attachment (relay reconciles after a service-worker restart). */
|
|
46
|
+
attachedTabIds: number[];
|
|
47
|
+
}
|
|
48
|
+
| { t: "cdpEvent"; tabId: number; sessionId?: string; method: string; params?: Record<string, unknown> }
|
|
49
|
+
| { t: "detached"; tabId: number; reason: string }
|
|
50
|
+
| { t: "tabCreated"; tab: TabSnapshot }
|
|
51
|
+
| { t: "tabUpdated"; tab: TabSnapshot }
|
|
52
|
+
| { t: "tabRemoved"; tabId: number }
|
|
53
|
+
| { t: "rpcResult"; id: number; ok: boolean; result?: unknown; error?: string }
|
|
54
|
+
| { t: "ping" };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP + WebSocket server for the browser relay.
|
|
3
|
+
*
|
|
4
|
+
* Impersonates Chrome's CDP discovery endpoint so the omp browser tool (and
|
|
5
|
+
* any puppeteer client) can connect with a plain `browserURL`:
|
|
6
|
+
* - `GET /json/version` → 200 with `webSocketDebuggerUrl` once the extension
|
|
7
|
+
* is connected, 503 before that (clients like `waitForCdp` keep polling).
|
|
8
|
+
* - `GET /json` / `/json/list` → attachable page targets (debugging aid).
|
|
9
|
+
* - `WS /cdp` → downstream CDP clients (puppeteer).
|
|
10
|
+
* - `WS /ext` → the Chrome extension (token-gated when configured).
|
|
11
|
+
*
|
|
12
|
+
* Binds loopback only: anything that can reach this port can drive the
|
|
13
|
+
* user's logged-in browser.
|
|
14
|
+
*/
|
|
15
|
+
import { RelayBridge } from "./bridge";
|
|
16
|
+
|
|
17
|
+
/** Options for {@link startRelayServer}. */
|
|
18
|
+
export interface RelayServerOptions {
|
|
19
|
+
port: number;
|
|
20
|
+
/** Shared secret the extension must present as `?token=`; unset disables the check. */
|
|
21
|
+
token?: string;
|
|
22
|
+
/** Group tabs the agent actively drives under one per-window Chrome tab group (default on); `false` disables. */
|
|
23
|
+
group?: boolean | { title: string; color: string };
|
|
24
|
+
log?: (message: string, data?: Record<string, unknown>) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A running relay server. */
|
|
28
|
+
export interface RelayServer {
|
|
29
|
+
bridge: RelayBridge;
|
|
30
|
+
port: number;
|
|
31
|
+
stop(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SocketData {
|
|
35
|
+
role: "cdp" | "ext";
|
|
36
|
+
connId?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type RelayWebSocket = Bun.ServerWebSocket<SocketData>;
|
|
40
|
+
|
|
41
|
+
const WS_KEEPALIVE_MS = 30_000;
|
|
42
|
+
/** Screenshots travel base64-encoded through both websocket legs. */
|
|
43
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024 * 1024;
|
|
44
|
+
/** Default appearance of the omp tab group. */
|
|
45
|
+
const DEFAULT_GROUP = { title: "omp", color: "cyan" } as const;
|
|
46
|
+
|
|
47
|
+
/** Start the relay server on 127.0.0.1. Throws if the port is taken. */
|
|
48
|
+
export function startRelayServer(opts: RelayServerOptions): RelayServer {
|
|
49
|
+
const log = opts.log ?? (() => {});
|
|
50
|
+
const group =
|
|
51
|
+
opts.group === false ? null : opts.group === true || opts.group === undefined ? DEFAULT_GROUP : opts.group;
|
|
52
|
+
const bridge = new RelayBridge({ log, group });
|
|
53
|
+
const sockets = new Set<RelayWebSocket>();
|
|
54
|
+
|
|
55
|
+
const server = Bun.serve({
|
|
56
|
+
hostname: "127.0.0.1",
|
|
57
|
+
port: opts.port,
|
|
58
|
+
fetch(req, srv): Response | undefined {
|
|
59
|
+
const url = new URL(req.url);
|
|
60
|
+
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
61
|
+
if (path === "/cdp") {
|
|
62
|
+
// Browsers set Origin on websocket upgrades; native CDP clients
|
|
63
|
+
// don't. Reject any Origin so a web page can't drive the relay.
|
|
64
|
+
if (req.headers.get("origin")) return new Response("Forbidden", { status: 403 });
|
|
65
|
+
const data: SocketData = { role: "cdp" };
|
|
66
|
+
if (srv.upgrade(req, { data })) return undefined;
|
|
67
|
+
return new Response("websocket upgrade required", { status: 426 });
|
|
68
|
+
}
|
|
69
|
+
if (path === "/ext") {
|
|
70
|
+
const origin = req.headers.get("origin");
|
|
71
|
+
if (origin && !origin.startsWith("chrome-extension://")) {
|
|
72
|
+
return new Response("Forbidden", { status: 403 });
|
|
73
|
+
}
|
|
74
|
+
if (opts.token && url.searchParams.get("token") !== opts.token) {
|
|
75
|
+
return new Response("Unauthorized", { status: 401 });
|
|
76
|
+
}
|
|
77
|
+
const data: SocketData = { role: "ext" };
|
|
78
|
+
if (srv.upgrade(req, { data })) return undefined;
|
|
79
|
+
return new Response("websocket upgrade required", { status: 426 });
|
|
80
|
+
}
|
|
81
|
+
if (req.method !== "GET") return new Response("Method not allowed", { status: 405 });
|
|
82
|
+
if (path === "/json/version") {
|
|
83
|
+
if (!bridge.ready) {
|
|
84
|
+
return Response.json({ error: "relay extension is not connected" }, { status: 503 });
|
|
85
|
+
}
|
|
86
|
+
return Response.json(bridge.versionInfo(`ws://127.0.0.1:${opts.port}/cdp`));
|
|
87
|
+
}
|
|
88
|
+
if (path === "/json" || path === "/json/list") {
|
|
89
|
+
return Response.json(bridge.listTargets());
|
|
90
|
+
}
|
|
91
|
+
return new Response("Not found", { status: 404 });
|
|
92
|
+
},
|
|
93
|
+
websocket: {
|
|
94
|
+
maxPayloadLength: MAX_PAYLOAD_BYTES,
|
|
95
|
+
// Disabled: Bun caps idleTimeout at 255s, and the keepalive pings
|
|
96
|
+
// below already detect dead peers via the websocket close path.
|
|
97
|
+
idleTimeout: 0,
|
|
98
|
+
open(ws: RelayWebSocket): void {
|
|
99
|
+
sockets.add(ws);
|
|
100
|
+
if (ws.data.role === "ext") {
|
|
101
|
+
bridge.extConnected(ws);
|
|
102
|
+
} else {
|
|
103
|
+
ws.data.connId = bridge.cdpConnected(ws);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
message(ws: RelayWebSocket, message: string | Buffer): void {
|
|
107
|
+
const text = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
108
|
+
if (ws.data.role === "ext") {
|
|
109
|
+
bridge.extMessage(ws, text);
|
|
110
|
+
} else if (ws.data.connId !== undefined) {
|
|
111
|
+
bridge.cdpMessage(ws.data.connId, text);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
close(ws: RelayWebSocket): void {
|
|
115
|
+
sockets.delete(ws);
|
|
116
|
+
if (ws.data.role === "ext") {
|
|
117
|
+
bridge.extClosed(ws);
|
|
118
|
+
} else if (ws.data.connId !== undefined) {
|
|
119
|
+
bridge.cdpClosed(ws.data.connId);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Puppeteer connections go silent while the agent is idle; protocol-level
|
|
126
|
+
// pings count as activity and keep them under the idle timeout.
|
|
127
|
+
const keepalive = setInterval(() => {
|
|
128
|
+
for (const ws of sockets) ws.ping();
|
|
129
|
+
}, WS_KEEPALIVE_MS);
|
|
130
|
+
keepalive.unref();
|
|
131
|
+
|
|
132
|
+
log("relay listening", { port: opts.port });
|
|
133
|
+
return {
|
|
134
|
+
bridge,
|
|
135
|
+
port: opts.port,
|
|
136
|
+
stop() {
|
|
137
|
+
clearInterval(keepalive);
|
|
138
|
+
server.stop(true);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|