friend-assistant 0.1.45 → 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)))];
@@ -3362,7 +3367,7 @@ var DEFAULT_VOICE = {
3362
3367
  duckPercent: 25
3363
3368
  };
3364
3369
  function sanitizeVoice(v) {
3365
- 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;
3366
3371
  const percent2 = typeof v.duckPercent === "number" && Number.isFinite(v.duckPercent) ? Math.max(0, Math.min(100, Math.round(v.duckPercent))) : DEFAULT_VOICE.duckPercent;
3367
3372
  const base = { ...v, duck: typeof v.duck === "boolean" ? v.duck : DEFAULT_VOICE.duck, duckPercent: percent2 };
3368
3373
  if (ok(v.accept) && ok(v.challenge) && v.accept > v.challenge) return base;
@@ -3557,7 +3562,7 @@ function integrationViews(config, hub, brokers, alexa, spotify, soundcloud, audi
3557
3562
  const vdNeeds = () => {
3558
3563
  if (!vs) return [];
3559
3564
  const need = [{ name: "yt-dlp", h: vs.ytdlp }, ...vdMpvWindow ? [] : [{ name: "ffmpeg", h: vs.ffmpeg }]];
3560
- 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 } : {} }));
3561
3566
  };
3562
3567
  const vdSteps = [
3563
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 },
@@ -3897,7 +3902,7 @@ function inspectPlugin(dir) {
3897
3902
  }
3898
3903
  const manifest = parsed.data;
3899
3904
  const seen = (names, kind) => {
3900
- const dup2 = names.find((n, i) => names.indexOf(n) !== i);
3905
+ const dup2 = names.find((n2, i) => names.indexOf(n2) !== i);
3901
3906
  return dup2 ? `The ${kind} name "${dup2}" is used twice.` : null;
3902
3907
  };
3903
3908
  const dup = seen(manifest.skills.map((s) => s.name), "skill") ?? seen(manifest.tools.map((t) => t.name), "tool");
@@ -4317,9 +4322,9 @@ var ConnectorStore = class {
4317
4322
  return true;
4318
4323
  }
4319
4324
  remove(id2) {
4320
- const n = this.items.length;
4325
+ const n2 = this.items.length;
4321
4326
  this.items = this.items.filter((i) => i.id !== id2);
4322
- if (this.items.length === n) return false;
4327
+ if (this.items.length === n2) return false;
4323
4328
  this.save();
4324
4329
  return true;
4325
4330
  }
@@ -4339,10 +4344,10 @@ var hasSecrets = (c) => c.kind === "stdio" ? Object.keys(c.env).length > 0 : Obj
4339
4344
  var MAX_RESULT_BYTES = 1024 * 1024;
4340
4345
  var MAX_TOOL_PART = 28;
4341
4346
  function friendlyToolName(original, taken) {
4342
- let n = original.replace(/[^A-Za-z0-9_-]/g, "-").replace(/_{2,}/g, "_").replace(/^[-_]+/, "") || "tool";
4343
- if (n.length > MAX_TOOL_PART) n = `${n.slice(0, MAX_TOOL_PART - 7)}_${createHash2("sha1").update(original).digest("hex").slice(0, 5)}`;
4344
- let out = n;
4345
- 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}`;
4346
4351
  taken.add(out);
4347
4352
  return out;
4348
4353
  }
@@ -4978,10 +4983,10 @@ import { join as join9 } from "node:path";
4978
4983
  import { deflateRawSync } from "node:zlib";
4979
4984
  var TABLE = (() => {
4980
4985
  const t = new Uint32Array(256);
4981
- for (let n = 0; n < 256; n++) {
4982
- let c = n;
4986
+ for (let n2 = 0; n2 < 256; n2++) {
4987
+ let c = n2;
4983
4988
  for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
4984
- t[n] = c >>> 0;
4989
+ t[n2] = c >>> 0;
4985
4990
  }
4986
4991
  return t;
4987
4992
  })();
@@ -5544,8 +5549,8 @@ var AudiusHub = class {
5544
5549
  };
5545
5550
  }
5546
5551
  async search(query, limit) {
5547
- const n = Math.max(1, Math.min(50, Math.floor(limit)));
5548
- 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) });
5549
5554
  return (Array.isArray(j) ? j : []).flatMap((t) => this.of(t) ?? []);
5550
5555
  }
5551
5556
  async playlists(_limit) {
@@ -5578,7 +5583,7 @@ var AudiusHub = class {
5578
5583
  }
5579
5584
  }
5580
5585
  /** Fetches the audio from a checked address, following at most three redirects and only to checked addresses. */
5581
- async fetchAudio(address, range, signal) {
5586
+ async fetchAudio(address, range2, signal) {
5582
5587
  let next = address;
5583
5588
  for (let hop = 0; hop <= MAX_HOPS; hop++) {
5584
5589
  if (!isAudiusAddress(next, hop > 0) || !await this.publicName(next)) return { ok: false, error: "Audius sent the audio somewhere Friend will not follow." };
@@ -5588,7 +5593,7 @@ var AudiusHub = class {
5588
5593
  const timer = setTimeout(() => ctl.abort(), this.d.requestTimeoutMs ?? 8e3);
5589
5594
  const stop2 = signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal;
5590
5595
  try {
5591
- 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 });
5592
5597
  } finally {
5593
5598
  clearTimeout(timer);
5594
5599
  }
@@ -5842,14 +5847,14 @@ var zipExtract = async (archive, plan, dest) => {
5842
5847
  await fh.read(tail2, 0, tailLen, size - tailLen);
5843
5848
  const eocd = tail2.lastIndexOf(Buffer.from([80, 75, 5, 6]));
5844
5849
  if (eocd < 0) throw new Error("not a zip file");
5845
- const count2 = tail2.readUInt16LE(eocd + 10);
5850
+ const count3 = tail2.readUInt16LE(eocd + 10);
5846
5851
  const cdSize = tail2.readUInt32LE(eocd + 12);
5847
5852
  const cdOffset = tail2.readUInt32LE(eocd + 16);
5848
5853
  if (cdOffset === 4294967295 || cdSize === 4294967295) throw new Error("zip64 archives are not supported");
5849
5854
  const cd = Buffer.alloc(cdSize);
5850
5855
  await fh.read(cd, 0, cdSize, cdOffset);
5851
5856
  let p = 0;
5852
- for (let i = 0; i < count2 && p + 46 <= cd.length; i++) {
5857
+ for (let i = 0; i < count3 && p + 46 <= cd.length; i++) {
5853
5858
  if (cd.readUInt32LE(p) !== 33639248) break;
5854
5859
  const method = cd.readUInt16LE(p + 10);
5855
5860
  const csize = cd.readUInt32LE(p + 20);
@@ -6395,7 +6400,7 @@ var VideoHub = class {
6395
6400
  return this.tokens.has(id2);
6396
6401
  }
6397
6402
  missing(status2, names) {
6398
- 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);
6399
6404
  }
6400
6405
  /** Downloads what the plugin needs (if the owner allows it) and looks again. Called when the plugin is installed or switched on. */
6401
6406
  async prepare() {
@@ -6598,9 +6603,9 @@ var VideoHub = class {
6598
6603
  return { ok: true, text: "On the Friend screen the video has its own buttons for pause, play and volume: use those." };
6599
6604
  }
6600
6605
  nowPlaying() {
6601
- const n = this.now;
6602
- if (!n) return "Nothing is playing.";
6603
- 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"}.`;
6604
6609
  }
6605
6610
  /** What is found and what is missing, in words, for the assistant to pass on. */
6606
6611
  async check() {
@@ -6769,8 +6774,8 @@ var Auth = class _Auth {
6769
6774
  }
6770
6775
  async login(password, ip) {
6771
6776
  const succeeded = this.begin("login", ip);
6772
- const row = this.db.prepare("select password_hash h from owner where id = 1").get();
6773
- 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;
6774
6779
  if (!ok) {
6775
6780
  this.audit.log("anonymous", "login.fail", { ip });
6776
6781
  throw new Error("Invalid credentials");
@@ -6801,8 +6806,8 @@ var Auth = class _Auth {
6801
6806
  }
6802
6807
  async verifyPin(pin, ip) {
6803
6808
  const succeeded = this.begin("pin", ip);
6804
- const row = this.db.prepare("select pin_hash h from owner where id = 1").get();
6805
- 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;
6806
6811
  if (ok) succeeded();
6807
6812
  else this.audit.log("owner", "pin.fail", { ip });
6808
6813
  return ok;
@@ -6826,8 +6831,8 @@ var Auth = class _Auth {
6826
6831
  const problem = _Auth.checkNewSecrets(input);
6827
6832
  if (problem) throw new Error(problem);
6828
6833
  const succeeded = this.begin("login", ip);
6829
- const row = this.db.prepare("select password_hash h from owner where id = 1").get();
6830
- 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;
6831
6836
  if (!ok) {
6832
6837
  this.audit.log("owner", "secrets.fail", { ip });
6833
6838
  throw new Error("Invalid credentials");
@@ -7123,7 +7128,7 @@ var MAX_TEXT = 2e5;
7123
7128
  var MAX_CSV_ROWS = 200;
7124
7129
  function parseCsv(text3) {
7125
7130
  const rows3 = [];
7126
- let row = [];
7131
+ let row2 = [];
7127
7132
  let cell = "";
7128
7133
  let quoted2 = false;
7129
7134
  for (let i = 0; i < text3.length; i++) {
@@ -7136,19 +7141,19 @@ function parseCsv(text3) {
7136
7141
  else cell += ch;
7137
7142
  } else if (ch === '"') quoted2 = true;
7138
7143
  else if (ch === ",") {
7139
- row.push(cell);
7144
+ row2.push(cell);
7140
7145
  cell = "";
7141
7146
  } else if (ch === "\n" || ch === "\r") {
7142
7147
  if (ch === "\r" && text3[i + 1] === "\n") i++;
7143
- row.push(cell);
7148
+ row2.push(cell);
7144
7149
  cell = "";
7145
- if (row.some((c) => c !== "")) rows3.push(row);
7146
- row = [];
7150
+ if (row2.some((c) => c !== "")) rows3.push(row2);
7151
+ row2 = [];
7147
7152
  if (rows3.length > MAX_CSV_ROWS + 1) return rows3;
7148
7153
  } else cell += ch;
7149
7154
  }
7150
- row.push(cell);
7151
- if (row.some((c) => c !== "")) rows3.push(row);
7155
+ row2.push(cell);
7156
+ if (row2.some((c) => c !== "")) rows3.push(row2);
7152
7157
  return rows3;
7153
7158
  }
7154
7159
  var insideOf = (dir, path3) => {
@@ -7218,25 +7223,25 @@ var MediaStore = class {
7218
7223
  if (!stat2 || !stat2.isFile()) return { ok: false, error: "That path is not a file." };
7219
7224
  const name = basename(real3);
7220
7225
  const ext = extname(real3).slice(1).toLowerCase();
7221
- const label3 = title2?.trim() || name;
7226
+ const label4 = title2?.trim() || name;
7222
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;
7223
7228
  if (IMAGE[ext]) {
7224
7229
  const big = tooBig(MAX_IMAGE, "image");
7225
7230
  if (big) return big;
7226
7231
  const id2 = this.grant(real3, IMAGE[ext], stat2.size);
7227
- return { ok: true, type: "image", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the image ${name}.` };
7232
+ return { ok: true, type: "image", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing the image ${name}.` };
7228
7233
  }
7229
7234
  if (VIDEO[ext]) {
7230
7235
  const big = tooBig(MAX_VIDEO, "file");
7231
7236
  if (big) return big;
7232
7237
  const id2 = this.grant(real3, VIDEO[ext], stat2.size);
7233
- return { ok: true, type: "video", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing ${name}.` };
7238
+ return { ok: true, type: "video", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing ${name}.` };
7234
7239
  }
7235
7240
  if (ext === "pdf") {
7236
7241
  const big = tooBig(MAX_PDF, "PDF");
7237
7242
  if (big) return big;
7238
7243
  const id2 = this.grant(real3, "application/pdf", stat2.size);
7239
- return { ok: true, type: "pdf", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the PDF ${name}.` };
7244
+ return { ok: true, type: "pdf", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing the PDF ${name}.` };
7240
7245
  }
7241
7246
  if (MARKDOWN.has(ext) || ext === "csv" || CODE_LANG[ext]) {
7242
7247
  const handle = await fs.open(real3, "r");
@@ -7253,13 +7258,13 @@ var MediaStore = class {
7253
7258
  const note = cut ? `
7254
7259
 
7255
7260
  [Only the first ${MAX_TEXT} characters are shown.]` : "";
7256
- 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." : ""}` };
7257
7262
  if (ext === "csv") {
7258
7263
  const rows3 = parseCsv(text3);
7259
7264
  const [head = [], ...body] = rows3;
7260
- 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).` };
7261
7266
  }
7262
- 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." : ""}` };
7263
7268
  }
7264
7269
  return { ok: false, error: `Friend cannot show .${ext || "?"} files. It can show images, GIFs, video, sound, PDF, Markdown, text, CSV and code.` };
7265
7270
  }
@@ -7314,7 +7319,7 @@ async function readLimited(res, max) {
7314
7319
  }
7315
7320
  return Buffer.concat(chunks);
7316
7321
  }
7317
- 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;
7318
7323
  var plain = (s) => s.replace(/[\u0000-\u001f\u007f‪-‮⁦-⁩]/g, " ");
7319
7324
  var digits = (s) => s.replace(/\D/g, "");
7320
7325
  var ChannelHub = class _ChannelHub {
@@ -7813,8 +7818,8 @@ var AlexaDevices = class {
7813
7818
  const matches = [...this.waitingMap.values()].filter((w2) => short.length === SHORT && w2.id.endsWith(short));
7814
7819
  if (matches.length !== 1 || this.store.config().allowedUsers.length >= this.store.config().maxDevices) return false;
7815
7820
  const w = matches[0];
7816
- const label3 = name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) || "Echo";
7817
- 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() }));
7818
7823
  this.waitingMap.delete(w.id);
7819
7824
  return true;
7820
7825
  }
@@ -8364,7 +8369,7 @@ var clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).pad
8364
8369
  var ref = (t) => ({ id: t.id, title: t.title, artist: t.artist, album: t.album, durationSec: t.durationSec });
8365
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 });
8366
8371
  var label2 = (t) => t.artist ? `${t.title} by ${t.artist}` : t.title;
8367
- var plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
8372
+ var plural = (n2, w) => `${n2} ${w}${n2 === 1 ? "" : "s"}`;
8368
8373
  var SERVICE_NAMES = { spotify: "Spotify", soundcloud: "SoundCloud", audius: "Audius" };
8369
8374
  var MusicHub = class {
8370
8375
  constructor(d) {
@@ -8455,10 +8460,10 @@ var MusicHub = class {
8455
8460
  const mode = a.mode ?? "replace";
8456
8461
  const failed = await this.send({ action: "play", tracks: tracks.map(refOfSource), mode, ...a.shuffle ? { shuffle: true } : {} });
8457
8462
  if (failed) return failed;
8458
- const n = tracks.length;
8463
+ const n2 = tracks.length;
8459
8464
  const first2 = tracks[0];
8460
8465
  const at = this.at();
8461
- 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)}.`;
8462
8467
  return { ok: true, text: [base, ...notes].join(" ") };
8463
8468
  }
8464
8469
  async playLocal(a, words) {
@@ -8470,11 +8475,11 @@ var MusicHub = class {
8470
8475
  const failed = await this.send({ action: "play", tracks: found.tracks.map(ref), mode, ...a.shuffle ? { shuffle: true } : {} });
8471
8476
  if (failed) return failed;
8472
8477
  const first2 = found.tracks[0];
8473
- const n = found.tracks.length;
8478
+ const n2 = found.tracks.length;
8474
8479
  const at = this.at();
8475
- if (mode === "add") return { ok: true, text: `Added ${plural(n, "track")} to the queue${at}.` };
8476
- if (mode === "next") return { ok: true, text: `Queued ${plural(n, "track")} to play next${at}, starting with ${label2(first2)}.` };
8477
- 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)}.` };
8478
8483
  }
8479
8484
  async control(a) {
8480
8485
  const bad = (text3) => ({ ok: false, text: text3 });
@@ -8838,8 +8843,8 @@ var SoundCloudHub = class {
8838
8843
  };
8839
8844
  }
8840
8845
  async search(query, limit) {
8841
- const n = Math.max(1, Math.min(50, Math.floor(limit)));
8842
- 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" })}`);
8843
8848
  return (Array.isArray(j) ? j : []).flatMap((t) => this.of(t) ?? []);
8844
8849
  }
8845
8850
  async playlists(limit) {
@@ -8870,7 +8875,7 @@ var SoundCloudHub = class {
8870
8875
  }
8871
8876
  }
8872
8877
  /** Fetches the audio from a vetted address, following at most three redirects and only to SoundCloud's own https addresses. */
8873
- async fetchAudio(address, range, signal) {
8878
+ async fetchAudio(address, range2, signal) {
8874
8879
  let next = address;
8875
8880
  for (let hop = 0; hop <= MAX_HOPS3; hop++) {
8876
8881
  if (!isSoundCloudAddress(next)) return { ok: false, error: "SoundCloud sent the audio somewhere Friend will not follow." };
@@ -8880,7 +8885,7 @@ var SoundCloudHub = class {
8880
8885
  const timer = setTimeout(() => ctl.abort(), this.d.requestTimeoutMs ?? 8e3);
8881
8886
  const stop2 = signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal;
8882
8887
  try {
8883
- 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 });
8884
8889
  } finally {
8885
8890
  clearTimeout(timer);
8886
8891
  }
@@ -9739,8 +9744,8 @@ var SpotifyHub = class {
9739
9744
  };
9740
9745
  }
9741
9746
  async search(query, limit) {
9742
- const n = Math.max(1, Math.min(50, Math.floor(limit)));
9743
- 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) })}`);
9744
9749
  return (j?.tracks?.items ?? []).flatMap((t) => this.of(t) ?? []);
9745
9750
  }
9746
9751
  async playlists(limit) {
@@ -9760,12 +9765,12 @@ var SpotifyHub = class {
9760
9765
  };
9761
9766
 
9762
9767
  // ../server/src/alexa/fast.ts
9763
- var rupees = (n, lang, o = {}) => {
9768
+ var rupees = (n2, lang, o = {}) => {
9764
9769
  const word2 = o.currencyName ?? "rupees";
9765
9770
  const spoken = lang === "hi" && word2 === "rupees" ? "\u0930\u0941\u092A\u092F\u0947" : word2;
9766
- 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}`;
9767
9772
  };
9768
- var pct = (n) => String(Math.round(Math.abs(n) * 100) / 100);
9773
+ var pct = (n2) => String(Math.round(Math.abs(n2) * 100) / 100);
9769
9774
  function portfolioSpeech(s, lang, o = {}) {
9770
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.";
9771
9776
  const up = s.dayChange >= 0;
@@ -9802,7 +9807,7 @@ var BrokerError = class extends Error {
9802
9807
  // ../server/src/brokers/portfolio.ts
9803
9808
  var MOVERS = 3;
9804
9809
  var TOP = 5;
9805
- 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;
9806
9811
  function summarize(holdings) {
9807
9812
  let value = 0;
9808
9813
  let invested = 0;
@@ -9874,7 +9879,7 @@ async function section(supported, read) {
9874
9879
  }
9875
9880
 
9876
9881
  // ../server/src/brokers/report.ts
9877
- 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);
9878
9883
  var signedPct = (p) => {
9879
9884
  const x = Number.isFinite(p) ? p : 0;
9880
9885
  const text3 = Math.abs(x).toFixed(2);
@@ -9883,7 +9888,7 @@ var signedPct = (p) => {
9883
9888
  var pct1 = (p) => Math.abs(p).toFixed(1);
9884
9889
  var TITLE = { morning: "Morning report", close: "Market close report", custom: "Portfolio report" };
9885
9890
  var WHY2 = { rate: "rate limit", session: "login expired", network: "network problem", auth: "login problem" };
9886
- 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)})`;
9887
9892
  var DEFAULT_REPORT_OPTIONS = { money: DEFAULT_MONEY, concentrationPct: 30, bigDropPct: 5 };
9888
9893
  function title(kind, broker, now, zone) {
9889
9894
  const parts = Object.fromEntries(
@@ -9895,8 +9900,8 @@ var dayMovePct = (h) => h.closePrice > 0 ? (h.lastPrice - h.closePrice) / h.clos
9895
9900
  function buildReport(input) {
9896
9901
  const { snapshot: snap, caps } = input;
9897
9902
  const o = input.options ?? DEFAULT_REPORT_OPTIONS;
9898
- const fmt = (n) => formatMoney(n, o.money);
9899
- const signed = (n) => signedMoney(n, o.money);
9903
+ const fmt = (n2) => formatMoney(n2, o.money);
9904
+ const signed = (n2) => signedMoney(n2, o.money);
9900
9905
  const out = [title(input.kind, input.broker, input.now, input.zone ?? "Asia/Kolkata"), ""];
9901
9906
  if (caps.holdings) {
9902
9907
  if (!snap.holdings.ok) out.push(missing("Holdings", snap.holdings));
@@ -10031,10 +10036,10 @@ var ReportSchedules = class {
10031
10036
  }
10032
10037
  /** Counts one try for this schedule on this local day and returns how many there have been today (including this one). */
10033
10038
  noteAttempt(id2, day) {
10034
- const row = this.db.prepare("select attempt_day d, attempts n from report_schedules where id = ?").get(id2);
10035
- const n = row && row.d === day ? row.n + 1 : 1;
10036
- this.db.prepare("update report_schedules set attempt_day = ?, attempts = ? where id = ?").run(day, n, id2);
10037
- 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;
10038
10043
  }
10039
10044
  };
10040
10045
 
@@ -10116,8 +10121,8 @@ function nextSixIst(now) {
10116
10121
  return today > now ? today : today + DAY_MS;
10117
10122
  }
10118
10123
  var num2 = (v) => {
10119
- const n = Number(v ?? 0);
10120
- return Number.isFinite(n) ? n : 0;
10124
+ const n2 = Number(v ?? 0);
10125
+ return Number.isFinite(n2) ? n2 : 0;
10121
10126
  };
10122
10127
  var str = (v) => typeof v === "string" ? v : v === void 0 || v === null ? "" : String(v);
10123
10128
  var rows2 = (v) => Array.isArray(v) ? v : [];
@@ -10464,7 +10469,7 @@ var CodeBox = class {
10464
10469
  if (input.code.length > MAX_CODE_CHARS) return { ok: false, error: `The code is too long (the limit is ${MAX_CODE_CHARS} characters).` };
10465
10470
  const extra = Object.entries(input.files ?? {});
10466
10471
  if (extra.length > MAX_EXTRA_FILES) return { ok: false, error: "Too many files for the sandbox." };
10467
- 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))) {
10468
10473
  return { ok: false, error: "That file name is not allowed in the sandbox." };
10469
10474
  }
10470
10475
  if (extra.reduce((sum, [, body]) => sum + body.length, 0) > MAX_EXTRA_FILE_CHARS) return { ok: false, error: "The sandbox files are too large." };
@@ -10799,6 +10804,22 @@ create table report_schedules (
10799
10804
  attempt_day text,
10800
10805
  attempts integer not null default 0
10801
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);
10802
10823
  `
10803
10824
  }
10804
10825
  ];
@@ -10823,6 +10844,77 @@ function openDb(dataDir) {
10823
10844
  return db;
10824
10845
  }
10825
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
+
10826
10918
  // ../server/src/devices.ts
10827
10919
  import { randomUUID as randomUUID5 } from "node:crypto";
10828
10920
  var DeviceHub = class {
@@ -11196,10 +11288,10 @@ function createGateway(deps) {
11196
11288
  const size = stat2.size;
11197
11289
  reply.header("accept-ranges", "bytes").header("x-content-type-options", "nosniff").header("cache-control", "private, max-age=300");
11198
11290
  if (grant.mime === "image/svg+xml") reply.header("content-security-policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox");
11199
- const range = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
11200
- if (range && (range[1] || range[2])) {
11201
- let start2 = range[1] ? Number(range[1]) : Math.max(0, size - Number(range[2]));
11202
- 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;
11203
11295
  end = Math.min(end, size - 1);
11204
11296
  if (!(start2 <= end) || start2 >= size) return reply.code(416).header("content-range", `bytes */${size}`).send();
11205
11297
  start2 = Math.max(0, start2);
@@ -11366,10 +11458,10 @@ function createGateway(deps) {
11366
11458
  if (!info || !info.isFile()) return reply.code(404).send({ error: "The file is gone" });
11367
11459
  const size = info.size;
11368
11460
  reply.header("accept-ranges", "bytes").header("x-content-type-options", "nosniff").header("cache-control", "private, max-age=300");
11369
- const range = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
11370
- if (range && (range[1] || range[2])) {
11371
- let start2 = range[1] ? Number(range[1]) : Math.max(0, size - Number(range[2]));
11372
- 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;
11373
11465
  end = Math.min(end, size - 1);
11374
11466
  if (!(start2 <= end) || start2 >= size) return reply.code(416).header("content-range", `bytes */${size}`).send();
11375
11467
  start2 = Math.max(0, start2);
@@ -11437,10 +11529,10 @@ function createGateway(deps) {
11437
11529
  app.get("/api/music/stream/:ref", { onRequest: musicGuard }, async (req, reply) => {
11438
11530
  const ctl = new AbortController();
11439
11531
  req.raw.on("close", () => ctl.abort());
11440
- 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;
11441
11533
  let r;
11442
11534
  try {
11443
- r = await deps.music.stream(req.params.ref, range, ctl.signal);
11535
+ r = await deps.music.stream(req.params.ref, range2, ctl.signal);
11444
11536
  } catch {
11445
11537
  return reply.code(502).send({ error: "That track could not be played right now." });
11446
11538
  }
@@ -11624,6 +11716,17 @@ function createGateway(deps) {
11624
11716
  return systemError(reply, e, "autostart");
11625
11717
  }
11626
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
+ });
11627
11730
  const chromeFailures = new SlidingWindow(20, 6e4);
11628
11731
  const chromeWss = new WebSocketServer({ noServer: true, maxPayload: MAX_DEVICE_FRAME_BYTES });
11629
11732
  if (deps.chrome) {
@@ -12083,8 +12186,8 @@ Connection: close\r
12083
12186
 
12084
12187
  // ../server/src/provider/claude.ts
12085
12188
  import Anthropic from "@anthropic-ai/sdk";
12086
- var encodeName = (n) => n.replaceAll(".", "__");
12087
- var decodeName = (n) => n.replaceAll("__", ".");
12189
+ var encodeName = (n2) => n2.replaceAll(".", "__");
12190
+ var decodeName = (n2) => n2.replaceAll("__", ".");
12088
12191
  var toAnthropicImage = (b) => ({
12089
12192
  type: "image",
12090
12193
  source: { type: "base64", media_type: b.mime, data: b.data }
@@ -12336,7 +12439,7 @@ function readBody(req) {
12336
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;
12337
12440
  var DEFAULT_BACKOFF_MS = 15 * 6e4;
12338
12441
  var MIN_BACKOFF_MS = 3e4;
12339
- var DAY_MS2 = 864e5;
12442
+ var DAY_MS3 = 864e5;
12340
12443
  function isUsageLimit(text3) {
12341
12444
  return LIMIT.test(text3);
12342
12445
  }
@@ -12361,7 +12464,7 @@ function parseResetDelayMs(message, now = /* @__PURE__ */ new Date()) {
12361
12464
  const zoneNow = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
12362
12465
  const offset = zoneNow - now.getTime();
12363
12466
  let reset2 = Date.UTC(get("year"), get("month") - 1, get("day"), hour, Number(min), 0) - offset;
12364
- if (reset2 <= now.getTime()) reset2 += DAY_MS2;
12467
+ if (reset2 <= now.getTime()) reset2 += DAY_MS3;
12365
12468
  const delay = reset2 - now.getTime();
12366
12469
  return Number.isFinite(delay) && delay > 0 ? delay : void 0;
12367
12470
  } catch {
@@ -13281,8 +13384,8 @@ function clientLook(l) {
13281
13384
 
13282
13385
  // ../server/src/provider/ollama.ts
13283
13386
  import { Agent as Agent2, fetch as undiciFetch } from "undici";
13284
- var encodeName2 = (n) => n.replaceAll(".", "__");
13285
- var decodeName2 = (n) => n.replaceAll("__", ".");
13387
+ var encodeName2 = (n2) => n2.replaceAll(".", "__");
13388
+ var decodeName2 = (n2) => n2.replaceAll("__", ".");
13286
13389
  function toOllamaMessages(system, messages, vision) {
13287
13390
  const out = [{ role: "system", content: system }];
13288
13391
  const nameById = /* @__PURE__ */ new Map();
@@ -13452,8 +13555,8 @@ var OllamaProvider = class {
13452
13555
 
13453
13556
  // ../server/src/provider/openai.ts
13454
13557
  import { Agent as Agent3, fetch as undiciFetch2 } from "undici";
13455
- var encodeName3 = (n) => n.replaceAll(".", "__");
13456
- var decodeName3 = (n) => n.replaceAll("__", ".");
13558
+ var encodeName3 = (n2) => n2.replaceAll(".", "__");
13559
+ var decodeName3 = (n2) => n2.replaceAll("__", ".");
13457
13560
  function toOpenAiMessages(system, messages, vision) {
13458
13561
  const out = [{ role: "system", content: system }];
13459
13562
  for (const m of messages) {
@@ -13945,7 +14048,7 @@ function currentSelf() {
13945
14048
  // ../server/src/system/npm.ts
13946
14049
  import { spawn as spawn7 } from "node:child_process";
13947
14050
  var PACKAGE = "friend-assistant";
13948
- var runningVersion = () => true ? "0.1.45" : "development";
14051
+ var runningVersion = () => true ? "0.1.46" : "development";
13949
14052
  async function npmLatest(fetchFn = fetch, timeoutMs = 1e4) {
13950
14053
  try {
13951
14054
  const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
@@ -13973,8 +14076,8 @@ function spawnUpgrade(self, platform = process.platform) {
13973
14076
  // ../server/src/system/updater.ts
13974
14077
  function isNewerVersion(a, b) {
13975
14078
  if (!/^\d/.test(a) || !/^\d/.test(b)) return false;
13976
- const n = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
13977
- const [x, y] = [n(a), n(b)];
14079
+ const n2 = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
14080
+ const [x, y] = [n2(a), n2(b)];
13978
14081
  for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
13979
14082
  return false;
13980
14083
  }
@@ -14260,9 +14363,9 @@ var Voiceprint = class {
14260
14363
  }
14261
14364
  /** Similarity of a new voiceprint to the owner's, or null when nobody is enrolled. Throws on a wrong key. */
14262
14365
  score(embedding) {
14263
- const row = this.db.prepare("select centroid_enc e, dim from voiceprint where id = 1").get();
14264
- if (!row) return null;
14265
- 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);
14266
14369
  const stored = Array.from(new Float32Array(raw.buffer, raw.byteOffset, raw.byteLength / 4));
14267
14370
  return cosine(stored, embedding);
14268
14371
  }
@@ -14303,10 +14406,10 @@ var Enrollment = class {
14303
14406
  if (!checkConsistency(this.samples, CONSISTENT).ok) {
14304
14407
  throw new EnrollError("The recordings do not agree with each other. They must all be the same person; start over.");
14305
14408
  }
14306
- const n = this.samples.length;
14409
+ const n2 = this.samples.length;
14307
14410
  this.voiceprint.enroll(this.samples, this.model);
14308
14411
  this.samples = [];
14309
- return { samples: n };
14412
+ return { samples: n2 };
14310
14413
  }
14311
14414
  cancel() {
14312
14415
  this.samples = [];
@@ -14322,17 +14425,17 @@ var GREETINGS = /* @__PURE__ */ new Set(["hey", "hay", "hi", "hello", "hellow",
14322
14425
  var WORD = /[\p{L}\p{M}\p{N}'’]+/gu;
14323
14426
  var GAP = /^[\s,.:;!?…\-–—]+/u;
14324
14427
  function distance(a, b) {
14325
- const row = Array.from({ length: b.length + 1 }, (_, i) => i);
14428
+ const row2 = Array.from({ length: b.length + 1 }, (_, i) => i);
14326
14429
  for (let i = 1; i <= a.length; i++) {
14327
- let prev = row[0];
14328
- row[0] = i;
14430
+ let prev = row2[0];
14431
+ row2[0] = i;
14329
14432
  for (let j = 1; j <= b.length; j++) {
14330
- const keep = row[j];
14331
- 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));
14332
14435
  prev = keep;
14333
14436
  }
14334
14437
  }
14335
- return row[b.length];
14438
+ return row2[b.length];
14336
14439
  }
14337
14440
  var sameWord = (heard, name) => heard === name || name.length >= 4 && distance(heard, name) <= 1;
14338
14441
  function matchWake(text3, name) {
@@ -14703,6 +14806,8 @@ async function createApp(opts) {
14703
14806
  const config = { ...loadConfig(opts.dataDir), ...opts.config };
14704
14807
  const db = openDb(opts.dataDir);
14705
14808
  const audit = new Audit(db);
14809
+ const usageLog = new UsageLog(db);
14810
+ usageLog.prune(USAGE_KEEP_DAYS);
14706
14811
  const auth = new Auth(db, audit);
14707
14812
  const policy = new PolicyEngine(db, config);
14708
14813
  const devices = new DeviceHub();
@@ -14722,6 +14827,13 @@ async function createApp(opts) {
14722
14827
  if (config.provider === "openai") return new OpenAiProvider({ ...config.openai });
14723
14828
  return config.apiKey ? new ClaudeProvider({ apiKey: config.apiKey, model: config.model }) : new MissingKeyProvider();
14724
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
+ };
14725
14837
  const switchable = opts.provider ? null : new SwitchableProvider(makeProvider());
14726
14838
  const provider = opts.provider ?? switchable;
14727
14839
  const camera = new CameraBroker(opts.cameraTimeoutMs);
@@ -14753,7 +14865,7 @@ async function createApp(opts) {
14753
14865
  if (m.type === "caption.delta") reply += m.text;
14754
14866
  else if (m.type === "caption.end") reply += "\n";
14755
14867
  };
14756
- await agent.runTurn(text3, { trust: "owner", emit, ip: `channel:${via}` });
14868
+ await agent.runTurn(text3, { trust: "owner", emit, ip: `channel:${via}`, source: "channel" });
14757
14869
  return reply.trim();
14758
14870
  }
14759
14871
  });
@@ -15059,6 +15171,7 @@ async function createApp(opts) {
15059
15171
  mcp: hub,
15060
15172
  admin,
15061
15173
  provider,
15174
+ usage: (e) => usageLog.record({ ...e, ...modelInUse() }),
15062
15175
  policy,
15063
15176
  approvals,
15064
15177
  devices,
@@ -15101,7 +15214,7 @@ async function createApp(opts) {
15101
15214
  store: { config: () => config.alexa, update: (fn) => void changeAlexaConfig(fn) },
15102
15215
  verify: { fetchCert: opts.alexaFetchCert ?? fetchAmazonCert, roots: opts.alexaRoots },
15103
15216
  clock: Date.now,
15104
- 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 }) },
15105
15218
  broadcast,
15106
15219
  screenOpen: () => clients.size > 0,
15107
15220
  fast: {
@@ -15222,7 +15335,7 @@ async function createApp(opts) {
15222
15335
  if (m.type === "caption.delta" && epoch === lockEpoch) talk?.push(m.text);
15223
15336
  broadcast(m);
15224
15337
  };
15225
- 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 } : {} });
15226
15339
  await talk?.end();
15227
15340
  }
15228
15341
  async function confirmChallenge(id2, pin, conn) {
@@ -15464,6 +15577,7 @@ async function createApp(opts) {
15464
15577
  connectors: connectorsApi,
15465
15578
  chrome: chromeRemoteApi,
15466
15579
  system: systemApi,
15580
+ usage: usageLog,
15467
15581
  video: {
15468
15582
  stream: (id2) => videoHub.streamOf(id2),
15469
15583
  status: async () => videoHub.status() ?? await videoHub.refresh(),
@@ -15779,7 +15893,7 @@ async function createApp(opts) {
15779
15893
  audit.log("owner", "music.soundcloud.disconnect", { ip });
15780
15894
  return { ok: true };
15781
15895
  },
15782
- stream: async (ref2, range, signal) => {
15896
+ stream: async (ref2, range2, signal) => {
15783
15897
  const p = parseRef(ref2);
15784
15898
  const svc = p?.source === "soundcloud" ? soundcloudHub : p?.source === "audius" ? audiusHub : null;
15785
15899
  if (!p || !svc) return { ok: false, status: 404, error: "Not found" };
@@ -15787,7 +15901,7 @@ async function createApp(opts) {
15787
15901
  if (!st.connected) return { ok: false, status: 404, error: st.needs ?? "That music source is not connected." };
15788
15902
  const s = await svc.streamOf(p.id);
15789
15903
  if (!s.ok) return { ok: false, status: 404, error: s.error };
15790
- const a = await svc.fetchAudio(s.url, range, signal);
15904
+ const a = await svc.fetchAudio(s.url, range2, signal);
15791
15905
  if (!a.ok) return { ok: false, status: 502, error: a.error };
15792
15906
  const h = a.res.headers;
15793
15907
  const type = h.get("content-type") ?? "";
@@ -15887,7 +16001,7 @@ ${url}`);
15887
16001
  if (speaker && voiceAvailable() && config.voice.speakTyped) {
15888
16002
  void runVoiceTurn(msg.text, conn.ip, lockEpoch).catch((e) => audit.log("system", "turn.crash", { message: e.message }));
15889
16003
  } else {
15890
- 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 }));
15891
16005
  }
15892
16006
  break;
15893
16007
  case "approval.respond":
@@ -16195,14 +16309,14 @@ var nonEmpty = (v) => {
16195
16309
  return v.trim();
16196
16310
  };
16197
16311
  var unit2 = (v) => {
16198
- const n = Number(v);
16199
- if (v.trim() === "" || !Number.isFinite(n) || n < 0 || n > 1) throw new Error("Expected a number between 0 and 1");
16200
- 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;
16201
16315
  };
16202
16316
  var percent = (v) => {
16203
- const n = Number(v);
16204
- if (v.trim() === "" || !Number.isFinite(n) || n < 0 || n > 100) throw new Error("Expected a number between 0 and 100");
16205
- 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);
16206
16320
  };
16207
16321
  var httpUrl = (v) => {
16208
16322
  let u;
@@ -16234,9 +16348,9 @@ var PARSERS = {
16234
16348
  },
16235
16349
  https: bool,
16236
16350
  port: (v) => {
16237
- const n = Number(v);
16238
- if (!Number.isInteger(n) || n < 0 || n > 65535) throw new Error("Port must be a number between 0 and 65535");
16239
- 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;
16240
16354
  },
16241
16355
  provider: (v) => {
16242
16356
  if (v !== "claude" && v !== "ollama" && v !== "claude-code" && v !== "openai") throw new Error("Provider must be claude, ollama, claude-code or openai");
@@ -16304,9 +16418,9 @@ var GROUPS = {
16304
16418
  model: nonEmpty,
16305
16419
  vision: bool,
16306
16420
  contextTokens: (v) => {
16307
- const n = Number(v);
16308
- if (!Number.isInteger(n) || n < 1024 || n > 131072) throw new Error("contextTokens must be a whole number between 1024 and 131072");
16309
- 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;
16310
16424
  }
16311
16425
  }
16312
16426
  };
@@ -16759,15 +16873,15 @@ function urlsFor(bind, port, ifaces = networkInterfaces2, https = false) {
16759
16873
  }
16760
16874
  var PORT_HELP = "The port must be a whole number from 1024 to 65535.";
16761
16875
  function parsePort(v) {
16762
- const n = Number(v);
16763
- if (v === void 0 || v.trim() === "" || !Number.isInteger(n) || n < 1024 || n > 65535) throw new Error(PORT_HELP);
16764
- 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;
16765
16879
  }
16766
16880
  function show(io2, bind, port, ifaces, https = false) {
16767
16881
  const u = urlsFor(bind, port, ifaces, https);
16768
16882
  io2.out(`Listens on: ${bind}:${port}`);
16769
16883
  io2.out(`On this PC: ${u.local}`);
16770
- 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}`);
16771
16885
  else if (isLoopbackBind(bind)) io2.out("On your network: not reachable (only this PC). Turn it on with: friend lan on");
16772
16886
  else io2.out("On your network: no network address found. Check that this PC is connected.");
16773
16887
  }
@@ -16881,7 +16995,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((res) => {
16881
16995
  child.on("error", (e) => res({ code: 127, stdout: stdout + e.message }));
16882
16996
  child.on("close", (code) => res({ code: code ?? 1, stdout }));
16883
16997
  });
16884
- 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");
16885
16999
  async function findPython(run2) {
16886
17000
  const candidates = [
16887
17001
  ...process.env.FRIEND_PYTHON ? [[process.env.FRIEND_PYTHON, []]] : [],
@@ -17128,7 +17242,7 @@ async function startFriend(opts) {
17128
17242
  });
17129
17243
  const urls = urlsFor(app.config.bind, port, void 0, app.config.https);
17130
17244
  opts.out(`Friend is running at ${urls.local}`);
17131
- 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}`);
17132
17246
  if (!app.auth.hasOwner()) opts.out("No owner yet. Run: friend onboard (guided), or friend setup (just the account)");
17133
17247
  const voiceAvailable = () => voice2?.available() ?? false;
17134
17248
  let lastAvailable = voiceAvailable();
@@ -17307,7 +17421,7 @@ async function memoryCommand(args, io2) {
17307
17421
  if (sub === "list") {
17308
17422
  const notes = memory.list();
17309
17423
  if (notes.length === 0) io2.out("No notes saved.");
17310
- 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()})`);
17311
17425
  return 0;
17312
17426
  }
17313
17427
  if (sub === "forget") {
@@ -17324,8 +17438,8 @@ async function memoryCommand(args, io2) {
17324
17438
  io2.out(`Forgot note #${id2}.`);
17325
17439
  return 0;
17326
17440
  }
17327
- const count2 = memory.count();
17328
- if (count2 === 0) {
17441
+ const count3 = memory.count();
17442
+ if (count3 === 0) {
17329
17443
  io2.out("No notes saved.");
17330
17444
  return 0;
17331
17445
  }
@@ -17342,8 +17456,8 @@ async function memoryCommand(args, io2) {
17342
17456
  return 1;
17343
17457
  }
17344
17458
  memory.clear();
17345
- audit.log("owner", "memory.clear", { count: count2, via: "cli" });
17346
- io2.out(`Deleted ${count2} notes.`);
17459
+ audit.log("owner", "memory.clear", { count: count3, via: "cli" });
17460
+ io2.out(`Deleted ${count3} notes.`);
17347
17461
  return 0;
17348
17462
  } finally {
17349
17463
  db.close();
@@ -17359,7 +17473,7 @@ function defaultCatalog() {
17359
17473
  const here2 = dirname11(fileURLToPath5(import.meta.url));
17360
17474
  return firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve10(here2, "plugins"), resolve10(here2, "../../../../plugins")]) ?? resolve10(here2, "plugins");
17361
17475
  }
17362
- var count = (n, word2) => `${n} ${word2}${n === 1 ? "" : "s"}`;
17476
+ var count2 = (n2, word2) => `${n2} ${word2}${n2 === 1 ? "" : "s"}`;
17363
17477
  async function pluginCommand(args, io2, deps = {}) {
17364
17478
  const catalogDir = deps.catalogDir ?? defaultCatalog();
17365
17479
  const [sub, ...rest] = args;
@@ -17375,7 +17489,7 @@ async function pluginCommand(args, io2, deps = {}) {
17375
17489
  continue;
17376
17490
  }
17377
17491
  const m = r.manifest;
17378
- 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}`);
17379
17493
  }
17380
17494
  return 0;
17381
17495
  }
@@ -17814,9 +17928,9 @@ function pick(options, typed) {
17814
17928
  if (/^\d+$/.test(a)) return options[Number(a) - 1];
17815
17929
  return options.find((o) => o.key.toLowerCase() === a || o.label.toLowerCase() === a || o.label.toLowerCase().split(/\s+/)[0] === a);
17816
17930
  }
17817
- function heading(ui, n, total, title2) {
17931
+ function heading(ui, n2, total, title2) {
17818
17932
  ui.io.out("");
17819
- 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))}`);
17820
17934
  }
17821
17935
  async function choose(ui, question, options, suggested) {
17822
17936
  const { io: io2 } = ui;
@@ -18194,7 +18308,7 @@ async function network(c) {
18194
18308
  saveConfig(cfg);
18195
18309
  const u = urlsFor(cfg.bind, cfg.port, void 0, cfg.https);
18196
18310
  io2.out(`Friend will be at ${u.local}`);
18197
- 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}`);
18198
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");
18199
18313
  return "done";
18200
18314
  }
@@ -18361,12 +18475,60 @@ async function autostartCommand(args, io2, autostart = real(io2)) {
18361
18475
  }
18362
18476
  }
18363
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
+
18364
18526
  // ../agent-cli/src/commands/daemon.ts
18365
18527
  import { spawn as spawn11, spawnSync as spawnSync2 } from "node:child_process";
18366
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";
18367
18529
  import { connect } from "node:net";
18368
18530
  import { join as join24 } from "node:path";
18369
- var USAGE4 = "Usage: friend daemon start | stop | restart | status";
18531
+ var USAGE5 = "Usage: friend daemon start | stop | restart | status";
18370
18532
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
18371
18533
  var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
18372
18534
  var realDeps = {
@@ -18471,7 +18633,7 @@ async function start(io2, deps) {
18471
18633
  for (let i = 0; i < 60; i++) {
18472
18634
  if (await deps.portOpen(cfg.port, 300)) {
18473
18635
  io2.out(`Friend is running in the background (pid ${pid}) at ${urls.local}`);
18474
- 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}`);
18475
18637
  io2.out(`Stop it with: friend daemon stop (log: ${log})`);
18476
18638
  return 0;
18477
18639
  }
@@ -18504,7 +18666,7 @@ async function status(io2, deps) {
18504
18666
  if (r) {
18505
18667
  io2.out(`Friend is running in the background (pid ${r.pid}, for ${since(deps.now() - r.startedAt)}).`);
18506
18668
  io2.out(`Address: ${urls.local}`);
18507
- 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}`);
18508
18670
  io2.out(`Log: ${logFileOf(io2.dataDir)}`);
18509
18671
  return 0;
18510
18672
  }
@@ -18525,7 +18687,7 @@ async function daemonCommand(args, io2, deps = realDeps) {
18525
18687
  case "status":
18526
18688
  return status(io2, deps);
18527
18689
  default:
18528
- io2.err(USAGE4);
18690
+ io2.err(USAGE5);
18529
18691
  return 1;
18530
18692
  }
18531
18693
  }
@@ -18536,7 +18698,7 @@ import { appendFileSync } from "node:fs";
18536
18698
  import { join as join25 } from "node:path";
18537
18699
  var PACKAGE2 = "friend-assistant";
18538
18700
  var real2 = {
18539
- current: () => true ? "0.1.45" : "development",
18701
+ current: () => true ? "0.1.46" : "development",
18540
18702
  latest: () => new Promise((resolve13) => {
18541
18703
  const child = crossSpawn5("npm", ["view", PACKAGE2, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
18542
18704
  let out = "";
@@ -18562,8 +18724,8 @@ var real2 = {
18562
18724
  daemon: realDeps
18563
18725
  };
18564
18726
  var newer = (a, b) => {
18565
- const n = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
18566
- 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)];
18567
18729
  for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
18568
18730
  return false;
18569
18731
  };
@@ -18744,16 +18906,16 @@ async function secretCommand(kind, io2) {
18744
18906
  io2.err("There is no owner yet. Run: friend setup");
18745
18907
  return 1;
18746
18908
  }
18747
- const label3 = kind === "password" ? "password" : "PIN";
18748
- 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 });
18749
18911
  const problem = Auth.checkNewSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
18750
18912
  if (problem) {
18751
18913
  io2.err(problem);
18752
18914
  return 1;
18753
18915
  }
18754
- 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 });
18755
18917
  if (again !== first2) {
18756
- 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.`);
18757
18919
  return 1;
18758
18920
  }
18759
18921
  await auth.resetSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
@@ -18774,7 +18936,7 @@ async function statusCommand3(io2) {
18774
18936
  io2.out(`Data folder: ${cfg.dataDir}`);
18775
18937
  io2.out(`Owner set up: ${owner}`);
18776
18938
  io2.out(`Listens on: ${cfg.bind}:${cfg.port}`);
18777
- 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}`);
18778
18940
  io2.out(`Public URL: ${cfg.publicUrl ?? "(none)"}`);
18779
18941
  io2.out(`Workspace: ${cfg.roots.length ? cfg.roots.join(", ") : "(none, every file action asks first)"}`);
18780
18942
  io2.out(`Model: ${cfg.model}`);
@@ -18783,7 +18945,7 @@ async function statusCommand3(io2) {
18783
18945
  }
18784
18946
 
18785
18947
  // ../agent-cli/src/cli.ts
18786
- var USAGE5 = `Usage: friend <command>
18948
+ var USAGE6 = `Usage: friend <command>
18787
18949
 
18788
18950
  onboard guided setup, step by step: checks this PC, installs what is missing, asks what you want
18789
18951
  doctor check what this PC has and what is missing (changes nothing)
@@ -18793,6 +18955,7 @@ var USAGE5 = `Usage: friend <command>
18793
18955
  start run the gateway and a local device node
18794
18956
  daemon start|stop|restart|status run Friend in the background (no terminal needed), stop it, or see if it is running
18795
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)
18796
18959
  autostart on|off|status start Friend by itself when you sign in to this computer (Windows, macOS, Linux)
18797
18960
  pair print a one-time code to connect another PC
18798
18961
  node --url <ws-url> --code <code> [--roots a,b] [--name pc]
@@ -18826,6 +18989,8 @@ async function run(argv, io2) {
18826
18989
  return await daemonCommand(rest, io2);
18827
18990
  case "upgrade":
18828
18991
  return await upgradeCommand(rest, io2);
18992
+ case "usage":
18993
+ return await usageCommand(rest, io2);
18829
18994
  case "autostart":
18830
18995
  return await autostartCommand(rest, io2);
18831
18996
  case "pair":
@@ -18843,12 +19008,12 @@ async function run(argv, io2) {
18843
19008
  case "help":
18844
19009
  case "--help":
18845
19010
  case "-h":
18846
- io2.out(USAGE5);
19011
+ io2.out(USAGE6);
18847
19012
  return 0;
18848
19013
  case "version":
18849
19014
  case "--version":
18850
19015
  case "-v":
18851
- io2.out(true ? "0.1.45" : "development");
19016
+ io2.out(true ? "0.1.46" : "development");
18852
19017
  return 0;
18853
19018
  case "lan":
18854
19019
  return await lanCommand(rest, io2);
@@ -18867,7 +19032,7 @@ async function run(argv, io2) {
18867
19032
  case "config":
18868
19033
  return await configCommand(rest, io2);
18869
19034
  default:
18870
- io2.err(USAGE5);
19035
+ io2.err(USAGE6);
18871
19036
  return 1;
18872
19037
  }
18873
19038
  } catch (e) {