friend-assistant 0.1.45 → 0.1.47
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 +3 -0
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +552 -190
- package/dist/web/assets/{index-BrLi-8KB.js → index-BmkXgcUH.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` : "";
|
|
@@ -2322,6 +2322,43 @@ function lexicalInsideRoots(path3, roots) {
|
|
|
2322
2322
|
});
|
|
2323
2323
|
}
|
|
2324
2324
|
|
|
2325
|
+
// ../server/src/agent/compact.ts
|
|
2326
|
+
var SYSTEM = "You write short notes for an assistant named Friend, so that it can carry on a long conversation after its memory of it is cleared. Write at most 250 words of plain text, with no preamble. Keep what the owner is working on, decisions made, preferences, names, things promised and tasks still open. Leave out greetings, small talk and anything already finished. Do not invent anything.";
|
|
2327
|
+
var MAX_LINE = 1500;
|
|
2328
|
+
var MAX_TEXT = 3e4;
|
|
2329
|
+
var MAX_SUMMARY = 4e3;
|
|
2330
|
+
function conversationText(history) {
|
|
2331
|
+
const lines = [];
|
|
2332
|
+
for (const m of history) {
|
|
2333
|
+
if (m.role === "tool") continue;
|
|
2334
|
+
const t = m.content.flatMap((b) => b.type === "text" ? [b.text] : []).join(" ").trim();
|
|
2335
|
+
if (t) lines.push(`${m.role === "user" ? "Owner" : "Friend"}: ${t.slice(0, MAX_LINE)}`);
|
|
2336
|
+
}
|
|
2337
|
+
return lines.join("\n").slice(-MAX_TEXT);
|
|
2338
|
+
}
|
|
2339
|
+
async function summarize(provider, history, earlier, signal) {
|
|
2340
|
+
const words = conversationText(history);
|
|
2341
|
+
if (!words) return null;
|
|
2342
|
+
const ask = `${earlier ? `Notes from before:
|
|
2343
|
+
${earlier}
|
|
2344
|
+
|
|
2345
|
+
` : ""}Conversation:
|
|
2346
|
+
${words}
|
|
2347
|
+
|
|
2348
|
+
Write the notes now.`;
|
|
2349
|
+
let out = "";
|
|
2350
|
+
try {
|
|
2351
|
+
for await (const ev of provider.stream({ system: SYSTEM, session: "guest", messages: [{ role: "user", content: [{ type: "text", text: ask }] }], tools: [], signal })) {
|
|
2352
|
+
if (ev.type === "text_delta") out += ev.text;
|
|
2353
|
+
else if (ev.type === "error") return null;
|
|
2354
|
+
}
|
|
2355
|
+
} catch {
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
out = out.trim().slice(0, MAX_SUMMARY);
|
|
2359
|
+
return out || null;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2325
2362
|
// ../server/src/agent/history.ts
|
|
2326
2363
|
var WINDOW = 40;
|
|
2327
2364
|
var PICTURE_STUB = "[camera picture, not kept]";
|
|
@@ -2552,6 +2589,11 @@ var Agent = class {
|
|
|
2552
2589
|
abort = null;
|
|
2553
2590
|
idleJobs = [];
|
|
2554
2591
|
memberTalk = /* @__PURE__ */ new Map();
|
|
2592
|
+
/** How much the model read for its last call (the size of the conversation it keeps), when it said. */
|
|
2593
|
+
context = 0;
|
|
2594
|
+
/** The notes the last tidy-up left in the model's conversation, so the next one does not lose what is in them. */
|
|
2595
|
+
lastSummary = null;
|
|
2596
|
+
tidyBlockedUntil = 0;
|
|
2555
2597
|
/** Runs `job` as soon as no request is being answered (right away when idle). */
|
|
2556
2598
|
whenIdle(job) {
|
|
2557
2599
|
if (this.busy) this.idleJobs.push(job);
|
|
@@ -2560,6 +2602,65 @@ var Agent = class {
|
|
|
2560
2602
|
isBusy() {
|
|
2561
2603
|
return this.busy;
|
|
2562
2604
|
}
|
|
2605
|
+
/** Tokens the model's own conversation holds (0 when unknown, or right after a fresh start). */
|
|
2606
|
+
contextTokens() {
|
|
2607
|
+
return this.context;
|
|
2608
|
+
}
|
|
2609
|
+
/** Writes notes on the conversation and starts the model's own conversation over from them. */
|
|
2610
|
+
async compact() {
|
|
2611
|
+
return this.whileIdle(() => this.compactNow(new AbortController().signal));
|
|
2612
|
+
}
|
|
2613
|
+
/** Starts the model's own conversation over from nothing. Friend's notes and memory are kept. */
|
|
2614
|
+
async clear() {
|
|
2615
|
+
return this.whileIdle(async () => {
|
|
2616
|
+
await this.deps.provider.resetSession();
|
|
2617
|
+
this.context = 0;
|
|
2618
|
+
this.lastSummary = null;
|
|
2619
|
+
return { ok: true, message: "Started fresh. Friend remembers its notes, but not the earlier conversation." };
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
async whileIdle(job) {
|
|
2623
|
+
if (!this.deps.provider.resetSession) return { ok: false, message: "This model does not keep a conversation of its own, so there is nothing to tidy." };
|
|
2624
|
+
if (this.busy) return { ok: false, message: "Friend is busy right now. Try again in a moment." };
|
|
2625
|
+
this.busy = true;
|
|
2626
|
+
try {
|
|
2627
|
+
return await job();
|
|
2628
|
+
} finally {
|
|
2629
|
+
this.busy = false;
|
|
2630
|
+
for (const j of this.idleJobs.splice(0)) {
|
|
2631
|
+
try {
|
|
2632
|
+
j();
|
|
2633
|
+
} catch (e) {
|
|
2634
|
+
this.deps.audit.log("system", "agent.idle_job_failed", { message: e.message });
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
async compactNow(signal) {
|
|
2640
|
+
const d = this.deps;
|
|
2641
|
+
const history = loadHistory(d.db);
|
|
2642
|
+
if (!conversationText(history)) return { ok: false, message: "There is nothing to compact yet." };
|
|
2643
|
+
const summary2 = await summarize(d.provider, history, this.lastSummary, signal);
|
|
2644
|
+
if (!summary2) return { ok: false, message: "Could not write the notes just now, so nothing was changed." };
|
|
2645
|
+
await d.provider.resetSession(summary2);
|
|
2646
|
+
this.lastSummary = summary2;
|
|
2647
|
+
this.context = 0;
|
|
2648
|
+
return { ok: true, message: "Done. Friend wrote notes on the conversation and started a fresh one from them." };
|
|
2649
|
+
}
|
|
2650
|
+
/** Before an answer: if the model's own conversation has grown too big, tidy it first. A failure only means trying again later. */
|
|
2651
|
+
async tidyIfBig(emit, signal) {
|
|
2652
|
+
const d = this.deps;
|
|
2653
|
+
const s = d.session?.auto();
|
|
2654
|
+
if (!s?.on || this.context <= s.limit || !d.provider.resetSession) return;
|
|
2655
|
+
const now = d.tasks?.now() ?? Date.now();
|
|
2656
|
+
if (now < this.tidyBlockedUntil) return;
|
|
2657
|
+
emit({ type: "activity", text: "Tidying up my memory" });
|
|
2658
|
+
const r = await this.compactNow(signal).catch(() => ({ ok: false, message: "The notes could not be written." }));
|
|
2659
|
+
if (!r.ok) {
|
|
2660
|
+
this.tidyBlockedUntil = now + 10 * 6e4;
|
|
2661
|
+
d.audit.log("system", "session.tidy_failed", { message: r.message });
|
|
2662
|
+
} else d.audit.log("system", "session.tidied", { auto: true });
|
|
2663
|
+
}
|
|
2563
2664
|
/** Stop the running turn (used by emergency lock). */
|
|
2564
2665
|
cancel() {
|
|
2565
2666
|
this.abort?.abort();
|
|
@@ -2572,6 +2673,8 @@ var Agent = class {
|
|
|
2572
2673
|
return;
|
|
2573
2674
|
}
|
|
2574
2675
|
this.busy = true;
|
|
2676
|
+
const spent = { input: 0, output: 0 };
|
|
2677
|
+
let reported = 0;
|
|
2575
2678
|
this.scenesThisTurn = 0;
|
|
2576
2679
|
const stage = (cmd) => {
|
|
2577
2680
|
d.stage.apply(cmd);
|
|
@@ -2592,6 +2695,7 @@ var Agent = class {
|
|
|
2592
2695
|
let lastAnswer = "";
|
|
2593
2696
|
try {
|
|
2594
2697
|
if (!isolated) repairDangling(d.db);
|
|
2698
|
+
if (!isolated) await this.tidyIfBig(emit, abort.signal);
|
|
2595
2699
|
const userMsg = { role: "user", content: [{ type: "text", text: text3 }] };
|
|
2596
2700
|
keep(userMsg);
|
|
2597
2701
|
const earlier = member ? earlierOf(this.memberTalk.get(ctx.person.id), d.tasks?.now() ?? Date.now()) : [];
|
|
@@ -2631,7 +2735,11 @@ var Agent = class {
|
|
|
2631
2735
|
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
2736
|
);
|
|
2633
2737
|
for await (const ev of d.provider.stream({ system, ...context ? { context } : {}, session: isolated ? "guest" : "owner", messages, tools, signal: abort.signal })) {
|
|
2634
|
-
if (ev.type === "
|
|
2738
|
+
if (ev.type === "usage") {
|
|
2739
|
+
spent.input += ev.input;
|
|
2740
|
+
spent.output += ev.output;
|
|
2741
|
+
if (ev.context) reported = ev.context;
|
|
2742
|
+
} else if (ev.type === "text_delta") {
|
|
2635
2743
|
if (!assistantText) {
|
|
2636
2744
|
stage({ cmd: "presence", state: "speaking" });
|
|
2637
2745
|
emit({ type: "activity", text: "Answering" });
|
|
@@ -2691,6 +2799,8 @@ var Agent = class {
|
|
|
2691
2799
|
d.audit.log("system", "agent.error", { message: e.message });
|
|
2692
2800
|
emit({ type: "caption.delta", text: "Sorry, something went wrong on my side." });
|
|
2693
2801
|
} finally {
|
|
2802
|
+
if (reported > 0 && !isolated) this.context = reported;
|
|
2803
|
+
if (d.usage && (spent.input > 0 || spent.output > 0)) d.usage({ source: ctx.source ?? "other", ...spent });
|
|
2694
2804
|
if (member && lastAnswer) {
|
|
2695
2805
|
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
2806
|
this.memberTalk.set(ctx.person.id, { at: d.tasks?.now() ?? Date.now(), lines: lines.slice(-MEMBER_MEMORY_MSGS) });
|
|
@@ -3213,8 +3323,8 @@ function sanitizeAlexa(raw) {
|
|
|
3213
3323
|
const users = (Array.isArray(r.allowedUsers) ? r.allowedUsers : []).flatMap((u) => {
|
|
3214
3324
|
const x = u && typeof u === "object" ? u : {};
|
|
3215
3325
|
if (typeof x.id !== "string" || !AMAZON_USER.test(x.id)) return [];
|
|
3216
|
-
const
|
|
3217
|
-
return [{ id: x.id, name:
|
|
3326
|
+
const label4 = typeof x.name === "string" ? x.name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "";
|
|
3327
|
+
return [{ id: x.id, name: label4 || "Echo", addedAt: typeof x.addedAt === "number" && Number.isFinite(x.addedAt) ? x.addedAt : 0 }];
|
|
3218
3328
|
});
|
|
3219
3329
|
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
3330
|
const locales = [...new Set((Array.isArray(r.locales) ? r.locales : []).filter((l) => ALEXA_LOCALES.includes(l)))];
|
|
@@ -3347,6 +3457,12 @@ function sanitizeLook(raw) {
|
|
|
3347
3457
|
};
|
|
3348
3458
|
}
|
|
3349
3459
|
var DEFAULT_OLLAMA = { url: "http://127.0.0.1:11434", model: "qwen2.5:7b", contextTokens: 8192, vision: false };
|
|
3460
|
+
var MIN_COMPACT_TOKENS = 5e4;
|
|
3461
|
+
var MAX_COMPACT_TOKENS = 8e5;
|
|
3462
|
+
var sanitizeSession = (s) => ({
|
|
3463
|
+
autoCompact: typeof s?.autoCompact === "boolean" ? s.autoCompact : true,
|
|
3464
|
+
compactAtTokens: typeof s?.compactAtTokens === "number" && Number.isFinite(s.compactAtTokens) ? Math.min(MAX_COMPACT_TOKENS, Math.max(MIN_COMPACT_TOKENS, Math.round(s.compactAtTokens))) : 1e5
|
|
3465
|
+
});
|
|
3350
3466
|
var sanitizeUpdates = (u) => ({ auto: typeof u?.auto === "boolean" ? u.auto : true });
|
|
3351
3467
|
var DEFAULT_VOICE = {
|
|
3352
3468
|
enabled: true,
|
|
@@ -3362,7 +3478,7 @@ var DEFAULT_VOICE = {
|
|
|
3362
3478
|
duckPercent: 25
|
|
3363
3479
|
};
|
|
3364
3480
|
function sanitizeVoice(v) {
|
|
3365
|
-
const ok = (
|
|
3481
|
+
const ok = (n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 0 && n2 <= 1;
|
|
3366
3482
|
const percent2 = typeof v.duckPercent === "number" && Number.isFinite(v.duckPercent) ? Math.max(0, Math.min(100, Math.round(v.duckPercent))) : DEFAULT_VOICE.duckPercent;
|
|
3367
3483
|
const base = { ...v, duck: typeof v.duck === "boolean" ? v.duck : DEFAULT_VOICE.duck, duckPercent: percent2 };
|
|
3368
3484
|
if (ok(v.accept) && ok(v.challenge) && v.accept > v.challenge) return base;
|
|
@@ -3414,6 +3530,7 @@ var defaults = (dataDir) => ({
|
|
|
3414
3530
|
claudeCode: { ...DEFAULT_CLAUDE_CODE },
|
|
3415
3531
|
voice: { ...DEFAULT_VOICE },
|
|
3416
3532
|
updates: sanitizeUpdates(void 0),
|
|
3533
|
+
session: sanitizeSession(void 0),
|
|
3417
3534
|
view: "companion",
|
|
3418
3535
|
look: { ...DEFAULT_LOOK },
|
|
3419
3536
|
ownerName: "",
|
|
@@ -3461,6 +3578,7 @@ function loadConfig(dataDir = defaultDataDir()) {
|
|
|
3461
3578
|
claudeCode: sanitizeClaudeCode(stored.claudeCode),
|
|
3462
3579
|
voice: sanitizeVoice({ ...DEFAULT_VOICE, ...stored.voice }),
|
|
3463
3580
|
updates: sanitizeUpdates(stored.updates),
|
|
3581
|
+
session: sanitizeSession(stored.session),
|
|
3464
3582
|
view: stored.view === "minimal" || stored.view === "workspace" ? stored.view : "companion",
|
|
3465
3583
|
look: sanitizeLook(stored.look),
|
|
3466
3584
|
ownerName: typeof stored.ownerName === "string" ? stored.ownerName.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "",
|
|
@@ -3557,7 +3675,7 @@ function integrationViews(config, hub, brokers, alexa, spotify, soundcloud, audi
|
|
|
3557
3675
|
const vdNeeds = () => {
|
|
3558
3676
|
if (!vs) return [];
|
|
3559
3677
|
const need = [{ name: "yt-dlp", h: vs.ytdlp }, ...vdMpvWindow ? [] : [{ name: "ffmpeg", h: vs.ffmpeg }]];
|
|
3560
|
-
return need.filter((
|
|
3678
|
+
return need.filter((n2) => !n2.h.found).map((n2) => ({ name: n2.name, ...n2.h.install ? { install: n2.h.install } : {} }));
|
|
3561
3679
|
};
|
|
3562
3680
|
const vdSteps = [
|
|
3563
3681
|
{ 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 +4015,7 @@ function inspectPlugin(dir) {
|
|
|
3897
4015
|
}
|
|
3898
4016
|
const manifest = parsed.data;
|
|
3899
4017
|
const seen = (names, kind) => {
|
|
3900
|
-
const dup2 = names.find((
|
|
4018
|
+
const dup2 = names.find((n2, i) => names.indexOf(n2) !== i);
|
|
3901
4019
|
return dup2 ? `The ${kind} name "${dup2}" is used twice.` : null;
|
|
3902
4020
|
};
|
|
3903
4021
|
const dup = seen(manifest.skills.map((s) => s.name), "skill") ?? seen(manifest.tools.map((t) => t.name), "tool");
|
|
@@ -4317,9 +4435,9 @@ var ConnectorStore = class {
|
|
|
4317
4435
|
return true;
|
|
4318
4436
|
}
|
|
4319
4437
|
remove(id2) {
|
|
4320
|
-
const
|
|
4438
|
+
const n2 = this.items.length;
|
|
4321
4439
|
this.items = this.items.filter((i) => i.id !== id2);
|
|
4322
|
-
if (this.items.length ===
|
|
4440
|
+
if (this.items.length === n2) return false;
|
|
4323
4441
|
this.save();
|
|
4324
4442
|
return true;
|
|
4325
4443
|
}
|
|
@@ -4339,10 +4457,10 @@ var hasSecrets = (c) => c.kind === "stdio" ? Object.keys(c.env).length > 0 : Obj
|
|
|
4339
4457
|
var MAX_RESULT_BYTES = 1024 * 1024;
|
|
4340
4458
|
var MAX_TOOL_PART = 28;
|
|
4341
4459
|
function friendlyToolName(original, taken) {
|
|
4342
|
-
let
|
|
4343
|
-
if (
|
|
4344
|
-
let out =
|
|
4345
|
-
for (let i = 2; taken.has(out); i++) out = `${
|
|
4460
|
+
let n2 = original.replace(/[^A-Za-z0-9_-]/g, "-").replace(/_{2,}/g, "_").replace(/^[-_]+/, "") || "tool";
|
|
4461
|
+
if (n2.length > MAX_TOOL_PART) n2 = `${n2.slice(0, MAX_TOOL_PART - 7)}_${createHash2("sha1").update(original).digest("hex").slice(0, 5)}`;
|
|
4462
|
+
let out = n2;
|
|
4463
|
+
for (let i = 2; taken.has(out); i++) out = `${n2.slice(0, MAX_TOOL_PART - 3)}_${i}`;
|
|
4346
4464
|
taken.add(out);
|
|
4347
4465
|
return out;
|
|
4348
4466
|
}
|
|
@@ -4978,10 +5096,10 @@ import { join as join9 } from "node:path";
|
|
|
4978
5096
|
import { deflateRawSync } from "node:zlib";
|
|
4979
5097
|
var TABLE = (() => {
|
|
4980
5098
|
const t = new Uint32Array(256);
|
|
4981
|
-
for (let
|
|
4982
|
-
let c =
|
|
5099
|
+
for (let n2 = 0; n2 < 256; n2++) {
|
|
5100
|
+
let c = n2;
|
|
4983
5101
|
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
4984
|
-
t[
|
|
5102
|
+
t[n2] = c >>> 0;
|
|
4985
5103
|
}
|
|
4986
5104
|
return t;
|
|
4987
5105
|
})();
|
|
@@ -5544,8 +5662,8 @@ var AudiusHub = class {
|
|
|
5544
5662
|
};
|
|
5545
5663
|
}
|
|
5546
5664
|
async search(query, limit) {
|
|
5547
|
-
const
|
|
5548
|
-
const j = await this.api("/v1/tracks/search", { query: query.slice(0, 200), limit: String(
|
|
5665
|
+
const n2 = Math.max(1, Math.min(50, Math.floor(limit)));
|
|
5666
|
+
const j = await this.api("/v1/tracks/search", { query: query.slice(0, 200), limit: String(n2) });
|
|
5549
5667
|
return (Array.isArray(j) ? j : []).flatMap((t) => this.of(t) ?? []);
|
|
5550
5668
|
}
|
|
5551
5669
|
async playlists(_limit) {
|
|
@@ -5578,7 +5696,7 @@ var AudiusHub = class {
|
|
|
5578
5696
|
}
|
|
5579
5697
|
}
|
|
5580
5698
|
/** Fetches the audio from a checked address, following at most three redirects and only to checked addresses. */
|
|
5581
|
-
async fetchAudio(address,
|
|
5699
|
+
async fetchAudio(address, range2, signal) {
|
|
5582
5700
|
let next = address;
|
|
5583
5701
|
for (let hop = 0; hop <= MAX_HOPS; hop++) {
|
|
5584
5702
|
if (!isAudiusAddress(next, hop > 0) || !await this.publicName(next)) return { ok: false, error: "Audius sent the audio somewhere Friend will not follow." };
|
|
@@ -5588,7 +5706,7 @@ var AudiusHub = class {
|
|
|
5588
5706
|
const timer = setTimeout(() => ctl.abort(), this.d.requestTimeoutMs ?? 8e3);
|
|
5589
5707
|
const stop2 = signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal;
|
|
5590
5708
|
try {
|
|
5591
|
-
res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers:
|
|
5709
|
+
res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers: range2 ? { range: range2 } : {}, signal: stop2 });
|
|
5592
5710
|
} finally {
|
|
5593
5711
|
clearTimeout(timer);
|
|
5594
5712
|
}
|
|
@@ -5842,14 +5960,14 @@ var zipExtract = async (archive, plan, dest) => {
|
|
|
5842
5960
|
await fh.read(tail2, 0, tailLen, size - tailLen);
|
|
5843
5961
|
const eocd = tail2.lastIndexOf(Buffer.from([80, 75, 5, 6]));
|
|
5844
5962
|
if (eocd < 0) throw new Error("not a zip file");
|
|
5845
|
-
const
|
|
5963
|
+
const count3 = tail2.readUInt16LE(eocd + 10);
|
|
5846
5964
|
const cdSize = tail2.readUInt32LE(eocd + 12);
|
|
5847
5965
|
const cdOffset = tail2.readUInt32LE(eocd + 16);
|
|
5848
5966
|
if (cdOffset === 4294967295 || cdSize === 4294967295) throw new Error("zip64 archives are not supported");
|
|
5849
5967
|
const cd = Buffer.alloc(cdSize);
|
|
5850
5968
|
await fh.read(cd, 0, cdSize, cdOffset);
|
|
5851
5969
|
let p = 0;
|
|
5852
|
-
for (let i = 0; i <
|
|
5970
|
+
for (let i = 0; i < count3 && p + 46 <= cd.length; i++) {
|
|
5853
5971
|
if (cd.readUInt32LE(p) !== 33639248) break;
|
|
5854
5972
|
const method = cd.readUInt16LE(p + 10);
|
|
5855
5973
|
const csize = cd.readUInt32LE(p + 20);
|
|
@@ -6395,7 +6513,7 @@ var VideoHub = class {
|
|
|
6395
6513
|
return this.tokens.has(id2);
|
|
6396
6514
|
}
|
|
6397
6515
|
missing(status2, names) {
|
|
6398
|
-
return names.filter((
|
|
6516
|
+
return names.filter((n2) => !(n2 === "yt-dlp" ? status2.ytdlp : status2.ffmpeg).found);
|
|
6399
6517
|
}
|
|
6400
6518
|
/** Downloads what the plugin needs (if the owner allows it) and looks again. Called when the plugin is installed or switched on. */
|
|
6401
6519
|
async prepare() {
|
|
@@ -6598,9 +6716,9 @@ var VideoHub = class {
|
|
|
6598
6716
|
return { ok: true, text: "On the Friend screen the video has its own buttons for pause, play and volume: use those." };
|
|
6599
6717
|
}
|
|
6600
6718
|
nowPlaying() {
|
|
6601
|
-
const
|
|
6602
|
-
if (!
|
|
6603
|
-
return `Playing "${
|
|
6719
|
+
const n2 = this.now;
|
|
6720
|
+
if (!n2) return "Nothing is playing.";
|
|
6721
|
+
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
6722
|
}
|
|
6605
6723
|
/** What is found and what is missing, in words, for the assistant to pass on. */
|
|
6606
6724
|
async check() {
|
|
@@ -6769,8 +6887,8 @@ var Auth = class _Auth {
|
|
|
6769
6887
|
}
|
|
6770
6888
|
async login(password, ip) {
|
|
6771
6889
|
const succeeded = this.begin("login", ip);
|
|
6772
|
-
const
|
|
6773
|
-
const ok =
|
|
6890
|
+
const row2 = this.db.prepare("select password_hash h from owner where id = 1").get();
|
|
6891
|
+
const ok = row2 ? await verifySecret(password, row2.h) : false;
|
|
6774
6892
|
if (!ok) {
|
|
6775
6893
|
this.audit.log("anonymous", "login.fail", { ip });
|
|
6776
6894
|
throw new Error("Invalid credentials");
|
|
@@ -6801,8 +6919,8 @@ var Auth = class _Auth {
|
|
|
6801
6919
|
}
|
|
6802
6920
|
async verifyPin(pin, ip) {
|
|
6803
6921
|
const succeeded = this.begin("pin", ip);
|
|
6804
|
-
const
|
|
6805
|
-
const ok =
|
|
6922
|
+
const row2 = this.db.prepare("select pin_hash h from owner where id = 1").get();
|
|
6923
|
+
const ok = row2 ? await verifySecret(pin, row2.h) : false;
|
|
6806
6924
|
if (ok) succeeded();
|
|
6807
6925
|
else this.audit.log("owner", "pin.fail", { ip });
|
|
6808
6926
|
return ok;
|
|
@@ -6826,8 +6944,8 @@ var Auth = class _Auth {
|
|
|
6826
6944
|
const problem = _Auth.checkNewSecrets(input);
|
|
6827
6945
|
if (problem) throw new Error(problem);
|
|
6828
6946
|
const succeeded = this.begin("login", ip);
|
|
6829
|
-
const
|
|
6830
|
-
const ok =
|
|
6947
|
+
const row2 = this.db.prepare("select password_hash h from owner where id = 1").get();
|
|
6948
|
+
const ok = row2 ? await verifySecret(input.current, row2.h) : false;
|
|
6831
6949
|
if (!ok) {
|
|
6832
6950
|
this.audit.log("owner", "secrets.fail", { ip });
|
|
6833
6951
|
throw new Error("Invalid credentials");
|
|
@@ -7119,11 +7237,11 @@ var MAX_SEND = 100 * 1024 * 1024;
|
|
|
7119
7237
|
var MAX_IMAGE = 40 * 1024 * 1024;
|
|
7120
7238
|
var MAX_VIDEO = 8 * 1024 * 1024 * 1024;
|
|
7121
7239
|
var MAX_PDF = 300 * 1024 * 1024;
|
|
7122
|
-
var
|
|
7240
|
+
var MAX_TEXT2 = 2e5;
|
|
7123
7241
|
var MAX_CSV_ROWS = 200;
|
|
7124
7242
|
function parseCsv(text3) {
|
|
7125
7243
|
const rows3 = [];
|
|
7126
|
-
let
|
|
7244
|
+
let row2 = [];
|
|
7127
7245
|
let cell = "";
|
|
7128
7246
|
let quoted2 = false;
|
|
7129
7247
|
for (let i = 0; i < text3.length; i++) {
|
|
@@ -7136,19 +7254,19 @@ function parseCsv(text3) {
|
|
|
7136
7254
|
else cell += ch;
|
|
7137
7255
|
} else if (ch === '"') quoted2 = true;
|
|
7138
7256
|
else if (ch === ",") {
|
|
7139
|
-
|
|
7257
|
+
row2.push(cell);
|
|
7140
7258
|
cell = "";
|
|
7141
7259
|
} else if (ch === "\n" || ch === "\r") {
|
|
7142
7260
|
if (ch === "\r" && text3[i + 1] === "\n") i++;
|
|
7143
|
-
|
|
7261
|
+
row2.push(cell);
|
|
7144
7262
|
cell = "";
|
|
7145
|
-
if (
|
|
7146
|
-
|
|
7263
|
+
if (row2.some((c) => c !== "")) rows3.push(row2);
|
|
7264
|
+
row2 = [];
|
|
7147
7265
|
if (rows3.length > MAX_CSV_ROWS + 1) return rows3;
|
|
7148
7266
|
} else cell += ch;
|
|
7149
7267
|
}
|
|
7150
|
-
|
|
7151
|
-
if (
|
|
7268
|
+
row2.push(cell);
|
|
7269
|
+
if (row2.some((c) => c !== "")) rows3.push(row2);
|
|
7152
7270
|
return rows3;
|
|
7153
7271
|
}
|
|
7154
7272
|
var insideOf = (dir, path3) => {
|
|
@@ -7218,48 +7336,48 @@ var MediaStore = class {
|
|
|
7218
7336
|
if (!stat2 || !stat2.isFile()) return { ok: false, error: "That path is not a file." };
|
|
7219
7337
|
const name = basename(real3);
|
|
7220
7338
|
const ext = extname(real3).slice(1).toLowerCase();
|
|
7221
|
-
const
|
|
7339
|
+
const label4 = title2?.trim() || name;
|
|
7222
7340
|
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
7341
|
if (IMAGE[ext]) {
|
|
7224
7342
|
const big = tooBig(MAX_IMAGE, "image");
|
|
7225
7343
|
if (big) return big;
|
|
7226
7344
|
const id2 = this.grant(real3, IMAGE[ext], stat2.size);
|
|
7227
|
-
return { ok: true, type: "image", props: { title:
|
|
7345
|
+
return { ok: true, type: "image", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing the image ${name}.` };
|
|
7228
7346
|
}
|
|
7229
7347
|
if (VIDEO[ext]) {
|
|
7230
7348
|
const big = tooBig(MAX_VIDEO, "file");
|
|
7231
7349
|
if (big) return big;
|
|
7232
7350
|
const id2 = this.grant(real3, VIDEO[ext], stat2.size);
|
|
7233
|
-
return { ok: true, type: "video", props: { title:
|
|
7351
|
+
return { ok: true, type: "video", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing ${name}.` };
|
|
7234
7352
|
}
|
|
7235
7353
|
if (ext === "pdf") {
|
|
7236
7354
|
const big = tooBig(MAX_PDF, "PDF");
|
|
7237
7355
|
if (big) return big;
|
|
7238
7356
|
const id2 = this.grant(real3, "application/pdf", stat2.size);
|
|
7239
|
-
return { ok: true, type: "pdf", props: { title:
|
|
7357
|
+
return { ok: true, type: "pdf", props: { title: label4, src: `/api/media/${id2}` }, summary: `Showing the PDF ${name}.` };
|
|
7240
7358
|
}
|
|
7241
7359
|
if (MARKDOWN.has(ext) || ext === "csv" || CODE_LANG[ext]) {
|
|
7242
7360
|
const handle = await fs.open(real3, "r");
|
|
7243
7361
|
let text3;
|
|
7244
7362
|
try {
|
|
7245
|
-
const buf = Buffer.alloc(Math.min(stat2.size,
|
|
7363
|
+
const buf = Buffer.alloc(Math.min(stat2.size, MAX_TEXT2));
|
|
7246
7364
|
await handle.read(buf, 0, buf.length, 0);
|
|
7247
7365
|
text3 = buf.toString("utf8");
|
|
7248
7366
|
} finally {
|
|
7249
7367
|
await handle.close();
|
|
7250
7368
|
}
|
|
7251
7369
|
if (text3.includes("\0")) return { ok: false, error: "That file is not text, so it cannot be shown." };
|
|
7252
|
-
const cut = stat2.size >
|
|
7370
|
+
const cut = stat2.size > MAX_TEXT2;
|
|
7253
7371
|
const note = cut ? `
|
|
7254
7372
|
|
|
7255
|
-
[Only the first ${
|
|
7256
|
-
if (MARKDOWN.has(ext)) return { ok: true, type: "markdown", props: { title:
|
|
7373
|
+
[Only the first ${MAX_TEXT2} characters are shown.]` : "";
|
|
7374
|
+
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
7375
|
if (ext === "csv") {
|
|
7258
7376
|
const rows3 = parseCsv(text3);
|
|
7259
7377
|
const [head = [], ...body] = rows3;
|
|
7260
|
-
return { ok: true, type: "table", props: { title:
|
|
7378
|
+
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
7379
|
}
|
|
7262
|
-
return { ok: true, type: "code", props: { title:
|
|
7380
|
+
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
7381
|
}
|
|
7264
7382
|
return { ok: false, error: `Friend cannot show .${ext || "?"} files. It can show images, GIFs, video, sound, PDF, Markdown, text, CSV and code.` };
|
|
7265
7383
|
}
|
|
@@ -7314,7 +7432,7 @@ async function readLimited(res, max) {
|
|
|
7314
7432
|
}
|
|
7315
7433
|
return Buffer.concat(chunks);
|
|
7316
7434
|
}
|
|
7317
|
-
var clip2 = (s,
|
|
7435
|
+
var clip2 = (s, n2) => s.length > n2 ? `${s.slice(0, n2)}\u2026` : s;
|
|
7318
7436
|
var plain = (s) => s.replace(/[\u0000-\u001f\u007f--]/g, " ");
|
|
7319
7437
|
var digits = (s) => s.replace(/\D/g, "");
|
|
7320
7438
|
var ChannelHub = class _ChannelHub {
|
|
@@ -7813,8 +7931,8 @@ var AlexaDevices = class {
|
|
|
7813
7931
|
const matches = [...this.waitingMap.values()].filter((w2) => short.length === SHORT && w2.id.endsWith(short));
|
|
7814
7932
|
if (matches.length !== 1 || this.store.config().allowedUsers.length >= this.store.config().maxDevices) return false;
|
|
7815
7933
|
const w = matches[0];
|
|
7816
|
-
const
|
|
7817
|
-
this.store.update((a) => a.allowedUsers.push({ id: w.id, name:
|
|
7934
|
+
const label4 = name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) || "Echo";
|
|
7935
|
+
this.store.update((a) => a.allowedUsers.push({ id: w.id, name: label4, addedAt: this.clock() }));
|
|
7818
7936
|
this.waitingMap.delete(w.id);
|
|
7819
7937
|
return true;
|
|
7820
7938
|
}
|
|
@@ -8364,7 +8482,7 @@ var clock = (sec) => `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).pad
|
|
|
8364
8482
|
var ref = (t) => ({ id: t.id, title: t.title, artist: t.artist, album: t.album, durationSec: t.durationSec });
|
|
8365
8483
|
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
8484
|
var label2 = (t) => t.artist ? `${t.title} by ${t.artist}` : t.title;
|
|
8367
|
-
var plural = (
|
|
8485
|
+
var plural = (n2, w) => `${n2} ${w}${n2 === 1 ? "" : "s"}`;
|
|
8368
8486
|
var SERVICE_NAMES = { spotify: "Spotify", soundcloud: "SoundCloud", audius: "Audius" };
|
|
8369
8487
|
var MusicHub = class {
|
|
8370
8488
|
constructor(d) {
|
|
@@ -8455,10 +8573,10 @@ var MusicHub = class {
|
|
|
8455
8573
|
const mode = a.mode ?? "replace";
|
|
8456
8574
|
const failed = await this.send({ action: "play", tracks: tracks.map(refOfSource), mode, ...a.shuffle ? { shuffle: true } : {} });
|
|
8457
8575
|
if (failed) return failed;
|
|
8458
|
-
const
|
|
8576
|
+
const n2 = tracks.length;
|
|
8459
8577
|
const first2 = tracks[0];
|
|
8460
8578
|
const at = this.at();
|
|
8461
|
-
const base = mode === "add" ? `Added ${plural(
|
|
8579
|
+
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
8580
|
return { ok: true, text: [base, ...notes].join(" ") };
|
|
8463
8581
|
}
|
|
8464
8582
|
async playLocal(a, words) {
|
|
@@ -8470,11 +8588,11 @@ var MusicHub = class {
|
|
|
8470
8588
|
const failed = await this.send({ action: "play", tracks: found.tracks.map(ref), mode, ...a.shuffle ? { shuffle: true } : {} });
|
|
8471
8589
|
if (failed) return failed;
|
|
8472
8590
|
const first2 = found.tracks[0];
|
|
8473
|
-
const
|
|
8591
|
+
const n2 = found.tracks.length;
|
|
8474
8592
|
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(
|
|
8593
|
+
if (mode === "add") return { ok: true, text: `Added ${plural(n2, "track")} to the queue${at}.` };
|
|
8594
|
+
if (mode === "next") return { ok: true, text: `Queued ${plural(n2, "track")} to play next${at}, starting with ${label2(first2)}.` };
|
|
8595
|
+
return { ok: true, text: `Playing ${plural(n2, "track")}${at}${a.shuffle ? " shuffled" : ", starting with " + label2(first2)}.` };
|
|
8478
8596
|
}
|
|
8479
8597
|
async control(a) {
|
|
8480
8598
|
const bad = (text3) => ({ ok: false, text: text3 });
|
|
@@ -8838,8 +8956,8 @@ var SoundCloudHub = class {
|
|
|
8838
8956
|
};
|
|
8839
8957
|
}
|
|
8840
8958
|
async search(query, limit) {
|
|
8841
|
-
const
|
|
8842
|
-
const j = await this.api(`/tracks?${new URLSearchParams({ q: query.slice(0, 200), limit: String(
|
|
8959
|
+
const n2 = Math.max(1, Math.min(50, Math.floor(limit)));
|
|
8960
|
+
const j = await this.api(`/tracks?${new URLSearchParams({ q: query.slice(0, 200), limit: String(n2), access: "playable" })}`);
|
|
8843
8961
|
return (Array.isArray(j) ? j : []).flatMap((t) => this.of(t) ?? []);
|
|
8844
8962
|
}
|
|
8845
8963
|
async playlists(limit) {
|
|
@@ -8870,7 +8988,7 @@ var SoundCloudHub = class {
|
|
|
8870
8988
|
}
|
|
8871
8989
|
}
|
|
8872
8990
|
/** Fetches the audio from a vetted address, following at most three redirects and only to SoundCloud's own https addresses. */
|
|
8873
|
-
async fetchAudio(address,
|
|
8991
|
+
async fetchAudio(address, range2, signal) {
|
|
8874
8992
|
let next = address;
|
|
8875
8993
|
for (let hop = 0; hop <= MAX_HOPS3; hop++) {
|
|
8876
8994
|
if (!isSoundCloudAddress(next)) return { ok: false, error: "SoundCloud sent the audio somewhere Friend will not follow." };
|
|
@@ -8880,7 +8998,7 @@ var SoundCloudHub = class {
|
|
|
8880
8998
|
const timer = setTimeout(() => ctl.abort(), this.d.requestTimeoutMs ?? 8e3);
|
|
8881
8999
|
const stop2 = signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal;
|
|
8882
9000
|
try {
|
|
8883
|
-
res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers:
|
|
9001
|
+
res = await (this.d.fetch ?? fetch)(next, { redirect: "manual", headers: range2 ? { range: range2 } : {}, signal: stop2 });
|
|
8884
9002
|
} finally {
|
|
8885
9003
|
clearTimeout(timer);
|
|
8886
9004
|
}
|
|
@@ -9739,8 +9857,8 @@ var SpotifyHub = class {
|
|
|
9739
9857
|
};
|
|
9740
9858
|
}
|
|
9741
9859
|
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(
|
|
9860
|
+
const n2 = Math.max(1, Math.min(50, Math.floor(limit)));
|
|
9861
|
+
const j = await this.api(`/v1/search?${new URLSearchParams({ q: query.slice(0, 200), type: "track", limit: String(n2) })}`);
|
|
9744
9862
|
return (j?.tracks?.items ?? []).flatMap((t) => this.of(t) ?? []);
|
|
9745
9863
|
}
|
|
9746
9864
|
async playlists(limit) {
|
|
@@ -9760,12 +9878,12 @@ var SpotifyHub = class {
|
|
|
9760
9878
|
};
|
|
9761
9879
|
|
|
9762
9880
|
// ../server/src/alexa/fast.ts
|
|
9763
|
-
var rupees = (
|
|
9881
|
+
var rupees = (n2, lang, o = {}) => {
|
|
9764
9882
|
const word2 = o.currencyName ?? "rupees";
|
|
9765
9883
|
const spoken = lang === "hi" && word2 === "rupees" ? "\u0930\u0941\u092A\u092F\u0947" : word2;
|
|
9766
|
-
return `${formatMoney(Math.abs(
|
|
9884
|
+
return `${formatMoney(Math.abs(n2), { symbol: "", locale: (o.money ?? DEFAULT_MONEY).locale }).replace(/[.,]00$/, "")} ${spoken}`;
|
|
9767
9885
|
};
|
|
9768
|
-
var pct = (
|
|
9886
|
+
var pct = (n2) => String(Math.round(Math.abs(n2) * 100) / 100);
|
|
9769
9887
|
function portfolioSpeech(s, lang, o = {}) {
|
|
9770
9888
|
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
9889
|
const up = s.dayChange >= 0;
|
|
@@ -9802,8 +9920,8 @@ var BrokerError = class extends Error {
|
|
|
9802
9920
|
// ../server/src/brokers/portfolio.ts
|
|
9803
9921
|
var MOVERS = 3;
|
|
9804
9922
|
var TOP = 5;
|
|
9805
|
-
var r2 = (
|
|
9806
|
-
function
|
|
9923
|
+
var r2 = (n2) => Number.isFinite(n2) ? Math.round((n2 + Number.EPSILON) * 100) / 100 : 0;
|
|
9924
|
+
function summarize2(holdings) {
|
|
9807
9925
|
let value = 0;
|
|
9808
9926
|
let invested = 0;
|
|
9809
9927
|
let yesterday = 0;
|
|
@@ -9874,7 +9992,7 @@ async function section(supported, read) {
|
|
|
9874
9992
|
}
|
|
9875
9993
|
|
|
9876
9994
|
// ../server/src/brokers/report.ts
|
|
9877
|
-
var signedMoney = (
|
|
9995
|
+
var signedMoney = (n2, m) => n2 > 4e-3 ? `+${formatMoney(n2, m)}` : formatMoney(n2, m);
|
|
9878
9996
|
var signedPct = (p) => {
|
|
9879
9997
|
const x = Number.isFinite(p) ? p : 0;
|
|
9880
9998
|
const text3 = Math.abs(x).toFixed(2);
|
|
@@ -9883,7 +10001,7 @@ var signedPct = (p) => {
|
|
|
9883
10001
|
var pct1 = (p) => Math.abs(p).toFixed(1);
|
|
9884
10002
|
var TITLE = { morning: "Morning report", close: "Market close report", custom: "Portfolio report" };
|
|
9885
10003
|
var WHY2 = { rate: "rate limit", session: "login expired", network: "network problem", auth: "login problem" };
|
|
9886
|
-
var missing = (
|
|
10004
|
+
var missing = (label4, s) => `${label4}: not available (${WHY2[s.kind] ?? s.error.slice(0, 60)})`;
|
|
9887
10005
|
var DEFAULT_REPORT_OPTIONS = { money: DEFAULT_MONEY, concentrationPct: 30, bigDropPct: 5 };
|
|
9888
10006
|
function title(kind, broker, now, zone) {
|
|
9889
10007
|
const parts = Object.fromEntries(
|
|
@@ -9895,15 +10013,15 @@ var dayMovePct = (h) => h.closePrice > 0 ? (h.lastPrice - h.closePrice) / h.clos
|
|
|
9895
10013
|
function buildReport(input) {
|
|
9896
10014
|
const { snapshot: snap, caps } = input;
|
|
9897
10015
|
const o = input.options ?? DEFAULT_REPORT_OPTIONS;
|
|
9898
|
-
const fmt = (
|
|
9899
|
-
const signed = (
|
|
10016
|
+
const fmt = (n2) => formatMoney(n2, o.money);
|
|
10017
|
+
const signed = (n2) => signedMoney(n2, o.money);
|
|
9900
10018
|
const out = [title(input.kind, input.broker, input.now, input.zone ?? "Asia/Kolkata"), ""];
|
|
9901
10019
|
if (caps.holdings) {
|
|
9902
10020
|
if (!snap.holdings.ok) out.push(missing("Holdings", snap.holdings));
|
|
9903
10021
|
else if (snap.holdings.data.length === 0) out.push("No holdings.");
|
|
9904
10022
|
else {
|
|
9905
10023
|
const holdings = snap.holdings.data;
|
|
9906
|
-
const s =
|
|
10024
|
+
const s = summarize2(holdings);
|
|
9907
10025
|
out.push(`Portfolio: ${fmt(s.value)} (invested ${fmt(s.invested)})`);
|
|
9908
10026
|
out.push(`Profit/loss: ${signed(s.pnl)} (${signedPct(s.pnlPct)})`);
|
|
9909
10027
|
out.push(`Today: ${signed(s.dayChange)} (${signedPct(s.dayChangePct)})`);
|
|
@@ -10031,10 +10149,10 @@ var ReportSchedules = class {
|
|
|
10031
10149
|
}
|
|
10032
10150
|
/** Counts one try for this schedule on this local day and returns how many there have been today (including this one). */
|
|
10033
10151
|
noteAttempt(id2, day) {
|
|
10034
|
-
const
|
|
10035
|
-
const
|
|
10036
|
-
this.db.prepare("update report_schedules set attempt_day = ?, attempts = ? where id = ?").run(day,
|
|
10037
|
-
return
|
|
10152
|
+
const row2 = this.db.prepare("select attempt_day d, attempts n from report_schedules where id = ?").get(id2);
|
|
10153
|
+
const n2 = row2 && row2.d === day ? row2.n + 1 : 1;
|
|
10154
|
+
this.db.prepare("update report_schedules set attempt_day = ?, attempts = ? where id = ?").run(day, n2, id2);
|
|
10155
|
+
return n2;
|
|
10038
10156
|
}
|
|
10039
10157
|
};
|
|
10040
10158
|
|
|
@@ -10116,8 +10234,8 @@ function nextSixIst(now) {
|
|
|
10116
10234
|
return today > now ? today : today + DAY_MS;
|
|
10117
10235
|
}
|
|
10118
10236
|
var num2 = (v) => {
|
|
10119
|
-
const
|
|
10120
|
-
return Number.isFinite(
|
|
10237
|
+
const n2 = Number(v ?? 0);
|
|
10238
|
+
return Number.isFinite(n2) ? n2 : 0;
|
|
10121
10239
|
};
|
|
10122
10240
|
var str = (v) => typeof v === "string" ? v : v === void 0 || v === null ? "" : String(v);
|
|
10123
10241
|
var rows2 = (v) => Array.isArray(v) ? v : [];
|
|
@@ -10464,7 +10582,7 @@ var CodeBox = class {
|
|
|
10464
10582
|
if (input.code.length > MAX_CODE_CHARS) return { ok: false, error: `The code is too long (the limit is ${MAX_CODE_CHARS} characters).` };
|
|
10465
10583
|
const extra = Object.entries(input.files ?? {});
|
|
10466
10584
|
if (extra.length > MAX_EXTRA_FILES) return { ok: false, error: "Too many files for the sandbox." };
|
|
10467
|
-
if (extra.some(([
|
|
10585
|
+
if (extra.some(([n2]) => !/^[A-Za-z0-9_.-]{1,64}$/.test(n2) || n2.startsWith("..") || Object.values(SETUP).some((s) => s.file === n2))) {
|
|
10468
10586
|
return { ok: false, error: "That file name is not allowed in the sandbox." };
|
|
10469
10587
|
}
|
|
10470
10588
|
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 +10917,22 @@ create table report_schedules (
|
|
|
10799
10917
|
attempt_day text,
|
|
10800
10918
|
attempts integer not null default 0
|
|
10801
10919
|
);
|
|
10920
|
+
`
|
|
10921
|
+
},
|
|
10922
|
+
{
|
|
10923
|
+
id: 11,
|
|
10924
|
+
sql: `
|
|
10925
|
+
create table usage (
|
|
10926
|
+
id integer primary key autoincrement,
|
|
10927
|
+
at integer not null,
|
|
10928
|
+
day text not null,
|
|
10929
|
+
provider text not null,
|
|
10930
|
+
model text not null,
|
|
10931
|
+
source text not null,
|
|
10932
|
+
input integer not null,
|
|
10933
|
+
output integer not null
|
|
10934
|
+
);
|
|
10935
|
+
create index usage_day on usage (day);
|
|
10802
10936
|
`
|
|
10803
10937
|
}
|
|
10804
10938
|
];
|
|
@@ -10823,6 +10957,77 @@ function openDb(dataDir) {
|
|
|
10823
10957
|
return db;
|
|
10824
10958
|
}
|
|
10825
10959
|
|
|
10960
|
+
// ../server/src/usage.ts
|
|
10961
|
+
var USAGE_SOURCES = ["voice", "chat", "task", "channel"];
|
|
10962
|
+
var DAY_MS2 = 864e5;
|
|
10963
|
+
var USAGE_KEEP_DAYS = 400;
|
|
10964
|
+
var pad = (n2) => String(n2).padStart(2, "0");
|
|
10965
|
+
function localDay2(ms2) {
|
|
10966
|
+
const d = new Date(ms2);
|
|
10967
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
10968
|
+
}
|
|
10969
|
+
var count = (n2) => Number.isFinite(n2) && n2 > 0 ? Math.floor(n2) : 0;
|
|
10970
|
+
var label3 = (s, fallback) => {
|
|
10971
|
+
const t = String(s ?? "").trim().slice(0, 80);
|
|
10972
|
+
return /^[\w.:\/@+\- ]+$/.test(t) ? t : fallback;
|
|
10973
|
+
};
|
|
10974
|
+
function range(days) {
|
|
10975
|
+
if (!Number.isFinite(days)) return 30;
|
|
10976
|
+
return Math.min(366, Math.max(1, Math.floor(days)));
|
|
10977
|
+
}
|
|
10978
|
+
var UsageLog = class {
|
|
10979
|
+
constructor(db, now = Date.now) {
|
|
10980
|
+
this.db = db;
|
|
10981
|
+
this.now = now;
|
|
10982
|
+
}
|
|
10983
|
+
db;
|
|
10984
|
+
now;
|
|
10985
|
+
record(e) {
|
|
10986
|
+
const input = count(e.input);
|
|
10987
|
+
const output = count(e.output);
|
|
10988
|
+
if (input === 0 && output === 0) return;
|
|
10989
|
+
const at = this.now();
|
|
10990
|
+
const source = USAGE_SOURCES.includes(e.source) ? e.source : "other";
|
|
10991
|
+
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);
|
|
10992
|
+
}
|
|
10993
|
+
/** The first day (local) that a range of `n` days ending today covers. */
|
|
10994
|
+
since(n2) {
|
|
10995
|
+
return localDay2(this.now() - (n2 - 1) * DAY_MS2);
|
|
10996
|
+
}
|
|
10997
|
+
days(n2) {
|
|
10998
|
+
const r = range(n2);
|
|
10999
|
+
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));
|
|
11000
|
+
const by = new Map(rows3.map((x) => [x.day, x]));
|
|
11001
|
+
const out = [];
|
|
11002
|
+
for (let i = r - 1; i >= 0; i--) {
|
|
11003
|
+
const day = localDay2(this.now() - i * DAY_MS2);
|
|
11004
|
+
out.push(by.get(day) ?? { day, input: 0, output: 0, turns: 0 });
|
|
11005
|
+
}
|
|
11006
|
+
return out;
|
|
11007
|
+
}
|
|
11008
|
+
totals(n2) {
|
|
11009
|
+
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)));
|
|
11010
|
+
return { input: r.input, output: r.output, turns: r.turns };
|
|
11011
|
+
}
|
|
11012
|
+
byModel(n2) {
|
|
11013
|
+
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)));
|
|
11014
|
+
}
|
|
11015
|
+
bySource(n2) {
|
|
11016
|
+
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)));
|
|
11017
|
+
}
|
|
11018
|
+
summary(n2) {
|
|
11019
|
+
const r = range(n2);
|
|
11020
|
+
return { range: r, days: this.days(r), total: this.totals(r), byModel: this.byModel(r), bySource: this.bySource(r) };
|
|
11021
|
+
}
|
|
11022
|
+
/** Removes rows older than `keepDays` days. Returns how many went. */
|
|
11023
|
+
prune(keepDays) {
|
|
11024
|
+
return this.db.prepare("delete from usage where day < ?").run(this.since(Math.max(1, Math.floor(keepDays)))).changes;
|
|
11025
|
+
}
|
|
11026
|
+
clear() {
|
|
11027
|
+
return this.db.prepare("delete from usage").run().changes;
|
|
11028
|
+
}
|
|
11029
|
+
};
|
|
11030
|
+
|
|
10826
11031
|
// ../server/src/devices.ts
|
|
10827
11032
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
10828
11033
|
var DeviceHub = class {
|
|
@@ -11196,10 +11401,10 @@ function createGateway(deps) {
|
|
|
11196
11401
|
const size = stat2.size;
|
|
11197
11402
|
reply.header("accept-ranges", "bytes").header("x-content-type-options", "nosniff").header("cache-control", "private, max-age=300");
|
|
11198
11403
|
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 =
|
|
11404
|
+
const range2 = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
|
|
11405
|
+
if (range2 && (range2[1] || range2[2])) {
|
|
11406
|
+
let start2 = range2[1] ? Number(range2[1]) : Math.max(0, size - Number(range2[2]));
|
|
11407
|
+
let end = range2[1] && range2[2] ? Number(range2[2]) : size - 1;
|
|
11203
11408
|
end = Math.min(end, size - 1);
|
|
11204
11409
|
if (!(start2 <= end) || start2 >= size) return reply.code(416).header("content-range", `bytes */${size}`).send();
|
|
11205
11410
|
start2 = Math.max(0, start2);
|
|
@@ -11366,10 +11571,10 @@ function createGateway(deps) {
|
|
|
11366
11571
|
if (!info || !info.isFile()) return reply.code(404).send({ error: "The file is gone" });
|
|
11367
11572
|
const size = info.size;
|
|
11368
11573
|
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 =
|
|
11574
|
+
const range2 = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
|
|
11575
|
+
if (range2 && (range2[1] || range2[2])) {
|
|
11576
|
+
let start2 = range2[1] ? Number(range2[1]) : Math.max(0, size - Number(range2[2]));
|
|
11577
|
+
let end = range2[1] && range2[2] ? Number(range2[2]) : size - 1;
|
|
11373
11578
|
end = Math.min(end, size - 1);
|
|
11374
11579
|
if (!(start2 <= end) || start2 >= size) return reply.code(416).header("content-range", `bytes */${size}`).send();
|
|
11375
11580
|
start2 = Math.max(0, start2);
|
|
@@ -11437,10 +11642,10 @@ function createGateway(deps) {
|
|
|
11437
11642
|
app.get("/api/music/stream/:ref", { onRequest: musicGuard }, async (req, reply) => {
|
|
11438
11643
|
const ctl = new AbortController();
|
|
11439
11644
|
req.raw.on("close", () => ctl.abort());
|
|
11440
|
-
const
|
|
11645
|
+
const range2 = typeof req.headers.range === "string" && /^bytes=\d*-\d*$/.test(req.headers.range) ? req.headers.range : void 0;
|
|
11441
11646
|
let r;
|
|
11442
11647
|
try {
|
|
11443
|
-
r = await deps.music.stream(req.params.ref,
|
|
11648
|
+
r = await deps.music.stream(req.params.ref, range2, ctl.signal);
|
|
11444
11649
|
} catch {
|
|
11445
11650
|
return reply.code(502).send({ error: "That track could not be played right now." });
|
|
11446
11651
|
}
|
|
@@ -11624,6 +11829,34 @@ function createGateway(deps) {
|
|
|
11624
11829
|
return systemError(reply, e, "autostart");
|
|
11625
11830
|
}
|
|
11626
11831
|
});
|
|
11832
|
+
const usageGuard = async (req, reply) => {
|
|
11833
|
+
const token = req.cookies[COOKIE_NAME];
|
|
11834
|
+
if (!(token && auth.resolveSession(token))) return reply.code(401).send({ error: "Unauthorized" });
|
|
11835
|
+
if (!deps.usage) return reply.code(503).send({ error: "Not available" });
|
|
11836
|
+
};
|
|
11837
|
+
app.get("/api/usage", { onRequest: usageGuard }, async (req) => deps.usage.summary(Number(req.query.days ?? 30)));
|
|
11838
|
+
app.delete("/api/usage", { onRequest: usageGuard }, async () => {
|
|
11839
|
+
const removed = deps.usage.clear();
|
|
11840
|
+
audit.log("owner", "usage.cleared", { removed });
|
|
11841
|
+
return { removed };
|
|
11842
|
+
});
|
|
11843
|
+
const sessionGuard = async (req, reply) => {
|
|
11844
|
+
const token = req.cookies[COOKIE_NAME];
|
|
11845
|
+
if (!(token && auth.resolveSession(token))) return reply.code(401).send({ error: "Unauthorized" });
|
|
11846
|
+
if (!deps.session) return reply.code(503).send({ error: "Not available" });
|
|
11847
|
+
};
|
|
11848
|
+
app.get("/api/conversation", { onRequest: sessionGuard }, async () => deps.session.info());
|
|
11849
|
+
app.post("/api/conversation/compact", { onRequest: sessionGuard }, async () => deps.session.compact());
|
|
11850
|
+
app.post("/api/conversation/clear", { onRequest: sessionGuard }, async (req, reply) => {
|
|
11851
|
+
const body = z10.object({ pin: z10.string().min(1).max(32) }).safeParse(req.body);
|
|
11852
|
+
if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
|
|
11853
|
+
try {
|
|
11854
|
+
return await deps.session.clear(body.data.pin, req.ip);
|
|
11855
|
+
} catch (e) {
|
|
11856
|
+
if (e instanceof HttpError) return reply.code(e.status).send({ error: e.message });
|
|
11857
|
+
throw e;
|
|
11858
|
+
}
|
|
11859
|
+
});
|
|
11627
11860
|
const chromeFailures = new SlidingWindow(20, 6e4);
|
|
11628
11861
|
const chromeWss = new WebSocketServer({ noServer: true, maxPayload: MAX_DEVICE_FRAME_BYTES });
|
|
11629
11862
|
if (deps.chrome) {
|
|
@@ -12083,8 +12316,8 @@ Connection: close\r
|
|
|
12083
12316
|
|
|
12084
12317
|
// ../server/src/provider/claude.ts
|
|
12085
12318
|
import Anthropic from "@anthropic-ai/sdk";
|
|
12086
|
-
var encodeName = (
|
|
12087
|
-
var decodeName = (
|
|
12319
|
+
var encodeName = (n2) => n2.replaceAll(".", "__");
|
|
12320
|
+
var decodeName = (n2) => n2.replaceAll("__", ".");
|
|
12088
12321
|
var toAnthropicImage = (b) => ({
|
|
12089
12322
|
type: "image",
|
|
12090
12323
|
source: { type: "base64", media_type: b.mime, data: b.data }
|
|
@@ -12336,7 +12569,7 @@ function readBody(req) {
|
|
|
12336
12569
|
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
12570
|
var DEFAULT_BACKOFF_MS = 15 * 6e4;
|
|
12338
12571
|
var MIN_BACKOFF_MS = 3e4;
|
|
12339
|
-
var
|
|
12572
|
+
var DAY_MS3 = 864e5;
|
|
12340
12573
|
function isUsageLimit(text3) {
|
|
12341
12574
|
return LIMIT.test(text3);
|
|
12342
12575
|
}
|
|
@@ -12361,7 +12594,7 @@ function parseResetDelayMs(message, now = /* @__PURE__ */ new Date()) {
|
|
|
12361
12594
|
const zoneNow = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
12362
12595
|
const offset = zoneNow - now.getTime();
|
|
12363
12596
|
let reset2 = Date.UTC(get("year"), get("month") - 1, get("day"), hour, Number(min), 0) - offset;
|
|
12364
|
-
if (reset2 <= now.getTime()) reset2 +=
|
|
12597
|
+
if (reset2 <= now.getTime()) reset2 += DAY_MS3;
|
|
12365
12598
|
const delay = reset2 - now.getTime();
|
|
12366
12599
|
return Number.isFinite(delay) && delay > 0 ? delay : void 0;
|
|
12367
12600
|
} catch {
|
|
@@ -12496,6 +12729,8 @@ var ClaudeCodeProvider = class {
|
|
|
12496
12729
|
id = "claude-code";
|
|
12497
12730
|
vision = true;
|
|
12498
12731
|
owner = newConv(false);
|
|
12732
|
+
/** Set when the owner's conversation was reset and the new one has not begun yet: what it starts from (a summary, or nothing). */
|
|
12733
|
+
fresh = null;
|
|
12499
12734
|
guests = /* @__PURE__ */ new Set();
|
|
12500
12735
|
bridge;
|
|
12501
12736
|
bridgeReady = null;
|
|
@@ -12538,6 +12773,25 @@ var ClaudeCodeProvider = class {
|
|
|
12538
12773
|
if (guest) this.endGuest(conv);
|
|
12539
12774
|
}
|
|
12540
12775
|
}
|
|
12776
|
+
/** Forgets the owner's conversation. The next message starts a new Claude with `seed` (if any) instead of the old story. */
|
|
12777
|
+
async resetSession(seed) {
|
|
12778
|
+
const conv = this.owner;
|
|
12779
|
+
const proc = conv.proc;
|
|
12780
|
+
conv.proc = null;
|
|
12781
|
+
proc?.kill();
|
|
12782
|
+
await proc?.exited;
|
|
12783
|
+
conv.inbox.clear();
|
|
12784
|
+
conv.awaiting = [];
|
|
12785
|
+
conv.hasContext = false;
|
|
12786
|
+
conv.systemKey = "";
|
|
12787
|
+
this.forgetSession(conv);
|
|
12788
|
+
mkdirSync9(this.home, { recursive: true });
|
|
12789
|
+
this.fresh = { seed: seed?.trim() || null };
|
|
12790
|
+
try {
|
|
12791
|
+
writeFileSync8(this.freshFile(), JSON.stringify(this.fresh));
|
|
12792
|
+
} catch {
|
|
12793
|
+
}
|
|
12794
|
+
}
|
|
12541
12795
|
async close() {
|
|
12542
12796
|
this.bridge.failPending("Friend is stopping.");
|
|
12543
12797
|
this.owner.proc?.kill();
|
|
@@ -12600,7 +12854,9 @@ var ClaudeCodeProvider = class {
|
|
|
12600
12854
|
const text3 = String(ev.result ?? turnText);
|
|
12601
12855
|
if (!ev.is_error) {
|
|
12602
12856
|
const u = ev.usage ?? {};
|
|
12603
|
-
|
|
12857
|
+
const sum = (x) => (x.input_tokens ?? 0) + (x.cache_read_input_tokens ?? 0) + (x.cache_creation_input_tokens ?? 0);
|
|
12858
|
+
const last = Array.isArray(u.iterations) ? u.iterations.at(-1) : void 0;
|
|
12859
|
+
yield { type: "usage", input: sum(u), output: u.output_tokens ?? 0, ...last ? { context: sum(last) } : {} };
|
|
12604
12860
|
yield { type: "done" };
|
|
12605
12861
|
return "ok";
|
|
12606
12862
|
}
|
|
@@ -12694,6 +12950,7 @@ ${item.stderr}`;
|
|
|
12694
12950
|
mkdirSync9(this.home, { recursive: true });
|
|
12695
12951
|
if (!conv.guest) {
|
|
12696
12952
|
conv.sessionId ??= this.readSession();
|
|
12953
|
+
this.fresh = conv.sessionId ? null : this.readFresh();
|
|
12697
12954
|
await this.startBridge();
|
|
12698
12955
|
this.bridge.setTools(req.tools);
|
|
12699
12956
|
}
|
|
@@ -12770,11 +13027,26 @@ ${item.stderr}`;
|
|
|
12770
13027
|
rememberSession(conv, id2) {
|
|
12771
13028
|
if (conv.guest || conv.sessionId === id2) return;
|
|
12772
13029
|
conv.sessionId = id2;
|
|
13030
|
+
this.fresh = null;
|
|
13031
|
+
try {
|
|
13032
|
+
rmSync8(this.freshFile(), { force: true });
|
|
13033
|
+
} catch {
|
|
13034
|
+
}
|
|
12773
13035
|
try {
|
|
12774
13036
|
writeFileSync8(this.sessionFile(), id2);
|
|
12775
13037
|
} catch {
|
|
12776
13038
|
}
|
|
12777
13039
|
}
|
|
13040
|
+
freshFile = () => join16(this.home, "fresh.json");
|
|
13041
|
+
readFresh() {
|
|
13042
|
+
try {
|
|
13043
|
+
if (!existsSync14(this.freshFile())) return null;
|
|
13044
|
+
const r = JSON.parse(readFileSync8(this.freshFile(), "utf8"));
|
|
13045
|
+
return { seed: typeof r.seed === "string" && r.seed ? r.seed : null };
|
|
13046
|
+
} catch {
|
|
13047
|
+
return null;
|
|
13048
|
+
}
|
|
13049
|
+
}
|
|
12778
13050
|
forgetSession(conv) {
|
|
12779
13051
|
conv.sessionId = null;
|
|
12780
13052
|
try {
|
|
@@ -12794,7 +13066,11 @@ ${item.stderr}`;
|
|
|
12794
13066
|
}
|
|
12795
13067
|
}
|
|
12796
13068
|
const blocks = [];
|
|
12797
|
-
|
|
13069
|
+
const fresh = conv.hasContext ? null : this.fresh;
|
|
13070
|
+
if (fresh) {
|
|
13071
|
+
if (fresh.seed) blocks.push({ type: "text", text: `Summary of our earlier conversation (for context only, it is already over):
|
|
13072
|
+
${fresh.seed}` });
|
|
13073
|
+
} else if (!conv.hasContext && last >= 0) blocks.push({ type: "text", text: transcript(msgs.slice(0, last + 1)) });
|
|
12798
13074
|
const ran = /* @__PURE__ */ new Map();
|
|
12799
13075
|
for (const m of msgs.slice(last + 1)) {
|
|
12800
13076
|
for (const b of m.content) {
|
|
@@ -12857,6 +13133,9 @@ var FallbackProvider = class {
|
|
|
12857
13133
|
yield ev;
|
|
12858
13134
|
}
|
|
12859
13135
|
}
|
|
13136
|
+
async resetSession(seed) {
|
|
13137
|
+
await Promise.all([this.primary.resetSession?.(seed), this.other.resetSession?.(seed)]);
|
|
13138
|
+
}
|
|
12860
13139
|
async close() {
|
|
12861
13140
|
await Promise.all([this.primary.close?.(), this.other.close?.()]);
|
|
12862
13141
|
}
|
|
@@ -12896,6 +13175,9 @@ var SwitchableProvider = class {
|
|
|
12896
13175
|
this.current = next;
|
|
12897
13176
|
return old;
|
|
12898
13177
|
}
|
|
13178
|
+
async resetSession(seed) {
|
|
13179
|
+
await this.current.resetSession?.(seed);
|
|
13180
|
+
}
|
|
12899
13181
|
async close() {
|
|
12900
13182
|
await this.current.close?.();
|
|
12901
13183
|
}
|
|
@@ -13042,7 +13324,8 @@ var SettingsPatch = z11.object({
|
|
|
13042
13324
|
duck: z11.boolean(),
|
|
13043
13325
|
duckPercent: z11.number().int().min(0).max(100)
|
|
13044
13326
|
}).partial().strict(),
|
|
13045
|
-
updates: z11.object({ auto: z11.boolean() }).partial().strict()
|
|
13327
|
+
updates: z11.object({ auto: z11.boolean() }).partial().strict(),
|
|
13328
|
+
session: z11.object({ autoCompact: z11.boolean(), compactAtTokens: z11.number().int().min(5e4).max(8e5) }).partial().strict()
|
|
13046
13329
|
}).partial().strict();
|
|
13047
13330
|
function settingsView(c) {
|
|
13048
13331
|
return {
|
|
@@ -13106,7 +13389,8 @@ function settingsView(c) {
|
|
|
13106
13389
|
duck: c.voice.duck,
|
|
13107
13390
|
duckPercent: c.voice.duckPercent
|
|
13108
13391
|
},
|
|
13109
|
-
updates: { auto: c.updates.auto }
|
|
13392
|
+
updates: { auto: c.updates.auto },
|
|
13393
|
+
session: { autoCompact: c.session.autoCompact, compactAtTokens: c.session.compactAtTokens }
|
|
13110
13394
|
};
|
|
13111
13395
|
}
|
|
13112
13396
|
var SettingsError = class extends Error {
|
|
@@ -13264,6 +13548,7 @@ function applySettings(config, raw) {
|
|
|
13264
13548
|
}
|
|
13265
13549
|
if (p.claudeCode) Object.assign(config.claudeCode, p.claudeCode);
|
|
13266
13550
|
if (p.updates) Object.assign(config.updates, p.updates);
|
|
13551
|
+
if (p.session) Object.assign(config.session, p.session);
|
|
13267
13552
|
if (p.voice) {
|
|
13268
13553
|
const safe = next.voice;
|
|
13269
13554
|
for (const k of Object.keys(p.voice)) config.voice[k] = safe[k];
|
|
@@ -13281,8 +13566,8 @@ function clientLook(l) {
|
|
|
13281
13566
|
|
|
13282
13567
|
// ../server/src/provider/ollama.ts
|
|
13283
13568
|
import { Agent as Agent2, fetch as undiciFetch } from "undici";
|
|
13284
|
-
var encodeName2 = (
|
|
13285
|
-
var decodeName2 = (
|
|
13569
|
+
var encodeName2 = (n2) => n2.replaceAll(".", "__");
|
|
13570
|
+
var decodeName2 = (n2) => n2.replaceAll("__", ".");
|
|
13286
13571
|
function toOllamaMessages(system, messages, vision) {
|
|
13287
13572
|
const out = [{ role: "system", content: system }];
|
|
13288
13573
|
const nameById = /* @__PURE__ */ new Map();
|
|
@@ -13452,8 +13737,8 @@ var OllamaProvider = class {
|
|
|
13452
13737
|
|
|
13453
13738
|
// ../server/src/provider/openai.ts
|
|
13454
13739
|
import { Agent as Agent3, fetch as undiciFetch2 } from "undici";
|
|
13455
|
-
var encodeName3 = (
|
|
13456
|
-
var decodeName3 = (
|
|
13740
|
+
var encodeName3 = (n2) => n2.replaceAll(".", "__");
|
|
13741
|
+
var decodeName3 = (n2) => n2.replaceAll("__", ".");
|
|
13457
13742
|
function toOpenAiMessages(system, messages, vision) {
|
|
13458
13743
|
const out = [{ role: "system", content: system }];
|
|
13459
13744
|
for (const m of messages) {
|
|
@@ -13945,7 +14230,7 @@ function currentSelf() {
|
|
|
13945
14230
|
// ../server/src/system/npm.ts
|
|
13946
14231
|
import { spawn as spawn7 } from "node:child_process";
|
|
13947
14232
|
var PACKAGE = "friend-assistant";
|
|
13948
|
-
var runningVersion = () => true ? "0.1.
|
|
14233
|
+
var runningVersion = () => true ? "0.1.47" : "development";
|
|
13949
14234
|
async function npmLatest(fetchFn = fetch, timeoutMs = 1e4) {
|
|
13950
14235
|
try {
|
|
13951
14236
|
const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -13973,8 +14258,8 @@ function spawnUpgrade(self, platform = process.platform) {
|
|
|
13973
14258
|
// ../server/src/system/updater.ts
|
|
13974
14259
|
function isNewerVersion(a, b) {
|
|
13975
14260
|
if (!/^\d/.test(a) || !/^\d/.test(b)) return false;
|
|
13976
|
-
const
|
|
13977
|
-
const [x, y] = [
|
|
14261
|
+
const n2 = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
|
|
14262
|
+
const [x, y] = [n2(a), n2(b)];
|
|
13978
14263
|
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
13979
14264
|
return false;
|
|
13980
14265
|
}
|
|
@@ -14260,9 +14545,9 @@ var Voiceprint = class {
|
|
|
14260
14545
|
}
|
|
14261
14546
|
/** Similarity of a new voiceprint to the owner's, or null when nobody is enrolled. Throws on a wrong key. */
|
|
14262
14547
|
score(embedding) {
|
|
14263
|
-
const
|
|
14264
|
-
if (!
|
|
14265
|
-
const raw = decrypt(this.key,
|
|
14548
|
+
const row2 = this.db.prepare("select centroid_enc e, dim from voiceprint where id = 1").get();
|
|
14549
|
+
if (!row2) return null;
|
|
14550
|
+
const raw = decrypt(this.key, row2.e);
|
|
14266
14551
|
const stored = Array.from(new Float32Array(raw.buffer, raw.byteOffset, raw.byteLength / 4));
|
|
14267
14552
|
return cosine(stored, embedding);
|
|
14268
14553
|
}
|
|
@@ -14303,10 +14588,10 @@ var Enrollment = class {
|
|
|
14303
14588
|
if (!checkConsistency(this.samples, CONSISTENT).ok) {
|
|
14304
14589
|
throw new EnrollError("The recordings do not agree with each other. They must all be the same person; start over.");
|
|
14305
14590
|
}
|
|
14306
|
-
const
|
|
14591
|
+
const n2 = this.samples.length;
|
|
14307
14592
|
this.voiceprint.enroll(this.samples, this.model);
|
|
14308
14593
|
this.samples = [];
|
|
14309
|
-
return { samples:
|
|
14594
|
+
return { samples: n2 };
|
|
14310
14595
|
}
|
|
14311
14596
|
cancel() {
|
|
14312
14597
|
this.samples = [];
|
|
@@ -14322,17 +14607,17 @@ var GREETINGS = /* @__PURE__ */ new Set(["hey", "hay", "hi", "hello", "hellow",
|
|
|
14322
14607
|
var WORD = /[\p{L}\p{M}\p{N}'’]+/gu;
|
|
14323
14608
|
var GAP = /^[\s,.:;!?…\-–—]+/u;
|
|
14324
14609
|
function distance(a, b) {
|
|
14325
|
-
const
|
|
14610
|
+
const row2 = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
14326
14611
|
for (let i = 1; i <= a.length; i++) {
|
|
14327
|
-
let prev =
|
|
14328
|
-
|
|
14612
|
+
let prev = row2[0];
|
|
14613
|
+
row2[0] = i;
|
|
14329
14614
|
for (let j = 1; j <= b.length; j++) {
|
|
14330
|
-
const keep =
|
|
14331
|
-
|
|
14615
|
+
const keep = row2[j];
|
|
14616
|
+
row2[j] = Math.min(row2[j] + 1, row2[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
14332
14617
|
prev = keep;
|
|
14333
14618
|
}
|
|
14334
14619
|
}
|
|
14335
|
-
return
|
|
14620
|
+
return row2[b.length];
|
|
14336
14621
|
}
|
|
14337
14622
|
var sameWord = (heard, name) => heard === name || name.length >= 4 && distance(heard, name) <= 1;
|
|
14338
14623
|
function matchWake(text3, name) {
|
|
@@ -14703,6 +14988,8 @@ async function createApp(opts) {
|
|
|
14703
14988
|
const config = { ...loadConfig(opts.dataDir), ...opts.config };
|
|
14704
14989
|
const db = openDb(opts.dataDir);
|
|
14705
14990
|
const audit = new Audit(db);
|
|
14991
|
+
const usageLog = new UsageLog(db);
|
|
14992
|
+
usageLog.prune(USAGE_KEEP_DAYS);
|
|
14706
14993
|
const auth = new Auth(db, audit);
|
|
14707
14994
|
const policy = new PolicyEngine(db, config);
|
|
14708
14995
|
const devices = new DeviceHub();
|
|
@@ -14722,6 +15009,13 @@ async function createApp(opts) {
|
|
|
14722
15009
|
if (config.provider === "openai") return new OpenAiProvider({ ...config.openai });
|
|
14723
15010
|
return config.apiKey ? new ClaudeProvider({ apiKey: config.apiKey, model: config.model }) : new MissingKeyProvider();
|
|
14724
15011
|
};
|
|
15012
|
+
const modelInUse = () => {
|
|
15013
|
+
if (opts.provider) return { provider: "custom", model: "custom" };
|
|
15014
|
+
if (config.provider === "claude-code") return { provider: "claude-code", model: config.claudeCode.model };
|
|
15015
|
+
if (config.provider === "ollama") return { provider: "ollama", model: config.ollama.model };
|
|
15016
|
+
if (config.provider === "openai") return { provider: "openai", model: config.openai.model };
|
|
15017
|
+
return { provider: "claude", model: config.model };
|
|
15018
|
+
};
|
|
14725
15019
|
const switchable = opts.provider ? null : new SwitchableProvider(makeProvider());
|
|
14726
15020
|
const provider = opts.provider ?? switchable;
|
|
14727
15021
|
const camera = new CameraBroker(opts.cameraTimeoutMs);
|
|
@@ -14753,7 +15047,7 @@ async function createApp(opts) {
|
|
|
14753
15047
|
if (m.type === "caption.delta") reply += m.text;
|
|
14754
15048
|
else if (m.type === "caption.end") reply += "\n";
|
|
14755
15049
|
};
|
|
14756
|
-
await agent.runTurn(text3, { trust: "owner", emit, ip: `channel:${via}
|
|
15050
|
+
await agent.runTurn(text3, { trust: "owner", emit, ip: `channel:${via}`, source: "channel" });
|
|
14757
15051
|
return reply.trim();
|
|
14758
15052
|
}
|
|
14759
15053
|
});
|
|
@@ -15059,6 +15353,8 @@ async function createApp(opts) {
|
|
|
15059
15353
|
mcp: hub,
|
|
15060
15354
|
admin,
|
|
15061
15355
|
provider,
|
|
15356
|
+
usage: (e) => usageLog.record({ ...e, ...modelInUse() }),
|
|
15357
|
+
session: { auto: () => ({ on: config.session.autoCompact, limit: config.session.compactAtTokens }) },
|
|
15062
15358
|
policy,
|
|
15063
15359
|
approvals,
|
|
15064
15360
|
devices,
|
|
@@ -15101,7 +15397,7 @@ async function createApp(opts) {
|
|
|
15101
15397
|
store: { config: () => config.alexa, update: (fn) => void changeAlexaConfig(fn) },
|
|
15102
15398
|
verify: { fetchCert: opts.alexaFetchCert ?? fetchAmazonCert, roots: opts.alexaRoots },
|
|
15103
15399
|
clock: Date.now,
|
|
15104
|
-
agent: { isBusy: () => agent.isBusy(), runTurn: (text3, ctx) => agent.runTurn(text3, ctx) },
|
|
15400
|
+
agent: { isBusy: () => agent.isBusy(), runTurn: (text3, ctx) => agent.runTurn(text3, { source: "task", ...ctx }) },
|
|
15105
15401
|
broadcast,
|
|
15106
15402
|
screenOpen: () => clients.size > 0,
|
|
15107
15403
|
fast: {
|
|
@@ -15109,7 +15405,7 @@ async function createApp(opts) {
|
|
|
15109
15405
|
const got = await brokerHub.snapshot("zerodha");
|
|
15110
15406
|
if (!got.ok) return got.sessionExpired ? "Zerodha needs you to log in first." : got.error;
|
|
15111
15407
|
if (!got.snapshot.holdings.ok) return "I could not read your holdings just now.";
|
|
15112
|
-
return portfolioSpeech(
|
|
15408
|
+
return portfolioSpeech(summarize2(got.snapshot.holdings.data), locale2.startsWith("hi") ? "hi" : "en", { money: { symbol: config.brokers.currencySymbol, locale: config.brokers.numberLocale }, currencyName: config.brokers.currencyName });
|
|
15113
15409
|
},
|
|
15114
15410
|
WeatherIntent: async (_locale, slots) => {
|
|
15115
15411
|
if (!slots.city) return "Tell me which city, for example: what is the weather in Pune.";
|
|
@@ -15222,7 +15518,7 @@ async function createApp(opts) {
|
|
|
15222
15518
|
if (m.type === "caption.delta" && epoch === lockEpoch) talk?.push(m.text);
|
|
15223
15519
|
broadcast(m);
|
|
15224
15520
|
};
|
|
15225
|
-
await agent.runTurn(text3, { trust, emit, ip, ...person ? { person } : {}, ...spoken ? { spoken } : {} });
|
|
15521
|
+
await agent.runTurn(text3, { trust, emit, ip, source: "voice", ...person ? { person } : {}, ...spoken ? { spoken } : {} });
|
|
15226
15522
|
await talk?.end();
|
|
15227
15523
|
}
|
|
15228
15524
|
async function confirmChallenge(id2, pin, conn) {
|
|
@@ -15464,6 +15760,21 @@ async function createApp(opts) {
|
|
|
15464
15760
|
connectors: connectorsApi,
|
|
15465
15761
|
chrome: chromeRemoteApi,
|
|
15466
15762
|
system: systemApi,
|
|
15763
|
+
usage: usageLog,
|
|
15764
|
+
session: {
|
|
15765
|
+
info: () => ({ supported: !!provider.resetSession, contextTokens: agent.contextTokens(), autoCompact: config.session.autoCompact, compactAtTokens: config.session.compactAtTokens }),
|
|
15766
|
+
compact: async () => {
|
|
15767
|
+
const r = await agent.compact();
|
|
15768
|
+
if (r.ok) audit.log("owner", "session.compacted", {});
|
|
15769
|
+
return r;
|
|
15770
|
+
},
|
|
15771
|
+
clear: async (pin, ip) => {
|
|
15772
|
+
await requirePin(pin, ip);
|
|
15773
|
+
const r = await agent.clear();
|
|
15774
|
+
if (r.ok) audit.log("owner", "session.cleared", { ip });
|
|
15775
|
+
return r;
|
|
15776
|
+
}
|
|
15777
|
+
},
|
|
15467
15778
|
video: {
|
|
15468
15779
|
stream: (id2) => videoHub.streamOf(id2),
|
|
15469
15780
|
status: async () => videoHub.status() ?? await videoHub.refresh(),
|
|
@@ -15779,7 +16090,7 @@ async function createApp(opts) {
|
|
|
15779
16090
|
audit.log("owner", "music.soundcloud.disconnect", { ip });
|
|
15780
16091
|
return { ok: true };
|
|
15781
16092
|
},
|
|
15782
|
-
stream: async (ref2,
|
|
16093
|
+
stream: async (ref2, range2, signal) => {
|
|
15783
16094
|
const p = parseRef(ref2);
|
|
15784
16095
|
const svc = p?.source === "soundcloud" ? soundcloudHub : p?.source === "audius" ? audiusHub : null;
|
|
15785
16096
|
if (!p || !svc) return { ok: false, status: 404, error: "Not found" };
|
|
@@ -15787,7 +16098,7 @@ async function createApp(opts) {
|
|
|
15787
16098
|
if (!st.connected) return { ok: false, status: 404, error: st.needs ?? "That music source is not connected." };
|
|
15788
16099
|
const s = await svc.streamOf(p.id);
|
|
15789
16100
|
if (!s.ok) return { ok: false, status: 404, error: s.error };
|
|
15790
|
-
const a = await svc.fetchAudio(s.url,
|
|
16101
|
+
const a = await svc.fetchAudio(s.url, range2, signal);
|
|
15791
16102
|
if (!a.ok) return { ok: false, status: 502, error: a.error };
|
|
15792
16103
|
const h = a.res.headers;
|
|
15793
16104
|
const type = h.get("content-type") ?? "";
|
|
@@ -15887,7 +16198,7 @@ ${url}`);
|
|
|
15887
16198
|
if (speaker && voiceAvailable() && config.voice.speakTyped) {
|
|
15888
16199
|
void runVoiceTurn(msg.text, conn.ip, lockEpoch).catch((e) => audit.log("system", "turn.crash", { message: e.message }));
|
|
15889
16200
|
} else {
|
|
15890
|
-
void agent.runTurn(msg.text, { trust: "owner", emit: broadcast, ip: conn.ip }).catch((e) => audit.log("system", "turn.crash", { message: e.message }));
|
|
16201
|
+
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
16202
|
}
|
|
15892
16203
|
break;
|
|
15893
16204
|
case "approval.respond":
|
|
@@ -16195,14 +16506,14 @@ var nonEmpty = (v) => {
|
|
|
16195
16506
|
return v.trim();
|
|
16196
16507
|
};
|
|
16197
16508
|
var unit2 = (v) => {
|
|
16198
|
-
const
|
|
16199
|
-
if (v.trim() === "" || !Number.isFinite(
|
|
16200
|
-
return
|
|
16509
|
+
const n2 = Number(v);
|
|
16510
|
+
if (v.trim() === "" || !Number.isFinite(n2) || n2 < 0 || n2 > 1) throw new Error("Expected a number between 0 and 1");
|
|
16511
|
+
return n2;
|
|
16201
16512
|
};
|
|
16202
16513
|
var percent = (v) => {
|
|
16203
|
-
const
|
|
16204
|
-
if (v.trim() === "" || !Number.isFinite(
|
|
16205
|
-
return Math.round(
|
|
16514
|
+
const n2 = Number(v);
|
|
16515
|
+
if (v.trim() === "" || !Number.isFinite(n2) || n2 < 0 || n2 > 100) throw new Error("Expected a number between 0 and 100");
|
|
16516
|
+
return Math.round(n2);
|
|
16206
16517
|
};
|
|
16207
16518
|
var httpUrl = (v) => {
|
|
16208
16519
|
let u;
|
|
@@ -16234,9 +16545,9 @@ var PARSERS = {
|
|
|
16234
16545
|
},
|
|
16235
16546
|
https: bool,
|
|
16236
16547
|
port: (v) => {
|
|
16237
|
-
const
|
|
16238
|
-
if (!Number.isInteger(
|
|
16239
|
-
return
|
|
16548
|
+
const n2 = Number(v);
|
|
16549
|
+
if (!Number.isInteger(n2) || n2 < 0 || n2 > 65535) throw new Error("Port must be a number between 0 and 65535");
|
|
16550
|
+
return n2;
|
|
16240
16551
|
},
|
|
16241
16552
|
provider: (v) => {
|
|
16242
16553
|
if (v !== "claude" && v !== "ollama" && v !== "claude-code" && v !== "openai") throw new Error("Provider must be claude, ollama, claude-code or openai");
|
|
@@ -16304,9 +16615,9 @@ var GROUPS = {
|
|
|
16304
16615
|
model: nonEmpty,
|
|
16305
16616
|
vision: bool,
|
|
16306
16617
|
contextTokens: (v) => {
|
|
16307
|
-
const
|
|
16308
|
-
if (!Number.isInteger(
|
|
16309
|
-
return
|
|
16618
|
+
const n2 = Number(v);
|
|
16619
|
+
if (!Number.isInteger(n2) || n2 < 1024 || n2 > 131072) throw new Error("contextTokens must be a whole number between 1024 and 131072");
|
|
16620
|
+
return n2;
|
|
16310
16621
|
}
|
|
16311
16622
|
}
|
|
16312
16623
|
};
|
|
@@ -16759,15 +17070,15 @@ function urlsFor(bind, port, ifaces = networkInterfaces2, https = false) {
|
|
|
16759
17070
|
}
|
|
16760
17071
|
var PORT_HELP = "The port must be a whole number from 1024 to 65535.";
|
|
16761
17072
|
function parsePort(v) {
|
|
16762
|
-
const
|
|
16763
|
-
if (v === void 0 || v.trim() === "" || !Number.isInteger(
|
|
16764
|
-
return
|
|
17073
|
+
const n2 = Number(v);
|
|
17074
|
+
if (v === void 0 || v.trim() === "" || !Number.isInteger(n2) || n2 < 1024 || n2 > 65535) throw new Error(PORT_HELP);
|
|
17075
|
+
return n2;
|
|
16765
17076
|
}
|
|
16766
17077
|
function show(io2, bind, port, ifaces, https = false) {
|
|
16767
17078
|
const u = urlsFor(bind, port, ifaces, https);
|
|
16768
17079
|
io2.out(`Listens on: ${bind}:${port}`);
|
|
16769
17080
|
io2.out(`On this PC: ${u.local}`);
|
|
16770
|
-
if (u.network.length) for (const
|
|
17081
|
+
if (u.network.length) for (const n2 of u.network) io2.out(`On your network: ${n2}`);
|
|
16771
17082
|
else if (isLoopbackBind(bind)) io2.out("On your network: not reachable (only this PC). Turn it on with: friend lan on");
|
|
16772
17083
|
else io2.out("On your network: no network address found. Check that this PC is connected.");
|
|
16773
17084
|
}
|
|
@@ -16881,7 +17192,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((res) => {
|
|
|
16881
17192
|
child.on("error", (e) => res({ code: 127, stdout: stdout + e.message }));
|
|
16882
17193
|
child.on("close", (code) => res({ code: code ?? 1, stdout }));
|
|
16883
17194
|
});
|
|
16884
|
-
var tail = (s,
|
|
17195
|
+
var tail = (s, n2 = 8) => s.trim().split(/\r?\n/).slice(-n2).join("\n");
|
|
16885
17196
|
async function findPython(run2) {
|
|
16886
17197
|
const candidates = [
|
|
16887
17198
|
...process.env.FRIEND_PYTHON ? [[process.env.FRIEND_PYTHON, []]] : [],
|
|
@@ -17128,7 +17439,7 @@ async function startFriend(opts) {
|
|
|
17128
17439
|
});
|
|
17129
17440
|
const urls = urlsFor(app.config.bind, port, void 0, app.config.https);
|
|
17130
17441
|
opts.out(`Friend is running at ${urls.local}`);
|
|
17131
|
-
for (const
|
|
17442
|
+
for (const n2 of urls.network) opts.out(`Also on your network: ${n2}`);
|
|
17132
17443
|
if (!app.auth.hasOwner()) opts.out("No owner yet. Run: friend onboard (guided), or friend setup (just the account)");
|
|
17133
17444
|
const voiceAvailable = () => voice2?.available() ?? false;
|
|
17134
17445
|
let lastAvailable = voiceAvailable();
|
|
@@ -17307,7 +17618,7 @@ async function memoryCommand(args, io2) {
|
|
|
17307
17618
|
if (sub === "list") {
|
|
17308
17619
|
const notes = memory.list();
|
|
17309
17620
|
if (notes.length === 0) io2.out("No notes saved.");
|
|
17310
|
-
for (const
|
|
17621
|
+
for (const n2 of notes) io2.out(`#${n2.id} ${n2.text} (${n2.source}, ${new Date(n2.createdAt).toLocaleDateString()})`);
|
|
17311
17622
|
return 0;
|
|
17312
17623
|
}
|
|
17313
17624
|
if (sub === "forget") {
|
|
@@ -17324,8 +17635,8 @@ async function memoryCommand(args, io2) {
|
|
|
17324
17635
|
io2.out(`Forgot note #${id2}.`);
|
|
17325
17636
|
return 0;
|
|
17326
17637
|
}
|
|
17327
|
-
const
|
|
17328
|
-
if (
|
|
17638
|
+
const count3 = memory.count();
|
|
17639
|
+
if (count3 === 0) {
|
|
17329
17640
|
io2.out("No notes saved.");
|
|
17330
17641
|
return 0;
|
|
17331
17642
|
}
|
|
@@ -17342,8 +17653,8 @@ async function memoryCommand(args, io2) {
|
|
|
17342
17653
|
return 1;
|
|
17343
17654
|
}
|
|
17344
17655
|
memory.clear();
|
|
17345
|
-
audit.log("owner", "memory.clear", { count:
|
|
17346
|
-
io2.out(`Deleted ${
|
|
17656
|
+
audit.log("owner", "memory.clear", { count: count3, via: "cli" });
|
|
17657
|
+
io2.out(`Deleted ${count3} notes.`);
|
|
17347
17658
|
return 0;
|
|
17348
17659
|
} finally {
|
|
17349
17660
|
db.close();
|
|
@@ -17359,7 +17670,7 @@ function defaultCatalog() {
|
|
|
17359
17670
|
const here2 = dirname11(fileURLToPath5(import.meta.url));
|
|
17360
17671
|
return firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve10(here2, "plugins"), resolve10(here2, "../../../../plugins")]) ?? resolve10(here2, "plugins");
|
|
17361
17672
|
}
|
|
17362
|
-
var
|
|
17673
|
+
var count2 = (n2, word2) => `${n2} ${word2}${n2 === 1 ? "" : "s"}`;
|
|
17363
17674
|
async function pluginCommand(args, io2, deps = {}) {
|
|
17364
17675
|
const catalogDir = deps.catalogDir ?? defaultCatalog();
|
|
17365
17676
|
const [sub, ...rest] = args;
|
|
@@ -17375,7 +17686,7 @@ async function pluginCommand(args, io2, deps = {}) {
|
|
|
17375
17686
|
continue;
|
|
17376
17687
|
}
|
|
17377
17688
|
const m = r.manifest;
|
|
17378
|
-
io2.out(`${m.id} ${m.name} ${m.version} [${
|
|
17689
|
+
io2.out(`${m.id} ${m.name} ${m.version} [${count2(m.tools.length, "tool")}, ${count2(m.skills.length, "skill")}] ${m.description}`);
|
|
17379
17690
|
}
|
|
17380
17691
|
return 0;
|
|
17381
17692
|
}
|
|
@@ -17814,9 +18125,9 @@ function pick(options, typed) {
|
|
|
17814
18125
|
if (/^\d+$/.test(a)) return options[Number(a) - 1];
|
|
17815
18126
|
return options.find((o) => o.key.toLowerCase() === a || o.label.toLowerCase() === a || o.label.toLowerCase().split(/\s+/)[0] === a);
|
|
17816
18127
|
}
|
|
17817
|
-
function heading(ui,
|
|
18128
|
+
function heading(ui, n2, total, title2) {
|
|
17818
18129
|
ui.io.out("");
|
|
17819
|
-
ui.io.out(`\u2500\u2500 Step ${
|
|
18130
|
+
ui.io.out(`\u2500\u2500 Step ${n2} of ${total} \xB7 ${title2} ${"\u2500".repeat(Math.max(2, 52 - title2.length))}`);
|
|
17820
18131
|
}
|
|
17821
18132
|
async function choose(ui, question, options, suggested) {
|
|
17822
18133
|
const { io: io2 } = ui;
|
|
@@ -18194,7 +18505,7 @@ async function network(c) {
|
|
|
18194
18505
|
saveConfig(cfg);
|
|
18195
18506
|
const u = urlsFor(cfg.bind, cfg.port, void 0, cfg.https);
|
|
18196
18507
|
io2.out(`Friend will be at ${u.local}`);
|
|
18197
|
-
for (const
|
|
18508
|
+
for (const n2 of u.network) io2.out(`and on your network at ${n2}`);
|
|
18198
18509
|
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
18510
|
return "done";
|
|
18200
18511
|
}
|
|
@@ -18361,12 +18672,60 @@ async function autostartCommand(args, io2, autostart = real(io2)) {
|
|
|
18361
18672
|
}
|
|
18362
18673
|
}
|
|
18363
18674
|
|
|
18675
|
+
// ../agent-cli/src/commands/usage.ts
|
|
18676
|
+
var USAGE4 = "Usage: friend usage [--days N] | friend usage clear";
|
|
18677
|
+
var n = (v) => v.toLocaleString("en-US");
|
|
18678
|
+
var row = (label4, input, output, turns, width) => `${label4.padEnd(width)} ${n(input).padStart(12)} ${n(output).padStart(12)} ${String(turns).padStart(6)}`;
|
|
18679
|
+
async function usageCommand(args, io2, log) {
|
|
18680
|
+
let days = 7;
|
|
18681
|
+
let clear = false;
|
|
18682
|
+
for (let i = 0; i < args.length; i++) {
|
|
18683
|
+
const a = args[i];
|
|
18684
|
+
if (a === "clear") clear = true;
|
|
18685
|
+
else if (a === "--days" && /^\d+$/.test(args[i + 1] ?? "")) days = Number(args[++i]);
|
|
18686
|
+
else {
|
|
18687
|
+
io2.err(USAGE4);
|
|
18688
|
+
return 1;
|
|
18689
|
+
}
|
|
18690
|
+
}
|
|
18691
|
+
const db = log ? null : openDb(io2.dataDir);
|
|
18692
|
+
try {
|
|
18693
|
+
const l = log ?? new UsageLog(db);
|
|
18694
|
+
if (clear) {
|
|
18695
|
+
io2.out(`Removed ${l.clear()} saved counts.`);
|
|
18696
|
+
return 0;
|
|
18697
|
+
}
|
|
18698
|
+
const s = l.summary(days);
|
|
18699
|
+
if (s.total.turns === 0) {
|
|
18700
|
+
io2.out(`Nothing used in the last ${s.range} days.`);
|
|
18701
|
+
io2.out("Friend keeps counts only (no words), once a model has answered.");
|
|
18702
|
+
return 0;
|
|
18703
|
+
}
|
|
18704
|
+
io2.out(`Tokens used in the last ${s.range} days`);
|
|
18705
|
+
io2.out("");
|
|
18706
|
+
io2.out(`${"Day".padEnd(10)} ${"Input".padStart(12)} ${"Output".padStart(12)} ${"Turns".padStart(6)}`);
|
|
18707
|
+
for (const d of s.days) io2.out(row(d.day, d.input, d.output, d.turns, 10));
|
|
18708
|
+
io2.out(row("Total", s.total.input, s.total.output, s.total.turns, 10));
|
|
18709
|
+
io2.out("");
|
|
18710
|
+
io2.out("By model");
|
|
18711
|
+
for (const m of s.byModel) io2.out(row(`${m.provider} / ${m.model}`, m.input, m.output, m.turns, 28));
|
|
18712
|
+
io2.out("");
|
|
18713
|
+
io2.out("By where it came from");
|
|
18714
|
+
for (const m of s.bySource) io2.out(row(m.source, m.input, m.output, m.turns, 28));
|
|
18715
|
+
io2.out("");
|
|
18716
|
+
io2.out("Friend keeps counts only (no words). Remove them with: friend usage clear");
|
|
18717
|
+
return 0;
|
|
18718
|
+
} finally {
|
|
18719
|
+
db?.close();
|
|
18720
|
+
}
|
|
18721
|
+
}
|
|
18722
|
+
|
|
18364
18723
|
// ../agent-cli/src/commands/daemon.ts
|
|
18365
18724
|
import { spawn as spawn11, spawnSync as spawnSync2 } from "node:child_process";
|
|
18366
18725
|
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
18726
|
import { connect } from "node:net";
|
|
18368
18727
|
import { join as join24 } from "node:path";
|
|
18369
|
-
var
|
|
18728
|
+
var USAGE5 = "Usage: friend daemon start | stop | restart | status";
|
|
18370
18729
|
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
18371
18730
|
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
18372
18731
|
var realDeps = {
|
|
@@ -18471,7 +18830,7 @@ async function start(io2, deps) {
|
|
|
18471
18830
|
for (let i = 0; i < 60; i++) {
|
|
18472
18831
|
if (await deps.portOpen(cfg.port, 300)) {
|
|
18473
18832
|
io2.out(`Friend is running in the background (pid ${pid}) at ${urls.local}`);
|
|
18474
|
-
for (const
|
|
18833
|
+
for (const n2 of urls.network) io2.out(`Also on your network: ${n2}`);
|
|
18475
18834
|
io2.out(`Stop it with: friend daemon stop (log: ${log})`);
|
|
18476
18835
|
return 0;
|
|
18477
18836
|
}
|
|
@@ -18504,7 +18863,7 @@ async function status(io2, deps) {
|
|
|
18504
18863
|
if (r) {
|
|
18505
18864
|
io2.out(`Friend is running in the background (pid ${r.pid}, for ${since(deps.now() - r.startedAt)}).`);
|
|
18506
18865
|
io2.out(`Address: ${urls.local}`);
|
|
18507
|
-
for (const
|
|
18866
|
+
for (const n2 of urls.network) io2.out(`Also on your network: ${n2}`);
|
|
18508
18867
|
io2.out(`Log: ${logFileOf(io2.dataDir)}`);
|
|
18509
18868
|
return 0;
|
|
18510
18869
|
}
|
|
@@ -18525,7 +18884,7 @@ async function daemonCommand(args, io2, deps = realDeps) {
|
|
|
18525
18884
|
case "status":
|
|
18526
18885
|
return status(io2, deps);
|
|
18527
18886
|
default:
|
|
18528
|
-
io2.err(
|
|
18887
|
+
io2.err(USAGE5);
|
|
18529
18888
|
return 1;
|
|
18530
18889
|
}
|
|
18531
18890
|
}
|
|
@@ -18536,7 +18895,7 @@ import { appendFileSync } from "node:fs";
|
|
|
18536
18895
|
import { join as join25 } from "node:path";
|
|
18537
18896
|
var PACKAGE2 = "friend-assistant";
|
|
18538
18897
|
var real2 = {
|
|
18539
|
-
current: () => true ? "0.1.
|
|
18898
|
+
current: () => true ? "0.1.47" : "development",
|
|
18540
18899
|
latest: () => new Promise((resolve13) => {
|
|
18541
18900
|
const child = crossSpawn5("npm", ["view", PACKAGE2, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
18542
18901
|
let out = "";
|
|
@@ -18562,8 +18921,8 @@ var real2 = {
|
|
|
18562
18921
|
daemon: realDeps
|
|
18563
18922
|
};
|
|
18564
18923
|
var newer = (a, b) => {
|
|
18565
|
-
const
|
|
18566
|
-
const [x, y] = [
|
|
18924
|
+
const n2 = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
|
|
18925
|
+
const [x, y] = [n2(a), n2(b)];
|
|
18567
18926
|
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
18568
18927
|
return false;
|
|
18569
18928
|
};
|
|
@@ -18744,16 +19103,16 @@ async function secretCommand(kind, io2) {
|
|
|
18744
19103
|
io2.err("There is no owner yet. Run: friend setup");
|
|
18745
19104
|
return 1;
|
|
18746
19105
|
}
|
|
18747
|
-
const
|
|
18748
|
-
const first2 = await io2.ask(`New ${
|
|
19106
|
+
const label4 = kind === "password" ? "password" : "PIN";
|
|
19107
|
+
const first2 = await io2.ask(`New ${label4} (${kind === "password" ? "at least 8 characters" : "at least 6 digits"}): `, { hidden: true });
|
|
18749
19108
|
const problem = Auth.checkNewSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
|
|
18750
19109
|
if (problem) {
|
|
18751
19110
|
io2.err(problem);
|
|
18752
19111
|
return 1;
|
|
18753
19112
|
}
|
|
18754
|
-
const again = await io2.ask(`Type the new ${
|
|
19113
|
+
const again = await io2.ask(`Type the new ${label4} again: `, { hidden: true });
|
|
18755
19114
|
if (again !== first2) {
|
|
18756
|
-
io2.err(`The two ${
|
|
19115
|
+
io2.err(`The two ${label4}s are not the same. Nothing was changed.`);
|
|
18757
19116
|
return 1;
|
|
18758
19117
|
}
|
|
18759
19118
|
await auth.resetSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
|
|
@@ -18774,7 +19133,7 @@ async function statusCommand3(io2) {
|
|
|
18774
19133
|
io2.out(`Data folder: ${cfg.dataDir}`);
|
|
18775
19134
|
io2.out(`Owner set up: ${owner}`);
|
|
18776
19135
|
io2.out(`Listens on: ${cfg.bind}:${cfg.port}`);
|
|
18777
|
-
for (const
|
|
19136
|
+
for (const n2 of urlsFor(cfg.bind, cfg.port, void 0, cfg.https).network) io2.out(`Network URL: ${n2}`);
|
|
18778
19137
|
io2.out(`Public URL: ${cfg.publicUrl ?? "(none)"}`);
|
|
18779
19138
|
io2.out(`Workspace: ${cfg.roots.length ? cfg.roots.join(", ") : "(none, every file action asks first)"}`);
|
|
18780
19139
|
io2.out(`Model: ${cfg.model}`);
|
|
@@ -18783,7 +19142,7 @@ async function statusCommand3(io2) {
|
|
|
18783
19142
|
}
|
|
18784
19143
|
|
|
18785
19144
|
// ../agent-cli/src/cli.ts
|
|
18786
|
-
var
|
|
19145
|
+
var USAGE6 = `Usage: friend <command>
|
|
18787
19146
|
|
|
18788
19147
|
onboard guided setup, step by step: checks this PC, installs what is missing, asks what you want
|
|
18789
19148
|
doctor check what this PC has and what is missing (changes nothing)
|
|
@@ -18793,6 +19152,7 @@ var USAGE5 = `Usage: friend <command>
|
|
|
18793
19152
|
start run the gateway and a local device node
|
|
18794
19153
|
daemon start|stop|restart|status run Friend in the background (no terminal needed), stop it, or see if it is running
|
|
18795
19154
|
upgrade [--force] stop the background Friend, install the newest version, start it again
|
|
19155
|
+
usage [--days N]|clear tokens used, day by day (counts only, never words)
|
|
18796
19156
|
autostart on|off|status start Friend by itself when you sign in to this computer (Windows, macOS, Linux)
|
|
18797
19157
|
pair print a one-time code to connect another PC
|
|
18798
19158
|
node --url <ws-url> --code <code> [--roots a,b] [--name pc]
|
|
@@ -18826,6 +19186,8 @@ async function run(argv, io2) {
|
|
|
18826
19186
|
return await daemonCommand(rest, io2);
|
|
18827
19187
|
case "upgrade":
|
|
18828
19188
|
return await upgradeCommand(rest, io2);
|
|
19189
|
+
case "usage":
|
|
19190
|
+
return await usageCommand(rest, io2);
|
|
18829
19191
|
case "autostart":
|
|
18830
19192
|
return await autostartCommand(rest, io2);
|
|
18831
19193
|
case "pair":
|
|
@@ -18843,12 +19205,12 @@ async function run(argv, io2) {
|
|
|
18843
19205
|
case "help":
|
|
18844
19206
|
case "--help":
|
|
18845
19207
|
case "-h":
|
|
18846
|
-
io2.out(
|
|
19208
|
+
io2.out(USAGE6);
|
|
18847
19209
|
return 0;
|
|
18848
19210
|
case "version":
|
|
18849
19211
|
case "--version":
|
|
18850
19212
|
case "-v":
|
|
18851
|
-
io2.out(true ? "0.1.
|
|
19213
|
+
io2.out(true ? "0.1.47" : "development");
|
|
18852
19214
|
return 0;
|
|
18853
19215
|
case "lan":
|
|
18854
19216
|
return await lanCommand(rest, io2);
|
|
@@ -18867,7 +19229,7 @@ async function run(argv, io2) {
|
|
|
18867
19229
|
case "config":
|
|
18868
19230
|
return await configCommand(rest, io2);
|
|
18869
19231
|
default:
|
|
18870
|
-
io2.err(
|
|
19232
|
+
io2.err(USAGE6);
|
|
18871
19233
|
return 1;
|
|
18872
19234
|
}
|
|
18873
19235
|
} catch (e) {
|