friend-assistant 0.1.44 → 0.1.46

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
@@ -99,7 +99,7 @@ function clampToSafe(r) {
99
99
  const h = Math.min(Math.max(r[3], 10), sh);
100
100
  const x = Math.min(Math.max(r[0], sx), sx + sw - w);
101
101
  const y = Math.min(Math.max(r[1], sy), sy + sh - h);
102
- const round = (n) => Math.round(n * 100) / 100;
102
+ const round = (n2) => Math.round(n2 * 100) / 100;
103
103
  return [round(x), round(y), round(w), round(h)];
104
104
  }
105
105
 
@@ -940,7 +940,7 @@ var TOOL_DEFS = [
940
940
  target: "gateway"
941
941
  }
942
942
  ];
943
- var clip = (s, n = 300) => s.length > n ? s.slice(0, n) + "\u2026" : s;
943
+ var clip = (s, n2 = 300) => s.length > n2 ? s.slice(0, n2) + "\u2026" : s;
944
944
  var INVISIBLE = /[\u0000-\u0008\u000B\u000C\u000D-\u001F\u007F-\u009F​-‏‪-‮⁠-⁤⁦-⁩]/g;
945
945
  var visible = (s) => s.replace(INVISIBLE, (c) => `\\u{${c.codePointAt(0).toString(16).toUpperCase()}}`);
946
946
  function describeCall(name, args, device) {
@@ -1023,9 +1023,9 @@ ${visible(clip(String(a.text ?? ""), 600))}`;
1023
1023
  case "reports.run":
1024
1024
  return a.send === true ? "Build your portfolio report and send it to your phone" : "Build your portfolio report";
1025
1025
  case "reports.schedule": {
1026
- const label3 = { telegram: "Telegram", whatsapp: "WhatsApp", sms: "SMS" }[String(a.channel)] ?? visible(String(a.channel ?? ""));
1026
+ const label4 = { telegram: "Telegram", whatsapp: "WhatsApp", sms: "SMS" }[String(a.channel)] ?? visible(String(a.channel ?? ""));
1027
1027
  const days = Array.isArray(a.days) ? a.days.map((d) => ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][Number(d)] ?? "?").join(", ") : "Mon to Fri";
1028
- return `Schedule the ${visible(String(a.kind ?? ""))} report "${visible(clip(String(a.name ?? ""), 60))}" at ${visible(String(a.time ?? ""))} (${days}). It sends your portfolio figures to ${label3}.`;
1028
+ return `Schedule the ${visible(String(a.kind ?? ""))} report "${visible(clip(String(a.name ?? ""), 60))}" at ${visible(String(a.time ?? ""))} (${days}). It sends your portfolio figures to ${label4}.`;
1029
1029
  }
1030
1030
  case "reports.remove":
1031
1031
  return `Delete the report schedule #${visible(String(a.id ?? ""))}`;
@@ -1114,8 +1114,8 @@ var MAX_LAUNCHER = 12;
1114
1114
  function launcherProblem(raw) {
1115
1115
  const r = raw && typeof raw === "object" ? raw : null;
1116
1116
  if (!r) return "A button needs a label and an action.";
1117
- const label3 = typeof r.label === "string" ? r.label.trim() : "";
1118
- if (label3.length < 1 || label3.length > 24) return "A button label is 1 to 24 characters.";
1117
+ const label4 = typeof r.label === "string" ? r.label.trim() : "";
1118
+ if (label4.length < 1 || label4.length > 24) return "A button label is 1 to 24 characters.";
1119
1119
  const icon = typeof r.icon === "string" ? r.icon.trim() : "";
1120
1120
  if (icon.length < 1 || [...icon].length > 4) return "A button icon is 1 to 4 characters (a symbol or an emoji).";
1121
1121
  const a = r.action && typeof r.action === "object" ? r.action : null;
@@ -1172,7 +1172,7 @@ function shuffled(items, random = Math.random) {
1172
1172
  }
1173
1173
  return a;
1174
1174
  }
1175
- var clamp = (n, min, max) => Math.min(max, Math.max(min, n));
1175
+ var clamp = (n2, min, max) => Math.min(max, Math.max(min, n2));
1176
1176
  var reset = (s, index, playing) => ({ ...s, index, playing, position: 0, duration: 0 });
1177
1177
  function playerReducer(s, a, env = {}) {
1178
1178
  const mix = env.shuffle ?? shuffled;
@@ -1333,8 +1333,8 @@ function formatterFor(locale2) {
1333
1333
  }
1334
1334
  return f;
1335
1335
  }
1336
- function formatMoney(n, o = DEFAULT_MONEY) {
1337
- const value = Number.isFinite(n) ? n : 0;
1336
+ function formatMoney(n2, o = DEFAULT_MONEY) {
1337
+ const value = Number.isFinite(n2) ? n2 : 0;
1338
1338
  const fixed = Math.abs(value).toFixed(2);
1339
1339
  const negative = value < 0 && fixed !== "0.00";
1340
1340
  return `${negative ? "-" : ""}${o.symbol}${formatterFor(o.locale).format(Number(fixed))}`;
@@ -1346,7 +1346,7 @@ var WHY = { rate: "Zerodha's rate limit was hit, try again in a minute", session
1346
1346
  var asOf = (at, zone) => new Intl.DateTimeFormat("en-GB", { timeZone: zone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(new Date(at));
1347
1347
  var rows = (items, line) => [...items.slice(0, MAX_ROWS).map(line), ...items.length > MAX_ROWS ? [`...and ${items.length - MAX_ROWS} more`] : []];
1348
1348
  function partText(part, snap, zone, money = DEFAULT_MONEY) {
1349
- const formatInr = (n) => formatMoney(n, money);
1349
+ const formatInr = (n2) => formatMoney(n2, money);
1350
1350
  const section2 = snap[part];
1351
1351
  if (!section2.ok) return `Could not read the ${part}: ${WHY[section2.kind] ?? section2.error.slice(0, 100)}.`;
1352
1352
  const head = `Zerodha ${part} as of ${asOf(snap.at, zone)}:`;
@@ -1378,7 +1378,7 @@ Available ${formatInr(f.available)}, used ${formatInr(f.used)}, total ${formatIn
1378
1378
  import { createHash } from "node:crypto";
1379
1379
 
1380
1380
  // ../server/src/stats.ts
1381
- var clampPct = (n) => Math.min(100, Math.max(0, Math.round(n)));
1381
+ var clampPct = (n2) => Math.min(100, Math.max(0, Math.round(n2)));
1382
1382
  function used(line) {
1383
1383
  const m = line && /([\d.]+) GB free of ([\d.]+) GB/.exec(line);
1384
1384
  if (!m) return null;
@@ -1629,7 +1629,7 @@ function nextSlot(due, step, after) {
1629
1629
  return next > after ? next : due + (Math.floor((after - due) / step) + 1) * step;
1630
1630
  }
1631
1631
  function localIso(d) {
1632
- const p = (n, w = 2) => String(Math.abs(n)).padStart(w, "0");
1632
+ const p = (n2, w = 2) => String(Math.abs(n2)).padStart(w, "0");
1633
1633
  const off = -d.getTimezoneOffset();
1634
1634
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}${off >= 0 ? "+" : "-"}${p(Math.floor(Math.abs(off) / 60))}:${p(Math.abs(off) % 60)}`;
1635
1635
  }
@@ -1667,10 +1667,10 @@ var Tasks = class {
1667
1667
  return rows3.map(toRow);
1668
1668
  }
1669
1669
  done(id2) {
1670
- const row = this.db.prepare(`select ${COLUMNS} from tasks where id = ? and status = 'open'`).get(id2);
1671
- if (!row) return { ok: false, error: "No open task with that number." };
1672
- if (row.repeat && row.due_at !== null) {
1673
- this.db.prepare("update tasks set due_at = ?, fired_at = null where id = ?").run(nextSlot(row.due_at, STEP[row.repeat], this.clock()), id2);
1670
+ const row2 = this.db.prepare(`select ${COLUMNS} from tasks where id = ? and status = 'open'`).get(id2);
1671
+ if (!row2) return { ok: false, error: "No open task with that number." };
1672
+ if (row2.repeat && row2.due_at !== null) {
1673
+ this.db.prepare("update tasks set due_at = ?, fired_at = null where id = ?").run(nextSlot(row2.due_at, STEP[row2.repeat], this.clock()), id2);
1674
1674
  return { ok: true, rescheduled: true };
1675
1675
  }
1676
1676
  this.db.prepare("update tasks set status = 'done' where id = ?").run(id2);
@@ -1686,10 +1686,10 @@ var Tasks = class {
1686
1686
  }
1687
1687
  /** The owner has been told. A repeating task moves to its next slot after now (missed slots are skipped, never replayed). */
1688
1688
  markFired(id2) {
1689
- const row = this.db.prepare(`select ${COLUMNS} from tasks where id = ?`).get(id2);
1690
- if (!row) return;
1691
- if (row.repeat && row.due_at !== null) {
1692
- this.db.prepare("update tasks set due_at = ?, fired_at = null where id = ?").run(nextSlot(row.due_at, STEP[row.repeat], this.clock()), id2);
1689
+ const row2 = this.db.prepare(`select ${COLUMNS} from tasks where id = ?`).get(id2);
1690
+ if (!row2) return;
1691
+ if (row2.repeat && row2.due_at !== null) {
1692
+ this.db.prepare("update tasks set due_at = ?, fired_at = null where id = ?").run(nextSlot(row2.due_at, STEP[row2.repeat], this.clock()), id2);
1693
1693
  } else {
1694
1694
  this.db.prepare("update tasks set fired_at = ? where id = ?").run(this.clock(), id2);
1695
1695
  }
@@ -1974,7 +1974,7 @@ function regionUse(objects) {
1974
1974
  });
1975
1975
  if (hits.length) used2.set(name, hits);
1976
1976
  }
1977
- return { used: used2, free: REGION_NAMES.filter((n) => !used2.has(n)) };
1977
+ return { used: used2, free: REGION_NAMES.filter((n2) => !used2.has(n2)) };
1978
1978
  }
1979
1979
  function describeScreen(scene, presence, entries, now, panels = [], screen = null, view, accent) {
1980
1980
  const open2 = panels.length ? `; the owner has open: ${panels.map((p) => PANEL_TEXT[p]).join(", ")}, so keep clear of them` : "";
@@ -2572,6 +2572,7 @@ var Agent = class {
2572
2572
  return;
2573
2573
  }
2574
2574
  this.busy = true;
2575
+ const spent = { input: 0, output: 0 };
2575
2576
  this.scenesThisTurn = 0;
2576
2577
  const stage = (cmd) => {
2577
2578
  d.stage.apply(cmd);
@@ -2631,7 +2632,10 @@ var Agent = class {
2631
2632
  languageNote(text3, ctx.spoken, voiceGender(devanagariLanguage(text3) === "mr" || ctx.spoken?.toLowerCase().startsWith("mr") ? "mr" : "hi", { main: d.config.voice.ttsVoice, hindi: d.config.voice.hindiVoice || void 0, marathi: d.config.voice.marathiVoice || void 0 }))
2632
2633
  );
2633
2634
  for await (const ev of d.provider.stream({ system, ...context ? { context } : {}, session: isolated ? "guest" : "owner", messages, tools, signal: abort.signal })) {
2634
- if (ev.type === "text_delta") {
2635
+ if (ev.type === "usage") {
2636
+ spent.input += ev.input;
2637
+ spent.output += ev.output;
2638
+ } else if (ev.type === "text_delta") {
2635
2639
  if (!assistantText) {
2636
2640
  stage({ cmd: "presence", state: "speaking" });
2637
2641
  emit({ type: "activity", text: "Answering" });
@@ -2691,6 +2695,7 @@ var Agent = class {
2691
2695
  d.audit.log("system", "agent.error", { message: e.message });
2692
2696
  emit({ type: "caption.delta", text: "Sorry, something went wrong on my side." });
2693
2697
  } finally {
2698
+ if (d.usage && (spent.input > 0 || spent.output > 0)) d.usage({ source: ctx.source ?? "other", ...spent });
2694
2699
  if (member && lastAnswer) {
2695
2700
  const lines = [...earlierOf(this.memberTalk.get(ctx.person.id), d.tasks?.now() ?? Date.now()), { role: "user", content: [{ type: "text", text: text3 }] }, { role: "assistant", content: [{ type: "text", text: lastAnswer }] }];
2696
2701
  this.memberTalk.set(ctx.person.id, { at: d.tasks?.now() ?? Date.now(), lines: lines.slice(-MEMBER_MEMORY_MSGS) });
@@ -3213,8 +3218,8 @@ function sanitizeAlexa(raw) {
3213
3218
  const users = (Array.isArray(r.allowedUsers) ? r.allowedUsers : []).flatMap((u) => {
3214
3219
  const x = u && typeof u === "object" ? u : {};
3215
3220
  if (typeof x.id !== "string" || !AMAZON_USER.test(x.id)) return [];
3216
- const label3 = typeof x.name === "string" ? x.name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "";
3217
- return [{ id: x.id, name: label3 || "Echo", addedAt: typeof x.addedAt === "number" && Number.isFinite(x.addedAt) ? x.addedAt : 0 }];
3221
+ const label4 = typeof x.name === "string" ? x.name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "";
3222
+ return [{ id: x.id, name: label4 || "Echo", addedAt: typeof x.addedAt === "number" && Number.isFinite(x.addedAt) ? x.addedAt : 0 }];
3218
3223
  });
3219
3224
  const num3 = (v, o, whole = true) => typeof v === "number" && Number.isFinite(v) ? Math.min(o.max, Math.max(o.min, whole ? Math.round(v) : Math.round(v * 10) / 10)) : o.def;
3220
3225
  const locales = [...new Set((Array.isArray(r.locales) ? r.locales : []).filter((l) => ALEXA_LOCALES.includes(l)))];
@@ -3347,6 +3352,7 @@ function sanitizeLook(raw) {
3347
3352
  };
3348
3353
  }
3349
3354
  var DEFAULT_OLLAMA = { url: "http://127.0.0.1:11434", model: "qwen2.5:7b", contextTokens: 8192, vision: false };
3355
+ var sanitizeUpdates = (u) => ({ auto: typeof u?.auto === "boolean" ? u.auto : true });
3350
3356
  var DEFAULT_VOICE = {
3351
3357
  enabled: true,
3352
3358
  sttModel: "small",
@@ -3361,7 +3367,7 @@ var DEFAULT_VOICE = {
3361
3367
  duckPercent: 25
3362
3368
  };
3363
3369
  function sanitizeVoice(v) {
3364
- const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1;
3370
+ const ok = (n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 0 && n2 <= 1;
3365
3371
  const percent2 = typeof v.duckPercent === "number" && Number.isFinite(v.duckPercent) ? Math.max(0, Math.min(100, Math.round(v.duckPercent))) : DEFAULT_VOICE.duckPercent;
3366
3372
  const base = { ...v, duck: typeof v.duck === "boolean" ? v.duck : DEFAULT_VOICE.duck, duckPercent: percent2 };
3367
3373
  if (ok(v.accept) && ok(v.challenge) && v.accept > v.challenge) return base;
@@ -3412,6 +3418,7 @@ var defaults = (dataDir) => ({
3412
3418
  openai: { ...DEFAULT_OPENAI },
3413
3419
  claudeCode: { ...DEFAULT_CLAUDE_CODE },
3414
3420
  voice: { ...DEFAULT_VOICE },
3421
+ updates: sanitizeUpdates(void 0),
3415
3422
  view: "companion",
3416
3423
  look: { ...DEFAULT_LOOK },
3417
3424
  ownerName: "",
@@ -3458,6 +3465,7 @@ function loadConfig(dataDir = defaultDataDir()) {
3458
3465
  openai: { ...DEFAULT_OPENAI, ...stored.openai },
3459
3466
  claudeCode: sanitizeClaudeCode(stored.claudeCode),
3460
3467
  voice: sanitizeVoice({ ...DEFAULT_VOICE, ...stored.voice }),
3468
+ updates: sanitizeUpdates(stored.updates),
3461
3469
  view: stored.view === "minimal" || stored.view === "workspace" ? stored.view : "companion",
3462
3470
  look: sanitizeLook(stored.look),
3463
3471
  ownerName: typeof stored.ownerName === "string" ? stored.ownerName.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "",
@@ -3554,7 +3562,7 @@ function integrationViews(config, hub, brokers, alexa, spotify, soundcloud, audi
3554
3562
  const vdNeeds = () => {
3555
3563
  if (!vs) return [];
3556
3564
  const need = [{ name: "yt-dlp", h: vs.ytdlp }, ...vdMpvWindow ? [] : [{ name: "ffmpeg", h: vs.ffmpeg }]];
3557
- return need.filter((n) => !n.h.found).map((n) => ({ name: n.name, ...n.h.install ? { install: n.h.install } : {} }));
3565
+ return need.filter((n2) => !n2.h.found).map((n2) => ({ name: n2.name, ...n2.h.install ? { install: n2.h.install } : {} }));
3558
3566
  };
3559
3567
  const vdSteps = [
3560
3568
  { text: "Friend gets the helper programs (yt-dlp and ffmpeg) by itself when you turn the plugin on: nothing to install.", done: !!vs && vdNeeds().length === 0 },
@@ -3894,7 +3902,7 @@ function inspectPlugin(dir) {
3894
3902
  }
3895
3903
  const manifest = parsed.data;
3896
3904
  const seen = (names, kind) => {
3897
- const dup2 = names.find((n, i) => names.indexOf(n) !== i);
3905
+ const dup2 = names.find((n2, i) => names.indexOf(n2) !== i);
3898
3906
  return dup2 ? `The ${kind} name "${dup2}" is used twice.` : null;
3899
3907
  };
3900
3908
  const dup = seen(manifest.skills.map((s) => s.name), "skill") ?? seen(manifest.tools.map((t) => t.name), "tool");
@@ -4314,9 +4322,9 @@ var ConnectorStore = class {
4314
4322
  return true;
4315
4323
  }
4316
4324
  remove(id2) {
4317
- const n = this.items.length;
4325
+ const n2 = this.items.length;
4318
4326
  this.items = this.items.filter((i) => i.id !== id2);
4319
- if (this.items.length === n) return false;
4327
+ if (this.items.length === n2) return false;
4320
4328
  this.save();
4321
4329
  return true;
4322
4330
  }
@@ -4336,10 +4344,10 @@ var hasSecrets = (c) => c.kind === "stdio" ? Object.keys(c.env).length > 0 : Obj
4336
4344
  var MAX_RESULT_BYTES = 1024 * 1024;
4337
4345
  var MAX_TOOL_PART = 28;
4338
4346
  function friendlyToolName(original, taken) {
4339
- let n = original.replace(/[^A-Za-z0-9_-]/g, "-").replace(/_{2,}/g, "_").replace(/^[-_]+/, "") || "tool";
4340
- if (n.length > MAX_TOOL_PART) n = `${n.slice(0, MAX_TOOL_PART - 7)}_${createHash2("sha1").update(original).digest("hex").slice(0, 5)}`;
4341
- let out = n;
4342
- for (let i = 2; taken.has(out); i++) out = `${n.slice(0, MAX_TOOL_PART - 3)}_${i}`;
4347
+ let n2 = original.replace(/[^A-Za-z0-9_-]/g, "-").replace(/_{2,}/g, "_").replace(/^[-_]+/, "") || "tool";
4348
+ if (n2.length > MAX_TOOL_PART) n2 = `${n2.slice(0, MAX_TOOL_PART - 7)}_${createHash2("sha1").update(original).digest("hex").slice(0, 5)}`;
4349
+ let out = n2;
4350
+ for (let i = 2; taken.has(out); i++) out = `${n2.slice(0, MAX_TOOL_PART - 3)}_${i}`;
4343
4351
  taken.add(out);
4344
4352
  return out;
4345
4353
  }
@@ -4884,9 +4892,9 @@ function chromeToken(home = homedir3()) {
4884
4892
  return null;
4885
4893
  }
4886
4894
  }
4887
- function sameKey(given, real2) {
4888
- if (!given || !real2) return false;
4889
- return timingSafeEqual(createHash3("sha256").update(given).digest(), createHash3("sha256").update(real2).digest());
4895
+ function sameKey(given, real3) {
4896
+ if (!given || !real3) return false;
4897
+ return timingSafeEqual(createHash3("sha256").update(given).digest(), createHash3("sha256").update(real3).digest());
4890
4898
  }
4891
4899
  function keyFromRequest(headers, search) {
4892
4900
  const bearer = /^Bearer (.+)$/i.exec(headers.authorization ?? "")?.[1];
@@ -4975,10 +4983,10 @@ import { join as join9 } from "node:path";
4975
4983
  import { deflateRawSync } from "node:zlib";
4976
4984
  var TABLE = (() => {
4977
4985
  const t = new Uint32Array(256);
4978
- for (let n = 0; n < 256; n++) {
4979
- let c = n;
4986
+ for (let n2 = 0; n2 < 256; n2++) {
4987
+ let c = n2;
4980
4988
  for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
4981
- t[n] = c >>> 0;
4989
+ t[n2] = c >>> 0;
4982
4990
  }
4983
4991
  return t;
4984
4992
  })();
@@ -5541,8 +5549,8 @@ var AudiusHub = class {
5541
5549
  };
5542
5550
  }
5543
5551
  async search(query, limit) {
5544
- const n = Math.max(1, Math.min(50, Math.floor(limit)));
5545
- const j = await this.api("/v1/tracks/search", { query: query.slice(0, 200), limit: String(n) });
5552
+ const n2 = Math.max(1, Math.min(50, Math.floor(limit)));
5553
+ const j = await this.api("/v1/tracks/search", { query: query.slice(0, 200), limit: String(n2) });
5546
5554
  return (Array.isArray(j) ? j : []).flatMap((t) => this.of(t) ?? []);
5547
5555
  }
5548
5556
  async playlists(_limit) {
@@ -5575,7 +5583,7 @@ var AudiusHub = class {
5575
5583
  }
5576
5584
  }
5577
5585
  /** Fetches the audio from a checked address, following at most three redirects and only to checked addresses. */
5578
- async fetchAudio(address, range, signal) {
5586
+ async fetchAudio(address, range2, signal) {
5579
5587
  let next = address;
5580
5588
  for (let hop = 0; hop <= MAX_HOPS; hop++) {
5581
5589
  if (!isAudiusAddress(next, hop > 0) || !await this.publicName(next)) return { ok: false, error: "Audius sent the audio somewhere Friend will not follow." };
@@ -5585,7 +5593,7 @@ var AudiusHub = class {
5585
5593
  const timer = setTimeout(() => ctl.abort(), this.d.requestTimeoutMs ?? 8e3);
5586
5594
  const stop2 = signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal;
5587
5595
  try {
5588
- res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers: range ? { range } : {}, signal: stop2 });
5596
+ res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers: range2 ? { range: range2 } : {}, signal: stop2 });
5589
5597
  } finally {
5590
5598
  clearTimeout(timer);
5591
5599
  }
@@ -5839,14 +5847,14 @@ var zipExtract = async (archive, plan, dest) => {
5839
5847
  await fh.read(tail2, 0, tailLen, size - tailLen);
5840
5848
  const eocd = tail2.lastIndexOf(Buffer.from([80, 75, 5, 6]));
5841
5849
  if (eocd < 0) throw new Error("not a zip file");
5842
- const count2 = tail2.readUInt16LE(eocd + 10);
5850
+ const count3 = tail2.readUInt16LE(eocd + 10);
5843
5851
  const cdSize = tail2.readUInt32LE(eocd + 12);
5844
5852
  const cdOffset = tail2.readUInt32LE(eocd + 16);
5845
5853
  if (cdOffset === 4294967295 || cdSize === 4294967295) throw new Error("zip64 archives are not supported");
5846
5854
  const cd = Buffer.alloc(cdSize);
5847
5855
  await fh.read(cd, 0, cdSize, cdOffset);
5848
5856
  let p = 0;
5849
- for (let i = 0; i < count2 && p + 46 <= cd.length; i++) {
5857
+ for (let i = 0; i < count3 && p + 46 <= cd.length; i++) {
5850
5858
  if (cd.readUInt32LE(p) !== 33639248) break;
5851
5859
  const method = cd.readUInt16LE(p + 10);
5852
5860
  const csize = cd.readUInt32LE(p + 20);
@@ -6392,7 +6400,7 @@ var VideoHub = class {
6392
6400
  return this.tokens.has(id2);
6393
6401
  }
6394
6402
  missing(status2, names) {
6395
- return names.filter((n) => !(n === "yt-dlp" ? status2.ytdlp : status2.ffmpeg).found);
6403
+ return names.filter((n2) => !(n2 === "yt-dlp" ? status2.ytdlp : status2.ffmpeg).found);
6396
6404
  }
6397
6405
  /** Downloads what the plugin needs (if the owner allows it) and looks again. Called when the plugin is installed or switched on. */
6398
6406
  async prepare() {
@@ -6595,9 +6603,9 @@ var VideoHub = class {
6595
6603
  return { ok: true, text: "On the Friend screen the video has its own buttons for pause, play and volume: use those." };
6596
6604
  }
6597
6605
  nowPlaying() {
6598
- const n = this.now;
6599
- if (!n) return "Nothing is playing.";
6600
- return `Playing "${n.title}"${n.uploader ? ` by ${n.uploader}` : ""} ${n.output === "host" ? n.via === "browser" ? "in a browser window on this PC" : "in a window on this PC" : "on the Friend screen"}.`;
6606
+ const n2 = this.now;
6607
+ if (!n2) return "Nothing is playing.";
6608
+ return `Playing "${n2.title}"${n2.uploader ? ` by ${n2.uploader}` : ""} ${n2.output === "host" ? n2.via === "browser" ? "in a browser window on this PC" : "in a window on this PC" : "on the Friend screen"}.`;
6601
6609
  }
6602
6610
  /** What is found and what is missing, in words, for the assistant to pass on. */
6603
6611
  async check() {
@@ -6766,8 +6774,8 @@ var Auth = class _Auth {
6766
6774
  }
6767
6775
  async login(password, ip) {
6768
6776
  const succeeded = this.begin("login", ip);
6769
- const row = this.db.prepare("select password_hash h from owner where id = 1").get();
6770
- const ok = row ? await verifySecret(password, row.h) : false;
6777
+ const row2 = this.db.prepare("select password_hash h from owner where id = 1").get();
6778
+ const ok = row2 ? await verifySecret(password, row2.h) : false;
6771
6779
  if (!ok) {
6772
6780
  this.audit.log("anonymous", "login.fail", { ip });
6773
6781
  throw new Error("Invalid credentials");
@@ -6798,8 +6806,8 @@ var Auth = class _Auth {
6798
6806
  }
6799
6807
  async verifyPin(pin, ip) {
6800
6808
  const succeeded = this.begin("pin", ip);
6801
- const row = this.db.prepare("select pin_hash h from owner where id = 1").get();
6802
- const ok = row ? await verifySecret(pin, row.h) : false;
6809
+ const row2 = this.db.prepare("select pin_hash h from owner where id = 1").get();
6810
+ const ok = row2 ? await verifySecret(pin, row2.h) : false;
6803
6811
  if (ok) succeeded();
6804
6812
  else this.audit.log("owner", "pin.fail", { ip });
6805
6813
  return ok;
@@ -6823,8 +6831,8 @@ var Auth = class _Auth {
6823
6831
  const problem = _Auth.checkNewSecrets(input);
6824
6832
  if (problem) throw new Error(problem);
6825
6833
  const succeeded = this.begin("login", ip);
6826
- const row = this.db.prepare("select password_hash h from owner where id = 1").get();
6827
- const ok = row ? await verifySecret(input.current, row.h) : false;
6834
+ const row2 = this.db.prepare("select password_hash h from owner where id = 1").get();
6835
+ const ok = row2 ? await verifySecret(input.current, row2.h) : false;
6828
6836
  if (!ok) {
6829
6837
  this.audit.log("owner", "secrets.fail", { ip });
6830
6838
  throw new Error("Invalid credentials");
@@ -7120,32 +7128,32 @@ var MAX_TEXT = 2e5;
7120
7128
  var MAX_CSV_ROWS = 200;
7121
7129
  function parseCsv(text3) {
7122
7130
  const rows3 = [];
7123
- let row = [];
7131
+ let row2 = [];
7124
7132
  let cell = "";
7125
- let quoted = false;
7133
+ let quoted2 = false;
7126
7134
  for (let i = 0; i < text3.length; i++) {
7127
7135
  const ch = text3[i];
7128
- if (quoted) {
7136
+ if (quoted2) {
7129
7137
  if (ch === '"' && text3[i + 1] === '"') {
7130
7138
  cell += '"';
7131
7139
  i++;
7132
- } else if (ch === '"') quoted = false;
7140
+ } else if (ch === '"') quoted2 = false;
7133
7141
  else cell += ch;
7134
- } else if (ch === '"') quoted = true;
7142
+ } else if (ch === '"') quoted2 = true;
7135
7143
  else if (ch === ",") {
7136
- row.push(cell);
7144
+ row2.push(cell);
7137
7145
  cell = "";
7138
7146
  } else if (ch === "\n" || ch === "\r") {
7139
7147
  if (ch === "\r" && text3[i + 1] === "\n") i++;
7140
- row.push(cell);
7148
+ row2.push(cell);
7141
7149
  cell = "";
7142
- if (row.some((c) => c !== "")) rows3.push(row);
7143
- row = [];
7150
+ if (row2.some((c) => c !== "")) rows3.push(row2);
7151
+ row2 = [];
7144
7152
  if (rows3.length > MAX_CSV_ROWS + 1) return rows3;
7145
7153
  } else cell += ch;
7146
7154
  }
7147
- row.push(cell);
7148
- if (row.some((c) => c !== "")) rows3.push(row);
7155
+ row2.push(cell);
7156
+ if (row2.some((c) => c !== "")) rows3.push(row2);
7149
7157
  return rows3;
7150
7158
  }
7151
7159
  var insideOf = (dir, path3) => {
@@ -7184,59 +7192,59 @@ var MediaStore = class {
7184
7192
  /** 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
7193
  async sendable(rawPath) {
7186
7194
  if (!isAbsolute(rawPath)) return { ok: false, error: "Give the full path of the file, for example D:\\Pictures\\photo.png." };
7187
- let real2;
7195
+ let real3;
7188
7196
  try {
7189
- real2 = await fs.realpath(resolve4(rawPath));
7197
+ real3 = await fs.realpath(resolve4(rawPath));
7190
7198
  } catch {
7191
7199
  return { ok: false, error: "That file does not exist." };
7192
7200
  }
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);
7201
+ if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real3)) return { ok: false, error: "Friend's own data folder cannot be sent." };
7202
+ const stat2 = await fs.stat(real3).catch(() => null);
7195
7203
  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();
7204
+ const name = basename(real3);
7205
+ const ext = extname(real3).slice(1).toLowerCase();
7198
7206
  if (NOT_SENT.has(ext)) return { ok: false, error: `Friend cannot send .${ext} files.` };
7199
7207
  if (stat2.size === 0) return { ok: false, error: "That file is empty." };
7200
7208
  if (stat2.size > MAX_SEND) return { ok: false, error: `That file is too big to send (${Math.round(stat2.size / 1048576)} MB).` };
7201
7209
  const mime = IMAGE[ext] ?? VIDEO[ext] ?? DOC_MIME[ext] ?? "application/octet-stream";
7202
7210
  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 };
7211
+ return { ok: true, path: real3, name, mime, size: stat2.size, kind };
7204
7212
  }
7205
7213
  async present(rawPath, title2) {
7206
7214
  if (!isAbsolute(rawPath)) return { ok: false, error: "Give the full path of the file, for example D:\\Pictures\\photo.png." };
7207
- let real2;
7215
+ let real3;
7208
7216
  try {
7209
- real2 = await fs.realpath(resolve4(rawPath));
7217
+ real3 = await fs.realpath(resolve4(rawPath));
7210
7218
  } catch {
7211
7219
  return { ok: false, error: "That file does not exist." };
7212
7220
  }
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);
7221
+ if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real3)) return { ok: false, error: "Friend's own data folder cannot be shown." };
7222
+ const stat2 = await fs.stat(real3).catch(() => null);
7215
7223
  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();
7218
- const label3 = title2?.trim() || name;
7224
+ const name = basename(real3);
7225
+ const ext = extname(real3).slice(1).toLowerCase();
7226
+ const label4 = title2?.trim() || name;
7219
7227
  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
7228
  if (IMAGE[ext]) {
7221
7229
  const big = tooBig(MAX_IMAGE, "image");
7222
7230
  if (big) return big;
7223
- const id2 = this.grant(real2, IMAGE[ext], stat2.size);
7224
- return { ok: true, type: "image", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the image ${name}.` };
7231
+ const id2 = this.grant(real3, IMAGE[ext], stat2.size);
7232
+ return { ok: true, type: "image", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing the image ${name}.` };
7225
7233
  }
7226
7234
  if (VIDEO[ext]) {
7227
7235
  const big = tooBig(MAX_VIDEO, "file");
7228
7236
  if (big) return big;
7229
- const id2 = this.grant(real2, VIDEO[ext], stat2.size);
7230
- return { ok: true, type: "video", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing ${name}.` };
7237
+ const id2 = this.grant(real3, VIDEO[ext], stat2.size);
7238
+ return { ok: true, type: "video", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing ${name}.` };
7231
7239
  }
7232
7240
  if (ext === "pdf") {
7233
7241
  const big = tooBig(MAX_PDF, "PDF");
7234
7242
  if (big) return big;
7235
- const id2 = this.grant(real2, "application/pdf", stat2.size);
7236
- return { ok: true, type: "pdf", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the PDF ${name}.` };
7243
+ const id2 = this.grant(real3, "application/pdf", stat2.size);
7244
+ return { ok: true, type: "pdf", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing the PDF ${name}.` };
7237
7245
  }
7238
7246
  if (MARKDOWN.has(ext) || ext === "csv" || CODE_LANG[ext]) {
7239
- const handle = await fs.open(real2, "r");
7247
+ const handle = await fs.open(real3, "r");
7240
7248
  let text3;
7241
7249
  try {
7242
7250
  const buf = Buffer.alloc(Math.min(stat2.size, MAX_TEXT));
@@ -7250,13 +7258,13 @@ var MediaStore = class {
7250
7258
  const note = cut ? `
7251
7259
 
7252
7260
  [Only the first ${MAX_TEXT} characters are shown.]` : "";
7253
- if (MARKDOWN.has(ext)) return { ok: true, type: "markdown", props: { title: label3, text: text3 + note }, summary: `Showing ${name}.${cut ? " It is long, so only the start is shown." : ""}` };
7261
+ if (MARKDOWN.has(ext)) return { ok: true, type: "markdown", props: { title: label4, text: text3 + note }, summary: `Showing ${name}.${cut ? " It is long, so only the start is shown." : ""}` };
7254
7262
  if (ext === "csv") {
7255
7263
  const rows3 = parseCsv(text3);
7256
7264
  const [head = [], ...body] = rows3;
7257
- return { ok: true, type: "table", props: { title: label3, columns: head, rows: body.slice(0, MAX_CSV_ROWS) }, summary: `Showing ${name} as a table (${Math.min(body.length, MAX_CSV_ROWS)} rows).` };
7265
+ return { ok: true, type: "table", props: { title: label4, columns: head, rows: body.slice(0, MAX_CSV_ROWS) }, summary: `Showing ${name} as a table (${Math.min(body.length, MAX_CSV_ROWS)} rows).` };
7258
7266
  }
7259
- return { ok: true, type: "code", props: { title: label3, language: CODE_LANG[ext], code: text3 + note.replace(/^\n\n/, "\n") }, summary: `Showing ${name}.${cut ? " It is long, so only the start is shown." : ""}` };
7267
+ return { ok: true, type: "code", props: { title: label4, language: CODE_LANG[ext], code: text3 + note.replace(/^\n\n/, "\n") }, summary: `Showing ${name}.${cut ? " It is long, so only the start is shown." : ""}` };
7260
7268
  }
7261
7269
  return { ok: false, error: `Friend cannot show .${ext || "?"} files. It can show images, GIFs, video, sound, PDF, Markdown, text, CSV and code.` };
7262
7270
  }
@@ -7311,7 +7319,7 @@ async function readLimited(res, max) {
7311
7319
  }
7312
7320
  return Buffer.concat(chunks);
7313
7321
  }
7314
- var clip2 = (s, n) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
7322
+ var clip2 = (s, n2) => s.length > n2 ? `${s.slice(0, n2)}\u2026` : s;
7315
7323
  var plain = (s) => s.replace(/[\u0000-\u001f\u007f‪-‮⁦-⁩]/g, " ");
7316
7324
  var digits = (s) => s.replace(/\D/g, "");
7317
7325
  var ChannelHub = class _ChannelHub {
@@ -7810,8 +7818,8 @@ var AlexaDevices = class {
7810
7818
  const matches = [...this.waitingMap.values()].filter((w2) => short.length === SHORT && w2.id.endsWith(short));
7811
7819
  if (matches.length !== 1 || this.store.config().allowedUsers.length >= this.store.config().maxDevices) return false;
7812
7820
  const w = matches[0];
7813
- const label3 = name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) || "Echo";
7814
- this.store.update((a) => a.allowedUsers.push({ id: w.id, name: label3, addedAt: this.clock() }));
7821
+ const label4 = name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) || "Echo";
7822
+ this.store.update((a) => a.allowedUsers.push({ id: w.id, name: label4, addedAt: this.clock() }));
7815
7823
  this.waitingMap.delete(w.id);
7816
7824
  return true;
7817
7825
  }
@@ -8361,7 +8369,7 @@ var clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).pad
8361
8369
  var ref = (t) => ({ id: t.id, title: t.title, artist: t.artist, album: t.album, durationSec: t.durationSec });
8362
8370
  var refOfSource = (t) => ({ id: t.source === "local" ? parseRef(t.ref)?.id ?? t.ref : t.ref, title: t.title, artist: t.artist, album: t.album, durationSec: t.durationSec });
8363
8371
  var label2 = (t) => t.artist ? `${t.title} by ${t.artist}` : t.title;
8364
- var plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
8372
+ var plural = (n2, w) => `${n2} ${w}${n2 === 1 ? "" : "s"}`;
8365
8373
  var SERVICE_NAMES = { spotify: "Spotify", soundcloud: "SoundCloud", audius: "Audius" };
8366
8374
  var MusicHub = class {
8367
8375
  constructor(d) {
@@ -8452,10 +8460,10 @@ var MusicHub = class {
8452
8460
  const mode = a.mode ?? "replace";
8453
8461
  const failed = await this.send({ action: "play", tracks: tracks.map(refOfSource), mode, ...a.shuffle ? { shuffle: true } : {} });
8454
8462
  if (failed) return failed;
8455
- const n = tracks.length;
8463
+ const n2 = tracks.length;
8456
8464
  const first2 = tracks[0];
8457
8465
  const at = this.at();
8458
- const base = mode === "add" ? `Added ${plural(n, "track")} to the queue${at}.` : mode === "next" ? `Queued ${plural(n, "track")} to play next${at}, starting with ${label2(first2)}.` : `Playing ${plural(n, "track")}${at}${a.shuffle ? " shuffled" : ", starting with " + label2(first2)}.`;
8466
+ const base = mode === "add" ? `Added ${plural(n2, "track")} to the queue${at}.` : mode === "next" ? `Queued ${plural(n2, "track")} to play next${at}, starting with ${label2(first2)}.` : `Playing ${plural(n2, "track")}${at}${a.shuffle ? " shuffled" : ", starting with " + label2(first2)}.`;
8459
8467
  return { ok: true, text: [base, ...notes].join(" ") };
8460
8468
  }
8461
8469
  async playLocal(a, words) {
@@ -8467,11 +8475,11 @@ var MusicHub = class {
8467
8475
  const failed = await this.send({ action: "play", tracks: found.tracks.map(ref), mode, ...a.shuffle ? { shuffle: true } : {} });
8468
8476
  if (failed) return failed;
8469
8477
  const first2 = found.tracks[0];
8470
- const n = found.tracks.length;
8478
+ const n2 = found.tracks.length;
8471
8479
  const at = this.at();
8472
- if (mode === "add") return { ok: true, text: `Added ${plural(n, "track")} to the queue${at}.` };
8473
- if (mode === "next") return { ok: true, text: `Queued ${plural(n, "track")} to play next${at}, starting with ${label2(first2)}.` };
8474
- return { ok: true, text: `Playing ${plural(n, "track")}${at}${a.shuffle ? " shuffled" : ", starting with " + label2(first2)}.` };
8480
+ if (mode === "add") return { ok: true, text: `Added ${plural(n2, "track")} to the queue${at}.` };
8481
+ if (mode === "next") return { ok: true, text: `Queued ${plural(n2, "track")} to play next${at}, starting with ${label2(first2)}.` };
8482
+ return { ok: true, text: `Playing ${plural(n2, "track")}${at}${a.shuffle ? " shuffled" : ", starting with " + label2(first2)}.` };
8475
8483
  }
8476
8484
  async control(a) {
8477
8485
  const bad = (text3) => ({ ok: false, text: text3 });
@@ -8835,8 +8843,8 @@ var SoundCloudHub = class {
8835
8843
  };
8836
8844
  }
8837
8845
  async search(query, limit) {
8838
- const n = Math.max(1, Math.min(50, Math.floor(limit)));
8839
- const j = await this.api(`/tracks?${new URLSearchParams({ q: query.slice(0, 200), limit: String(n), access: "playable" })}`);
8846
+ const n2 = Math.max(1, Math.min(50, Math.floor(limit)));
8847
+ const j = await this.api(`/tracks?${new URLSearchParams({ q: query.slice(0, 200), limit: String(n2), access: "playable" })}`);
8840
8848
  return (Array.isArray(j) ? j : []).flatMap((t) => this.of(t) ?? []);
8841
8849
  }
8842
8850
  async playlists(limit) {
@@ -8867,7 +8875,7 @@ var SoundCloudHub = class {
8867
8875
  }
8868
8876
  }
8869
8877
  /** Fetches the audio from a vetted address, following at most three redirects and only to SoundCloud's own https addresses. */
8870
- async fetchAudio(address, range, signal) {
8878
+ async fetchAudio(address, range2, signal) {
8871
8879
  let next = address;
8872
8880
  for (let hop = 0; hop <= MAX_HOPS3; hop++) {
8873
8881
  if (!isSoundCloudAddress(next)) return { ok: false, error: "SoundCloud sent the audio somewhere Friend will not follow." };
@@ -8877,7 +8885,7 @@ var SoundCloudHub = class {
8877
8885
  const timer = setTimeout(() => ctl.abort(), this.d.requestTimeoutMs ?? 8e3);
8878
8886
  const stop2 = signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal;
8879
8887
  try {
8880
- res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers: range ? { range } : {}, signal: stop2 });
8888
+ res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers: range2 ? { range: range2 } : {}, signal: stop2 });
8881
8889
  } finally {
8882
8890
  clearTimeout(timer);
8883
8891
  }
@@ -9400,8 +9408,8 @@ var MusicLibrary = class {
9400
9408
  try {
9401
9409
  const info = await lstat(path3);
9402
9410
  if (!info.isFile() || info.isSymbolicLink()) return null;
9403
- const real2 = await realpath(path3);
9404
- return real2.startsWith(this.root + sep2) ? path3 : null;
9411
+ const real3 = await realpath(path3);
9412
+ return real3.startsWith(this.root + sep2) ? path3 : null;
9405
9413
  } catch {
9406
9414
  return null;
9407
9415
  }
@@ -9736,8 +9744,8 @@ var SpotifyHub = class {
9736
9744
  };
9737
9745
  }
9738
9746
  async search(query, limit) {
9739
- const n = Math.max(1, Math.min(50, Math.floor(limit)));
9740
- const j = await this.api(`/v1/search?${new URLSearchParams({ q: query.slice(0, 200), type: "track", limit: String(n) })}`);
9747
+ const n2 = Math.max(1, Math.min(50, Math.floor(limit)));
9748
+ const j = await this.api(`/v1/search?${new URLSearchParams({ q: query.slice(0, 200), type: "track", limit: String(n2) })}`);
9741
9749
  return (j?.tracks?.items ?? []).flatMap((t) => this.of(t) ?? []);
9742
9750
  }
9743
9751
  async playlists(limit) {
@@ -9757,12 +9765,12 @@ var SpotifyHub = class {
9757
9765
  };
9758
9766
 
9759
9767
  // ../server/src/alexa/fast.ts
9760
- var rupees = (n, lang, o = {}) => {
9768
+ var rupees = (n2, lang, o = {}) => {
9761
9769
  const word2 = o.currencyName ?? "rupees";
9762
9770
  const spoken = lang === "hi" && word2 === "rupees" ? "\u0930\u0941\u092A\u092F\u0947" : word2;
9763
- return `${formatMoney(Math.abs(n), { symbol: "", locale: (o.money ?? DEFAULT_MONEY).locale }).replace(/[.,]00$/, "")} ${spoken}`;
9771
+ return `${formatMoney(Math.abs(n2), { symbol: "", locale: (o.money ?? DEFAULT_MONEY).locale }).replace(/[.,]00$/, "")} ${spoken}`;
9764
9772
  };
9765
- var pct = (n) => String(Math.round(Math.abs(n) * 100) / 100);
9773
+ var pct = (n2) => String(Math.round(Math.abs(n2) * 100) / 100);
9766
9774
  function portfolioSpeech(s, lang, o = {}) {
9767
9775
  if (s.value === 0 && s.invested === 0) return lang === "hi" ? "\u0906\u092A\u0915\u0947 \u0916\u093E\u0924\u0947 \u092E\u0947\u0902 \u0905\u092D\u0940 \u0915\u094B\u0908 \u0939\u094B\u0932\u094D\u0921\u093F\u0902\u0917 \u0928\u0939\u0940\u0902 \u0939\u0948\u0964" : "You have no holdings right now.";
9768
9776
  const up = s.dayChange >= 0;
@@ -9799,7 +9807,7 @@ var BrokerError = class extends Error {
9799
9807
  // ../server/src/brokers/portfolio.ts
9800
9808
  var MOVERS = 3;
9801
9809
  var TOP = 5;
9802
- var r2 = (n) => Number.isFinite(n) ? Math.round((n + Number.EPSILON) * 100) / 100 : 0;
9810
+ var r2 = (n2) => Number.isFinite(n2) ? Math.round((n2 + Number.EPSILON) * 100) / 100 : 0;
9803
9811
  function summarize(holdings) {
9804
9812
  let value = 0;
9805
9813
  let invested = 0;
@@ -9871,7 +9879,7 @@ async function section(supported, read) {
9871
9879
  }
9872
9880
 
9873
9881
  // ../server/src/brokers/report.ts
9874
- var signedMoney = (n, m) => n > 4e-3 ? `+${formatMoney(n, m)}` : formatMoney(n, m);
9882
+ var signedMoney = (n2, m) => n2 > 4e-3 ? `+${formatMoney(n2, m)}` : formatMoney(n2, m);
9875
9883
  var signedPct = (p) => {
9876
9884
  const x = Number.isFinite(p) ? p : 0;
9877
9885
  const text3 = Math.abs(x).toFixed(2);
@@ -9880,7 +9888,7 @@ var signedPct = (p) => {
9880
9888
  var pct1 = (p) => Math.abs(p).toFixed(1);
9881
9889
  var TITLE = { morning: "Morning report", close: "Market close report", custom: "Portfolio report" };
9882
9890
  var WHY2 = { rate: "rate limit", session: "login expired", network: "network problem", auth: "login problem" };
9883
- var missing = (label3, s) => `${label3}: not available (${WHY2[s.kind] ?? s.error.slice(0, 60)})`;
9891
+ var missing = (label4, s) => `${label4}: not available (${WHY2[s.kind] ?? s.error.slice(0, 60)})`;
9884
9892
  var DEFAULT_REPORT_OPTIONS = { money: DEFAULT_MONEY, concentrationPct: 30, bigDropPct: 5 };
9885
9893
  function title(kind, broker, now, zone) {
9886
9894
  const parts = Object.fromEntries(
@@ -9892,8 +9900,8 @@ var dayMovePct = (h) => h.closePrice > 0 ? (h.lastPrice - h.closePrice) / h.clos
9892
9900
  function buildReport(input) {
9893
9901
  const { snapshot: snap, caps } = input;
9894
9902
  const o = input.options ?? DEFAULT_REPORT_OPTIONS;
9895
- const fmt = (n) => formatMoney(n, o.money);
9896
- const signed = (n) => signedMoney(n, o.money);
9903
+ const fmt = (n2) => formatMoney(n2, o.money);
9904
+ const signed = (n2) => signedMoney(n2, o.money);
9897
9905
  const out = [title(input.kind, input.broker, input.now, input.zone ?? "Asia/Kolkata"), ""];
9898
9906
  if (caps.holdings) {
9899
9907
  if (!snap.holdings.ok) out.push(missing("Holdings", snap.holdings));
@@ -10028,10 +10036,10 @@ var ReportSchedules = class {
10028
10036
  }
10029
10037
  /** Counts one try for this schedule on this local day and returns how many there have been today (including this one). */
10030
10038
  noteAttempt(id2, day) {
10031
- const row = this.db.prepare("select attempt_day d, attempts n from report_schedules where id = ?").get(id2);
10032
- const n = row && row.d === day ? row.n + 1 : 1;
10033
- this.db.prepare("update report_schedules set attempt_day = ?, attempts = ? where id = ?").run(day, n, id2);
10034
- return n;
10039
+ const row2 = this.db.prepare("select attempt_day d, attempts n from report_schedules where id = ?").get(id2);
10040
+ const n2 = row2 && row2.d === day ? row2.n + 1 : 1;
10041
+ this.db.prepare("update report_schedules set attempt_day = ?, attempts = ? where id = ?").run(day, n2, id2);
10042
+ return n2;
10035
10043
  }
10036
10044
  };
10037
10045
 
@@ -10113,8 +10121,8 @@ function nextSixIst(now) {
10113
10121
  return today > now ? today : today + DAY_MS;
10114
10122
  }
10115
10123
  var num2 = (v) => {
10116
- const n = Number(v ?? 0);
10117
- return Number.isFinite(n) ? n : 0;
10124
+ const n2 = Number(v ?? 0);
10125
+ return Number.isFinite(n2) ? n2 : 0;
10118
10126
  };
10119
10127
  var str = (v) => typeof v === "string" ? v : v === void 0 || v === null ? "" : String(v);
10120
10128
  var rows2 = (v) => Array.isArray(v) ? v : [];
@@ -10461,7 +10469,7 @@ var CodeBox = class {
10461
10469
  if (input.code.length > MAX_CODE_CHARS) return { ok: false, error: `The code is too long (the limit is ${MAX_CODE_CHARS} characters).` };
10462
10470
  const extra = Object.entries(input.files ?? {});
10463
10471
  if (extra.length > MAX_EXTRA_FILES) return { ok: false, error: "Too many files for the sandbox." };
10464
- if (extra.some(([n]) => !/^[A-Za-z0-9_.-]{1,64}$/.test(n) || n.startsWith("..") || Object.values(SETUP).some((s) => s.file === n))) {
10472
+ if (extra.some(([n2]) => !/^[A-Za-z0-9_.-]{1,64}$/.test(n2) || n2.startsWith("..") || Object.values(SETUP).some((s) => s.file === n2))) {
10465
10473
  return { ok: false, error: "That file name is not allowed in the sandbox." };
10466
10474
  }
10467
10475
  if (extra.reduce((sum, [, body]) => sum + body.length, 0) > MAX_EXTRA_FILE_CHARS) return { ok: false, error: "The sandbox files are too large." };
@@ -10608,7 +10616,7 @@ var Memory = class {
10608
10616
  };
10609
10617
 
10610
10618
  // ../server/src/app.ts
10611
- import { dirname as dirname5, extname as extname3, resolve as resolve5 } from "node:path";
10619
+ import { dirname as dirname6, extname as extname3, resolve as resolve5 } from "node:path";
10612
10620
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10613
10621
  import { join as join18 } from "node:path";
10614
10622
 
@@ -10796,6 +10804,22 @@ create table report_schedules (
10796
10804
  attempt_day text,
10797
10805
  attempts integer not null default 0
10798
10806
  );
10807
+ `
10808
+ },
10809
+ {
10810
+ id: 11,
10811
+ sql: `
10812
+ create table usage (
10813
+ id integer primary key autoincrement,
10814
+ at integer not null,
10815
+ day text not null,
10816
+ provider text not null,
10817
+ model text not null,
10818
+ source text not null,
10819
+ input integer not null,
10820
+ output integer not null
10821
+ );
10822
+ create index usage_day on usage (day);
10799
10823
  `
10800
10824
  }
10801
10825
  ];
@@ -10820,6 +10844,77 @@ function openDb(dataDir) {
10820
10844
  return db;
10821
10845
  }
10822
10846
 
10847
+ // ../server/src/usage.ts
10848
+ var USAGE_SOURCES = ["voice", "chat", "task", "channel"];
10849
+ var DAY_MS2 = 864e5;
10850
+ var USAGE_KEEP_DAYS = 400;
10851
+ var pad = (n2) => String(n2).padStart(2, "0");
10852
+ function localDay2(ms2) {
10853
+ const d = new Date(ms2);
10854
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
10855
+ }
10856
+ var count = (n2) => Number.isFinite(n2) && n2 > 0 ? Math.floor(n2) : 0;
10857
+ var label3 = (s, fallback) => {
10858
+ const t = String(s ?? "").trim().slice(0, 80);
10859
+ return /^[\w.:\/@+\- ]+$/.test(t) ? t : fallback;
10860
+ };
10861
+ function range(days) {
10862
+ if (!Number.isFinite(days)) return 30;
10863
+ return Math.min(366, Math.max(1, Math.floor(days)));
10864
+ }
10865
+ var UsageLog = class {
10866
+ constructor(db, now = Date.now) {
10867
+ this.db = db;
10868
+ this.now = now;
10869
+ }
10870
+ db;
10871
+ now;
10872
+ record(e) {
10873
+ const input = count(e.input);
10874
+ const output = count(e.output);
10875
+ if (input === 0 && output === 0) return;
10876
+ const at = this.now();
10877
+ const source = USAGE_SOURCES.includes(e.source) ? e.source : "other";
10878
+ this.db.prepare("insert into usage (at, day, provider, model, source, input, output) values (?, ?, ?, ?, ?, ?, ?)").run(at, localDay2(at), label3(e.provider, "unknown"), label3(e.model, "unknown"), source, input, output);
10879
+ }
10880
+ /** The first day (local) that a range of `n` days ending today covers. */
10881
+ since(n2) {
10882
+ return localDay2(this.now() - (n2 - 1) * DAY_MS2);
10883
+ }
10884
+ days(n2) {
10885
+ const r = range(n2);
10886
+ const rows3 = this.db.prepare("select day, sum(input) as input, sum(output) as output, count(*) as turns from usage where day >= ? group by day").all(this.since(r));
10887
+ const by = new Map(rows3.map((x) => [x.day, x]));
10888
+ const out = [];
10889
+ for (let i = r - 1; i >= 0; i--) {
10890
+ const day = localDay2(this.now() - i * DAY_MS2);
10891
+ out.push(by.get(day) ?? { day, input: 0, output: 0, turns: 0 });
10892
+ }
10893
+ return out;
10894
+ }
10895
+ totals(n2) {
10896
+ const r = this.db.prepare("select coalesce(sum(input), 0) as input, coalesce(sum(output), 0) as output, count(*) as turns from usage where day >= ?").get(this.since(range(n2)));
10897
+ return { input: r.input, output: r.output, turns: r.turns };
10898
+ }
10899
+ byModel(n2) {
10900
+ return this.db.prepare("select provider, model, sum(input) as input, sum(output) as output, count(*) as turns from usage where day >= ? group by provider, model order by sum(input) + sum(output) desc, model").all(this.since(range(n2)));
10901
+ }
10902
+ bySource(n2) {
10903
+ return this.db.prepare("select source, sum(input) as input, sum(output) as output, count(*) as turns from usage where day >= ? group by source order by sum(input) + sum(output) desc, source").all(this.since(range(n2)));
10904
+ }
10905
+ summary(n2) {
10906
+ const r = range(n2);
10907
+ return { range: r, days: this.days(r), total: this.totals(r), byModel: this.byModel(r), bySource: this.bySource(r) };
10908
+ }
10909
+ /** Removes rows older than `keepDays` days. Returns how many went. */
10910
+ prune(keepDays) {
10911
+ return this.db.prepare("delete from usage where day < ?").run(this.since(Math.max(1, Math.floor(keepDays)))).changes;
10912
+ }
10913
+ clear() {
10914
+ return this.db.prepare("delete from usage").run().changes;
10915
+ }
10916
+ };
10917
+
10823
10918
  // ../server/src/devices.ts
10824
10919
  import { randomUUID as randomUUID5 } from "node:crypto";
10825
10920
  var DeviceHub = class {
@@ -11193,10 +11288,10 @@ function createGateway(deps) {
11193
11288
  const size = stat2.size;
11194
11289
  reply.header("accept-ranges", "bytes").header("x-content-type-options", "nosniff").header("cache-control", "private, max-age=300");
11195
11290
  if (grant.mime === "image/svg+xml") reply.header("content-security-policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox");
11196
- const range = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
11197
- if (range && (range[1] || range[2])) {
11198
- let start2 = range[1] ? Number(range[1]) : Math.max(0, size - Number(range[2]));
11199
- let end = range[1] && range[2] ? Number(range[2]) : size - 1;
11291
+ const range2 = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
11292
+ if (range2 && (range2[1] || range2[2])) {
11293
+ let start2 = range2[1] ? Number(range2[1]) : Math.max(0, size - Number(range2[2]));
11294
+ let end = range2[1] && range2[2] ? Number(range2[2]) : size - 1;
11200
11295
  end = Math.min(end, size - 1);
11201
11296
  if (!(start2 <= end) || start2 >= size) return reply.code(416).header("content-range", `bytes */${size}`).send();
11202
11297
  start2 = Math.max(0, start2);
@@ -11363,10 +11458,10 @@ function createGateway(deps) {
11363
11458
  if (!info || !info.isFile()) return reply.code(404).send({ error: "The file is gone" });
11364
11459
  const size = info.size;
11365
11460
  reply.header("accept-ranges", "bytes").header("x-content-type-options", "nosniff").header("cache-control", "private, max-age=300");
11366
- const range = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
11367
- if (range && (range[1] || range[2])) {
11368
- let start2 = range[1] ? Number(range[1]) : Math.max(0, size - Number(range[2]));
11369
- let end = range[1] && range[2] ? Number(range[2]) : size - 1;
11461
+ const range2 = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
11462
+ if (range2 && (range2[1] || range2[2])) {
11463
+ let start2 = range2[1] ? Number(range2[1]) : Math.max(0, size - Number(range2[2]));
11464
+ let end = range2[1] && range2[2] ? Number(range2[2]) : size - 1;
11370
11465
  end = Math.min(end, size - 1);
11371
11466
  if (!(start2 <= end) || start2 >= size) return reply.code(416).header("content-range", `bytes */${size}`).send();
11372
11467
  start2 = Math.max(0, start2);
@@ -11434,10 +11529,10 @@ function createGateway(deps) {
11434
11529
  app.get("/api/music/stream/:ref", { onRequest: musicGuard }, async (req, reply) => {
11435
11530
  const ctl = new AbortController();
11436
11531
  req.raw.on("close", () => ctl.abort());
11437
- const range = typeof req.headers.range === "string" && /^bytes=\d*-\d*$/.test(req.headers.range) ? req.headers.range : void 0;
11532
+ const range2 = typeof req.headers.range === "string" && /^bytes=\d*-\d*$/.test(req.headers.range) ? req.headers.range : void 0;
11438
11533
  let r;
11439
11534
  try {
11440
- r = await deps.music.stream(req.params.ref, range, ctl.signal);
11535
+ r = await deps.music.stream(req.params.ref, range2, ctl.signal);
11441
11536
  } catch {
11442
11537
  return reply.code(502).send({ error: "That track could not be played right now." });
11443
11538
  }
@@ -11582,6 +11677,56 @@ function createGateway(deps) {
11582
11677
  if (!deps.video) return reply.code(404).send({ error: "Not found" });
11583
11678
  return deps.video.prepare();
11584
11679
  });
11680
+ const systemWindow = new SlidingWindow(30, 6e4);
11681
+ const systemGuard = async (req, reply) => {
11682
+ const token = req.cookies[COOKIE_NAME];
11683
+ const session = token ? auth.resolveSession(token) : null;
11684
+ if (!session) return reply.code(401).send({ error: "Unauthorized" });
11685
+ if (!systemWindow.allow(String(session.id))) return reply.code(429).header("retry-after", 10).send({ error: "Too many requests" });
11686
+ if (!deps.system) return reply.code(503).send({ error: "Not available" });
11687
+ };
11688
+ const systemError = (reply, e, what) => {
11689
+ if (e instanceof HttpError) return reply.code(e.status).send({ error: e.message });
11690
+ audit.log("owner", "system.failed", { what, message: e.message });
11691
+ return reply.code(500).send({ error: "That did not work. Try again." });
11692
+ };
11693
+ app.get("/api/system", { onRequest: systemGuard }, async () => deps.system.info());
11694
+ app.post("/api/system/update/check", { onRequest: systemGuard }, async (_req, reply) => {
11695
+ try {
11696
+ return await deps.system.check();
11697
+ } catch (e) {
11698
+ return systemError(reply, e, "update.check");
11699
+ }
11700
+ });
11701
+ app.post("/api/system/update/apply", { onRequest: systemGuard }, async (req, reply) => {
11702
+ const body = z10.object({ pin: z10.string().min(1).max(32) }).safeParse(req.body);
11703
+ if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
11704
+ try {
11705
+ return await deps.system.update(body.data.pin, req.ip);
11706
+ } catch (e) {
11707
+ return systemError(reply, e, "update.apply");
11708
+ }
11709
+ });
11710
+ app.post("/api/system/autostart", { onRequest: systemGuard }, async (req, reply) => {
11711
+ const body = z10.object({ enabled: z10.boolean(), pin: z10.string().min(1).max(32) }).safeParse(req.body);
11712
+ if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
11713
+ try {
11714
+ return await deps.system.setAutostart(body.data.enabled, body.data.pin, req.ip);
11715
+ } catch (e) {
11716
+ return systemError(reply, e, "autostart");
11717
+ }
11718
+ });
11719
+ const usageGuard = async (req, reply) => {
11720
+ const token = req.cookies[COOKIE_NAME];
11721
+ if (!(token && auth.resolveSession(token))) return reply.code(401).send({ error: "Unauthorized" });
11722
+ if (!deps.usage) return reply.code(503).send({ error: "Not available" });
11723
+ };
11724
+ app.get("/api/usage", { onRequest: usageGuard }, async (req) => deps.usage.summary(Number(req.query.days ?? 30)));
11725
+ app.delete("/api/usage", { onRequest: usageGuard }, async () => {
11726
+ const removed = deps.usage.clear();
11727
+ audit.log("owner", "usage.cleared", { removed });
11728
+ return { removed };
11729
+ });
11585
11730
  const chromeFailures = new SlidingWindow(20, 6e4);
11586
11731
  const chromeWss = new WebSocketServer({ noServer: true, maxPayload: MAX_DEVICE_FRAME_BYTES });
11587
11732
  if (deps.chrome) {
@@ -12041,8 +12186,8 @@ Connection: close\r
12041
12186
 
12042
12187
  // ../server/src/provider/claude.ts
12043
12188
  import Anthropic from "@anthropic-ai/sdk";
12044
- var encodeName = (n) => n.replaceAll(".", "__");
12045
- var decodeName = (n) => n.replaceAll("__", ".");
12189
+ var encodeName = (n2) => n2.replaceAll(".", "__");
12190
+ var decodeName = (n2) => n2.replaceAll("__", ".");
12046
12191
  var toAnthropicImage = (b) => ({
12047
12192
  type: "image",
12048
12193
  source: { type: "base64", media_type: b.mime, data: b.data }
@@ -12294,7 +12439,7 @@ function readBody(req) {
12294
12439
  var LIMIT = /\b(?:hit|reached)\s+(?:your\s+|the\s+)?(?:[\w-]+\s+)?(?:usage\s+)?limit\b|\busage\s+limit\b|\b(?:\d+-hour|weekly|daily|monthly)\s+limit\s+reached\b|\bhit your (?:weekly|daily|monthly|usage|\d+-hour) limit\b/i;
12295
12440
  var DEFAULT_BACKOFF_MS = 15 * 6e4;
12296
12441
  var MIN_BACKOFF_MS = 3e4;
12297
- var DAY_MS2 = 864e5;
12442
+ var DAY_MS3 = 864e5;
12298
12443
  function isUsageLimit(text3) {
12299
12444
  return LIMIT.test(text3);
12300
12445
  }
@@ -12319,7 +12464,7 @@ function parseResetDelayMs(message, now = /* @__PURE__ */ new Date()) {
12319
12464
  const zoneNow = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
12320
12465
  const offset = zoneNow - now.getTime();
12321
12466
  let reset2 = Date.UTC(get("year"), get("month") - 1, get("day"), hour, Number(min), 0) - offset;
12322
- if (reset2 <= now.getTime()) reset2 += DAY_MS2;
12467
+ if (reset2 <= now.getTime()) reset2 += DAY_MS3;
12323
12468
  const delay = reset2 - now.getTime();
12324
12469
  return Number.isFinite(delay) && delay > 0 ? delay : void 0;
12325
12470
  } catch {
@@ -12999,7 +13144,8 @@ var SettingsPatch = z11.object({
12999
13144
  sttModel: z11.string().trim().min(1).max(80).regex(/^[\w./-]+$/),
13000
13145
  duck: z11.boolean(),
13001
13146
  duckPercent: z11.number().int().min(0).max(100)
13002
- }).partial().strict()
13147
+ }).partial().strict(),
13148
+ updates: z11.object({ auto: z11.boolean() }).partial().strict()
13003
13149
  }).partial().strict();
13004
13150
  function settingsView(c) {
13005
13151
  return {
@@ -13062,7 +13208,8 @@ function settingsView(c) {
13062
13208
  marathiVoice: c.voice.marathiVoice,
13063
13209
  duck: c.voice.duck,
13064
13210
  duckPercent: c.voice.duckPercent
13065
- }
13211
+ },
13212
+ updates: { auto: c.updates.auto }
13066
13213
  };
13067
13214
  }
13068
13215
  var SettingsError = class extends Error {
@@ -13074,6 +13221,7 @@ function refuseForAgent(raw) {
13074
13221
  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
13222
  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
13223
  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.");
13224
+ if ("updates" in r) throw new SettingsError("Whether Friend updates itself can only be changed by the owner on the settings screen.");
13077
13225
  if ("alexa" in r) throw new SettingsError("Alexa settings can only be changed by the owner on the settings screen.");
13078
13226
  if ("channels" in r) throw new SettingsError("Messaging channels can only be set up by the owner on the settings screen.");
13079
13227
  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 +13366,7 @@ function applySettings(config, raw) {
13218
13366
  if (apiKey) config.openai.apiKey = apiKey;
13219
13367
  }
13220
13368
  if (p.claudeCode) Object.assign(config.claudeCode, p.claudeCode);
13369
+ if (p.updates) Object.assign(config.updates, p.updates);
13221
13370
  if (p.voice) {
13222
13371
  const safe = next.voice;
13223
13372
  for (const k of Object.keys(p.voice)) config.voice[k] = safe[k];
@@ -13235,8 +13384,8 @@ function clientLook(l) {
13235
13384
 
13236
13385
  // ../server/src/provider/ollama.ts
13237
13386
  import { Agent as Agent2, fetch as undiciFetch } from "undici";
13238
- var encodeName2 = (n) => n.replaceAll(".", "__");
13239
- var decodeName2 = (n) => n.replaceAll("__", ".");
13387
+ var encodeName2 = (n2) => n2.replaceAll(".", "__");
13388
+ var decodeName2 = (n2) => n2.replaceAll("__", ".");
13240
13389
  function toOllamaMessages(system, messages, vision) {
13241
13390
  const out = [{ role: "system", content: system }];
13242
13391
  const nameById = /* @__PURE__ */ new Map();
@@ -13406,8 +13555,8 @@ var OllamaProvider = class {
13406
13555
 
13407
13556
  // ../server/src/provider/openai.ts
13408
13557
  import { Agent as Agent3, fetch as undiciFetch2 } from "undici";
13409
- var encodeName3 = (n) => n.replaceAll(".", "__");
13410
- var decodeName3 = (n) => n.replaceAll("__", ".");
13558
+ var encodeName3 = (n2) => n2.replaceAll(".", "__");
13559
+ var decodeName3 = (n2) => n2.replaceAll("__", ".");
13411
13560
  function toOpenAiMessages(system, messages, vision) {
13412
13561
  const out = [{ role: "system", content: system }];
13413
13562
  for (const m of messages) {
@@ -13689,6 +13838,368 @@ var DuckController = class {
13689
13838
  }
13690
13839
  };
13691
13840
 
13841
+ // ../server/src/system/autostart.ts
13842
+ import { spawn as spawn6 } from "node:child_process";
13843
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync10, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "node:fs";
13844
+ import { homedir as homedir4 } from "node:os";
13845
+ import { dirname as dirname4, posix as posix2, win32 as win322 } from "node:path";
13846
+ var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
13847
+ var NAME2 = "Friend";
13848
+ var LABEL = "com.friend.assistant";
13849
+ var UNIT = "friend.service";
13850
+ var quoted = (s) => `"${s}"`;
13851
+ function vbsLauncher(self) {
13852
+ const cmd = `${quoted(self.node)} ${quoted(self.script)} daemon start`;
13853
+ const inVbs = cmd.replace(/"/g, '""');
13854
+ 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");
13855
+ }
13856
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13857
+ function plistFor(self, o) {
13858
+ const log = `${o.dataDir}/friend-autostart.log`;
13859
+ return [
13860
+ `<?xml version="1.0" encoding="UTF-8"?>`,
13861
+ `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
13862
+ `<plist version="1.0">`,
13863
+ `<dict>`,
13864
+ ` <key>Label</key><string>${LABEL}</string>`,
13865
+ ` <key>ProgramArguments</key>`,
13866
+ ` <array>`,
13867
+ ` <string>${xml(self.node)}</string>`,
13868
+ ` <string>${xml(self.script)}</string>`,
13869
+ ` <string>daemon</string>`,
13870
+ ` <string>start</string>`,
13871
+ ` </array>`,
13872
+ ` <key>RunAtLoad</key>`,
13873
+ ` <true/>`,
13874
+ ` <key>EnvironmentVariables</key>`,
13875
+ ` <dict><key>PATH</key><string>${xml(o.path)}</string></dict>`,
13876
+ ` <key>StandardOutPath</key><string>${xml(log)}</string>`,
13877
+ ` <key>StandardErrorPath</key><string>${xml(log)}</string>`,
13878
+ `</dict>`,
13879
+ `</plist>`,
13880
+ ""
13881
+ ].join("\n");
13882
+ }
13883
+ var unitWord = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
13884
+ function systemdUnit(self, o) {
13885
+ return [
13886
+ "[Unit]",
13887
+ "Description=Friend assistant",
13888
+ "After=network-online.target",
13889
+ "",
13890
+ "[Service]",
13891
+ "Type=oneshot",
13892
+ "RemainAfterExit=yes",
13893
+ `Environment=${unitWord(`PATH=${o.path}`)}`,
13894
+ `ExecStart=${unitWord(self.node)} ${unitWord(self.script)} daemon start`,
13895
+ `ExecStop=${unitWord(self.node)} ${unitWord(self.script)} daemon stop`,
13896
+ "",
13897
+ "[Install]",
13898
+ "WantedBy=default.target",
13899
+ ""
13900
+ ].join("\n");
13901
+ }
13902
+ var desktopWord = (s) => `"${s.replace(/(["`$\\])/g, "\\$1")}"`;
13903
+ function desktopEntry(self) {
13904
+ 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");
13905
+ }
13906
+ var Autostart = class {
13907
+ constructor(d, self) {
13908
+ this.d = d;
13909
+ this.self = self;
13910
+ }
13911
+ d;
13912
+ self;
13913
+ get win() {
13914
+ return this.d.platform === "win32";
13915
+ }
13916
+ get mac() {
13917
+ return this.d.platform === "darwin";
13918
+ }
13919
+ get linux() {
13920
+ return this.d.platform === "linux";
13921
+ }
13922
+ p() {
13923
+ return this.win ? win322 : posix2;
13924
+ }
13925
+ vbs() {
13926
+ return win322.join(this.d.dataDir, "autostart.vbs");
13927
+ }
13928
+ plist() {
13929
+ return posix2.join(this.d.home, "Library", "LaunchAgents", `${LABEL}.plist`);
13930
+ }
13931
+ configDir() {
13932
+ return this.d.configHome || posix2.join(this.d.home, ".config");
13933
+ }
13934
+ unitFile() {
13935
+ return posix2.join(this.configDir(), "systemd", "user", UNIT);
13936
+ }
13937
+ desktopFile() {
13938
+ return posix2.join(this.configDir(), "autostart", "friend.desktop");
13939
+ }
13940
+ async hasSystemd() {
13941
+ return (await this.d.run("systemctl", ["--user", "--version"])).code === 0;
13942
+ }
13943
+ async status() {
13944
+ if (this.win) return { supported: true, enabled: (await this.d.run("reg", ["query", RUN_KEY, "/v", NAME2])).code === 0, kind: "registry" };
13945
+ if (this.mac) return { supported: true, enabled: this.d.exists(this.plist()), kind: "launchd" };
13946
+ if (this.linux) {
13947
+ if (await this.hasSystemd()) {
13948
+ const r = await this.d.run("systemctl", ["--user", "is-enabled", UNIT]);
13949
+ if (r.code === 0 && /enabled/.test(r.stdout)) return { supported: true, enabled: true, kind: "systemd" };
13950
+ }
13951
+ return { supported: true, enabled: this.d.exists(this.desktopFile()), kind: this.d.exists(this.desktopFile()) ? "xdg" : "none" };
13952
+ }
13953
+ return { supported: false, enabled: false, kind: "none" };
13954
+ }
13955
+ async enable() {
13956
+ const self = this.self();
13957
+ 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." };
13958
+ if (this.win) return this.enableWindows(self);
13959
+ if (this.mac) return this.enableMac(self);
13960
+ if (this.linux) return this.enableLinux(self);
13961
+ return { ok: false, message: "Starting Friend by itself is not supported on this kind of computer." };
13962
+ }
13963
+ async disable() {
13964
+ if (this.win) {
13965
+ await this.d.run("reg", ["delete", RUN_KEY, "/v", NAME2, "/f"]);
13966
+ this.d.remove(this.vbs());
13967
+ } else if (this.mac) {
13968
+ await this.d.run("launchctl", ["bootout", `gui/${this.d.uid}`, this.plist()]);
13969
+ this.d.remove(this.plist());
13970
+ } else if (this.linux) {
13971
+ if (await this.hasSystemd()) {
13972
+ await this.d.run("systemctl", ["--user", "disable", UNIT]);
13973
+ this.d.remove(this.unitFile());
13974
+ await this.d.run("systemctl", ["--user", "daemon-reload"]);
13975
+ }
13976
+ this.d.remove(this.desktopFile());
13977
+ } else return { ok: false, message: "Starting Friend by itself is not supported on this kind of computer." };
13978
+ return { ok: true, message: "Friend will no longer start by itself when you sign in." };
13979
+ }
13980
+ /** Writes the setup again with the program's current place, when it is on. Used after an upgrade. */
13981
+ async refresh() {
13982
+ if ((await this.status()).enabled) await this.enable();
13983
+ }
13984
+ async enableWindows(self) {
13985
+ this.d.write(this.vbs(), vbsLauncher(self));
13986
+ const r = await this.d.run("reg", ["add", RUN_KEY, "/v", NAME2, "/t", "REG_SZ", "/d", `wscript.exe ${quoted(this.vbs())}`, "/f"]);
13987
+ if (r.code !== 0) return { ok: false, message: "Could not add Friend to the programs that start when you sign in." };
13988
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
13989
+ }
13990
+ async enableMac(self) {
13991
+ this.d.write(this.plist(), plistFor(self, { dataDir: this.d.dataDir, path: this.d.path }));
13992
+ await this.d.run("launchctl", ["bootout", `gui/${this.d.uid}`, this.plist()]);
13993
+ let r = await this.d.run("launchctl", ["bootstrap", `gui/${this.d.uid}`, this.plist()]);
13994
+ if (r.code !== 0) r = await this.d.run("launchctl", ["load", "-w", this.plist()]);
13995
+ 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." };
13996
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
13997
+ }
13998
+ async enableLinux(self) {
13999
+ if (await this.hasSystemd()) {
14000
+ this.d.write(this.unitFile(), systemdUnit(self, { path: this.d.path }));
14001
+ await this.d.run("systemctl", ["--user", "daemon-reload"]);
14002
+ const r = await this.d.run("systemctl", ["--user", "enable", UNIT]);
14003
+ if (r.code === 0) {
14004
+ this.d.remove(this.desktopFile());
14005
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
14006
+ }
14007
+ }
14008
+ this.d.write(this.desktopFile(), desktopEntry(self));
14009
+ return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
14010
+ }
14011
+ };
14012
+ function realAutostartDeps(dataDir) {
14013
+ return {
14014
+ platform: process.platform,
14015
+ home: homedir4(),
14016
+ dataDir,
14017
+ uid: typeof process.getuid === "function" ? process.getuid() : 0,
14018
+ path: process.env.PATH ?? "",
14019
+ ...process.env.XDG_CONFIG_HOME ? { configHome: process.env.XDG_CONFIG_HOME } : {},
14020
+ exists: (p) => existsSync16(p),
14021
+ read: (p) => {
14022
+ try {
14023
+ return readFileSync10(p, "utf8");
14024
+ } catch {
14025
+ return null;
14026
+ }
14027
+ },
14028
+ write: (p, text3) => {
14029
+ mkdirSync11(dirname4(p), { recursive: true });
14030
+ writeFileSync10(p, text3);
14031
+ },
14032
+ remove: (p) => rmSync9(p, { force: true }),
14033
+ run: (cmd, args) => new Promise((resolve13) => {
14034
+ let out = "";
14035
+ const child = spawn6(cmd, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
14036
+ child.stdout?.on("data", (d) => out += d.toString());
14037
+ child.stderr?.on("data", (d) => out += d.toString());
14038
+ child.on("error", () => resolve13({ code: 127, stdout: out }));
14039
+ child.on("close", (code) => resolve13({ code: code ?? 1, stdout: out }));
14040
+ })
14041
+ };
14042
+ }
14043
+ function currentSelf() {
14044
+ const script = process.argv[1] ?? "";
14045
+ return { node: process.execPath, script: /\.(mjs|js|cjs)$/i.test(script) ? script : "" };
14046
+ }
14047
+
14048
+ // ../server/src/system/npm.ts
14049
+ import { spawn as spawn7 } from "node:child_process";
14050
+ var PACKAGE = "friend-assistant";
14051
+ var runningVersion = () => true ? "0.1.46" : "development";
14052
+ async function npmLatest(fetchFn = fetch, timeoutMs = 1e4) {
14053
+ try {
14054
+ const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
14055
+ if (!res.ok) return null;
14056
+ const v = (await res.json()).version;
14057
+ return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v) ? v : null;
14058
+ } catch {
14059
+ return null;
14060
+ }
14061
+ }
14062
+ var ps = (s) => `'${s.replace(/'/g, "''")}'`;
14063
+ function spawnUpgrade(self, platform = process.platform) {
14064
+ if (!self.node || !self.script) return false;
14065
+ try {
14066
+ 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" });
14067
+ child.on("error", () => {
14068
+ });
14069
+ child.unref();
14070
+ return child.pid !== void 0;
14071
+ } catch {
14072
+ return false;
14073
+ }
14074
+ }
14075
+
14076
+ // ../server/src/system/updater.ts
14077
+ function isNewerVersion(a, b) {
14078
+ if (!/^\d/.test(a) || !/^\d/.test(b)) return false;
14079
+ const n2 = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
14080
+ const [x, y] = [n2(a), n2(b)];
14081
+ for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
14082
+ return false;
14083
+ }
14084
+ var Updater = class {
14085
+ constructor(d, t = {}) {
14086
+ this.d = d;
14087
+ this.firstMs = t.firstMs ?? 2e4;
14088
+ this.everyMs = t.everyMs ?? 6 * 36e5;
14089
+ this.retryMs = t.retryMs ?? 5 * 6e4;
14090
+ }
14091
+ d;
14092
+ latest = null;
14093
+ checkedAt = null;
14094
+ applying = false;
14095
+ error = null;
14096
+ notified = null;
14097
+ inflight = null;
14098
+ first = null;
14099
+ every = null;
14100
+ retry = null;
14101
+ firstMs;
14102
+ everyMs;
14103
+ retryMs;
14104
+ state() {
14105
+ const current = this.d.current();
14106
+ 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 };
14107
+ }
14108
+ start() {
14109
+ this.stop();
14110
+ this.first = setTimeout(() => {
14111
+ this.first = null;
14112
+ void this.check();
14113
+ this.every = setInterval(() => void this.check(), this.everyMs);
14114
+ this.every.unref?.();
14115
+ }, this.firstMs);
14116
+ this.first.unref?.();
14117
+ }
14118
+ stop() {
14119
+ if (this.first) clearTimeout(this.first);
14120
+ if (this.every) clearInterval(this.every);
14121
+ if (this.retry) clearTimeout(this.retry);
14122
+ this.first = this.every = this.retry = null;
14123
+ }
14124
+ /** Asks npm for the newest version (one question at a time), tells the owner, and updates when that is switched on. */
14125
+ check() {
14126
+ this.inflight ??= (async () => {
14127
+ try {
14128
+ const latest = await this.d.latest();
14129
+ this.checkedAt = Date.now();
14130
+ if (latest) {
14131
+ this.latest = latest;
14132
+ this.error = null;
14133
+ await this.consider();
14134
+ } else {
14135
+ this.error = "Could not ask npm for the newest version. Check the internet connection.";
14136
+ }
14137
+ } catch {
14138
+ this.error = "Could not ask npm for the newest version. Check the internet connection.";
14139
+ } finally {
14140
+ this.inflight = null;
14141
+ }
14142
+ return this.state();
14143
+ })();
14144
+ return this.inflight;
14145
+ }
14146
+ /** The owner pressed Update now: installs even when automatic updates are off, but never in the middle of an answer. */
14147
+ async applyNow() {
14148
+ if (this.applying) return { ok: true, message: "The update is already running. Reload this page in a minute." };
14149
+ if (!this.latest) await this.check();
14150
+ const s = this.state();
14151
+ if (!s.latest) return { ok: false, message: s.error ?? "Could not ask npm for the newest version." };
14152
+ if (this.applying) return { ok: true, message: "The update is already running. Reload this page in a minute." };
14153
+ if (!s.available) return { ok: false, message: `Friend is up to date (${s.current}).` };
14154
+ if (!s.canApply) return { ok: false, message: "This Friend is running in a terminal and cannot restart itself. Close it and run: friend upgrade" };
14155
+ if (this.d.busy()) return { ok: false, message: "Friend is busy with a request. Try again in a moment." };
14156
+ 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." };
14157
+ }
14158
+ async consider() {
14159
+ const s = this.state();
14160
+ if (!s.available || !s.latest) return;
14161
+ const canApply = this.d.canApply();
14162
+ if (this.notified !== s.latest) {
14163
+ this.notified = s.latest;
14164
+ this.d.notify(
14165
+ 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`
14166
+ );
14167
+ }
14168
+ if (this.d.auto() && canApply) await this.maybeApply();
14169
+ }
14170
+ async maybeApply() {
14171
+ const s = this.state();
14172
+ if (this.applying || !s.available || !s.latest) return;
14173
+ if (this.d.busy()) {
14174
+ if (!this.retry) {
14175
+ this.retry = setTimeout(() => {
14176
+ this.retry = null;
14177
+ void this.maybeApply();
14178
+ }, this.retryMs);
14179
+ this.retry.unref?.();
14180
+ }
14181
+ return;
14182
+ }
14183
+ await this.begin(s.latest);
14184
+ }
14185
+ async begin(version) {
14186
+ this.applying = true;
14187
+ this.error = null;
14188
+ this.d.notify(`Updating Friend to ${version}. It restarts in a moment: reload this page afterwards.`);
14189
+ let ok = false;
14190
+ try {
14191
+ ok = await this.d.apply(version);
14192
+ } catch {
14193
+ ok = false;
14194
+ }
14195
+ if (!ok) {
14196
+ this.applying = false;
14197
+ this.error = "Could not start the update.";
14198
+ }
14199
+ return ok;
14200
+ }
14201
+ };
14202
+
13692
14203
  // ../server/src/voice/challenge.ts
13693
14204
  import { randomUUID as randomUUID6 } from "node:crypto";
13694
14205
  var Challenges = class {
@@ -13750,20 +14261,20 @@ var Challenges = class {
13750
14261
 
13751
14262
  // ../server/src/voice/crypto.ts
13752
14263
  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";
14264
+ import { chmodSync as chmodSync9, existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
14265
+ import { dirname as dirname5 } from "node:path";
13755
14266
  var KEY_BYTES = 32;
13756
14267
  var IV_BYTES = 12;
13757
14268
  var TAG_BYTES = 16;
13758
14269
  function loadOrCreateKey(path3) {
13759
- if (existsSync16(path3)) {
13760
- const key2 = readFileSync10(path3);
14270
+ if (existsSync17(path3)) {
14271
+ const key2 = readFileSync11(path3);
13761
14272
  if (key2.length !== KEY_BYTES) throw new Error(`invalid voice key file (expected ${KEY_BYTES} bytes): ${path3}`);
13762
14273
  return key2;
13763
14274
  }
13764
- mkdirSync11(dirname4(path3), { recursive: true });
14275
+ mkdirSync12(dirname5(path3), { recursive: true });
13765
14276
  const key = randomBytes9(KEY_BYTES);
13766
- writeFileSync10(path3, key, { mode: 384 });
14277
+ writeFileSync11(path3, key, { mode: 384 });
13767
14278
  try {
13768
14279
  chmodSync9(path3, 384);
13769
14280
  } catch {
@@ -13852,9 +14363,9 @@ var Voiceprint = class {
13852
14363
  }
13853
14364
  /** Similarity of a new voiceprint to the owner's, or null when nobody is enrolled. Throws on a wrong key. */
13854
14365
  score(embedding) {
13855
- const row = this.db.prepare("select centroid_enc e, dim from voiceprint where id = 1").get();
13856
- if (!row) return null;
13857
- const raw = decrypt(this.key, row.e);
14366
+ const row2 = this.db.prepare("select centroid_enc e, dim from voiceprint where id = 1").get();
14367
+ if (!row2) return null;
14368
+ const raw = decrypt(this.key, row2.e);
13858
14369
  const stored = Array.from(new Float32Array(raw.buffer, raw.byteOffset, raw.byteLength / 4));
13859
14370
  return cosine(stored, embedding);
13860
14371
  }
@@ -13895,10 +14406,10 @@ var Enrollment = class {
13895
14406
  if (!checkConsistency(this.samples, CONSISTENT).ok) {
13896
14407
  throw new EnrollError("The recordings do not agree with each other. They must all be the same person; start over.");
13897
14408
  }
13898
- const n = this.samples.length;
14409
+ const n2 = this.samples.length;
13899
14410
  this.voiceprint.enroll(this.samples, this.model);
13900
14411
  this.samples = [];
13901
- return { samples: n };
14412
+ return { samples: n2 };
13902
14413
  }
13903
14414
  cancel() {
13904
14415
  this.samples = [];
@@ -13914,17 +14425,17 @@ var GREETINGS = /* @__PURE__ */ new Set(["hey", "hay", "hi", "hello", "hellow",
13914
14425
  var WORD = /[\p{L}\p{M}\p{N}'’]+/gu;
13915
14426
  var GAP = /^[\s,.:;!?…\-–—]+/u;
13916
14427
  function distance(a, b) {
13917
- const row = Array.from({ length: b.length + 1 }, (_, i) => i);
14428
+ const row2 = Array.from({ length: b.length + 1 }, (_, i) => i);
13918
14429
  for (let i = 1; i <= a.length; i++) {
13919
- let prev = row[0];
13920
- row[0] = i;
14430
+ let prev = row2[0];
14431
+ row2[0] = i;
13921
14432
  for (let j = 1; j <= b.length; j++) {
13922
- const keep = row[j];
13923
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
14433
+ const keep = row2[j];
14434
+ row2[j] = Math.min(row2[j] + 1, row2[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
13924
14435
  prev = keep;
13925
14436
  }
13926
14437
  }
13927
- return row[b.length];
14438
+ return row2[b.length];
13928
14439
  }
13929
14440
  var sameWord = (heard, name) => heard === name || name.length >= 4 && distance(heard, name) <= 1;
13930
14441
  function matchWake(text3, name) {
@@ -14295,6 +14806,8 @@ async function createApp(opts) {
14295
14806
  const config = { ...loadConfig(opts.dataDir), ...opts.config };
14296
14807
  const db = openDb(opts.dataDir);
14297
14808
  const audit = new Audit(db);
14809
+ const usageLog = new UsageLog(db);
14810
+ usageLog.prune(USAGE_KEEP_DAYS);
14298
14811
  const auth = new Auth(db, audit);
14299
14812
  const policy = new PolicyEngine(db, config);
14300
14813
  const devices = new DeviceHub();
@@ -14314,6 +14827,13 @@ async function createApp(opts) {
14314
14827
  if (config.provider === "openai") return new OpenAiProvider({ ...config.openai });
14315
14828
  return config.apiKey ? new ClaudeProvider({ apiKey: config.apiKey, model: config.model }) : new MissingKeyProvider();
14316
14829
  };
14830
+ const modelInUse = () => {
14831
+ if (opts.provider) return { provider: "custom", model: "custom" };
14832
+ if (config.provider === "claude-code") return { provider: "claude-code", model: config.claudeCode.model };
14833
+ if (config.provider === "ollama") return { provider: "ollama", model: config.ollama.model };
14834
+ if (config.provider === "openai") return { provider: "openai", model: config.openai.model };
14835
+ return { provider: "claude", model: config.model };
14836
+ };
14317
14837
  const switchable = opts.provider ? null : new SwitchableProvider(makeProvider());
14318
14838
  const provider = opts.provider ?? switchable;
14319
14839
  const camera = new CameraBroker(opts.cameraTimeoutMs);
@@ -14345,7 +14865,7 @@ async function createApp(opts) {
14345
14865
  if (m.type === "caption.delta") reply += m.text;
14346
14866
  else if (m.type === "caption.end") reply += "\n";
14347
14867
  };
14348
- await agent.runTurn(text3, { trust: "owner", emit, ip: `channel:${via}` });
14868
+ await agent.runTurn(text3, { trust: "owner", emit, ip: `channel:${via}`, source: "channel" });
14349
14869
  return reply.trim();
14350
14870
  }
14351
14871
  });
@@ -14651,6 +15171,7 @@ async function createApp(opts) {
14651
15171
  mcp: hub,
14652
15172
  admin,
14653
15173
  provider,
15174
+ usage: (e) => usageLog.record({ ...e, ...modelInUse() }),
14654
15175
  policy,
14655
15176
  approvals,
14656
15177
  devices,
@@ -14693,7 +15214,7 @@ async function createApp(opts) {
14693
15214
  store: { config: () => config.alexa, update: (fn) => void changeAlexaConfig(fn) },
14694
15215
  verify: { fetchCert: opts.alexaFetchCert ?? fetchAmazonCert, roots: opts.alexaRoots },
14695
15216
  clock: Date.now,
14696
- agent: { isBusy: () => agent.isBusy(), runTurn: (text3, ctx) => agent.runTurn(text3, ctx) },
15217
+ agent: { isBusy: () => agent.isBusy(), runTurn: (text3, ctx) => agent.runTurn(text3, { source: "task", ...ctx }) },
14697
15218
  broadcast,
14698
15219
  screenOpen: () => clients.size > 0,
14699
15220
  fast: {
@@ -14814,7 +15335,7 @@ async function createApp(opts) {
14814
15335
  if (m.type === "caption.delta" && epoch === lockEpoch) talk?.push(m.text);
14815
15336
  broadcast(m);
14816
15337
  };
14817
- await agent.runTurn(text3, { trust, emit, ip, ...person ? { person } : {}, ...spoken ? { spoken } : {} });
15338
+ await agent.runTurn(text3, { trust, emit, ip, source: "voice", ...person ? { person } : {}, ...spoken ? { spoken } : {} });
14818
15339
  await talk?.end();
14819
15340
  }
14820
15341
  async function confirmChallenge(id2, pin, conn) {
@@ -14848,7 +15369,7 @@ async function createApp(opts) {
14848
15369
  broadcast({ type: "voice.heard", text: held.text, trust: "owner" });
14849
15370
  await runVoiceTurn(held.text, conn.ip, epoch);
14850
15371
  }
14851
- const here2 = dirname5(fileURLToPath2(import.meta.url));
15372
+ const here2 = dirname6(fileURLToPath2(import.meta.url));
14852
15373
  const catalogDir = opts.pluginCatalogDir ?? firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve5(here2, "plugins"), resolve5(here2, "../../..", "plugins")]) ?? resolve5(here2, "plugins");
14853
15374
  const pluginsApi = {
14854
15375
  list: () => {
@@ -15020,11 +15541,43 @@ async function createApp(opts) {
15020
15541
  });
15021
15542
  }
15022
15543
  });
15544
+ const self = opts.system?.self ?? currentSelf();
15545
+ const autostart = new Autostart(opts.system?.autostartDeps ?? realAutostartDeps(opts.dataDir), () => self);
15546
+ const updater = new Updater(
15547
+ {
15548
+ current: opts.system?.current ?? runningVersion,
15549
+ latest: opts.system?.latest ?? (() => npmLatest()),
15550
+ auto: () => config.updates.auto,
15551
+ busy: () => agent.isBusy(),
15552
+ canApply: opts.system?.canApply ?? (() => process.env.FRIEND_DAEMON === "1"),
15553
+ // only Friend in the background can replace itself
15554
+ apply: opts.system?.apply ?? (() => spawnUpgrade(self)),
15555
+ notify: (text3) => broadcast({ type: "alert", level: "info", text: text3 })
15556
+ },
15557
+ opts.system?.times
15558
+ );
15559
+ const systemApi = {
15560
+ info: async () => ({ version: updater.state().current, update: updater.state(), autostart: await autostart.status(), auto: config.updates.auto }),
15561
+ check: () => updater.check(),
15562
+ update: async (pin, ip) => {
15563
+ await requirePin(pin, ip);
15564
+ audit.log("owner", "system.update_now", { ip });
15565
+ return updater.applyNow();
15566
+ },
15567
+ setAutostart: async (enabled, pin, ip) => {
15568
+ await requirePin(pin, ip);
15569
+ audit.log("owner", enabled ? "system.autostart_on" : "system.autostart_off", { ip });
15570
+ const r = enabled ? await autostart.enable() : await autostart.disable();
15571
+ return { ...r, status: await autostart.status() };
15572
+ }
15573
+ };
15023
15574
  const tls = config.https ? await ensureTlsCert(opts.dataDir, [...config.allowedHosts, ...config.publicUrl ? [new URL(config.publicUrl).hostname] : []]) : void 0;
15024
15575
  const gateway = createGateway({
15025
15576
  ...tls ? { tls } : {},
15026
15577
  connectors: connectorsApi,
15027
15578
  chrome: chromeRemoteApi,
15579
+ system: systemApi,
15580
+ usage: usageLog,
15028
15581
  video: {
15029
15582
  stream: (id2) => videoHub.streamOf(id2),
15030
15583
  status: async () => videoHub.status() ?? await videoHub.refresh(),
@@ -15340,7 +15893,7 @@ async function createApp(opts) {
15340
15893
  audit.log("owner", "music.soundcloud.disconnect", { ip });
15341
15894
  return { ok: true };
15342
15895
  },
15343
- stream: async (ref2, range, signal) => {
15896
+ stream: async (ref2, range2, signal) => {
15344
15897
  const p = parseRef(ref2);
15345
15898
  const svc = p?.source === "soundcloud" ? soundcloudHub : p?.source === "audius" ? audiusHub : null;
15346
15899
  if (!p || !svc) return { ok: false, status: 404, error: "Not found" };
@@ -15348,7 +15901,7 @@ async function createApp(opts) {
15348
15901
  if (!st.connected) return { ok: false, status: 404, error: st.needs ?? "That music source is not connected." };
15349
15902
  const s = await svc.streamOf(p.id);
15350
15903
  if (!s.ok) return { ok: false, status: 404, error: s.error };
15351
- const a = await svc.fetchAudio(s.url, range, signal);
15904
+ const a = await svc.fetchAudio(s.url, range2, signal);
15352
15905
  if (!a.ok) return { ok: false, status: 502, error: a.error };
15353
15906
  const h = a.res.headers;
15354
15907
  const type = h.get("content-type") ?? "";
@@ -15448,7 +16001,7 @@ ${url}`);
15448
16001
  if (speaker && voiceAvailable() && config.voice.speakTyped) {
15449
16002
  void runVoiceTurn(msg.text, conn.ip, lockEpoch).catch((e) => audit.log("system", "turn.crash", { message: e.message }));
15450
16003
  } else {
15451
- void agent.runTurn(msg.text, { trust: "owner", emit: broadcast, ip: conn.ip }).catch((e) => audit.log("system", "turn.crash", { message: e.message }));
16004
+ void agent.runTurn(msg.text, { trust: "owner", emit: broadcast, ip: conn.ip, source: "chat" }).catch((e) => audit.log("system", "turn.crash", { message: e.message }));
15452
16005
  }
15453
16006
  break;
15454
16007
  case "approval.respond":
@@ -15594,12 +16147,14 @@ ${url}`);
15594
16147
  for (const id2 of gone) broadcast({ type: "stage", command: { cmd: "hide", id: id2 } });
15595
16148
  if (gone.length) for (const follow of stage.settleNow()) broadcast({ type: "stage", command: follow });
15596
16149
  }, opts.expireIntervalMs ?? 1e3);
16150
+ if (opts.system?.checkForUpdates) updater.start();
15597
16151
  const listening = await gateway.listen();
15598
16152
  listenPort = listening.port;
15599
16153
  return listening;
15600
16154
  },
15601
16155
  async stop() {
15602
16156
  stopped = true;
16157
+ updater.stop();
15603
16158
  hostPlayer.dispose();
15604
16159
  videoHub.dispose();
15605
16160
  heartbeat.stop();
@@ -15622,7 +16177,7 @@ ${url}`);
15622
16177
  }
15623
16178
 
15624
16179
  // ../server/src/voice/sidecar.ts
15625
- import { spawn as spawn6 } from "node:child_process";
16180
+ import { spawn as spawn8 } from "node:child_process";
15626
16181
  import { createInterface } from "node:readline";
15627
16182
  var SidecarProcess = class {
15628
16183
  constructor(opts) {
@@ -15659,7 +16214,7 @@ var SidecarProcess = class {
15659
16214
  settled = true;
15660
16215
  reject(new Error(message));
15661
16216
  };
15662
- const child = spawn6(this.opts.command, this.opts.args, {
16217
+ const child = spawn8(this.opts.command, this.opts.args, {
15663
16218
  cwd: this.opts.cwd,
15664
16219
  env: { ...this.opts.env ?? process.env, FRIEND_VOICE_TOKEN: this.opts.token },
15665
16220
  windowsHide: true,
@@ -15754,14 +16309,14 @@ var nonEmpty = (v) => {
15754
16309
  return v.trim();
15755
16310
  };
15756
16311
  var unit2 = (v) => {
15757
- const n = Number(v);
15758
- if (v.trim() === "" || !Number.isFinite(n) || n < 0 || n > 1) throw new Error("Expected a number between 0 and 1");
15759
- return n;
16312
+ const n2 = Number(v);
16313
+ if (v.trim() === "" || !Number.isFinite(n2) || n2 < 0 || n2 > 1) throw new Error("Expected a number between 0 and 1");
16314
+ return n2;
15760
16315
  };
15761
16316
  var percent = (v) => {
15762
- const n = Number(v);
15763
- if (v.trim() === "" || !Number.isFinite(n) || n < 0 || n > 100) throw new Error("Expected a number between 0 and 100");
15764
- return Math.round(n);
16317
+ const n2 = Number(v);
16318
+ if (v.trim() === "" || !Number.isFinite(n2) || n2 < 0 || n2 > 100) throw new Error("Expected a number between 0 and 100");
16319
+ return Math.round(n2);
15765
16320
  };
15766
16321
  var httpUrl = (v) => {
15767
16322
  let u;
@@ -15793,9 +16348,9 @@ var PARSERS = {
15793
16348
  },
15794
16349
  https: bool,
15795
16350
  port: (v) => {
15796
- const n = Number(v);
15797
- if (!Number.isInteger(n) || n < 0 || n > 65535) throw new Error("Port must be a number between 0 and 65535");
15798
- return n;
16351
+ const n2 = Number(v);
16352
+ if (!Number.isInteger(n2) || n2 < 0 || n2 > 65535) throw new Error("Port must be a number between 0 and 65535");
16353
+ return n2;
15799
16354
  },
15800
16355
  provider: (v) => {
15801
16356
  if (v !== "claude" && v !== "ollama" && v !== "claude-code" && v !== "openai") throw new Error("Provider must be claude, ollama, claude-code or openai");
@@ -15807,6 +16362,9 @@ var PARSERS = {
15807
16362
  trustProxy: bool
15808
16363
  };
15809
16364
  var GROUPS = {
16365
+ updates: {
16366
+ auto: bool
16367
+ },
15810
16368
  voice: {
15811
16369
  enabled: bool,
15812
16370
  sttModel: nonEmpty,
@@ -15860,9 +16418,9 @@ var GROUPS = {
15860
16418
  model: nonEmpty,
15861
16419
  vision: bool,
15862
16420
  contextTokens: (v) => {
15863
- const n = Number(v);
15864
- if (!Number.isInteger(n) || n < 1024 || n > 131072) throw new Error("contextTokens must be a whole number between 1024 and 131072");
15865
- return n;
16421
+ const n2 = Number(v);
16422
+ if (!Number.isInteger(n2) || n2 < 1024 || n2 > 131072) throw new Error("contextTokens must be a whole number between 1024 and 131072");
16423
+ return n2;
15866
16424
  }
15867
16425
  }
15868
16426
  };
@@ -15880,6 +16438,7 @@ var KEY_HELP = {
15880
16438
  allowedOrigins: "extra web addresses allowed to connect, separated by commas",
15881
16439
  allowedHosts: "extra host names that may reach Friend (for example my-pc), separated by commas",
15882
16440
  trustProxy: "true when Friend sits behind your own reverse proxy",
16441
+ "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
16442
  "voice.enabled": "true or false: use speech at all",
15884
16443
  "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
16444
  "voice.ttsVoice": "speaking voice (easier: friend voice setup, or Settings)",
@@ -15947,12 +16506,12 @@ import { resolve as resolve9 } from "node:path";
15947
16506
  import { WebSocket as WebSocket3 } from "ws";
15948
16507
 
15949
16508
  // ../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";
16509
+ import { closeSync, mkdirSync as mkdirSync13, openSync, readSync, readdirSync as readdirSync4, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
16510
+ import { dirname as dirname8, join as join19 } from "node:path";
15952
16511
 
15953
16512
  // ../agent-cli/src/tools/roots.ts
15954
16513
  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";
16514
+ import { dirname as dirname7, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve6, sep as sep3 } from "node:path";
15956
16515
  var isWin = process.platform === "win32";
15957
16516
  var norm = (p) => isWin ? p.toLowerCase() : p;
15958
16517
  var exists = (p) => {
@@ -15968,18 +16527,18 @@ function realish(abs) {
15968
16527
  let cur = abs;
15969
16528
  const tail2 = [];
15970
16529
  while (!exists(cur)) {
15971
- const parent = dirname6(cur);
16530
+ const parent = dirname7(cur);
15972
16531
  if (parent === cur) return abs;
15973
16532
  tail2.unshift(cur.slice(parent.length).replace(/^[\\/]+/, ""));
15974
16533
  cur = parent;
15975
16534
  }
15976
- let real2;
16535
+ let real3;
15977
16536
  try {
15978
- real2 = realpathSync.native(cur);
16537
+ real3 = realpathSync.native(cur);
15979
16538
  } catch {
15980
16539
  return UNRESOLVABLE + abs;
15981
16540
  }
15982
- return tail2.length ? resolve6(real2, ...tail2) : real2;
16541
+ return tail2.length ? resolve6(real3, ...tail2) : real3;
15983
16542
  }
15984
16543
  function within(root, target) {
15985
16544
  if (target.startsWith(UNRESOLVABLE)) return false;
@@ -16051,8 +16610,8 @@ function fsWrite(args, ctx) {
16051
16610
  if ("error" in g) return { ok: false, error: g.error };
16052
16611
  if (Buffer.byteLength(args.content) > MAX_WRITE) return { ok: false, error: "Content too large (5 MB limit)" };
16053
16612
  try {
16054
- mkdirSync12(dirname7(g.abs), { recursive: true });
16055
- writeFileSync11(g.abs, args.content);
16613
+ mkdirSync13(dirname8(g.abs), { recursive: true });
16614
+ writeFileSync12(g.abs, args.content);
16056
16615
  return { ok: true, output: `Wrote ${Buffer.byteLength(args.content)} bytes to ${g.abs}`, truncated: false };
16057
16616
  } catch (e) {
16058
16617
  return { ok: false, error: e.message };
@@ -16060,7 +16619,7 @@ function fsWrite(args, ctx) {
16060
16619
  }
16061
16620
 
16062
16621
  // ../agent-cli/src/tools/shell.ts
16063
- import { spawn as spawn7, spawnSync } from "node:child_process";
16622
+ import { spawn as spawn9, spawnSync } from "node:child_process";
16064
16623
  function killTree2(child) {
16065
16624
  if (!child.pid) return;
16066
16625
  if (process.platform === "win32") {
@@ -16086,7 +16645,7 @@ function runCommand2(command, cwdArg, ctx, argv) {
16086
16645
  const file = argv?.file ?? (win ? "powershell.exe" : "sh");
16087
16646
  const args = argv?.args ?? (win ? ["-NoProfile", "-NonInteractive", "-Command", command] : ["-c", command]);
16088
16647
  return new Promise((resolve13) => {
16089
- const child = spawn7(file, args, { cwd, windowsHide: true, detached: !win, stdio: ["ignore", "pipe", "pipe"] });
16648
+ const child = spawn9(file, args, { cwd, windowsHide: true, detached: !win, stdio: ["ignore", "pipe", "pipe"] });
16090
16649
  let out = "";
16091
16650
  let truncated = false;
16092
16651
  let timedOut = false;
@@ -16288,8 +16847,8 @@ function startDeviceNode(opts) {
16288
16847
 
16289
16848
  // ../agent-cli/src/commands/start.ts
16290
16849
  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";
16850
+ import { existsSync as existsSync19, readFileSync as readFileSync13, writeFileSync as writeFileSync14 } from "node:fs";
16851
+ import { dirname as dirname10, join as join21, resolve as resolve8 } from "node:path";
16293
16852
  import { fileURLToPath as fileURLToPath4 } from "node:url";
16294
16853
 
16295
16854
  // ../agent-cli/src/commands/lan.ts
@@ -16314,15 +16873,15 @@ function urlsFor(bind, port, ifaces = networkInterfaces2, https = false) {
16314
16873
  }
16315
16874
  var PORT_HELP = "The port must be a whole number from 1024 to 65535.";
16316
16875
  function parsePort(v) {
16317
- const n = Number(v);
16318
- if (v === void 0 || v.trim() === "" || !Number.isInteger(n) || n < 1024 || n > 65535) throw new Error(PORT_HELP);
16319
- return n;
16876
+ const n2 = Number(v);
16877
+ if (v === void 0 || v.trim() === "" || !Number.isInteger(n2) || n2 < 1024 || n2 > 65535) throw new Error(PORT_HELP);
16878
+ return n2;
16320
16879
  }
16321
16880
  function show(io2, bind, port, ifaces, https = false) {
16322
16881
  const u = urlsFor(bind, port, ifaces, https);
16323
16882
  io2.out(`Listens on: ${bind}:${port}`);
16324
16883
  io2.out(`On this PC: ${u.local}`);
16325
- if (u.network.length) for (const n of u.network) io2.out(`On your network: ${n}`);
16884
+ if (u.network.length) for (const n2 of u.network) io2.out(`On your network: ${n2}`);
16326
16885
  else if (isLoopbackBind(bind)) io2.out("On your network: not reachable (only this PC). Turn it on with: friend lan on");
16327
16886
  else io2.out("On your network: no network address found. Check that this PC is connected.");
16328
16887
  }
@@ -16394,9 +16953,9 @@ async function lanCommand(args, io2, ifaces = networkInterfaces2) {
16394
16953
  }
16395
16954
 
16396
16955
  // ../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";
16956
+ import { spawn as spawn10 } from "node:child_process";
16957
+ import { existsSync as existsSync18, mkdirSync as mkdirSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync13 } from "node:fs";
16958
+ import { dirname as dirname9, join as join20, resolve as resolve7 } from "node:path";
16400
16959
  import { fileURLToPath as fileURLToPath3 } from "node:url";
16401
16960
  function voicePaths(dataDir) {
16402
16961
  const home = join20(dataDir, "voice");
@@ -16411,19 +16970,19 @@ function voicePaths(dataDir) {
16411
16970
  }
16412
16971
  function readMarker(dataDir) {
16413
16972
  try {
16414
- return JSON.parse(readFileSync11(voicePaths(dataDir).marker, "utf8"));
16973
+ return JSON.parse(readFileSync12(voicePaths(dataDir).marker, "utf8"));
16415
16974
  } catch {
16416
16975
  return null;
16417
16976
  }
16418
16977
  }
16419
16978
  function findVoiceDir() {
16420
- const here2 = dirname8(fileURLToPath3(import.meta.url));
16979
+ const here2 = dirname9(fileURLToPath3(import.meta.url));
16421
16980
  return firstExistingDir([process.env.FRIEND_VOICE_DIR, resolve7(here2, "voice"), resolve7(here2, "../../../voice")], "friend_voice") ?? resolve7(here2, "voice");
16422
16981
  }
16423
16982
  var QUIET_ENV = { ...process.env, HF_HUB_DISABLE_SYMLINKS_WARNING: "1", PYTHONUTF8: "1" };
16424
16983
  var defaultRunner = (cmd, args, opts) => new Promise((res) => {
16425
16984
  let stdout = "";
16426
- const child = spawn8(cmd, args, { cwd: opts?.cwd, env: QUIET_ENV, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
16985
+ const child = spawn10(cmd, args, { cwd: opts?.cwd, env: QUIET_ENV, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
16427
16986
  const feed = (d) => {
16428
16987
  const s = d.toString();
16429
16988
  stdout += s;
@@ -16436,7 +16995,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((res) => {
16436
16995
  child.on("error", (e) => res({ code: 127, stdout: stdout + e.message }));
16437
16996
  child.on("close", (code) => res({ code: code ?? 1, stdout }));
16438
16997
  });
16439
- var tail = (s, n = 8) => s.trim().split(/\r?\n/).slice(-n).join("\n");
16998
+ var tail = (s, n2 = 8) => s.trim().split(/\r?\n/).slice(-n2).join("\n");
16440
16999
  async function findPython(run2) {
16441
17000
  const candidates = [
16442
17001
  ...process.env.FRIEND_PYTHON ? [[process.env.FRIEND_PYTHON, []]] : [],
@@ -16508,8 +17067,8 @@ async function voiceSetupCommand(io2, deps = {}, args = []) {
16508
17067
  io2.err("Python 3.10 or newer is required. Install it from https://www.python.org/downloads/ and run this again.");
16509
17068
  return 1;
16510
17069
  }
16511
- mkdirSync13(p.home, { recursive: true });
16512
- if (!existsSync17(p.python)) {
17070
+ mkdirSync14(p.home, { recursive: true });
17071
+ if (!existsSync18(p.python)) {
16513
17072
  io2.out("1/3 Creating a private Python environment\u2026");
16514
17073
  const r = await run2(py.cmd, [...py.args, "-m", "venv", p.venv], { onLine });
16515
17074
  if (r.code !== 0) {
@@ -16543,7 +17102,7 @@ ${tail(dl.stdout)}`);
16543
17102
  return 1;
16544
17103
  }
16545
17104
  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));
17105
+ writeFileSync13(p.marker, JSON.stringify(marker, null, 2));
16547
17106
  io2.out("Voice is ready. Start Friend with `friend start`, then hold Space to talk.");
16548
17107
  return 0;
16549
17108
  }
@@ -16559,7 +17118,7 @@ async function voiceStatusCommand(io2) {
16559
17118
  io2.out("Voice engine: not installed. Run: friend voice setup");
16560
17119
  return 0;
16561
17120
  }
16562
- if (!existsSync17(marker.python)) {
17121
+ if (!existsSync18(marker.python)) {
16563
17122
  io2.out("Voice engine: installed, but its Python is missing. Run: friend voice setup");
16564
17123
  return 0;
16565
17124
  }
@@ -16630,9 +17189,9 @@ async function voiceCommand(args, io2) {
16630
17189
 
16631
17190
  // ../agent-cli/src/commands/start.ts
16632
17191
  function findWebDir(explicit) {
16633
- const here2 = dirname9(fileURLToPath4(import.meta.url));
17192
+ const here2 = dirname10(fileURLToPath4(import.meta.url));
16634
17193
  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")));
17194
+ return candidates.find((c) => !!c && existsSync19(join21(c, "index.html")));
16636
17195
  }
16637
17196
  async function startFriend(opts) {
16638
17197
  const stored = loadConfig(opts.dataDir);
@@ -16657,6 +17216,8 @@ async function startFriend(opts) {
16657
17216
  }) : null;
16658
17217
  const voice2 = sidecar ? new HttpVoiceService({ baseUrl: () => sidecar.baseUrl(), token: secret }) : void 0;
16659
17218
  const app = await createApp({
17219
+ system: { checkForUpdates: true },
17220
+ // looks for a newer Friend after it starts, and updates by itself when that is on
16660
17221
  dataDir: opts.dataDir,
16661
17222
  webDir: findWebDir(opts.webDir),
16662
17223
  voice: voice2,
@@ -16667,21 +17228,21 @@ async function startFriend(opts) {
16667
17228
  });
16668
17229
  const { port } = await app.start();
16669
17230
  const tokenFile = join21(opts.dataDir, "local-device.token");
16670
- let token = existsSync18(tokenFile) ? readFileSync12(tokenFile, "utf8").trim() : "";
17231
+ let token = existsSync19(tokenFile) ? readFileSync13(tokenFile, "utf8").trim() : "";
16671
17232
  if (!token || !app.auth.resolveDevice(token)) {
16672
17233
  token = app.auth.redeemPairCode(app.auth.createPairCode(), "this-pc");
16673
- writeFileSync13(tokenFile, token, { mode: 384 });
17234
+ writeFileSync14(tokenFile, token, { mode: 384 });
16674
17235
  }
16675
17236
  const node = startDeviceNode({
16676
17237
  url: `${app.config.https ? "wss" : "ws"}://127.0.0.1:${port}/device`,
16677
17238
  token,
16678
17239
  roots: app.config.roots,
16679
- ...app.config.https ? { ca: readFileSync12(join21(opts.dataDir, "tls", "cert.pem"), "utf8") } : {}
17240
+ ...app.config.https ? { ca: readFileSync13(join21(opts.dataDir, "tls", "cert.pem"), "utf8") } : {}
16680
17241
  // this PC trusts only its own Friend's certificate
16681
17242
  });
16682
17243
  const urls = urlsFor(app.config.bind, port, void 0, app.config.https);
16683
17244
  opts.out(`Friend is running at ${urls.local}`);
16684
- for (const n of urls.network) opts.out(`Also on your network: ${n}`);
17245
+ for (const n2 of urls.network) opts.out(`Also on your network: ${n2}`);
16685
17246
  if (!app.auth.hasOwner()) opts.out("No owner yet. Run: friend onboard (guided), or friend setup (just the account)");
16686
17247
  const voiceAvailable = () => voice2?.available() ?? false;
16687
17248
  let lastAvailable = voiceAvailable();
@@ -16860,7 +17421,7 @@ async function memoryCommand(args, io2) {
16860
17421
  if (sub === "list") {
16861
17422
  const notes = memory.list();
16862
17423
  if (notes.length === 0) io2.out("No notes saved.");
16863
- for (const n of notes) io2.out(`#${n.id} ${n.text} (${n.source}, ${new Date(n.createdAt).toLocaleDateString()})`);
17424
+ for (const n2 of notes) io2.out(`#${n2.id} ${n2.text} (${n2.source}, ${new Date(n2.createdAt).toLocaleDateString()})`);
16864
17425
  return 0;
16865
17426
  }
16866
17427
  if (sub === "forget") {
@@ -16877,8 +17438,8 @@ async function memoryCommand(args, io2) {
16877
17438
  io2.out(`Forgot note #${id2}.`);
16878
17439
  return 0;
16879
17440
  }
16880
- const count2 = memory.count();
16881
- if (count2 === 0) {
17441
+ const count3 = memory.count();
17442
+ if (count3 === 0) {
16882
17443
  io2.out("No notes saved.");
16883
17444
  return 0;
16884
17445
  }
@@ -16895,8 +17456,8 @@ async function memoryCommand(args, io2) {
16895
17456
  return 1;
16896
17457
  }
16897
17458
  memory.clear();
16898
- audit.log("owner", "memory.clear", { count: count2, via: "cli" });
16899
- io2.out(`Deleted ${count2} notes.`);
17459
+ audit.log("owner", "memory.clear", { count: count3, via: "cli" });
17460
+ io2.out(`Deleted ${count3} notes.`);
16900
17461
  return 0;
16901
17462
  } finally {
16902
17463
  db.close();
@@ -16904,22 +17465,22 @@ async function memoryCommand(args, io2) {
16904
17465
  }
16905
17466
 
16906
17467
  // ../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";
17468
+ import { existsSync as existsSync20, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
17469
+ import { dirname as dirname11, join as join22, resolve as resolve10 } from "node:path";
16909
17470
  import { fileURLToPath as fileURLToPath5 } from "node:url";
16910
17471
  var USAGE2 = "Usage: friend plugin catalog | list | install <name|folder> [--yes] | enable <id> | disable <id> | remove <id>";
16911
17472
  function defaultCatalog() {
16912
- const here2 = dirname10(fileURLToPath5(import.meta.url));
17473
+ const here2 = dirname11(fileURLToPath5(import.meta.url));
16913
17474
  return firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve10(here2, "plugins"), resolve10(here2, "../../../../plugins")]) ?? resolve10(here2, "plugins");
16914
17475
  }
16915
- var count = (n, word2) => `${n} ${word2}${n === 1 ? "" : "s"}`;
17476
+ var count2 = (n2, word2) => `${n2} ${word2}${n2 === 1 ? "" : "s"}`;
16916
17477
  async function pluginCommand(args, io2, deps = {}) {
16917
17478
  const catalogDir = deps.catalogDir ?? defaultCatalog();
16918
17479
  const [sub, ...rest] = args;
16919
17480
  const flags = rest.filter((a) => a.startsWith("--"));
16920
17481
  const arg = rest.find((a) => !a.startsWith("--"));
16921
17482
  if (sub === "catalog") {
16922
- const dirs = existsSync19(catalogDir) ? readdirSync5(catalogDir).filter((d) => statSync5(join22(catalogDir, d)).isDirectory()) : [];
17483
+ const dirs = existsSync20(catalogDir) ? readdirSync5(catalogDir).filter((d) => statSync5(join22(catalogDir, d)).isDirectory()) : [];
16923
17484
  if (dirs.length === 0) io2.out("The catalog is empty.");
16924
17485
  for (const d of dirs.sort()) {
16925
17486
  const r = inspectPlugin(join22(catalogDir, d));
@@ -16928,7 +17489,7 @@ async function pluginCommand(args, io2, deps = {}) {
16928
17489
  continue;
16929
17490
  }
16930
17491
  const m = r.manifest;
16931
- io2.out(`${m.id} ${m.name} ${m.version} [${count(m.tools.length, "tool")}, ${count(m.skills.length, "skill")}] ${m.description}`);
17492
+ io2.out(`${m.id} ${m.name} ${m.version} [${count2(m.tools.length, "tool")}, ${count2(m.skills.length, "skill")}] ${m.description}`);
16932
17493
  }
16933
17494
  return 0;
16934
17495
  }
@@ -16952,8 +17513,8 @@ async function pluginCommand(args, io2, deps = {}) {
16952
17513
  io2.err(USAGE2);
16953
17514
  return 1;
16954
17515
  }
16955
- const inCatalog = !/[\\/]/.test(arg) && existsSync19(join22(catalogDir, arg));
16956
- const source = inCatalog ? join22(catalogDir, arg) : existsSync19(resolve10(arg)) ? resolve10(arg) : null;
17516
+ const inCatalog = !/[\\/]/.test(arg) && existsSync20(join22(catalogDir, arg));
17517
+ const source = inCatalog ? join22(catalogDir, arg) : existsSync20(resolve10(arg)) ? resolve10(arg) : null;
16957
17518
  if (!source) {
16958
17519
  io2.err(`"${arg}" is not in the catalog and there is no such folder. See: friend plugin catalog`);
16959
17520
  return 1;
@@ -17008,7 +17569,7 @@ async function pluginCommand(args, io2, deps = {}) {
17008
17569
 
17009
17570
  // ../agent-cli/src/commands/onboard/index.ts
17010
17571
  import crossSpawn4 from "cross-spawn";
17011
- import { mkdirSync as mkdirSync15 } from "node:fs";
17572
+ import { mkdirSync as mkdirSync16 } from "node:fs";
17012
17573
  import { resolve as resolve11 } from "node:path";
17013
17574
 
17014
17575
  // ../agent-cli/src/commands/ollama.ts
@@ -17331,16 +17892,16 @@ function installPlan(id2, platform, have) {
17331
17892
  }
17332
17893
 
17333
17894
  // ../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";
17895
+ import { existsSync as existsSync21, mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync10, writeFileSync as writeFileSync15 } from "node:fs";
17896
+ import { dirname as dirname12, join as join23 } from "node:path";
17336
17897
  var FEATURES = ["claude-code", "claude-api", "ollama", "voice", "sandbox"];
17337
17898
  var emptyState = () => ({ version: 1, done: {}, features: [], restart: [] });
17338
17899
  var statePath = (dataDir) => join23(dataDir, "onboarding.json");
17339
17900
  function loadState(dataDir) {
17340
17901
  const f = statePath(dataDir);
17341
- if (!existsSync20(f)) return emptyState();
17902
+ if (!existsSync21(f)) return emptyState();
17342
17903
  try {
17343
- const raw = JSON.parse(readFileSync13(f, "utf8"));
17904
+ const raw = JSON.parse(readFileSync14(f, "utf8"));
17344
17905
  const done = raw.done && typeof raw.done === "object" ? Object.fromEntries(Object.entries(raw.done).filter(([, v]) => typeof v === "string")) : {};
17345
17906
  const features2 = Array.isArray(raw.features) ? raw.features.filter((x) => FEATURES.includes(x)) : [];
17346
17907
  const restart = Array.isArray(raw.restart) ? raw.restart.filter((x) => typeof x === "string") : [];
@@ -17351,13 +17912,13 @@ function loadState(dataDir) {
17351
17912
  }
17352
17913
  function saveState(dataDir, state) {
17353
17914
  const f = statePath(dataDir);
17354
- mkdirSync14(dirname11(f), { recursive: true });
17915
+ mkdirSync15(dirname12(f), { recursive: true });
17355
17916
  const tmp = `${f}.tmp`;
17356
- writeFileSync14(tmp, JSON.stringify(state, null, 2));
17917
+ writeFileSync15(tmp, JSON.stringify(state, null, 2));
17357
17918
  renameSync4(tmp, f);
17358
17919
  }
17359
17920
  function resetState(dataDir) {
17360
- rmSync9(statePath(dataDir), { force: true });
17921
+ rmSync10(statePath(dataDir), { force: true });
17361
17922
  }
17362
17923
 
17363
17924
  // ../agent-cli/src/commands/onboard/ui.ts
@@ -17367,9 +17928,9 @@ function pick(options, typed) {
17367
17928
  if (/^\d+$/.test(a)) return options[Number(a) - 1];
17368
17929
  return options.find((o) => o.key.toLowerCase() === a || o.label.toLowerCase() === a || o.label.toLowerCase().split(/\s+/)[0] === a);
17369
17930
  }
17370
- function heading(ui, n, total, title2) {
17931
+ function heading(ui, n2, total, title2) {
17371
17932
  ui.io.out("");
17372
- ui.io.out(`\u2500\u2500 Step ${n} of ${total} \xB7 ${title2} ${"\u2500".repeat(Math.max(2, 52 - title2.length))}`);
17933
+ ui.io.out(`\u2500\u2500 Step ${n2} of ${total} \xB7 ${title2} ${"\u2500".repeat(Math.max(2, 52 - title2.length))}`);
17373
17934
  }
17374
17935
  async function choose(ui, question, options, suggested) {
17375
17936
  const { io: io2 } = ui;
@@ -17709,7 +18270,7 @@ async function workspace(c) {
17709
18270
  const answer = await text2(c.ui, current ? "Workspace folder" : "Workspace folder (Enter to skip)", current ?? "");
17710
18271
  if (answer) {
17711
18272
  const abs = resolve11(answer);
17712
- mkdirSync15(abs, { recursive: true });
18273
+ mkdirSync16(abs, { recursive: true });
17713
18274
  cfg.roots = [abs];
17714
18275
  saveConfig(cfg);
17715
18276
  io2.out(`Workspace: ${abs}`);
@@ -17747,7 +18308,7 @@ async function network(c) {
17747
18308
  saveConfig(cfg);
17748
18309
  const u = urlsFor(cfg.bind, cfg.port, void 0, cfg.https);
17749
18310
  io2.out(`Friend will be at ${u.local}`);
17750
- for (const n of u.network) io2.out(`and on your network at ${n}`);
18311
+ for (const n2 of u.network) io2.out(`and on your network at ${n2}`);
17751
18312
  if (where === "lan" && !cfg.https) io2.out("Note: the microphone and camera only work on this PC (or over HTTPS). Everything else works from the other devices. Later: friend lan https on");
17752
18313
  return "done";
17753
18314
  }
@@ -17879,19 +18440,102 @@ async function doctorCommand(io2, partial = {}) {
17879
18440
  return requiredMissing ? 1 : 0;
17880
18441
  }
17881
18442
 
18443
+ // ../agent-cli/src/commands/autostart.ts
18444
+ var USAGE3 = "Usage: friend autostart on|off|status";
18445
+ var real = (io2) => new Autostart(realAutostartDeps(io2.dataDir), currentSelf);
18446
+ async function autostartCommand(args, io2, autostart = real(io2)) {
18447
+ switch (args[0]) {
18448
+ case "on":
18449
+ case "off": {
18450
+ const r = await (args[0] === "on" ? autostart.enable() : autostart.disable());
18451
+ if (!r.ok) {
18452
+ io2.err(r.message);
18453
+ return 1;
18454
+ }
18455
+ io2.out(r.message);
18456
+ if (args[0] === "on") io2.out("Turn it off again with: friend autostart off");
18457
+ return 0;
18458
+ }
18459
+ case "status": {
18460
+ const s = await autostart.status();
18461
+ if (!s.supported) {
18462
+ io2.out("Starting Friend by itself is not supported on this kind of computer.");
18463
+ return 0;
18464
+ }
18465
+ io2.out(`Starts by itself when you sign in: ${s.enabled ? "yes" : "no"}${s.enabled ? ` (${s.kind})` : ""}`);
18466
+ if (!s.enabled) io2.out("Turn it on with: friend autostart on");
18467
+ return 0;
18468
+ }
18469
+ case "refresh":
18470
+ await autostart.refresh();
18471
+ return 0;
18472
+ default:
18473
+ io2.err(USAGE3);
18474
+ return 1;
18475
+ }
18476
+ }
18477
+
18478
+ // ../agent-cli/src/commands/usage.ts
18479
+ var USAGE4 = "Usage: friend usage [--days N] | friend usage clear";
18480
+ var n = (v) => v.toLocaleString("en-US");
18481
+ var row = (label4, input, output, turns, width) => `${label4.padEnd(width)} ${n(input).padStart(12)} ${n(output).padStart(12)} ${String(turns).padStart(6)}`;
18482
+ async function usageCommand(args, io2, log) {
18483
+ let days = 7;
18484
+ let clear = false;
18485
+ for (let i = 0; i < args.length; i++) {
18486
+ const a = args[i];
18487
+ if (a === "clear") clear = true;
18488
+ else if (a === "--days" && /^\d+$/.test(args[i + 1] ?? "")) days = Number(args[++i]);
18489
+ else {
18490
+ io2.err(USAGE4);
18491
+ return 1;
18492
+ }
18493
+ }
18494
+ const db = log ? null : openDb(io2.dataDir);
18495
+ try {
18496
+ const l = log ?? new UsageLog(db);
18497
+ if (clear) {
18498
+ io2.out(`Removed ${l.clear()} saved counts.`);
18499
+ return 0;
18500
+ }
18501
+ const s = l.summary(days);
18502
+ if (s.total.turns === 0) {
18503
+ io2.out(`Nothing used in the last ${s.range} days.`);
18504
+ io2.out("Friend keeps counts only (no words), once a model has answered.");
18505
+ return 0;
18506
+ }
18507
+ io2.out(`Tokens used in the last ${s.range} days`);
18508
+ io2.out("");
18509
+ io2.out(`${"Day".padEnd(10)} ${"Input".padStart(12)} ${"Output".padStart(12)} ${"Turns".padStart(6)}`);
18510
+ for (const d of s.days) io2.out(row(d.day, d.input, d.output, d.turns, 10));
18511
+ io2.out(row("Total", s.total.input, s.total.output, s.total.turns, 10));
18512
+ io2.out("");
18513
+ io2.out("By model");
18514
+ for (const m of s.byModel) io2.out(row(`${m.provider} / ${m.model}`, m.input, m.output, m.turns, 28));
18515
+ io2.out("");
18516
+ io2.out("By where it came from");
18517
+ for (const m of s.bySource) io2.out(row(m.source, m.input, m.output, m.turns, 28));
18518
+ io2.out("");
18519
+ io2.out("Friend keeps counts only (no words). Remove them with: friend usage clear");
18520
+ return 0;
18521
+ } finally {
18522
+ db?.close();
18523
+ }
18524
+ }
18525
+
17882
18526
  // ../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";
18527
+ import { spawn as spawn11, spawnSync as spawnSync2 } from "node:child_process";
18528
+ 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
18529
  import { connect } from "node:net";
17886
18530
  import { join as join24 } from "node:path";
17887
- var USAGE3 = "Usage: friend daemon start | stop | restart | status";
18531
+ var USAGE5 = "Usage: friend daemon start | stop | restart | status";
17888
18532
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
17889
18533
  var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
17890
18534
  var realDeps = {
17891
18535
  launch(logFile) {
17892
18536
  const fd = openSync2(logFile, "a");
17893
18537
  try {
17894
- const child = spawn9(process.execPath, [process.argv[1], "start"], { detached: true, stdio: ["ignore", fd, fd], windowsHide: true });
18538
+ const child = spawn11(process.execPath, [process.argv[1], "start"], { detached: true, stdio: ["ignore", fd, fd], windowsHide: true, env: { ...process.env, FRIEND_DAEMON: "1" } });
17895
18539
  child.unref();
17896
18540
  return child.pid;
17897
18541
  } finally {
@@ -17943,7 +18587,7 @@ var pidFile = (dataDir) => join24(dataDir, "friend.pid");
17943
18587
  var logFileOf = (dataDir) => join24(dataDir, "friend.log");
17944
18588
  function readPid(dataDir) {
17945
18589
  try {
17946
- const r = JSON.parse(readFileSync14(pidFile(dataDir), "utf8"));
18590
+ const r = JSON.parse(readFileSync15(pidFile(dataDir), "utf8"));
17947
18591
  return Number.isInteger(r.pid) && r.pid > 0 ? r : null;
17948
18592
  } catch {
17949
18593
  return null;
@@ -17952,7 +18596,7 @@ function readPid(dataDir) {
17952
18596
  function running(dataDir, deps) {
17953
18597
  const r = readPid(dataDir);
17954
18598
  if (r && deps.alive(r.pid)) return r;
17955
- if (r) rmSync10(pidFile(dataDir), { force: true });
18599
+ if (r) rmSync11(pidFile(dataDir), { force: true });
17956
18600
  return null;
17957
18601
  }
17958
18602
  function since(ms2) {
@@ -17974,10 +18618,10 @@ async function start(io2, deps) {
17974
18618
  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
18619
  return 1;
17976
18620
  }
17977
- mkdirSync16(io2.dataDir, { recursive: true });
18621
+ mkdirSync17(io2.dataDir, { recursive: true });
17978
18622
  const log = logFileOf(io2.dataDir);
17979
18623
  try {
17980
- if (existsSync21(log) && statSync6(log).size > MAX_LOG_BYTES) renameSync5(log, `${log}.old`);
18624
+ if (existsSync22(log) && statSync6(log).size > MAX_LOG_BYTES) renameSync5(log, `${log}.old`);
17981
18625
  } catch {
17982
18626
  }
17983
18627
  const pid = deps.launch(log);
@@ -17985,18 +18629,18 @@ async function start(io2, deps) {
17985
18629
  io2.err("Could not start Friend in the background.");
17986
18630
  return 1;
17987
18631
  }
17988
- writeFileSync15(pidFile(io2.dataDir), JSON.stringify({ pid, startedAt: deps.now() }));
18632
+ writeFileSync16(pidFile(io2.dataDir), JSON.stringify({ pid, startedAt: deps.now() }));
17989
18633
  for (let i = 0; i < 60; i++) {
17990
18634
  if (await deps.portOpen(cfg.port, 300)) {
17991
18635
  io2.out(`Friend is running in the background (pid ${pid}) at ${urls.local}`);
17992
- for (const n of urls.network) io2.out(`Also on your network: ${n}`);
18636
+ for (const n2 of urls.network) io2.out(`Also on your network: ${n2}`);
17993
18637
  io2.out(`Stop it with: friend daemon stop (log: ${log})`);
17994
18638
  return 0;
17995
18639
  }
17996
18640
  if (!deps.alive(pid)) break;
17997
18641
  await sleep(250);
17998
18642
  }
17999
- rmSync10(pidFile(io2.dataDir), { force: true });
18643
+ rmSync11(pidFile(io2.dataDir), { force: true });
18000
18644
  io2.err(`Friend did not start. See the log: ${log}`);
18001
18645
  return 1;
18002
18646
  }
@@ -18011,7 +18655,7 @@ async function stop(io2, deps) {
18011
18655
  io2.err(`Could not stop Friend (pid ${r.pid}). End that process yourself.`);
18012
18656
  return 1;
18013
18657
  }
18014
- rmSync10(pidFile(io2.dataDir), { force: true });
18658
+ rmSync11(pidFile(io2.dataDir), { force: true });
18015
18659
  io2.out("Friend stopped.");
18016
18660
  return 0;
18017
18661
  }
@@ -18022,7 +18666,7 @@ async function status(io2, deps) {
18022
18666
  if (r) {
18023
18667
  io2.out(`Friend is running in the background (pid ${r.pid}, for ${since(deps.now() - r.startedAt)}).`);
18024
18668
  io2.out(`Address: ${urls.local}`);
18025
- for (const n of urls.network) io2.out(`Also on your network: ${n}`);
18669
+ for (const n2 of urls.network) io2.out(`Also on your network: ${n2}`);
18026
18670
  io2.out(`Log: ${logFileOf(io2.dataDir)}`);
18027
18671
  return 0;
18028
18672
  }
@@ -18043,25 +18687,27 @@ async function daemonCommand(args, io2, deps = realDeps) {
18043
18687
  case "status":
18044
18688
  return status(io2, deps);
18045
18689
  default:
18046
- io2.err(USAGE3);
18690
+ io2.err(USAGE5);
18047
18691
  return 1;
18048
18692
  }
18049
18693
  }
18050
18694
 
18051
18695
  // ../agent-cli/src/commands/upgrade.ts
18052
18696
  import crossSpawn5 from "cross-spawn";
18053
- var PACKAGE = "friend-assistant";
18054
- var real = {
18055
- current: () => true ? "0.1.44" : "development",
18697
+ import { appendFileSync } from "node:fs";
18698
+ import { join as join25 } from "node:path";
18699
+ var PACKAGE2 = "friend-assistant";
18700
+ var real2 = {
18701
+ current: () => true ? "0.1.46" : "development",
18056
18702
  latest: () => new Promise((resolve13) => {
18057
- const child = crossSpawn5("npm", ["view", PACKAGE, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
18703
+ const child = crossSpawn5("npm", ["view", PACKAGE2, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
18058
18704
  let out = "";
18059
18705
  child.stdout?.on("data", (d) => out += d.toString());
18060
18706
  child.on("error", () => resolve13(null));
18061
18707
  child.on("close", (code) => resolve13(code === 0 && /^\d+\.\d+\.\d+/.test(out.trim()) ? out.trim().split(/\s+/)[0] : null));
18062
18708
  }),
18063
18709
  install: (version) => new Promise((resolve13) => {
18064
- const child = crossSpawn5("npm", ["install", "-g", `${PACKAGE}@${version}`, "--prefer-online"], { stdio: "inherit" });
18710
+ const child = crossSpawn5("npm", ["install", "-g", `${PACKAGE2}@${version}`, "--prefer-online"], { stdio: "inherit" });
18065
18711
  child.on("error", () => resolve13(1));
18066
18712
  child.on("close", (code) => resolve13(code ?? 1));
18067
18713
  }),
@@ -18070,20 +18716,39 @@ var real = {
18070
18716
  child.on("error", () => resolve13(1));
18071
18717
  child.on("close", (code) => resolve13(code ?? 1));
18072
18718
  }),
18719
+ refreshAutostart: () => new Promise((resolve13) => {
18720
+ const child = crossSpawn5("friend", ["autostart", "refresh"], { stdio: "ignore" });
18721
+ child.on("error", () => resolve13());
18722
+ child.on("close", () => resolve13());
18723
+ }),
18073
18724
  daemon: realDeps
18074
18725
  };
18075
18726
  var newer = (a, b) => {
18076
- const n = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
18077
- const [x, y] = [n(a), n(b)];
18727
+ const n2 = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
18728
+ const [x, y] = [n2(a), n2(b)];
18078
18729
  for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
18079
18730
  return false;
18080
18731
  };
18081
- async function upgradeCommand(args, io2, deps = real) {
18732
+ async function upgradeCommand(args, io0, deps = real2) {
18082
18733
  const force = args.includes("--force");
18083
- if (args.some((a) => a !== "--force")) {
18084
- io2.err("Usage: friend upgrade [--force]");
18734
+ const auto = args.includes("--auto");
18735
+ if (args.some((a) => a !== "--force" && a !== "--auto")) {
18736
+ io0.err("Usage: friend upgrade [--force] [--auto]");
18085
18737
  return 1;
18086
18738
  }
18739
+ const log = deps.log ?? ((line) => appendFileSync(join25(io0.dataDir, "update.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
18740
+ `));
18741
+ const io2 = auto ? {
18742
+ ...io0,
18743
+ out: (l) => {
18744
+ io0.out(l);
18745
+ log(l);
18746
+ },
18747
+ err: (l) => {
18748
+ io0.err(l);
18749
+ log(l);
18750
+ }
18751
+ } : io0;
18087
18752
  const current = deps.current();
18088
18753
  io2.out(`Installed: ${current}`);
18089
18754
  const latest = await deps.latest();
@@ -18106,7 +18771,7 @@ async function upgradeCommand(args, io2, deps = real) {
18106
18771
  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
18772
  return 1;
18108
18773
  }
18109
- io2.out(`Installing ${PACKAGE}@${latest}\u2026`);
18774
+ io2.out(`Installing ${PACKAGE2}@${latest}\u2026`);
18110
18775
  const code = await deps.install(latest);
18111
18776
  if (code !== 0) {
18112
18777
  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 +18779,8 @@ async function upgradeCommand(args, io2, deps = real) {
18114
18779
  return 1;
18115
18780
  }
18116
18781
  io2.out(`Upgraded ${current} \u2192 ${latest}.`);
18782
+ await deps.refreshAutostart?.().catch(() => {
18783
+ });
18117
18784
  if (wasRunning) return deps.startAgain();
18118
18785
  io2.out("Start it with: friend start (or in the background: friend daemon start)");
18119
18786
  return 0;
@@ -18187,7 +18854,7 @@ async function claudeCommand(args, io2, deps = {}) {
18187
18854
  }
18188
18855
 
18189
18856
  // ../agent-cli/src/commands/setup.ts
18190
- import { mkdirSync as mkdirSync17 } from "node:fs";
18857
+ import { mkdirSync as mkdirSync18 } from "node:fs";
18191
18858
  import { resolve as resolve12 } from "node:path";
18192
18859
  async function setupCommand3(io2) {
18193
18860
  const db = openDb(io2.dataDir);
@@ -18213,7 +18880,7 @@ async function setupCommand3(io2) {
18213
18880
  const cfg = loadConfig(io2.dataDir);
18214
18881
  if (workspace2) {
18215
18882
  const abs = resolve12(workspace2);
18216
- mkdirSync17(abs, { recursive: true });
18883
+ mkdirSync18(abs, { recursive: true });
18217
18884
  cfg.roots = [abs];
18218
18885
  }
18219
18886
  if (apiKey) cfg.apiKey = apiKey;
@@ -18239,16 +18906,16 @@ async function secretCommand(kind, io2) {
18239
18906
  io2.err("There is no owner yet. Run: friend setup");
18240
18907
  return 1;
18241
18908
  }
18242
- const label3 = kind === "password" ? "password" : "PIN";
18243
- const first2 = await io2.ask(`New ${label3} (${kind === "password" ? "at least 8 characters" : "at least 6 digits"}): `, { hidden: true });
18909
+ const label4 = kind === "password" ? "password" : "PIN";
18910
+ const first2 = await io2.ask(`New ${label4} (${kind === "password" ? "at least 8 characters" : "at least 6 digits"}): `, { hidden: true });
18244
18911
  const problem = Auth.checkNewSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
18245
18912
  if (problem) {
18246
18913
  io2.err(problem);
18247
18914
  return 1;
18248
18915
  }
18249
- const again = await io2.ask(`Type the new ${label3} again: `, { hidden: true });
18916
+ const again = await io2.ask(`Type the new ${label4} again: `, { hidden: true });
18250
18917
  if (again !== first2) {
18251
- io2.err(`The two ${label3}s are not the same. Nothing was changed.`);
18918
+ io2.err(`The two ${label4}s are not the same. Nothing was changed.`);
18252
18919
  return 1;
18253
18920
  }
18254
18921
  await auth.resetSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
@@ -18269,7 +18936,7 @@ async function statusCommand3(io2) {
18269
18936
  io2.out(`Data folder: ${cfg.dataDir}`);
18270
18937
  io2.out(`Owner set up: ${owner}`);
18271
18938
  io2.out(`Listens on: ${cfg.bind}:${cfg.port}`);
18272
- for (const n of urlsFor(cfg.bind, cfg.port, void 0, cfg.https).network) io2.out(`Network URL: ${n}`);
18939
+ for (const n2 of urlsFor(cfg.bind, cfg.port, void 0, cfg.https).network) io2.out(`Network URL: ${n2}`);
18273
18940
  io2.out(`Public URL: ${cfg.publicUrl ?? "(none)"}`);
18274
18941
  io2.out(`Workspace: ${cfg.roots.length ? cfg.roots.join(", ") : "(none, every file action asks first)"}`);
18275
18942
  io2.out(`Model: ${cfg.model}`);
@@ -18278,7 +18945,7 @@ async function statusCommand3(io2) {
18278
18945
  }
18279
18946
 
18280
18947
  // ../agent-cli/src/cli.ts
18281
- var USAGE4 = `Usage: friend <command>
18948
+ var USAGE6 = `Usage: friend <command>
18282
18949
 
18283
18950
  onboard guided setup, step by step: checks this PC, installs what is missing, asks what you want
18284
18951
  doctor check what this PC has and what is missing (changes nothing)
@@ -18288,6 +18955,8 @@ var USAGE4 = `Usage: friend <command>
18288
18955
  start run the gateway and a local device node
18289
18956
  daemon start|stop|restart|status run Friend in the background (no terminal needed), stop it, or see if it is running
18290
18957
  upgrade [--force] stop the background Friend, install the newest version, start it again
18958
+ usage [--days N]|clear tokens used, day by day (counts only, never words)
18959
+ autostart on|off|status start Friend by itself when you sign in to this computer (Windows, macOS, Linux)
18291
18960
  pair print a one-time code to connect another PC
18292
18961
  node --url <ws-url> --code <code> [--roots a,b] [--name pc]
18293
18962
  connect this PC to a Friend gateway
@@ -18320,6 +18989,10 @@ async function run(argv, io2) {
18320
18989
  return await daemonCommand(rest, io2);
18321
18990
  case "upgrade":
18322
18991
  return await upgradeCommand(rest, io2);
18992
+ case "usage":
18993
+ return await usageCommand(rest, io2);
18994
+ case "autostart":
18995
+ return await autostartCommand(rest, io2);
18323
18996
  case "pair":
18324
18997
  return await pairCommand(io2);
18325
18998
  case "node":
@@ -18335,12 +19008,12 @@ async function run(argv, io2) {
18335
19008
  case "help":
18336
19009
  case "--help":
18337
19010
  case "-h":
18338
- io2.out(USAGE4);
19011
+ io2.out(USAGE6);
18339
19012
  return 0;
18340
19013
  case "version":
18341
19014
  case "--version":
18342
19015
  case "-v":
18343
- io2.out(true ? "0.1.44" : "development");
19016
+ io2.out(true ? "0.1.46" : "development");
18344
19017
  return 0;
18345
19018
  case "lan":
18346
19019
  return await lanCommand(rest, io2);
@@ -18359,7 +19032,7 @@ async function run(argv, io2) {
18359
19032
  case "config":
18360
19033
  return await configCommand(rest, io2);
18361
19034
  default:
18362
- io2.err(USAGE4);
19035
+ io2.err(USAGE6);
18363
19036
  return 1;
18364
19037
  }
18365
19038
  } catch (e) {