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/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/types.ts
5
- var BrokerError = class extends Error {
6
- constructor(message, kind) {
7
- super(message);
8
- this.kind = kind;
9
- this.name = "BrokerError";
10
- }
11
- kind;
12
- };
13
-
14
- // ../server/src/brokers/portfolio.ts
15
- var MOVERS = 3;
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 ? "-" : ""}\u20B9${grouped}.${cents}`;
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 WHY2 = { 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" };
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}: ${WHY2[section2.kind] ?? section2.error.slice(0, 100)}.`;
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" || input.baseTier === "T0" || input.baseTier === "T1" || NOT_BY_VOICE.some((p) => input.tool.startsWith(p))) return "deny";
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
- allowedUsers: users.slice(0, 10)
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 = (ms, what) => new Promise((_, rej) => {
3740
- const t = setTimeout(() => rej(new Error(what)), ms);
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 ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
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 >= MAX_ALLOWED) return false;
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 ?? 6e5)) {
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 ?? 6500);
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), this.d.waitMs ?? 12e4)
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 2 minutes.`;
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({ ask: (t, l, k) => this.ask(t, l, k), fast: d.fast, clock: d.clock });
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) => `${formatInr(Math.abs(n)).replace("\u20B9", "").replace(/\.00$/, "")} ${lang === "hi" ? "\u0930\u0941\u092A\u092F\u0947" : "rupees"}`;
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 MAX_TRIES_PER_DAY = 3;
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) > MAX_TRIES_PER_DAY) continue;
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
- constructor(db) {
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
- if (this.list().length >= MAX_SCHEDULES) throw new Error(`There can be at most ${MAX_SCHEDULES} schedules.`);
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 ms = e instanceof LockoutError ? e.retryAfterMs : 6e4;
6773
- return reply.code(429).header("retry-after", Math.ceil(ms / 1e3)).send({ error: "Too many attempts" });
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
- const alexaWindow = new SlidingWindow(60, 6e4);
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 (!PACKAGE_FILES.includes(name) || !endpoint || !config.alexa.invocationName) return null;
10368
+ if (!packageFileNames(config.alexa.locales).includes(name) || !endpoint || !config.alexa.invocationName) return null;
10245
10369
  try {
10246
- return { type: "application/json", text: buildSkillPackage({ invocationName: config.alexa.invocationName, endpoint })[name] };
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 = (ms) => new Promise((r) => setTimeout(r, ms).unref?.());
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 = (ms) => new Promise((r) => setTimeout(r, ms));
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(ms) {
12773
- const s = Math.max(0, Math.round(ms / 1e3));
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.25" : "development",
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.25" : "development");
13282
+ io2.out(true ? "0.1.26" : "development");
13158
13283
  return 0;
13159
13284
  case "lan":
13160
13285
  return await lanCommand(rest, io2);