@jobshimo/browser-link 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/auth/process-identity.d.ts +19 -3
  2. package/dist/auth/process-identity.js +81 -20
  3. package/dist/auth/process-identity.js.map +1 -1
  4. package/dist/bridge/dispatch.d.ts +6 -0
  5. package/dist/bridge/dispatch.js +2 -2
  6. package/dist/bridge/dispatch.js.map +1 -1
  7. package/dist/bridge/events.d.ts +1 -1
  8. package/dist/bridge/events.js.map +1 -1
  9. package/dist/bridge/server.d.ts +7 -1
  10. package/dist/bridge/server.js +42 -5
  11. package/dist/bridge/server.js.map +1 -1
  12. package/dist/commands/self-update.d.ts +59 -0
  13. package/dist/commands/self-update.js +110 -0
  14. package/dist/commands/self-update.js.map +1 -0
  15. package/dist/commands/updates.js +27 -1
  16. package/dist/commands/updates.js.map +1 -1
  17. package/dist/config.d.ts +12 -10
  18. package/dist/config.js +55 -20
  19. package/dist/config.js.map +1 -1
  20. package/dist/extension/background.js +56 -6
  21. package/dist/extension/background.js.map +1 -1
  22. package/dist/extension/manifest.json +2 -2
  23. package/dist/extension/popup.d.ts +4 -0
  24. package/dist/extension/popup.html +238 -48
  25. package/dist/extension/popup.js +37 -24
  26. package/dist/extension/popup.js.map +1 -1
  27. package/dist/server.js +50 -1
  28. package/dist/server.js.map +1 -1
  29. package/dist/tools/browser-definitions.js +36 -1
  30. package/dist/tools/browser-definitions.js.map +1 -1
  31. package/dist/tools/browser-dispatch.d.ts +39 -3
  32. package/dist/tools/browser-dispatch.js +170 -63
  33. package/dist/tools/browser-dispatch.js.map +1 -1
  34. package/dist/tools/server-instructions.d.ts +1 -1
  35. package/dist/tools/server-instructions.js +32 -0
  36. package/dist/tools/server-instructions.js.map +1 -1
  37. package/dist/tools/tab-claims.d.ts +117 -0
  38. package/dist/tools/tab-claims.js +186 -0
  39. package/dist/tools/tab-claims.js.map +1 -0
  40. package/dist/ui/screens.js +57 -6
  41. package/dist/ui/screens.js.map +1 -1
  42. package/package.json +1 -1
@@ -4,15 +4,37 @@
4
4
  * runServer().
5
5
  *
6
6
  * The handlers do not own state: they receive a `BrowserToolDeps` object
7
- * with the live tab map and the call function. This makes the dispatcher
8
- * unit-testable with a fake `callBrowserTool`.
7
+ * with the live tab map, the call function, and the claim registry. This
8
+ * makes the dispatcher unit-testable with a fake `callBrowserTool` and a
9
+ * fresh `TabClaimRegistry`.
10
+ *
11
+ * Each invocation also receives an `AgentCaller` so claim/ownership
12
+ * decisions can be made per request. The bridge layers (primary's stdio
13
+ * handler + IPC server) are responsible for supplying it.
9
14
  */
10
15
  import type { BridgeEvent } from '../bridge/events.js';
16
+ import { type AgentCaller, type TabClaimRegistry } from './tab-claims.js';
11
17
  export interface TabSnapshot {
12
18
  tab_id: string;
13
19
  url: string;
14
20
  title: string;
15
21
  }
22
+ /** Public view of a TabClaim — what other agents and the user are allowed to see.
23
+ * Keeps the wire payload stable even if the internal `TabClaim` grows fields. */
24
+ export interface PublicClaim {
25
+ tab_id: string;
26
+ agent_id: string;
27
+ pid: number;
28
+ binary: string;
29
+ label?: string;
30
+ claimed_at: number;
31
+ last_activity_at: number;
32
+ ttl_ms: number;
33
+ }
34
+ export interface EnrichedTabSnapshot extends TabSnapshot {
35
+ claimed_by: PublicClaim | null;
36
+ claimed_by_me: boolean;
37
+ }
16
38
  export interface BrowserToolDeps {
17
39
  listTabs(): TabSnapshot[];
18
40
  callBrowserTool(tabId: string, tool: string, params: unknown, timeoutMs?: number): Promise<unknown>;
@@ -22,6 +44,20 @@ export interface BrowserToolDeps {
22
44
  sinceId?: number;
23
45
  limit?: number;
24
46
  }): BridgeEvent[];
47
+ /** Claim registry. Optional so existing test fixtures keep compiling — when
48
+ * absent the dispatcher behaves as before (no enforcement, list_tabs
49
+ * returns claimed_by:null for every tab). */
50
+ tabClaims?: TabClaimRegistry;
25
51
  }
26
52
  export declare function isBrowserTool(name: string): boolean;
27
- export declare function handleBrowserTool(name: string, args: unknown, deps: BrowserToolDeps): Promise<unknown>;
53
+ /**
54
+ * Dispatch a browser tool call.
55
+ *
56
+ * The switch is intentional: every reachable arm is matched against the
57
+ * closed `BrowserToolName` literal union, so the `name` string can only
58
+ * reach a known handler (or fall through to the `default` throw). This
59
+ * pattern is what CodeQL's `js/unvalidated-dynamic-method-call` accepts as
60
+ * a fixed allowlist — a Map-of-handlers lookup looks identical at runtime
61
+ * but is flagged because the static analyser can't prove the bound.
62
+ */
63
+ export declare function handleBrowserTool(name: string, args: unknown, deps: BrowserToolDeps, caller: AgentCaller): Promise<unknown>;
@@ -4,83 +4,190 @@
4
4
  * runServer().
5
5
  *
6
6
  * The handlers do not own state: they receive a `BrowserToolDeps` object
7
- * with the live tab map and the call function. This makes the dispatcher
8
- * unit-testable with a fake `callBrowserTool`.
7
+ * with the live tab map, the call function, and the claim registry. This
8
+ * makes the dispatcher unit-testable with a fake `callBrowserTool` and a
9
+ * fresh `TabClaimRegistry`.
10
+ *
11
+ * Each invocation also receives an `AgentCaller` so claim/ownership
12
+ * decisions can be made per request. The bridge layers (primary's stdio
13
+ * handler + IPC server) are responsible for supplying it.
9
14
  */
10
15
  import { requireTabId } from './responses.js';
16
+ import { formatClaimConflict, } from './tab-claims.js';
17
+ /** Closed set of browser tool names. Used both as the discriminant in
18
+ * `handleBrowserTool`'s switch and as the allowlist behind `isBrowserTool`,
19
+ * so adding a tool requires touching one place. The literal-union type lets
20
+ * TypeScript prove the switch is exhaustive at compile time. */
21
+ const BROWSER_TOOL_NAMES = [
22
+ 'browser.list_tabs',
23
+ 'browser.claim_tab',
24
+ 'browser.release_tab',
25
+ 'browser.my_tabs',
26
+ 'browser.ping',
27
+ 'browser.navigate',
28
+ 'browser.snapshot',
29
+ 'browser.console',
30
+ 'browser.network',
31
+ 'browser.network_body',
32
+ 'browser.click',
33
+ 'browser.type',
34
+ 'browser.evaluate',
35
+ 'browser.events',
36
+ ];
37
+ const BROWSER_TOOL_NAME_SET = new Set(BROWSER_TOOL_NAMES);
11
38
  export function isBrowserTool(name) {
12
- return name === 'browser.list_tabs' || BROWSER_TOOL_HANDLERS.has(name);
39
+ return BROWSER_TOOL_NAME_SET.has(name);
13
40
  }
14
41
  const NAVIGATE_TIMEOUT_MS = 30_000;
15
- const BROWSER_TOOL_HANDLERS = new Map([
16
- ['browser.list_tabs', (_args, deps) => deps.listTabs()],
17
- ['browser.ping', (args, deps) => deps.callBrowserTool(requireTabId(args), 'ping', {})],
18
- [
19
- 'browser.navigate',
20
- (args, deps) => {
42
+ /** Convert an internal TabClaim into the wire-safe `PublicClaim`. */
43
+ function toPublicClaim(claim) {
44
+ const publicClaim = {
45
+ tab_id: claim.tab_id,
46
+ agent_id: claim.agent_id,
47
+ pid: claim.pid,
48
+ binary: claim.binary,
49
+ claimed_at: claim.claimed_at,
50
+ last_activity_at: claim.last_activity_at,
51
+ ttl_ms: claim.ttl_ms,
52
+ };
53
+ if (claim.label !== undefined)
54
+ publicClaim.label = claim.label;
55
+ return publicClaim;
56
+ }
57
+ /** Run an action through the claim registry. Returns the response payload of
58
+ * the action when the agent is allowed, or throws a descriptive Error otherwise.
59
+ * When no registry is wired (test fixtures, or future configs that disable
60
+ * coordination), behaves like the pre-claim dispatcher. */
61
+ async function runAction(tool, tabId, params, deps, caller, timeoutMs) {
62
+ if (deps.tabClaims) {
63
+ const outcome = deps.tabClaims.ensureActionAllowed(tabId, caller);
64
+ if (!outcome.ok) {
65
+ throw new Error(formatClaimConflict(caller, outcome.existing));
66
+ }
67
+ }
68
+ // Preserve the pre-claim call shape — only forward timeoutMs when set so
69
+ // existing assertions and the bridge's default behaviour stay unchanged.
70
+ return timeoutMs !== undefined
71
+ ? deps.callBrowserTool(tabId, tool, params, timeoutMs)
72
+ : deps.callBrowserTool(tabId, tool, params);
73
+ }
74
+ function handleListTabs(deps, caller) {
75
+ return deps.listTabs().map((t) => {
76
+ const claim = deps.tabClaims?.getClaim(t.tab_id) ?? null;
77
+ return {
78
+ ...t,
79
+ claimed_by: claim ? toPublicClaim(claim) : null,
80
+ claimed_by_me: claim ? claim.agent_id === caller.agent_id : false,
81
+ };
82
+ });
83
+ }
84
+ function handleClaimTab(args, deps, caller) {
85
+ const { tab_id, ttl_minutes, label } = (args ?? {});
86
+ if (!tab_id)
87
+ throw new Error('tab_id required');
88
+ if (!deps.tabClaims) {
89
+ return {
90
+ ok: false,
91
+ reason: 'unsupported',
92
+ message: 'Tab coordination is disabled in this build of browser-link.',
93
+ };
94
+ }
95
+ const outcome = deps.tabClaims.claim(tab_id, caller, { ttlMinutes: ttl_minutes, label });
96
+ if (!outcome.ok) {
97
+ return { ok: false, reason: outcome.reason, existing: toPublicClaim(outcome.existing) };
98
+ }
99
+ return { ok: true, created: outcome.created, claim: toPublicClaim(outcome.claim) };
100
+ }
101
+ function handleReleaseTab(args, deps, caller) {
102
+ const tab_id = requireTabId(args);
103
+ if (!deps.tabClaims)
104
+ return { ok: true };
105
+ const result = deps.tabClaims.release(tab_id, caller);
106
+ if (result.ok)
107
+ return { ok: true };
108
+ const payload = { ok: false, reason: result.reason };
109
+ if (result.existing)
110
+ payload.existing = toPublicClaim(result.existing);
111
+ return payload;
112
+ }
113
+ function handleMyTabs(deps, caller) {
114
+ if (!deps.tabClaims)
115
+ return { claims: [] };
116
+ return { claims: deps.tabClaims.myTabs(caller).map(toPublicClaim) };
117
+ }
118
+ function handleEvents(args, deps) {
119
+ const { since_id, limit } = args ?? {};
120
+ if (!deps.recentEvents)
121
+ return { events: [], latest_id: 0 };
122
+ const events = deps.recentEvents({ sinceId: since_id, limit });
123
+ const latest_id = events.length > 0 ? events[events.length - 1].id : (since_id ?? 0);
124
+ return { events, latest_id };
125
+ }
126
+ /**
127
+ * Dispatch a browser tool call.
128
+ *
129
+ * The switch is intentional: every reachable arm is matched against the
130
+ * closed `BrowserToolName` literal union, so the `name` string can only
131
+ * reach a known handler (or fall through to the `default` throw). This
132
+ * pattern is what CodeQL's `js/unvalidated-dynamic-method-call` accepts as
133
+ * a fixed allowlist — a Map-of-handlers lookup looks identical at runtime
134
+ * but is flagged because the static analyser can't prove the bound.
135
+ */
136
+ export async function handleBrowserTool(name, args, deps, caller) {
137
+ if (!isBrowserTool(name))
138
+ throw new Error(`Unknown browser tool: ${name}`);
139
+ const toolName = name;
140
+ switch (toolName) {
141
+ case 'browser.list_tabs':
142
+ return handleListTabs(deps, caller);
143
+ case 'browser.claim_tab':
144
+ return handleClaimTab(args, deps, caller);
145
+ case 'browser.release_tab':
146
+ return handleReleaseTab(args, deps, caller);
147
+ case 'browser.my_tabs':
148
+ return handleMyTabs(deps, caller);
149
+ case 'browser.ping':
150
+ return deps.callBrowserTool(requireTabId(args), 'ping', {});
151
+ case 'browser.navigate': {
21
152
  const { url, wait_for_load = true } = args;
22
- return deps.callBrowserTool(requireTabId(args), 'navigate', { url, wait_for_load }, NAVIGATE_TIMEOUT_MS);
23
- },
24
- ],
25
- ['browser.snapshot', (args, deps) => deps.callBrowserTool(requireTabId(args), 'snapshot', {})],
26
- [
27
- 'browser.console',
28
- (args, deps) => {
153
+ return runAction('navigate', requireTabId(args), { url, wait_for_load }, deps, caller, NAVIGATE_TIMEOUT_MS);
154
+ }
155
+ case 'browser.snapshot':
156
+ return deps.callBrowserTool(requireTabId(args), 'snapshot', {});
157
+ case 'browser.console': {
29
158
  const { level } = args ?? {};
30
159
  return deps.callBrowserTool(requireTabId(args), 'console', { level });
31
- },
32
- ],
33
- [
34
- 'browser.network',
35
- (args, deps) => {
160
+ }
161
+ case 'browser.network': {
36
162
  const { url_filter } = args ?? {};
37
163
  return deps.callBrowserTool(requireTabId(args), 'network', { url_filter });
38
- },
39
- ],
40
- [
41
- 'browser.network_body',
42
- (args, deps) => {
164
+ }
165
+ case 'browser.network_body': {
43
166
  const { request_id } = args;
44
167
  return deps.callBrowserTool(requireTabId(args), 'network_body', { request_id });
45
- },
46
- ],
47
- [
48
- 'browser.click',
49
- (args, deps) => {
168
+ }
169
+ case 'browser.click': {
50
170
  const { selector } = args;
51
- return deps.callBrowserTool(requireTabId(args), 'click', { selector });
52
- },
53
- ],
54
- [
55
- 'browser.type',
56
- (args, deps) => {
171
+ return runAction('click', requireTabId(args), { selector }, deps, caller);
172
+ }
173
+ case 'browser.type': {
57
174
  const { selector, text, clear = false, } = args;
58
- return deps.callBrowserTool(requireTabId(args), 'type', { selector, text, clear });
59
- },
60
- ],
61
- [
62
- 'browser.evaluate',
63
- (args, deps) => {
175
+ return runAction('type', requireTabId(args), { selector, text, clear }, deps, caller);
176
+ }
177
+ case 'browser.evaluate': {
64
178
  const { expression } = args;
65
- return deps.callBrowserTool(requireTabId(args), 'evaluate', { expression });
66
- },
67
- ],
68
- [
69
- 'browser.events',
70
- (args, deps) => {
71
- const { since_id, limit } = args ?? {};
72
- if (!deps.recentEvents)
73
- return { events: [], latest_id: 0 };
74
- const events = deps.recentEvents({ sinceId: since_id, limit });
75
- const latest_id = events.length > 0 ? events[events.length - 1].id : (since_id ?? 0);
76
- return { events, latest_id };
77
- },
78
- ],
79
- ]);
80
- export async function handleBrowserTool(name, args, deps) {
81
- const handler = BROWSER_TOOL_HANDLERS.get(name);
82
- if (!handler)
83
- throw new Error(`Unknown browser tool: ${name}`);
84
- return handler(args, deps);
179
+ return runAction('evaluate', requireTabId(args), { expression }, deps, caller);
180
+ }
181
+ case 'browser.events':
182
+ return handleEvents(args, deps);
183
+ default: {
184
+ // The earlier `isBrowserTool(name)` check makes this branch unreachable
185
+ // for any value within `BrowserToolName`. The exhaustive cast surfaces a
186
+ // compile error if a future tool name is added to the union but missed
187
+ // here.
188
+ const _exhaustive = toolName;
189
+ throw new Error(`Unhandled browser tool: ${String(_exhaustive)}`);
190
+ }
191
+ }
85
192
  }
86
193
  //# sourceMappingURL=browser-dispatch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser-dispatch.js","sourceRoot":"","sources":["../../src/tools/browser-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAsB9C,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,KAAK,mBAAmB,IAAI,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACzE,CAAC;AAID,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,MAAM,qBAAqB,GAAiC,IAAI,GAAG,CAAkB;IACnF,CAAC,mBAAmB,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;IACvD,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;IACtF;QACE,kBAAkB;QAClB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI,EAAE,GAAG,IAAgD,CAAC;YACvF,OAAO,IAAI,CAAC,eAAe,CACzB,YAAY,CAAC,IAAI,CAAC,EAClB,UAAU,EACV,EAAE,GAAG,EAAE,aAAa,EAAE,EACtB,mBAAmB,CACpB,CAAC;QACJ,CAAC;KACF;IACD,CAAC,kBAAkB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;IAC9F;QACE,iBAAiB;QACjB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EAAE,KAAK,EAAE,GAAI,IAA2B,IAAI,EAAE,CAAC;YACrD,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACxE,CAAC;KACF;IACD;QACE,iBAAiB;QACjB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EAAE,UAAU,EAAE,GAAI,IAAgC,IAAI,EAAE,CAAC;YAC/D,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QAC7E,CAAC;KACF;IACD;QACE,sBAAsB;QACtB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EAAE,UAAU,EAAE,GAAG,IAA8B,CAAC;YACtD,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QAClF,CAAC;KACF;IACD;QACE,eAAe;QACf,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EAAE,QAAQ,EAAE,GAAG,IAA4B,CAAC;YAClD,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzE,CAAC;KACF;IACD;QACE,cAAc;QACd,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,GAAG,KAAK,GACd,GAAG,IAIH,CAAC;YACF,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACrF,CAAC;KACF;IACD;QACE,kBAAkB;QAClB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EAAE,UAAU,EAAE,GAAG,IAA8B,CAAC;YACtD,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QAC9E,CAAC;KACF;IACD;QACE,gBAAgB;QAChB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAI,IAA8C,IAAI,EAAE,CAAC;YAClF,IAAI,CAAC,IAAI,CAAC,YAAY;gBAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;YAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAC/D,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC;YACtF,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC/B,CAAC;KACF;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAY,EACZ,IAAa,EACb,IAAqB;IAErB,MAAM,OAAO,GAAG,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;IAC/D,OAAO,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC7B,CAAC"}
1
+ {"version":3,"file":"browser-dispatch.js","sourceRoot":"","sources":["../../src/tools/browser-dispatch.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  /** Usage protocol pushed to the MCP client on `initialize`. Plain string,
2
2
  * intentionally kept short. Edit here when the protocol changes. */
3
- export declare const SERVER_INSTRUCTIONS = "browser-link bridges Claude Code to the Chrome tabs the user has\nexplicitly connected through the companion extension, and ships a\npersistent UI map backed by a local SQLite DB. The data dir resolves\nper-OS via env-paths ($XDG_DATA_HOME/browser-link on Linux,\n~/Library/Application Support/browser-link on macOS, %APPDATA%/browser-link\non Windows). Override with $BROWSER_LINK_DATA_DIR. The map is private\nand per-machine; never persisted in any repo.\n\n## When operating on a tab\n\n1. Before doing anything on a tab whose URL you don't already know,\n call `browser.map.recall` with { origin } (and optionally url) to load\n selectors, flows and gotchas previously learned for that app.\n2. If recall returns entries with `failed_at` more recent than\n `verified_at`, treat them as suspect: re-verify (snapshot / evaluate)\n before reusing, or replace them.\n3. After every interaction that used a map entry, call\n `browser.map.record_use` with { entry_id, ok }. ok=true updates\n verified_at; ok=false updates failed_at. Keep the map honest.\n4. After a non-trivial flow that worked end-to-end, persist it with\n `browser.map.save`. Three `kind` values:\n - selector: { selector, evidence? } \u2014 a CSS selector tied to a purpose.\n - flow: { steps: [...] } \u2014 an ordered list of actions to reach an outcome.\n - gotcha: { body } \u2014 free-form note about something non-obvious.\n Use `url_pattern` = pathname (exact). Promote to glob only if you have\n evidence of a parametric route. Provide `purpose` as a stable, reusable\n label (\"open task detail dialog\", not \"open IB0311 detail\").\n5. Never save selectors or flows you have not just successfully executed.\n6. Never store domain data (IDs, user names, dates, etc.). The map captures\n UI structure only.\n\n## Identifying the app\n\n- `origin` = scheme://host:port of the tab.\n- `app_key` distinguishes apps that share an origin over time. On first\n save you may omit it; it will be derived from the page title (slugified).\n Use `browser.map.rename_app` if that initial guess is poor.\n\n## When something is wrong\n\n- A selector from recall fails \u2192 record_use({ok:false}), learn the new\n one, save it (upsert on purpose).\n- A whole app got refactored \u2192 `browser.map.forget` the app_id and let\n the map repopulate as you learn the new structure.\n- A tool call fails with \"Tab not connected: tab_X\" \u2192 call\n `browser.events` to see whether the bridge changed primary (the\n Chrome tab probably got a new tab_id after a reconnect). Look for a\n `tab-renamed` event with previous=tab_X and resume on the current id.\n\nThe map is a cache of navigation, not a substitute for `browser.snapshot`.\nThe live snapshot is always the source of truth.";
3
+ export declare const SERVER_INSTRUCTIONS = "browser-link bridges Claude Code to the Chrome tabs the user has\nexplicitly connected through the companion extension, and ships a\npersistent UI map backed by a local SQLite DB. The data dir resolves\nper-OS via env-paths ($XDG_DATA_HOME/browser-link on Linux,\n~/Library/Application Support/browser-link on macOS, %APPDATA%/browser-link\non Windows). Override with $BROWSER_LINK_DATA_DIR. The map is private\nand per-machine; never persisted in any repo.\n\n## When operating on a tab\n\n1. Before doing anything on a tab whose URL you don't already know,\n call `browser.map.recall` with { origin } (and optionally url) to load\n selectors, flows and gotchas previously learned for that app.\n2. If recall returns entries with `failed_at` more recent than\n `verified_at`, treat them as suspect: re-verify (snapshot / evaluate)\n before reusing, or replace them.\n3. After every interaction that used a map entry, call\n `browser.map.record_use` with { entry_id, ok }. ok=true updates\n verified_at; ok=false updates failed_at. Keep the map honest.\n4. After a non-trivial flow that worked end-to-end, persist it with\n `browser.map.save`. Three `kind` values:\n - selector: { selector, evidence? } \u2014 a CSS selector tied to a purpose.\n - flow: { steps: [...] } \u2014 an ordered list of actions to reach an outcome.\n - gotcha: { body } \u2014 free-form note about something non-obvious.\n Use `url_pattern` = pathname (exact). Promote to glob only if you have\n evidence of a parametric route. Provide `purpose` as a stable, reusable\n label (\"open task detail dialog\", not \"open IB0311 detail\").\n5. Never save selectors or flows you have not just successfully executed.\n6. Never store domain data (IDs, user names, dates, etc.). The map captures\n UI structure only.\n\n## Identifying the app\n\n- `origin` = scheme://host:port of the tab.\n- `app_key` distinguishes apps that share an origin over time. On first\n save you may omit it; it will be derived from the page title (slugified).\n Use `browser.map.rename_app` if that initial guess is poor.\n\n## Sharing tabs with other agents\n\nThis primary may be serving several MCP clients at once (multi-agent mode).\nTo stop two agents fighting over the same Chrome tab there is a cooperative\nclaim layer:\n\n- `browser.list_tabs` includes `claimed_by` (null if free, otherwise the\n agent that holds the claim) and `claimed_by_me` (boolean). Use it before\n starting work on a tab whose state you don't already own.\n- `browser.my_tabs` returns YOUR active claims with timestamps. If the\n user asks which tab you are using, this is the answer.\n- Action tools (`browser.click`, `browser.type`, `browser.navigate`,\n `browser.evaluate`) auto-claim a free tab on first use and refresh\n activity on subsequent calls. If another agent holds the tab, they\n return an error naming the owner \u2014 do NOT retry blindly; ask the user\n whose tab it should be, or use a different tab.\n- Read tools (`browser.snapshot`, `browser.console`, `browser.network`,\n `browser.network_body`, `browser.events`, `browser.ping`) ignore claims.\n- `browser.claim_tab({ tab_id, ttl_minutes?, label? })` reserves a tab\n explicitly. Provide a stable `label` (eg \"claude-code\", \"opencode\") so\n other agents and the user see WHO holds the tab. The label is display\n only \u2014 security relies on the IPC session id (kernel-vetted), not on\n what an agent calls itself.\n- `browser.release_tab({ tab_id })` hands a tab back. Claims also auto-\n release when an agent disconnects or after the inactivity TTL elapses\n (default 10 minutes), so explicit release is only needed for early\n hand-off.\n\nWhen you get a claim-conflict error: do NOT spin-retry. Either work on a\ndifferent tab from `list_tabs`, or surface the conflict to the user and\nlet them decide.\n\n## When something is wrong\n\n- A selector from recall fails \u2192 record_use({ok:false}), learn the new\n one, save it (upsert on purpose).\n- A whole app got refactored \u2192 `browser.map.forget` the app_id and let\n the map repopulate as you learn the new structure.\n- A tool call fails with \"Tab not connected: tab_X\" \u2192 call\n `browser.events` to see whether the bridge changed primary (the\n Chrome tab probably got a new tab_id after a reconnect). Look for a\n `tab-renamed` event with previous=tab_X and resume on the current id.\n\nThe map is a cache of navigation, not a substitute for `browser.snapshot`.\nThe live snapshot is always the source of truth.";
@@ -38,6 +38,38 @@ and per-machine; never persisted in any repo.
38
38
  save you may omit it; it will be derived from the page title (slugified).
39
39
  Use \`browser.map.rename_app\` if that initial guess is poor.
40
40
 
41
+ ## Sharing tabs with other agents
42
+
43
+ This primary may be serving several MCP clients at once (multi-agent mode).
44
+ To stop two agents fighting over the same Chrome tab there is a cooperative
45
+ claim layer:
46
+
47
+ - \`browser.list_tabs\` includes \`claimed_by\` (null if free, otherwise the
48
+ agent that holds the claim) and \`claimed_by_me\` (boolean). Use it before
49
+ starting work on a tab whose state you don't already own.
50
+ - \`browser.my_tabs\` returns YOUR active claims with timestamps. If the
51
+ user asks which tab you are using, this is the answer.
52
+ - Action tools (\`browser.click\`, \`browser.type\`, \`browser.navigate\`,
53
+ \`browser.evaluate\`) auto-claim a free tab on first use and refresh
54
+ activity on subsequent calls. If another agent holds the tab, they
55
+ return an error naming the owner — do NOT retry blindly; ask the user
56
+ whose tab it should be, or use a different tab.
57
+ - Read tools (\`browser.snapshot\`, \`browser.console\`, \`browser.network\`,
58
+ \`browser.network_body\`, \`browser.events\`, \`browser.ping\`) ignore claims.
59
+ - \`browser.claim_tab({ tab_id, ttl_minutes?, label? })\` reserves a tab
60
+ explicitly. Provide a stable \`label\` (eg "claude-code", "opencode") so
61
+ other agents and the user see WHO holds the tab. The label is display
62
+ only — security relies on the IPC session id (kernel-vetted), not on
63
+ what an agent calls itself.
64
+ - \`browser.release_tab({ tab_id })\` hands a tab back. Claims also auto-
65
+ release when an agent disconnects or after the inactivity TTL elapses
66
+ (default 10 minutes), so explicit release is only needed for early
67
+ hand-off.
68
+
69
+ When you get a claim-conflict error: do NOT spin-retry. Either work on a
70
+ different tab from \`list_tabs\`, or surface the conflict to the user and
71
+ let them decide.
72
+
41
73
  ## When something is wrong
42
74
 
43
75
  - A selector from recall fails → record_use({ok:false}), learn the new
@@ -1 +1 @@
1
- {"version":3,"file":"server-instructions.js","sourceRoot":"","sources":["../../src/tools/server-instructions.ts"],"names":[],"mappings":"AAAA;oEACoE;AACpE,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iDAkDc,CAAC"}
1
+ {"version":3,"file":"server-instructions.js","sourceRoot":"","sources":["../../src/tools/server-instructions.ts"],"names":[],"mappings":"AAAA;oEACoE;AACpE,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iDAkFc,CAAC"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Cooperative tab-ownership registry for multi-agent mode.
3
+ *
4
+ * Several MCP clients can share a single browser-link primary. Without
5
+ * coordination they end up racing on the same Chrome tab — clicking,
6
+ * navigating, evaluating in interleaved order. This module tracks who
7
+ * "owns" each tab and lets the dispatcher refuse cross-agent action.
8
+ *
9
+ * Identity model:
10
+ * - `agent_id` is the IPC session id for proxies (UUID minted after the
11
+ * hello/token handshake) and the literal string `"primary"` for the
12
+ * primary's own MCP client.
13
+ * - The auth that the agent passed (peerLookup + Node binary allowlist +
14
+ * rotated token) is the trust anchor. This registry never re-validates
15
+ * identity — it consumes the `AgentCaller` the bridge layer supplies.
16
+ * - Self-declared `label` (eg `"claude-code"`) is display-only and is
17
+ * never used for ownership comparisons.
18
+ *
19
+ * TTL model:
20
+ * - Claims expire after `ttl_minutes` of inactivity. Every action on the
21
+ * tab refreshes `last_activity_at`. Stale claims are dropped by
22
+ * `pruneStale()` (called periodically by the primary).
23
+ * - Sessions ending (proxy disconnect) drop the agent's claims
24
+ * immediately via `onAgentDisconnect()`.
25
+ */
26
+ export interface AgentCaller {
27
+ agent_id: string;
28
+ pid: number;
29
+ binary: string;
30
+ label?: string;
31
+ }
32
+ export interface TabClaim {
33
+ tab_id: string;
34
+ agent_id: string;
35
+ pid: number;
36
+ binary: string;
37
+ label?: string;
38
+ claimed_at: number;
39
+ last_activity_at: number;
40
+ ttl_ms: number;
41
+ }
42
+ export type ClaimEvent = {
43
+ kind: 'tab-claimed';
44
+ tab_id: string;
45
+ agent_id: string;
46
+ pid: number;
47
+ binary: string;
48
+ label?: string;
49
+ ttl_ms: number;
50
+ auto: boolean;
51
+ } | {
52
+ kind: 'tab-released';
53
+ tab_id: string;
54
+ agent_id: string;
55
+ reason: 'explicit' | 'agent-disconnect' | 'ttl';
56
+ } | {
57
+ kind: 'tab-claim-rejected';
58
+ tab_id: string;
59
+ requester_agent_id: string;
60
+ existing_agent_id: string;
61
+ };
62
+ export type ClaimOutcome = {
63
+ ok: true;
64
+ claim: TabClaim;
65
+ created: boolean;
66
+ } | {
67
+ ok: false;
68
+ reason: 'conflict';
69
+ existing: TabClaim;
70
+ };
71
+ export interface TabClaimRegistryOptions {
72
+ /** Default TTL when a claim does not specify one. Defaults to 10 minutes. */
73
+ defaultTtlMinutes?: number;
74
+ /** Upper bound on TTL a caller may request. Defaults to 60 minutes. */
75
+ maxTtlMinutes?: number;
76
+ /** Time source. Tests inject a fake clock to make TTL assertions deterministic. */
77
+ nowMs?: () => number;
78
+ /** Event callback. The primary wires this to its `BridgeEventLog`. */
79
+ onEvent?: (event: ClaimEvent) => void;
80
+ }
81
+ export declare class TabClaimRegistry {
82
+ private claims;
83
+ private readonly defaultTtlMs;
84
+ private readonly maxTtlMs;
85
+ private readonly now;
86
+ private readonly onEvent;
87
+ constructor(opts?: TabClaimRegistryOptions);
88
+ /** Snapshot of the current claim for a tab, or null when free or expired. */
89
+ getClaim(tab_id: string): TabClaim | null;
90
+ /** Explicit claim. Returns conflict if another agent owns the tab. Same-agent re-claims refresh activity and update the label/TTL. */
91
+ claim(tab_id: string, caller: AgentCaller, opts?: {
92
+ ttlMinutes?: number;
93
+ label?: string;
94
+ }): ClaimOutcome;
95
+ /** For action tools. Auto-claims a free tab for the caller, refreshes when the caller already owns it, conflicts otherwise. */
96
+ ensureActionAllowed(tab_id: string, caller: AgentCaller): ClaimOutcome;
97
+ /** Explicit release. Only the owner may release. */
98
+ release(tab_id: string, caller: AgentCaller): {
99
+ ok: true;
100
+ } | {
101
+ ok: false;
102
+ reason: 'not-claimed' | 'not-owner';
103
+ existing?: TabClaim;
104
+ };
105
+ /** Claims owned by this caller, sorted by `claimed_at`. */
106
+ myTabs(caller: AgentCaller): TabClaim[];
107
+ /** Drop every claim held by `agent_id` (called when a proxy disconnects). */
108
+ onAgentDisconnect(agent_id: string): TabClaim[];
109
+ /** Sweep claims past their TTL. Returns the dropped claims. */
110
+ pruneStale(): TabClaim[];
111
+ /** Test helper. Production callers should not need this. */
112
+ size(): number;
113
+ private isExpired;
114
+ private claimInternal;
115
+ }
116
+ /** User-facing error string for action tools when another agent holds the tab. Kept here so the wording is consistent across handlers. */
117
+ export declare function formatClaimConflict(caller: AgentCaller, existing: TabClaim, nowMs?: number): string;