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