friend-assistant 0.1.42 → 0.1.44
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 +1 -1
- package/dist/BUILD.json +2 -2
- package/dist/chrome-control/dist/index.js +255 -76
- package/dist/friend.mjs +194 -20
- package/dist/web/assets/index-D6qxQkPK.js +4456 -0
- package/dist/web/assets/index-DrNs6WlQ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DdfqW6WX.js +0 -4456
- package/dist/web/assets/index-ehxATbhd.css +0 -1
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ Connectors (Gmail, Chrome control, any MCP service) are managed inside the app,
|
|
|
138
138
|
| `ollama.url`, `ollama.model`, `ollama.vision`, `ollama.contextTokens` | The local model: server address, model name, whether it can see pictures, and how much it remembers at once. |
|
|
139
139
|
| `openai.url`, `openai.model`, `openai.apiKey`, `openai.vision` | An OpenAI-compatible server: its address (ending in `/v1`), the model name it knows, its API key (or empty), and whether the model can see pictures. For OmniRoute (https://github.com/diegosouzapw/OmniRoute) run `npm install -g omniroute` and `omniroute`, then `friend config set provider openai` (the defaults are OmniRoute's: `http://localhost:20128/v1` and model `auto`). Also works with LM Studio, LiteLLM, vLLM and OpenRouter. |
|
|
140
140
|
| `voice.hindiVoice`, `voice.marathiVoice` | The voices that read Hindi and Marathi (Devanagari) text: `hi_IN-priyamvada-medium`, `hi_IN-pratham-medium`, `hi_IN-rohan-medium`, `mr_IN-google-medium`. Empty makes the main voice read that language. See "Hindi, Marathi and English together". |
|
|
141
|
-
| `voice.enabled`, `voice.sttModel`, `voice.ttsVoice`, `voice.language`, `voice.speakTyped`, `voice.accept`, `voice.challenge` | Speech on or off, the listening model, the speaking voice, a forced language, speaking typed replies, and the two thresholds for recognising your voice. |
|
|
141
|
+
| `voice.enabled`, `voice.sttModel`, `voice.ttsVoice`, `voice.language`, `voice.speakTyped`, `voice.duck`, `voice.duckPercent`, `voice.accept`, `voice.challenge` | Speech on or off, the listening model, the speaking voice, a forced language, speaking typed replies, turning music and video down while you talk and while Friend speaks (and how far, in percent), and the two thresholds for recognising your voice. |
|
|
142
142
|
| `bind`, `port` | Where Friend listens (easier: `friend lan`). |
|
|
143
143
|
| `https` | `true` serves a secure `https://` address with a certificate Friend makes for itself, so a phone may use the microphone (easier: `friend lan https on`). |
|
|
144
144
|
| `publicUrl`, `allowedOrigins`, `allowedHosts`, `trustProxy` | For running behind your own domain or reverse proxy (see "Using your own domain later"). |
|
package/dist/BUILD.json
CHANGED
|
@@ -11302,6 +11302,156 @@ var init_bridge = __esm({
|
|
|
11302
11302
|
}
|
|
11303
11303
|
});
|
|
11304
11304
|
|
|
11305
|
+
// ../chrome-control/src/browser.ts
|
|
11306
|
+
var browser_exports = {};
|
|
11307
|
+
__export(browser_exports, {
|
|
11308
|
+
DEBUG_FLAG: () => DEBUG_FLAG,
|
|
11309
|
+
chromeCommandLines: () => chromeCommandLines,
|
|
11310
|
+
countShortcuts: () => countShortcuts,
|
|
11311
|
+
describeMains: () => describeMains,
|
|
11312
|
+
flagStatus: () => flagStatus,
|
|
11313
|
+
hasFlag: () => hasFlag,
|
|
11314
|
+
isMainProcess: () => isMainProcess,
|
|
11315
|
+
parseShortcutRows: () => parseShortcutRows,
|
|
11316
|
+
patchArguments: () => patchArguments,
|
|
11317
|
+
patchShortcuts: () => patchShortcuts,
|
|
11318
|
+
restartChrome: () => restartChrome,
|
|
11319
|
+
shortcutScript: () => shortcutScript,
|
|
11320
|
+
startChrome: () => startChrome
|
|
11321
|
+
});
|
|
11322
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11323
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
11324
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
11325
|
+
import { join as join2 } from "node:path";
|
|
11326
|
+
import { rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
11327
|
+
function describeMains(cmds) {
|
|
11328
|
+
const mains = cmds.filter(isMainProcess);
|
|
11329
|
+
if (!mains.length) return { running: false, hidden: null };
|
|
11330
|
+
return { running: true, hidden: mains.every(hasFlag) };
|
|
11331
|
+
}
|
|
11332
|
+
function patchArguments(args) {
|
|
11333
|
+
if (hasFlag(args)) return { args, changed: false };
|
|
11334
|
+
return { args: `${args} ${DEBUG_FLAG}`.trim(), changed: true };
|
|
11335
|
+
}
|
|
11336
|
+
function shortcutScript(patch) {
|
|
11337
|
+
return [
|
|
11338
|
+
`$flag = '${DEBUG_FLAG}'`,
|
|
11339
|
+
"$dirs = @([Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('CommonDesktopDirectory'),",
|
|
11340
|
+
" [Environment]::GetFolderPath('Programs'), [Environment]::GetFolderPath('CommonPrograms'),",
|
|
11341
|
+
" (Join-Path $env:APPDATA 'Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar')) | Where-Object { $_ -and (Test-Path $_) }",
|
|
11342
|
+
"$sh = New-Object -ComObject WScript.Shell",
|
|
11343
|
+
"foreach ($d in $dirs) { Get-ChildItem -Path $d -Filter *.lnk -Recurse -ErrorAction SilentlyContinue | ForEach-Object {",
|
|
11344
|
+
" try { $l = $sh.CreateShortcut($_.FullName)",
|
|
11345
|
+
" if ($l.TargetPath -match '(chrome|msedge|brave)\\.exe$') {",
|
|
11346
|
+
patch ? ` if ($l.Arguments -notlike "*$flag*") { $l.Arguments = ($l.Arguments + ' ' + $flag).Trim(); $l.Save(); Write-Output ('patched|' + $_.FullName) }` : ` if ($l.Arguments -notlike "*$flag*") { Write-Output ('none|' + $_.FullName) }`,
|
|
11347
|
+
patch ? " else { Write-Output ('already|' + $_.FullName) } } } catch { Write-Output ('failed|' + $_.FullName) } } }" : " else { Write-Output ('has|' + $_.FullName) } } } catch { Write-Output ('failed|' + $_.FullName) } } }"
|
|
11348
|
+
].join("\n");
|
|
11349
|
+
}
|
|
11350
|
+
function parseShortcutRows(out) {
|
|
11351
|
+
const rows = out.split(/\r?\n/).map((l) => l.split("|")[0].trim()).filter(Boolean);
|
|
11352
|
+
const n = (k) => rows.filter((r) => r === k).length;
|
|
11353
|
+
const c = { patched: n("patched"), already: n("already"), failed: n("failed"), has: n("has"), none: n("none") };
|
|
11354
|
+
return { patched: c.patched, already: c.already, failed: c.failed, withFlag: c.patched + c.already + c.has, total: c.patched + c.already + c.failed + c.has + c.none };
|
|
11355
|
+
}
|
|
11356
|
+
function powershell(script) {
|
|
11357
|
+
const f = join2(tmpdir(), `chrome-control-${process.pid}-${Date.now()}.ps1`);
|
|
11358
|
+
writeFileSync2(f, script);
|
|
11359
|
+
try {
|
|
11360
|
+
const r = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", f], { encoding: "utf8" });
|
|
11361
|
+
return String(r.stdout || "");
|
|
11362
|
+
} finally {
|
|
11363
|
+
rmSync(f, { force: true });
|
|
11364
|
+
}
|
|
11365
|
+
}
|
|
11366
|
+
function chromeCommandLines() {
|
|
11367
|
+
if (WIN) {
|
|
11368
|
+
return powershell(`Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | ForEach-Object { $_.CommandLine }`).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
11369
|
+
}
|
|
11370
|
+
const r = spawnSync("ps", ["-eo", "args"], { encoding: "utf8" });
|
|
11371
|
+
return String(r.stdout || "").split("\n").filter((l) => /(^|\/)(google-chrome(-stable)?|chromium(-browser)?|Google Chrome)(\s|$)/.test(l.trim().split(/\s--/)[0]));
|
|
11372
|
+
}
|
|
11373
|
+
function patchLinux() {
|
|
11374
|
+
const counts = { patched: 0, already: 0, failed: 0, withFlag: 0, total: 0 };
|
|
11375
|
+
for (const name of LINUX_LAUNCHERS) {
|
|
11376
|
+
const src = [linuxDir(), "/usr/share/applications", "/var/lib/snapd/desktop/applications"].map((d) => join2(d, name)).find(existsSync2);
|
|
11377
|
+
if (!src) continue;
|
|
11378
|
+
const text2 = readFileSync2(src, "utf8");
|
|
11379
|
+
counts.total++;
|
|
11380
|
+
if (text2.includes(DEBUG_FLAG)) {
|
|
11381
|
+
counts.already++;
|
|
11382
|
+
continue;
|
|
11383
|
+
}
|
|
11384
|
+
try {
|
|
11385
|
+
const out = text2.replace(/^(Exec=.*)$/gm, (l) => l.includes(DEBUG_FLAG) ? l : l.replace(/(Exec=\S+)/, `$1 ${DEBUG_FLAG}`));
|
|
11386
|
+
writeFileSync2(join2(linuxDir(), name), out);
|
|
11387
|
+
counts.patched++;
|
|
11388
|
+
} catch {
|
|
11389
|
+
counts.failed++;
|
|
11390
|
+
}
|
|
11391
|
+
}
|
|
11392
|
+
counts.withFlag = counts.patched + counts.already;
|
|
11393
|
+
return counts;
|
|
11394
|
+
}
|
|
11395
|
+
function countShortcuts() {
|
|
11396
|
+
if (WIN) return parseShortcutRows(powershell(shortcutScript(false)));
|
|
11397
|
+
if (MAC) return null;
|
|
11398
|
+
const total = LINUX_LAUNCHERS.filter((n) => [linuxDir(), "/usr/share/applications"].some((d) => existsSync2(join2(d, n)))).length;
|
|
11399
|
+
const withFlag = LINUX_LAUNCHERS.filter((n) => existsSync2(join2(linuxDir(), n)) && readFileSync2(join2(linuxDir(), n), "utf8").includes(DEBUG_FLAG)).length;
|
|
11400
|
+
return { patched: 0, already: 0, failed: 0, withFlag, total };
|
|
11401
|
+
}
|
|
11402
|
+
function patchShortcuts() {
|
|
11403
|
+
if (WIN) return parseShortcutRows(powershell(shortcutScript(true)));
|
|
11404
|
+
if (MAC) return null;
|
|
11405
|
+
return patchLinux();
|
|
11406
|
+
}
|
|
11407
|
+
function flagStatus() {
|
|
11408
|
+
return { platform: process.platform, ...describeMains(chromeCommandLines()), shortcuts: countShortcuts() };
|
|
11409
|
+
}
|
|
11410
|
+
function chromeExe() {
|
|
11411
|
+
const cands = WIN ? [join2(process.env.PROGRAMFILES ?? "", "Google", "Chrome", "Application", "chrome.exe"), join2(process.env["PROGRAMFILES(X86)"] ?? "", "Google", "Chrome", "Application", "chrome.exe"), join2(process.env.LOCALAPPDATA ?? "", "Google", "Chrome", "Application", "chrome.exe")] : MAC ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium"];
|
|
11412
|
+
return cands.find(existsSync2) ?? null;
|
|
11413
|
+
}
|
|
11414
|
+
function startChrome(extra = []) {
|
|
11415
|
+
const exe = chromeExe();
|
|
11416
|
+
if (!exe) throw new Error("Chrome was not found. Start it yourself with the flag " + DEBUG_FLAG);
|
|
11417
|
+
spawn(exe, [DEBUG_FLAG, ...extra], { detached: true, stdio: "ignore" }).unref();
|
|
11418
|
+
}
|
|
11419
|
+
async function restartChrome() {
|
|
11420
|
+
const lines = chromeCommandLines();
|
|
11421
|
+
if (describeMains(lines).running) {
|
|
11422
|
+
if (WIN) {
|
|
11423
|
+
powershell("Get-Process chrome -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object { [void]$_.CloseMainWindow() }");
|
|
11424
|
+
const windowsOpen = () => powershell("(Get-Process chrome -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Measure-Object).Count").trim() !== "0";
|
|
11425
|
+
for (let i = 0; i < 20 && windowsOpen(); i++) await wait(500);
|
|
11426
|
+
if (windowsOpen()) return { ok: false, message: "Chrome did not close (it may be asking you to save something). Close it yourself, then press Start." };
|
|
11427
|
+
powershell("Get-Process chrome -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue");
|
|
11428
|
+
await wait(800);
|
|
11429
|
+
} else {
|
|
11430
|
+
return { ok: false, message: "Close Chrome yourself (all its windows), then press Start." };
|
|
11431
|
+
}
|
|
11432
|
+
}
|
|
11433
|
+
try {
|
|
11434
|
+
startChrome(["--restore-last-session"]);
|
|
11435
|
+
} catch (e) {
|
|
11436
|
+
return { ok: false, message: e.message };
|
|
11437
|
+
}
|
|
11438
|
+
return { ok: true, message: "Chrome was restarted with the banner hidden. Your tabs come back." };
|
|
11439
|
+
}
|
|
11440
|
+
var DEBUG_FLAG, WIN, MAC, hasFlag, isMainProcess, LINUX_LAUNCHERS, linuxDir, wait;
|
|
11441
|
+
var init_browser = __esm({
|
|
11442
|
+
"../chrome-control/src/browser.ts"() {
|
|
11443
|
+
"use strict";
|
|
11444
|
+
DEBUG_FLAG = "--silent-debugger-extension-api";
|
|
11445
|
+
WIN = process.platform === "win32";
|
|
11446
|
+
MAC = process.platform === "darwin";
|
|
11447
|
+
hasFlag = (cmd2) => new RegExp(`(^|\\s)${DEBUG_FLAG}(=\\S*)?(\\s|$)`).test(cmd2);
|
|
11448
|
+
isMainProcess = (cmd2) => !/(^|\s)--type=/.test(cmd2);
|
|
11449
|
+
LINUX_LAUNCHERS = ["google-chrome.desktop", "microsoft-edge.desktop", "brave-browser.desktop", "chromium.desktop"];
|
|
11450
|
+
linuxDir = () => join2(homedir2(), ".local", "share", "applications");
|
|
11451
|
+
wait = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
11452
|
+
}
|
|
11453
|
+
});
|
|
11454
|
+
|
|
11305
11455
|
// ../chrome-control/src/setup.ts
|
|
11306
11456
|
var setup_exports = {};
|
|
11307
11457
|
__export(setup_exports, {
|
|
@@ -11310,17 +11460,18 @@ __export(setup_exports, {
|
|
|
11310
11460
|
EXT_DIR: () => EXT_DIR,
|
|
11311
11461
|
HOST_DIR: () => HOST_DIR,
|
|
11312
11462
|
HOST_NAME: () => HOST_NAME,
|
|
11463
|
+
hideBanner: () => hideBanner,
|
|
11313
11464
|
launchChrome: () => launchChrome,
|
|
11314
11465
|
runSetup: () => runSetup,
|
|
11315
11466
|
runUninstall: () => runUninstall
|
|
11316
11467
|
});
|
|
11317
|
-
import { chmodSync, copyFileSync, cpSync, existsSync as
|
|
11318
|
-
import { homedir as
|
|
11319
|
-
import { dirname, join as
|
|
11320
|
-
import {
|
|
11468
|
+
import { chmodSync, copyFileSync, cpSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
11469
|
+
import { homedir as homedir3 } from "node:os";
|
|
11470
|
+
import { dirname, join as join3, resolve } from "node:path";
|
|
11471
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
11321
11472
|
import { fileURLToPath } from "node:url";
|
|
11322
11473
|
function manifestDir(b) {
|
|
11323
|
-
return
|
|
11474
|
+
return MAC2 ? join3(homedir3(), "Library", "Application Support", b.mac, "NativeMessagingHosts") : join3(homedir3(), ".config", b.linux, "NativeMessagingHosts");
|
|
11324
11475
|
}
|
|
11325
11476
|
function parseArgs(args) {
|
|
11326
11477
|
const o = { extensionIds: [DEV_EXTENSION_ID], claudeDesktop: true, claudeCode: true, force: false, open: true, flag: true };
|
|
@@ -11344,20 +11495,20 @@ function parseArgs(args) {
|
|
|
11344
11495
|
return o;
|
|
11345
11496
|
}
|
|
11346
11497
|
function installHost(ids) {
|
|
11347
|
-
const hostJs =
|
|
11348
|
-
if (!
|
|
11498
|
+
const hostJs = join3(PKG_ROOT, "dist", "native-host.js");
|
|
11499
|
+
if (!existsSync3(hostJs)) throw new Error("dist/native-host.js is missing. Run npm run build first.");
|
|
11349
11500
|
mkdirSync2(HOST_DIR, { recursive: true });
|
|
11350
|
-
copyFileSync(hostJs,
|
|
11501
|
+
copyFileSync(hostJs, join3(HOST_DIR, "native-host.mjs"));
|
|
11351
11502
|
const node = process.execPath;
|
|
11352
11503
|
let launcher;
|
|
11353
|
-
if (
|
|
11354
|
-
launcher =
|
|
11355
|
-
|
|
11504
|
+
if (WIN2) {
|
|
11505
|
+
launcher = join3(HOST_DIR, "native-host.bat");
|
|
11506
|
+
writeFileSync3(launcher, `@echo off\r
|
|
11356
11507
|
"${node}" "%~dp0native-host.mjs" %*\r
|
|
11357
11508
|
`);
|
|
11358
11509
|
} else {
|
|
11359
|
-
launcher =
|
|
11360
|
-
|
|
11510
|
+
launcher = join3(HOST_DIR, "native-host.sh");
|
|
11511
|
+
writeFileSync3(launcher, `#!/bin/sh
|
|
11361
11512
|
exec "${node}" "$(dirname "$0")/native-host.mjs" "$@"
|
|
11362
11513
|
`);
|
|
11363
11514
|
chmodSync(launcher, 493);
|
|
@@ -11369,19 +11520,19 @@ exec "${node}" "$(dirname "$0")/native-host.mjs" "$@"
|
|
|
11369
11520
|
type: "stdio",
|
|
11370
11521
|
allowed_origins: [...new Set(ids)].map((id) => `chrome-extension://${id}/`)
|
|
11371
11522
|
};
|
|
11372
|
-
const manifestPath =
|
|
11373
|
-
|
|
11523
|
+
const manifestPath = join3(HOST_DIR, `${HOST_NAME}.json`);
|
|
11524
|
+
writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2));
|
|
11374
11525
|
const registered = [];
|
|
11375
11526
|
for (const b of BROWSERS) {
|
|
11376
|
-
if (
|
|
11527
|
+
if (WIN2) {
|
|
11377
11528
|
const key = `HKCU\\${b.win}\\NativeMessagingHosts\\${HOST_NAME}`;
|
|
11378
|
-
const r =
|
|
11529
|
+
const r = spawnSync2("reg", ["add", key, "/ve", "/t", "REG_SZ", "/d", manifestPath, "/f"], { encoding: "utf8" });
|
|
11379
11530
|
if (r.status === 0) registered.push(b.name);
|
|
11380
11531
|
} else {
|
|
11381
11532
|
const dir = manifestDir(b);
|
|
11382
|
-
if (!
|
|
11533
|
+
if (!existsSync3(dirname(dir))) continue;
|
|
11383
11534
|
mkdirSync2(dir, { recursive: true });
|
|
11384
|
-
copyFileSync(manifestPath,
|
|
11535
|
+
copyFileSync(manifestPath, join3(dir, `${HOST_NAME}.json`));
|
|
11385
11536
|
registered.push(b.name);
|
|
11386
11537
|
}
|
|
11387
11538
|
}
|
|
@@ -11389,17 +11540,17 @@ exec "${node}" "$(dirname "$0")/native-host.mjs" "$@"
|
|
|
11389
11540
|
}
|
|
11390
11541
|
function claudeDesktopConfigs() {
|
|
11391
11542
|
const out = [];
|
|
11392
|
-
if (
|
|
11393
|
-
if (process.env.APPDATA) out.push(
|
|
11394
|
-
const pk = process.env.LOCALAPPDATA ?
|
|
11395
|
-
if (pk &&
|
|
11543
|
+
if (WIN2) {
|
|
11544
|
+
if (process.env.APPDATA) out.push(join3(process.env.APPDATA, "Claude", "claude_desktop_config.json"));
|
|
11545
|
+
const pk = process.env.LOCALAPPDATA ? join3(process.env.LOCALAPPDATA, "Packages") : "";
|
|
11546
|
+
if (pk && existsSync3(pk)) {
|
|
11396
11547
|
for (const d of readdirSync(pk)) {
|
|
11397
|
-
if (d.startsWith("Claude_")) out.push(
|
|
11548
|
+
if (d.startsWith("Claude_")) out.push(join3(pk, d, "LocalCache", "Roaming", "Claude", "claude_desktop_config.json"));
|
|
11398
11549
|
}
|
|
11399
11550
|
}
|
|
11400
|
-
} else if (
|
|
11401
|
-
else out.push(
|
|
11402
|
-
return out.filter((f) =>
|
|
11551
|
+
} else if (MAC2) out.push(join3(homedir3(), "Library", "Application Support", "Claude", "claude_desktop_config.json"));
|
|
11552
|
+
else out.push(join3(homedir3(), ".config", "Claude", "claude_desktop_config.json"));
|
|
11553
|
+
return out.filter((f) => existsSync3(f));
|
|
11403
11554
|
}
|
|
11404
11555
|
function configureClaudeDesktop(force, log) {
|
|
11405
11556
|
const files = claudeDesktopConfigs();
|
|
@@ -11407,7 +11558,7 @@ function configureClaudeDesktop(force, log) {
|
|
|
11407
11558
|
for (const f of files) {
|
|
11408
11559
|
let cfg;
|
|
11409
11560
|
try {
|
|
11410
|
-
cfg = JSON.parse(
|
|
11561
|
+
cfg = JSON.parse(readFileSync3(f, "utf8"));
|
|
11411
11562
|
} catch {
|
|
11412
11563
|
log(` Could not read ${f} as JSON. Skipped.`);
|
|
11413
11564
|
continue;
|
|
@@ -11418,15 +11569,15 @@ function configureClaudeDesktop(force, log) {
|
|
|
11418
11569
|
continue;
|
|
11419
11570
|
}
|
|
11420
11571
|
copyFileSync(f, `${f}.bak-chrome-control`);
|
|
11421
|
-
cfg.mcpServers["chrome-control"] =
|
|
11422
|
-
|
|
11572
|
+
cfg.mcpServers["chrome-control"] = WIN2 ? { command: "cmd", args: ["/c", "npx", "-y", "chrome-control-mcp"] } : { command: "npx", args: ["-y", "chrome-control-mcp"] };
|
|
11573
|
+
writeFileSync3(f, JSON.stringify(cfg, null, 2));
|
|
11423
11574
|
log(` Added chrome-control to ${f} (backup saved next to it). Restart Claude Desktop.`);
|
|
11424
11575
|
}
|
|
11425
11576
|
}
|
|
11426
11577
|
function configureClaudeCode(log) {
|
|
11427
|
-
const v =
|
|
11578
|
+
const v = spawnSync2("claude", ["--version"], { encoding: "utf8", shell: WIN2 });
|
|
11428
11579
|
if (v.status !== 0) return log(" Claude Code CLI not found. Skipped.");
|
|
11429
|
-
const r =
|
|
11580
|
+
const r = spawnSync2("claude", ["mcp", "add", "chrome-control", "--", "npx", "-y", "chrome-control-mcp"], { encoding: "utf8", shell: WIN2 });
|
|
11430
11581
|
const msg = `${r.stderr || ""}${r.stdout || ""}`;
|
|
11431
11582
|
if (r.status === 0) log(" Added chrome-control to Claude Code.");
|
|
11432
11583
|
else if (/already exists/i.test(msg)) log(" Already set in Claude Code.");
|
|
@@ -11434,25 +11585,30 @@ function configureClaudeCode(log) {
|
|
|
11434
11585
|
Run it yourself: claude mcp add chrome-control -- npx -y chrome-control-mcp`);
|
|
11435
11586
|
}
|
|
11436
11587
|
function copyToClipboard(text2) {
|
|
11437
|
-
const r =
|
|
11588
|
+
const r = WIN2 ? spawnSync2("clip", { input: text2, shell: true }) : MAC2 ? spawnSync2("pbcopy", { input: text2 }) : spawnSync2("xclip", ["-selection", "clipboard"], { input: text2 });
|
|
11438
11589
|
return r.status === 0;
|
|
11439
11590
|
}
|
|
11440
11591
|
function openExtensionsPage() {
|
|
11441
11592
|
const url = "chrome://extensions";
|
|
11442
|
-
const r =
|
|
11593
|
+
const r = WIN2 ? spawnSync2("cmd", ["/c", "start", "", "chrome", url], { stdio: "ignore" }) : MAC2 ? spawnSync2("open", ["-a", "Google Chrome", url], { stdio: "ignore" }) : spawnSync2("google-chrome", [url], { stdio: "ignore" });
|
|
11443
11594
|
return r.status === 0;
|
|
11444
11595
|
}
|
|
11445
11596
|
function launchChrome() {
|
|
11446
|
-
|
|
11447
|
-
join2(process.env.PROGRAMFILES ?? "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
11448
|
-
join2(process.env["PROGRAMFILES(X86)"] ?? "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
11449
|
-
join2(process.env.LOCALAPPDATA ?? "", "Google", "Chrome", "Application", "chrome.exe")
|
|
11450
|
-
] : MAC ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium"];
|
|
11451
|
-
const exe = cands.find(existsSync2);
|
|
11452
|
-
if (!exe) throw new Error("Chrome was not found. Start it yourself with the flag " + DEBUG_FLAG);
|
|
11453
|
-
spawn(exe, [DEBUG_FLAG], { detached: true, stdio: "ignore" }).unref();
|
|
11597
|
+
startChrome();
|
|
11454
11598
|
console.log(`Started Chrome with ${DEBUG_FLAG}. If Chrome was already running, quit it fully first and run this again.`);
|
|
11455
11599
|
}
|
|
11600
|
+
function hideBanner(log) {
|
|
11601
|
+
const c = patchShortcuts();
|
|
11602
|
+
if (!c) {
|
|
11603
|
+
log(` macOS has no shortcut to edit. Start Chrome with: chrome-control-mcp chrome`);
|
|
11604
|
+
return c;
|
|
11605
|
+
}
|
|
11606
|
+
log(` Shortcuts patched: ${c.patched}, already set: ${c.already}, could not edit: ${c.failed}`);
|
|
11607
|
+
if (c.failed) log(" (Shortcuts in shared folders may need an administrator. Add the flag to them by hand.)");
|
|
11608
|
+
if (!c.total) log(" No Chrome shortcuts found. Start Chrome with: chrome-control-mcp chrome");
|
|
11609
|
+
if (c.patched || c.already) log(" The flag only counts when Chrome starts: if Chrome is open now, close it completely and open it from a shortcut (or run: chrome-control-mcp chrome-restart).");
|
|
11610
|
+
return c;
|
|
11611
|
+
}
|
|
11456
11612
|
function runSetup(args) {
|
|
11457
11613
|
const o = parseArgs(args);
|
|
11458
11614
|
const log = (s) => console.log(s);
|
|
@@ -11461,13 +11617,13 @@ function runSetup(args) {
|
|
|
11461
11617
|
log("1. Token ready.");
|
|
11462
11618
|
if (process.env.CHROME_BRIDGE_PORT) {
|
|
11463
11619
|
mkdirSync2(BASE, { recursive: true });
|
|
11464
|
-
|
|
11620
|
+
writeFileSync3(join3(BASE, "config.json"), JSON.stringify({ port: Number(process.env.CHROME_BRIDGE_PORT) }));
|
|
11465
11621
|
}
|
|
11466
11622
|
const h = installHost(o.extensionIds);
|
|
11467
11623
|
log(`2. Native host installed in ${HOST_DIR}`);
|
|
11468
11624
|
log(` Registered for: ${h.registered.length ? h.registered.join(", ") : "no browser found"}`);
|
|
11469
11625
|
mkdirSync2(BASE, { recursive: true });
|
|
11470
|
-
cpSync(
|
|
11626
|
+
cpSync(join3(PKG_ROOT, "extension"), EXT_DIR, { recursive: true, force: true });
|
|
11471
11627
|
log(`3. Extension copied to ${EXT_DIR}`);
|
|
11472
11628
|
if (o.claudeDesktop || o.claudeCode) log("4. Claude config:");
|
|
11473
11629
|
if (o.claudeDesktop) configureClaudeDesktop(o.force, log);
|
|
@@ -11480,6 +11636,11 @@ function runSetup(args) {
|
|
|
11480
11636
|
if (openExtensionsPage()) log(" (Opened chrome://extensions for you.)");
|
|
11481
11637
|
}
|
|
11482
11638
|
log(" The extension gets its token by itself and turns ON. You do not paste anything.");
|
|
11639
|
+
if (o.flag) {
|
|
11640
|
+
log(`
|
|
11641
|
+
5. Hiding Chrome's "started debugging this browser" banner:`);
|
|
11642
|
+
hideBanner(log);
|
|
11643
|
+
}
|
|
11483
11644
|
if (!o.claudeDesktop && !o.claudeCode) {
|
|
11484
11645
|
log("\nClaude was skipped (--no-claude). Run setup again without that flag to add the MCP to Claude.");
|
|
11485
11646
|
}
|
|
@@ -11487,34 +11648,34 @@ function runSetup(args) {
|
|
|
11487
11648
|
function runUninstall() {
|
|
11488
11649
|
const log = (s) => console.log(s);
|
|
11489
11650
|
for (const b of BROWSERS) {
|
|
11490
|
-
if (
|
|
11491
|
-
else
|
|
11651
|
+
if (WIN2) spawnSync2("reg", ["delete", `HKCU\\${b.win}\\NativeMessagingHosts\\${HOST_NAME}`, "/f"], { encoding: "utf8" });
|
|
11652
|
+
else rmSync2(join3(manifestDir(b), `${HOST_NAME}.json`), { force: true });
|
|
11492
11653
|
}
|
|
11493
|
-
|
|
11494
|
-
|
|
11654
|
+
rmSync2(HOST_DIR, { recursive: true, force: true });
|
|
11655
|
+
rmSync2(EXT_DIR, { recursive: true, force: true });
|
|
11495
11656
|
log("Removed the native host registration, the host files and the extension copy.");
|
|
11496
11657
|
log("Your token file was kept. Remove the extension in chrome://extensions yourself.");
|
|
11497
11658
|
}
|
|
11498
|
-
var HOST_NAME, DEV_EXTENSION_ID, PKG_ROOT, BASE, HOST_DIR, EXT_DIR,
|
|
11659
|
+
var HOST_NAME, DEV_EXTENSION_ID, PKG_ROOT, BASE, HOST_DIR, EXT_DIR, WIN2, MAC2, BROWSERS;
|
|
11499
11660
|
var init_setup = __esm({
|
|
11500
11661
|
"../chrome-control/src/setup.ts"() {
|
|
11501
11662
|
"use strict";
|
|
11502
11663
|
init_bridge();
|
|
11664
|
+
init_browser();
|
|
11503
11665
|
HOST_NAME = "com.chromecontrol.mcp";
|
|
11504
11666
|
DEV_EXTENSION_ID = "ommdbcleeojmjlkimeficggcjgbapmhk";
|
|
11505
11667
|
PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11506
|
-
BASE =
|
|
11507
|
-
HOST_DIR =
|
|
11508
|
-
EXT_DIR =
|
|
11509
|
-
|
|
11510
|
-
|
|
11668
|
+
BASE = join3(homedir3(), ".chrome-control-mcp");
|
|
11669
|
+
HOST_DIR = join3(BASE, "native-host");
|
|
11670
|
+
EXT_DIR = join3(BASE, "extension");
|
|
11671
|
+
WIN2 = process.platform === "win32";
|
|
11672
|
+
MAC2 = process.platform === "darwin";
|
|
11511
11673
|
BROWSERS = [
|
|
11512
11674
|
{ name: "Chrome", win: "Software\\Google\\Chrome", mac: "Google/Chrome", linux: "google-chrome" },
|
|
11513
11675
|
{ name: "Edge", win: "Software\\Microsoft\\Edge", mac: "Microsoft Edge", linux: "microsoft-edge" },
|
|
11514
11676
|
{ name: "Brave", win: "Software\\BraveSoftware\\Brave-Browser", mac: "BraveSoftware/Brave-Browser", linux: "BraveSoftware/Brave-Browser" },
|
|
11515
11677
|
{ name: "Chromium", win: "Software\\Chromium", mac: "Chromium", linux: "chromium" }
|
|
11516
11678
|
];
|
|
11517
|
-
DEBUG_FLAG = "--silent-debugger-extension-api";
|
|
11518
11679
|
}
|
|
11519
11680
|
});
|
|
11520
11681
|
|
|
@@ -11523,14 +11684,14 @@ var daemon_exports = {};
|
|
|
11523
11684
|
__export(daemon_exports, {
|
|
11524
11685
|
runDaemon: () => runDaemon
|
|
11525
11686
|
});
|
|
11526
|
-
import { spawn as
|
|
11527
|
-
import { closeSync, existsSync as
|
|
11528
|
-
import { homedir as
|
|
11529
|
-
import { join as
|
|
11687
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
11688
|
+
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, readSync, renameSync, statSync, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
11689
|
+
import { homedir as homedir4 } from "node:os";
|
|
11690
|
+
import { join as join4 } from "node:path";
|
|
11530
11691
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
11531
11692
|
function readPid() {
|
|
11532
11693
|
try {
|
|
11533
|
-
const n = Number(
|
|
11694
|
+
const n = Number(readFileSync4(PID_FILE, "utf8").trim());
|
|
11534
11695
|
return n > 0 ? n : null;
|
|
11535
11696
|
} catch {
|
|
11536
11697
|
return null;
|
|
@@ -11560,7 +11721,7 @@ async function health() {
|
|
|
11560
11721
|
}
|
|
11561
11722
|
function tail(n) {
|
|
11562
11723
|
try {
|
|
11563
|
-
return
|
|
11724
|
+
return readFileSync4(LOG_FILE, "utf8").split(/\r?\n/).filter(Boolean).slice(-n).join("\n");
|
|
11564
11725
|
} catch {
|
|
11565
11726
|
return "";
|
|
11566
11727
|
}
|
|
@@ -11579,14 +11740,14 @@ async function start() {
|
|
|
11579
11740
|
}
|
|
11580
11741
|
const fd = openSync(LOG_FILE, "a", 384);
|
|
11581
11742
|
const indexJs = fileURLToPath2(new URL("./index.js", import.meta.url));
|
|
11582
|
-
const child =
|
|
11743
|
+
const child = spawn3(process.execPath, [indexJs, "serve"], { detached: true, stdio: ["ignore", fd, fd], windowsHide: true, env: { ...process.env, CHROME_CONTROL_DAEMON: "1" } });
|
|
11583
11744
|
closeSync(fd);
|
|
11584
11745
|
child.unref();
|
|
11585
11746
|
if (!child.pid) {
|
|
11586
11747
|
console.error("Could not start the server.");
|
|
11587
11748
|
return 1;
|
|
11588
11749
|
}
|
|
11589
|
-
|
|
11750
|
+
writeFileSync4(PID_FILE, String(child.pid));
|
|
11590
11751
|
for (let i = 0; i < 40; i++) {
|
|
11591
11752
|
await sleep2(250);
|
|
11592
11753
|
if (await health()) {
|
|
@@ -11654,7 +11815,7 @@ async function status() {
|
|
|
11654
11815
|
async function logs(args) {
|
|
11655
11816
|
const ni = args.findIndex((a) => a === "-n" || a === "--lines");
|
|
11656
11817
|
const n = ni >= 0 ? Number(args[ni + 1]) || 40 : 40;
|
|
11657
|
-
if (!
|
|
11818
|
+
if (!existsSync4(LOG_FILE)) {
|
|
11658
11819
|
console.log("No log yet. Start the server first.");
|
|
11659
11820
|
return 0;
|
|
11660
11821
|
}
|
|
@@ -11705,9 +11866,9 @@ var DIR, PID_FILE, LOG_FILE, HTTP_PORT, sleep2;
|
|
|
11705
11866
|
var init_daemon = __esm({
|
|
11706
11867
|
"../chrome-control/src/daemon.ts"() {
|
|
11707
11868
|
"use strict";
|
|
11708
|
-
DIR =
|
|
11709
|
-
PID_FILE =
|
|
11710
|
-
LOG_FILE =
|
|
11869
|
+
DIR = join4(homedir4(), ".chrome-control-mcp");
|
|
11870
|
+
PID_FILE = join4(DIR, "server.pid");
|
|
11871
|
+
LOG_FILE = join4(DIR, "server.log");
|
|
11711
11872
|
HTTP_PORT = Number(process.env.CHROME_BRIDGE_HTTP_PORT ?? 8766);
|
|
11712
11873
|
sleep2 = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
11713
11874
|
}
|
|
@@ -28136,9 +28297,9 @@ var StreamableHTTPServerTransport = class {
|
|
|
28136
28297
|
import { createServer as createServer2 } from "node:http";
|
|
28137
28298
|
import { randomUUID } from "node:crypto";
|
|
28138
28299
|
init_bridge();
|
|
28139
|
-
import { createWriteStream, existsSync as
|
|
28140
|
-
import { homedir as
|
|
28141
|
-
import { dirname as dirname2, join as
|
|
28300
|
+
import { createWriteStream, existsSync as existsSync5, readFileSync as readFileSync5, mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
28301
|
+
import { homedir as homedir5 } from "node:os";
|
|
28302
|
+
import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
|
|
28142
28303
|
import { gzipSync } from "node:zlib";
|
|
28143
28304
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
28144
28305
|
|
|
@@ -28738,9 +28899,9 @@ async function uploadFiles(tabId, uid, files) {
|
|
|
28738
28899
|
}
|
|
28739
28900
|
await cdp(tabId, "Page.setInterceptFileChooserDialog", { enabled: true });
|
|
28740
28901
|
try {
|
|
28741
|
-
const
|
|
28902
|
+
const wait2 = waitForEvent(tabId, "Page.fileChooserOpened", 5e3);
|
|
28742
28903
|
await clickUid(tabId, uid);
|
|
28743
|
-
const ev = await
|
|
28904
|
+
const ev = await wait2;
|
|
28744
28905
|
if (!ev) throw new Error("No file chooser opened after clicking the element");
|
|
28745
28906
|
await cdp(tabId, "DOM.setFileInputFiles", { files, backendNodeId: ev.backendNodeId });
|
|
28746
28907
|
} finally {
|
|
@@ -28829,6 +28990,9 @@ Usage: chrome-control-mcp <command>
|
|
|
28829
28990
|
token Print the secret token
|
|
28830
28991
|
extension Print the folder to load in chrome://extensions
|
|
28831
28992
|
chrome Start Chrome with the debugger banner hidden (--silent-debugger-extension-api)
|
|
28993
|
+
flag Add that flag to your Chrome, Edge and Brave shortcuts (prints one line of JSON)
|
|
28994
|
+
flag-status Say whether the running Chrome has the flag, and how many shortcuts do (JSON)
|
|
28995
|
+
chrome-restart Close Chrome politely and start it again with the flag and your tabs (JSON)
|
|
28832
28996
|
uninstall Remove the native host and the extension copy
|
|
28833
28997
|
version Print the version
|
|
28834
28998
|
help Show this help
|
|
@@ -28839,7 +29003,7 @@ if (cmd === "help" || cmd === "--help" || cmd === "-h" || cmd === void 0 && proc
|
|
|
28839
29003
|
process.exit(0);
|
|
28840
29004
|
}
|
|
28841
29005
|
if (cmd === "version" || cmd === "--version" || cmd === "-v") {
|
|
28842
|
-
console.log(JSON.parse(
|
|
29006
|
+
console.log(JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
28843
29007
|
process.exit(0);
|
|
28844
29008
|
}
|
|
28845
29009
|
if (cmd === "token") {
|
|
@@ -28847,14 +29011,29 @@ if (cmd === "token") {
|
|
|
28847
29011
|
process.exit(0);
|
|
28848
29012
|
}
|
|
28849
29013
|
if (cmd === "extension") {
|
|
28850
|
-
const stable =
|
|
28851
|
-
console.log(
|
|
29014
|
+
const stable = join5(homedir5(), ".chrome-control-mcp", "extension");
|
|
29015
|
+
console.log(existsSync5(stable) ? stable : fileURLToPath3(new URL("../extension", import.meta.url)));
|
|
28852
29016
|
process.exit(0);
|
|
28853
29017
|
}
|
|
28854
29018
|
if (cmd === "url") {
|
|
28855
29019
|
console.log(`http://127.0.0.1:${process.env.CHROME_BRIDGE_HTTP_PORT ?? 8766}/mcp?token=${loadToken()}`);
|
|
28856
29020
|
process.exit(0);
|
|
28857
29021
|
}
|
|
29022
|
+
if (cmd === "flag" || cmd === "flag-status" || cmd === "chrome-restart") {
|
|
29023
|
+
try {
|
|
29024
|
+
const b = await Promise.resolve().then(() => (init_browser(), browser_exports));
|
|
29025
|
+
if (cmd === "flag-status") console.log(JSON.stringify(b.flagStatus()));
|
|
29026
|
+
else if (cmd === "flag") {
|
|
29027
|
+
const c = (await Promise.resolve().then(() => (init_setup(), setup_exports))).hideBanner(() => {
|
|
29028
|
+
});
|
|
29029
|
+
console.log(JSON.stringify({ ok: c !== null && (c.patched > 0 || c.already > 0), counts: c }));
|
|
29030
|
+
} else console.log(JSON.stringify(await b.restartChrome()));
|
|
29031
|
+
process.exit(0);
|
|
29032
|
+
} catch (e) {
|
|
29033
|
+
console.log(JSON.stringify({ ok: false, message: e.message }));
|
|
29034
|
+
process.exit(1);
|
|
29035
|
+
}
|
|
29036
|
+
}
|
|
28858
29037
|
if (cmd === "chrome") {
|
|
28859
29038
|
try {
|
|
28860
29039
|
(await Promise.resolve().then(() => (init_setup(), setup_exports))).launchChrome();
|
|
@@ -28900,7 +29079,7 @@ var abs = (p) => resolve2(process.cwd(), p);
|
|
|
28900
29079
|
function saveFile(p, data) {
|
|
28901
29080
|
const f = abs(p);
|
|
28902
29081
|
mkdirSync4(dirname2(f), { recursive: true });
|
|
28903
|
-
|
|
29082
|
+
writeFileSync5(f, data);
|
|
28904
29083
|
return f;
|
|
28905
29084
|
}
|
|
28906
29085
|
var selected = null;
|