@moneysiren/cli 0.1.0-alpha.0

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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/apps/cli/src/cli.d.ts +56 -0
  4. package/dist/apps/cli/src/cli.js +182 -0
  5. package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
  6. package/dist/apps/cli/src/commands/dashboard.js +239 -0
  7. package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
  8. package/dist/apps/cli/src/commands/doctor.js +25 -0
  9. package/dist/apps/cli/src/commands/init.d.ts +3 -0
  10. package/dist/apps/cli/src/commands/init.js +18 -0
  11. package/dist/apps/cli/src/commands/install.d.ts +3 -0
  12. package/dist/apps/cli/src/commands/install.js +116 -0
  13. package/dist/apps/cli/src/commands/modes.d.ts +3 -0
  14. package/dist/apps/cli/src/commands/modes.js +65 -0
  15. package/dist/apps/cli/src/commands/notify.d.ts +3 -0
  16. package/dist/apps/cli/src/commands/notify.js +430 -0
  17. package/dist/apps/cli/src/commands/report.d.ts +3 -0
  18. package/dist/apps/cli/src/commands/report.js +206 -0
  19. package/dist/apps/cli/src/commands/runtime.d.ts +5 -0
  20. package/dist/apps/cli/src/commands/runtime.js +133 -0
  21. package/dist/apps/cli/src/commands/shared.d.ts +9 -0
  22. package/dist/apps/cli/src/commands/shared.js +29 -0
  23. package/dist/apps/cli/src/commands/summary.d.ts +3 -0
  24. package/dist/apps/cli/src/commands/summary.js +15 -0
  25. package/dist/apps/cli/src/commands/sync.d.ts +3 -0
  26. package/dist/apps/cli/src/commands/sync.js +393 -0
  27. package/dist/apps/cli/src/commands/theme.d.ts +3 -0
  28. package/dist/apps/cli/src/commands/theme.js +181 -0
  29. package/dist/apps/cli/src/home.d.ts +7 -0
  30. package/dist/apps/cli/src/home.js +97 -0
  31. package/dist/apps/cli/src/index.d.ts +3 -0
  32. package/dist/apps/cli/src/index.js +14 -0
  33. package/dist/apps/cli/src/install-profile.d.ts +35 -0
  34. package/dist/apps/cli/src/install-profile.js +124 -0
  35. package/dist/apps/cli/src/install-selector.d.ts +10 -0
  36. package/dist/apps/cli/src/install-selector.js +66 -0
  37. package/dist/apps/cli/src/interactive.d.ts +3 -0
  38. package/dist/apps/cli/src/interactive.js +32 -0
  39. package/dist/apps/cli/src/postinstall.d.ts +3 -0
  40. package/dist/apps/cli/src/postinstall.js +42 -0
  41. package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
  42. package/dist/apps/cli/src/runtime-adapter.js +185 -0
  43. package/dist/apps/cli/src/slash.d.ts +15 -0
  44. package/dist/apps/cli/src/slash.js +202 -0
  45. package/dist/apps/cli/src/summary-model.d.ts +51 -0
  46. package/dist/apps/cli/src/summary-model.js +136 -0
  47. package/dist/apps/cli/src/theme.d.ts +18 -0
  48. package/dist/apps/cli/src/theme.js +118 -0
  49. package/dist/packages/config/src/index.d.ts +3 -0
  50. package/dist/packages/config/src/index.js +3 -0
  51. package/dist/packages/config/src/load.d.ts +3 -0
  52. package/dist/packages/config/src/load.js +77 -0
  53. package/dist/packages/config/src/schema.d.ts +46 -0
  54. package/dist/packages/config/src/schema.js +25 -0
  55. package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
  56. package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
  57. package/dist/packages/connectors/aws/src/index.d.ts +35 -0
  58. package/dist/packages/connectors/aws/src/index.js +67 -0
  59. package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
  60. package/dist/packages/connectors/aws/src/normalize.js +141 -0
  61. package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
  62. package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
  63. package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
  64. package/dist/packages/connectors/cloudflare/src/client.js +107 -0
  65. package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
  66. package/dist/packages/connectors/cloudflare/src/index.js +81 -0
  67. package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
  68. package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
  69. package/dist/packages/connectors/mock/src/index.d.ts +58 -0
  70. package/dist/packages/connectors/mock/src/index.js +66 -0
  71. package/dist/packages/connectors/openai/src/index.d.ts +55 -0
  72. package/dist/packages/connectors/openai/src/index.js +169 -0
  73. package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
  74. package/dist/packages/connectors/openai/src/normalize.js +180 -0
  75. package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
  76. package/dist/packages/connectors/supabase/src/client.js +132 -0
  77. package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
  78. package/dist/packages/connectors/supabase/src/index.js +87 -0
  79. package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
  80. package/dist/packages/connectors/supabase/src/normalize.js +266 -0
  81. package/dist/packages/core/src/collector.d.ts +12 -0
  82. package/dist/packages/core/src/collector.js +68 -0
  83. package/dist/packages/core/src/index.d.ts +5 -0
  84. package/dist/packages/core/src/index.js +4 -0
  85. package/dist/packages/core/src/provider.d.ts +18 -0
  86. package/dist/packages/core/src/provider.js +2 -0
  87. package/dist/packages/core/src/risk-engine.d.ts +9 -0
  88. package/dist/packages/core/src/risk-engine.js +4 -0
  89. package/dist/packages/core/src/snapshots.d.ts +49 -0
  90. package/dist/packages/core/src/snapshots.js +9 -0
  91. package/dist/packages/db/src/client.d.ts +11 -0
  92. package/dist/packages/db/src/client.js +14 -0
  93. package/dist/packages/db/src/index.d.ts +6 -0
  94. package/dist/packages/db/src/index.js +6 -0
  95. package/dist/packages/db/src/local-store.d.ts +161 -0
  96. package/dist/packages/db/src/local-store.js +623 -0
  97. package/dist/packages/db/src/migrate.d.ts +17 -0
  98. package/dist/packages/db/src/migrate.js +35 -0
  99. package/dist/packages/db/src/schema.d.ts +5 -0
  100. package/dist/packages/db/src/schema.js +120 -0
  101. package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
  102. package/dist/packages/db/src/sqlite-bin.js +16 -0
  103. package/dist/packages/local-api/src/index.d.ts +2 -0
  104. package/dist/packages/local-api/src/index.js +2 -0
  105. package/dist/packages/local-api/src/server.d.ts +36 -0
  106. package/dist/packages/local-api/src/server.js +310 -0
  107. package/dist/packages/report/src/daily.d.ts +24 -0
  108. package/dist/packages/report/src/daily.js +9 -0
  109. package/dist/packages/report/src/index.d.ts +4 -0
  110. package/dist/packages/report/src/index.js +4 -0
  111. package/dist/packages/report/src/korean.d.ts +3 -0
  112. package/dist/packages/report/src/korean.js +62 -0
  113. package/dist/packages/report/src/slack.d.ts +34 -0
  114. package/dist/packages/report/src/slack.js +134 -0
  115. package/dist/packages/runtime/src/index.d.ts +2 -0
  116. package/dist/packages/runtime/src/index.js +2 -0
  117. package/dist/packages/runtime/src/runtime.d.ts +26 -0
  118. package/dist/packages/runtime/src/runtime.js +182 -0
  119. package/dist/packages/view-model/src/index.d.ts +3 -0
  120. package/dist/packages/view-model/src/index.js +3 -0
  121. package/dist/packages/view-model/src/notification-preferences-model.d.ts +47 -0
  122. package/dist/packages/view-model/src/notification-preferences-model.js +218 -0
  123. package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
  124. package/dist/packages/view-model/src/notification-preferences.js +36 -0
  125. package/dist/packages/view-model/src/view-model.d.ts +193 -0
  126. package/dist/packages/view-model/src/view-model.js +684 -0
  127. package/package.json +49 -0
  128. package/scripts/postinstall.mjs +11 -0
@@ -0,0 +1,134 @@
1
+ export class SlackReportDeliveryError extends Error {
2
+ statusCode;
3
+ constructor(message, options = {}) {
4
+ super(message);
5
+ this.name = "SlackReportDeliveryError";
6
+ if (options.statusCode !== undefined) {
7
+ this.statusCode = options.statusCode;
8
+ }
9
+ }
10
+ }
11
+ const DEFAULT_SLACK_REPORT_TIMEOUT_MS = 5_000;
12
+ const MAX_SLACK_ERROR_MESSAGE_LENGTH = 500;
13
+ const MAX_SLACK_RESPONSE_BODY_LENGTH = 300;
14
+ const TRUNCATED_SUFFIX = "... [truncated]";
15
+ export function buildSlackReportPayload(text) {
16
+ const trimmedText = text.trim();
17
+ if (trimmedText.length === 0) {
18
+ throw new SlackReportDeliveryError("Slack report text must not be blank.");
19
+ }
20
+ return {
21
+ text: trimmedText,
22
+ mrkdwn: true,
23
+ };
24
+ }
25
+ export async function sendSlackReport(options) {
26
+ const webhookUrl = normalizeWebhookUrl(options.webhookUrl);
27
+ const payload = buildSlackReportPayload(options.text);
28
+ const transport = options.transport ?? fetchSlackReportTransport;
29
+ const timeoutMs = resolveSlackReportTimeoutMs(options.timeoutMs);
30
+ const controller = new AbortController();
31
+ let timeout;
32
+ let response;
33
+ try {
34
+ const transportPromise = transport({
35
+ webhookUrl,
36
+ payload,
37
+ signal: controller.signal,
38
+ });
39
+ transportPromise.catch(() => undefined);
40
+ const timeoutPromise = new Promise((_resolve, reject) => {
41
+ timeout = setTimeout(() => {
42
+ controller.abort();
43
+ reject(new SlackReportDeliveryError(`Slack report delivery timed out after ${timeoutMs}ms.`));
44
+ }, timeoutMs);
45
+ });
46
+ response = await Promise.race([transportPromise, timeoutPromise]);
47
+ }
48
+ catch (error) {
49
+ const message = error instanceof SlackReportDeliveryError
50
+ ? error.message
51
+ : `Slack report delivery failed: ${errorMessage(error)}`;
52
+ const errorOptions = error instanceof SlackReportDeliveryError && error.statusCode !== undefined
53
+ ? { statusCode: error.statusCode }
54
+ : {};
55
+ throw new SlackReportDeliveryError(sanitizeWebhookMessage(message, webhookUrl), errorOptions);
56
+ }
57
+ finally {
58
+ if (timeout !== undefined) {
59
+ clearTimeout(timeout);
60
+ }
61
+ }
62
+ if (!response.ok) {
63
+ const responseBody = formatResponseBodyDetail(response.body, webhookUrl);
64
+ const responseDetail = responseBody === undefined
65
+ ? ""
66
+ : ` Response: ${responseBody}`;
67
+ throw new SlackReportDeliveryError(sanitizeWebhookMessage(`Slack report delivery failed with HTTP ${response.status}.${responseDetail}`, webhookUrl), {
68
+ statusCode: response.status,
69
+ });
70
+ }
71
+ return {
72
+ status: "sent",
73
+ statusCode: response.status,
74
+ };
75
+ }
76
+ async function fetchSlackReportTransport(request) {
77
+ const response = await fetch(request.webhookUrl, {
78
+ method: "POST",
79
+ headers: {
80
+ "Content-Type": "application/json",
81
+ },
82
+ body: JSON.stringify(request.payload),
83
+ signal: request.signal,
84
+ });
85
+ const body = await response.text();
86
+ return {
87
+ ok: response.ok,
88
+ status: response.status,
89
+ ...(body.length === 0 ? {} : { body }),
90
+ };
91
+ }
92
+ function normalizeWebhookUrl(webhookUrl) {
93
+ const trimmed = webhookUrl.trim();
94
+ if (trimmed.length === 0) {
95
+ throw new SlackReportDeliveryError("SLACK_WEBHOOK_URL is required for Slack delivery.");
96
+ }
97
+ return trimmed;
98
+ }
99
+ function resolveSlackReportTimeoutMs(timeoutMs) {
100
+ if (timeoutMs === undefined) {
101
+ return DEFAULT_SLACK_REPORT_TIMEOUT_MS;
102
+ }
103
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
104
+ throw new SlackReportDeliveryError("Slack report timeout must be a positive safe integer.");
105
+ }
106
+ return timeoutMs;
107
+ }
108
+ function formatResponseBodyDetail(body, webhookUrl) {
109
+ const trimmedBody = body?.trim();
110
+ if (trimmedBody === undefined || trimmedBody.length === 0) {
111
+ return undefined;
112
+ }
113
+ return limitText(redactSensitiveText(trimmedBody, webhookUrl), MAX_SLACK_RESPONSE_BODY_LENGTH);
114
+ }
115
+ function sanitizeWebhookMessage(message, webhookUrl) {
116
+ return limitText(redactSensitiveText(message, webhookUrl), MAX_SLACK_ERROR_MESSAGE_LENGTH);
117
+ }
118
+ function redactSensitiveText(message, webhookUrl) {
119
+ return message
120
+ .replaceAll(webhookUrl, "[REDACTED:webhook_url]")
121
+ .replace(/https:\/\/hooks\.slack\.com\/services\/[^\s"')]+/gi, "[REDACTED:webhook_url]")
122
+ .replace(/\bsk-[A-Za-z0-9_-]+/g, "[REDACTED:token]")
123
+ .replace(/\bxox[baprs]-[A-Za-z0-9-]+/gi, "[REDACTED:token]");
124
+ }
125
+ function limitText(message, maxLength) {
126
+ if (message.length <= maxLength) {
127
+ return message;
128
+ }
129
+ return `${message.slice(0, maxLength - TRUNCATED_SUFFIX.length)}${TRUNCATED_SUFFIX}`;
130
+ }
131
+ function errorMessage(error) {
132
+ return error instanceof Error ? error.message : String(error);
133
+ }
134
+ //# sourceMappingURL=slack.js.map
@@ -0,0 +1,2 @@
1
+ export { assertLoopbackHost, assertRuntimeHealthy, findRuntime, isLoopbackHost, readRuntimeLock, removeRuntimeLock, resolveRuntimeLockPath, writeRuntimeLock, type LocalRuntime, type RuntimeHealthCheckOptions, type RuntimeLockOptions, } from "./runtime.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { assertLoopbackHost, assertRuntimeHealthy, findRuntime, isLoopbackHost, readRuntimeLock, removeRuntimeLock, resolveRuntimeLockPath, writeRuntimeLock, } from "./runtime.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ export interface LocalRuntime {
2
+ pid: number;
3
+ port: number;
4
+ baseUrl: string;
5
+ startedAt: string;
6
+ version: string;
7
+ }
8
+ export interface RuntimeLockOptions {
9
+ cwd?: string;
10
+ env?: Record<string, string | undefined>;
11
+ lockPath?: string;
12
+ platform?: NodeJS.Platform;
13
+ }
14
+ export interface RuntimeHealthCheckOptions {
15
+ fetchImpl?: typeof fetch;
16
+ timeoutMs?: number;
17
+ }
18
+ export declare function isLoopbackHost(host: string): boolean;
19
+ export declare function assertLoopbackHost(host: string): void;
20
+ export declare function resolveRuntimeLockPath(options?: RuntimeLockOptions): string;
21
+ export declare function readRuntimeLock(options?: RuntimeLockOptions): Promise<LocalRuntime | null>;
22
+ export declare function writeRuntimeLock(runtime: LocalRuntime, options?: RuntimeLockOptions): Promise<string>;
23
+ export declare function removeRuntimeLock(options?: RuntimeLockOptions): Promise<void>;
24
+ export declare function findRuntime(options?: RuntimeLockOptions): Promise<LocalRuntime | null>;
25
+ export declare function assertRuntimeHealthy(runtime: LocalRuntime, options?: RuntimeHealthCheckOptions): Promise<boolean>;
26
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1,182 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, posix, win32 } from "node:path";
4
+ const RUNTIME_LOCK_ENV_KEY = "MONEYSIREN_RUNTIME_LOCK_PATH";
5
+ const DEFAULT_HEALTH_TIMEOUT_MS = 2_000;
6
+ export function isLoopbackHost(host) {
7
+ const normalized = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
8
+ return (normalized === "localhost" ||
9
+ normalized === "::1" ||
10
+ normalized === "0:0:0:0:0:0:0:1" ||
11
+ normalized.startsWith("127."));
12
+ }
13
+ export function assertLoopbackHost(host) {
14
+ if (!isLoopbackHost(host)) {
15
+ throw new Error("MoneySiren local runtime must bind to a loopback host.");
16
+ }
17
+ }
18
+ export function resolveRuntimeLockPath(options = {}) {
19
+ if (options.lockPath !== undefined && options.lockPath.trim().length > 0) {
20
+ return resolveRuntimePath(options.lockPath, options.cwd);
21
+ }
22
+ const configuredPath = options.env?.[RUNTIME_LOCK_ENV_KEY]?.trim();
23
+ if (configuredPath !== undefined && configuredPath.length > 0) {
24
+ return resolveRuntimePath(configuredPath, options.cwd);
25
+ }
26
+ return defaultRuntimeLockPath(options.env ?? process.env, options.platform ?? process.platform);
27
+ }
28
+ export async function readRuntimeLock(options = {}) {
29
+ const lockPath = resolveRuntimeLockPath(options);
30
+ try {
31
+ const parsed = JSON.parse(await readFile(lockPath, "utf8"));
32
+ return parseRuntime(parsed);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ export async function writeRuntimeLock(runtime, options = {}) {
39
+ const parsedUrl = parseLoopbackUrl(runtime.baseUrl);
40
+ if (parsedUrl.port !== String(runtime.port)) {
41
+ throw new Error("Runtime lock baseUrl port must match the runtime port.");
42
+ }
43
+ const lockPath = resolveRuntimeLockPath(options);
44
+ await mkdir(dirname(lockPath), { recursive: true });
45
+ await writeFile(lockPath, `${JSON.stringify(runtime, null, 2)}\n`, "utf8");
46
+ return lockPath;
47
+ }
48
+ export async function removeRuntimeLock(options = {}) {
49
+ await rm(resolveRuntimeLockPath(options), {
50
+ force: true,
51
+ });
52
+ }
53
+ export async function findRuntime(options = {}) {
54
+ const runtime = await readRuntimeLock(options);
55
+ if (runtime === null) {
56
+ return null;
57
+ }
58
+ if (!isProcessAlive(runtime.pid)) {
59
+ await removeStaleRuntimeLock(options);
60
+ return null;
61
+ }
62
+ return runtime;
63
+ }
64
+ export async function assertRuntimeHealthy(runtime, options = {}) {
65
+ parseLoopbackUrl(runtime.baseUrl);
66
+ const fetchImpl = options.fetchImpl ?? fetch;
67
+ const controller = new AbortController();
68
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS);
69
+ try {
70
+ const response = await fetchImpl(`${runtime.baseUrl}/api/local/health`, {
71
+ method: "GET",
72
+ signal: controller.signal,
73
+ });
74
+ if (!response.ok) {
75
+ return false;
76
+ }
77
+ const payload = await response.json();
78
+ return payload.secretsReturned === false;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ finally {
84
+ clearTimeout(timeout);
85
+ }
86
+ }
87
+ async function removeStaleRuntimeLock(options) {
88
+ try {
89
+ await removeRuntimeLock(options);
90
+ }
91
+ catch {
92
+ // A stale lock must not make status commands fail, especially after npm/global installs.
93
+ }
94
+ }
95
+ function parseRuntime(value) {
96
+ if (!isRecord(value)) {
97
+ return null;
98
+ }
99
+ const pid = value.pid;
100
+ const port = value.port;
101
+ const baseUrl = value.baseUrl;
102
+ const startedAt = value.startedAt;
103
+ const version = value.version;
104
+ if (typeof pid !== "number" ||
105
+ typeof port !== "number" ||
106
+ typeof baseUrl !== "string" ||
107
+ typeof startedAt !== "string" ||
108
+ typeof version !== "string" ||
109
+ !Number.isSafeInteger(pid) ||
110
+ !Number.isSafeInteger(port)) {
111
+ return null;
112
+ }
113
+ try {
114
+ parseLoopbackUrl(baseUrl);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ return {
120
+ pid,
121
+ port,
122
+ baseUrl,
123
+ startedAt,
124
+ version,
125
+ };
126
+ }
127
+ function parseLoopbackUrl(value) {
128
+ const parsedUrl = new URL(value);
129
+ if (parsedUrl.protocol !== "http:") {
130
+ throw new Error("MoneySiren local runtime must use http on loopback.");
131
+ }
132
+ assertLoopbackHost(parsedUrl.hostname);
133
+ return parsedUrl;
134
+ }
135
+ function resolveRuntimePath(path, cwd = process.cwd()) {
136
+ return isAbsolute(path) ? path : join(cwd, path);
137
+ }
138
+ function defaultRuntimeLockPath(env, platform) {
139
+ if (platform === "darwin") {
140
+ return joinForPlatform(platform, resolveHomeDirectory(env), "Library", "Application Support", "MoneySiren", "runtime.json");
141
+ }
142
+ if (platform === "win32") {
143
+ return joinForPlatform(platform, resolveWindowsAppDataDirectory(env), "MoneySiren", "runtime.json");
144
+ }
145
+ const configHome = trimToNull(env.XDG_CONFIG_HOME) ?? joinForPlatform(platform, resolveHomeDirectory(env), ".config");
146
+ return joinForPlatform(platform, configHome, "moneysiren", "runtime.json");
147
+ }
148
+ function resolveWindowsAppDataDirectory(env) {
149
+ return trimToNull(env.APPDATA) ?? win32.join(resolveHomeDirectory(env), "AppData", "Roaming");
150
+ }
151
+ function resolveHomeDirectory(env) {
152
+ return trimToNull(env.HOME) ?? trimToNull(env.USERPROFILE) ?? homedir();
153
+ }
154
+ function trimToNull(value) {
155
+ const trimmed = value?.trim();
156
+ return trimmed === undefined || trimmed.length === 0 ? null : trimmed;
157
+ }
158
+ function joinForPlatform(platform, ...segments) {
159
+ return platform === "win32" ? win32.join(...segments) : posix.join(...segments);
160
+ }
161
+ function isProcessAlive(pid) {
162
+ if (pid <= 0) {
163
+ return false;
164
+ }
165
+ try {
166
+ process.kill(pid, 0);
167
+ return true;
168
+ }
169
+ catch (error) {
170
+ if (isNodeError(error) && error.code === "EPERM") {
171
+ return true;
172
+ }
173
+ return false;
174
+ }
175
+ }
176
+ function isRecord(value) {
177
+ return typeof value === "object" && value !== null;
178
+ }
179
+ function isNodeError(value) {
180
+ return value instanceof Error && "code" in value;
181
+ }
182
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,3 @@
1
+ export { buildNotificationDigest, buildOperationsOverview, buildTodayLiveView, buildTrayMenuModel, readNotificationDigest, readOperationsOverview, readTodayLiveView, readTrayMenuModel, type LocalSafeEnvelope, type NotificationDigest, type NotificationDigestItem, type OperationsOverview, type OperationsOverviewAlert, type OperationsOverviewProvider, type ReadNotificationDigestOptions, type ReadOperationsOverviewOptions, type ReadTodayLiveViewOptions, type ReadTrayMenuModelOptions, type TodayLiveProviderInput, type TodayLiveProviderView, type TodayLiveView, type TrayMenuItem, type TrayMenuModel, type ViewModelAlertRecord, type ViewModelBillingSnapshotRecord, type ViewModelCostEstimateRecord, type ViewModelHealthStatus, type ViewModelProviderRecord, type ViewModelReadStore, type ViewModelRiskSeverity, type ViewModelServiceHealthSnapshotRecord, type ViewModelStore, type ViewModelUsageSnapshotRecord, } from "./view-model.js";
2
+ export { cloneNotificationPreferences, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, readNotificationPreferencesFile, resolveNotificationPreferencesPath, writeNotificationPreferencesFile, type DashboardDisplayPreferences, type DigestInterval, type LocalCliDashboardMetricKey, type NotificationPreferenceFileOptions, type NotificationPreferences, type NotificationThresholdRule, type NotificationWidgetKey, type ThresholdOperator, } from "./notification-preferences.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { buildNotificationDigest, buildOperationsOverview, buildTodayLiveView, buildTrayMenuModel, readNotificationDigest, readOperationsOverview, readTodayLiveView, readTrayMenuModel, } from "./view-model.js";
2
+ export { cloneNotificationPreferences, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, readNotificationPreferencesFile, resolveNotificationPreferencesPath, writeNotificationPreferencesFile, } from "./notification-preferences.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,47 @@
1
+ export declare const NOTIFICATION_WIDGET_KEYS: readonly ["month_forecast", "today_live_cost", "risk_high_count", "stale_connection_count", "aws_month_forecast", "openai_today_cost", "openai_today_tokens", "claude_five_hour_percent", "claude_weekly_percent", "codex_five_hour_percent", "codex_weekly_percent", "supabase_usage_health", "cloudflare_month_to_date"];
2
+ export declare const LOCAL_CLI_DASHBOARD_METRIC_KEYS: readonly ["context_percent", "last_request_tokens", "total_tokens", "five_hour_limit_percent", "weekly_limit_percent", "five_hour_remaining_tokens", "weekly_remaining_tokens", "context_tokens", "input_tokens", "output_tokens", "cache_tokens", "reasoning_tokens", "sessions", "turns", "tool_calls", "log_files"];
3
+ export type NotificationWidgetKey = (typeof NOTIFICATION_WIDGET_KEYS)[number];
4
+ export type LocalCliDashboardMetricKey = (typeof LOCAL_CLI_DASHBOARD_METRIC_KEYS)[number];
5
+ export type ThresholdOperator = "gte" | "lte" | "eq";
6
+ export type DigestInterval = "six-hours" | "daily" | "weekly";
7
+ export interface NotificationThresholdRule {
8
+ widgetKey: NotificationWidgetKey;
9
+ operator: ThresholdOperator;
10
+ value: number;
11
+ cooldownMinutes: number;
12
+ }
13
+ export interface NotificationPreferences {
14
+ enabled: boolean;
15
+ digestEnabled: boolean;
16
+ digestInterval: DigestInterval;
17
+ quietHours: {
18
+ start: string;
19
+ end: string;
20
+ };
21
+ selectedWidgets: readonly NotificationWidgetKey[];
22
+ thresholdRules: readonly NotificationThresholdRule[];
23
+ desktopEnabled: boolean;
24
+ dashboard: DashboardDisplayPreferences;
25
+ hud: HudPreferences;
26
+ }
27
+ export interface DashboardDisplayPreferences {
28
+ localCliMetricKeys: readonly LocalCliDashboardMetricKey[];
29
+ }
30
+ export interface HudPreferences {
31
+ alwaysOnTop: boolean;
32
+ fontScale: number;
33
+ opacity: number;
34
+ selectedWidgets: readonly NotificationWidgetKey[];
35
+ }
36
+ export interface NotificationPreferenceFileOptions {
37
+ cwd?: string;
38
+ env?: Record<string, string | undefined>;
39
+ path?: string;
40
+ }
41
+ export declare const DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS: readonly NotificationWidgetKey[];
42
+ export declare const DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS: readonly LocalCliDashboardMetricKey[];
43
+ export declare const DEFAULT_NOTIFICATION_THRESHOLD_RULES: readonly NotificationThresholdRule[];
44
+ export declare const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences;
45
+ export declare function parseNotificationPreferences(value: unknown): NotificationPreferences;
46
+ export declare function cloneNotificationPreferences(preferences: NotificationPreferences): NotificationPreferences;
47
+ //# sourceMappingURL=notification-preferences-model.d.ts.map
@@ -0,0 +1,218 @@
1
+ export const NOTIFICATION_WIDGET_KEYS = [
2
+ "month_forecast",
3
+ "today_live_cost",
4
+ "risk_high_count",
5
+ "stale_connection_count",
6
+ "aws_month_forecast",
7
+ "openai_today_cost",
8
+ "openai_today_tokens",
9
+ "claude_five_hour_percent",
10
+ "claude_weekly_percent",
11
+ "codex_five_hour_percent",
12
+ "codex_weekly_percent",
13
+ "supabase_usage_health",
14
+ "cloudflare_month_to_date",
15
+ ];
16
+ export const LOCAL_CLI_DASHBOARD_METRIC_KEYS = [
17
+ "context_percent",
18
+ "last_request_tokens",
19
+ "total_tokens",
20
+ "five_hour_limit_percent",
21
+ "weekly_limit_percent",
22
+ "five_hour_remaining_tokens",
23
+ "weekly_remaining_tokens",
24
+ "context_tokens",
25
+ "input_tokens",
26
+ "output_tokens",
27
+ "cache_tokens",
28
+ "reasoning_tokens",
29
+ "sessions",
30
+ "turns",
31
+ "tool_calls",
32
+ "log_files",
33
+ ];
34
+ export const DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS = [
35
+ "month_forecast",
36
+ "today_live_cost",
37
+ "risk_high_count",
38
+ "stale_connection_count",
39
+ "openai_today_tokens",
40
+ "codex_five_hour_percent",
41
+ ];
42
+ export const DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS = [
43
+ "context_percent",
44
+ "last_request_tokens",
45
+ "total_tokens",
46
+ ];
47
+ export const DEFAULT_NOTIFICATION_THRESHOLD_RULES = [
48
+ {
49
+ widgetKey: "risk_high_count",
50
+ operator: "gte",
51
+ value: 1,
52
+ cooldownMinutes: 60,
53
+ },
54
+ {
55
+ widgetKey: "today_live_cost",
56
+ operator: "gte",
57
+ value: 10,
58
+ cooldownMinutes: 180,
59
+ },
60
+ {
61
+ widgetKey: "codex_weekly_percent",
62
+ operator: "gte",
63
+ value: 90,
64
+ cooldownMinutes: 360,
65
+ },
66
+ ];
67
+ export const DEFAULT_NOTIFICATION_PREFERENCES = {
68
+ enabled: true,
69
+ digestEnabled: true,
70
+ digestInterval: "daily",
71
+ quietHours: {
72
+ start: "22:00",
73
+ end: "08:00",
74
+ },
75
+ selectedWidgets: DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS,
76
+ thresholdRules: DEFAULT_NOTIFICATION_THRESHOLD_RULES,
77
+ desktopEnabled: false,
78
+ dashboard: {
79
+ localCliMetricKeys: DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS,
80
+ },
81
+ hud: {
82
+ alwaysOnTop: true,
83
+ fontScale: 0.95,
84
+ opacity: 0.94,
85
+ selectedWidgets: DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS,
86
+ },
87
+ };
88
+ export function parseNotificationPreferences(value) {
89
+ if (!isRecord(value)) {
90
+ return cloneNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
91
+ }
92
+ const selectedWidgets = parseSelectedWidgets(value.selectedWidgets);
93
+ return {
94
+ enabled: typeof value.enabled === "boolean" ? value.enabled : DEFAULT_NOTIFICATION_PREFERENCES.enabled,
95
+ digestEnabled: typeof value.digestEnabled === "boolean"
96
+ ? value.digestEnabled
97
+ : DEFAULT_NOTIFICATION_PREFERENCES.digestEnabled,
98
+ digestInterval: parseDigestInterval(value.digestInterval),
99
+ quietHours: parseQuietHours(value.quietHours),
100
+ selectedWidgets,
101
+ thresholdRules: parseThresholdRules(value.thresholdRules),
102
+ desktopEnabled: typeof value.desktopEnabled === "boolean"
103
+ ? value.desktopEnabled
104
+ : DEFAULT_NOTIFICATION_PREFERENCES.desktopEnabled,
105
+ dashboard: parseDashboardDisplayPreferences(value.dashboard),
106
+ hud: parseHudPreferences(value.hud, selectedWidgets),
107
+ };
108
+ }
109
+ export function cloneNotificationPreferences(preferences) {
110
+ return {
111
+ enabled: preferences.enabled,
112
+ digestEnabled: preferences.digestEnabled,
113
+ digestInterval: preferences.digestInterval,
114
+ quietHours: {
115
+ ...preferences.quietHours,
116
+ },
117
+ selectedWidgets: [...preferences.selectedWidgets],
118
+ thresholdRules: preferences.thresholdRules.map((rule) => ({ ...rule })),
119
+ desktopEnabled: preferences.desktopEnabled,
120
+ dashboard: parseDashboardDisplayPreferences(preferences.dashboard),
121
+ hud: parseHudPreferences(preferences.hud),
122
+ };
123
+ }
124
+ function parseDashboardDisplayPreferences(value) {
125
+ const record = isRecord(value) ? value : {};
126
+ return {
127
+ localCliMetricKeys: parseLocalCliDashboardMetricKeys(record.localCliMetricKeys),
128
+ };
129
+ }
130
+ function parseHudPreferences(value, fallbackSelectedWidgets = DEFAULT_NOTIFICATION_PREFERENCES.hud.selectedWidgets) {
131
+ const record = isRecord(value) ? value : {};
132
+ return {
133
+ alwaysOnTop: typeof record.alwaysOnTop === "boolean"
134
+ ? record.alwaysOnTop
135
+ : DEFAULT_NOTIFICATION_PREFERENCES.hud.alwaysOnTop,
136
+ fontScale: clampNumber(record.fontScale, 0.8, 1.3, DEFAULT_NOTIFICATION_PREFERENCES.hud.fontScale),
137
+ opacity: clampNumber(record.opacity, 0, 1, DEFAULT_NOTIFICATION_PREFERENCES.hud.opacity),
138
+ selectedWidgets: parseSelectedWidgets(record.selectedWidgets, fallbackSelectedWidgets),
139
+ };
140
+ }
141
+ function parseLocalCliDashboardMetricKeys(value, fallbackMetricKeys = DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS) {
142
+ const metricKeys = new Set(LOCAL_CLI_DASHBOARD_METRIC_KEYS);
143
+ const selected = Array.isArray(value)
144
+ ? value.filter((item) => typeof item === "string" && metricKeys.has(item))
145
+ : [...fallbackMetricKeys];
146
+ return selected.length === 0 ? [...fallbackMetricKeys] : [...new Set(selected)];
147
+ }
148
+ function parseDigestInterval(value) {
149
+ return value === "six-hours" || value === "daily" || value === "weekly"
150
+ ? value
151
+ : DEFAULT_NOTIFICATION_PREFERENCES.digestInterval;
152
+ }
153
+ function parseQuietHours(value) {
154
+ const record = isRecord(value) ? value : {};
155
+ return {
156
+ start: parseTime(record.start, DEFAULT_NOTIFICATION_PREFERENCES.quietHours.start),
157
+ end: parseTime(record.end, DEFAULT_NOTIFICATION_PREFERENCES.quietHours.end),
158
+ };
159
+ }
160
+ function parseSelectedWidgets(value, fallbackSelectedWidgets = DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS) {
161
+ const widgetKeys = new Set(NOTIFICATION_WIDGET_KEYS);
162
+ const selected = Array.isArray(value)
163
+ ? value.filter((item) => typeof item === "string" && widgetKeys.has(item))
164
+ : [...fallbackSelectedWidgets];
165
+ return selected.length === 0 ? [...fallbackSelectedWidgets] : [...new Set(selected)];
166
+ }
167
+ function parseThresholdRules(value) {
168
+ if (!Array.isArray(value)) {
169
+ return DEFAULT_NOTIFICATION_THRESHOLD_RULES.map((rule) => ({ ...rule }));
170
+ }
171
+ const rules = value
172
+ .map((item) => isRecord(item) ? item : null)
173
+ .filter((item) => item !== null)
174
+ .map((item) => {
175
+ const widgetKey = parseWidgetKey(item.widgetKey);
176
+ const operator = parseOperator(item.operator);
177
+ const numericValue = parseNonNegativeNumber(item.value);
178
+ const cooldownMinutes = parseNonNegativeNumber(item.cooldownMinutes);
179
+ if (widgetKey === null || operator === null || numericValue === null || cooldownMinutes === null) {
180
+ return null;
181
+ }
182
+ return {
183
+ widgetKey,
184
+ operator,
185
+ value: numericValue,
186
+ cooldownMinutes,
187
+ };
188
+ })
189
+ .filter((item) => item !== null);
190
+ return rules.length === 0 ? DEFAULT_NOTIFICATION_THRESHOLD_RULES.map((rule) => ({ ...rule })) : rules;
191
+ }
192
+ function parseWidgetKey(value) {
193
+ return typeof value === "string" && NOTIFICATION_WIDGET_KEYS.includes(value)
194
+ ? value
195
+ : null;
196
+ }
197
+ function parseOperator(value) {
198
+ return value === "gte" || value === "lte" || value === "eq" ? value : null;
199
+ }
200
+ function parseNonNegativeNumber(value) {
201
+ if (typeof value !== "number" || !Number.isFinite(value)) {
202
+ return null;
203
+ }
204
+ return Math.max(0, value);
205
+ }
206
+ function clampNumber(value, min, max, fallback) {
207
+ if (typeof value !== "number" || !Number.isFinite(value)) {
208
+ return fallback;
209
+ }
210
+ return Math.min(max, Math.max(min, Math.round(value * 100) / 100));
211
+ }
212
+ function parseTime(value, fallback) {
213
+ return typeof value === "string" && /^\d{2}:\d{2}$/.test(value) ? value : fallback;
214
+ }
215
+ function isRecord(value) {
216
+ return typeof value === "object" && value !== null && !Array.isArray(value);
217
+ }
218
+ //# sourceMappingURL=notification-preferences-model.js.map
@@ -0,0 +1,6 @@
1
+ import { type NotificationPreferenceFileOptions, type NotificationPreferences } from "./notification-preferences-model.js";
2
+ export { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, type DigestInterval, type DashboardDisplayPreferences, type LocalCliDashboardMetricKey, type NotificationPreferenceFileOptions, type NotificationPreferences, type NotificationThresholdRule, type NotificationWidgetKey, type ThresholdOperator, } from "./notification-preferences-model.js";
3
+ export declare function readNotificationPreferencesFile(options?: NotificationPreferenceFileOptions): Promise<NotificationPreferences>;
4
+ export declare function writeNotificationPreferencesFile(preferences: NotificationPreferences, options?: NotificationPreferenceFileOptions): Promise<NotificationPreferences>;
5
+ export declare function resolveNotificationPreferencesPath(options?: NotificationPreferenceFileOptions): string;
6
+ //# sourceMappingURL=notification-preferences.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join } from "node:path";
3
+ import { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, parseNotificationPreferences, } from "./notification-preferences-model.js";
4
+ export { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, } from "./notification-preferences-model.js";
5
+ const PREFERENCES_PATH_ENV = "MONEYSIREN_NOTIFICATION_PREFS_PATH";
6
+ const DEFAULT_PREFERENCES_PATH = ".moneysiren/notification-preferences.json";
7
+ export async function readNotificationPreferencesFile(options = {}) {
8
+ try {
9
+ const parsed = JSON.parse(await readFile(resolveNotificationPreferencesPath(options), "utf8"));
10
+ return parseNotificationPreferences(parsed);
11
+ }
12
+ catch {
13
+ return cloneNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
14
+ }
15
+ }
16
+ export async function writeNotificationPreferencesFile(preferences, options = {}) {
17
+ const normalized = parseNotificationPreferences(preferences);
18
+ const path = resolveNotificationPreferencesPath(options);
19
+ await mkdir(dirname(path), { recursive: true });
20
+ await writeFile(path, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
21
+ return normalized;
22
+ }
23
+ export function resolveNotificationPreferencesPath(options = {}) {
24
+ if (options.path !== undefined && options.path.trim().length > 0) {
25
+ return resolveLocalPath(options.path, options.cwd);
26
+ }
27
+ const configuredPath = options.env?.[PREFERENCES_PATH_ENV]?.trim();
28
+ if (configuredPath !== undefined && configuredPath.length > 0) {
29
+ return resolveLocalPath(configuredPath, options.cwd);
30
+ }
31
+ return resolveLocalPath(DEFAULT_PREFERENCES_PATH, options.cwd);
32
+ }
33
+ function resolveLocalPath(path, cwd = process.cwd()) {
34
+ return isAbsolute(path) ? path : join(cwd, path);
35
+ }
36
+ //# sourceMappingURL=notification-preferences.js.map