friend-assistant 0.1.25 → 0.1.26
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 +2 -1
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +383 -258
- package/dist/web/assets/index-BCgrBB_0.js +4456 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-BKJTqNA2.js +0 -4455
package/dist/friend.mjs
CHANGED
|
@@ -1,169 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire as __friendCreateRequire } from 'node:module'; const require = __friendCreateRequire(import.meta.url);
|
|
3
3
|
|
|
4
|
-
// ../server/src/brokers/
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
var TOP = 5;
|
|
17
|
-
var r2 = (n) => Number.isFinite(n) ? Math.round((n + Number.EPSILON) * 100) / 100 : 0;
|
|
18
|
-
function summarize(holdings) {
|
|
19
|
-
let value = 0;
|
|
20
|
-
let invested = 0;
|
|
21
|
-
let yesterday = 0;
|
|
22
|
-
for (const h of holdings) {
|
|
23
|
-
value += h.quantity * h.lastPrice;
|
|
24
|
-
invested += h.quantity * h.avgPrice;
|
|
25
|
-
yesterday += h.quantity * h.closePrice;
|
|
26
|
-
}
|
|
27
|
-
const dayChange = value - yesterday;
|
|
28
|
-
const pnl = value - invested;
|
|
29
|
-
const move = (h) => h.quantity * (h.lastPrice - h.closePrice);
|
|
30
|
-
const worth = (h) => h.quantity * h.lastPrice;
|
|
31
|
-
const top = [...holdings].sort((a, b) => worth(b) - worth(a)).slice(0, TOP).map((h) => ({ symbol: h.symbol, pct: value > 0 ? r2(worth(h) / value * 100) : 0 }));
|
|
32
|
-
return {
|
|
33
|
-
value: r2(value),
|
|
34
|
-
invested: r2(invested),
|
|
35
|
-
pnl: r2(pnl),
|
|
36
|
-
pnlPct: invested > 0 ? r2(pnl / invested * 100) : 0,
|
|
37
|
-
dayChange: r2(dayChange),
|
|
38
|
-
dayChangePct: yesterday > 0 ? r2(dayChange / yesterday * 100) : 0,
|
|
39
|
-
gainers: holdings.filter((h) => move(h) > 0).sort((a, b) => move(b) - move(a)).slice(0, MOVERS),
|
|
40
|
-
losers: holdings.filter((h) => move(h) < 0).sort((a, b) => move(a) - move(b)).slice(0, MOVERS),
|
|
41
|
-
topHoldingPct: top[0]?.pct ?? 0,
|
|
42
|
-
top
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
var Portfolio = class {
|
|
46
|
-
constructor(provider, clock = Date.now, ttlMs = 3e4) {
|
|
47
|
-
this.provider = provider;
|
|
48
|
-
this.clock = clock;
|
|
49
|
-
this.ttlMs = ttlMs;
|
|
50
|
-
}
|
|
51
|
-
provider;
|
|
52
|
-
clock;
|
|
53
|
-
ttlMs;
|
|
54
|
-
cached = null;
|
|
55
|
-
inflight = null;
|
|
56
|
-
invalidate() {
|
|
57
|
-
this.cached = null;
|
|
58
|
-
}
|
|
59
|
-
snapshot() {
|
|
60
|
-
if (this.cached && this.clock() - this.cached.at < this.ttlMs) return Promise.resolve(this.cached);
|
|
61
|
-
this.inflight ??= this.read().finally(() => {
|
|
62
|
-
this.inflight = null;
|
|
63
|
-
});
|
|
64
|
-
return this.inflight;
|
|
65
|
-
}
|
|
66
|
-
async read() {
|
|
67
|
-
const at = this.clock();
|
|
68
|
-
const caps = this.provider.capabilities;
|
|
69
|
-
const [holdings, positions, funds, orders] = await Promise.all([
|
|
70
|
-
section(caps.holdings, () => this.provider.holdings()),
|
|
71
|
-
section(caps.positions, () => this.provider.positions()),
|
|
72
|
-
section(caps.funds, () => this.provider.funds()),
|
|
73
|
-
section(caps.orders, () => this.provider.orders())
|
|
74
|
-
]);
|
|
75
|
-
this.cached = { at, holdings, positions, funds, orders };
|
|
76
|
-
return this.cached;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
async function section(supported, read) {
|
|
80
|
-
if (!supported) return { ok: false, error: "Not available from this broker.", kind: "other" };
|
|
81
|
-
try {
|
|
82
|
-
return { ok: true, data: await read() };
|
|
83
|
-
} catch (e) {
|
|
84
|
-
return e instanceof BrokerError ? { ok: false, error: e.message, kind: e.kind } : { ok: false, error: "Could not read this.", kind: "other" };
|
|
4
|
+
// ../server/src/brokers/money.ts
|
|
5
|
+
var DEFAULT_MONEY = { symbol: "\u20B9", locale: "en-IN" };
|
|
6
|
+
var formats = /* @__PURE__ */ new Map();
|
|
7
|
+
function formatterFor(locale2) {
|
|
8
|
+
let f = formats.get(locale2);
|
|
9
|
+
if (!f) {
|
|
10
|
+
try {
|
|
11
|
+
f = new Intl.NumberFormat(locale2, { minimumFractionDigits: 2, maximumFractionDigits: 2, useGrouping: true });
|
|
12
|
+
} catch {
|
|
13
|
+
f = formatterFor(DEFAULT_MONEY.locale);
|
|
14
|
+
}
|
|
15
|
+
formats.set(locale2, f);
|
|
85
16
|
}
|
|
17
|
+
return f;
|
|
86
18
|
}
|
|
87
|
-
|
|
88
|
-
// ../server/src/brokers/report.ts
|
|
89
|
-
function formatInr(n) {
|
|
19
|
+
function formatMoney(n, o = DEFAULT_MONEY) {
|
|
90
20
|
const value = Number.isFinite(n) ? n : 0;
|
|
91
21
|
const fixed = Math.abs(value).toFixed(2);
|
|
92
|
-
const [whole, cents] = fixed.split(".");
|
|
93
|
-
const grouped = whole.length <= 3 ? whole : `${whole.slice(0, -3).replace(/\B(?=(\d{2})+(?!\d))/g, ",")},${whole.slice(-3)}`;
|
|
94
22
|
const negative = value < 0 && fixed !== "0.00";
|
|
95
|
-
return `${negative ? "-" : ""}
|
|
96
|
-
}
|
|
97
|
-
var signedInr = (n) => n > 4e-3 ? `+${formatInr(n)}` : formatInr(n);
|
|
98
|
-
var signedPct = (p) => {
|
|
99
|
-
const x = Number.isFinite(p) ? p : 0;
|
|
100
|
-
const text2 = Math.abs(x).toFixed(2);
|
|
101
|
-
return text2 === "0.00" ? "0.00%" : `${x > 0 ? "+" : "-"}${text2}%`;
|
|
102
|
-
};
|
|
103
|
-
var pct1 = (p) => Math.abs(p).toFixed(1);
|
|
104
|
-
var TITLE = { morning: "Morning report", close: "Market close report", custom: "Portfolio report" };
|
|
105
|
-
var WHY = { rate: "rate limit", session: "login expired", network: "network problem", auth: "login problem" };
|
|
106
|
-
var missing = (label2, s) => `${label2}: not available (${WHY[s.kind] ?? s.error.slice(0, 60)})`;
|
|
107
|
-
var CONCENTRATED = 30;
|
|
108
|
-
var BIG_DROP = 5;
|
|
109
|
-
function title(kind, broker, now, zone) {
|
|
110
|
-
const parts = Object.fromEntries(
|
|
111
|
-
new Intl.DateTimeFormat("en-GB", { timeZone: zone, weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(now).map((p) => [p.type, p.value])
|
|
112
|
-
);
|
|
113
|
-
return `${TITLE[kind]} - ${broker} - ${parts.weekday} ${parts.day} ${parts.month}, ${parts.hour}:${parts.minute}`;
|
|
114
|
-
}
|
|
115
|
-
var dayMovePct = (h) => h.closePrice > 0 ? (h.lastPrice - h.closePrice) / h.closePrice * 100 : 0;
|
|
116
|
-
function buildReport(input) {
|
|
117
|
-
const { snapshot: snap, caps } = input;
|
|
118
|
-
const out = [title(input.kind, input.broker, input.now, input.zone ?? "Asia/Kolkata"), ""];
|
|
119
|
-
if (caps.holdings) {
|
|
120
|
-
if (!snap.holdings.ok) out.push(missing("Holdings", snap.holdings));
|
|
121
|
-
else if (snap.holdings.data.length === 0) out.push("No holdings.");
|
|
122
|
-
else {
|
|
123
|
-
const holdings = snap.holdings.data;
|
|
124
|
-
const s = summarize(holdings);
|
|
125
|
-
out.push(`Portfolio: ${formatInr(s.value)} (invested ${formatInr(s.invested)})`);
|
|
126
|
-
out.push(`Profit/loss: ${signedInr(s.pnl)} (${signedPct(s.pnlPct)})`);
|
|
127
|
-
out.push(`Today: ${signedInr(s.dayChange)} (${signedPct(s.dayChangePct)})`);
|
|
128
|
-
const mover = (h) => `- ${h.symbol} ${signedInr(h.quantity * (h.lastPrice - h.closePrice))} (${signedPct(dayMovePct(h))})`;
|
|
129
|
-
if (s.gainers.length) out.push("", "Up today:", ...s.gainers.map(mover));
|
|
130
|
-
if (s.losers.length) out.push("", "Down today:", ...s.losers.map(mover));
|
|
131
|
-
const flags = [];
|
|
132
|
-
const top = s.top[0];
|
|
133
|
-
if (top && s.topHoldingPct > CONCENTRATED) flags.push(`- ${top.symbol} is ${pct1(s.topHoldingPct)}% of the portfolio.`);
|
|
134
|
-
for (const h of holdings) if (dayMovePct(h) < -BIG_DROP) flags.push(`- ${h.symbol} is down ${pct1(dayMovePct(h))}% today.`);
|
|
135
|
-
if (flags.length) out.push("", "Worth a look:", ...flags);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (caps.positions) {
|
|
139
|
-
out.push("");
|
|
140
|
-
if (!snap.positions.ok) out.push(missing("Positions", snap.positions));
|
|
141
|
-
else {
|
|
142
|
-
const open = snap.positions.data.filter((p) => p.quantity !== 0);
|
|
143
|
-
if (open.length === 0) out.push("No open positions.");
|
|
144
|
-
else {
|
|
145
|
-
out.push("Open positions:");
|
|
146
|
-
for (const p of open) out.push(`- ${p.symbol} ${p.product} ${p.quantity}${p.quantity < 0 ? " (short)" : ""} @ ${formatInr(p.avgPrice)}, P&L ${signedInr(p.pnl)}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (caps.funds) {
|
|
151
|
-
out.push("");
|
|
152
|
-
if (!snap.funds.ok) out.push(missing("Funds", snap.funds));
|
|
153
|
-
else out.push(`Funds: ${formatInr(snap.funds.data.available)} available, ${formatInr(snap.funds.data.used)} used`);
|
|
154
|
-
}
|
|
155
|
-
if (caps.orders && snap.orders.ok && snap.orders.data.length > 0) out.push(`Orders today: ${snap.orders.data.length}`);
|
|
156
|
-
return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
23
|
+
return `${negative ? "-" : ""}${o.symbol}${formatterFor(o.locale).format(Number(fixed))}`;
|
|
157
24
|
}
|
|
158
25
|
|
|
159
26
|
// ../server/src/brokers/tooltext.ts
|
|
160
27
|
var MAX_ROWS = 40;
|
|
161
|
-
var
|
|
28
|
+
var WHY = { rate: "Zerodha's rate limit was hit, try again in a minute", session: "the login has ended", network: "Zerodha could not be reached", auth: "there is a login problem" };
|
|
162
29
|
var asOf = (at, zone) => new Intl.DateTimeFormat("en-GB", { timeZone: zone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(new Date(at));
|
|
163
30
|
var rows = (items, line) => [...items.slice(0, MAX_ROWS).map(line), ...items.length > MAX_ROWS ? [`...and ${items.length - MAX_ROWS} more`] : []];
|
|
164
|
-
function partText(part, snap, zone) {
|
|
31
|
+
function partText(part, snap, zone, money = DEFAULT_MONEY) {
|
|
32
|
+
const formatInr = (n) => formatMoney(n, money);
|
|
165
33
|
const section2 = snap[part];
|
|
166
|
-
if (!section2.ok) return `Could not read the ${part}: ${
|
|
34
|
+
if (!section2.ok) return `Could not read the ${part}: ${WHY[section2.kind] ?? section2.error.slice(0, 100)}.`;
|
|
167
35
|
const head = `Zerodha ${part} as of ${asOf(snap.at, zone)}:`;
|
|
168
36
|
switch (part) {
|
|
169
37
|
case "holdings": {
|
|
@@ -563,6 +431,19 @@ function matcherMatches(m, args) {
|
|
|
563
431
|
return true;
|
|
564
432
|
}
|
|
565
433
|
|
|
434
|
+
// ../server/src/alexa/constants.ts
|
|
435
|
+
var ALEXA_LOCALES = ["en-IN", "hi-IN"];
|
|
436
|
+
var ALEXA_LIMITS = {
|
|
437
|
+
answerSeconds: { min: 2, max: 7.5, def: 6.5 },
|
|
438
|
+
holdMinutes: { min: 1, max: 60, def: 10 },
|
|
439
|
+
permissionSeconds: { min: 30, max: 600, def: 120 },
|
|
440
|
+
requestsPerMinute: { min: 10, max: 600, def: 60 },
|
|
441
|
+
maxDevices: { min: 1, max: 50, def: 10 }
|
|
442
|
+
};
|
|
443
|
+
var TOOL_NAME = /^[a-z][a-z0-9._-]{1,60}$/;
|
|
444
|
+
var NOT_BY_VOICE = ["shell.", "fs.", "code.", "settings.", "connectors.", "plugins.", "plugin.", "integrations.", "camera.", "screen.", "mcp."];
|
|
445
|
+
var isNeverByVoice = (tool) => NOT_BY_VOICE.some((p) => tool.startsWith(p));
|
|
446
|
+
|
|
566
447
|
// ../server/src/policy/engine.ts
|
|
567
448
|
var ASK_EVERY_TIME = /* @__PURE__ */ new Set(["camera.snap"]);
|
|
568
449
|
var MEMBER_TOOLS = /* @__PURE__ */ new Set(["weather.get", "stage.scene", "stage.show", "stage.update", "stage.hide", "stage.presence", "stage.mood"]);
|
|
@@ -586,7 +467,6 @@ var VOICE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
586
467
|
"stage.mood",
|
|
587
468
|
"stage.presence"
|
|
588
469
|
]);
|
|
589
|
-
var NOT_BY_VOICE = ["shell.", "fs.", "code.", "settings.", "connectors.", "plugins.", "plugin.", "integrations.", "camera.", "screen.", "mcp."];
|
|
590
470
|
var EDIT_TOOLS = /* @__PURE__ */ new Set(["fs.list", "fs.read", "fs.write", "code.run"]);
|
|
591
471
|
var isAdminTool = (tool) => tool.startsWith("connectors.") || tool.startsWith("plugins.") || tool.startsWith("integrations.");
|
|
592
472
|
var PolicyEngine = class {
|
|
@@ -607,7 +487,9 @@ var PolicyEngine = class {
|
|
|
607
487
|
if (input.trust === "member") return MEMBER_TOOLS.has(input.tool) ? "allow" : "deny";
|
|
608
488
|
if (input.trust === "voice-device") {
|
|
609
489
|
if (VOICE_TOOLS.has(input.tool)) return "allow";
|
|
610
|
-
if (input.baseTier === "T3" ||
|
|
490
|
+
if (input.baseTier === "T3" || isNeverByVoice(input.tool)) return "deny";
|
|
491
|
+
if (this.config.alexa.extraVoiceTools.includes(input.tool)) return "allow";
|
|
492
|
+
if (input.baseTier === "T0" || input.baseTier === "T1") return "deny";
|
|
611
493
|
return "ask";
|
|
612
494
|
}
|
|
613
495
|
let tier = input.baseTier;
|
|
@@ -2501,7 +2383,7 @@ var Agent = class {
|
|
|
2501
2383
|
case "portfolio.orders": {
|
|
2502
2384
|
const got = await b.snapshot("zerodha");
|
|
2503
2385
|
if (!got.ok) return { ok: false, text: got.sessionExpired ? ended : got.error };
|
|
2504
|
-
return { ok: true, text: partText(name.slice("portfolio.".length), got.snapshot, zone) };
|
|
2386
|
+
return { ok: true, text: partText(name.slice("portfolio.".length), got.snapshot, zone, { symbol: this.deps.config.brokers.currencySymbol, locale: this.deps.config.brokers.numberLocale }) };
|
|
2505
2387
|
}
|
|
2506
2388
|
case "reports.list": {
|
|
2507
2389
|
const all = b.schedules.list();
|
|
@@ -2807,15 +2689,35 @@ function sanitizeAlexa(raw) {
|
|
|
2807
2689
|
const label2 = typeof x.name === "string" ? x.name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "";
|
|
2808
2690
|
return [{ id: x.id, name: label2 || "Echo", addedAt: typeof x.addedAt === "number" && Number.isFinite(x.addedAt) ? x.addedAt : 0 }];
|
|
2809
2691
|
});
|
|
2692
|
+
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;
|
|
2693
|
+
const locales = [...new Set((Array.isArray(r.locales) ? r.locales : []).filter((l) => ALEXA_LOCALES.includes(l)))];
|
|
2694
|
+
const tools = [...new Set((Array.isArray(r.extraVoiceTools) ? r.extraVoiceTools : []).filter((t) => typeof t === "string" && TOOL_NAME.test(t) && !isNeverByVoice(t)))].slice(0, 20);
|
|
2695
|
+
const maxDevices = num3(r.maxDevices, ALEXA_LIMITS.maxDevices);
|
|
2810
2696
|
return {
|
|
2811
2697
|
installed: r.installed === true || r.installed === void 0 && (r.enabled === true || skillId !== ""),
|
|
2812
2698
|
enabled: r.enabled === true,
|
|
2813
2699
|
skillId,
|
|
2814
2700
|
invocationName: name,
|
|
2815
|
-
|
|
2701
|
+
skillName: (typeof r.skillName === "string" ? r.skillName.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 50) : "") || "Friend Assistant",
|
|
2702
|
+
locales: locales.length ? locales : [...ALEXA_LOCALES],
|
|
2703
|
+
answerSeconds: num3(r.answerSeconds, ALEXA_LIMITS.answerSeconds, false),
|
|
2704
|
+
holdMinutes: num3(r.holdMinutes, ALEXA_LIMITS.holdMinutes),
|
|
2705
|
+
permissionSeconds: num3(r.permissionSeconds, ALEXA_LIMITS.permissionSeconds),
|
|
2706
|
+
requestsPerMinute: num3(r.requestsPerMinute, ALEXA_LIMITS.requestsPerMinute),
|
|
2707
|
+
maxDevices,
|
|
2708
|
+
extraVoiceTools: tools,
|
|
2709
|
+
allowedUsers: users.slice(0, maxDevices)
|
|
2816
2710
|
};
|
|
2817
2711
|
}
|
|
2818
2712
|
var DEFAULT_TIMEZONE = "Asia/Kolkata";
|
|
2713
|
+
var validNumberLocale = (l) => {
|
|
2714
|
+
if (typeof l !== "string" || !l || l.length > 35) return false;
|
|
2715
|
+
try {
|
|
2716
|
+
return Intl.NumberFormat.supportedLocalesOf([l]).length === 1;
|
|
2717
|
+
} catch {
|
|
2718
|
+
return false;
|
|
2719
|
+
}
|
|
2720
|
+
};
|
|
2819
2721
|
var isValidTimeZone = (z12) => {
|
|
2820
2722
|
if (typeof z12 !== "string" || !z12) return false;
|
|
2821
2723
|
try {
|
|
@@ -2835,7 +2737,16 @@ function sanitizeBrokers(raw) {
|
|
|
2835
2737
|
const z12 = r.zerodha && typeof r.zerodha === "object" ? r.zerodha : {};
|
|
2836
2738
|
const str2 = (v, max) => typeof v === "string" ? v.trim().slice(0, max) : "";
|
|
2837
2739
|
const reminder = typeof r.loginReminderMinutes === "number" && Number.isFinite(r.loginReminderMinutes) ? Math.min(120, Math.max(0, Math.round(r.loginReminderMinutes))) : 15;
|
|
2740
|
+
const num3 = (v, min, max, def) => typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, Math.round(v))) : def;
|
|
2838
2741
|
return {
|
|
2742
|
+
currencySymbol: str2(r.currencySymbol, 4) || "\u20B9",
|
|
2743
|
+
currencyName: str2(r.currencyName, 20) || "rupees",
|
|
2744
|
+
numberLocale: validNumberLocale(r.numberLocale) ? r.numberLocale : "en-IN",
|
|
2745
|
+
concentrationPct: num3(r.concentrationPct, 5, 90, 30),
|
|
2746
|
+
bigDropPct: num3(r.bigDropPct, 1, 50, 5),
|
|
2747
|
+
cacheSeconds: num3(r.cacheSeconds, 5, 300, 30),
|
|
2748
|
+
maxSchedules: num3(r.maxSchedules, 1, 50, 20),
|
|
2749
|
+
maxTriesPerDay: num3(r.maxTriesPerDay, 1, 10, 3),
|
|
2839
2750
|
zerodha: {
|
|
2840
2751
|
installed: z12.installed === true || z12.installed === void 0 && (z12.enabled === true || !!str2(z12.apiKey, 64)),
|
|
2841
2752
|
enabled: z12.enabled === true,
|
|
@@ -3736,8 +3647,8 @@ async function stopLive(l) {
|
|
|
3736
3647
|
if (pid) killTree(pid);
|
|
3737
3648
|
}
|
|
3738
3649
|
var messageOf = (e) => (e instanceof Error ? e.message : String(e)).slice(0, 400);
|
|
3739
|
-
var sleepReject = (
|
|
3740
|
-
const t = setTimeout(() => rej(new Error(what)),
|
|
3650
|
+
var sleepReject = (ms2, what) => new Promise((_, rej) => {
|
|
3651
|
+
const t = setTimeout(() => rej(new Error(what)), ms2);
|
|
3741
3652
|
t.unref?.();
|
|
3742
3653
|
});
|
|
3743
3654
|
var McpHub = class {
|
|
@@ -4722,7 +4633,7 @@ var ChannelHub = class _ChannelHub {
|
|
|
4722
4633
|
this.deps = deps;
|
|
4723
4634
|
this.f = deps.fetch ?? fetch;
|
|
4724
4635
|
this.now = deps.clock ?? Date.now;
|
|
4725
|
-
this.sleep = deps.sleep ?? ((
|
|
4636
|
+
this.sleep = deps.sleep ?? ((ms2) => new Promise((r) => setTimeout(r, ms2)));
|
|
4726
4637
|
}
|
|
4727
4638
|
deps;
|
|
4728
4639
|
f;
|
|
@@ -5021,7 +4932,6 @@ var ChannelHub = class _ChannelHub {
|
|
|
5021
4932
|
|
|
5022
4933
|
// ../server/src/alexa/devices.ts
|
|
5023
4934
|
var MAX_WAITING = 20;
|
|
5024
|
-
var MAX_ALLOWED = 10;
|
|
5025
4935
|
var WAITING_TTL_MS = 24 * 36e5;
|
|
5026
4936
|
var USER_ID = /^amzn1\.ask\.account\.[A-Za-z0-9._-]{8,500}$/;
|
|
5027
4937
|
var SHORT = 8;
|
|
@@ -5056,7 +4966,7 @@ var AlexaDevices = class {
|
|
|
5056
4966
|
allow(short, name) {
|
|
5057
4967
|
this.prune();
|
|
5058
4968
|
const matches = [...this.waitingMap.values()].filter((w2) => short.length === SHORT && w2.id.endsWith(short));
|
|
5059
|
-
if (matches.length !== 1 || this.store.config().allowedUsers.length >=
|
|
4969
|
+
if (matches.length !== 1 || this.store.config().allowedUsers.length >= this.store.config().maxDevices) return false;
|
|
5060
4970
|
const w = matches[0];
|
|
5061
4971
|
const label2 = name.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) || "Echo";
|
|
5062
4972
|
this.store.update((a) => a.allowedUsers.push({ id: w.id, name: label2, addedAt: this.clock() }));
|
|
@@ -5159,6 +5069,7 @@ function shortForVoice(text2) {
|
|
|
5159
5069
|
|
|
5160
5070
|
// ../server/src/alexa/engine.ts
|
|
5161
5071
|
var FAST = ["PortfolioIntent", "WeatherIntent", "TasksIntent", "StatusIntent"];
|
|
5072
|
+
var ms = (v, def) => typeof v === "function" ? v() : v ?? def;
|
|
5162
5073
|
var AlexaEngine = class {
|
|
5163
5074
|
constructor(d) {
|
|
5164
5075
|
this.d = d;
|
|
@@ -5206,7 +5117,7 @@ var AlexaEngine = class {
|
|
|
5206
5117
|
ready(key, lang) {
|
|
5207
5118
|
const slot = this.slots.get(key);
|
|
5208
5119
|
if (slot?.pending) return { speech: say(lang, "working"), end: false };
|
|
5209
|
-
if (!slot || this.d.clock() - slot.at > (this.d.holdMs
|
|
5120
|
+
if (!slot || this.d.clock() - slot.at > ms(this.d.holdMs, 6e5)) {
|
|
5210
5121
|
this.slots.delete(key);
|
|
5211
5122
|
return { speech: say(lang, "nothing"), end: true };
|
|
5212
5123
|
}
|
|
@@ -5231,7 +5142,7 @@ var AlexaEngine = class {
|
|
|
5231
5142
|
const waited = await Promise.race([
|
|
5232
5143
|
work,
|
|
5233
5144
|
new Promise((resolve11) => {
|
|
5234
|
-
timer = setTimeout(() => resolve11("late"), this.d.budgetMs
|
|
5145
|
+
timer = setTimeout(() => resolve11("late"), ms(this.d.budgetMs, 6500));
|
|
5235
5146
|
})
|
|
5236
5147
|
]);
|
|
5237
5148
|
clearTimeout(timer);
|
|
@@ -5250,6 +5161,70 @@ var AlexaEngine = class {
|
|
|
5250
5161
|
}
|
|
5251
5162
|
};
|
|
5252
5163
|
|
|
5164
|
+
// ../server/src/alexa/package.ts
|
|
5165
|
+
var packageFileNames = (locales) => ["skill.json", ...locales.map((l) => `${l}.json`)];
|
|
5166
|
+
var langOf2 = (locale2) => locale2.startsWith("hi") ? "hi" : "en";
|
|
5167
|
+
var FIXED = {
|
|
5168
|
+
en: {
|
|
5169
|
+
PortfolioIntent: ["how is my portfolio", "how is my portfolio doing", "how are my stocks doing", "what is my portfolio", "my portfolio", "portfolio"],
|
|
5170
|
+
TasksIntent: ["what are my tasks", "what do i have to do", "what is on my list", "read my tasks", "my tasks"],
|
|
5171
|
+
StatusIntent: ["status", "give me the status", "what is the status", "are you there"],
|
|
5172
|
+
ReadyIntent: ["is it ready", "is it done", "are you done", "do you have an answer", "what is the answer"],
|
|
5173
|
+
weather: ["what is the weather in {city}", "how is the weather in {city}", "weather in {city}", "tell me the weather in {city}"]
|
|
5174
|
+
},
|
|
5175
|
+
hi: {
|
|
5176
|
+
PortfolioIntent: ["\u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B \u0915\u0948\u0938\u093E \u0939\u0948", "\u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B \u0915\u0948\u0938\u093E \u091A\u0932 \u0930\u0939\u093E \u0939\u0948", "\u092E\u0947\u0930\u0947 \u0936\u0947\u092F\u0930 \u0915\u0948\u0938\u0947 \u0939\u0948\u0902", "\u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B", "\u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B"],
|
|
5177
|
+
TasksIntent: ["\u092E\u0947\u0930\u0947 \u0915\u093E\u092E \u0915\u094D\u092F\u093E \u0939\u0948\u0902", "\u092E\u0941\u091D\u0947 \u0915\u094D\u092F\u093E \u0915\u0930\u0928\u093E \u0939\u0948", "\u092E\u0947\u0930\u0940 \u0938\u0942\u091A\u0940 \u092E\u0947\u0902 \u0915\u094D\u092F\u093E \u0939\u0948", "\u092E\u0947\u0930\u0947 \u0915\u093E\u092E \u092A\u0922\u093C\u094B", "\u092E\u0947\u0930\u0947 \u0915\u093E\u092E"],
|
|
5178
|
+
StatusIntent: ["\u0938\u094D\u0925\u093F\u0924\u093F", "\u0938\u094D\u0925\u093F\u0924\u093F \u092C\u0924\u093E\u0913", "\u0915\u094D\u092F\u093E \u0924\u0941\u092E \u092F\u0939\u093E\u0901 \u0939\u094B"],
|
|
5179
|
+
ReadyIntent: ["\u0915\u094D\u092F\u093E \u0924\u0948\u092F\u093E\u0930 \u0939\u0948", "\u091C\u0935\u093E\u092C \u0924\u0948\u092F\u093E\u0930 \u0939\u0948", "\u0939\u094B \u0917\u092F\u093E \u0915\u094D\u092F\u093E", "\u0915\u094D\u092F\u093E \u091C\u0935\u093E\u092C \u0939\u0948"],
|
|
5180
|
+
weather: ["{city} \u092E\u0947\u0902 \u092E\u094C\u0938\u092E \u0915\u0948\u0938\u093E \u0939\u0948", "{city} \u0915\u093E \u092E\u094C\u0938\u092E \u0915\u0948\u0938\u093E \u0939\u0948", "{city} \u0915\u093E \u092E\u094C\u0938\u092E"]
|
|
5181
|
+
}
|
|
5182
|
+
};
|
|
5183
|
+
var BUILT_IN = ["AMAZON.CancelIntent", "AMAZON.HelpIntent", "AMAZON.StopIntent", "AMAZON.FallbackIntent", "AMAZON.NavigateHomeIntent"];
|
|
5184
|
+
function intents(lang) {
|
|
5185
|
+
const f = FIXED[lang];
|
|
5186
|
+
return [
|
|
5187
|
+
...BUILT_IN.map((name) => ({ name, samples: [] })),
|
|
5188
|
+
{ name: "PortfolioIntent", samples: f.PortfolioIntent },
|
|
5189
|
+
{ name: "WeatherIntent", slots: [{ name: "city", type: "AMAZON.SearchQuery" }], samples: f.weather },
|
|
5190
|
+
{ name: "TasksIntent", samples: f.TasksIntent },
|
|
5191
|
+
{ name: "StatusIntent", samples: f.StatusIntent },
|
|
5192
|
+
{ name: "ReadyIntent", samples: f.ReadyIntent },
|
|
5193
|
+
// open questions: one free-text slot per sentence, so each lead-in is its own intent and the engine can put the sentence back together
|
|
5194
|
+
...LEAD_INTENTS.map((name) => ({ name, slots: [{ name: "q", type: "AMAZON.SearchQuery" }], samples: [`${LEAD_INS[name][lang]} {q}`] }))
|
|
5195
|
+
];
|
|
5196
|
+
}
|
|
5197
|
+
var model = (invocationName, lang) => JSON.stringify({ interactionModel: { languageModel: { invocationName, intents: intents(lang), types: [] } } }, null, 2);
|
|
5198
|
+
var locale = (lang, invocationName, name) => ({
|
|
5199
|
+
name,
|
|
5200
|
+
summary: lang === "hi" ? "\u0905\u092A\u0928\u0947 \u092B\u094D\u0930\u0947\u0902\u0921 \u0905\u0938\u093F\u0938\u094D\u091F\u0947\u0902\u091F \u0938\u0947 \u092C\u093E\u0924 \u0915\u0930\u0947\u0902\u0964" : "Talk to your own Friend assistant.",
|
|
5201
|
+
description: lang === "hi" ? "\u092F\u0939 \u0906\u092A\u0915\u0947 \u0905\u092A\u0928\u0947 \u092B\u094D\u0930\u0947\u0902\u0921 \u0905\u0938\u093F\u0938\u094D\u091F\u0947\u0902\u091F \u0915\u0947 \u0932\u093F\u090F \u0928\u093F\u091C\u0940 \u0938\u094D\u0915\u093F\u0932 \u0939\u0948\u0964 \u0906\u092A\u0915\u0947 \u0905\u092A\u0928\u0947 \u0915\u0902\u092A\u094D\u092F\u0942\u091F\u0930 \u092A\u0930 \u091A\u0932\u0924\u093E \u0939\u0948\u0964" : "A private skill for your own self-hosted Friend assistant. It talks to your own computer.",
|
|
5202
|
+
examplePhrases: [lang === "hi" ? `\u0905\u0932\u0947\u0915\u094D\u0938\u093E, ${invocationName} \u0938\u0947 \u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B \u0915\u0948\u0938\u093E \u0939\u0948 \u092A\u0942\u091B\u094B` : `Alexa, ask ${invocationName} how my portfolio is`, `Alexa, open ${invocationName}`, `Alexa, ask ${invocationName} what is on my list`],
|
|
5203
|
+
keywords: ["friend", "assistant"]
|
|
5204
|
+
});
|
|
5205
|
+
function buildSkillPackage(o) {
|
|
5206
|
+
const locales = o.locales?.length ? o.locales : ALEXA_LOCALES;
|
|
5207
|
+
const skillName = o.skillName?.trim() || "Friend Assistant";
|
|
5208
|
+
const problem = invocationNameProblem(o.invocationName);
|
|
5209
|
+
if (problem) throw new Error(problem);
|
|
5210
|
+
if (!/^https:\/\/[^\s/]+\/\S*$/.test(o.endpoint)) throw new Error("The address must be an https address, such as https://your-address/api/alexa.");
|
|
5211
|
+
const manifest = {
|
|
5212
|
+
manifest: {
|
|
5213
|
+
manifestVersion: "1.0",
|
|
5214
|
+
publishingInformation: {
|
|
5215
|
+
locales: Object.fromEntries(locales.map((l) => [l, locale(langOf2(l), o.invocationName, skillName)])),
|
|
5216
|
+
isAvailableWorldwide: false,
|
|
5217
|
+
distributionCountries: [...new Set(locales.map((l) => l.slice(3)))],
|
|
5218
|
+
category: "ORGANIZERS_AND_ASSISTANTS",
|
|
5219
|
+
testingInstructions: "A private skill for its owner's own self-hosted assistant. It is used in development mode only."
|
|
5220
|
+
},
|
|
5221
|
+
apis: { custom: { endpoint: { uri: o.endpoint, sslCertificateType: "Trusted" } } },
|
|
5222
|
+
privacyAndCompliance: { allowsPurchases: false, usesPersonalInfo: false, isChildDirected: false, isExportCompliant: true, containsAds: false }
|
|
5223
|
+
}
|
|
5224
|
+
};
|
|
5225
|
+
return { "skill.json": JSON.stringify(manifest, null, 2), ...Object.fromEntries(locales.map((l) => [`${l}.json`, model(o.invocationName, langOf2(l))])) };
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5253
5228
|
// ../server/src/alexa/permission.ts
|
|
5254
5229
|
var YES = /* @__PURE__ */ new Set(["yes", "y", "yeah", "ok", "okay", "ha", "haan", "han", "\u0939\u093E\u0901", "\u0939\u093E\u0902", "\u0939\u093E"]);
|
|
5255
5230
|
var NO = /* @__PURE__ */ new Set(["no", "n", "nope", "nahi", "nahin", "\u0928\u0939\u0940\u0902", "\u0928\u093E"]);
|
|
@@ -5270,17 +5245,19 @@ var VoicePermission = class {
|
|
|
5270
5245
|
const channels = this.d.linked();
|
|
5271
5246
|
const screen = this.d.screenOpen();
|
|
5272
5247
|
if (card.needsPin || !channels.length && !screen) return answer(card.id, false);
|
|
5248
|
+
const waitMs = typeof this.d.waitMs === "function" ? this.d.waitMs() : this.d.waitMs ?? 12e4;
|
|
5249
|
+
const minutes2 = Math.max(1, Math.round(waitMs / 6e4));
|
|
5273
5250
|
const entry = {
|
|
5274
5251
|
id: card.id,
|
|
5275
5252
|
answer,
|
|
5276
|
-
timer: setTimeout(() => this.settle(card.id, false),
|
|
5253
|
+
timer: setTimeout(() => this.settle(card.id, false), waitMs)
|
|
5277
5254
|
};
|
|
5278
5255
|
this.waiting.push(entry);
|
|
5279
5256
|
if (screen) base(card);
|
|
5280
5257
|
if (channels.length) {
|
|
5281
5258
|
const channel = channels.includes("telegram") ? "telegram" : channels[0];
|
|
5282
5259
|
const text2 = `Alexa asked to: ${card.summary}
|
|
5283
|
-
Reply YES to allow it, or NO to refuse, within
|
|
5260
|
+
Reply YES to allow it, or NO to refuse, within ${minutes2} ${minutes2 === 1 ? "minute" : "minutes"}.`;
|
|
5284
5261
|
this.d.send(channel, "me", text2).then(
|
|
5285
5262
|
(r) => {
|
|
5286
5263
|
if (!r.ok && !screen) this.settle(card.id, false);
|
|
@@ -5437,6 +5414,7 @@ var AlexaHub = class {
|
|
|
5437
5414
|
this.d = d;
|
|
5438
5415
|
this.devices = new AlexaDevices(d.store, d.clock);
|
|
5439
5416
|
this.permission = new VoicePermission({
|
|
5417
|
+
waitMs: () => d.config.alexa.permissionSeconds * 1e3,
|
|
5440
5418
|
send: async (channel, to, text2) => {
|
|
5441
5419
|
const r = await d.channels.send(channel, to, text2);
|
|
5442
5420
|
return { ok: r.ok };
|
|
@@ -5444,7 +5422,13 @@ var AlexaHub = class {
|
|
|
5444
5422
|
linked: d.linked,
|
|
5445
5423
|
screenOpen: d.screenOpen
|
|
5446
5424
|
});
|
|
5447
|
-
this.engine = new AlexaEngine({
|
|
5425
|
+
this.engine = new AlexaEngine({
|
|
5426
|
+
ask: (t, l, k) => this.ask(t, l, k),
|
|
5427
|
+
fast: d.fast,
|
|
5428
|
+
clock: d.clock,
|
|
5429
|
+
budgetMs: () => d.config.alexa.answerSeconds * 1e3,
|
|
5430
|
+
holdMs: () => d.config.alexa.holdMinutes * 6e4
|
|
5431
|
+
});
|
|
5448
5432
|
}
|
|
5449
5433
|
d;
|
|
5450
5434
|
devices;
|
|
@@ -5456,7 +5440,7 @@ var AlexaHub = class {
|
|
|
5456
5440
|
}
|
|
5457
5441
|
status() {
|
|
5458
5442
|
const a = this.d.config.alexa;
|
|
5459
|
-
return { installed: a.installed, enabled: a.enabled, skillIdSet: !!a.skillId, invocationName: a.invocationName, endpoint: this.endpoint(), waiting: this.devices.waiting(), allowed: this.devices.allowed() };
|
|
5443
|
+
return { installed: a.installed, enabled: a.enabled, skillIdSet: !!a.skillId, invocationName: a.invocationName, endpoint: this.endpoint(), files: packageFileNames(a.locales), waiting: this.devices.waiting(), allowed: this.devices.allowed() };
|
|
5460
5444
|
}
|
|
5461
5445
|
/** The owner's reply on Telegram or WhatsApp. True when it answered a permission request from Alexa. */
|
|
5462
5446
|
tryApprove(text2) {
|
|
@@ -5528,83 +5512,21 @@ var AlexaHub = class {
|
|
|
5528
5512
|
}
|
|
5529
5513
|
};
|
|
5530
5514
|
|
|
5531
|
-
// ../server/src/alexa/package.ts
|
|
5532
|
-
var PACKAGE_FILES = ["skill.json", "en-IN.json", "hi-IN.json"];
|
|
5533
|
-
var FIXED = {
|
|
5534
|
-
en: {
|
|
5535
|
-
PortfolioIntent: ["how is my portfolio", "how is my portfolio doing", "how are my stocks doing", "what is my portfolio", "my portfolio", "portfolio"],
|
|
5536
|
-
TasksIntent: ["what are my tasks", "what do i have to do", "what is on my list", "read my tasks", "my tasks"],
|
|
5537
|
-
StatusIntent: ["status", "give me the status", "what is the status", "are you there"],
|
|
5538
|
-
ReadyIntent: ["is it ready", "is it done", "are you done", "do you have an answer", "what is the answer"],
|
|
5539
|
-
weather: ["what is the weather in {city}", "how is the weather in {city}", "weather in {city}", "tell me the weather in {city}"]
|
|
5540
|
-
},
|
|
5541
|
-
hi: {
|
|
5542
|
-
PortfolioIntent: ["\u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B \u0915\u0948\u0938\u093E \u0939\u0948", "\u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B \u0915\u0948\u0938\u093E \u091A\u0932 \u0930\u0939\u093E \u0939\u0948", "\u092E\u0947\u0930\u0947 \u0936\u0947\u092F\u0930 \u0915\u0948\u0938\u0947 \u0939\u0948\u0902", "\u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B", "\u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B"],
|
|
5543
|
-
TasksIntent: ["\u092E\u0947\u0930\u0947 \u0915\u093E\u092E \u0915\u094D\u092F\u093E \u0939\u0948\u0902", "\u092E\u0941\u091D\u0947 \u0915\u094D\u092F\u093E \u0915\u0930\u0928\u093E \u0939\u0948", "\u092E\u0947\u0930\u0940 \u0938\u0942\u091A\u0940 \u092E\u0947\u0902 \u0915\u094D\u092F\u093E \u0939\u0948", "\u092E\u0947\u0930\u0947 \u0915\u093E\u092E \u092A\u0922\u093C\u094B", "\u092E\u0947\u0930\u0947 \u0915\u093E\u092E"],
|
|
5544
|
-
StatusIntent: ["\u0938\u094D\u0925\u093F\u0924\u093F", "\u0938\u094D\u0925\u093F\u0924\u093F \u092C\u0924\u093E\u0913", "\u0915\u094D\u092F\u093E \u0924\u0941\u092E \u092F\u0939\u093E\u0901 \u0939\u094B"],
|
|
5545
|
-
ReadyIntent: ["\u0915\u094D\u092F\u093E \u0924\u0948\u092F\u093E\u0930 \u0939\u0948", "\u091C\u0935\u093E\u092C \u0924\u0948\u092F\u093E\u0930 \u0939\u0948", "\u0939\u094B \u0917\u092F\u093E \u0915\u094D\u092F\u093E", "\u0915\u094D\u092F\u093E \u091C\u0935\u093E\u092C \u0939\u0948"],
|
|
5546
|
-
weather: ["{city} \u092E\u0947\u0902 \u092E\u094C\u0938\u092E \u0915\u0948\u0938\u093E \u0939\u0948", "{city} \u0915\u093E \u092E\u094C\u0938\u092E \u0915\u0948\u0938\u093E \u0939\u0948", "{city} \u0915\u093E \u092E\u094C\u0938\u092E"]
|
|
5547
|
-
}
|
|
5548
|
-
};
|
|
5549
|
-
var BUILT_IN = ["AMAZON.CancelIntent", "AMAZON.HelpIntent", "AMAZON.StopIntent", "AMAZON.FallbackIntent", "AMAZON.NavigateHomeIntent"];
|
|
5550
|
-
function intents(lang) {
|
|
5551
|
-
const f = FIXED[lang];
|
|
5552
|
-
return [
|
|
5553
|
-
...BUILT_IN.map((name) => ({ name, samples: [] })),
|
|
5554
|
-
{ name: "PortfolioIntent", samples: f.PortfolioIntent },
|
|
5555
|
-
{ name: "WeatherIntent", slots: [{ name: "city", type: "AMAZON.SearchQuery" }], samples: f.weather },
|
|
5556
|
-
{ name: "TasksIntent", samples: f.TasksIntent },
|
|
5557
|
-
{ name: "StatusIntent", samples: f.StatusIntent },
|
|
5558
|
-
{ name: "ReadyIntent", samples: f.ReadyIntent },
|
|
5559
|
-
// open questions: one free-text slot per sentence, so each lead-in is its own intent and the engine can put the sentence back together
|
|
5560
|
-
...LEAD_INTENTS.map((name) => ({ name, slots: [{ name: "q", type: "AMAZON.SearchQuery" }], samples: [`${LEAD_INS[name][lang]} {q}`] }))
|
|
5561
|
-
];
|
|
5562
|
-
}
|
|
5563
|
-
var model = (invocationName, lang) => JSON.stringify({ interactionModel: { languageModel: { invocationName, intents: intents(lang), types: [] } } }, null, 2);
|
|
5564
|
-
var SKILL_NAME = "Friend Assistant";
|
|
5565
|
-
var locale = (lang, invocationName) => ({
|
|
5566
|
-
name: SKILL_NAME,
|
|
5567
|
-
summary: lang === "hi" ? "\u0905\u092A\u0928\u0947 \u092B\u094D\u0930\u0947\u0902\u0921 \u0905\u0938\u093F\u0938\u094D\u091F\u0947\u0902\u091F \u0938\u0947 \u092C\u093E\u0924 \u0915\u0930\u0947\u0902\u0964" : "Talk to your own Friend assistant.",
|
|
5568
|
-
description: lang === "hi" ? "\u092F\u0939 \u0906\u092A\u0915\u0947 \u0905\u092A\u0928\u0947 \u092B\u094D\u0930\u0947\u0902\u0921 \u0905\u0938\u093F\u0938\u094D\u091F\u0947\u0902\u091F \u0915\u0947 \u0932\u093F\u090F \u0928\u093F\u091C\u0940 \u0938\u094D\u0915\u093F\u0932 \u0939\u0948\u0964 \u0906\u092A\u0915\u0947 \u0905\u092A\u0928\u0947 \u0915\u0902\u092A\u094D\u092F\u0942\u091F\u0930 \u092A\u0930 \u091A\u0932\u0924\u093E \u0939\u0948\u0964" : "A private skill for your own self-hosted Friend assistant. It talks to your own computer.",
|
|
5569
|
-
examplePhrases: [lang === "hi" ? `\u0905\u0932\u0947\u0915\u094D\u0938\u093E, ${invocationName} \u0938\u0947 \u092E\u0947\u0930\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B \u0915\u0948\u0938\u093E \u0939\u0948 \u092A\u0942\u091B\u094B` : `Alexa, ask ${invocationName} how my portfolio is`, `Alexa, open ${invocationName}`, `Alexa, ask ${invocationName} what is on my list`],
|
|
5570
|
-
keywords: ["friend", "assistant"]
|
|
5571
|
-
});
|
|
5572
|
-
function buildSkillPackage(o) {
|
|
5573
|
-
const problem = invocationNameProblem(o.invocationName);
|
|
5574
|
-
if (problem) throw new Error(problem);
|
|
5575
|
-
if (!/^https:\/\/[^\s/]+\/\S*$/.test(o.endpoint)) throw new Error("The address must be an https address, such as https://your-address/api/alexa.");
|
|
5576
|
-
const manifest = {
|
|
5577
|
-
manifest: {
|
|
5578
|
-
manifestVersion: "1.0",
|
|
5579
|
-
publishingInformation: {
|
|
5580
|
-
locales: { "en-IN": locale("en", o.invocationName), "hi-IN": locale("hi", o.invocationName) },
|
|
5581
|
-
isAvailableWorldwide: false,
|
|
5582
|
-
distributionCountries: ["IN"],
|
|
5583
|
-
category: "ORGANIZERS_AND_ASSISTANTS",
|
|
5584
|
-
testingInstructions: "A private skill for its owner's own self-hosted assistant. It is used in development mode only."
|
|
5585
|
-
},
|
|
5586
|
-
apis: { custom: { endpoint: { uri: o.endpoint, sslCertificateType: "Trusted" } } },
|
|
5587
|
-
privacyAndCompliance: { allowsPurchases: false, usesPersonalInfo: false, isChildDirected: false, isExportCompliant: true, containsAds: false }
|
|
5588
|
-
}
|
|
5589
|
-
};
|
|
5590
|
-
return {
|
|
5591
|
-
"skill.json": JSON.stringify(manifest, null, 2),
|
|
5592
|
-
"en-IN.json": model(o.invocationName, "en"),
|
|
5593
|
-
"hi-IN.json": model(o.invocationName, "hi")
|
|
5594
|
-
};
|
|
5595
|
-
}
|
|
5596
|
-
|
|
5597
5515
|
// ../server/src/alexa/fast.ts
|
|
5598
|
-
var rupees = (n, lang
|
|
5516
|
+
var rupees = (n, lang, o = {}) => {
|
|
5517
|
+
const word2 = o.currencyName ?? "rupees";
|
|
5518
|
+
const spoken = lang === "hi" && word2 === "rupees" ? "\u0930\u0941\u092A\u092F\u0947" : word2;
|
|
5519
|
+
return `${formatMoney(Math.abs(n), { symbol: "", locale: (o.money ?? DEFAULT_MONEY).locale }).replace(/[.,]00$/, "")} ${spoken}`;
|
|
5520
|
+
};
|
|
5599
5521
|
var pct = (n) => String(Math.round(Math.abs(n) * 100) / 100);
|
|
5600
|
-
function portfolioSpeech(s, lang) {
|
|
5522
|
+
function portfolioSpeech(s, lang, o = {}) {
|
|
5601
5523
|
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.";
|
|
5602
5524
|
const up = s.dayChange >= 0;
|
|
5603
5525
|
const gain = s.pnl >= 0;
|
|
5604
5526
|
if (lang === "hi") {
|
|
5605
|
-
return `\u0906\u092A\u0915\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B ${rupees(s.value, lang)} \u0915\u093E \u0939\u0948\u0964 \u0906\u091C \u092F\u0939 ${pct(s.dayChangePct)} \u092A\u094D\u0930\u0924\u093F\u0936\u0924 ${up ? "\u090A\u092A\u0930" : "\u0928\u0940\u091A\u0947"} \u0939\u0948\u0964 \u0915\u0941\u0932 ${gain ? "\u092E\u0941\u0928\u093E\u092B\u093C\u093E" : "\u0918\u093E\u091F\u093E"} ${rupees(s.pnl, lang)} \u0939\u0948\u0964`;
|
|
5527
|
+
return `\u0906\u092A\u0915\u093E \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B ${rupees(s.value, lang, o)} \u0915\u093E \u0939\u0948\u0964 \u0906\u091C \u092F\u0939 ${pct(s.dayChangePct)} \u092A\u094D\u0930\u0924\u093F\u0936\u0924 ${up ? "\u090A\u092A\u0930" : "\u0928\u0940\u091A\u0947"} \u0939\u0948\u0964 \u0915\u0941\u0932 ${gain ? "\u092E\u0941\u0928\u093E\u092B\u093C\u093E" : "\u0918\u093E\u091F\u093E"} ${rupees(s.pnl, lang, o)} \u0939\u0948\u0964`;
|
|
5606
5528
|
}
|
|
5607
|
-
return `Your portfolio is worth ${rupees(s.value, lang)}. It is ${up ? "up" : "down"} ${pct(s.dayChangePct)} percent today, ${rupees(s.dayChange, lang)}. Overall you have a ${gain ? "profit" : "loss"} of ${rupees(s.pnl, lang)}.`;
|
|
5529
|
+
return `Your portfolio is worth ${rupees(s.value, lang, o)}. It is ${up ? "up" : "down"} ${pct(s.dayChangePct)} percent today, ${rupees(s.dayChange, lang, o)}. Overall you have a ${gain ? "profit" : "loss"} of ${rupees(s.pnl, lang, o)}.`;
|
|
5608
5530
|
}
|
|
5609
5531
|
var FIRST_TASKS = 3;
|
|
5610
5532
|
function tasksSpeech(rows3, lang) {
|
|
@@ -5620,8 +5542,157 @@ function statusSpeech(s, lang) {
|
|
|
5620
5542
|
return `Friend is running. You have ${s.openTasks} open ${s.openTasks === 1 ? "task" : "tasks"}.${s.brokerLoggedIn === false ? " Zerodha needs you to log in." : ""}`;
|
|
5621
5543
|
}
|
|
5622
5544
|
|
|
5545
|
+
// ../server/src/brokers/types.ts
|
|
5546
|
+
var BrokerError = class extends Error {
|
|
5547
|
+
constructor(message, kind) {
|
|
5548
|
+
super(message);
|
|
5549
|
+
this.kind = kind;
|
|
5550
|
+
this.name = "BrokerError";
|
|
5551
|
+
}
|
|
5552
|
+
kind;
|
|
5553
|
+
};
|
|
5554
|
+
|
|
5555
|
+
// ../server/src/brokers/portfolio.ts
|
|
5556
|
+
var MOVERS = 3;
|
|
5557
|
+
var TOP = 5;
|
|
5558
|
+
var r2 = (n) => Number.isFinite(n) ? Math.round((n + Number.EPSILON) * 100) / 100 : 0;
|
|
5559
|
+
function summarize(holdings) {
|
|
5560
|
+
let value = 0;
|
|
5561
|
+
let invested = 0;
|
|
5562
|
+
let yesterday = 0;
|
|
5563
|
+
for (const h of holdings) {
|
|
5564
|
+
value += h.quantity * h.lastPrice;
|
|
5565
|
+
invested += h.quantity * h.avgPrice;
|
|
5566
|
+
yesterday += h.quantity * h.closePrice;
|
|
5567
|
+
}
|
|
5568
|
+
const dayChange = value - yesterday;
|
|
5569
|
+
const pnl = value - invested;
|
|
5570
|
+
const move = (h) => h.quantity * (h.lastPrice - h.closePrice);
|
|
5571
|
+
const worth = (h) => h.quantity * h.lastPrice;
|
|
5572
|
+
const top = [...holdings].sort((a, b) => worth(b) - worth(a)).slice(0, TOP).map((h) => ({ symbol: h.symbol, pct: value > 0 ? r2(worth(h) / value * 100) : 0 }));
|
|
5573
|
+
return {
|
|
5574
|
+
value: r2(value),
|
|
5575
|
+
invested: r2(invested),
|
|
5576
|
+
pnl: r2(pnl),
|
|
5577
|
+
pnlPct: invested > 0 ? r2(pnl / invested * 100) : 0,
|
|
5578
|
+
dayChange: r2(dayChange),
|
|
5579
|
+
dayChangePct: yesterday > 0 ? r2(dayChange / yesterday * 100) : 0,
|
|
5580
|
+
gainers: holdings.filter((h) => move(h) > 0).sort((a, b) => move(b) - move(a)).slice(0, MOVERS),
|
|
5581
|
+
losers: holdings.filter((h) => move(h) < 0).sort((a, b) => move(a) - move(b)).slice(0, MOVERS),
|
|
5582
|
+
topHoldingPct: top[0]?.pct ?? 0,
|
|
5583
|
+
top
|
|
5584
|
+
};
|
|
5585
|
+
}
|
|
5586
|
+
var Portfolio = class {
|
|
5587
|
+
constructor(provider, clock = Date.now, ttlMs = 3e4) {
|
|
5588
|
+
this.provider = provider;
|
|
5589
|
+
this.clock = clock;
|
|
5590
|
+
this.ttlMs = ttlMs;
|
|
5591
|
+
}
|
|
5592
|
+
provider;
|
|
5593
|
+
clock;
|
|
5594
|
+
ttlMs;
|
|
5595
|
+
cached = null;
|
|
5596
|
+
inflight = null;
|
|
5597
|
+
invalidate() {
|
|
5598
|
+
this.cached = null;
|
|
5599
|
+
}
|
|
5600
|
+
snapshot() {
|
|
5601
|
+
if (this.cached && this.clock() - this.cached.at < (typeof this.ttlMs === "function" ? this.ttlMs() : this.ttlMs)) return Promise.resolve(this.cached);
|
|
5602
|
+
this.inflight ??= this.read().finally(() => {
|
|
5603
|
+
this.inflight = null;
|
|
5604
|
+
});
|
|
5605
|
+
return this.inflight;
|
|
5606
|
+
}
|
|
5607
|
+
async read() {
|
|
5608
|
+
const at = this.clock();
|
|
5609
|
+
const caps = this.provider.capabilities;
|
|
5610
|
+
const [holdings, positions, funds, orders] = await Promise.all([
|
|
5611
|
+
section(caps.holdings, () => this.provider.holdings()),
|
|
5612
|
+
section(caps.positions, () => this.provider.positions()),
|
|
5613
|
+
section(caps.funds, () => this.provider.funds()),
|
|
5614
|
+
section(caps.orders, () => this.provider.orders())
|
|
5615
|
+
]);
|
|
5616
|
+
this.cached = { at, holdings, positions, funds, orders };
|
|
5617
|
+
return this.cached;
|
|
5618
|
+
}
|
|
5619
|
+
};
|
|
5620
|
+
async function section(supported, read) {
|
|
5621
|
+
if (!supported) return { ok: false, error: "Not available from this broker.", kind: "other" };
|
|
5622
|
+
try {
|
|
5623
|
+
return { ok: true, data: await read() };
|
|
5624
|
+
} catch (e) {
|
|
5625
|
+
return e instanceof BrokerError ? { ok: false, error: e.message, kind: e.kind } : { ok: false, error: "Could not read this.", kind: "other" };
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
// ../server/src/brokers/report.ts
|
|
5630
|
+
var signedMoney = (n, m) => n > 4e-3 ? `+${formatMoney(n, m)}` : formatMoney(n, m);
|
|
5631
|
+
var signedPct = (p) => {
|
|
5632
|
+
const x = Number.isFinite(p) ? p : 0;
|
|
5633
|
+
const text2 = Math.abs(x).toFixed(2);
|
|
5634
|
+
return text2 === "0.00" ? "0.00%" : `${x > 0 ? "+" : "-"}${text2}%`;
|
|
5635
|
+
};
|
|
5636
|
+
var pct1 = (p) => Math.abs(p).toFixed(1);
|
|
5637
|
+
var TITLE = { morning: "Morning report", close: "Market close report", custom: "Portfolio report" };
|
|
5638
|
+
var WHY2 = { rate: "rate limit", session: "login expired", network: "network problem", auth: "login problem" };
|
|
5639
|
+
var missing = (label2, s) => `${label2}: not available (${WHY2[s.kind] ?? s.error.slice(0, 60)})`;
|
|
5640
|
+
var DEFAULT_REPORT_OPTIONS = { money: DEFAULT_MONEY, concentrationPct: 30, bigDropPct: 5 };
|
|
5641
|
+
function title(kind, broker, now, zone) {
|
|
5642
|
+
const parts = Object.fromEntries(
|
|
5643
|
+
new Intl.DateTimeFormat("en-GB", { timeZone: zone, weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(now).map((p) => [p.type, p.value])
|
|
5644
|
+
);
|
|
5645
|
+
return `${TITLE[kind]} - ${broker} - ${parts.weekday} ${parts.day} ${parts.month}, ${parts.hour}:${parts.minute}`;
|
|
5646
|
+
}
|
|
5647
|
+
var dayMovePct = (h) => h.closePrice > 0 ? (h.lastPrice - h.closePrice) / h.closePrice * 100 : 0;
|
|
5648
|
+
function buildReport(input) {
|
|
5649
|
+
const { snapshot: snap, caps } = input;
|
|
5650
|
+
const o = input.options ?? DEFAULT_REPORT_OPTIONS;
|
|
5651
|
+
const fmt = (n) => formatMoney(n, o.money);
|
|
5652
|
+
const signed = (n) => signedMoney(n, o.money);
|
|
5653
|
+
const out = [title(input.kind, input.broker, input.now, input.zone ?? "Asia/Kolkata"), ""];
|
|
5654
|
+
if (caps.holdings) {
|
|
5655
|
+
if (!snap.holdings.ok) out.push(missing("Holdings", snap.holdings));
|
|
5656
|
+
else if (snap.holdings.data.length === 0) out.push("No holdings.");
|
|
5657
|
+
else {
|
|
5658
|
+
const holdings = snap.holdings.data;
|
|
5659
|
+
const s = summarize(holdings);
|
|
5660
|
+
out.push(`Portfolio: ${fmt(s.value)} (invested ${fmt(s.invested)})`);
|
|
5661
|
+
out.push(`Profit/loss: ${signed(s.pnl)} (${signedPct(s.pnlPct)})`);
|
|
5662
|
+
out.push(`Today: ${signed(s.dayChange)} (${signedPct(s.dayChangePct)})`);
|
|
5663
|
+
const mover = (h) => `- ${h.symbol} ${signed(h.quantity * (h.lastPrice - h.closePrice))} (${signedPct(dayMovePct(h))})`;
|
|
5664
|
+
if (s.gainers.length) out.push("", "Up today:", ...s.gainers.map(mover));
|
|
5665
|
+
if (s.losers.length) out.push("", "Down today:", ...s.losers.map(mover));
|
|
5666
|
+
const flags = [];
|
|
5667
|
+
const top = s.top[0];
|
|
5668
|
+
if (top && s.topHoldingPct > o.concentrationPct) flags.push(`- ${top.symbol} is ${pct1(s.topHoldingPct)}% of the portfolio.`);
|
|
5669
|
+
for (const h of holdings) if (dayMovePct(h) < -o.bigDropPct) flags.push(`- ${h.symbol} is down ${pct1(dayMovePct(h))}% today.`);
|
|
5670
|
+
if (flags.length) out.push("", "Worth a look:", ...flags);
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
if (caps.positions) {
|
|
5674
|
+
out.push("");
|
|
5675
|
+
if (!snap.positions.ok) out.push(missing("Positions", snap.positions));
|
|
5676
|
+
else {
|
|
5677
|
+
const open = snap.positions.data.filter((p) => p.quantity !== 0);
|
|
5678
|
+
if (open.length === 0) out.push("No open positions.");
|
|
5679
|
+
else {
|
|
5680
|
+
out.push("Open positions:");
|
|
5681
|
+
for (const p of open) out.push(`- ${p.symbol} ${p.product} ${p.quantity}${p.quantity < 0 ? " (short)" : ""} @ ${fmt(p.avgPrice)}, P&L ${signed(p.pnl)}`);
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
if (caps.funds) {
|
|
5686
|
+
out.push("");
|
|
5687
|
+
if (!snap.funds.ok) out.push(missing("Funds", snap.funds));
|
|
5688
|
+
else out.push(`Funds: ${fmt(snap.funds.data.available)} available, ${fmt(snap.funds.data.used)} used`);
|
|
5689
|
+
}
|
|
5690
|
+
if (caps.orders && snap.orders.ok && snap.orders.data.length > 0) out.push(`Orders today: ${snap.orders.data.length}`);
|
|
5691
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
5692
|
+
}
|
|
5693
|
+
|
|
5623
5694
|
// ../server/src/brokers/scheduler.ts
|
|
5624
|
-
var
|
|
5695
|
+
var DEFAULT_TRIES_PER_DAY = 3;
|
|
5625
5696
|
var WEEKDAYS = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
|
|
5626
5697
|
function localDay(now, zone) {
|
|
5627
5698
|
return new Intl.DateTimeFormat("en-CA", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit" }).format(now);
|
|
@@ -5647,7 +5718,7 @@ var ReportScheduler = class {
|
|
|
5647
5718
|
let sent = 0;
|
|
5648
5719
|
for (const s of this.d.schedules.list()) {
|
|
5649
5720
|
if (!s.enabled || s.lastRunDay === day || !s.days.includes(weekday) || time < s.time) continue;
|
|
5650
|
-
if (this.d.schedules.noteAttempt(s.id, day) >
|
|
5721
|
+
if (this.d.schedules.noteAttempt(s.id, day) > (this.d.maxTries?.() ?? DEFAULT_TRIES_PER_DAY)) continue;
|
|
5651
5722
|
try {
|
|
5652
5723
|
await this.d.run(s);
|
|
5653
5724
|
this.d.schedules.markRun(s.id, day);
|
|
@@ -5677,10 +5748,13 @@ var toSchedule = (r) => ({
|
|
|
5677
5748
|
lastRunDay: r.last_run_day
|
|
5678
5749
|
});
|
|
5679
5750
|
var ReportSchedules = class {
|
|
5680
|
-
|
|
5751
|
+
/** `max`: how many schedules there may be (the owner's setting). */
|
|
5752
|
+
constructor(db, max = () => MAX_SCHEDULES) {
|
|
5681
5753
|
this.db = db;
|
|
5754
|
+
this.max = max;
|
|
5682
5755
|
}
|
|
5683
5756
|
db;
|
|
5757
|
+
max;
|
|
5684
5758
|
list() {
|
|
5685
5759
|
return this.db.prepare("select id, name, broker, kind, time, days, channel, commentary, enabled, last_run_day from report_schedules order by id").all().map(toSchedule);
|
|
5686
5760
|
}
|
|
@@ -5693,7 +5767,8 @@ var ReportSchedules = class {
|
|
|
5693
5767
|
if (days.length === 0 || days.length !== new Set(s.days).size) throw new Error("Choose the days as numbers 1 (Monday) to 7 (Sunday).");
|
|
5694
5768
|
if (!CHANNELS.includes(s.channel)) throw new Error("The channel must be telegram, whatsapp or sms.");
|
|
5695
5769
|
if (typeof s.broker !== "string" || !/^[a-z0-9-]{1,40}$/.test(s.broker)) throw new Error("That is not a broker id.");
|
|
5696
|
-
|
|
5770
|
+
const limit = this.max();
|
|
5771
|
+
if (this.list().length >= limit) throw new Error(`There can be at most ${limit} schedules.`);
|
|
5697
5772
|
const info = this.db.prepare("insert into report_schedules (name, broker, kind, time, days, channel, commentary, enabled) values (?, ?, ?, ?, ?, ?, ?, ?)").run(name, s.broker, s.kind, s.time, days.join(","), s.channel, s.commentary ? 1 : 0, s.enabled ? 1 : 0);
|
|
5698
5773
|
return this.list().find((x) => x.id === Number(info.lastInsertRowid));
|
|
5699
5774
|
}
|
|
@@ -5918,16 +5993,18 @@ function createZerodha(opts) {
|
|
|
5918
5993
|
}
|
|
5919
5994
|
|
|
5920
5995
|
// ../server/src/brokers/hub.ts
|
|
5996
|
+
var reportOptions = (b) => ({ money: { symbol: b.currencySymbol, locale: b.numberLocale }, concentrationPct: b.concentrationPct, bigDropPct: b.bigDropPct });
|
|
5921
5997
|
var NAMES = { zerodha: "Zerodha" };
|
|
5922
5998
|
var minutes = (hhmm) => Number(hhmm.slice(0, 2)) * 60 + Number(hhmm.slice(3, 5));
|
|
5923
5999
|
var BrokerHub = class {
|
|
5924
6000
|
constructor(d) {
|
|
5925
6001
|
this.d = d;
|
|
5926
|
-
this.schedules = new ReportSchedules(d.db);
|
|
6002
|
+
this.schedules = new ReportSchedules(d.db, () => d.config.brokers.maxSchedules);
|
|
5927
6003
|
this.sessions = new BrokerSessions(d.dataDir, this.now);
|
|
5928
6004
|
this.scheduler = new ReportScheduler({
|
|
5929
6005
|
schedules: this.schedules,
|
|
5930
6006
|
zone: () => d.config.brokers.timezone,
|
|
6007
|
+
maxTries: () => d.config.brokers.maxTriesPerDay,
|
|
5931
6008
|
skipDates: () => d.config.brokers.skipDates,
|
|
5932
6009
|
run: (s) => this.deliver(s)
|
|
5933
6010
|
});
|
|
@@ -5952,7 +6029,7 @@ ${z12.apiSecret}`;
|
|
|
5952
6029
|
if (!usable) this.providers.delete("zerodha");
|
|
5953
6030
|
else if (!have || have.made !== made) {
|
|
5954
6031
|
const provider = createZerodha({ apiKey: z12.apiKey, apiSecret: z12.apiSecret, fetch: this.d.fetch, clock: this.now });
|
|
5955
|
-
this.providers.set("zerodha", { provider, portfolio: new Portfolio(provider, this.now), made });
|
|
6032
|
+
this.providers.set("zerodha", { provider, portfolio: new Portfolio(provider, this.now, () => this.d.config.brokers.cacheSeconds * 1e3), made });
|
|
5956
6033
|
}
|
|
5957
6034
|
const now = this.providers.get("zerodha");
|
|
5958
6035
|
if (now) {
|
|
@@ -6035,7 +6112,7 @@ ${z12.apiSecret}`;
|
|
|
6035
6112
|
async report(id2, kind = "custom") {
|
|
6036
6113
|
const got = await this.snapshot(id2);
|
|
6037
6114
|
if (!got.ok) return got;
|
|
6038
|
-
const text2 = buildReport({ snapshot: got.snapshot, kind, caps: got.caps, now: new Date(this.now()), broker: got.broker, zone: got.zone });
|
|
6115
|
+
const text2 = buildReport({ snapshot: got.snapshot, kind, caps: got.caps, now: new Date(this.now()), broker: got.broker, zone: got.zone, options: reportOptions(this.d.config.brokers) });
|
|
6039
6116
|
this.d.audit.log("system", "broker.report", { broker: id2, kind, lines: text2.split("\n").length });
|
|
6040
6117
|
return { ok: true, text: text2 };
|
|
6041
6118
|
}
|
|
@@ -6769,8 +6846,8 @@ function createGateway(deps) {
|
|
|
6769
6846
|
try {
|
|
6770
6847
|
pairLimiter.check(key);
|
|
6771
6848
|
} catch (e) {
|
|
6772
|
-
const
|
|
6773
|
-
return reply.code(429).header("retry-after", Math.ceil(
|
|
6849
|
+
const ms2 = e instanceof LockoutError ? e.retryAfterMs : 6e4;
|
|
6850
|
+
return reply.code(429).header("retry-after", Math.ceil(ms2 / 1e3)).send({ error: "Too many attempts" });
|
|
6774
6851
|
}
|
|
6775
6852
|
try {
|
|
6776
6853
|
const token = auth.redeemPairCode(body.data.code, body.data.name);
|
|
@@ -6989,11 +7066,16 @@ function createGateway(deps) {
|
|
|
6989
7066
|
return brokerError(reply, e);
|
|
6990
7067
|
}
|
|
6991
7068
|
});
|
|
6992
|
-
|
|
7069
|
+
let alexaLimit = config.alexa.requestsPerMinute;
|
|
7070
|
+
let alexaWindow = new SlidingWindow(alexaLimit, 6e4);
|
|
6993
7071
|
void app.register(async (inst) => {
|
|
6994
7072
|
inst.removeContentTypeParser("application/json");
|
|
6995
7073
|
inst.addContentTypeParser("application/json", { parseAs: "buffer", bodyLimit: 32 * 1024 }, (_req, body, done) => done(null, body));
|
|
6996
7074
|
inst.addHook("onRequest", async (req, reply) => {
|
|
7075
|
+
if (config.alexa.requestsPerMinute !== alexaLimit) {
|
|
7076
|
+
alexaLimit = config.alexa.requestsPerMinute;
|
|
7077
|
+
alexaWindow = new SlidingWindow(alexaLimit, 6e4);
|
|
7078
|
+
}
|
|
6997
7079
|
if (!alexaWindow.allow(`alexa:${req.ip}`)) return reply.code(429).header("retry-after", 30).send();
|
|
6998
7080
|
});
|
|
6999
7081
|
inst.post("/api/alexa", async (req, reply) => {
|
|
@@ -8283,13 +8365,29 @@ var SettingsPatch = z11.object({
|
|
|
8283
8365
|
apiSecret: z11.string().trim().max(100)
|
|
8284
8366
|
}).partial().strict(),
|
|
8285
8367
|
timezone: z11.string().trim().min(1).max(60),
|
|
8368
|
+
currencySymbol: z11.string().trim().min(1).max(4),
|
|
8369
|
+
currencyName: z11.string().trim().min(1).max(20),
|
|
8370
|
+
numberLocale: z11.string().trim().min(2).max(35),
|
|
8371
|
+
concentrationPct: z11.number().int().min(5).max(90),
|
|
8372
|
+
bigDropPct: z11.number().int().min(1).max(50),
|
|
8373
|
+
cacheSeconds: z11.number().int().min(5).max(300),
|
|
8374
|
+
maxSchedules: z11.number().int().min(1).max(50),
|
|
8375
|
+
maxTriesPerDay: z11.number().int().min(1).max(10),
|
|
8286
8376
|
skipDates: z11.array(z11.string().regex(/^\d{4}-\d{2}-\d{2}$/, "A date like 2026-10-02")).max(100),
|
|
8287
8377
|
loginReminderMinutes: z11.number().int().min(0).max(120)
|
|
8288
8378
|
}).partial().strict(),
|
|
8289
8379
|
/** The owner's Alexa skill. The skill id is not a secret; both need the PIN to change. */
|
|
8290
8380
|
alexa: z11.object({
|
|
8291
8381
|
skillId: z11.string().trim().max(100).regex(/^(amzn1\.ask\.skill\.[A-Za-z0-9-]{8,80})?$/, "A skill id like amzn1.ask.skill.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),
|
|
8292
|
-
invocationName: z11.string().trim().max(60)
|
|
8382
|
+
invocationName: z11.string().trim().max(60),
|
|
8383
|
+
skillName: z11.string().trim().min(1).max(50),
|
|
8384
|
+
locales: z11.array(z11.enum(ALEXA_LOCALES)).min(1).max(ALEXA_LOCALES.length),
|
|
8385
|
+
answerSeconds: z11.number().min(ALEXA_LIMITS.answerSeconds.min).max(ALEXA_LIMITS.answerSeconds.max),
|
|
8386
|
+
holdMinutes: z11.number().int().min(ALEXA_LIMITS.holdMinutes.min).max(ALEXA_LIMITS.holdMinutes.max),
|
|
8387
|
+
permissionSeconds: z11.number().int().min(ALEXA_LIMITS.permissionSeconds.min).max(ALEXA_LIMITS.permissionSeconds.max),
|
|
8388
|
+
requestsPerMinute: z11.number().int().min(ALEXA_LIMITS.requestsPerMinute.min).max(ALEXA_LIMITS.requestsPerMinute.max),
|
|
8389
|
+
maxDevices: z11.number().int().min(ALEXA_LIMITS.maxDevices.min).max(ALEXA_LIMITS.maxDevices.max),
|
|
8390
|
+
extraVoiceTools: z11.array(z11.string().regex(TOOL_NAME, "A tool name like calendar.list")).max(20)
|
|
8293
8391
|
}).partial().strict(),
|
|
8294
8392
|
/** What the assistant may do with money. Only "read" exists in this version; any other level is refused. */
|
|
8295
8393
|
trading: z11.object({ level: z11.enum(["read"]) }).partial().strict(),
|
|
@@ -8351,7 +8449,15 @@ function settingsView(c) {
|
|
|
8351
8449
|
zerodha: { installed: c.brokers.zerodha.installed, enabled: c.brokers.zerodha.enabled, apiKey: c.brokers.zerodha.apiKey, secretSet: !!c.brokers.zerodha.apiSecret },
|
|
8352
8450
|
timezone: c.brokers.timezone,
|
|
8353
8451
|
skipDates: [...c.brokers.skipDates],
|
|
8354
|
-
loginReminderMinutes: c.brokers.loginReminderMinutes
|
|
8452
|
+
loginReminderMinutes: c.brokers.loginReminderMinutes,
|
|
8453
|
+
currencySymbol: c.brokers.currencySymbol,
|
|
8454
|
+
currencyName: c.brokers.currencyName,
|
|
8455
|
+
numberLocale: c.brokers.numberLocale,
|
|
8456
|
+
concentrationPct: c.brokers.concentrationPct,
|
|
8457
|
+
bigDropPct: c.brokers.bigDropPct,
|
|
8458
|
+
cacheSeconds: c.brokers.cacheSeconds,
|
|
8459
|
+
maxSchedules: c.brokers.maxSchedules,
|
|
8460
|
+
maxTriesPerDay: c.brokers.maxTriesPerDay
|
|
8355
8461
|
},
|
|
8356
8462
|
trading: { level: c.trading.level },
|
|
8357
8463
|
alexa: {
|
|
@@ -8359,7 +8465,15 @@ function settingsView(c) {
|
|
|
8359
8465
|
enabled: c.alexa.enabled,
|
|
8360
8466
|
skillId: c.alexa.skillId,
|
|
8361
8467
|
invocationName: c.alexa.invocationName,
|
|
8362
|
-
allowed: c.alexa.allowedUsers.map((u) => ({ id: "\u2026" + u.id.slice(-8), name: u.name }))
|
|
8468
|
+
allowed: c.alexa.allowedUsers.map((u) => ({ id: "\u2026" + u.id.slice(-8), name: u.name })),
|
|
8469
|
+
skillName: c.alexa.skillName,
|
|
8470
|
+
locales: [...c.alexa.locales],
|
|
8471
|
+
answerSeconds: c.alexa.answerSeconds,
|
|
8472
|
+
holdMinutes: c.alexa.holdMinutes,
|
|
8473
|
+
permissionSeconds: c.alexa.permissionSeconds,
|
|
8474
|
+
requestsPerMinute: c.alexa.requestsPerMinute,
|
|
8475
|
+
maxDevices: c.alexa.maxDevices,
|
|
8476
|
+
extraVoiceTools: [...c.alexa.extraVoiceTools]
|
|
8363
8477
|
},
|
|
8364
8478
|
look: { ...c.look },
|
|
8365
8479
|
ollama: { url: c.ollama.url, model: c.ollama.model, vision: !!c.ollama.vision, contextTokens: c.ollama.contextTokens },
|
|
@@ -8401,6 +8515,10 @@ function applySettings(config, raw) {
|
|
|
8401
8515
|
}
|
|
8402
8516
|
const p = parsed.data;
|
|
8403
8517
|
if (p.brokers?.timezone !== void 0 && !isValidTimeZone(p.brokers.timezone)) throw new SettingsError("brokers.timezone: That is not a time zone, try one like Asia/Kolkata.");
|
|
8518
|
+
if (p.brokers?.numberLocale !== void 0 && !validNumberLocale(p.brokers.numberLocale)) throw new SettingsError("brokers.numberLocale: That is not a number format this PC knows, try one like en-IN or en-US.");
|
|
8519
|
+
for (const t of p.alexa?.extraVoiceTools ?? []) {
|
|
8520
|
+
if (isNeverByVoice(t)) throw new SettingsError(`alexa.extraVoiceTools: ${t} can never be used by voice (it reaches your PC, files, settings, add-ons or camera).`);
|
|
8521
|
+
}
|
|
8404
8522
|
if (p.alexa?.invocationName) {
|
|
8405
8523
|
const problem = invocationNameProblem(p.alexa.invocationName);
|
|
8406
8524
|
if (problem) throw new SettingsError(`alexa.invocationName: ${problem}`);
|
|
@@ -8457,11 +8575,17 @@ function applySettings(config, raw) {
|
|
|
8457
8575
|
if (rest.timezone !== void 0) config.brokers.timezone = rest.timezone;
|
|
8458
8576
|
if (rest.skipDates !== void 0) config.brokers.skipDates = sanitizeBrokers({ skipDates: rest.skipDates }).skipDates;
|
|
8459
8577
|
if (rest.loginReminderMinutes !== void 0) config.brokers.loginReminderMinutes = rest.loginReminderMinutes;
|
|
8578
|
+
for (const k of ["currencySymbol", "currencyName", "numberLocale", "concentrationPct", "bigDropPct", "cacheSeconds", "maxSchedules", "maxTriesPerDay"]) {
|
|
8579
|
+
if (rest[k] !== void 0) config.brokers[k] = rest[k];
|
|
8580
|
+
}
|
|
8460
8581
|
}
|
|
8461
8582
|
if (p.alexa) {
|
|
8462
8583
|
config.alexa.installed = true;
|
|
8463
8584
|
if (p.alexa.skillId !== void 0) config.alexa.skillId = p.alexa.skillId;
|
|
8464
8585
|
if (p.alexa.invocationName !== void 0) config.alexa.invocationName = p.alexa.invocationName;
|
|
8586
|
+
for (const k of ["skillName", "locales", "answerSeconds", "holdMinutes", "permissionSeconds", "requestsPerMinute", "maxDevices", "extraVoiceTools"]) {
|
|
8587
|
+
if (p.alexa[k] !== void 0) config.alexa[k] = p.alexa[k];
|
|
8588
|
+
}
|
|
8465
8589
|
}
|
|
8466
8590
|
if (p.approvals) config.approvals = p.approvals;
|
|
8467
8591
|
if (p.channels) {
|
|
@@ -9754,7 +9878,7 @@ async function createApp(opts) {
|
|
|
9754
9878
|
const got = await brokerHub.snapshot("zerodha");
|
|
9755
9879
|
if (!got.ok) return got.sessionExpired ? "Zerodha needs you to log in first." : got.error;
|
|
9756
9880
|
if (!got.snapshot.holdings.ok) return "I could not read your holdings just now.";
|
|
9757
|
-
return portfolioSpeech(summarize(got.snapshot.holdings.data), locale2.startsWith("hi") ? "hi" : "en");
|
|
9881
|
+
return portfolioSpeech(summarize(got.snapshot.holdings.data), locale2.startsWith("hi") ? "hi" : "en", { money: { symbol: config.brokers.currencySymbol, locale: config.brokers.numberLocale }, currencyName: config.brokers.currencyName });
|
|
9758
9882
|
},
|
|
9759
9883
|
WeatherIntent: async (_locale, slots) => {
|
|
9760
9884
|
if (!slots.city) return "Tell me which city, for example: what is the weather in Pune.";
|
|
@@ -10241,9 +10365,10 @@ async function createApp(opts) {
|
|
|
10241
10365
|
},
|
|
10242
10366
|
packageFile: (name) => {
|
|
10243
10367
|
const endpoint = alexaHub.endpoint();
|
|
10244
|
-
if (!
|
|
10368
|
+
if (!packageFileNames(config.alexa.locales).includes(name) || !endpoint || !config.alexa.invocationName) return null;
|
|
10245
10369
|
try {
|
|
10246
|
-
|
|
10370
|
+
const text2 = buildSkillPackage({ invocationName: config.alexa.invocationName, endpoint, skillName: config.alexa.skillName, locales: config.alexa.locales })[name];
|
|
10371
|
+
return text2 ? { type: "application/json", text: text2 } : null;
|
|
10247
10372
|
} catch {
|
|
10248
10373
|
return null;
|
|
10249
10374
|
}
|
|
@@ -11523,7 +11648,7 @@ async function startFriend(opts) {
|
|
|
11523
11648
|
async stop() {
|
|
11524
11649
|
if (watcher) clearInterval(watcher);
|
|
11525
11650
|
node.stop();
|
|
11526
|
-
const wait = (
|
|
11651
|
+
const wait = (ms2) => new Promise((r) => setTimeout(r, ms2).unref?.());
|
|
11527
11652
|
await Promise.race([Promise.allSettled([sidecar?.stop(), app.stop()]), wait(6e3)]);
|
|
11528
11653
|
}
|
|
11529
11654
|
};
|
|
@@ -12700,7 +12825,7 @@ import { connect } from "node:net";
|
|
|
12700
12825
|
import { join as join19 } from "node:path";
|
|
12701
12826
|
var USAGE3 = "Usage: friend daemon start | stop | restart | status";
|
|
12702
12827
|
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
12703
|
-
var sleep = (
|
|
12828
|
+
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
12704
12829
|
var realDeps = {
|
|
12705
12830
|
launch(logFile) {
|
|
12706
12831
|
const fd = openSync2(logFile, "a");
|
|
@@ -12769,8 +12894,8 @@ function running(dataDir, deps) {
|
|
|
12769
12894
|
if (r) rmSync7(pidFile(dataDir), { force: true });
|
|
12770
12895
|
return null;
|
|
12771
12896
|
}
|
|
12772
|
-
function since(
|
|
12773
|
-
const s = Math.max(0, Math.round(
|
|
12897
|
+
function since(ms2) {
|
|
12898
|
+
const s = Math.max(0, Math.round(ms2 / 1e3));
|
|
12774
12899
|
if (s < 90) return `${s} seconds`;
|
|
12775
12900
|
if (s < 5400) return `${Math.round(s / 60)} minutes`;
|
|
12776
12901
|
if (s < 129600) return `${Math.round(s / 3600)} hours`;
|
|
@@ -12866,7 +12991,7 @@ async function daemonCommand(args, io2, deps = realDeps) {
|
|
|
12866
12991
|
import crossSpawn5 from "cross-spawn";
|
|
12867
12992
|
var PACKAGE = "friend-assistant";
|
|
12868
12993
|
var real = {
|
|
12869
|
-
current: () => true ? "0.1.
|
|
12994
|
+
current: () => true ? "0.1.26" : "development",
|
|
12870
12995
|
latest: () => new Promise((resolve11) => {
|
|
12871
12996
|
const child = crossSpawn5("npm", ["view", PACKAGE, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
12872
12997
|
let out = "";
|
|
@@ -13154,7 +13279,7 @@ async function run(argv, io2) {
|
|
|
13154
13279
|
case "version":
|
|
13155
13280
|
case "--version":
|
|
13156
13281
|
case "-v":
|
|
13157
|
-
io2.out(true ? "0.1.
|
|
13282
|
+
io2.out(true ? "0.1.26" : "development");
|
|
13158
13283
|
return 0;
|
|
13159
13284
|
case "lan":
|
|
13160
13285
|
return await lanCommand(rest, io2);
|