friend-assistant 0.1.44 → 0.1.45

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/dist/friend.mjs CHANGED
@@ -3347,6 +3347,7 @@ function sanitizeLook(raw) {
3347
3347
  };
3348
3348
  }
3349
3349
  var DEFAULT_OLLAMA = { url: "http://127.0.0.1:11434", model: "qwen2.5:7b", contextTokens: 8192, vision: false };
3350
+ var sanitizeUpdates = (u) => ({ auto: typeof u?.auto === "boolean" ? u.auto : true });
3350
3351
  var DEFAULT_VOICE = {
3351
3352
  enabled: true,
3352
3353
  sttModel: "small",
@@ -3412,6 +3413,7 @@ var defaults = (dataDir) => ({
3412
3413
  openai: { ...DEFAULT_OPENAI },
3413
3414
  claudeCode: { ...DEFAULT_CLAUDE_CODE },
3414
3415
  voice: { ...DEFAULT_VOICE },
3416
+ updates: sanitizeUpdates(void 0),
3415
3417
  view: "companion",
3416
3418
  look: { ...DEFAULT_LOOK },
3417
3419
  ownerName: "",
@@ -3458,6 +3460,7 @@ function loadConfig(dataDir = defaultDataDir()) {
3458
3460
  openai: { ...DEFAULT_OPENAI, ...stored.openai },
3459
3461
  claudeCode: sanitizeClaudeCode(stored.claudeCode),
3460
3462
  voice: sanitizeVoice({ ...DEFAULT_VOICE, ...stored.voice }),
3463
+ updates: sanitizeUpdates(stored.updates),
3461
3464
  view: stored.view === "minimal" || stored.view === "workspace" ? stored.view : "companion",
3462
3465
  look: sanitizeLook(stored.look),
3463
3466
  ownerName: typeof stored.ownerName === "string" ? stored.ownerName.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "",
@@ -4884,9 +4887,9 @@ function chromeToken(home = homedir3()) {
4884
4887
  return null;
4885
4888
  }
4886
4889
  }
4887
- function sameKey(given, real2) {
4888
- if (!given || !real2) return false;
4889
- return timingSafeEqual(createHash3("sha256").update(given).digest(), createHash3("sha256").update(real2).digest());
4890
+ function sameKey(given, real3) {
4891
+ if (!given || !real3) return false;
4892
+ return timingSafeEqual(createHash3("sha256").update(given).digest(), createHash3("sha256").update(real3).digest());
4890
4893
  }
4891
4894
  function keyFromRequest(headers, search) {
4892
4895
  const bearer = /^Bearer (.+)$/i.exec(headers.authorization ?? "")?.[1];
@@ -7122,16 +7125,16 @@ function parseCsv(text3) {
7122
7125
  const rows3 = [];
7123
7126
  let row = [];
7124
7127
  let cell = "";
7125
- let quoted = false;
7128
+ let quoted2 = false;
7126
7129
  for (let i = 0; i < text3.length; i++) {
7127
7130
  const ch = text3[i];
7128
- if (quoted) {
7131
+ if (quoted2) {
7129
7132
  if (ch === '"' && text3[i + 1] === '"') {
7130
7133
  cell += '"';
7131
7134
  i++;
7132
- } else if (ch === '"') quoted = false;
7135
+ } else if (ch === '"') quoted2 = false;
7133
7136
  else cell += ch;
7134
- } else if (ch === '"') quoted = true;
7137
+ } else if (ch === '"') quoted2 = true;
7135
7138
  else if (ch === ",") {
7136
7139
  row.push(cell);
7137
7140
  cell = "";
@@ -7184,59 +7187,59 @@ var MediaStore = class {
7184
7187
  /** A file the assistant wants to send to the owner's chat: checked here (it must exist, be a real file, not be Friend's own data, not a program). */
7185
7188
  async sendable(rawPath) {
7186
7189
  if (!isAbsolute(rawPath)) return { ok: false, error: "Give the full path of the file, for example D:\\Pictures\\photo.png." };
7187
- let real2;
7190
+ let real3;
7188
7191
  try {
7189
- real2 = await fs.realpath(resolve4(rawPath));
7192
+ real3 = await fs.realpath(resolve4(rawPath));
7190
7193
  } catch {
7191
7194
  return { ok: false, error: "That file does not exist." };
7192
7195
  }
7193
- if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real2)) return { ok: false, error: "Friend's own data folder cannot be sent." };
7194
- const stat2 = await fs.stat(real2).catch(() => null);
7196
+ if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real3)) return { ok: false, error: "Friend's own data folder cannot be sent." };
7197
+ const stat2 = await fs.stat(real3).catch(() => null);
7195
7198
  if (!stat2 || !stat2.isFile()) return { ok: false, error: "That path is not a file." };
7196
- const name = basename(real2);
7197
- const ext = extname(real2).slice(1).toLowerCase();
7199
+ const name = basename(real3);
7200
+ const ext = extname(real3).slice(1).toLowerCase();
7198
7201
  if (NOT_SENT.has(ext)) return { ok: false, error: `Friend cannot send .${ext} files.` };
7199
7202
  if (stat2.size === 0) return { ok: false, error: "That file is empty." };
7200
7203
  if (stat2.size > MAX_SEND) return { ok: false, error: `That file is too big to send (${Math.round(stat2.size / 1048576)} MB).` };
7201
7204
  const mime = IMAGE[ext] ?? VIDEO[ext] ?? DOC_MIME[ext] ?? "application/octet-stream";
7202
7205
  const kind = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document";
7203
- return { ok: true, path: real2, name, mime, size: stat2.size, kind };
7206
+ return { ok: true, path: real3, name, mime, size: stat2.size, kind };
7204
7207
  }
7205
7208
  async present(rawPath, title2) {
7206
7209
  if (!isAbsolute(rawPath)) return { ok: false, error: "Give the full path of the file, for example D:\\Pictures\\photo.png." };
7207
- let real2;
7210
+ let real3;
7208
7211
  try {
7209
- real2 = await fs.realpath(resolve4(rawPath));
7212
+ real3 = await fs.realpath(resolve4(rawPath));
7210
7213
  } catch {
7211
7214
  return { ok: false, error: "That file does not exist." };
7212
7215
  }
7213
- if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real2)) return { ok: false, error: "Friend's own data folder cannot be shown." };
7214
- const stat2 = await fs.stat(real2).catch(() => null);
7216
+ if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real3)) return { ok: false, error: "Friend's own data folder cannot be shown." };
7217
+ const stat2 = await fs.stat(real3).catch(() => null);
7215
7218
  if (!stat2 || !stat2.isFile()) return { ok: false, error: "That path is not a file." };
7216
- const name = basename(real2);
7217
- const ext = extname(real2).slice(1).toLowerCase();
7219
+ const name = basename(real3);
7220
+ const ext = extname(real3).slice(1).toLowerCase();
7218
7221
  const label3 = title2?.trim() || name;
7219
7222
  const tooBig = (limit, what) => stat2.size > limit ? { ok: false, error: `That ${what} is too large to show (${Math.round(stat2.size / 1048576)} MB).` } : null;
7220
7223
  if (IMAGE[ext]) {
7221
7224
  const big = tooBig(MAX_IMAGE, "image");
7222
7225
  if (big) return big;
7223
- const id2 = this.grant(real2, IMAGE[ext], stat2.size);
7226
+ const id2 = this.grant(real3, IMAGE[ext], stat2.size);
7224
7227
  return { ok: true, type: "image", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the image ${name}.` };
7225
7228
  }
7226
7229
  if (VIDEO[ext]) {
7227
7230
  const big = tooBig(MAX_VIDEO, "file");
7228
7231
  if (big) return big;
7229
- const id2 = this.grant(real2, VIDEO[ext], stat2.size);
7232
+ const id2 = this.grant(real3, VIDEO[ext], stat2.size);
7230
7233
  return { ok: true, type: "video", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing ${name}.` };
7231
7234
  }
7232
7235
  if (ext === "pdf") {
7233
7236
  const big = tooBig(MAX_PDF, "PDF");
7234
7237
  if (big) return big;
7235
- const id2 = this.grant(real2, "application/pdf", stat2.size);
7238
+ const id2 = this.grant(real3, "application/pdf", stat2.size);
7236
7239
  return { ok: true, type: "pdf", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the PDF ${name}.` };
7237
7240
  }
7238
7241
  if (MARKDOWN.has(ext) || ext === "csv" || CODE_LANG[ext]) {
7239
- const handle = await fs.open(real2, "r");
7242
+ const handle = await fs.open(real3, "r");
7240
7243
  let text3;
7241
7244
  try {
7242
7245
  const buf = Buffer.alloc(Math.min(stat2.size, MAX_TEXT));
@@ -9400,8 +9403,8 @@ var MusicLibrary = class {
9400
9403
  try {
9401
9404
  const info = await lstat(path3);
9402
9405
  if (!info.isFile() || info.isSymbolicLink()) return null;
9403
- const real2 = await realpath(path3);
9404
- return real2.startsWith(this.root + sep2) ? path3 : null;
9406
+ const real3 = await realpath(path3);
9407
+ return real3.startsWith(this.root + sep2) ? path3 : null;
9405
9408
  } catch {
9406
9409
  return null;
9407
9410
  }
@@ -10608,7 +10611,7 @@ var Memory = class {
10608
10611
  };
10609
10612
 
10610
10613
  // ../server/src/app.ts
10611
- import { dirname as dirname5, extname as extname3, resolve as resolve5 } from "node:path";
10614
+ import { dirname as dirname6, extname as extname3, resolve as resolve5 } from "node:path";
10612
10615
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10613
10616
  import { join as join18 } from "node:path";
10614
10617
 
@@ -11582,6 +11585,45 @@ function createGateway(deps) {
11582
11585
  if (!deps.video) return reply.code(404).send({ error: "Not found" });
11583
11586
  return deps.video.prepare();
11584
11587
  });
11588
+ const systemWindow = new SlidingWindow(30, 6e4);
11589
+ const systemGuard = async (req, reply) => {
11590
+ const token = req.cookies[COOKIE_NAME];
11591
+ const session = token ? auth.resolveSession(token) : null;
11592
+ if (!session) return reply.code(401).send({ error: "Unauthorized" });
11593
+ if (!systemWindow.allow(String(session.id))) return reply.code(429).header("retry-after", 10).send({ error: "Too many requests" });
11594
+ if (!deps.system) return reply.code(503).send({ error: "Not available" });
11595
+ };
11596
+ const systemError = (reply, e, what) => {
11597
+ if (e instanceof HttpError) return reply.code(e.status).send({ error: e.message });
11598
+ audit.log("owner", "system.failed", { what, message: e.message });
11599
+ return reply.code(500).send({ error: "That did not work. Try again." });
11600
+ };
11601
+ app.get("/api/system", { onRequest: systemGuard }, async () => deps.system.info());
11602
+ app.post("/api/system/update/check", { onRequest: systemGuard }, async (_req, reply) => {
11603
+ try {
11604
+ return await deps.system.check();
11605
+ } catch (e) {
11606
+ return systemError(reply, e, "update.check");
11607
+ }
11608
+ });
11609
+ app.post("/api/system/update/apply", { onRequest: systemGuard }, async (req, reply) => {
11610
+ const body = z10.object({ pin: z10.string().min(1).max(32) }).safeParse(req.body);
11611
+ if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
11612
+ try {
11613
+ return await deps.system.update(body.data.pin, req.ip);
11614
+ } catch (e) {
11615
+ return systemError(reply, e, "update.apply");
11616
+ }
11617
+ });
11618
+ app.post("/api/system/autostart", { onRequest: systemGuard }, async (req, reply) => {
11619
+ const body = z10.object({ enabled: z10.boolean(), pin: z10.string().min(1).max(32) }).safeParse(req.body);
11620
+ if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
11621
+ try {
11622
+ return await deps.system.setAutostart(body.data.enabled, body.data.pin, req.ip);
11623
+ } catch (e) {
11624
+ return systemError(reply, e, "autostart");
11625
+ }
11626
+ });
11585
11627
  const chromeFailures = new SlidingWindow(20, 6e4);
11586
11628
  const chromeWss = new WebSocketServer({ noServer: true, maxPayload: MAX_DEVICE_FRAME_BYTES });
11587
11629
  if (deps.chrome) {
@@ -12999,7 +13041,8 @@ var SettingsPatch = z11.object({
12999
13041
  sttModel: z11.string().trim().min(1).max(80).regex(/^[\w./-]+$/),
13000
13042
  duck: z11.boolean(),
13001
13043
  duckPercent: z11.number().int().min(0).max(100)
13002
- }).partial().strict()
13044
+ }).partial().strict(),
13045
+ updates: z11.object({ auto: z11.boolean() }).partial().strict()
13003
13046
  }).partial().strict();
13004
13047
  function settingsView(c) {
13005
13048
  return {
@@ -13062,7 +13105,8 @@ function settingsView(c) {
13062
13105
  marathiVoice: c.voice.marathiVoice,
13063
13106
  duck: c.voice.duck,
13064
13107
  duckPercent: c.voice.duckPercent
13065
- }
13108
+ },
13109
+ updates: { auto: c.updates.auto }
13066
13110
  };
13067
13111
  }
13068
13112
  var SettingsError = class extends Error {
@@ -13074,6 +13118,7 @@ function refuseForAgent(raw) {
13074
13118
  if ("brokers" in r || "trading" in r) throw new SettingsError("Broker accounts and the trading level can only be changed by the owner on the settings screen.");
13075
13119
  if ("music" in r || "hud" in r) throw new SettingsError("The music folder and the screen widgets can only be changed by the owner on the settings screen.");
13076
13120
  if ("video" in r) throw new SettingsError("Video settings (which programs run, where videos play, which sites) can only be changed by the owner on the settings screen.");
13121
+ if ("updates" in r) throw new SettingsError("Whether Friend updates itself can only be changed by the owner on the settings screen.");
13077
13122
  if ("alexa" in r) throw new SettingsError("Alexa settings can only be changed by the owner on the settings screen.");
13078
13123
  if ("channels" in r) throw new SettingsError("Messaging channels can only be set up by the owner on the settings screen.");
13079
13124
  if ("approvals" in r) throw new SettingsError("How much Friend asks before acting can only be changed by the owner on the settings screen.");
@@ -13218,6 +13263,7 @@ function applySettings(config, raw) {
13218
13263
  if (apiKey) config.openai.apiKey = apiKey;
13219
13264
  }
13220
13265
  if (p.claudeCode) Object.assign(config.claudeCode, p.claudeCode);
13266
+ if (p.updates) Object.assign(config.updates, p.updates);
13221
13267
  if (p.voice) {
13222
13268
  const safe = next.voice;
13223
13269
  for (const k of Object.keys(p.voice)) config.voice[k] = safe[k];
@@ -13689,6 +13735,368 @@ var DuckController = class {
13689
13735
  }
13690
13736
  };
13691
13737
 
13738
+ // ../server/src/system/autostart.ts
13739
+ import { spawn as spawn6 } from "node:child_process";
13740
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync10, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "node:fs";
13741
+ import { homedir as homedir4 } from "node:os";
13742
+ import { dirname as dirname4, posix as posix2, win32 as win322 } from "node:path";
13743
+ var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
13744
+ var NAME2 = "Friend";
13745
+ var LABEL = "com.friend.assistant";
13746
+ var UNIT = "friend.service";
13747
+ var quoted = (s) => `"${s}"`;
13748
+ function vbsLauncher(self) {
13749
+ const cmd = `${quoted(self.node)} ${quoted(self.script)} daemon start`;
13750
+ const inVbs = cmd.replace(/"/g, '""');
13751
+ return [`' Starts Friend in the background when you sign in. Made by: friend autostart on`, `CreateObject("WScript.Shell").Run "${inVbs}", 0, False`, ""].join("\r\n");
13752
+ }
13753
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13754
+ function plistFor(self, o) {
13755
+ const log = `${o.dataDir}/friend-autostart.log`;
13756
+ return [
13757
+ `<?xml version="1.0" encoding="UTF-8"?>`,
13758
+ `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
13759
+ `<plist version="1.0">`,
13760
+ `<dict>`,
13761
+ ` <key>Label</key><string>${LABEL}</string>`,
13762
+ ` <key>ProgramArguments</key>`,
13763
+ ` <array>`,
13764
+ ` <string>${xml(self.node)}</string>`,
13765
+ ` <string>${xml(self.script)}</string>`,
13766
+ ` <string>daemon</string>`,
13767
+ ` <string>start</string>`,
13768
+ ` </array>`,
13769
+ ` <key>RunAtLoad</key>`,
13770
+ ` <true/>`,
13771
+ ` <key>EnvironmentVariables</key>`,
13772
+ ` <dict><key>PATH</key><string>${xml(o.path)}</string></dict>`,
13773
+ ` <key>StandardOutPath</key><string>${xml(log)}</string>`,
13774
+ ` <key>StandardErrorPath</key><string>${xml(log)}</string>`,
13775
+ `</dict>`,
13776
+ `</plist>`,
13777
+ ""
13778
+ ].join("\n");
13779
+ }
13780
+ var unitWord = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
13781
+ function systemdUnit(self, o) {
13782
+ return [
13783
+ "[Unit]",
13784
+ "Description=Friend assistant",
13785
+ "After=network-online.target",
13786
+ "",
13787
+ "[Service]",
13788
+ "Type=oneshot",
13789
+ "RemainAfterExit=yes",
13790
+ `Environment=${unitWord(`PATH=${o.path}`)}`,
13791
+ `ExecStart=${unitWord(self.node)} ${unitWord(self.script)} daemon start`,
13792
+ `ExecStop=${unitWord(self.node)} ${unitWord(self.script)} daemon stop`,
13793
+ "",
13794
+ "[Install]",
13795
+ "WantedBy=default.target",
13796
+ ""
13797
+ ].join("\n");
13798
+ }
13799
+ var desktopWord = (s) => `"${s.replace(/(["`$\\])/g, "\\$1")}"`;
13800
+ function desktopEntry(self) {
13801
+ return ["[Desktop Entry]", "Type=Application", "Name=Friend", "Comment=Starts Friend in the background", `Exec=${desktopWord(self.node)} ${desktopWord(self.script)} daemon start`, "Terminal=false", "X-GNOME-Autostart-enabled=true", ""].join("\n");
13802
+ }
13803
+ var Autostart = class {
13804
+ constructor(d, self) {
13805
+ this.d = d;
13806
+ this.self = self;
13807
+ }
13808
+ d;
13809
+ self;
13810
+ get win() {
13811
+ return this.d.platform === "win32";
13812
+ }
13813
+ get mac() {
13814
+ return this.d.platform === "darwin";
13815
+ }
13816
+ get linux() {
13817
+ return this.d.platform === "linux";
13818
+ }
13819
+ p() {
13820
+ return this.win ? win322 : posix2;
13821
+ }
13822
+ vbs() {
13823
+ return win322.join(this.d.dataDir, "autostart.vbs");
13824
+ }
13825
+ plist() {
13826
+ return posix2.join(this.d.home, "Library", "LaunchAgents", `${LABEL}.plist`);
13827
+ }
13828
+ configDir() {
13829
+ return this.d.configHome || posix2.join(this.d.home, ".config");
13830
+ }
13831
+ unitFile() {
13832
+ return posix2.join(this.configDir(), "systemd", "user", UNIT);
13833
+ }
13834
+ desktopFile() {
13835
+ return posix2.join(this.configDir(), "autostart", "friend.desktop");
13836
+ }
13837
+ async hasSystemd() {
13838
+ return (await this.d.run("systemctl", ["--user", "--version"])).code === 0;
13839
+ }
13840
+ async status() {
13841
+ if (this.win) return { supported: true, enabled: (await this.d.run("reg", ["query", RUN_KEY, "/v", NAME2])).code === 0, kind: "registry" };
13842
+ if (this.mac) return { supported: true, enabled: this.d.exists(this.plist()), kind: "launchd" };
13843
+ if (this.linux) {
13844
+ if (await this.hasSystemd()) {
13845
+ const r = await this.d.run("systemctl", ["--user", "is-enabled", UNIT]);
13846
+ if (r.code === 0 && /enabled/.test(r.stdout)) return { supported: true, enabled: true, kind: "systemd" };
13847
+ }
13848
+ return { supported: true, enabled: this.d.exists(this.desktopFile()), kind: this.d.exists(this.desktopFile()) ? "xdg" : "none" };
13849
+ }
13850
+ return { supported: false, enabled: false, kind: "none" };
13851
+ }
13852
+ async enable() {
13853
+ const self = this.self();
13854
+ if (!self.node || !self.script) return { ok: false, message: "Friend does not know which program to start. Run it with the friend command and try again." };
13855
+ if (this.win) return this.enableWindows(self);
13856
+ if (this.mac) return this.enableMac(self);
13857
+ if (this.linux) return this.enableLinux(self);
13858
+ return { ok: false, message: "Starting Friend by itself is not supported on this kind of computer." };
13859
+ }
13860
+ async disable() {
13861
+ if (this.win) {
13862
+ await this.d.run("reg", ["delete", RUN_KEY, "/v", NAME2, "/f"]);
13863
+ this.d.remove(this.vbs());
13864
+ } else if (this.mac) {
13865
+ await this.d.run("launchctl", ["bootout", `gui/${this.d.uid}`, this.plist()]);
13866
+ this.d.remove(this.plist());
13867
+ } else if (this.linux) {
13868
+ if (await this.hasSystemd()) {
13869
+ await this.d.run("systemctl", ["--user", "disable", UNIT]);
13870
+ this.d.remove(this.unitFile());
13871
+ await this.d.run("systemctl", ["--user", "daemon-reload"]);
13872
+ }
13873
+ this.d.remove(this.desktopFile());
13874
+ } else return { ok: false, message: "Starting Friend by itself is not supported on this kind of computer." };
13875
+ return { ok: true, message: "Friend will no longer start by itself when you sign in." };
13876
+ }
13877
+ /** Writes the setup again with the program's current place, when it is on. Used after an upgrade. */
13878
+ async refresh() {
13879
+ if ((await this.status()).enabled) await this.enable();
13880
+ }
13881
+ async enableWindows(self) {
13882
+ this.d.write(this.vbs(), vbsLauncher(self));
13883
+ const r = await this.d.run("reg", ["add", RUN_KEY, "/v", NAME2, "/t", "REG_SZ", "/d", `wscript.exe ${quoted(this.vbs())}`, "/f"]);
13884
+ if (r.code !== 0) return { ok: false, message: "Could not add Friend to the programs that start when you sign in." };
13885
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
13886
+ }
13887
+ async enableMac(self) {
13888
+ this.d.write(this.plist(), plistFor(self, { dataDir: this.d.dataDir, path: this.d.path }));
13889
+ await this.d.run("launchctl", ["bootout", `gui/${this.d.uid}`, this.plist()]);
13890
+ let r = await this.d.run("launchctl", ["bootstrap", `gui/${this.d.uid}`, this.plist()]);
13891
+ if (r.code !== 0) r = await this.d.run("launchctl", ["load", "-w", this.plist()]);
13892
+ if (r.code !== 0) return { ok: false, message: "Could not switch the launch agent on. It will still be used the next time you sign in." };
13893
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
13894
+ }
13895
+ async enableLinux(self) {
13896
+ if (await this.hasSystemd()) {
13897
+ this.d.write(this.unitFile(), systemdUnit(self, { path: this.d.path }));
13898
+ await this.d.run("systemctl", ["--user", "daemon-reload"]);
13899
+ const r = await this.d.run("systemctl", ["--user", "enable", UNIT]);
13900
+ if (r.code === 0) {
13901
+ this.d.remove(this.desktopFile());
13902
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
13903
+ }
13904
+ }
13905
+ this.d.write(this.desktopFile(), desktopEntry(self));
13906
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
13907
+ }
13908
+ };
13909
+ function realAutostartDeps(dataDir) {
13910
+ return {
13911
+ platform: process.platform,
13912
+ home: homedir4(),
13913
+ dataDir,
13914
+ uid: typeof process.getuid === "function" ? process.getuid() : 0,
13915
+ path: process.env.PATH ?? "",
13916
+ ...process.env.XDG_CONFIG_HOME ? { configHome: process.env.XDG_CONFIG_HOME } : {},
13917
+ exists: (p) => existsSync16(p),
13918
+ read: (p) => {
13919
+ try {
13920
+ return readFileSync10(p, "utf8");
13921
+ } catch {
13922
+ return null;
13923
+ }
13924
+ },
13925
+ write: (p, text3) => {
13926
+ mkdirSync11(dirname4(p), { recursive: true });
13927
+ writeFileSync10(p, text3);
13928
+ },
13929
+ remove: (p) => rmSync9(p, { force: true }),
13930
+ run: (cmd, args) => new Promise((resolve13) => {
13931
+ let out = "";
13932
+ const child = spawn6(cmd, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
13933
+ child.stdout?.on("data", (d) => out += d.toString());
13934
+ child.stderr?.on("data", (d) => out += d.toString());
13935
+ child.on("error", () => resolve13({ code: 127, stdout: out }));
13936
+ child.on("close", (code) => resolve13({ code: code ?? 1, stdout: out }));
13937
+ })
13938
+ };
13939
+ }
13940
+ function currentSelf() {
13941
+ const script = process.argv[1] ?? "";
13942
+ return { node: process.execPath, script: /\.(mjs|js|cjs)$/i.test(script) ? script : "" };
13943
+ }
13944
+
13945
+ // ../server/src/system/npm.ts
13946
+ import { spawn as spawn7 } from "node:child_process";
13947
+ var PACKAGE = "friend-assistant";
13948
+ var runningVersion = () => true ? "0.1.45" : "development";
13949
+ async function npmLatest(fetchFn = fetch, timeoutMs = 1e4) {
13950
+ try {
13951
+ const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
13952
+ if (!res.ok) return null;
13953
+ const v = (await res.json()).version;
13954
+ return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v) ? v : null;
13955
+ } catch {
13956
+ return null;
13957
+ }
13958
+ }
13959
+ var ps = (s) => `'${s.replace(/'/g, "''")}'`;
13960
+ function spawnUpgrade(self, platform = process.platform) {
13961
+ if (!self.node || !self.script) return false;
13962
+ try {
13963
+ const child = platform === "win32" ? spawn7("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", `Start-Process -WindowStyle Hidden -FilePath ${ps(self.node)} -ArgumentList ${ps(`"${self.script}"`)},'upgrade','--auto'`], { detached: true, stdio: "ignore", windowsHide: true }) : spawn7(self.node, [self.script, "upgrade", "--auto"], { detached: true, stdio: "ignore" });
13964
+ child.on("error", () => {
13965
+ });
13966
+ child.unref();
13967
+ return child.pid !== void 0;
13968
+ } catch {
13969
+ return false;
13970
+ }
13971
+ }
13972
+
13973
+ // ../server/src/system/updater.ts
13974
+ function isNewerVersion(a, b) {
13975
+ if (!/^\d/.test(a) || !/^\d/.test(b)) return false;
13976
+ const n = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
13977
+ const [x, y] = [n(a), n(b)];
13978
+ for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
13979
+ return false;
13980
+ }
13981
+ var Updater = class {
13982
+ constructor(d, t = {}) {
13983
+ this.d = d;
13984
+ this.firstMs = t.firstMs ?? 2e4;
13985
+ this.everyMs = t.everyMs ?? 6 * 36e5;
13986
+ this.retryMs = t.retryMs ?? 5 * 6e4;
13987
+ }
13988
+ d;
13989
+ latest = null;
13990
+ checkedAt = null;
13991
+ applying = false;
13992
+ error = null;
13993
+ notified = null;
13994
+ inflight = null;
13995
+ first = null;
13996
+ every = null;
13997
+ retry = null;
13998
+ firstMs;
13999
+ everyMs;
14000
+ retryMs;
14001
+ state() {
14002
+ const current = this.d.current();
14003
+ return { current, latest: this.latest, checkedAt: this.checkedAt, available: this.latest !== null && isNewerVersion(this.latest, current), applying: this.applying, canApply: this.d.canApply(), error: this.error };
14004
+ }
14005
+ start() {
14006
+ this.stop();
14007
+ this.first = setTimeout(() => {
14008
+ this.first = null;
14009
+ void this.check();
14010
+ this.every = setInterval(() => void this.check(), this.everyMs);
14011
+ this.every.unref?.();
14012
+ }, this.firstMs);
14013
+ this.first.unref?.();
14014
+ }
14015
+ stop() {
14016
+ if (this.first) clearTimeout(this.first);
14017
+ if (this.every) clearInterval(this.every);
14018
+ if (this.retry) clearTimeout(this.retry);
14019
+ this.first = this.every = this.retry = null;
14020
+ }
14021
+ /** Asks npm for the newest version (one question at a time), tells the owner, and updates when that is switched on. */
14022
+ check() {
14023
+ this.inflight ??= (async () => {
14024
+ try {
14025
+ const latest = await this.d.latest();
14026
+ this.checkedAt = Date.now();
14027
+ if (latest) {
14028
+ this.latest = latest;
14029
+ this.error = null;
14030
+ await this.consider();
14031
+ } else {
14032
+ this.error = "Could not ask npm for the newest version. Check the internet connection.";
14033
+ }
14034
+ } catch {
14035
+ this.error = "Could not ask npm for the newest version. Check the internet connection.";
14036
+ } finally {
14037
+ this.inflight = null;
14038
+ }
14039
+ return this.state();
14040
+ })();
14041
+ return this.inflight;
14042
+ }
14043
+ /** The owner pressed Update now: installs even when automatic updates are off, but never in the middle of an answer. */
14044
+ async applyNow() {
14045
+ if (this.applying) return { ok: true, message: "The update is already running. Reload this page in a minute." };
14046
+ if (!this.latest) await this.check();
14047
+ const s = this.state();
14048
+ if (!s.latest) return { ok: false, message: s.error ?? "Could not ask npm for the newest version." };
14049
+ if (this.applying) return { ok: true, message: "The update is already running. Reload this page in a minute." };
14050
+ if (!s.available) return { ok: false, message: `Friend is up to date (${s.current}).` };
14051
+ if (!s.canApply) return { ok: false, message: "This Friend is running in a terminal and cannot restart itself. Close it and run: friend upgrade" };
14052
+ if (this.d.busy()) return { ok: false, message: "Friend is busy with a request. Try again in a moment." };
14053
+ return await this.begin(s.latest) ? { ok: true, message: `Updating to ${s.latest}. Friend restarts in a moment: reload this page afterwards.` } : { ok: false, message: this.error ?? "Could not start the update." };
14054
+ }
14055
+ async consider() {
14056
+ const s = this.state();
14057
+ if (!s.available || !s.latest) return;
14058
+ const canApply = this.d.canApply();
14059
+ if (this.notified !== s.latest) {
14060
+ this.notified = s.latest;
14061
+ this.d.notify(
14062
+ canApply ? this.d.auto() ? `Friend ${s.latest} is available. It will update by itself when Friend is not busy.` : `Friend ${s.latest} is available. Turn on automatic updates in Settings, or press Update now there.` : `Friend ${s.latest} is available. Close this Friend and run: friend upgrade`
14063
+ );
14064
+ }
14065
+ if (this.d.auto() && canApply) await this.maybeApply();
14066
+ }
14067
+ async maybeApply() {
14068
+ const s = this.state();
14069
+ if (this.applying || !s.available || !s.latest) return;
14070
+ if (this.d.busy()) {
14071
+ if (!this.retry) {
14072
+ this.retry = setTimeout(() => {
14073
+ this.retry = null;
14074
+ void this.maybeApply();
14075
+ }, this.retryMs);
14076
+ this.retry.unref?.();
14077
+ }
14078
+ return;
14079
+ }
14080
+ await this.begin(s.latest);
14081
+ }
14082
+ async begin(version) {
14083
+ this.applying = true;
14084
+ this.error = null;
14085
+ this.d.notify(`Updating Friend to ${version}. It restarts in a moment: reload this page afterwards.`);
14086
+ let ok = false;
14087
+ try {
14088
+ ok = await this.d.apply(version);
14089
+ } catch {
14090
+ ok = false;
14091
+ }
14092
+ if (!ok) {
14093
+ this.applying = false;
14094
+ this.error = "Could not start the update.";
14095
+ }
14096
+ return ok;
14097
+ }
14098
+ };
14099
+
13692
14100
  // ../server/src/voice/challenge.ts
13693
14101
  import { randomUUID as randomUUID6 } from "node:crypto";
13694
14102
  var Challenges = class {
@@ -13750,20 +14158,20 @@ var Challenges = class {
13750
14158
 
13751
14159
  // ../server/src/voice/crypto.ts
13752
14160
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes9 } from "node:crypto";
13753
- import { chmodSync as chmodSync9, existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
13754
- import { dirname as dirname4 } from "node:path";
14161
+ import { chmodSync as chmodSync9, existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
14162
+ import { dirname as dirname5 } from "node:path";
13755
14163
  var KEY_BYTES = 32;
13756
14164
  var IV_BYTES = 12;
13757
14165
  var TAG_BYTES = 16;
13758
14166
  function loadOrCreateKey(path3) {
13759
- if (existsSync16(path3)) {
13760
- const key2 = readFileSync10(path3);
14167
+ if (existsSync17(path3)) {
14168
+ const key2 = readFileSync11(path3);
13761
14169
  if (key2.length !== KEY_BYTES) throw new Error(`invalid voice key file (expected ${KEY_BYTES} bytes): ${path3}`);
13762
14170
  return key2;
13763
14171
  }
13764
- mkdirSync11(dirname4(path3), { recursive: true });
14172
+ mkdirSync12(dirname5(path3), { recursive: true });
13765
14173
  const key = randomBytes9(KEY_BYTES);
13766
- writeFileSync10(path3, key, { mode: 384 });
14174
+ writeFileSync11(path3, key, { mode: 384 });
13767
14175
  try {
13768
14176
  chmodSync9(path3, 384);
13769
14177
  } catch {
@@ -14848,7 +15256,7 @@ async function createApp(opts) {
14848
15256
  broadcast({ type: "voice.heard", text: held.text, trust: "owner" });
14849
15257
  await runVoiceTurn(held.text, conn.ip, epoch);
14850
15258
  }
14851
- const here2 = dirname5(fileURLToPath2(import.meta.url));
15259
+ const here2 = dirname6(fileURLToPath2(import.meta.url));
14852
15260
  const catalogDir = opts.pluginCatalogDir ?? firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve5(here2, "plugins"), resolve5(here2, "../../..", "plugins")]) ?? resolve5(here2, "plugins");
14853
15261
  const pluginsApi = {
14854
15262
  list: () => {
@@ -15020,11 +15428,42 @@ async function createApp(opts) {
15020
15428
  });
15021
15429
  }
15022
15430
  });
15431
+ const self = opts.system?.self ?? currentSelf();
15432
+ const autostart = new Autostart(opts.system?.autostartDeps ?? realAutostartDeps(opts.dataDir), () => self);
15433
+ const updater = new Updater(
15434
+ {
15435
+ current: opts.system?.current ?? runningVersion,
15436
+ latest: opts.system?.latest ?? (() => npmLatest()),
15437
+ auto: () => config.updates.auto,
15438
+ busy: () => agent.isBusy(),
15439
+ canApply: opts.system?.canApply ?? (() => process.env.FRIEND_DAEMON === "1"),
15440
+ // only Friend in the background can replace itself
15441
+ apply: opts.system?.apply ?? (() => spawnUpgrade(self)),
15442
+ notify: (text3) => broadcast({ type: "alert", level: "info", text: text3 })
15443
+ },
15444
+ opts.system?.times
15445
+ );
15446
+ const systemApi = {
15447
+ info: async () => ({ version: updater.state().current, update: updater.state(), autostart: await autostart.status(), auto: config.updates.auto }),
15448
+ check: () => updater.check(),
15449
+ update: async (pin, ip) => {
15450
+ await requirePin(pin, ip);
15451
+ audit.log("owner", "system.update_now", { ip });
15452
+ return updater.applyNow();
15453
+ },
15454
+ setAutostart: async (enabled, pin, ip) => {
15455
+ await requirePin(pin, ip);
15456
+ audit.log("owner", enabled ? "system.autostart_on" : "system.autostart_off", { ip });
15457
+ const r = enabled ? await autostart.enable() : await autostart.disable();
15458
+ return { ...r, status: await autostart.status() };
15459
+ }
15460
+ };
15023
15461
  const tls = config.https ? await ensureTlsCert(opts.dataDir, [...config.allowedHosts, ...config.publicUrl ? [new URL(config.publicUrl).hostname] : []]) : void 0;
15024
15462
  const gateway = createGateway({
15025
15463
  ...tls ? { tls } : {},
15026
15464
  connectors: connectorsApi,
15027
15465
  chrome: chromeRemoteApi,
15466
+ system: systemApi,
15028
15467
  video: {
15029
15468
  stream: (id2) => videoHub.streamOf(id2),
15030
15469
  status: async () => videoHub.status() ?? await videoHub.refresh(),
@@ -15594,12 +16033,14 @@ ${url}`);
15594
16033
  for (const id2 of gone) broadcast({ type: "stage", command: { cmd: "hide", id: id2 } });
15595
16034
  if (gone.length) for (const follow of stage.settleNow()) broadcast({ type: "stage", command: follow });
15596
16035
  }, opts.expireIntervalMs ?? 1e3);
16036
+ if (opts.system?.checkForUpdates) updater.start();
15597
16037
  const listening = await gateway.listen();
15598
16038
  listenPort = listening.port;
15599
16039
  return listening;
15600
16040
  },
15601
16041
  async stop() {
15602
16042
  stopped = true;
16043
+ updater.stop();
15603
16044
  hostPlayer.dispose();
15604
16045
  videoHub.dispose();
15605
16046
  heartbeat.stop();
@@ -15622,7 +16063,7 @@ ${url}`);
15622
16063
  }
15623
16064
 
15624
16065
  // ../server/src/voice/sidecar.ts
15625
- import { spawn as spawn6 } from "node:child_process";
16066
+ import { spawn as spawn8 } from "node:child_process";
15626
16067
  import { createInterface } from "node:readline";
15627
16068
  var SidecarProcess = class {
15628
16069
  constructor(opts) {
@@ -15659,7 +16100,7 @@ var SidecarProcess = class {
15659
16100
  settled = true;
15660
16101
  reject(new Error(message));
15661
16102
  };
15662
- const child = spawn6(this.opts.command, this.opts.args, {
16103
+ const child = spawn8(this.opts.command, this.opts.args, {
15663
16104
  cwd: this.opts.cwd,
15664
16105
  env: { ...this.opts.env ?? process.env, FRIEND_VOICE_TOKEN: this.opts.token },
15665
16106
  windowsHide: true,
@@ -15807,6 +16248,9 @@ var PARSERS = {
15807
16248
  trustProxy: bool
15808
16249
  };
15809
16250
  var GROUPS = {
16251
+ updates: {
16252
+ auto: bool
16253
+ },
15810
16254
  voice: {
15811
16255
  enabled: bool,
15812
16256
  sttModel: nonEmpty,
@@ -15880,6 +16324,7 @@ var KEY_HELP = {
15880
16324
  allowedOrigins: "extra web addresses allowed to connect, separated by commas",
15881
16325
  allowedHosts: "extra host names that may reach Friend (for example my-pc), separated by commas",
15882
16326
  trustProxy: "true when Friend sits behind your own reverse proxy",
16327
+ "updates.auto": "true or false: look for a newer Friend when it starts and install it by itself while Friend is not busy (false: only when you run friend upgrade or press Update now)",
15883
16328
  "voice.enabled": "true or false: use speech at all",
15884
16329
  "voice.sttModel": "listening model: tiny, base, small, large-v3-turbo \u2026 (bigger is more accurate and slower; Marathi and far-from-mic speech do best with small or large-v3-turbo)",
15885
16330
  "voice.ttsVoice": "speaking voice (easier: friend voice setup, or Settings)",
@@ -15947,12 +16392,12 @@ import { resolve as resolve9 } from "node:path";
15947
16392
  import { WebSocket as WebSocket3 } from "ws";
15948
16393
 
15949
16394
  // ../agent-cli/src/tools/fs.ts
15950
- import { closeSync, mkdirSync as mkdirSync12, openSync, readSync, readdirSync as readdirSync4, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
15951
- import { dirname as dirname7, join as join19 } from "node:path";
16395
+ import { closeSync, mkdirSync as mkdirSync13, openSync, readSync, readdirSync as readdirSync4, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
16396
+ import { dirname as dirname8, join as join19 } from "node:path";
15952
16397
 
15953
16398
  // ../agent-cli/src/tools/roots.ts
15954
16399
  import { lstatSync as lstatSync2, realpathSync } from "node:fs";
15955
- import { dirname as dirname6, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve6, sep as sep3 } from "node:path";
16400
+ import { dirname as dirname7, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve6, sep as sep3 } from "node:path";
15956
16401
  var isWin = process.platform === "win32";
15957
16402
  var norm = (p) => isWin ? p.toLowerCase() : p;
15958
16403
  var exists = (p) => {
@@ -15968,18 +16413,18 @@ function realish(abs) {
15968
16413
  let cur = abs;
15969
16414
  const tail2 = [];
15970
16415
  while (!exists(cur)) {
15971
- const parent = dirname6(cur);
16416
+ const parent = dirname7(cur);
15972
16417
  if (parent === cur) return abs;
15973
16418
  tail2.unshift(cur.slice(parent.length).replace(/^[\\/]+/, ""));
15974
16419
  cur = parent;
15975
16420
  }
15976
- let real2;
16421
+ let real3;
15977
16422
  try {
15978
- real2 = realpathSync.native(cur);
16423
+ real3 = realpathSync.native(cur);
15979
16424
  } catch {
15980
16425
  return UNRESOLVABLE + abs;
15981
16426
  }
15982
- return tail2.length ? resolve6(real2, ...tail2) : real2;
16427
+ return tail2.length ? resolve6(real3, ...tail2) : real3;
15983
16428
  }
15984
16429
  function within(root, target) {
15985
16430
  if (target.startsWith(UNRESOLVABLE)) return false;
@@ -16051,8 +16496,8 @@ function fsWrite(args, ctx) {
16051
16496
  if ("error" in g) return { ok: false, error: g.error };
16052
16497
  if (Buffer.byteLength(args.content) > MAX_WRITE) return { ok: false, error: "Content too large (5 MB limit)" };
16053
16498
  try {
16054
- mkdirSync12(dirname7(g.abs), { recursive: true });
16055
- writeFileSync11(g.abs, args.content);
16499
+ mkdirSync13(dirname8(g.abs), { recursive: true });
16500
+ writeFileSync12(g.abs, args.content);
16056
16501
  return { ok: true, output: `Wrote ${Buffer.byteLength(args.content)} bytes to ${g.abs}`, truncated: false };
16057
16502
  } catch (e) {
16058
16503
  return { ok: false, error: e.message };
@@ -16060,7 +16505,7 @@ function fsWrite(args, ctx) {
16060
16505
  }
16061
16506
 
16062
16507
  // ../agent-cli/src/tools/shell.ts
16063
- import { spawn as spawn7, spawnSync } from "node:child_process";
16508
+ import { spawn as spawn9, spawnSync } from "node:child_process";
16064
16509
  function killTree2(child) {
16065
16510
  if (!child.pid) return;
16066
16511
  if (process.platform === "win32") {
@@ -16086,7 +16531,7 @@ function runCommand2(command, cwdArg, ctx, argv) {
16086
16531
  const file = argv?.file ?? (win ? "powershell.exe" : "sh");
16087
16532
  const args = argv?.args ?? (win ? ["-NoProfile", "-NonInteractive", "-Command", command] : ["-c", command]);
16088
16533
  return new Promise((resolve13) => {
16089
- const child = spawn7(file, args, { cwd, windowsHide: true, detached: !win, stdio: ["ignore", "pipe", "pipe"] });
16534
+ const child = spawn9(file, args, { cwd, windowsHide: true, detached: !win, stdio: ["ignore", "pipe", "pipe"] });
16090
16535
  let out = "";
16091
16536
  let truncated = false;
16092
16537
  let timedOut = false;
@@ -16288,8 +16733,8 @@ function startDeviceNode(opts) {
16288
16733
 
16289
16734
  // ../agent-cli/src/commands/start.ts
16290
16735
  import { randomBytes as randomBytes10 } from "node:crypto";
16291
- import { existsSync as existsSync18, readFileSync as readFileSync12, writeFileSync as writeFileSync13 } from "node:fs";
16292
- import { dirname as dirname9, join as join21, resolve as resolve8 } from "node:path";
16736
+ import { existsSync as existsSync19, readFileSync as readFileSync13, writeFileSync as writeFileSync14 } from "node:fs";
16737
+ import { dirname as dirname10, join as join21, resolve as resolve8 } from "node:path";
16293
16738
  import { fileURLToPath as fileURLToPath4 } from "node:url";
16294
16739
 
16295
16740
  // ../agent-cli/src/commands/lan.ts
@@ -16394,9 +16839,9 @@ async function lanCommand(args, io2, ifaces = networkInterfaces2) {
16394
16839
  }
16395
16840
 
16396
16841
  // ../agent-cli/src/commands/voice.ts
16397
- import { spawn as spawn8 } from "node:child_process";
16398
- import { existsSync as existsSync17, mkdirSync as mkdirSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync12 } from "node:fs";
16399
- import { dirname as dirname8, join as join20, resolve as resolve7 } from "node:path";
16842
+ import { spawn as spawn10 } from "node:child_process";
16843
+ import { existsSync as existsSync18, mkdirSync as mkdirSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync13 } from "node:fs";
16844
+ import { dirname as dirname9, join as join20, resolve as resolve7 } from "node:path";
16400
16845
  import { fileURLToPath as fileURLToPath3 } from "node:url";
16401
16846
  function voicePaths(dataDir) {
16402
16847
  const home = join20(dataDir, "voice");
@@ -16411,19 +16856,19 @@ function voicePaths(dataDir) {
16411
16856
  }
16412
16857
  function readMarker(dataDir) {
16413
16858
  try {
16414
- return JSON.parse(readFileSync11(voicePaths(dataDir).marker, "utf8"));
16859
+ return JSON.parse(readFileSync12(voicePaths(dataDir).marker, "utf8"));
16415
16860
  } catch {
16416
16861
  return null;
16417
16862
  }
16418
16863
  }
16419
16864
  function findVoiceDir() {
16420
- const here2 = dirname8(fileURLToPath3(import.meta.url));
16865
+ const here2 = dirname9(fileURLToPath3(import.meta.url));
16421
16866
  return firstExistingDir([process.env.FRIEND_VOICE_DIR, resolve7(here2, "voice"), resolve7(here2, "../../../voice")], "friend_voice") ?? resolve7(here2, "voice");
16422
16867
  }
16423
16868
  var QUIET_ENV = { ...process.env, HF_HUB_DISABLE_SYMLINKS_WARNING: "1", PYTHONUTF8: "1" };
16424
16869
  var defaultRunner = (cmd, args, opts) => new Promise((res) => {
16425
16870
  let stdout = "";
16426
- const child = spawn8(cmd, args, { cwd: opts?.cwd, env: QUIET_ENV, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
16871
+ const child = spawn10(cmd, args, { cwd: opts?.cwd, env: QUIET_ENV, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
16427
16872
  const feed = (d) => {
16428
16873
  const s = d.toString();
16429
16874
  stdout += s;
@@ -16508,8 +16953,8 @@ async function voiceSetupCommand(io2, deps = {}, args = []) {
16508
16953
  io2.err("Python 3.10 or newer is required. Install it from https://www.python.org/downloads/ and run this again.");
16509
16954
  return 1;
16510
16955
  }
16511
- mkdirSync13(p.home, { recursive: true });
16512
- if (!existsSync17(p.python)) {
16956
+ mkdirSync14(p.home, { recursive: true });
16957
+ if (!existsSync18(p.python)) {
16513
16958
  io2.out("1/3 Creating a private Python environment\u2026");
16514
16959
  const r = await run2(py.cmd, [...py.args, "-m", "venv", p.venv], { onLine });
16515
16960
  if (r.code !== 0) {
@@ -16543,7 +16988,7 @@ ${tail(dl.stdout)}`);
16543
16988
  return 1;
16544
16989
  }
16545
16990
  const marker = { python: p.python, sttModel: cfg.voice.sttModel, ttsVoice: cfg.voice.ttsVoice, at: Date.now() };
16546
- writeFileSync12(p.marker, JSON.stringify(marker, null, 2));
16991
+ writeFileSync13(p.marker, JSON.stringify(marker, null, 2));
16547
16992
  io2.out("Voice is ready. Start Friend with `friend start`, then hold Space to talk.");
16548
16993
  return 0;
16549
16994
  }
@@ -16559,7 +17004,7 @@ async function voiceStatusCommand(io2) {
16559
17004
  io2.out("Voice engine: not installed. Run: friend voice setup");
16560
17005
  return 0;
16561
17006
  }
16562
- if (!existsSync17(marker.python)) {
17007
+ if (!existsSync18(marker.python)) {
16563
17008
  io2.out("Voice engine: installed, but its Python is missing. Run: friend voice setup");
16564
17009
  return 0;
16565
17010
  }
@@ -16630,9 +17075,9 @@ async function voiceCommand(args, io2) {
16630
17075
 
16631
17076
  // ../agent-cli/src/commands/start.ts
16632
17077
  function findWebDir(explicit) {
16633
- const here2 = dirname9(fileURLToPath4(import.meta.url));
17078
+ const here2 = dirname10(fileURLToPath4(import.meta.url));
16634
17079
  const candidates = [explicit, process.env.FRIEND_WEB_DIR, resolve8(here2, "../../../web/dist"), resolve8(here2, "web")];
16635
- return candidates.find((c) => !!c && existsSync18(join21(c, "index.html")));
17080
+ return candidates.find((c) => !!c && existsSync19(join21(c, "index.html")));
16636
17081
  }
16637
17082
  async function startFriend(opts) {
16638
17083
  const stored = loadConfig(opts.dataDir);
@@ -16657,6 +17102,8 @@ async function startFriend(opts) {
16657
17102
  }) : null;
16658
17103
  const voice2 = sidecar ? new HttpVoiceService({ baseUrl: () => sidecar.baseUrl(), token: secret }) : void 0;
16659
17104
  const app = await createApp({
17105
+ system: { checkForUpdates: true },
17106
+ // looks for a newer Friend after it starts, and updates by itself when that is on
16660
17107
  dataDir: opts.dataDir,
16661
17108
  webDir: findWebDir(opts.webDir),
16662
17109
  voice: voice2,
@@ -16667,16 +17114,16 @@ async function startFriend(opts) {
16667
17114
  });
16668
17115
  const { port } = await app.start();
16669
17116
  const tokenFile = join21(opts.dataDir, "local-device.token");
16670
- let token = existsSync18(tokenFile) ? readFileSync12(tokenFile, "utf8").trim() : "";
17117
+ let token = existsSync19(tokenFile) ? readFileSync13(tokenFile, "utf8").trim() : "";
16671
17118
  if (!token || !app.auth.resolveDevice(token)) {
16672
17119
  token = app.auth.redeemPairCode(app.auth.createPairCode(), "this-pc");
16673
- writeFileSync13(tokenFile, token, { mode: 384 });
17120
+ writeFileSync14(tokenFile, token, { mode: 384 });
16674
17121
  }
16675
17122
  const node = startDeviceNode({
16676
17123
  url: `${app.config.https ? "wss" : "ws"}://127.0.0.1:${port}/device`,
16677
17124
  token,
16678
17125
  roots: app.config.roots,
16679
- ...app.config.https ? { ca: readFileSync12(join21(opts.dataDir, "tls", "cert.pem"), "utf8") } : {}
17126
+ ...app.config.https ? { ca: readFileSync13(join21(opts.dataDir, "tls", "cert.pem"), "utf8") } : {}
16680
17127
  // this PC trusts only its own Friend's certificate
16681
17128
  });
16682
17129
  const urls = urlsFor(app.config.bind, port, void 0, app.config.https);
@@ -16904,12 +17351,12 @@ async function memoryCommand(args, io2) {
16904
17351
  }
16905
17352
 
16906
17353
  // ../agent-cli/src/commands/plugin.ts
16907
- import { existsSync as existsSync19, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
16908
- import { dirname as dirname10, join as join22, resolve as resolve10 } from "node:path";
17354
+ import { existsSync as existsSync20, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
17355
+ import { dirname as dirname11, join as join22, resolve as resolve10 } from "node:path";
16909
17356
  import { fileURLToPath as fileURLToPath5 } from "node:url";
16910
17357
  var USAGE2 = "Usage: friend plugin catalog | list | install <name|folder> [--yes] | enable <id> | disable <id> | remove <id>";
16911
17358
  function defaultCatalog() {
16912
- const here2 = dirname10(fileURLToPath5(import.meta.url));
17359
+ const here2 = dirname11(fileURLToPath5(import.meta.url));
16913
17360
  return firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve10(here2, "plugins"), resolve10(here2, "../../../../plugins")]) ?? resolve10(here2, "plugins");
16914
17361
  }
16915
17362
  var count = (n, word2) => `${n} ${word2}${n === 1 ? "" : "s"}`;
@@ -16919,7 +17366,7 @@ async function pluginCommand(args, io2, deps = {}) {
16919
17366
  const flags = rest.filter((a) => a.startsWith("--"));
16920
17367
  const arg = rest.find((a) => !a.startsWith("--"));
16921
17368
  if (sub === "catalog") {
16922
- const dirs = existsSync19(catalogDir) ? readdirSync5(catalogDir).filter((d) => statSync5(join22(catalogDir, d)).isDirectory()) : [];
17369
+ const dirs = existsSync20(catalogDir) ? readdirSync5(catalogDir).filter((d) => statSync5(join22(catalogDir, d)).isDirectory()) : [];
16923
17370
  if (dirs.length === 0) io2.out("The catalog is empty.");
16924
17371
  for (const d of dirs.sort()) {
16925
17372
  const r = inspectPlugin(join22(catalogDir, d));
@@ -16952,8 +17399,8 @@ async function pluginCommand(args, io2, deps = {}) {
16952
17399
  io2.err(USAGE2);
16953
17400
  return 1;
16954
17401
  }
16955
- const inCatalog = !/[\\/]/.test(arg) && existsSync19(join22(catalogDir, arg));
16956
- const source = inCatalog ? join22(catalogDir, arg) : existsSync19(resolve10(arg)) ? resolve10(arg) : null;
17402
+ const inCatalog = !/[\\/]/.test(arg) && existsSync20(join22(catalogDir, arg));
17403
+ const source = inCatalog ? join22(catalogDir, arg) : existsSync20(resolve10(arg)) ? resolve10(arg) : null;
16957
17404
  if (!source) {
16958
17405
  io2.err(`"${arg}" is not in the catalog and there is no such folder. See: friend plugin catalog`);
16959
17406
  return 1;
@@ -17008,7 +17455,7 @@ async function pluginCommand(args, io2, deps = {}) {
17008
17455
 
17009
17456
  // ../agent-cli/src/commands/onboard/index.ts
17010
17457
  import crossSpawn4 from "cross-spawn";
17011
- import { mkdirSync as mkdirSync15 } from "node:fs";
17458
+ import { mkdirSync as mkdirSync16 } from "node:fs";
17012
17459
  import { resolve as resolve11 } from "node:path";
17013
17460
 
17014
17461
  // ../agent-cli/src/commands/ollama.ts
@@ -17331,16 +17778,16 @@ function installPlan(id2, platform, have) {
17331
17778
  }
17332
17779
 
17333
17780
  // ../agent-cli/src/commands/onboard/state.ts
17334
- import { existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync13, renameSync as renameSync4, rmSync as rmSync9, writeFileSync as writeFileSync14 } from "node:fs";
17335
- import { dirname as dirname11, join as join23 } from "node:path";
17781
+ import { existsSync as existsSync21, mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync10, writeFileSync as writeFileSync15 } from "node:fs";
17782
+ import { dirname as dirname12, join as join23 } from "node:path";
17336
17783
  var FEATURES = ["claude-code", "claude-api", "ollama", "voice", "sandbox"];
17337
17784
  var emptyState = () => ({ version: 1, done: {}, features: [], restart: [] });
17338
17785
  var statePath = (dataDir) => join23(dataDir, "onboarding.json");
17339
17786
  function loadState(dataDir) {
17340
17787
  const f = statePath(dataDir);
17341
- if (!existsSync20(f)) return emptyState();
17788
+ if (!existsSync21(f)) return emptyState();
17342
17789
  try {
17343
- const raw = JSON.parse(readFileSync13(f, "utf8"));
17790
+ const raw = JSON.parse(readFileSync14(f, "utf8"));
17344
17791
  const done = raw.done && typeof raw.done === "object" ? Object.fromEntries(Object.entries(raw.done).filter(([, v]) => typeof v === "string")) : {};
17345
17792
  const features2 = Array.isArray(raw.features) ? raw.features.filter((x) => FEATURES.includes(x)) : [];
17346
17793
  const restart = Array.isArray(raw.restart) ? raw.restart.filter((x) => typeof x === "string") : [];
@@ -17351,13 +17798,13 @@ function loadState(dataDir) {
17351
17798
  }
17352
17799
  function saveState(dataDir, state) {
17353
17800
  const f = statePath(dataDir);
17354
- mkdirSync14(dirname11(f), { recursive: true });
17801
+ mkdirSync15(dirname12(f), { recursive: true });
17355
17802
  const tmp = `${f}.tmp`;
17356
- writeFileSync14(tmp, JSON.stringify(state, null, 2));
17803
+ writeFileSync15(tmp, JSON.stringify(state, null, 2));
17357
17804
  renameSync4(tmp, f);
17358
17805
  }
17359
17806
  function resetState(dataDir) {
17360
- rmSync9(statePath(dataDir), { force: true });
17807
+ rmSync10(statePath(dataDir), { force: true });
17361
17808
  }
17362
17809
 
17363
17810
  // ../agent-cli/src/commands/onboard/ui.ts
@@ -17709,7 +18156,7 @@ async function workspace(c) {
17709
18156
  const answer = await text2(c.ui, current ? "Workspace folder" : "Workspace folder (Enter to skip)", current ?? "");
17710
18157
  if (answer) {
17711
18158
  const abs = resolve11(answer);
17712
- mkdirSync15(abs, { recursive: true });
18159
+ mkdirSync16(abs, { recursive: true });
17713
18160
  cfg.roots = [abs];
17714
18161
  saveConfig(cfg);
17715
18162
  io2.out(`Workspace: ${abs}`);
@@ -17879,19 +18326,54 @@ async function doctorCommand(io2, partial = {}) {
17879
18326
  return requiredMissing ? 1 : 0;
17880
18327
  }
17881
18328
 
18329
+ // ../agent-cli/src/commands/autostart.ts
18330
+ var USAGE3 = "Usage: friend autostart on|off|status";
18331
+ var real = (io2) => new Autostart(realAutostartDeps(io2.dataDir), currentSelf);
18332
+ async function autostartCommand(args, io2, autostart = real(io2)) {
18333
+ switch (args[0]) {
18334
+ case "on":
18335
+ case "off": {
18336
+ const r = await (args[0] === "on" ? autostart.enable() : autostart.disable());
18337
+ if (!r.ok) {
18338
+ io2.err(r.message);
18339
+ return 1;
18340
+ }
18341
+ io2.out(r.message);
18342
+ if (args[0] === "on") io2.out("Turn it off again with: friend autostart off");
18343
+ return 0;
18344
+ }
18345
+ case "status": {
18346
+ const s = await autostart.status();
18347
+ if (!s.supported) {
18348
+ io2.out("Starting Friend by itself is not supported on this kind of computer.");
18349
+ return 0;
18350
+ }
18351
+ io2.out(`Starts by itself when you sign in: ${s.enabled ? "yes" : "no"}${s.enabled ? ` (${s.kind})` : ""}`);
18352
+ if (!s.enabled) io2.out("Turn it on with: friend autostart on");
18353
+ return 0;
18354
+ }
18355
+ case "refresh":
18356
+ await autostart.refresh();
18357
+ return 0;
18358
+ default:
18359
+ io2.err(USAGE3);
18360
+ return 1;
18361
+ }
18362
+ }
18363
+
17882
18364
  // ../agent-cli/src/commands/daemon.ts
17883
- import { spawn as spawn9, spawnSync as spawnSync2 } from "node:child_process";
17884
- import { closeSync as closeSync2, existsSync as existsSync21, mkdirSync as mkdirSync16, openSync as openSync2, readFileSync as readFileSync14, renameSync as renameSync5, rmSync as rmSync10, statSync as statSync6, writeFileSync as writeFileSync15 } from "node:fs";
18365
+ import { spawn as spawn11, spawnSync as spawnSync2 } from "node:child_process";
18366
+ import { closeSync as closeSync2, existsSync as existsSync22, mkdirSync as mkdirSync17, openSync as openSync2, readFileSync as readFileSync15, renameSync as renameSync5, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync16 } from "node:fs";
17885
18367
  import { connect } from "node:net";
17886
18368
  import { join as join24 } from "node:path";
17887
- var USAGE3 = "Usage: friend daemon start | stop | restart | status";
18369
+ var USAGE4 = "Usage: friend daemon start | stop | restart | status";
17888
18370
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
17889
18371
  var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
17890
18372
  var realDeps = {
17891
18373
  launch(logFile) {
17892
18374
  const fd = openSync2(logFile, "a");
17893
18375
  try {
17894
- const child = spawn9(process.execPath, [process.argv[1], "start"], { detached: true, stdio: ["ignore", fd, fd], windowsHide: true });
18376
+ const child = spawn11(process.execPath, [process.argv[1], "start"], { detached: true, stdio: ["ignore", fd, fd], windowsHide: true, env: { ...process.env, FRIEND_DAEMON: "1" } });
17895
18377
  child.unref();
17896
18378
  return child.pid;
17897
18379
  } finally {
@@ -17943,7 +18425,7 @@ var pidFile = (dataDir) => join24(dataDir, "friend.pid");
17943
18425
  var logFileOf = (dataDir) => join24(dataDir, "friend.log");
17944
18426
  function readPid(dataDir) {
17945
18427
  try {
17946
- const r = JSON.parse(readFileSync14(pidFile(dataDir), "utf8"));
18428
+ const r = JSON.parse(readFileSync15(pidFile(dataDir), "utf8"));
17947
18429
  return Number.isInteger(r.pid) && r.pid > 0 ? r : null;
17948
18430
  } catch {
17949
18431
  return null;
@@ -17952,7 +18434,7 @@ function readPid(dataDir) {
17952
18434
  function running(dataDir, deps) {
17953
18435
  const r = readPid(dataDir);
17954
18436
  if (r && deps.alive(r.pid)) return r;
17955
- if (r) rmSync10(pidFile(dataDir), { force: true });
18437
+ if (r) rmSync11(pidFile(dataDir), { force: true });
17956
18438
  return null;
17957
18439
  }
17958
18440
  function since(ms2) {
@@ -17974,10 +18456,10 @@ async function start(io2, deps) {
17974
18456
  io2.err(`Port ${cfg.port} is already in use. Friend may be running in a terminal (stop it with Ctrl+C), or another program uses that port (friend lan port N).`);
17975
18457
  return 1;
17976
18458
  }
17977
- mkdirSync16(io2.dataDir, { recursive: true });
18459
+ mkdirSync17(io2.dataDir, { recursive: true });
17978
18460
  const log = logFileOf(io2.dataDir);
17979
18461
  try {
17980
- if (existsSync21(log) && statSync6(log).size > MAX_LOG_BYTES) renameSync5(log, `${log}.old`);
18462
+ if (existsSync22(log) && statSync6(log).size > MAX_LOG_BYTES) renameSync5(log, `${log}.old`);
17981
18463
  } catch {
17982
18464
  }
17983
18465
  const pid = deps.launch(log);
@@ -17985,7 +18467,7 @@ async function start(io2, deps) {
17985
18467
  io2.err("Could not start Friend in the background.");
17986
18468
  return 1;
17987
18469
  }
17988
- writeFileSync15(pidFile(io2.dataDir), JSON.stringify({ pid, startedAt: deps.now() }));
18470
+ writeFileSync16(pidFile(io2.dataDir), JSON.stringify({ pid, startedAt: deps.now() }));
17989
18471
  for (let i = 0; i < 60; i++) {
17990
18472
  if (await deps.portOpen(cfg.port, 300)) {
17991
18473
  io2.out(`Friend is running in the background (pid ${pid}) at ${urls.local}`);
@@ -17996,7 +18478,7 @@ async function start(io2, deps) {
17996
18478
  if (!deps.alive(pid)) break;
17997
18479
  await sleep(250);
17998
18480
  }
17999
- rmSync10(pidFile(io2.dataDir), { force: true });
18481
+ rmSync11(pidFile(io2.dataDir), { force: true });
18000
18482
  io2.err(`Friend did not start. See the log: ${log}`);
18001
18483
  return 1;
18002
18484
  }
@@ -18011,7 +18493,7 @@ async function stop(io2, deps) {
18011
18493
  io2.err(`Could not stop Friend (pid ${r.pid}). End that process yourself.`);
18012
18494
  return 1;
18013
18495
  }
18014
- rmSync10(pidFile(io2.dataDir), { force: true });
18496
+ rmSync11(pidFile(io2.dataDir), { force: true });
18015
18497
  io2.out("Friend stopped.");
18016
18498
  return 0;
18017
18499
  }
@@ -18043,25 +18525,27 @@ async function daemonCommand(args, io2, deps = realDeps) {
18043
18525
  case "status":
18044
18526
  return status(io2, deps);
18045
18527
  default:
18046
- io2.err(USAGE3);
18528
+ io2.err(USAGE4);
18047
18529
  return 1;
18048
18530
  }
18049
18531
  }
18050
18532
 
18051
18533
  // ../agent-cli/src/commands/upgrade.ts
18052
18534
  import crossSpawn5 from "cross-spawn";
18053
- var PACKAGE = "friend-assistant";
18054
- var real = {
18055
- current: () => true ? "0.1.44" : "development",
18535
+ import { appendFileSync } from "node:fs";
18536
+ import { join as join25 } from "node:path";
18537
+ var PACKAGE2 = "friend-assistant";
18538
+ var real2 = {
18539
+ current: () => true ? "0.1.45" : "development",
18056
18540
  latest: () => new Promise((resolve13) => {
18057
- const child = crossSpawn5("npm", ["view", PACKAGE, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
18541
+ const child = crossSpawn5("npm", ["view", PACKAGE2, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
18058
18542
  let out = "";
18059
18543
  child.stdout?.on("data", (d) => out += d.toString());
18060
18544
  child.on("error", () => resolve13(null));
18061
18545
  child.on("close", (code) => resolve13(code === 0 && /^\d+\.\d+\.\d+/.test(out.trim()) ? out.trim().split(/\s+/)[0] : null));
18062
18546
  }),
18063
18547
  install: (version) => new Promise((resolve13) => {
18064
- const child = crossSpawn5("npm", ["install", "-g", `${PACKAGE}@${version}`, "--prefer-online"], { stdio: "inherit" });
18548
+ const child = crossSpawn5("npm", ["install", "-g", `${PACKAGE2}@${version}`, "--prefer-online"], { stdio: "inherit" });
18065
18549
  child.on("error", () => resolve13(1));
18066
18550
  child.on("close", (code) => resolve13(code ?? 1));
18067
18551
  }),
@@ -18070,6 +18554,11 @@ var real = {
18070
18554
  child.on("error", () => resolve13(1));
18071
18555
  child.on("close", (code) => resolve13(code ?? 1));
18072
18556
  }),
18557
+ refreshAutostart: () => new Promise((resolve13) => {
18558
+ const child = crossSpawn5("friend", ["autostart", "refresh"], { stdio: "ignore" });
18559
+ child.on("error", () => resolve13());
18560
+ child.on("close", () => resolve13());
18561
+ }),
18073
18562
  daemon: realDeps
18074
18563
  };
18075
18564
  var newer = (a, b) => {
@@ -18078,12 +18567,26 @@ var newer = (a, b) => {
18078
18567
  for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
18079
18568
  return false;
18080
18569
  };
18081
- async function upgradeCommand(args, io2, deps = real) {
18570
+ async function upgradeCommand(args, io0, deps = real2) {
18082
18571
  const force = args.includes("--force");
18083
- if (args.some((a) => a !== "--force")) {
18084
- io2.err("Usage: friend upgrade [--force]");
18572
+ const auto = args.includes("--auto");
18573
+ if (args.some((a) => a !== "--force" && a !== "--auto")) {
18574
+ io0.err("Usage: friend upgrade [--force] [--auto]");
18085
18575
  return 1;
18086
18576
  }
18577
+ const log = deps.log ?? ((line) => appendFileSync(join25(io0.dataDir, "update.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
18578
+ `));
18579
+ const io2 = auto ? {
18580
+ ...io0,
18581
+ out: (l) => {
18582
+ io0.out(l);
18583
+ log(l);
18584
+ },
18585
+ err: (l) => {
18586
+ io0.err(l);
18587
+ log(l);
18588
+ }
18589
+ } : io0;
18087
18590
  const current = deps.current();
18088
18591
  io2.out(`Installed: ${current}`);
18089
18592
  const latest = await deps.latest();
@@ -18106,7 +18609,7 @@ async function upgradeCommand(args, io2, deps = real) {
18106
18609
  io2.err(`Friend is running in a terminal (port ${cfg.port} is in use). Press Ctrl+C there to stop it, then run: friend upgrade`);
18107
18610
  return 1;
18108
18611
  }
18109
- io2.out(`Installing ${PACKAGE}@${latest}\u2026`);
18612
+ io2.out(`Installing ${PACKAGE2}@${latest}\u2026`);
18110
18613
  const code = await deps.install(latest);
18111
18614
  if (code !== 0) {
18112
18615
  io2.err("The install failed. If it says EBUSY or EPERM, close whatever is using Friend (a terminal running it, a voice process) and run: friend upgrade");
@@ -18114,6 +18617,8 @@ async function upgradeCommand(args, io2, deps = real) {
18114
18617
  return 1;
18115
18618
  }
18116
18619
  io2.out(`Upgraded ${current} \u2192 ${latest}.`);
18620
+ await deps.refreshAutostart?.().catch(() => {
18621
+ });
18117
18622
  if (wasRunning) return deps.startAgain();
18118
18623
  io2.out("Start it with: friend start (or in the background: friend daemon start)");
18119
18624
  return 0;
@@ -18187,7 +18692,7 @@ async function claudeCommand(args, io2, deps = {}) {
18187
18692
  }
18188
18693
 
18189
18694
  // ../agent-cli/src/commands/setup.ts
18190
- import { mkdirSync as mkdirSync17 } from "node:fs";
18695
+ import { mkdirSync as mkdirSync18 } from "node:fs";
18191
18696
  import { resolve as resolve12 } from "node:path";
18192
18697
  async function setupCommand3(io2) {
18193
18698
  const db = openDb(io2.dataDir);
@@ -18213,7 +18718,7 @@ async function setupCommand3(io2) {
18213
18718
  const cfg = loadConfig(io2.dataDir);
18214
18719
  if (workspace2) {
18215
18720
  const abs = resolve12(workspace2);
18216
- mkdirSync17(abs, { recursive: true });
18721
+ mkdirSync18(abs, { recursive: true });
18217
18722
  cfg.roots = [abs];
18218
18723
  }
18219
18724
  if (apiKey) cfg.apiKey = apiKey;
@@ -18278,7 +18783,7 @@ async function statusCommand3(io2) {
18278
18783
  }
18279
18784
 
18280
18785
  // ../agent-cli/src/cli.ts
18281
- var USAGE4 = `Usage: friend <command>
18786
+ var USAGE5 = `Usage: friend <command>
18282
18787
 
18283
18788
  onboard guided setup, step by step: checks this PC, installs what is missing, asks what you want
18284
18789
  doctor check what this PC has and what is missing (changes nothing)
@@ -18288,6 +18793,7 @@ var USAGE4 = `Usage: friend <command>
18288
18793
  start run the gateway and a local device node
18289
18794
  daemon start|stop|restart|status run Friend in the background (no terminal needed), stop it, or see if it is running
18290
18795
  upgrade [--force] stop the background Friend, install the newest version, start it again
18796
+ autostart on|off|status start Friend by itself when you sign in to this computer (Windows, macOS, Linux)
18291
18797
  pair print a one-time code to connect another PC
18292
18798
  node --url <ws-url> --code <code> [--roots a,b] [--name pc]
18293
18799
  connect this PC to a Friend gateway
@@ -18320,6 +18826,8 @@ async function run(argv, io2) {
18320
18826
  return await daemonCommand(rest, io2);
18321
18827
  case "upgrade":
18322
18828
  return await upgradeCommand(rest, io2);
18829
+ case "autostart":
18830
+ return await autostartCommand(rest, io2);
18323
18831
  case "pair":
18324
18832
  return await pairCommand(io2);
18325
18833
  case "node":
@@ -18335,12 +18843,12 @@ async function run(argv, io2) {
18335
18843
  case "help":
18336
18844
  case "--help":
18337
18845
  case "-h":
18338
- io2.out(USAGE4);
18846
+ io2.out(USAGE5);
18339
18847
  return 0;
18340
18848
  case "version":
18341
18849
  case "--version":
18342
18850
  case "-v":
18343
- io2.out(true ? "0.1.44" : "development");
18851
+ io2.out(true ? "0.1.45" : "development");
18344
18852
  return 0;
18345
18853
  case "lan":
18346
18854
  return await lanCommand(rest, io2);
@@ -18359,7 +18867,7 @@ async function run(argv, io2) {
18359
18867
  case "config":
18360
18868
  return await configCommand(rest, io2);
18361
18869
  default:
18362
- io2.err(USAGE4);
18870
+ io2.err(USAGE5);
18363
18871
  return 1;
18364
18872
  }
18365
18873
  } catch (e) {