mcp-browser-extension 0.9.35 → 0.9.38

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 (58) hide show
  1. package/README.md +33 -15
  2. package/dist/shared/client-auth.d.ts +2 -0
  3. package/dist/shared/phone-pairing.d.ts +35 -0
  4. package/dist/shared/phone-pairing.js +10 -0
  5. package/dist/shared/policy.js +7 -0
  6. package/dist/shared/protocol.d.ts +13 -2
  7. package/dist/shared/protocol.js +13 -1
  8. package/dist/shared/site-risk.d.ts +31 -0
  9. package/dist/shared/site-risk.js +190 -0
  10. package/dist/src/bridge/client-identity.d.ts +11 -5
  11. package/dist/src/bridge/client-identity.js +7 -4
  12. package/dist/src/bridge/connection.js +1 -0
  13. package/dist/src/bridge/server.d.ts +1 -0
  14. package/dist/src/bridge/server.js +38 -19
  15. package/dist/src/bridge/workspace.d.ts +1 -0
  16. package/dist/src/bridge/workspace.js +11 -3
  17. package/dist/src/cli.js +61 -18
  18. package/dist/src/config.d.ts +2 -0
  19. package/dist/src/config.js +26 -1
  20. package/dist/src/executor/extension-executor.d.ts +1 -0
  21. package/dist/src/executor/extension-executor.js +10 -1
  22. package/dist/src/executor/types.d.ts +3 -1
  23. package/dist/src/mcp/helpers.js +18 -6
  24. package/dist/src/mcp/person-wait.js +2 -1
  25. package/dist/src/mcp/server.d.ts +2 -1
  26. package/dist/src/mcp/server.js +4 -4
  27. package/dist/src/mcp/tools.d.ts +2 -2
  28. package/dist/src/mcp/tools.js +46 -9
  29. package/dist/src/native-host.js +11 -1
  30. package/dist/src/remote/claude-chat.d.ts +21 -0
  31. package/dist/src/remote/claude-chat.js +209 -0
  32. package/dist/src/remote/codex-rpc.d.ts +19 -0
  33. package/dist/src/remote/codex-rpc.js +94 -0
  34. package/dist/src/remote/http.d.ts +5 -0
  35. package/dist/src/remote/http.js +99 -7
  36. package/dist/src/remote/key.d.ts +4 -0
  37. package/dist/src/remote/key.js +6 -2
  38. package/dist/src/remote/phone-chat.d.ts +57 -0
  39. package/dist/src/remote/phone-chat.js +366 -0
  40. package/dist/src/remote/phone-control.d.ts +6 -0
  41. package/dist/src/remote/phone-control.js +132 -0
  42. package/dist/src/remote/phone-pairing.d.ts +41 -0
  43. package/dist/src/remote/phone-pairing.js +180 -0
  44. package/dist/src/remote/relay.d.ts +37 -0
  45. package/dist/src/remote/relay.js +207 -0
  46. package/docs/CLIENT-APPROVAL.md +44 -9
  47. package/docs/PHONE-PAIRING.md +97 -0
  48. package/extension-dist/THIRD-PARTY-NOTICES.txt +35 -0
  49. package/extension-dist/activity-dashboard.css +4 -9
  50. package/extension-dist/background.js +725 -127
  51. package/extension-dist/brand.css +63 -0
  52. package/extension-dist/manifest.json +1 -1
  53. package/extension-dist/options.html +25 -22
  54. package/extension-dist/options.js +179 -42
  55. package/extension-dist/popup.html +33 -41
  56. package/extension-dist/popup.js +2529 -126
  57. package/extension-dist/site-protection.css +11 -0
  58. package/package.json +13 -2
@@ -492,6 +492,16 @@ async function gatedUrl(ex, tabId) {
492
492
  */
493
493
  async function gate(ctx, method, opts = {}) {
494
494
  const url = opts.url ?? ((0, policy_1.isUrlGated)(method) ? await gatedUrl(ctx.ex, opts.tabId) : '');
495
+ // New extensions own the local site mode and warning decisions. Check the
496
+ // capability gates here first, then ask that exact paired browser to authorise
497
+ // the URL. The browser checks again against the live page before execution.
498
+ if ((0, policy_1.isUrlGated)(method) && /^https?:\/\//i.test(url) && ctx.ex.checkSiteAccess) {
499
+ (0, policy_1.assertUrlAllowed)(url, method, { ...ctx.policy, allowDomains: [(0, policy_1.hostOf)(url)] });
500
+ if (await ctx.ex.checkSiteAccess(url)) {
501
+ (0, audit_1.noteGate)(url, true);
502
+ return;
503
+ }
504
+ }
495
505
  try {
496
506
  (0, policy_1.assertUrlAllowed)(url, method, ctx.policy);
497
507
  }
@@ -940,10 +950,13 @@ exports.TOOL_HANDLERS = {
940
950
  page_watch: async (a, ctx) => {
941
951
  const action = (0, validators_1.requireString)(a, 'action');
942
952
  const ws = (0, workspace_1.getActiveWorkspace)();
953
+ // Phones share a browser profile, but their saved page data belongs only
954
+ // to their own workspace. Desktop checkpoints remain profile-scoped.
955
+ const watchDir = (0, client_identity_1.currentClientIdentity)()?.profile ? ws.taskDir : ws.profileDir;
943
956
  if (action === 'list')
944
- return (0, envelopes_1.jsonResult)({ watches: (0, page_watch_1.listWatches)(ws.profileDir) });
957
+ return (0, envelopes_1.jsonResult)({ watches: (0, page_watch_1.listWatches)(watchDir) });
945
958
  if (action === 'delete') {
946
- (0, page_watch_1.removeWatch)(ws.profileDir, (0, validators_1.requireString)(a, 'watchId'));
959
+ (0, page_watch_1.removeWatch)(watchDir, (0, validators_1.requireString)(a, 'watchId'));
947
960
  return (0, envelopes_1.jsonResult)({ deleted: true });
948
961
  }
949
962
  await gate(ctx, 'get_text', { tabId: tabId(a) });
@@ -951,13 +964,13 @@ exports.TOOL_HANDLERS = {
951
964
  const clean = (0, page_watch_1.safeSnapshot)(snap, (s) => (0, redact_1.redactText)(s, redaction(ctx.policy)).value);
952
965
  if (action === 'create') {
953
966
  const name = (0, validators_1.requireWithinLength)((0, validators_1.requireString)(a, 'name'), 'name', 80);
954
- const saved = (0, page_watch_1.createWatch)(ws.profileDir, name, clean);
955
- return (0, envelopes_1.jsonResult)({ watchId: saved.id, name: saved.name, url: saved.url, title: clean.title, elements: clean.nodes.length, savedAt: new Date(saved.ts).toISOString(), note: 'Checkpoint saved locally for this Chrome profile. Form values are not stored.' });
967
+ const saved = (0, page_watch_1.createWatch)(watchDir, name, clean);
968
+ return (0, envelopes_1.jsonResult)({ watchId: saved.id, name: saved.name, url: saved.url, title: clean.title, elements: clean.nodes.length, savedAt: new Date(saved.ts).toISOString(), note: `Checkpoint saved locally for this ${(0, client_identity_1.currentClientIdentity)()?.profile ? 'phone' : 'Chrome profile'}. Form values are not stored.` });
956
969
  }
957
970
  if (action === 'compare') {
958
- const previous = (0, page_watch_1.readWatch)(ws.profileDir, (0, validators_1.requireString)(a, 'watchId'));
971
+ const previous = (0, page_watch_1.readWatch)(watchDir, (0, validators_1.requireString)(a, 'watchId'));
959
972
  // The site's grant may have been revoked since the checkpoint was saved.
960
- (0, policy_1.assertUrlAllowed)(previous.url, 'get_text', ctx.policy);
973
+ await gate(ctx, 'get_text', { url: previous.url });
961
974
  const diff = (0, page_watch_1.compareWatch)(previous, clean, (s) => (0, redact_1.redactText)(s, redaction(ctx.policy)).value);
962
975
  return (0, envelopes_1.jsonResult)({ watchId: previous.id, name: previous.name, previousUrl: previous.url, currentUrl: clean.url, previousAt: new Date(previous.ts).toISOString(), checkedAt: new Date().toISOString(), ...diff });
963
976
  }
@@ -1431,6 +1444,15 @@ function isRetryableFault(name, err) {
1431
1444
  return err instanceof types_1.ExecutorError && err.code === 'EXTENSION_DISCONNECTED' && RETRY_SAFE_TOOLS.has(name);
1432
1445
  }
1433
1446
  async function dispatchToolCall(name, rawArgs, opts = {}) {
1447
+ const client = (0, client_identity_1.currentClientIdentity)();
1448
+ try {
1449
+ client?.assertAccess?.();
1450
+ }
1451
+ catch {
1452
+ return (0, envelopes_1.errorResult)('Phone access was removed or expired.');
1453
+ }
1454
+ if (client?.profile && PHONE_DISABLED_TOOLS.has(name))
1455
+ return (0, envelopes_1.errorResult)('This tool is not available to a paired phone.');
1434
1456
  // The allowlist is checked here, not only at registration, so a `batch` op
1435
1457
  // cannot reach a tool the operator kept off the surface.
1436
1458
  if (!isToolEnabled(name))
@@ -1520,9 +1542,10 @@ function modelFromMeta(meta) {
1520
1542
  return typeof model === 'string' ? model : undefined;
1521
1543
  }
1522
1544
  /** Register the tool surface; returns the names actually advertised. */
1523
- function registerTools(server, policy, accessGuard = access_1.requireAccess) {
1545
+ const PHONE_DISABLED_TOOLS = new Set(['profile_use', 'profile_rename', 'task_new', 'tasks_list', 'task_status']);
1546
+ function registerTools(server, policy, accessGuard = access_1.requireAccess, phoneIdentity) {
1524
1547
  assertNoDrift();
1525
- const identity = (0, client_identity_1.createClientIdentity)(() => {
1548
+ const identity = phoneIdentity ?? (0, client_identity_1.createClientIdentity)(() => {
1526
1549
  const client = server.server.getClientVersion();
1527
1550
  return client ? `${client.name} ${client.version}` : 'Unnamed app';
1528
1551
  });
@@ -1535,6 +1558,8 @@ function registerTools(server, policy, accessGuard = access_1.requireAccess) {
1535
1558
  for (const d of exports.TOOL_DEFINITIONS) {
1536
1559
  if (!isToolEnabled(d.name))
1537
1560
  continue;
1561
+ if (phoneIdentity && PHONE_DISABLED_TOOLS.has(d.name))
1562
+ continue;
1538
1563
  // A tool the policy has switched off can only ever answer POLICY_DENIED.
1539
1564
  // Don't pay for its schema on every turn, and don't invite a wasted call.
1540
1565
  if (!isToolPolicyUsable(d.name, policy)) {
@@ -1542,6 +1567,12 @@ function registerTools(server, policy, accessGuard = access_1.requireAccess) {
1542
1567
  continue;
1543
1568
  }
1544
1569
  server.registerTool(d.name, { description: d.description, inputSchema: d.inputSchema }, async (args, extra) => (0, client_identity_1.withClientIdentity)(identity, async () => {
1570
+ try {
1571
+ identity.assertAccess?.();
1572
+ }
1573
+ catch {
1574
+ return (0, envelopes_1.errorResult)('Phone access was removed or expired.');
1575
+ }
1545
1576
  (0, telemetry_1.noteMcpModel)(modelFromMeta(extra?._meta));
1546
1577
  let verifiedNow = false;
1547
1578
  const accessError = await accessGuard(server.server, (0, config_1.resolveDataDir)(), {
@@ -1556,9 +1587,15 @@ function registerTools(server, policy, accessGuard = access_1.requireAccess) {
1556
1587
  // Approval and site refusals run nothing, so the call waits for the
1557
1588
  // person here and continues once they click Approve or Allow.
1558
1589
  const result = await (0, person_wait_1.waitForPerson)(async (retry) => {
1590
+ try {
1591
+ identity.assertAccess?.();
1592
+ }
1593
+ catch {
1594
+ return (0, envelopes_1.errorResult)('Phone access was removed or expired.');
1595
+ }
1559
1596
  if (profileBridge?.hasActiveExtension()) {
1560
1597
  const active = (0, workspace_1.peekActiveWorkspace)()?.profile ?? 'default';
1561
- const profile = profileBridge.hasConnection(active) ? active : profileBridge.pairedProfiles()[0]?.name;
1598
+ const profile = phoneIdentity?.profile ?? (profileBridge.hasConnection(active) ? active : profileBridge.pairedProfiles()[0]?.name);
1562
1599
  try {
1563
1600
  await profileBridge.sendCommand('ping_probe', { requireClientApproval: true }, { profile });
1564
1601
  }
@@ -31,6 +31,7 @@ const node_fs_1 = require("node:fs");
31
31
  const node_os_1 = require("node:os");
32
32
  const node_path_1 = require("node:path");
33
33
  const protocol_1 = require("../shared/protocol");
34
+ const phone_control_1 = require("./remote/phone-control");
34
35
  /** The host's name, as the extension addresses it. */
35
36
  exports.NATIVE_HOST_NAME = 'com.mehmoodqureshi.mcp_browser_extension';
36
37
  function nativeHostPaths(dataDir, platform = process.platform) {
@@ -197,6 +198,7 @@ function frameMessage(msg) {
197
198
  function runNativeHost(dataDir, profile) {
198
199
  const chunks = [];
199
200
  let answered = false;
201
+ let pending = false;
200
202
  const answer = (reply) => {
201
203
  if (answered)
202
204
  return;
@@ -204,6 +206,8 @@ function runNativeHost(dataDir, profile) {
204
206
  process.stdout.write(frameMessage(reply), () => process.exit(0));
205
207
  };
206
208
  process.stdin.on('data', (c) => {
209
+ if (answered || pending)
210
+ return;
207
211
  chunks.push(c);
208
212
  const buf = Buffer.concat(chunks);
209
213
  if (buf.length < 4)
@@ -220,10 +224,16 @@ function runNativeHost(dataDir, profile) {
220
224
  catch {
221
225
  return answer({ ok: false, error: 'bad_request', message: 'not JSON' });
222
226
  }
227
+ if (msg.type === 'phone_pairing') {
228
+ pending = true;
229
+ void (0, phone_control_1.phoneControlReply)(dataDir, msg).then(answer, () => answer({ ok: false, message: 'Could not reach phone pairing.' }));
230
+ return;
231
+ }
223
232
  if (msg.type !== 'pairing')
224
233
  return answer({ ok: false, error: 'bad_request', message: 'unknown request' });
225
234
  answer(pairingReply(dataDir, profile));
226
235
  });
227
- process.stdin.on('end', () => answer({ ok: false, error: 'bad_request', message: 'no request' }));
236
+ process.stdin.on('end', () => { if (!pending)
237
+ answer({ ok: false, error: 'bad_request', message: 'no request' }); });
228
238
  }
229
239
  //# sourceMappingURL=native-host.js.map
@@ -0,0 +1,21 @@
1
+ import type { Options, Query, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
+ import { requireAccess } from '../access';
3
+ import type { Policy } from '../security/policy';
4
+ import type { PhonePairing } from './phone-pairing';
5
+ import { type ChatRunner } from './phone-chat';
6
+ /**
7
+ * The Claude Agent SDK bundles Claude Code (hundreds of MB), so it is an
8
+ * optional peer: only computers that offer Claude in phone chat install it.
9
+ */
10
+ export declare function claudeChatAvailable(resolve?: (id: string) => string): boolean;
11
+ type QueryFactory = (params: {
12
+ prompt: AsyncIterable<SDKUserMessage>;
13
+ options: Options;
14
+ }) => Query;
15
+ /**
16
+ * Phone chat through the local Claude Code sign-in. Claude only receives the
17
+ * paired phone's browser tools: no built-in tools, user settings, hooks,
18
+ * skills, other MCP servers or saved sessions.
19
+ */
20
+ export declare function createClaudeChatRunner(pairing: PhonePairing, id: string, version: string, policy: Policy, accessGuard?: typeof requireAccess, queryFactory?: QueryFactory): ChatRunner;
21
+ export {};
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.claudeChatAvailable = claudeChatAvailable;
37
+ exports.createClaudeChatRunner = createClaudeChatRunner;
38
+ const node_fs_1 = require("node:fs");
39
+ const node_path_1 = require("node:path");
40
+ const server_1 = require("../mcp/server");
41
+ const access_1 = require("../access");
42
+ const phone_chat_1 = require("./phone-chat");
43
+ const SERVER = 'browser';
44
+ const SDK = '@anthropic-ai/claude-agent-sdk';
45
+ /**
46
+ * The Claude Agent SDK bundles Claude Code (hundreds of MB), so it is an
47
+ * optional peer: only computers that offer Claude in phone chat install it.
48
+ */
49
+ function claudeChatAvailable(resolve = require.resolve) {
50
+ try {
51
+ resolve(SDK);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ const PREFIX = `mcp__${SERVER}__`;
59
+ const models = () => ({
60
+ fast: process.env.MBE_PHONE_CLAUDE_FAST_MODEL || 'haiku',
61
+ smart: process.env.MBE_PHONE_CLAUDE_MODEL || 'sonnet',
62
+ });
63
+ /**
64
+ * Phone chat through the local Claude Code sign-in. Claude only receives the
65
+ * paired phone's browser tools: no built-in tools, user settings, hooks,
66
+ * skills, other MCP servers or saved sessions.
67
+ */
68
+ function createClaudeChatRunner(pairing, id, version, policy, accessGuard = access_1.requireAccess, queryFactory) {
69
+ const identity = pairing.identity(id);
70
+ const cwd = (0, node_path_1.join)(pairing.dataDir, 'phone-chat');
71
+ (0, node_fs_1.mkdirSync)(cwd, { recursive: true, mode: 0o700 });
72
+ const server = (0, server_1.createServer)(version, policy, accessGuard, identity);
73
+ const abort = new AbortController();
74
+ const inbox = [];
75
+ let wakeInbox;
76
+ let closed = false;
77
+ let session;
78
+ let starting;
79
+ let model = models().smart;
80
+ let turn;
81
+ const prompt = async function* () {
82
+ while (!closed) {
83
+ const next = inbox.shift();
84
+ if (next) {
85
+ yield next;
86
+ continue;
87
+ }
88
+ await new Promise((resolve) => { wakeInbox = resolve; });
89
+ wakeInbox = undefined;
90
+ }
91
+ };
92
+ const shown = () => [...turn.parts.values(), turn.live].filter(Boolean).join('\n\n').slice(0, 24_000);
93
+ const canUseTool = async (name, input) => {
94
+ if (closed || !turn || !name.startsWith(PREFIX))
95
+ return { behavior: 'deny', message: 'This tool is not available.' };
96
+ try {
97
+ identity.assertAccess?.();
98
+ }
99
+ catch {
100
+ return { behavior: 'deny', message: 'Phone access was removed. Ask the user to check the extension.', interrupt: true };
101
+ }
102
+ turn.update(null, phone_chat_1.toolActivity.get(name.slice(PREFIX.length)) ?? 'Using the browser');
103
+ return { behavior: 'allow', updatedInput: input };
104
+ };
105
+ const handle = (message) => {
106
+ if (!turn)
107
+ return;
108
+ if (message.type === 'stream_event') {
109
+ const event = message.event;
110
+ if (event.type === 'message_start')
111
+ turn.live = '';
112
+ if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta' && typeof event.delta.text === 'string') {
113
+ turn.live = (turn.live + event.delta.text).slice(0, 24_000);
114
+ turn.update(shown(), 'Writing a reply');
115
+ }
116
+ }
117
+ else if (message.type === 'assistant') {
118
+ const text = message.message.content.filter((block) => block.type === 'text').map((block) => block.text).join('');
119
+ turn.live = '';
120
+ if (text)
121
+ turn.parts.set(message.message.id ?? message.uuid, text.slice(0, 24_000));
122
+ turn.update(shown(), message.message.content.some((block) => block.type === 'tool_use') ? 'Using the browser' : 'Writing a reply');
123
+ }
124
+ else if (message.type === 'result') {
125
+ const current = turn;
126
+ turn = undefined;
127
+ if (message.subtype === 'success' && !message.is_error)
128
+ current.resolve();
129
+ else
130
+ current.reject(new Error('Claude turn failed or stopped.'));
131
+ }
132
+ };
133
+ const start = async (initial) => {
134
+ model = initial;
135
+ const factory = queryFactory ?? await Promise.resolve(`${SDK}`).then(s => __importStar(require(s))).then((sdk) => sdk.query, () => {
136
+ throw new phone_chat_1.ChatRequestError(409, 'Claude is not set up on your computer. Install @anthropic-ai/claude-agent-sdk next to MCP Browser Extension, then start a new chat.');
137
+ });
138
+ const env = { PATH: process.env.PATH, CLAUDE_AGENT_SDK_CLIENT_APP: `mcp-browser-extension-mobile/${version}` };
139
+ // Claude Code owns its sign-in. Never forward API keys or tokens from this process.
140
+ for (const key of ['HOME', 'USER', 'TMPDIR', 'LANG', 'CLAUDE_CONFIG_DIR'])
141
+ if (process.env[key])
142
+ env[key] = process.env[key];
143
+ session = factory({ prompt: prompt(), options: {
144
+ cwd, env, abortController: abort, systemPrompt: phone_chat_1.chatInstructions,
145
+ model, effort: 'low',
146
+ tools: [], settingSources: [], strictMcpConfig: true, persistSession: false, includePartialMessages: true,
147
+ mcpServers: { [SERVER]: { type: 'sdk', name: SERVER, instance: server, timeout: 300_000 } },
148
+ permissionMode: 'default', canUseTool,
149
+ ...(process.env.MBE_CLAUDE_BIN ? { pathToClaudeCodeExecutable: process.env.MBE_CLAUDE_BIN } : {}),
150
+ } });
151
+ void (async () => {
152
+ try {
153
+ for await (const message of session) {
154
+ if (closed)
155
+ return;
156
+ handle(message);
157
+ }
158
+ }
159
+ catch { /* Reported to the active turn below. */ }
160
+ if (!closed && turn) {
161
+ const current = turn;
162
+ turn = undefined;
163
+ current.reject(new phone_chat_1.ChatRequestError(409, 'Claude could not finish. Check that Claude Code is installed and signed in on your computer, then start a new chat.'));
164
+ }
165
+ })();
166
+ };
167
+ const close = () => {
168
+ if (closed)
169
+ return;
170
+ closed = true;
171
+ abort.abort();
172
+ wakeInbox?.();
173
+ turn?.reject(new Error('Stopped.'));
174
+ turn = undefined;
175
+ try {
176
+ session?.close();
177
+ }
178
+ catch { /* Already stopped. */ }
179
+ void server.close().catch(() => undefined);
180
+ };
181
+ return {
182
+ // Spawning Claude Code and loading the browser tools happens before the first message.
183
+ warm: () => { if (!closed) {
184
+ starting ??= start(models().smart);
185
+ starting.catch(() => undefined);
186
+ } },
187
+ run: async (text, update, speed = 'smart') => {
188
+ if (closed)
189
+ throw new Error('Closed chat.');
190
+ identity.assertAccess?.();
191
+ const wanted = models()[speed];
192
+ starting ??= start(wanted);
193
+ await starting;
194
+ if (closed)
195
+ throw new Error('Closed chat.');
196
+ if (model !== wanted) {
197
+ await session.setModel(wanted);
198
+ model = wanted;
199
+ }
200
+ await new Promise((resolve, reject) => {
201
+ turn = { resolve, reject, update, parts: new Map(), live: '' };
202
+ update(null, 'Working on your request');
203
+ inbox.push({ type: 'user', message: { role: 'user', content: text }, parent_tool_use_id: null });
204
+ wakeInbox?.();
205
+ });
206
+ }, close,
207
+ };
208
+ }
209
+ //# sourceMappingURL=claude-chat.js.map
@@ -0,0 +1,19 @@
1
+ type Message = {
2
+ id?: number | string;
3
+ method?: string;
4
+ params?: any;
5
+ result?: any;
6
+ error?: {
7
+ message?: string;
8
+ };
9
+ };
10
+ export interface CodexRpc {
11
+ request(method: string, params: unknown): Promise<any>;
12
+ notify(method: string, params: unknown): void;
13
+ respond(id: number | string, result: unknown): void;
14
+ close(): void;
15
+ }
16
+ export declare const CODEX_DISABLED_FEATURES: string[];
17
+ /** Local stdio only. Codex owns its sign-in; credentials never enter the phone protocol. */
18
+ export declare function startCodexRpc(cwd: string, event: (message: Message) => void, failed: () => void): CodexRpc;
19
+ export {};
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CODEX_DISABLED_FEATURES = void 0;
4
+ exports.startCodexRpc = startCodexRpc;
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_path_1 = require("node:path");
7
+ exports.CODEX_DISABLED_FEATURES = ['shell_tool', 'unified_exec', 'apps', 'plugins', 'remote_plugin', 'multi_agent', 'multi_agent_v2',
8
+ 'browser_use', 'browser_use_external', 'computer_use', 'image_generation', 'view_image', 'code_mode',
9
+ 'hooks', 'memories', 'skill_search', 'skill_mcp_dependency_install'];
10
+ /** Local stdio only. Codex owns its sign-in; credentials never enter the phone protocol. */
11
+ function startCodexRpc(cwd, event, failed) {
12
+ const overrides = { web_search: 'disabled', notify: [], project_doc_max_bytes: 0, model_provider: 'openai',
13
+ model_reasoning_effort: 'low', approval_policy: 'never', sandbox_mode: 'read-only', service_tier: 'default' };
14
+ const args = ['app-server', '--listen', 'stdio://', ...exports.CODEX_DISABLED_FEATURES.flatMap((name) => ['--disable', name]),
15
+ ...Object.entries(overrides).flatMap(([key, value]) => ['-c', `${key}=${JSON.stringify(value)}`])];
16
+ const env = { PATH: `${(0, node_path_1.dirname)(process.execPath)}:${process.env.PATH ?? '/usr/bin:/bin'}` };
17
+ for (const key of ['HOME', 'USER', 'TMPDIR', 'LANG', 'CODEX_HOME'])
18
+ if (process.env[key])
19
+ env[key] = process.env[key];
20
+ const child = (0, node_child_process_1.spawn)(process.env.MBE_CODEX_BIN || 'codex', args, { cwd, env, stdio: 'pipe' });
21
+ const pending = new Map();
22
+ let closed = false;
23
+ let sequence = 0;
24
+ let buffer = '';
25
+ const close = () => {
26
+ if (closed)
27
+ return;
28
+ closed = true;
29
+ child.kill();
30
+ const kill = setTimeout(() => { if (child.exitCode === null)
31
+ child.kill('SIGKILL'); }, 1500);
32
+ kill.unref();
33
+ for (const p of pending.values()) {
34
+ clearTimeout(p.timer);
35
+ p.reject(new Error('Codex connection closed.'));
36
+ }
37
+ pending.clear();
38
+ buffer = '';
39
+ };
40
+ const fail = () => { if (!closed) {
41
+ close();
42
+ failed();
43
+ } };
44
+ const write = (message) => { if (!closed)
45
+ child.stdin.write(JSON.stringify(message) + '\n'); };
46
+ child.stderr.resume(); // Do not log model text, browser content or account diagnostics.
47
+ child.on('error', fail);
48
+ child.on('exit', fail);
49
+ child.stdin.on('error', fail);
50
+ child.stdout.setEncoding('utf8');
51
+ child.stdout.on('data', (chunk) => {
52
+ buffer += chunk;
53
+ if (buffer.length > 8 * 1024 * 1024)
54
+ return fail();
55
+ let newline;
56
+ while ((newline = buffer.indexOf('\n')) >= 0) {
57
+ const line = buffer.slice(0, newline);
58
+ buffer = buffer.slice(newline + 1);
59
+ let message;
60
+ try {
61
+ message = JSON.parse(line);
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ if (!message || typeof message !== 'object')
67
+ continue;
68
+ const request = typeof message.id === 'number' && !message.method ? pending.get(message.id) : undefined;
69
+ if (request) {
70
+ pending.delete(message.id);
71
+ clearTimeout(request.timer);
72
+ if (message.error)
73
+ request.reject(new Error('Codex could not complete the request.'));
74
+ else
75
+ request.resolve(message.result);
76
+ }
77
+ else
78
+ event(message);
79
+ }
80
+ });
81
+ return {
82
+ request: (method, params) => new Promise((resolve, reject) => {
83
+ if (closed)
84
+ return reject(new Error('Codex connection closed.'));
85
+ const id = ++sequence;
86
+ const timer = setTimeout(() => { pending.delete(id); reject(new Error('Codex took too long to connect.')); }, 45_000);
87
+ pending.set(id, { resolve, reject, timer });
88
+ write({ id, method, params });
89
+ }),
90
+ notify: (method, params) => write({ method, params }),
91
+ respond: (id, result) => write({ id, result }), close,
92
+ };
93
+ }
94
+ //# sourceMappingURL=codex-rpc.js.map
@@ -14,6 +14,8 @@
14
14
  import { type IncomingMessage } from 'node:http';
15
15
  import { requireAccess } from '../access';
16
16
  import type { Policy } from '../security/policy';
17
+ import type { PhonePairing } from './phone-pairing';
18
+ import { PhoneChat } from './phone-chat';
17
19
  export interface HttpMcpOptions {
18
20
  port: number;
19
21
  key: string;
@@ -22,6 +24,9 @@ export interface HttpMcpOptions {
22
24
  log: (message: string) => void;
23
25
  /** Test seam; production callers use the Firebase-backed gate by default. */
24
26
  accessGuard?: typeof requireAccess;
27
+ phonePairing?: PhonePairing;
28
+ /** Personal, opt-in local Codex or Claude companion. Never runs on the relay host. */
29
+ phoneChat?: boolean | PhoneChat;
25
30
  }
26
31
  export interface HttpMcp {
27
32
  port: number;