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.
- package/README.md +33 -15
- package/dist/shared/client-auth.d.ts +2 -0
- package/dist/shared/phone-pairing.d.ts +35 -0
- package/dist/shared/phone-pairing.js +10 -0
- package/dist/shared/policy.js +7 -0
- package/dist/shared/protocol.d.ts +13 -2
- package/dist/shared/protocol.js +13 -1
- package/dist/shared/site-risk.d.ts +31 -0
- package/dist/shared/site-risk.js +190 -0
- package/dist/src/bridge/client-identity.d.ts +11 -5
- package/dist/src/bridge/client-identity.js +7 -4
- package/dist/src/bridge/connection.js +1 -0
- package/dist/src/bridge/server.d.ts +1 -0
- package/dist/src/bridge/server.js +38 -19
- package/dist/src/bridge/workspace.d.ts +1 -0
- package/dist/src/bridge/workspace.js +11 -3
- package/dist/src/cli.js +61 -18
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +26 -1
- package/dist/src/executor/extension-executor.d.ts +1 -0
- package/dist/src/executor/extension-executor.js +10 -1
- package/dist/src/executor/types.d.ts +3 -1
- package/dist/src/mcp/helpers.js +18 -6
- package/dist/src/mcp/person-wait.js +2 -1
- package/dist/src/mcp/server.d.ts +2 -1
- package/dist/src/mcp/server.js +4 -4
- package/dist/src/mcp/tools.d.ts +2 -2
- package/dist/src/mcp/tools.js +46 -9
- package/dist/src/native-host.js +11 -1
- package/dist/src/remote/claude-chat.d.ts +21 -0
- package/dist/src/remote/claude-chat.js +209 -0
- package/dist/src/remote/codex-rpc.d.ts +19 -0
- package/dist/src/remote/codex-rpc.js +94 -0
- package/dist/src/remote/http.d.ts +5 -0
- package/dist/src/remote/http.js +99 -7
- package/dist/src/remote/key.d.ts +4 -0
- package/dist/src/remote/key.js +6 -2
- package/dist/src/remote/phone-chat.d.ts +57 -0
- package/dist/src/remote/phone-chat.js +366 -0
- package/dist/src/remote/phone-control.d.ts +6 -0
- package/dist/src/remote/phone-control.js +132 -0
- package/dist/src/remote/phone-pairing.d.ts +41 -0
- package/dist/src/remote/phone-pairing.js +180 -0
- package/dist/src/remote/relay.d.ts +37 -0
- package/dist/src/remote/relay.js +207 -0
- package/docs/CLIENT-APPROVAL.md +44 -9
- package/docs/PHONE-PAIRING.md +97 -0
- package/extension-dist/THIRD-PARTY-NOTICES.txt +35 -0
- package/extension-dist/activity-dashboard.css +4 -9
- package/extension-dist/background.js +725 -127
- package/extension-dist/brand.css +63 -0
- package/extension-dist/manifest.json +1 -1
- package/extension-dist/options.html +25 -22
- package/extension-dist/options.js +179 -42
- package/extension-dist/popup.html +33 -41
- package/extension-dist/popup.js +2529 -126
- package/extension-dist/site-protection.css +11 -0
- package/package.json +13 -2
package/dist/src/mcp/tools.js
CHANGED
|
@@ -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)(
|
|
957
|
+
return (0, envelopes_1.jsonResult)({ watches: (0, page_watch_1.listWatches)(watchDir) });
|
|
945
958
|
if (action === 'delete') {
|
|
946
|
-
(0, page_watch_1.removeWatch)(
|
|
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)(
|
|
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:
|
|
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)(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/src/native-host.js
CHANGED
|
@@ -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', () =>
|
|
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;
|