@hasna/microservices 0.0.9 → 0.0.11

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 (100) hide show
  1. package/bin/index.js +236 -36
  2. package/bin/mcp.js +153 -4
  3. package/dist/index.js +120 -3
  4. package/microservices/microservice-analytics/package.json +27 -0
  5. package/microservices/microservice-analytics/src/cli/index.ts +373 -0
  6. package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
  7. package/microservices/microservice-analytics/src/db/database.ts +93 -0
  8. package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
  9. package/microservices/microservice-analytics/src/index.ts +37 -0
  10. package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
  11. package/microservices/microservice-assets/package.json +27 -0
  12. package/microservices/microservice-assets/src/cli/index.ts +375 -0
  13. package/microservices/microservice-assets/src/db/assets.ts +370 -0
  14. package/microservices/microservice-assets/src/db/database.ts +93 -0
  15. package/microservices/microservice-assets/src/db/migrations.ts +51 -0
  16. package/microservices/microservice-assets/src/index.ts +32 -0
  17. package/microservices/microservice-assets/src/mcp/index.ts +346 -0
  18. package/microservices/microservice-compliance/package.json +27 -0
  19. package/microservices/microservice-compliance/src/cli/index.ts +467 -0
  20. package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
  21. package/microservices/microservice-compliance/src/db/database.ts +93 -0
  22. package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
  23. package/microservices/microservice-compliance/src/index.ts +46 -0
  24. package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
  25. package/microservices/microservice-habits/package.json +27 -0
  26. package/microservices/microservice-habits/src/cli/index.ts +315 -0
  27. package/microservices/microservice-habits/src/db/database.ts +93 -0
  28. package/microservices/microservice-habits/src/db/habits.ts +451 -0
  29. package/microservices/microservice-habits/src/db/migrations.ts +46 -0
  30. package/microservices/microservice-habits/src/index.ts +31 -0
  31. package/microservices/microservice-habits/src/mcp/index.ts +313 -0
  32. package/microservices/microservice-health/package.json +27 -0
  33. package/microservices/microservice-health/src/cli/index.ts +484 -0
  34. package/microservices/microservice-health/src/db/database.ts +93 -0
  35. package/microservices/microservice-health/src/db/health.ts +708 -0
  36. package/microservices/microservice-health/src/db/migrations.ts +70 -0
  37. package/microservices/microservice-health/src/index.ts +63 -0
  38. package/microservices/microservice-health/src/mcp/index.ts +437 -0
  39. package/microservices/microservice-leads/package.json +27 -0
  40. package/microservices/microservice-leads/src/cli/index.ts +596 -0
  41. package/microservices/microservice-leads/src/db/database.ts +93 -0
  42. package/microservices/microservice-leads/src/db/leads.ts +520 -0
  43. package/microservices/microservice-leads/src/db/lists.ts +151 -0
  44. package/microservices/microservice-leads/src/db/migrations.ts +93 -0
  45. package/microservices/microservice-leads/src/index.ts +65 -0
  46. package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
  47. package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
  48. package/microservices/microservice-leads/src/mcp/index.ts +533 -0
  49. package/microservices/microservice-notifications/package.json +27 -0
  50. package/microservices/microservice-notifications/src/cli/index.ts +349 -0
  51. package/microservices/microservice-notifications/src/db/database.ts +93 -0
  52. package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
  53. package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
  54. package/microservices/microservice-notifications/src/index.ts +41 -0
  55. package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
  56. package/microservices/microservice-products/package.json +27 -0
  57. package/microservices/microservice-products/src/cli/index.ts +416 -0
  58. package/microservices/microservice-products/src/db/categories.ts +154 -0
  59. package/microservices/microservice-products/src/db/database.ts +93 -0
  60. package/microservices/microservice-products/src/db/migrations.ts +58 -0
  61. package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
  62. package/microservices/microservice-products/src/db/products.ts +452 -0
  63. package/microservices/microservice-products/src/index.ts +53 -0
  64. package/microservices/microservice-products/src/mcp/index.ts +453 -0
  65. package/microservices/microservice-projects/package.json +27 -0
  66. package/microservices/microservice-projects/src/cli/index.ts +480 -0
  67. package/microservices/microservice-projects/src/db/database.ts +93 -0
  68. package/microservices/microservice-projects/src/db/migrations.ts +65 -0
  69. package/microservices/microservice-projects/src/db/projects.ts +715 -0
  70. package/microservices/microservice-projects/src/index.ts +57 -0
  71. package/microservices/microservice-projects/src/mcp/index.ts +501 -0
  72. package/microservices/microservice-proposals/package.json +27 -0
  73. package/microservices/microservice-proposals/src/cli/index.ts +400 -0
  74. package/microservices/microservice-proposals/src/db/database.ts +93 -0
  75. package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
  76. package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
  77. package/microservices/microservice-proposals/src/index.ts +37 -0
  78. package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
  79. package/microservices/microservice-reading/package.json +27 -0
  80. package/microservices/microservice-reading/src/cli/index.ts +464 -0
  81. package/microservices/microservice-reading/src/db/database.ts +93 -0
  82. package/microservices/microservice-reading/src/db/migrations.ts +59 -0
  83. package/microservices/microservice-reading/src/db/reading.ts +524 -0
  84. package/microservices/microservice-reading/src/index.ts +51 -0
  85. package/microservices/microservice-reading/src/mcp/index.ts +368 -0
  86. package/microservices/microservice-travel/package.json +27 -0
  87. package/microservices/microservice-travel/src/cli/index.ts +505 -0
  88. package/microservices/microservice-travel/src/db/database.ts +93 -0
  89. package/microservices/microservice-travel/src/db/migrations.ts +77 -0
  90. package/microservices/microservice-travel/src/db/travel.ts +802 -0
  91. package/microservices/microservice-travel/src/index.ts +60 -0
  92. package/microservices/microservice-travel/src/mcp/index.ts +495 -0
  93. package/microservices/microservice-wiki/package.json +27 -0
  94. package/microservices/microservice-wiki/src/cli/index.ts +345 -0
  95. package/microservices/microservice-wiki/src/db/database.ts +93 -0
  96. package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
  97. package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
  98. package/microservices/microservice-wiki/src/index.ts +32 -0
  99. package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
  100. package/package.json +1 -1
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Analytics CRUD operations — KPIs, Dashboards, Reports
3
+ */
4
+
5
+ import { getDatabase } from "./database.js";
6
+
7
+ // ─── KPI Types ───
8
+
9
+ export interface Kpi {
10
+ id: string;
11
+ name: string;
12
+ category: string | null;
13
+ value: number;
14
+ period: string | null;
15
+ source_service: string | null;
16
+ metadata: Record<string, unknown>;
17
+ recorded_at: string;
18
+ }
19
+
20
+ interface KpiRow {
21
+ id: string;
22
+ name: string;
23
+ category: string | null;
24
+ value: number;
25
+ period: string | null;
26
+ source_service: string | null;
27
+ metadata: string;
28
+ recorded_at: string;
29
+ }
30
+
31
+ function rowToKpi(row: KpiRow): Kpi {
32
+ return {
33
+ ...row,
34
+ metadata: JSON.parse(row.metadata || "{}"),
35
+ };
36
+ }
37
+
38
+ // ─── Dashboard Types ───
39
+
40
+ export interface Dashboard {
41
+ id: string;
42
+ name: string;
43
+ description: string | null;
44
+ widgets: unknown[];
45
+ created_at: string;
46
+ }
47
+
48
+ interface DashboardRow {
49
+ id: string;
50
+ name: string;
51
+ description: string | null;
52
+ widgets: string;
53
+ created_at: string;
54
+ }
55
+
56
+ function rowToDashboard(row: DashboardRow): Dashboard {
57
+ return {
58
+ ...row,
59
+ widgets: JSON.parse(row.widgets || "[]"),
60
+ };
61
+ }
62
+
63
+ // ─── Report Types ───
64
+
65
+ export type ReportType = "daily" | "weekly" | "monthly" | "quarterly" | "annual" | "custom";
66
+
67
+ export interface Report {
68
+ id: string;
69
+ name: string;
70
+ type: ReportType;
71
+ content: string | null;
72
+ period: string | null;
73
+ generated_at: string;
74
+ metadata: Record<string, unknown>;
75
+ }
76
+
77
+ interface ReportRow {
78
+ id: string;
79
+ name: string;
80
+ type: ReportType;
81
+ content: string | null;
82
+ period: string | null;
83
+ generated_at: string;
84
+ metadata: string;
85
+ }
86
+
87
+ function rowToReport(row: ReportRow): Report {
88
+ return {
89
+ ...row,
90
+ metadata: JSON.parse(row.metadata || "{}"),
91
+ };
92
+ }
93
+
94
+ // ─── KPI Operations ───
95
+
96
+ export interface RecordKpiInput {
97
+ name: string;
98
+ value: number;
99
+ category?: string;
100
+ source_service?: string;
101
+ period?: string;
102
+ metadata?: Record<string, unknown>;
103
+ }
104
+
105
+ export function recordKpi(input: RecordKpiInput): Kpi {
106
+ const db = getDatabase();
107
+ const id = crypto.randomUUID();
108
+ const metadata = JSON.stringify(input.metadata || {});
109
+
110
+ db.prepare(
111
+ `INSERT INTO kpis (id, name, value, category, source_service, period, metadata)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
113
+ ).run(
114
+ id,
115
+ input.name,
116
+ input.value,
117
+ input.category || null,
118
+ input.source_service || null,
119
+ input.period || null,
120
+ metadata
121
+ );
122
+
123
+ return getKpiById(id)!;
124
+ }
125
+
126
+ export function getKpiById(id: string): Kpi | null {
127
+ const db = getDatabase();
128
+ const row = db.prepare("SELECT * FROM kpis WHERE id = ?").get(id) as KpiRow | null;
129
+ return row ? rowToKpi(row) : null;
130
+ }
131
+
132
+ export function getKpi(name: string, period?: string): Kpi | null {
133
+ const db = getDatabase();
134
+ let sql = "SELECT * FROM kpis WHERE name = ?";
135
+ const params: unknown[] = [name];
136
+
137
+ if (period) {
138
+ sql += " AND period = ?";
139
+ params.push(period);
140
+ }
141
+
142
+ sql += " ORDER BY recorded_at DESC LIMIT 1";
143
+
144
+ const row = db.prepare(sql).get(...params) as KpiRow | null;
145
+ return row ? rowToKpi(row) : null;
146
+ }
147
+
148
+ export function getKpiTrend(name: string, days: number = 30): Kpi[] {
149
+ const db = getDatabase();
150
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
151
+
152
+ const rows = db
153
+ .prepare(
154
+ "SELECT * FROM kpis WHERE name = ? AND recorded_at >= ? ORDER BY recorded_at ASC"
155
+ )
156
+ .all(name, cutoff) as KpiRow[];
157
+
158
+ return rows.map(rowToKpi);
159
+ }
160
+
161
+ export interface ListKpisOptions {
162
+ category?: string;
163
+ source_service?: string;
164
+ limit?: number;
165
+ offset?: number;
166
+ }
167
+
168
+ export function listKpis(options: ListKpisOptions = {}): Kpi[] {
169
+ const db = getDatabase();
170
+ const conditions: string[] = [];
171
+ const params: unknown[] = [];
172
+
173
+ if (options.category) {
174
+ conditions.push("category = ?");
175
+ params.push(options.category);
176
+ }
177
+
178
+ if (options.source_service) {
179
+ conditions.push("source_service = ?");
180
+ params.push(options.source_service);
181
+ }
182
+
183
+ let sql = "SELECT * FROM kpis";
184
+ if (conditions.length > 0) {
185
+ sql += " WHERE " + conditions.join(" AND ");
186
+ }
187
+ sql += " ORDER BY recorded_at DESC";
188
+
189
+ if (options.limit) {
190
+ sql += " LIMIT ?";
191
+ params.push(options.limit);
192
+ }
193
+ if (options.offset) {
194
+ sql += " OFFSET ?";
195
+ params.push(options.offset);
196
+ }
197
+
198
+ const rows = db.prepare(sql).all(...params) as KpiRow[];
199
+ return rows.map(rowToKpi);
200
+ }
201
+
202
+ export function getLatestKpis(): Kpi[] {
203
+ const db = getDatabase();
204
+ // Get the most recent value for each unique KPI name (using MAX(rowid) to break ties)
205
+ const rows = db
206
+ .prepare(
207
+ `SELECT * FROM kpis WHERE rowid IN (
208
+ SELECT MAX(rowid) FROM kpis GROUP BY name
209
+ ) ORDER BY name`
210
+ )
211
+ .all() as KpiRow[];
212
+
213
+ return rows.map(rowToKpi);
214
+ }
215
+
216
+ export function deleteKpi(id: string): boolean {
217
+ const db = getDatabase();
218
+ const result = db.prepare("DELETE FROM kpis WHERE id = ?").run(id);
219
+ return result.changes > 0;
220
+ }
221
+
222
+ // ─── Dashboard Operations ───
223
+
224
+ export interface CreateDashboardInput {
225
+ name: string;
226
+ description?: string;
227
+ widgets?: unknown[];
228
+ }
229
+
230
+ export function createDashboard(input: CreateDashboardInput): Dashboard {
231
+ const db = getDatabase();
232
+ const id = crypto.randomUUID();
233
+ const widgets = JSON.stringify(input.widgets || []);
234
+
235
+ db.prepare(
236
+ `INSERT INTO dashboards (id, name, description, widgets)
237
+ VALUES (?, ?, ?, ?)`
238
+ ).run(id, input.name, input.description || null, widgets);
239
+
240
+ return getDashboard(id)!;
241
+ }
242
+
243
+ export function getDashboard(id: string): Dashboard | null {
244
+ const db = getDatabase();
245
+ const row = db.prepare("SELECT * FROM dashboards WHERE id = ?").get(id) as DashboardRow | null;
246
+ return row ? rowToDashboard(row) : null;
247
+ }
248
+
249
+ export function listDashboards(): Dashboard[] {
250
+ const db = getDatabase();
251
+ const rows = db
252
+ .prepare("SELECT * FROM dashboards ORDER BY created_at DESC")
253
+ .all() as DashboardRow[];
254
+ return rows.map(rowToDashboard);
255
+ }
256
+
257
+ export interface UpdateDashboardInput {
258
+ name?: string;
259
+ description?: string;
260
+ widgets?: unknown[];
261
+ }
262
+
263
+ export function updateDashboard(
264
+ id: string,
265
+ input: UpdateDashboardInput
266
+ ): Dashboard | null {
267
+ const db = getDatabase();
268
+ const existing = getDashboard(id);
269
+ if (!existing) return null;
270
+
271
+ const sets: string[] = [];
272
+ const params: unknown[] = [];
273
+
274
+ if (input.name !== undefined) {
275
+ sets.push("name = ?");
276
+ params.push(input.name);
277
+ }
278
+ if (input.description !== undefined) {
279
+ sets.push("description = ?");
280
+ params.push(input.description);
281
+ }
282
+ if (input.widgets !== undefined) {
283
+ sets.push("widgets = ?");
284
+ params.push(JSON.stringify(input.widgets));
285
+ }
286
+
287
+ if (sets.length === 0) return existing;
288
+
289
+ params.push(id);
290
+
291
+ db.prepare(
292
+ `UPDATE dashboards SET ${sets.join(", ")} WHERE id = ?`
293
+ ).run(...params);
294
+
295
+ return getDashboard(id);
296
+ }
297
+
298
+ export function deleteDashboard(id: string): boolean {
299
+ const db = getDatabase();
300
+ const result = db.prepare("DELETE FROM dashboards WHERE id = ?").run(id);
301
+ return result.changes > 0;
302
+ }
303
+
304
+ // ─── Report Operations ───
305
+
306
+ export interface GenerateReportInput {
307
+ name: string;
308
+ type: ReportType;
309
+ period?: string;
310
+ metadata?: Record<string, unknown>;
311
+ }
312
+
313
+ export function generateReport(input: GenerateReportInput): Report {
314
+ const db = getDatabase();
315
+ const id = crypto.randomUUID();
316
+ const metadata = JSON.stringify(input.metadata || {});
317
+
318
+ // Build report content from current KPIs
319
+ const latestKpis = getLatestKpis();
320
+ const categories = new Map<string, Kpi[]>();
321
+ for (const kpi of latestKpis) {
322
+ const cat = kpi.category || "Uncategorized";
323
+ if (!categories.has(cat)) categories.set(cat, []);
324
+ categories.get(cat)!.push(kpi);
325
+ }
326
+
327
+ const lines: string[] = [];
328
+ lines.push(`=== ${input.type.toUpperCase()} REPORT: ${input.name} ===`);
329
+ if (input.period) lines.push(`Period: ${input.period}`);
330
+ lines.push(`Generated: ${new Date().toISOString()}`);
331
+ lines.push("");
332
+
333
+ for (const [category, kpis] of categories) {
334
+ lines.push(`--- ${category} ---`);
335
+ for (const kpi of kpis) {
336
+ lines.push(` ${kpi.name}: ${kpi.value}${kpi.period ? ` (${kpi.period})` : ""}`);
337
+ }
338
+ lines.push("");
339
+ }
340
+
341
+ if (latestKpis.length === 0) {
342
+ lines.push("No KPIs recorded yet.");
343
+ }
344
+
345
+ const content = lines.join("\n");
346
+
347
+ db.prepare(
348
+ `INSERT INTO reports (id, name, type, content, period, metadata)
349
+ VALUES (?, ?, ?, ?, ?, ?)`
350
+ ).run(id, input.name, input.type, content, input.period || null, metadata);
351
+
352
+ return getReport(id)!;
353
+ }
354
+
355
+ export function getReport(id: string): Report | null {
356
+ const db = getDatabase();
357
+ const row = db.prepare("SELECT * FROM reports WHERE id = ?").get(id) as ReportRow | null;
358
+ return row ? rowToReport(row) : null;
359
+ }
360
+
361
+ export interface ListReportsOptions {
362
+ type?: ReportType;
363
+ limit?: number;
364
+ offset?: number;
365
+ }
366
+
367
+ export function listReports(options: ListReportsOptions = {}): Report[] {
368
+ const db = getDatabase();
369
+ const conditions: string[] = [];
370
+ const params: unknown[] = [];
371
+
372
+ if (options.type) {
373
+ conditions.push("type = ?");
374
+ params.push(options.type);
375
+ }
376
+
377
+ let sql = "SELECT * FROM reports";
378
+ if (conditions.length > 0) {
379
+ sql += " WHERE " + conditions.join(" AND ");
380
+ }
381
+ sql += " ORDER BY generated_at DESC";
382
+
383
+ if (options.limit) {
384
+ sql += " LIMIT ?";
385
+ params.push(options.limit);
386
+ }
387
+ if (options.offset) {
388
+ sql += " OFFSET ?";
389
+ params.push(options.offset);
390
+ }
391
+
392
+ const rows = db.prepare(sql).all(...params) as ReportRow[];
393
+ return rows.map(rowToReport);
394
+ }
395
+
396
+ export function deleteReport(id: string): boolean {
397
+ const db = getDatabase();
398
+ const result = db.prepare("DELETE FROM reports WHERE id = ?").run(id);
399
+ return result.changes > 0;
400
+ }
401
+
402
+ // ─── Business Health ───
403
+
404
+ export interface BusinessHealth {
405
+ total_kpis: number;
406
+ categories: { category: string; count: number; latest_value: number }[];
407
+ latest_kpis: Kpi[];
408
+ report_count: number;
409
+ dashboard_count: number;
410
+ }
411
+
412
+ export function getBusinessHealth(): BusinessHealth {
413
+ const db = getDatabase();
414
+
415
+ const totalKpis = (
416
+ db.prepare("SELECT COUNT(DISTINCT name) as count FROM kpis").get() as { count: number }
417
+ ).count;
418
+
419
+ const categoryRows = db
420
+ .prepare(
421
+ `SELECT category, COUNT(DISTINCT name) as count
422
+ FROM kpis WHERE category IS NOT NULL
423
+ GROUP BY category ORDER BY category`
424
+ )
425
+ .all() as { category: string; count: number }[];
426
+
427
+ // Get latest value per category
428
+ const categories = categoryRows.map((row) => {
429
+ const latestInCat = db
430
+ .prepare(
431
+ `SELECT value FROM kpis WHERE category = ? ORDER BY recorded_at DESC LIMIT 1`
432
+ )
433
+ .get(row.category) as { value: number } | null;
434
+ return {
435
+ category: row.category,
436
+ count: row.count,
437
+ latest_value: latestInCat?.value ?? 0,
438
+ };
439
+ });
440
+
441
+ const latestKpis = getLatestKpis();
442
+
443
+ const reportCount = (
444
+ db.prepare("SELECT COUNT(*) as count FROM reports").get() as { count: number }
445
+ ).count;
446
+
447
+ const dashboardCount = (
448
+ db.prepare("SELECT COUNT(*) as count FROM dashboards").get() as { count: number }
449
+ ).count;
450
+
451
+ return {
452
+ total_kpis: totalKpis,
453
+ categories,
454
+ latest_kpis: latestKpis,
455
+ report_count: reportCount,
456
+ dashboard_count: dashboardCount,
457
+ };
458
+ }
459
+
460
+ // ─── AI Executive Summary ───
461
+
462
+ export async function generateExecutiveSummary(): Promise<string> {
463
+ const health = getBusinessHealth();
464
+ const latestKpis = getLatestKpis();
465
+
466
+ // Build context for AI
467
+ const kpiSummary = latestKpis
468
+ .map((k) => `${k.name} (${k.category || "uncategorized"}): ${k.value}`)
469
+ .join("\n");
470
+
471
+ const prompt = `You are a business analyst. Generate a concise executive summary based on these KPIs:
472
+
473
+ ${kpiSummary || "No KPIs recorded yet."}
474
+
475
+ Total unique KPIs: ${health.total_kpis}
476
+ Categories: ${health.categories.map((c) => `${c.category} (${c.count} KPIs)`).join(", ") || "none"}
477
+ Reports generated: ${health.report_count}
478
+ Dashboards: ${health.dashboard_count}
479
+
480
+ Provide a brief, actionable executive summary in 3-5 sentences.`;
481
+
482
+ // Try Anthropic first, then OpenAI, then fallback
483
+ const anthropicKey = process.env["ANTHROPIC_API_KEY"];
484
+ const openaiKey = process.env["OPENAI_API_KEY"];
485
+
486
+ if (anthropicKey) {
487
+ try {
488
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
489
+ method: "POST",
490
+ headers: {
491
+ "Content-Type": "application/json",
492
+ "x-api-key": anthropicKey,
493
+ "anthropic-version": "2023-06-01",
494
+ },
495
+ body: JSON.stringify({
496
+ model: "claude-sonnet-4-20250514",
497
+ max_tokens: 500,
498
+ messages: [{ role: "user", content: prompt }],
499
+ }),
500
+ });
501
+
502
+ if (response.ok) {
503
+ const data = (await response.json()) as {
504
+ content: { type: string; text: string }[];
505
+ };
506
+ return data.content[0].text;
507
+ }
508
+ } catch {
509
+ // Fall through to OpenAI
510
+ }
511
+ }
512
+
513
+ if (openaiKey) {
514
+ try {
515
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
516
+ method: "POST",
517
+ headers: {
518
+ "Content-Type": "application/json",
519
+ Authorization: `Bearer ${openaiKey}`,
520
+ },
521
+ body: JSON.stringify({
522
+ model: "gpt-4o-mini",
523
+ max_tokens: 500,
524
+ messages: [{ role: "user", content: prompt }],
525
+ }),
526
+ });
527
+
528
+ if (response.ok) {
529
+ const data = (await response.json()) as {
530
+ choices: { message: { content: string } }[];
531
+ };
532
+ return data.choices[0].message.content;
533
+ }
534
+ } catch {
535
+ // Fall through to local summary
536
+ }
537
+ }
538
+
539
+ // Local fallback — no AI API keys
540
+ const lines: string[] = [];
541
+ lines.push("=== Executive Summary ===");
542
+ lines.push("");
543
+
544
+ if (latestKpis.length === 0) {
545
+ lines.push("No KPIs have been recorded yet. Start tracking key metrics to enable business health reporting.");
546
+ return lines.join("\n");
547
+ }
548
+
549
+ lines.push(`Tracking ${health.total_kpis} unique KPI(s) across ${health.categories.length} category(s).`);
550
+
551
+ for (const cat of health.categories) {
552
+ lines.push(`- ${cat.category}: ${cat.count} KPI(s), latest value: ${cat.latest_value}`);
553
+ }
554
+
555
+ if (health.report_count > 0) {
556
+ lines.push(`\n${health.report_count} report(s) have been generated.`);
557
+ }
558
+
559
+ if (health.dashboard_count > 0) {
560
+ lines.push(`${health.dashboard_count} dashboard(s) configured.`);
561
+ }
562
+
563
+ return lines.join("\n");
564
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Database connection for microservice-analytics
3
+ */
4
+
5
+ import { Database } from "bun:sqlite";
6
+ import { existsSync, mkdirSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { MIGRATIONS } from "./migrations.js";
9
+
10
+ let _db: Database | null = null;
11
+
12
+ function getDbPath(): string {
13
+ // Environment variable override
14
+ if (process.env["MICROSERVICES_DIR"]) {
15
+ return join(process.env["MICROSERVICES_DIR"], "microservice-analytics", "data.db");
16
+ }
17
+
18
+ // Check for .microservices in current or parent directories
19
+ let dir = resolve(process.cwd());
20
+ while (true) {
21
+ const candidate = join(dir, ".microservices", "microservice-analytics", "data.db");
22
+ const msDir = join(dir, ".microservices");
23
+ if (existsSync(msDir)) return candidate;
24
+ const parent = dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+
29
+ // Global fallback
30
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
31
+ return join(home, ".microservices", "microservice-analytics", "data.db");
32
+ }
33
+
34
+ function ensureDir(filePath: string): void {
35
+ const dir = dirname(resolve(filePath));
36
+ if (!existsSync(dir)) {
37
+ mkdirSync(dir, { recursive: true });
38
+ }
39
+ }
40
+
41
+ export function getDatabase(): Database {
42
+ if (_db) return _db;
43
+
44
+ const dbPath = getDbPath();
45
+ ensureDir(dbPath);
46
+
47
+ _db = new Database(dbPath);
48
+ _db.exec("PRAGMA journal_mode = WAL");
49
+ _db.exec("PRAGMA foreign_keys = ON");
50
+
51
+ // Create migrations table
52
+ _db.exec(`
53
+ CREATE TABLE IF NOT EXISTS _migrations (
54
+ id INTEGER PRIMARY KEY,
55
+ name TEXT NOT NULL,
56
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
57
+ )
58
+ `);
59
+
60
+ // Apply pending migrations
61
+ const applied = _db
62
+ .query("SELECT id FROM _migrations ORDER BY id")
63
+ .all() as { id: number }[];
64
+ const appliedIds = new Set(applied.map((r) => r.id));
65
+
66
+ for (const migration of MIGRATIONS) {
67
+ if (appliedIds.has(migration.id)) continue;
68
+
69
+ _db.exec("BEGIN");
70
+ try {
71
+ _db.exec(migration.sql);
72
+ _db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(
73
+ migration.id,
74
+ migration.name
75
+ );
76
+ _db.exec("COMMIT");
77
+ } catch (error) {
78
+ _db.exec("ROLLBACK");
79
+ throw new Error(
80
+ `Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`
81
+ );
82
+ }
83
+ }
84
+
85
+ return _db;
86
+ }
87
+
88
+ export function closeDatabase(): void {
89
+ if (_db) {
90
+ _db.close();
91
+ _db = null;
92
+ }
93
+ }
@@ -0,0 +1,50 @@
1
+ export interface MigrationEntry {
2
+ id: number;
3
+ name: string;
4
+ sql: string;
5
+ }
6
+
7
+ export const MIGRATIONS: MigrationEntry[] = [
8
+ {
9
+ id: 1,
10
+ name: "initial_schema",
11
+ sql: `
12
+ CREATE TABLE IF NOT EXISTS kpis (
13
+ id TEXT PRIMARY KEY,
14
+ name TEXT NOT NULL,
15
+ category TEXT,
16
+ value REAL NOT NULL,
17
+ period TEXT,
18
+ source_service TEXT,
19
+ metadata TEXT NOT NULL DEFAULT '{}',
20
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS dashboards (
24
+ id TEXT PRIMARY KEY,
25
+ name TEXT NOT NULL,
26
+ description TEXT,
27
+ widgets TEXT NOT NULL DEFAULT '[]',
28
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS reports (
32
+ id TEXT PRIMARY KEY,
33
+ name TEXT NOT NULL,
34
+ type TEXT NOT NULL CHECK(type IN ('daily','weekly','monthly','quarterly','annual','custom')),
35
+ content TEXT,
36
+ period TEXT,
37
+ generated_at TEXT NOT NULL DEFAULT (datetime('now')),
38
+ metadata TEXT NOT NULL DEFAULT '{}'
39
+ );
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_kpis_name ON kpis(name);
42
+ CREATE INDEX IF NOT EXISTS idx_kpis_category ON kpis(category);
43
+ CREATE INDEX IF NOT EXISTS idx_kpis_recorded_at ON kpis(recorded_at);
44
+ CREATE INDEX IF NOT EXISTS idx_kpis_name_recorded ON kpis(name, recorded_at);
45
+ CREATE INDEX IF NOT EXISTS idx_reports_type ON reports(type);
46
+ CREATE INDEX IF NOT EXISTS idx_reports_generated_at ON reports(generated_at);
47
+ CREATE INDEX IF NOT EXISTS idx_dashboards_name ON dashboards(name);
48
+ `,
49
+ },
50
+ ];