friend-assistant 0.1.36 → 0.1.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 CHANGED
@@ -154,6 +154,7 @@ Example: `friend config set voice.speakTyped false` keeps typed messages silent.
154
154
  | `FRIEND_ANTHROPIC_KEY` | An API key to use when none is saved. |
155
155
  | `FRIEND_PYTHON` | The Python program to use for voice, if the usual ones are not the right version. |
156
156
  | `FRIEND_WEB_DIR`, `FRIEND_PLUGIN_CATALOG`, `FRIEND_VOICE_DIR`, `FRIEND_CHROME_CONTROL` | Where the web app, the bundled plugins, the Python voice code and the Chrome control program (its `index.js`) are, for unusual installs. |
157
+ | `CHROME_BRIDGE_PORT`, `CHROME_BRIDGE_HTTP_PORT` | Where Chrome control itself listens on this PC (its extension port, default 8765, and its MCP port, default 8766). Friend reaches both for you: other PCs only need Friend's own address. |
157
158
  | `FRIEND_DEMO_PORT` | The port `pnpm demo` listens on (default 8788). |
158
159
 
159
160
  ### From the source code
@@ -225,7 +226,7 @@ pnpm friend claude status
225
226
 
226
227
  A connector gives Friend the tools of an outside program or service through the MCP standard. Open **Esc, then Connectors** (or type `/connectors`). Ready-made entries:
227
228
 
228
- - **Chrome control** is a plugin that comes with Friend, so there is nothing else to install (no npm package, no `claude mcp add`). Open **Plugins**, install **Chrome control** (PIN), and Friend sets it up by itself from the copy inside it: the small helper Chrome needs and the extension folder. The one step only you can do is loading that folder in Chrome (chrome://extensions, Developer mode, Load unpacked; Configure shows the folder with a Copy button) and switching the extension on from its icon. Then switch the plugin on (PIN); Friend connects, and the plugin page shows whether the extension is linked. Every browser action still asks for your approval. Removing the plugin only removes Friend's connection; the helper and the extension stay, because other programs on your PC may use them. See [docs/guides/chrome-control.md](docs/guides/chrome-control.md).
229
+ - **Chrome control** is a plugin that comes with Friend, so there is nothing else to install (no npm package, no `claude mcp add`). Open **Plugins**, install **Chrome control** (PIN), and Friend sets it up by itself from the copy inside it: the small helper Chrome needs and the extension folder. The one step only you can do is loading that folder in Chrome (chrome://extensions, Developer mode, Load unpacked; Configure shows the folder with a Copy button) and switching the extension on from its icon. Then switch the plugin on (PIN); Friend connects, and the plugin page shows whether the extension is linked. Every browser action still asks for your approval. Removing the plugin only removes Friend's connection; the helper and the extension stay, because other programs on your PC may use them. See [docs/guides/chrome-control.md](docs/guides/chrome-control.md). **From another PC** it needs no extra port: switch on *Allow other PCs* in Configure and Friend serves Chrome control on its own address (`/chrome/mcp` for apps, `/chrome/ws` for the extension), with a download link for the extension and the key shown only after your PIN.
229
230
  - **Gmail**: a community Gmail MCP server (`@gongrzhe/server-gmail-autoauth-mcp`). You create the Google OAuth credentials and sign in on Google's own page; **Friend never sees your Google password**. More mail and calendar connectors can be added the same way ("Something else": any program or any server by address).
230
231
 
231
232
  How it is kept safe:
@@ -356,6 +357,7 @@ Friend's screen has a media player and four widgets drawn in a HUD style (angled
356
357
  - **Files and pictures to your Telegram or WhatsApp.** Ask Friend to send you a photo, a clip, a song or a document that is on the PC (`message.send_file`). You approve every file after seeing its name, every time. Telegram takes up to 50 MB; WhatsApp up to 16 MB and needs Friend's public address (Twilio fetches the file from a link that works for ten minutes). Only to you, never as SMS; programs (.exe, scripts) and Friend's own data folder are never sent.
357
358
  - **Voice messages in Telegram and WhatsApp.** Send Friend a voice message from your own linked Telegram chat or your own WhatsApp number and it is understood and answered like a typed one (the reply starts with what it heard). It uses the same voice engine as the PC microphone, so that must be running; if not, you are told. Up to 5 minutes or 8 MB; from anyone else voice messages are ignored.
358
359
  - **Audius (plugin).** Free music from Audius, an open music service: no account, no key, no payment. Install it from Plugins and switch it on; the assistant and the player can then search and play it, on the screen or from the PC. See `docs/guides/audius-setup.md`.
360
+ - **Web video (plugin).** Ask Friend to play a video or a song from YouTube or almost any video site: "play lofi hip hop on YouTube", or give it a link. It plays inside Friend, in a card on your Friend screen (a browser, also a phone) or in a window on the PC, as you choose in the plugin's Configure page; Friend can pause, jump to a time, change the volume and stop it, and it never opens YouTube in your Chrome. **Nothing to install**: when you turn the plugin on, Friend downloads the two free programs it needs (**yt-dlp** and **ffmpeg**) by itself from their projects' own release pages, checks them against the published checksums, keeps them in its own folder and keeps yt-dlp up to date. (**mpv** is optional, for a window of its own on the PC; without it a browser window is used.) Safety: only public http and https links, never an address on your own network, a limit on length and an optional list of allowed sites; nothing is saved to your disk. See [docs/guides/web-video.md](docs/guides/web-video.md).
359
361
  - **Play from the PC's speakers.** Settings → Widgets → *Where the sound comes out*: the screen's browser (default) or the PC that runs Friend, with no browser open. The folder's music is played by mpv (free; "Check mpv" finds it or shows the install command for your system) and Spotify on a Spotify device you name. The screen then shows what the PC plays and controls it. See [docs/guides/host-output.md](docs/guides/host-output.md).
360
362
  - **Control by the assistant.** Say or type "play something by ...", "next", "turn it down", "shuffle", "what's playing". The assistant can play, pause, skip, seek, change volume, shuffle, repeat and edit the queue, and search the library; it cannot change the folder or any widget setting. Over Alexa the music tools are off unless you add them to the voice tools yourself.
361
363
  - **Quick launch.** Up to 12 buttons, each opening an address (http or https only), asking the assistant something, opening a panel (chat, tasks, plugins, settings, music) or controlling the music.
package/dist/BUILD.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "friend-assistant",
3
- "version": "0.1.36",
4
- "builtAt": "2026-10-11T16:24:06.938Z"
3
+ "version": "0.1.38",
4
+ "builtAt": "2026-10-12T14:21:14.686Z"
5
5
  }
@@ -32,19 +32,33 @@ function parseTarget(c) {
32
32
  scheme = m[1].toLowerCase();
33
33
  raw = raw.slice(m[0].length);
34
34
  }
35
- raw = raw.replace(/\/+$/, "");
35
+ // An address with a path (Friend's own, wss://name/chrome/ws) is used as it is: its port is the one in the address, not the port box.
36
+ let path = "";
37
+ const slash = raw.indexOf("/");
38
+ if (slash >= 0) {
39
+ path = raw.slice(slash).replace(/\/{2,}/g, "/").replace(/\/+$/, "");
40
+ raw = raw.slice(0, slash);
41
+ if (path && !/^\/[A-Za-z0-9._~/-]*$/.test(path)) return null;
42
+ }
36
43
  if (!raw) raw = "127.0.0.1";
44
+ let own = null;
45
+ const hp = /^(\[[0-9a-f:.]+\]|[^:[\]]+):(\d{1,5})$/i.exec(raw);
46
+ if (hp) {
47
+ raw = hp[1];
48
+ own = Number(hp[2]);
49
+ }
37
50
  if (/^[0-9a-f:]*:[0-9a-f:]*$/i.test(raw)) raw = `[${raw}]`; // bare IPv6
38
51
  const valid = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i.test(raw) || /^\[[0-9a-f:.]+\]$/i.test(raw);
39
52
  if (!valid) return null;
40
- const port = Number(c.port) || 8765;
41
- const url = `${scheme}://${raw}:${port}`;
53
+ const port = own ?? (path ? null : Number(c.port) || 8765);
54
+ const where = `${raw}${port ? ":" + port : ""}${path}`;
55
+ const base = `${scheme}://${where}${path ? "" : "/"}`;
42
56
  try {
43
- new URL(url);
57
+ new URL(base);
44
58
  } catch {
45
59
  return null;
46
60
  }
47
- return { url, label: `${raw}:${port}`, local: LOCAL_HOSTS.has(raw.toLowerCase()) };
61
+ return { base, label: where, local: LOCAL_HOSTS.has(raw.toLowerCase()) && !path };
48
62
  }
49
63
 
50
64
  let ws = null;
@@ -290,7 +304,7 @@ async function doConnect() {
290
304
  if (!c.token) return setStatus("no token");
291
305
  if (nameTaken) return setStatus("name in use");
292
306
  setStatus("connecting");
293
- const sock = new WebSocket(`${target.url}/?token=${encodeURIComponent(c.token)}&name=${encodeURIComponent(c.name)}`);
307
+ const sock = new WebSocket(`${target.base}?token=${encodeURIComponent(c.token)}&name=${encodeURIComponent(c.name)}`);
294
308
  ws = sock;
295
309
  sock.onopen = () => {
296
310
  stats = { attached: attached.size, commands: 0, last: "", lastAt: 0 };
@@ -84,7 +84,7 @@
84
84
  <input id="name" type="text" maxlength="40" autocomplete="off" spellcheck="false" placeholder="tells your Chromes apart, for example office-pc">
85
85
  </label>
86
86
  <label>SERVER HOST
87
- <input id="host" type="text" autocomplete="off" spellcheck="false" placeholder="empty = this PC (127.0.0.1). or a domain / IP of another PC">
87
+ <input id="host" type="text" autocomplete="off" spellcheck="false" placeholder="empty = this PC. or an IP / domain, or wss://friend.example.com/chrome/ws">
88
88
  </label>
89
89
  <label>PORT
90
90
  <input id="port" type="number" min="1" max="65535" placeholder="8765">
@@ -31,8 +31,8 @@ function look(s) {
31
31
  return { cls: "state-nokey", word: "NO KEY", sub: isRemote(s.host) ? "PASTE THE SERVER'S ACCESS KEY" : "RUN: NPX CHROME-CONTROL-MCP SETUP" };
32
32
  }
33
33
  if (s.status === "name in use") return { cls: "state-nokey", word: "NAME TAKEN", sub: "ANOTHER CHROME USES THIS NAME. RENAME THIS ONE" };
34
- if (s.status === "bad host") return { cls: "state-nokey", word: "BAD HOST", sub: "USE A DOMAIN OR IP, NO PATH" };
35
- const where = isRemote(s.host) ? `${hostName(s.host).toUpperCase()}:${port}` : `PORT ${port}`;
34
+ if (s.status === "bad host") return { cls: "state-nokey", word: "BAD HOST", sub: "USE A DOMAIN, AN IP, OR WSS://NAME/CHROME/WS" };
35
+ const where = isRemote(s.host) ? (hostName(s.host).includes("/") ? hostName(s.host).toUpperCase() : `${hostName(s.host).toUpperCase()}:${port}`) : `PORT ${port}`;
36
36
  return { cls: "state-nosig", word: "NO SIGNAL", sub: `MCP SERVER NOT FOUND ON ${where}` };
37
37
  }
38
38
 
@@ -47,7 +47,10 @@ function render() {
47
47
  $("engage").setAttribute("aria-checked", String(on));
48
48
  $("engageLbl").textContent = on ? "DISENGAGE LINK" : "ENGAGE LINK";
49
49
 
50
- $("tPort").textContent = state.port || 8765;
50
+ // an address with a path (wss://name/chrome/ws) carries its own port, so the port box does not apply to it
51
+ const hasPath = hostName(state.host).includes("/");
52
+ $("tPort").textContent = hasPath ? "IN ADDRESS" : state.port || 8765;
53
+ $("port").disabled = hasPath;
51
54
  const key = $("tKey");
52
55
  key.className = "";
53
56
  if (!state.token) {