kiro-memory 1.7.1 → 1.8.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.
@@ -1570,19 +1570,28 @@ __export(Observations_exports, {
1570
1570
  deleteObservation: () => deleteObservation,
1571
1571
  getObservationsByProject: () => getObservationsByProject,
1572
1572
  getObservationsBySession: () => getObservationsBySession,
1573
+ isDuplicateObservation: () => isDuplicateObservation,
1573
1574
  searchObservations: () => searchObservations,
1574
1575
  updateLastAccessed: () => updateLastAccessed
1575
1576
  });
1576
1577
  function escapeLikePattern(input) {
1577
1578
  return input.replace(/[%_\\]/g, "\\$&");
1578
1579
  }
1579
- function createObservation(db2, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
1580
+ function isDuplicateObservation(db2, contentHash, windowMs = 3e4) {
1581
+ if (!contentHash) return false;
1582
+ const threshold = Date.now() - windowMs;
1583
+ const result = db2.query(
1584
+ "SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch > ? LIMIT 1"
1585
+ ).get(contentHash, threshold);
1586
+ return !!result;
1587
+ }
1588
+ function createObservation(db2, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
1580
1589
  const now = /* @__PURE__ */ new Date();
1581
1590
  const result = db2.run(
1582
- `INSERT INTO observations
1583
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
1584
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1585
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
1591
+ `INSERT INTO observations
1592
+ (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch, content_hash, discovery_tokens)
1593
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1594
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
1586
1595
  );
1587
1596
  return Number(result.lastInsertRowid);
1588
1597
  }
@@ -1638,39 +1647,42 @@ function consolidateObservations(db2, project, options = {}) {
1638
1647
  if (groups.length === 0) return { merged: 0, removed: 0 };
1639
1648
  let totalMerged = 0;
1640
1649
  let totalRemoved = 0;
1641
- for (const group of groups) {
1642
- const obsIds = group.ids.split(",").map(Number);
1643
- const placeholders = obsIds.map(() => "?").join(",");
1644
- const observations = db2.query(
1645
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
1646
- ).all(...obsIds);
1647
- if (observations.length < minGroupSize) continue;
1648
- if (options.dryRun) {
1649
- totalMerged += 1;
1650
- totalRemoved += observations.length - 1;
1651
- continue;
1652
- }
1653
- const keeper = observations[0];
1654
- const others = observations.slice(1);
1655
- const uniqueTexts = /* @__PURE__ */ new Set();
1656
- if (keeper.text) uniqueTexts.add(keeper.text);
1657
- for (const obs of others) {
1658
- if (obs.text && !uniqueTexts.has(obs.text)) {
1659
- uniqueTexts.add(obs.text);
1650
+ const runConsolidation = db2.transaction(() => {
1651
+ for (const group of groups) {
1652
+ const obsIds = group.ids.split(",").map(Number);
1653
+ const placeholders = obsIds.map(() => "?").join(",");
1654
+ const observations = db2.query(
1655
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
1656
+ ).all(...obsIds);
1657
+ if (observations.length < minGroupSize) continue;
1658
+ if (options.dryRun) {
1659
+ totalMerged += 1;
1660
+ totalRemoved += observations.length - 1;
1661
+ continue;
1660
1662
  }
1663
+ const keeper = observations[0];
1664
+ const others = observations.slice(1);
1665
+ const uniqueTexts = /* @__PURE__ */ new Set();
1666
+ if (keeper.text) uniqueTexts.add(keeper.text);
1667
+ for (const obs of others) {
1668
+ if (obs.text && !uniqueTexts.has(obs.text)) {
1669
+ uniqueTexts.add(obs.text);
1670
+ }
1671
+ }
1672
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
1673
+ db2.run(
1674
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
1675
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
1676
+ );
1677
+ const removeIds = others.map((o) => o.id);
1678
+ const removePlaceholders = removeIds.map(() => "?").join(",");
1679
+ db2.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
1680
+ db2.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
1681
+ totalMerged += 1;
1682
+ totalRemoved += removeIds.length;
1661
1683
  }
1662
- const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
1663
- db2.run(
1664
- "UPDATE observations SET text = ?, title = ? WHERE id = ?",
1665
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
1666
- );
1667
- const removeIds = others.map((o) => o.id);
1668
- const removePlaceholders = removeIds.map(() => "?").join(",");
1669
- db2.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
1670
- db2.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
1671
- totalMerged += 1;
1672
- totalRemoved += removeIds.length;
1673
- }
1684
+ });
1685
+ runConsolidation();
1674
1686
  return { merged: totalMerged, removed: totalRemoved };
1675
1687
  }
1676
1688
  var init_Observations = __esm({
@@ -1728,7 +1740,7 @@ function searchObservationsFTS(db2, query, filters = {}) {
1728
1740
  sql += " AND o.created_at_epoch <= ?";
1729
1741
  params.push(filters.dateEnd);
1730
1742
  }
1731
- sql += " ORDER BY rank LIMIT ?";
1743
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
1732
1744
  params.push(limit);
1733
1745
  const stmt = db2.query(sql);
1734
1746
  return stmt.all(...params);
@@ -1742,7 +1754,7 @@ function searchObservationsFTSWithRank(db2, query, filters = {}) {
1742
1754
  const safeQuery = sanitizeFTS5Query(query);
1743
1755
  if (!safeQuery) return [];
1744
1756
  let sql = `
1745
- SELECT o.*, rank as fts5_rank FROM observations o
1757
+ SELECT o.*, bm25(observations_fts, ${BM25_WEIGHTS}) as fts5_rank FROM observations o
1746
1758
  JOIN observations_fts fts ON o.id = fts.rowid
1747
1759
  WHERE observations_fts MATCH ?
1748
1760
  `;
@@ -1763,7 +1775,7 @@ function searchObservationsFTSWithRank(db2, query, filters = {}) {
1763
1775
  sql += " AND o.created_at_epoch <= ?";
1764
1776
  params.push(filters.dateEnd);
1765
1777
  }
1766
- sql += " ORDER BY rank LIMIT ?";
1778
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
1767
1779
  params.push(limit);
1768
1780
  const stmt = db2.query(sql);
1769
1781
  return stmt.all(...params);
@@ -1867,11 +1879,23 @@ function getProjectStats(db2, project) {
1867
1879
  const sumStmt = db2.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
1868
1880
  const sesStmt = db2.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
1869
1881
  const prmStmt = db2.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
1882
+ const discoveryStmt = db2.query(
1883
+ "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
1884
+ );
1885
+ const discoveryTokens = discoveryStmt.get(project)?.total || 0;
1886
+ const readStmt = db2.query(
1887
+ `SELECT COALESCE(SUM(
1888
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
1889
+ ), 0) as total FROM observations WHERE project = ?`
1890
+ );
1891
+ const readTokens = readStmt.get(project)?.total || 0;
1892
+ const savings = Math.max(0, discoveryTokens - readTokens);
1870
1893
  return {
1871
1894
  observations: obsStmt.get(project)?.count || 0,
1872
1895
  summaries: sumStmt.get(project)?.count || 0,
1873
1896
  sessions: sesStmt.get(project)?.count || 0,
1874
- prompts: prmStmt.get(project)?.count || 0
1897
+ prompts: prmStmt.get(project)?.count || 0,
1898
+ tokenEconomics: { discoveryTokens, readTokens, savings }
1875
1899
  };
1876
1900
  }
1877
1901
  function getStaleObservations(db2, project) {
@@ -1913,9 +1937,11 @@ function markObservationsStale(db2, ids, stale) {
1913
1937
  [stale ? 1 : 0, ...validIds]
1914
1938
  );
1915
1939
  }
1940
+ var BM25_WEIGHTS;
1916
1941
  var init_Search = __esm({
1917
1942
  "src/services/sqlite/Search.ts"() {
1918
1943
  "use strict";
1944
+ BM25_WEIGHTS = "10.0, 1.0, 5.0, 3.0";
1919
1945
  }
1920
1946
  });
1921
1947
 
@@ -3945,6 +3971,29 @@ var MigrationRunner = class {
3945
3971
  db2.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
3946
3972
  db2.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
3947
3973
  }
3974
+ },
3975
+ {
3976
+ version: 7,
3977
+ up: (db2) => {
3978
+ db2.run("ALTER TABLE observations ADD COLUMN content_hash TEXT");
3979
+ db2.run("CREATE INDEX IF NOT EXISTS idx_observations_hash ON observations(content_hash)");
3980
+ }
3981
+ },
3982
+ {
3983
+ version: 8,
3984
+ up: (db2) => {
3985
+ db2.run("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
3986
+ db2.run("ALTER TABLE summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
3987
+ }
3988
+ },
3989
+ {
3990
+ version: 9,
3991
+ up: (db2) => {
3992
+ db2.run("CREATE INDEX IF NOT EXISTS idx_observations_project_epoch ON observations(project, created_at_epoch DESC)");
3993
+ db2.run("CREATE INDEX IF NOT EXISTS idx_observations_project_type ON observations(project, type)");
3994
+ db2.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_epoch ON summaries(project, created_at_epoch DESC)");
3995
+ db2.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_epoch ON prompts(project, created_at_epoch DESC)");
3996
+ }
3948
3997
  }
3949
3998
  ];
3950
3999
  }
@@ -4510,6 +4559,14 @@ function getAnalyticsOverview(db2, project) {
4510
4559
  const knowledgeSql = project ? `SELECT COUNT(*) as count FROM observations WHERE project = ? AND type IN ('constraint', 'decision', 'heuristic', 'rejected')` : `SELECT COUNT(*) as count FROM observations WHERE type IN ('constraint', 'decision', 'heuristic', 'rejected')`;
4511
4560
  const knowledgeStmt = db2.query(knowledgeSql);
4512
4561
  const knowledgeCount = project ? knowledgeStmt.get(project)?.count || 0 : knowledgeStmt.get()?.count || 0;
4562
+ const discoverySql = project ? "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?" : "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations";
4563
+ const discoveryStmt = db2.query(discoverySql);
4564
+ const discoveryTokens = project ? discoveryStmt.get(project)?.total || 0 : discoveryStmt.get()?.total || 0;
4565
+ const readSql = project ? `SELECT COALESCE(SUM(CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)), 0) as total FROM observations WHERE project = ?` : `SELECT COALESCE(SUM(CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)), 0) as total FROM observations`;
4566
+ const readStmt = db2.query(readSql);
4567
+ const readTokens = project ? readStmt.get(project)?.total || 0 : readStmt.get()?.total || 0;
4568
+ const savings = Math.max(0, discoveryTokens - readTokens);
4569
+ const reductionPct = discoveryTokens > 0 ? Math.round((1 - readTokens / discoveryTokens) * 100) : 0;
4513
4570
  return {
4514
4571
  observations,
4515
4572
  summaries,
@@ -4518,7 +4575,8 @@ function getAnalyticsOverview(db2, project) {
4518
4575
  observationsToday,
4519
4576
  observationsThisWeek,
4520
4577
  staleCount,
4521
- knowledgeCount
4578
+ knowledgeCount,
4579
+ tokenEconomics: { discoveryTokens, readTokens, savings, reductionPct }
4522
4580
  };
4523
4581
  }
4524
4582
 
@@ -4951,6 +5009,7 @@ app.post("/api/observations", (req, res) => {
4951
5009
  0
4952
5010
  );
4953
5011
  broadcast("observation-created", { id, project, title });
5012
+ projectsCache.ts = 0;
4954
5013
  generateEmbeddingForObservation(id, title, content, concepts).catch(() => {
4955
5014
  });
4956
5015
  res.json({ id, success: true });
@@ -5301,8 +5360,12 @@ app.get("/api/project-aliases", (_req, res) => {
5301
5360
  app.put("/api/project-aliases/:project", (req, res) => {
5302
5361
  const { project } = req.params;
5303
5362
  const { displayName } = req.body;
5304
- if (!displayName || typeof displayName !== "string") {
5305
- res.status(400).json({ error: 'Field "displayName" (string) is required' });
5363
+ if (!isValidProject(project)) {
5364
+ res.status(400).json({ error: "Invalid project name" });
5365
+ return;
5366
+ }
5367
+ if (!displayName || typeof displayName !== "string" || displayName.trim().length === 0 || displayName.length > 100) {
5368
+ res.status(400).json({ error: 'Field "displayName" is required (string, max 100 chars)' });
5306
5369
  return;
5307
5370
  }
5308
5371
  try {
@@ -5319,8 +5382,15 @@ app.put("/api/project-aliases/:project", (req, res) => {
5319
5382
  res.status(500).json({ error: "Failed to update project alias" });
5320
5383
  }
5321
5384
  });
5385
+ var projectsCache = { data: [], ts: 0 };
5386
+ var PROJECTS_CACHE_TTL = 6e4;
5322
5387
  app.get("/api/projects", (_req, res) => {
5323
5388
  try {
5389
+ const now = Date.now();
5390
+ if (now - projectsCache.ts < PROJECTS_CACHE_TTL && projectsCache.data.length > 0) {
5391
+ res.json(projectsCache.data);
5392
+ return;
5393
+ }
5324
5394
  const stmt = db.db.query(
5325
5395
  `SELECT DISTINCT project FROM (
5326
5396
  SELECT project FROM observations
@@ -5331,7 +5401,8 @@ app.get("/api/projects", (_req, res) => {
5331
5401
  ) ORDER BY project ASC`
5332
5402
  );
5333
5403
  const rows = stmt.all();
5334
- res.json(rows.map((r) => r.project));
5404
+ projectsCache = { data: rows.map((r) => r.project), ts: now };
5405
+ res.json(projectsCache.data);
5335
5406
  } catch (error) {
5336
5407
  logger.error("WORKER", "Lista progetti fallita", {}, error);
5337
5408
  res.status(500).json({ error: "Failed to list projects" });
@@ -5477,6 +5548,154 @@ app.get("/api/report", (req, res) => {
5477
5548
  res.status(500).json({ error: "Report generation failed" });
5478
5549
  }
5479
5550
  });
5551
+ app.post("/api/memory/save", (req, res) => {
5552
+ const { project, title, content, type, concepts } = req.body;
5553
+ if (!isValidProject(project)) {
5554
+ res.status(400).json({ error: 'Invalid or missing "project"' });
5555
+ return;
5556
+ }
5557
+ if (!isValidString(title, 500)) {
5558
+ res.status(400).json({ error: 'Invalid or missing "title" (max 500 chars)' });
5559
+ return;
5560
+ }
5561
+ if (!isValidString(content, 1e5)) {
5562
+ res.status(400).json({ error: 'Invalid or missing "content" (max 100KB)' });
5563
+ return;
5564
+ }
5565
+ const obsType = type || "research";
5566
+ const conceptStr = Array.isArray(concepts) ? concepts.join(", ") : concepts || null;
5567
+ try {
5568
+ const id = createObservation(
5569
+ db.db,
5570
+ "memory-save-" + Date.now(),
5571
+ project,
5572
+ obsType,
5573
+ title,
5574
+ null,
5575
+ // subtitle
5576
+ content,
5577
+ // text
5578
+ content,
5579
+ // narrative
5580
+ null,
5581
+ // facts
5582
+ conceptStr,
5583
+ null,
5584
+ // filesRead
5585
+ null,
5586
+ // filesModified
5587
+ 0
5588
+ // promptNumber
5589
+ );
5590
+ broadcast("observation-created", { id, project, title });
5591
+ projectsCache.ts = 0;
5592
+ generateEmbeddingForObservation(id, title, content, Array.isArray(concepts) ? concepts : void 0).catch(() => {
5593
+ });
5594
+ res.json({ id, success: true });
5595
+ } catch (error) {
5596
+ logger.error("WORKER", "Memory save fallito", {}, error);
5597
+ res.status(500).json({ error: "Failed to save memory" });
5598
+ }
5599
+ });
5600
+ app.post("/api/retention/cleanup", (req, res) => {
5601
+ const { maxAgeDays, dryRun } = req.body || {};
5602
+ const days = parseIntSafe(String(maxAgeDays), 90, 7, 730);
5603
+ const threshold = Date.now() - days * 864e5;
5604
+ try {
5605
+ if (dryRun) {
5606
+ const obsCount = db.db.query("SELECT COUNT(*) as c FROM observations WHERE created_at_epoch < ?").get(threshold).c;
5607
+ const sumCount = db.db.query("SELECT COUNT(*) as c FROM summaries WHERE created_at_epoch < ?").get(threshold).c;
5608
+ const promptCount = db.db.query("SELECT COUNT(*) as c FROM prompts WHERE created_at_epoch < ?").get(threshold).c;
5609
+ res.json({ dryRun: true, maxAgeDays: days, wouldDelete: { observations: obsCount, summaries: sumCount, prompts: promptCount } });
5610
+ return;
5611
+ }
5612
+ const cleanup = db.db.transaction(() => {
5613
+ db.db.run("DELETE FROM observation_embeddings WHERE observation_id IN (SELECT id FROM observations WHERE created_at_epoch < ?)", [threshold]);
5614
+ const obsResult = db.db.run("DELETE FROM observations WHERE created_at_epoch < ?", [threshold]);
5615
+ const sumResult = db.db.run("DELETE FROM summaries WHERE created_at_epoch < ?", [threshold]);
5616
+ const promptResult = db.db.run("DELETE FROM prompts WHERE created_at_epoch < ?", [threshold]);
5617
+ return {
5618
+ observations: obsResult.changes,
5619
+ summaries: sumResult.changes,
5620
+ prompts: promptResult.changes
5621
+ };
5622
+ });
5623
+ const deleted = cleanup();
5624
+ projectsCache.ts = 0;
5625
+ logger.info("WORKER", `Retention cleanup: eliminati ${deleted.observations} obs, ${deleted.summaries} sum, ${deleted.prompts} prompts (> ${days}gg)`);
5626
+ res.json({ success: true, maxAgeDays: days, deleted });
5627
+ } catch (error) {
5628
+ logger.error("WORKER", "Retention cleanup fallito", { maxAgeDays: days }, error);
5629
+ res.status(500).json({ error: "Retention cleanup failed" });
5630
+ }
5631
+ });
5632
+ app.get("/api/export", (req, res) => {
5633
+ const { project, format: fmt, type, days } = req.query;
5634
+ if (project && !isValidProject(project)) {
5635
+ res.status(400).json({ error: "Invalid project name" });
5636
+ return;
5637
+ }
5638
+ const daysBack = parseIntSafe(days, 30, 1, 365);
5639
+ const threshold = Date.now() - daysBack * 864e5;
5640
+ try {
5641
+ let sql = "SELECT * FROM observations WHERE created_at_epoch > ?";
5642
+ const params = [threshold];
5643
+ if (project) {
5644
+ sql += " AND project = ?";
5645
+ params.push(project);
5646
+ }
5647
+ if (type) {
5648
+ sql += " AND type = ?";
5649
+ params.push(type);
5650
+ }
5651
+ sql += " ORDER BY created_at_epoch DESC LIMIT 1000";
5652
+ const observations = db.db.query(sql).all(...params);
5653
+ let sumSql = "SELECT * FROM summaries WHERE created_at_epoch > ?";
5654
+ const sumParams = [threshold];
5655
+ if (project) {
5656
+ sumSql += " AND project = ?";
5657
+ sumParams.push(project);
5658
+ }
5659
+ sumSql += " ORDER BY created_at_epoch DESC LIMIT 100";
5660
+ const summaries = db.db.query(sumSql).all(...sumParams);
5661
+ if (fmt === "markdown" || fmt === "md") {
5662
+ const lines = [
5663
+ `# Kiro Memory Export`,
5664
+ `> Project: ${project || "All"} | Period: ${daysBack} days | Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
5665
+ "",
5666
+ `## Observations (${observations.length})`,
5667
+ ""
5668
+ ];
5669
+ for (const obs of observations) {
5670
+ const date = new Date(obs.created_at_epoch).toISOString().split("T")[0];
5671
+ lines.push(`### [${obs.type}] ${obs.title}`);
5672
+ lines.push(`- **Date**: ${date} | **Project**: ${obs.project} | **ID**: #${obs.id}`);
5673
+ if (obs.narrative) lines.push(`- ${obs.narrative}`);
5674
+ if (obs.concepts) lines.push(`- **Concepts**: ${obs.concepts}`);
5675
+ lines.push("");
5676
+ }
5677
+ lines.push(`## Summaries (${summaries.length})`, "");
5678
+ for (const sum of summaries) {
5679
+ const date = new Date(sum.created_at_epoch).toISOString().split("T")[0];
5680
+ lines.push(`### Session ${sum.session_id} (${date})`);
5681
+ if (sum.request) lines.push(`- **Request**: ${sum.request}`);
5682
+ if (sum.completed) lines.push(`- **Completed**: ${sum.completed}`);
5683
+ if (sum.next_steps) lines.push(`- **Next steps**: ${sum.next_steps}`);
5684
+ lines.push("");
5685
+ }
5686
+ res.type("text/markdown").send(lines.join("\n"));
5687
+ } else {
5688
+ res.json({
5689
+ meta: { project: project || "all", daysBack, exportedAt: (/* @__PURE__ */ new Date()).toISOString() },
5690
+ observations,
5691
+ summaries
5692
+ });
5693
+ }
5694
+ } catch (error) {
5695
+ logger.error("WORKER", "Export fallito", { project, fmt }, error);
5696
+ res.status(500).json({ error: "Export failed" });
5697
+ }
5698
+ });
5480
5699
  app.use(express.static(__worker_dirname, {
5481
5700
  index: false,
5482
5701
  maxAge: "1h"