friend-assistant 0.1.24 → 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 +3 -2
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +421 -268
- package/dist/web/assets/index-BCgrBB_0.js +4456 -0
- package/dist/web/assets/index-EUY3fIc8.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CDr97J0R.css +0 -1
- package/dist/web/assets/index-ZTFnakMv.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;
|
|
@@ -1560,7 +1442,7 @@ var BANNED = /* @__PURE__ */ new Set(["alexa", "amazon", "echo", "skill", "app",
|
|
|
1560
1442
|
var MAX_LENGTH = 50;
|
|
1561
1443
|
function invocationNameProblem(name) {
|
|
1562
1444
|
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
1563
|
-
if (words.length <
|
|
1445
|
+
if (words.length < 1) return 'Enter the name you will say after "Alexa, ask".';
|
|
1564
1446
|
if (name.length > MAX_LENGTH) return `That is too long (at most ${MAX_LENGTH} characters).`;
|
|
1565
1447
|
if (!/^[a-z' ]+$/.test(name)) return "Use lower-case letters, spaces and apostrophes only. Spell numbers out as words.";
|
|
1566
1448
|
const bad = words.find((w) => BANNED.has(w));
|
|
@@ -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() }));
|
|
@@ -5079,15 +4989,43 @@ var AlexaDevices = class {
|
|
|
5079
4989
|
|
|
5080
4990
|
// ../server/src/alexa/phrases.ts
|
|
5081
4991
|
var langOf = (locale2) => locale2?.toLowerCase().startsWith("hi") ? "hi" : "en";
|
|
5082
|
-
var
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
4992
|
+
var LEADS = [
|
|
4993
|
+
["HowIntent", "how", "\u0915\u0948\u0938\u0947"],
|
|
4994
|
+
["WhatIntent", "what", "\u0915\u094D\u092F\u093E"],
|
|
4995
|
+
["TellMeIntent", "tell me", "\u092E\u0941\u091D\u0947 \u092C\u0924\u093E\u0913"],
|
|
4996
|
+
["ShowMeIntent", "show me", "\u092E\u0941\u091D\u0947 \u0926\u093F\u0916\u093E\u0913"],
|
|
4997
|
+
["RemindMeIntent", "remind me", "\u092E\u0941\u091D\u0947 \u092F\u093E\u0926 \u0926\u093F\u0932\u093E\u0913"],
|
|
4998
|
+
["PleaseIntent", "please", "\u0915\u0943\u092A\u092F\u093E"],
|
|
4999
|
+
["CanYouIntent", "can you", "\u0915\u094D\u092F\u093E \u0924\u0941\u092E"],
|
|
5000
|
+
["CouldYouIntent", "could you", "\u0915\u094D\u092F\u093E \u0906\u092A"],
|
|
5001
|
+
["IsThereIntent", "is there", "\u0915\u094D\u092F\u093E \u0915\u094B\u0908"],
|
|
5002
|
+
["GiveMeIntent", "give me", "\u092E\u0941\u091D\u0947 \u0926\u094B"],
|
|
5003
|
+
["FindIntent", "find", "\u0916\u094B\u091C\u094B"],
|
|
5004
|
+
["SearchIntent", "search for", "\u0922\u0942\u0901\u0922\u094B"],
|
|
5005
|
+
["CheckIntent", "check", "\u091C\u093E\u0901\u091A\u094B"],
|
|
5006
|
+
["AddIntent", "add", "\u091C\u094B\u0921\u093C\u094B"],
|
|
5007
|
+
["SetIntent", "set", "\u0938\u0947\u091F \u0915\u0930\u094B"],
|
|
5008
|
+
["CreateIntent", "create", "\u092C\u0928\u093E\u0913"],
|
|
5009
|
+
["WriteIntent", "write", "\u0932\u093F\u0916\u094B"],
|
|
5010
|
+
["ReadIntent", "read", "\u092A\u0922\u093C\u094B"],
|
|
5011
|
+
["SendIntent", "send", "\u092D\u0947\u091C\u094B"],
|
|
5012
|
+
["WhenIntent", "when", "\u0915\u092C"],
|
|
5013
|
+
["WhereIntent", "where", "\u0915\u0939\u093E\u0901"],
|
|
5014
|
+
["WhoIntent", "who", "\u0915\u094C\u0928"],
|
|
5015
|
+
["WhyIntent", "why", "\u0915\u094D\u092F\u094B\u0902"],
|
|
5016
|
+
["WhichIntent", "which", "\u0915\u094C\u0928 \u0938\u093E"],
|
|
5017
|
+
["ExplainIntent", "explain", "\u0938\u092E\u091D\u093E\u0913"],
|
|
5018
|
+
["SummarizeIntent", "summarize", "\u0938\u093E\u0930\u093E\u0902\u0936 \u0926\u094B"],
|
|
5019
|
+
["CalculateIntent", "calculate", "\u0917\u0923\u0928\u093E \u0915\u0930\u094B"],
|
|
5020
|
+
["ConvertIntent", "convert", "\u092C\u0926\u0932\u094B"],
|
|
5021
|
+
["TranslateIntent", "translate", "\u0905\u0928\u0941\u0935\u093E\u0926 \u0915\u0930\u094B"],
|
|
5022
|
+
["ListIntent", "list", "\u0938\u0942\u091A\u0940 \u0926\u093F\u0916\u093E\u0913"],
|
|
5023
|
+
["DoIntent", "do", "\u0915\u094D\u092F\u093E \u092E\u0948\u0902"],
|
|
5024
|
+
["IsIntent", "is", "\u0915\u094D\u092F\u093E \u092F\u0939"],
|
|
5025
|
+
["AreIntent", "are", "\u0915\u094D\u092F\u093E \u092F\u0947"]
|
|
5026
|
+
];
|
|
5027
|
+
var LEAD_INTENTS = LEADS.map((l) => l[0]);
|
|
5028
|
+
var LEAD_INS = Object.fromEntries(LEADS.map(([name, en, hi]) => [name, { en, hi }]));
|
|
5091
5029
|
var SAY = {
|
|
5092
5030
|
notAllowed: { en: "I do not know this Echo yet. Allow it in Friend, under Plugins, Alexa.", hi: "\u092E\u0948\u0902 \u0907\u0938 \u0907\u0915\u094B \u0915\u094B \u0905\u092D\u0940 \u0928\u0939\u0940\u0902 \u092A\u0939\u091A\u093E\u0928\u0924\u093E\u0964 \u092B\u094D\u0930\u0947\u0902\u0921 \u092E\u0947\u0902 \u092A\u094D\u0932\u0917\u0907\u0928\u094D\u0938, \u0905\u0932\u0947\u0915\u094D\u0938\u093E \u092E\u0947\u0902 \u0907\u0938\u0947 \u0905\u0928\u0941\u092E\u0924\u093F \u0926\u0940\u091C\u093F\u090F\u0964" },
|
|
5093
5031
|
greet: { en: "Friend here. Ask me about your portfolio, the weather, your tasks, or anything else.", hi: "\u092B\u094D\u0930\u0947\u0902\u0921 \u092F\u0939\u093E\u0901 \u0939\u0948\u0964 \u092E\u0941\u091D\u0938\u0947 \u0905\u092A\u0928\u0947 \u092A\u094B\u0930\u094D\u091F\u092B\u094B\u0932\u093F\u092F\u094B, \u092E\u094C\u0938\u092E, \u0915\u093E\u092E\u094B\u0902 \u092F\u093E \u0915\u0941\u091B \u092D\u0940 \u092A\u0942\u091B\u093F\u090F\u0964" },
|
|
@@ -5131,6 +5069,7 @@ function shortForVoice(text2) {
|
|
|
5131
5069
|
|
|
5132
5070
|
// ../server/src/alexa/engine.ts
|
|
5133
5071
|
var FAST = ["PortfolioIntent", "WeatherIntent", "TasksIntent", "StatusIntent"];
|
|
5072
|
+
var ms = (v, def) => typeof v === "function" ? v() : v ?? def;
|
|
5134
5073
|
var AlexaEngine = class {
|
|
5135
5074
|
constructor(d) {
|
|
5136
5075
|
this.d = d;
|
|
@@ -5178,7 +5117,7 @@ var AlexaEngine = class {
|
|
|
5178
5117
|
ready(key, lang) {
|
|
5179
5118
|
const slot = this.slots.get(key);
|
|
5180
5119
|
if (slot?.pending) return { speech: say(lang, "working"), end: false };
|
|
5181
|
-
if (!slot || this.d.clock() - slot.at > (this.d.holdMs
|
|
5120
|
+
if (!slot || this.d.clock() - slot.at > ms(this.d.holdMs, 6e5)) {
|
|
5182
5121
|
this.slots.delete(key);
|
|
5183
5122
|
return { speech: say(lang, "nothing"), end: true };
|
|
5184
5123
|
}
|
|
@@ -5203,7 +5142,7 @@ var AlexaEngine = class {
|
|
|
5203
5142
|
const waited = await Promise.race([
|
|
5204
5143
|
work,
|
|
5205
5144
|
new Promise((resolve11) => {
|
|
5206
|
-
timer = setTimeout(() => resolve11("late"), this.d.budgetMs
|
|
5145
|
+
timer = setTimeout(() => resolve11("late"), ms(this.d.budgetMs, 6500));
|
|
5207
5146
|
})
|
|
5208
5147
|
]);
|
|
5209
5148
|
clearTimeout(timer);
|
|
@@ -5222,6 +5161,70 @@ var AlexaEngine = class {
|
|
|
5222
5161
|
}
|
|
5223
5162
|
};
|
|
5224
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
|
+
|
|
5225
5228
|
// ../server/src/alexa/permission.ts
|
|
5226
5229
|
var YES = /* @__PURE__ */ new Set(["yes", "y", "yeah", "ok", "okay", "ha", "haan", "han", "\u0939\u093E\u0901", "\u0939\u093E\u0902", "\u0939\u093E"]);
|
|
5227
5230
|
var NO = /* @__PURE__ */ new Set(["no", "n", "nope", "nahi", "nahin", "\u0928\u0939\u0940\u0902", "\u0928\u093E"]);
|
|
@@ -5242,17 +5245,19 @@ var VoicePermission = class {
|
|
|
5242
5245
|
const channels = this.d.linked();
|
|
5243
5246
|
const screen = this.d.screenOpen();
|
|
5244
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));
|
|
5245
5250
|
const entry = {
|
|
5246
5251
|
id: card.id,
|
|
5247
5252
|
answer,
|
|
5248
|
-
timer: setTimeout(() => this.settle(card.id, false),
|
|
5253
|
+
timer: setTimeout(() => this.settle(card.id, false), waitMs)
|
|
5249
5254
|
};
|
|
5250
5255
|
this.waiting.push(entry);
|
|
5251
5256
|
if (screen) base(card);
|
|
5252
5257
|
if (channels.length) {
|
|
5253
5258
|
const channel = channels.includes("telegram") ? "telegram" : channels[0];
|
|
5254
5259
|
const text2 = `Alexa asked to: ${card.summary}
|
|
5255
|
-
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"}.`;
|
|
5256
5261
|
this.d.send(channel, "me", text2).then(
|
|
5257
5262
|
(r) => {
|
|
5258
5263
|
if (!r.ok && !screen) this.settle(card.id, false);
|
|
@@ -5409,6 +5414,7 @@ var AlexaHub = class {
|
|
|
5409
5414
|
this.d = d;
|
|
5410
5415
|
this.devices = new AlexaDevices(d.store, d.clock);
|
|
5411
5416
|
this.permission = new VoicePermission({
|
|
5417
|
+
waitMs: () => d.config.alexa.permissionSeconds * 1e3,
|
|
5412
5418
|
send: async (channel, to, text2) => {
|
|
5413
5419
|
const r = await d.channels.send(channel, to, text2);
|
|
5414
5420
|
return { ok: r.ok };
|
|
@@ -5416,7 +5422,13 @@ var AlexaHub = class {
|
|
|
5416
5422
|
linked: d.linked,
|
|
5417
5423
|
screenOpen: d.screenOpen
|
|
5418
5424
|
});
|
|
5419
|
-
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
|
+
});
|
|
5420
5432
|
}
|
|
5421
5433
|
d;
|
|
5422
5434
|
devices;
|
|
@@ -5428,7 +5440,7 @@ var AlexaHub = class {
|
|
|
5428
5440
|
}
|
|
5429
5441
|
status() {
|
|
5430
5442
|
const a = this.d.config.alexa;
|
|
5431
|
-
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() };
|
|
5432
5444
|
}
|
|
5433
5445
|
/** The owner's reply on Telegram or WhatsApp. True when it answered a permission request from Alexa. */
|
|
5434
5446
|
tryApprove(text2) {
|
|
@@ -5500,83 +5512,21 @@ var AlexaHub = class {
|
|
|
5500
5512
|
}
|
|
5501
5513
|
};
|
|
5502
5514
|
|
|
5503
|
-
// ../server/src/alexa/package.ts
|
|
5504
|
-
var PACKAGE_FILES = ["skill.json", "en-IN.json", "hi-IN.json"];
|
|
5505
|
-
var FIXED = {
|
|
5506
|
-
en: {
|
|
5507
|
-
PortfolioIntent: ["how is my portfolio", "how is my portfolio doing", "how are my stocks doing", "what is my portfolio", "my portfolio", "portfolio"],
|
|
5508
|
-
TasksIntent: ["what are my tasks", "what do i have to do", "what is on my list", "read my tasks", "my tasks"],
|
|
5509
|
-
StatusIntent: ["status", "give me the status", "what is the status", "are you there"],
|
|
5510
|
-
ReadyIntent: ["is it ready", "is it done", "are you done", "do you have an answer", "what is the answer"],
|
|
5511
|
-
weather: ["what is the weather in {city}", "how is the weather in {city}", "weather in {city}", "tell me the weather in {city}"]
|
|
5512
|
-
},
|
|
5513
|
-
hi: {
|
|
5514
|
-
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"],
|
|
5515
|
-
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"],
|
|
5516
|
-
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"],
|
|
5517
|
-
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"],
|
|
5518
|
-
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"]
|
|
5519
|
-
}
|
|
5520
|
-
};
|
|
5521
|
-
var BUILT_IN = ["AMAZON.CancelIntent", "AMAZON.HelpIntent", "AMAZON.StopIntent", "AMAZON.FallbackIntent", "AMAZON.NavigateHomeIntent"];
|
|
5522
|
-
function intents(lang) {
|
|
5523
|
-
const f = FIXED[lang];
|
|
5524
|
-
return [
|
|
5525
|
-
...BUILT_IN.map((name) => ({ name, samples: [] })),
|
|
5526
|
-
{ name: "PortfolioIntent", samples: f.PortfolioIntent },
|
|
5527
|
-
{ name: "WeatherIntent", slots: [{ name: "city", type: "AMAZON.SearchQuery" }], samples: f.weather },
|
|
5528
|
-
{ name: "TasksIntent", samples: f.TasksIntent },
|
|
5529
|
-
{ name: "StatusIntent", samples: f.StatusIntent },
|
|
5530
|
-
{ name: "ReadyIntent", samples: f.ReadyIntent },
|
|
5531
|
-
// 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
|
|
5532
|
-
...LEAD_INTENTS.map((name) => ({ name, slots: [{ name: "q", type: "AMAZON.SearchQuery" }], samples: [`${LEAD_INS[name][lang]} {q}`] }))
|
|
5533
|
-
];
|
|
5534
|
-
}
|
|
5535
|
-
var model = (invocationName, lang) => JSON.stringify({ interactionModel: { languageModel: { invocationName, intents: intents(lang), types: [] } } }, null, 2);
|
|
5536
|
-
var SKILL_NAME = "Friend Assistant";
|
|
5537
|
-
var locale = (lang, invocationName) => ({
|
|
5538
|
-
name: SKILL_NAME,
|
|
5539
|
-
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.",
|
|
5540
|
-
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.",
|
|
5541
|
-
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`],
|
|
5542
|
-
keywords: ["friend", "assistant"]
|
|
5543
|
-
});
|
|
5544
|
-
function buildSkillPackage(o) {
|
|
5545
|
-
const problem = invocationNameProblem(o.invocationName);
|
|
5546
|
-
if (problem) throw new Error(problem);
|
|
5547
|
-
if (!/^https:\/\/[^\s/]+\/\S*$/.test(o.endpoint)) throw new Error("The address must be an https address, such as https://your-address/api/alexa.");
|
|
5548
|
-
const manifest = {
|
|
5549
|
-
manifest: {
|
|
5550
|
-
manifestVersion: "1.0",
|
|
5551
|
-
publishingInformation: {
|
|
5552
|
-
locales: { "en-IN": locale("en", o.invocationName), "hi-IN": locale("hi", o.invocationName) },
|
|
5553
|
-
isAvailableWorldwide: false,
|
|
5554
|
-
distributionCountries: ["IN"],
|
|
5555
|
-
category: "ORGANIZERS_AND_ASSISTANTS",
|
|
5556
|
-
testingInstructions: "A private skill for its owner's own self-hosted assistant. It is used in development mode only."
|
|
5557
|
-
},
|
|
5558
|
-
apis: { custom: { endpoint: { uri: o.endpoint, sslCertificateType: "Trusted" } } },
|
|
5559
|
-
privacyAndCompliance: { allowsPurchases: false, usesPersonalInfo: false, isChildDirected: false, isExportCompliant: true, containsAds: false }
|
|
5560
|
-
}
|
|
5561
|
-
};
|
|
5562
|
-
return {
|
|
5563
|
-
"skill.json": JSON.stringify(manifest, null, 2),
|
|
5564
|
-
"en-IN.json": model(o.invocationName, "en"),
|
|
5565
|
-
"hi-IN.json": model(o.invocationName, "hi")
|
|
5566
|
-
};
|
|
5567
|
-
}
|
|
5568
|
-
|
|
5569
5515
|
// ../server/src/alexa/fast.ts
|
|
5570
|
-
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
|
+
};
|
|
5571
5521
|
var pct = (n) => String(Math.round(Math.abs(n) * 100) / 100);
|
|
5572
|
-
function portfolioSpeech(s, lang) {
|
|
5522
|
+
function portfolioSpeech(s, lang, o = {}) {
|
|
5573
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.";
|
|
5574
5524
|
const up = s.dayChange >= 0;
|
|
5575
5525
|
const gain = s.pnl >= 0;
|
|
5576
5526
|
if (lang === "hi") {
|
|
5577
|
-
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`;
|
|
5578
5528
|
}
|
|
5579
|
-
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)}.`;
|
|
5580
5530
|
}
|
|
5581
5531
|
var FIRST_TASKS = 3;
|
|
5582
5532
|
function tasksSpeech(rows3, lang) {
|
|
@@ -5592,8 +5542,157 @@ function statusSpeech(s, lang) {
|
|
|
5592
5542
|
return `Friend is running. You have ${s.openTasks} open ${s.openTasks === 1 ? "task" : "tasks"}.${s.brokerLoggedIn === false ? " Zerodha needs you to log in." : ""}`;
|
|
5593
5543
|
}
|
|
5594
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
|
+
|
|
5595
5694
|
// ../server/src/brokers/scheduler.ts
|
|
5596
|
-
var
|
|
5695
|
+
var DEFAULT_TRIES_PER_DAY = 3;
|
|
5597
5696
|
var WEEKDAYS = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
|
|
5598
5697
|
function localDay(now, zone) {
|
|
5599
5698
|
return new Intl.DateTimeFormat("en-CA", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit" }).format(now);
|
|
@@ -5619,7 +5718,7 @@ var ReportScheduler = class {
|
|
|
5619
5718
|
let sent = 0;
|
|
5620
5719
|
for (const s of this.d.schedules.list()) {
|
|
5621
5720
|
if (!s.enabled || s.lastRunDay === day || !s.days.includes(weekday) || time < s.time) continue;
|
|
5622
|
-
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;
|
|
5623
5722
|
try {
|
|
5624
5723
|
await this.d.run(s);
|
|
5625
5724
|
this.d.schedules.markRun(s.id, day);
|
|
@@ -5649,10 +5748,13 @@ var toSchedule = (r) => ({
|
|
|
5649
5748
|
lastRunDay: r.last_run_day
|
|
5650
5749
|
});
|
|
5651
5750
|
var ReportSchedules = class {
|
|
5652
|
-
|
|
5751
|
+
/** `max`: how many schedules there may be (the owner's setting). */
|
|
5752
|
+
constructor(db, max = () => MAX_SCHEDULES) {
|
|
5653
5753
|
this.db = db;
|
|
5754
|
+
this.max = max;
|
|
5654
5755
|
}
|
|
5655
5756
|
db;
|
|
5757
|
+
max;
|
|
5656
5758
|
list() {
|
|
5657
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);
|
|
5658
5760
|
}
|
|
@@ -5665,7 +5767,8 @@ var ReportSchedules = class {
|
|
|
5665
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).");
|
|
5666
5768
|
if (!CHANNELS.includes(s.channel)) throw new Error("The channel must be telegram, whatsapp or sms.");
|
|
5667
5769
|
if (typeof s.broker !== "string" || !/^[a-z0-9-]{1,40}$/.test(s.broker)) throw new Error("That is not a broker id.");
|
|
5668
|
-
|
|
5770
|
+
const limit = this.max();
|
|
5771
|
+
if (this.list().length >= limit) throw new Error(`There can be at most ${limit} schedules.`);
|
|
5669
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);
|
|
5670
5773
|
return this.list().find((x) => x.id === Number(info.lastInsertRowid));
|
|
5671
5774
|
}
|
|
@@ -5890,16 +5993,18 @@ function createZerodha(opts) {
|
|
|
5890
5993
|
}
|
|
5891
5994
|
|
|
5892
5995
|
// ../server/src/brokers/hub.ts
|
|
5996
|
+
var reportOptions = (b) => ({ money: { symbol: b.currencySymbol, locale: b.numberLocale }, concentrationPct: b.concentrationPct, bigDropPct: b.bigDropPct });
|
|
5893
5997
|
var NAMES = { zerodha: "Zerodha" };
|
|
5894
5998
|
var minutes = (hhmm) => Number(hhmm.slice(0, 2)) * 60 + Number(hhmm.slice(3, 5));
|
|
5895
5999
|
var BrokerHub = class {
|
|
5896
6000
|
constructor(d) {
|
|
5897
6001
|
this.d = d;
|
|
5898
|
-
this.schedules = new ReportSchedules(d.db);
|
|
6002
|
+
this.schedules = new ReportSchedules(d.db, () => d.config.brokers.maxSchedules);
|
|
5899
6003
|
this.sessions = new BrokerSessions(d.dataDir, this.now);
|
|
5900
6004
|
this.scheduler = new ReportScheduler({
|
|
5901
6005
|
schedules: this.schedules,
|
|
5902
6006
|
zone: () => d.config.brokers.timezone,
|
|
6007
|
+
maxTries: () => d.config.brokers.maxTriesPerDay,
|
|
5903
6008
|
skipDates: () => d.config.brokers.skipDates,
|
|
5904
6009
|
run: (s) => this.deliver(s)
|
|
5905
6010
|
});
|
|
@@ -5924,7 +6029,7 @@ ${z12.apiSecret}`;
|
|
|
5924
6029
|
if (!usable) this.providers.delete("zerodha");
|
|
5925
6030
|
else if (!have || have.made !== made) {
|
|
5926
6031
|
const provider = createZerodha({ apiKey: z12.apiKey, apiSecret: z12.apiSecret, fetch: this.d.fetch, clock: this.now });
|
|
5927
|
-
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 });
|
|
5928
6033
|
}
|
|
5929
6034
|
const now = this.providers.get("zerodha");
|
|
5930
6035
|
if (now) {
|
|
@@ -6007,7 +6112,7 @@ ${z12.apiSecret}`;
|
|
|
6007
6112
|
async report(id2, kind = "custom") {
|
|
6008
6113
|
const got = await this.snapshot(id2);
|
|
6009
6114
|
if (!got.ok) return got;
|
|
6010
|
-
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) });
|
|
6011
6116
|
this.d.audit.log("system", "broker.report", { broker: id2, kind, lines: text2.split("\n").length });
|
|
6012
6117
|
return { ok: true, text: text2 };
|
|
6013
6118
|
}
|
|
@@ -6741,8 +6846,8 @@ function createGateway(deps) {
|
|
|
6741
6846
|
try {
|
|
6742
6847
|
pairLimiter.check(key);
|
|
6743
6848
|
} catch (e) {
|
|
6744
|
-
const
|
|
6745
|
-
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" });
|
|
6746
6851
|
}
|
|
6747
6852
|
try {
|
|
6748
6853
|
const token = auth.redeemPairCode(body.data.code, body.data.name);
|
|
@@ -6961,11 +7066,16 @@ function createGateway(deps) {
|
|
|
6961
7066
|
return brokerError(reply, e);
|
|
6962
7067
|
}
|
|
6963
7068
|
});
|
|
6964
|
-
|
|
7069
|
+
let alexaLimit = config.alexa.requestsPerMinute;
|
|
7070
|
+
let alexaWindow = new SlidingWindow(alexaLimit, 6e4);
|
|
6965
7071
|
void app.register(async (inst) => {
|
|
6966
7072
|
inst.removeContentTypeParser("application/json");
|
|
6967
7073
|
inst.addContentTypeParser("application/json", { parseAs: "buffer", bodyLimit: 32 * 1024 }, (_req, body, done) => done(null, body));
|
|
6968
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
|
+
}
|
|
6969
7079
|
if (!alexaWindow.allow(`alexa:${req.ip}`)) return reply.code(429).header("retry-after", 30).send();
|
|
6970
7080
|
});
|
|
6971
7081
|
inst.post("/api/alexa", async (req, reply) => {
|
|
@@ -8255,13 +8365,29 @@ var SettingsPatch = z11.object({
|
|
|
8255
8365
|
apiSecret: z11.string().trim().max(100)
|
|
8256
8366
|
}).partial().strict(),
|
|
8257
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),
|
|
8258
8376
|
skipDates: z11.array(z11.string().regex(/^\d{4}-\d{2}-\d{2}$/, "A date like 2026-10-02")).max(100),
|
|
8259
8377
|
loginReminderMinutes: z11.number().int().min(0).max(120)
|
|
8260
8378
|
}).partial().strict(),
|
|
8261
8379
|
/** The owner's Alexa skill. The skill id is not a secret; both need the PIN to change. */
|
|
8262
8380
|
alexa: z11.object({
|
|
8263
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"),
|
|
8264
|
-
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)
|
|
8265
8391
|
}).partial().strict(),
|
|
8266
8392
|
/** What the assistant may do with money. Only "read" exists in this version; any other level is refused. */
|
|
8267
8393
|
trading: z11.object({ level: z11.enum(["read"]) }).partial().strict(),
|
|
@@ -8323,7 +8449,15 @@ function settingsView(c) {
|
|
|
8323
8449
|
zerodha: { installed: c.brokers.zerodha.installed, enabled: c.brokers.zerodha.enabled, apiKey: c.brokers.zerodha.apiKey, secretSet: !!c.brokers.zerodha.apiSecret },
|
|
8324
8450
|
timezone: c.brokers.timezone,
|
|
8325
8451
|
skipDates: [...c.brokers.skipDates],
|
|
8326
|
-
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
|
|
8327
8461
|
},
|
|
8328
8462
|
trading: { level: c.trading.level },
|
|
8329
8463
|
alexa: {
|
|
@@ -8331,7 +8465,15 @@ function settingsView(c) {
|
|
|
8331
8465
|
enabled: c.alexa.enabled,
|
|
8332
8466
|
skillId: c.alexa.skillId,
|
|
8333
8467
|
invocationName: c.alexa.invocationName,
|
|
8334
|
-
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]
|
|
8335
8477
|
},
|
|
8336
8478
|
look: { ...c.look },
|
|
8337
8479
|
ollama: { url: c.ollama.url, model: c.ollama.model, vision: !!c.ollama.vision, contextTokens: c.ollama.contextTokens },
|
|
@@ -8373,6 +8515,10 @@ function applySettings(config, raw) {
|
|
|
8373
8515
|
}
|
|
8374
8516
|
const p = parsed.data;
|
|
8375
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
|
+
}
|
|
8376
8522
|
if (p.alexa?.invocationName) {
|
|
8377
8523
|
const problem = invocationNameProblem(p.alexa.invocationName);
|
|
8378
8524
|
if (problem) throw new SettingsError(`alexa.invocationName: ${problem}`);
|
|
@@ -8429,11 +8575,17 @@ function applySettings(config, raw) {
|
|
|
8429
8575
|
if (rest.timezone !== void 0) config.brokers.timezone = rest.timezone;
|
|
8430
8576
|
if (rest.skipDates !== void 0) config.brokers.skipDates = sanitizeBrokers({ skipDates: rest.skipDates }).skipDates;
|
|
8431
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
|
+
}
|
|
8432
8581
|
}
|
|
8433
8582
|
if (p.alexa) {
|
|
8434
8583
|
config.alexa.installed = true;
|
|
8435
8584
|
if (p.alexa.skillId !== void 0) config.alexa.skillId = p.alexa.skillId;
|
|
8436
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
|
+
}
|
|
8437
8589
|
}
|
|
8438
8590
|
if (p.approvals) config.approvals = p.approvals;
|
|
8439
8591
|
if (p.channels) {
|
|
@@ -9726,7 +9878,7 @@ async function createApp(opts) {
|
|
|
9726
9878
|
const got = await brokerHub.snapshot("zerodha");
|
|
9727
9879
|
if (!got.ok) return got.sessionExpired ? "Zerodha needs you to log in first." : got.error;
|
|
9728
9880
|
if (!got.snapshot.holdings.ok) return "I could not read your holdings just now.";
|
|
9729
|
-
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 });
|
|
9730
9882
|
},
|
|
9731
9883
|
WeatherIntent: async (_locale, slots) => {
|
|
9732
9884
|
if (!slots.city) return "Tell me which city, for example: what is the weather in Pune.";
|
|
@@ -10213,9 +10365,10 @@ async function createApp(opts) {
|
|
|
10213
10365
|
},
|
|
10214
10366
|
packageFile: (name) => {
|
|
10215
10367
|
const endpoint = alexaHub.endpoint();
|
|
10216
|
-
if (!
|
|
10368
|
+
if (!packageFileNames(config.alexa.locales).includes(name) || !endpoint || !config.alexa.invocationName) return null;
|
|
10217
10369
|
try {
|
|
10218
|
-
|
|
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;
|
|
10219
10372
|
} catch {
|
|
10220
10373
|
return null;
|
|
10221
10374
|
}
|
|
@@ -11495,7 +11648,7 @@ async function startFriend(opts) {
|
|
|
11495
11648
|
async stop() {
|
|
11496
11649
|
if (watcher) clearInterval(watcher);
|
|
11497
11650
|
node.stop();
|
|
11498
|
-
const wait = (
|
|
11651
|
+
const wait = (ms2) => new Promise((r) => setTimeout(r, ms2).unref?.());
|
|
11499
11652
|
await Promise.race([Promise.allSettled([sidecar?.stop(), app.stop()]), wait(6e3)]);
|
|
11500
11653
|
}
|
|
11501
11654
|
};
|
|
@@ -12672,7 +12825,7 @@ import { connect } from "node:net";
|
|
|
12672
12825
|
import { join as join19 } from "node:path";
|
|
12673
12826
|
var USAGE3 = "Usage: friend daemon start | stop | restart | status";
|
|
12674
12827
|
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
12675
|
-
var sleep = (
|
|
12828
|
+
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
12676
12829
|
var realDeps = {
|
|
12677
12830
|
launch(logFile) {
|
|
12678
12831
|
const fd = openSync2(logFile, "a");
|
|
@@ -12741,8 +12894,8 @@ function running(dataDir, deps) {
|
|
|
12741
12894
|
if (r) rmSync7(pidFile(dataDir), { force: true });
|
|
12742
12895
|
return null;
|
|
12743
12896
|
}
|
|
12744
|
-
function since(
|
|
12745
|
-
const s = Math.max(0, Math.round(
|
|
12897
|
+
function since(ms2) {
|
|
12898
|
+
const s = Math.max(0, Math.round(ms2 / 1e3));
|
|
12746
12899
|
if (s < 90) return `${s} seconds`;
|
|
12747
12900
|
if (s < 5400) return `${Math.round(s / 60)} minutes`;
|
|
12748
12901
|
if (s < 129600) return `${Math.round(s / 3600)} hours`;
|
|
@@ -12838,7 +12991,7 @@ async function daemonCommand(args, io2, deps = realDeps) {
|
|
|
12838
12991
|
import crossSpawn5 from "cross-spawn";
|
|
12839
12992
|
var PACKAGE = "friend-assistant";
|
|
12840
12993
|
var real = {
|
|
12841
|
-
current: () => true ? "0.1.
|
|
12994
|
+
current: () => true ? "0.1.26" : "development",
|
|
12842
12995
|
latest: () => new Promise((resolve11) => {
|
|
12843
12996
|
const child = crossSpawn5("npm", ["view", PACKAGE, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
12844
12997
|
let out = "";
|
|
@@ -13126,7 +13279,7 @@ async function run(argv, io2) {
|
|
|
13126
13279
|
case "version":
|
|
13127
13280
|
case "--version":
|
|
13128
13281
|
case "-v":
|
|
13129
|
-
io2.out(true ? "0.1.
|
|
13282
|
+
io2.out(true ? "0.1.26" : "development");
|
|
13130
13283
|
return 0;
|
|
13131
13284
|
case "lan":
|
|
13132
13285
|
return await lanCommand(rest, io2);
|