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/README.md +1 -0
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +344 -179
- package/dist/web/assets/{index-BrLi-8KB.js → index-CbiARSIY.js} +23 -23
- package/dist/web/assets/index-TN8E9kni.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DrNs6WlQ.css +0 -1
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 = (
|
|
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,
|
|
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
|
|
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 ${
|
|
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
|
|
1118
|
-
if (
|
|
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 = (
|
|
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(
|
|
1337
|
-
const value = Number.isFinite(
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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
|
|
1671
|
-
if (!
|
|
1672
|
-
if (
|
|
1673
|
-
this.db.prepare("update tasks set due_at = ?, fired_at = null where id = ?").run(nextSlot(
|
|
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
|
|
1690
|
-
if (!
|
|
1691
|
-
if (
|
|
1692
|
-
this.db.prepare("update tasks set due_at = ?, fired_at = null where id = ?").run(nextSlot(
|
|
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((
|
|
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 === "
|
|
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
|
|
3217
|
-
return [{ id: x.id, name:
|
|
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 = (
|
|
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((
|
|
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((
|
|
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
|
|
4325
|
+
const n2 = this.items.length;
|
|
4321
4326
|
this.items = this.items.filter((i) => i.id !== id2);
|
|
4322
|
-
if (this.items.length ===
|
|
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
|
|
4343
|
-
if (
|
|
4344
|
-
let out =
|
|
4345
|
-
for (let i = 2; taken.has(out); i++) out = `${
|
|
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
|
|
4982
|
-
let c =
|
|
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[
|
|
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
|
|
5548
|
-
const j = await this.api("/v1/tracks/search", { query: query.slice(0, 200), limit: String(
|
|
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,
|
|
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:
|
|
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
|
|
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 <
|
|
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((
|
|
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
|
|
6602
|
-
if (!
|
|
6603
|
-
return `Playing "${
|
|
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
|
|
6773
|
-
const ok =
|
|
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
|
|
6805
|
-
const ok =
|
|
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
|
|
6830
|
-
const ok =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7148
|
+
row2.push(cell);
|
|
7144
7149
|
cell = "";
|
|
7145
|
-
if (
|
|
7146
|
-
|
|
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
|
-
|
|
7151
|
-
if (
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
|
7817
|
-
this.store.update((a) => a.allowedUsers.push({ id: w.id, name:
|
|
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 = (
|
|
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
|
|
8463
|
+
const n2 = tracks.length;
|
|
8459
8464
|
const first2 = tracks[0];
|
|
8460
8465
|
const at = this.at();
|
|
8461
|
-
const base = mode === "add" ? `Added ${plural(
|
|
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
|
|
8478
|
+
const n2 = found.tracks.length;
|
|
8474
8479
|
const at = this.at();
|
|
8475
|
-
if (mode === "add") return { ok: true, text: `Added ${plural(
|
|
8476
|
-
if (mode === "next") return { ok: true, text: `Queued ${plural(
|
|
8477
|
-
return { ok: true, text: `Playing ${plural(
|
|
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
|
|
8842
|
-
const j = await this.api(`/tracks?${new URLSearchParams({ q: query.slice(0, 200), limit: String(
|
|
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,
|
|
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:
|
|
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
|
|
9743
|
-
const j = await this.api(`/v1/search?${new URLSearchParams({ q: query.slice(0, 200), type: "track", limit: String(
|
|
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 = (
|
|
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(
|
|
9771
|
+
return `${formatMoney(Math.abs(n2), { symbol: "", locale: (o.money ?? DEFAULT_MONEY).locale }).replace(/[.,]00$/, "")} ${spoken}`;
|
|
9767
9772
|
};
|
|
9768
|
-
var pct = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
9899
|
-
const signed = (
|
|
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
|
|
10035
|
-
const
|
|
10036
|
-
this.db.prepare("update report_schedules set attempt_day = ?, attempts = ? where id = ?").run(day,
|
|
10037
|
-
return
|
|
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
|
|
10120
|
-
return Number.isFinite(
|
|
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(([
|
|
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
|
|
11200
|
-
if (
|
|
11201
|
-
let start2 =
|
|
11202
|
-
let end =
|
|
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
|
|
11370
|
-
if (
|
|
11371
|
-
let start2 =
|
|
11372
|
-
let end =
|
|
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
|
|
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,
|
|
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 = (
|
|
12087
|
-
var decodeName = (
|
|
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
|
|
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 +=
|
|
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 = (
|
|
13285
|
-
var decodeName2 = (
|
|
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 = (
|
|
13456
|
-
var decodeName3 = (
|
|
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.
|
|
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
|
|
13977
|
-
const [x, y] = [
|
|
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
|
|
14264
|
-
if (!
|
|
14265
|
-
const raw = decrypt(this.key,
|
|
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
|
|
14409
|
+
const n2 = this.samples.length;
|
|
14307
14410
|
this.voiceprint.enroll(this.samples, this.model);
|
|
14308
14411
|
this.samples = [];
|
|
14309
|
-
return { samples:
|
|
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
|
|
14428
|
+
const row2 = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
14326
14429
|
for (let i = 1; i <= a.length; i++) {
|
|
14327
|
-
let prev =
|
|
14328
|
-
|
|
14430
|
+
let prev = row2[0];
|
|
14431
|
+
row2[0] = i;
|
|
14329
14432
|
for (let j = 1; j <= b.length; j++) {
|
|
14330
|
-
const keep =
|
|
14331
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
16199
|
-
if (v.trim() === "" || !Number.isFinite(
|
|
16200
|
-
return
|
|
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
|
|
16204
|
-
if (v.trim() === "" || !Number.isFinite(
|
|
16205
|
-
return Math.round(
|
|
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
|
|
16238
|
-
if (!Number.isInteger(
|
|
16239
|
-
return
|
|
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
|
|
16308
|
-
if (!Number.isInteger(
|
|
16309
|
-
return
|
|
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
|
|
16763
|
-
if (v === void 0 || v.trim() === "" || !Number.isInteger(
|
|
16764
|
-
return
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
17328
|
-
if (
|
|
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:
|
|
17346
|
-
io2.out(`Deleted ${
|
|
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
|
|
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} [${
|
|
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,
|
|
17931
|
+
function heading(ui, n2, total, title2) {
|
|
17818
17932
|
ui.io.out("");
|
|
17819
|
-
ui.io.out(`\u2500\u2500 Step ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
18566
|
-
const [x, y] = [
|
|
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
|
|
18748
|
-
const first2 = await io2.ask(`New ${
|
|
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 ${
|
|
18916
|
+
const again = await io2.ask(`Type the new ${label4} again: `, { hidden: true });
|
|
18755
18917
|
if (again !== first2) {
|
|
18756
|
-
io2.err(`The two ${
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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(
|
|
19035
|
+
io2.err(USAGE6);
|
|
18871
19036
|
return 1;
|
|
18872
19037
|
}
|
|
18873
19038
|
} catch (e) {
|