hae-vault 0.1.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 (160) hide show
  1. package/.env.example +7 -0
  2. package/CLAUDE.md +220 -0
  3. package/README.md +206 -0
  4. package/SKILL.md +60 -0
  5. package/dist/cli/dashboard.d.ts +3 -0
  6. package/dist/cli/dashboard.d.ts.map +1 -0
  7. package/dist/cli/dashboard.js +206 -0
  8. package/dist/cli/dashboard.js.map +1 -0
  9. package/dist/cli/import.d.ts +3 -0
  10. package/dist/cli/import.d.ts.map +1 -0
  11. package/dist/cli/import.js +78 -0
  12. package/dist/cli/import.js.map +1 -0
  13. package/dist/cli/index.d.ts +3 -0
  14. package/dist/cli/index.d.ts.map +1 -0
  15. package/dist/cli/index.js +31 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/info.d.ts +5 -0
  18. package/dist/cli/info.d.ts.map +1 -0
  19. package/dist/cli/info.js +34 -0
  20. package/dist/cli/info.js.map +1 -0
  21. package/dist/cli/metrics.d.ts +3 -0
  22. package/dist/cli/metrics.d.ts.map +1 -0
  23. package/dist/cli/metrics.js +20 -0
  24. package/dist/cli/metrics.js.map +1 -0
  25. package/dist/cli/query.d.ts +3 -0
  26. package/dist/cli/query.d.ts.map +1 -0
  27. package/dist/cli/query.js +18 -0
  28. package/dist/cli/query.js.map +1 -0
  29. package/dist/cli/serve.d.ts +3 -0
  30. package/dist/cli/serve.d.ts.map +1 -0
  31. package/dist/cli/serve.js +19 -0
  32. package/dist/cli/serve.js.map +1 -0
  33. package/dist/cli/sleep.d.ts +3 -0
  34. package/dist/cli/sleep.d.ts.map +1 -0
  35. package/dist/cli/sleep.js +19 -0
  36. package/dist/cli/sleep.js.map +1 -0
  37. package/dist/cli/summary.d.ts +3 -0
  38. package/dist/cli/summary.d.ts.map +1 -0
  39. package/dist/cli/summary.js +53 -0
  40. package/dist/cli/summary.js.map +1 -0
  41. package/dist/cli/trends.d.ts +3 -0
  42. package/dist/cli/trends.d.ts.map +1 -0
  43. package/dist/cli/trends.js +77 -0
  44. package/dist/cli/trends.js.map +1 -0
  45. package/dist/cli/watch.d.ts +12 -0
  46. package/dist/cli/watch.d.ts.map +1 -0
  47. package/dist/cli/watch.js +89 -0
  48. package/dist/cli/watch.js.map +1 -0
  49. package/dist/cli/workouts.d.ts +3 -0
  50. package/dist/cli/workouts.d.ts.map +1 -0
  51. package/dist/cli/workouts.js +19 -0
  52. package/dist/cli/workouts.js.map +1 -0
  53. package/dist/config.d.ts +9 -0
  54. package/dist/config.d.ts.map +1 -0
  55. package/dist/config.js +25 -0
  56. package/dist/config.js.map +1 -0
  57. package/dist/db/importLog.d.ts +5 -0
  58. package/dist/db/importLog.d.ts.map +1 -0
  59. package/dist/db/importLog.js +10 -0
  60. package/dist/db/importLog.js.map +1 -0
  61. package/dist/db/metrics.d.ts +4 -0
  62. package/dist/db/metrics.d.ts.map +1 -0
  63. package/dist/db/metrics.js +14 -0
  64. package/dist/db/metrics.js.map +1 -0
  65. package/dist/db/schema.d.ts +5 -0
  66. package/dist/db/schema.d.ts.map +1 -0
  67. package/dist/db/schema.js +100 -0
  68. package/dist/db/schema.js.map +1 -0
  69. package/dist/db/sleep.d.ts +4 -0
  70. package/dist/db/sleep.d.ts.map +1 -0
  71. package/dist/db/sleep.js +13 -0
  72. package/dist/db/sleep.js.map +1 -0
  73. package/dist/db/workouts.d.ts +4 -0
  74. package/dist/db/workouts.d.ts.map +1 -0
  75. package/dist/db/workouts.js +11 -0
  76. package/dist/db/workouts.js.map +1 -0
  77. package/dist/index.d.ts +3 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +5 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/parse/metrics.d.ts +17 -0
  82. package/dist/parse/metrics.d.ts.map +1 -0
  83. package/dist/parse/metrics.js +33 -0
  84. package/dist/parse/metrics.js.map +1 -0
  85. package/dist/parse/sleep.d.ts +23 -0
  86. package/dist/parse/sleep.d.ts.map +1 -0
  87. package/dist/parse/sleep.js +58 -0
  88. package/dist/parse/sleep.js.map +1 -0
  89. package/dist/parse/time.d.ts +4 -0
  90. package/dist/parse/time.d.ts.map +1 -0
  91. package/dist/parse/time.js +41 -0
  92. package/dist/parse/time.js.map +1 -0
  93. package/dist/parse/workouts.d.ts +17 -0
  94. package/dist/parse/workouts.d.ts.map +1 -0
  95. package/dist/parse/workouts.js +24 -0
  96. package/dist/parse/workouts.js.map +1 -0
  97. package/dist/server/app.d.ts +5 -0
  98. package/dist/server/app.d.ts.map +1 -0
  99. package/dist/server/app.js +39 -0
  100. package/dist/server/app.js.map +1 -0
  101. package/dist/server/ingest.d.ts +15 -0
  102. package/dist/server/ingest.d.ts.map +1 -0
  103. package/dist/server/ingest.js +41 -0
  104. package/dist/server/ingest.js.map +1 -0
  105. package/dist/types/hae.d.ts +103 -0
  106. package/dist/types/hae.d.ts.map +1 -0
  107. package/dist/types/hae.js +2 -0
  108. package/dist/types/hae.js.map +1 -0
  109. package/dist/util/zip.d.ts +3 -0
  110. package/dist/util/zip.d.ts.map +1 -0
  111. package/dist/util/zip.js +24 -0
  112. package/dist/util/zip.js.map +1 -0
  113. package/docs/COMMANDS.md +315 -0
  114. package/docs/plans/2026-02-18-hae-vault-initial-implementation.md +2015 -0
  115. package/docs/plans/2026-02-18-readme-dashboard-design.md +213 -0
  116. package/docs/plans/2026-02-18-readme-dashboard-plan.md +1306 -0
  117. package/docs/plans/2026-02-18-zip-env-watch-design.md +213 -0
  118. package/docs/plans/2026-02-18-zip-env-watch.md +966 -0
  119. package/package.json +57 -0
  120. package/src/cli/dashboard.ts +242 -0
  121. package/src/cli/import.ts +85 -0
  122. package/src/cli/index.ts +32 -0
  123. package/src/cli/info.ts +36 -0
  124. package/src/cli/metrics.ts +20 -0
  125. package/src/cli/query.ts +17 -0
  126. package/src/cli/serve.ts +18 -0
  127. package/src/cli/sleep.ts +19 -0
  128. package/src/cli/summary.ts +58 -0
  129. package/src/cli/trends.ts +103 -0
  130. package/src/cli/watch.ts +111 -0
  131. package/src/cli/workouts.ts +19 -0
  132. package/src/config.ts +28 -0
  133. package/src/db/importLog.ts +18 -0
  134. package/src/db/metrics.ts +15 -0
  135. package/src/db/schema.ts +105 -0
  136. package/src/db/sleep.ts +15 -0
  137. package/src/db/workouts.ts +13 -0
  138. package/src/index.ts +4 -0
  139. package/src/parse/metrics.ts +50 -0
  140. package/src/parse/sleep.ts +82 -0
  141. package/src/parse/time.ts +43 -0
  142. package/src/parse/workouts.ts +42 -0
  143. package/src/server/app.ts +46 -0
  144. package/src/server/ingest.ts +68 -0
  145. package/src/types/hae.ts +94 -0
  146. package/src/util/zip.ts +24 -0
  147. package/tests/cli-watch.test.ts +64 -0
  148. package/tests/db-import-log.test.ts +40 -0
  149. package/tests/db-metrics.test.ts +44 -0
  150. package/tests/db-schema.test.ts +55 -0
  151. package/tests/db-sleep.test.ts +36 -0
  152. package/tests/db-workouts.test.ts +34 -0
  153. package/tests/ingest.test.ts +99 -0
  154. package/tests/parse-metrics.test.ts +55 -0
  155. package/tests/parse-sleep.test.ts +65 -0
  156. package/tests/parse-time.test.ts +48 -0
  157. package/tests/parse-workouts.test.ts +43 -0
  158. package/tests/types.test.ts +27 -0
  159. package/tests/util-zip.test.ts +46 -0
  160. package/tsconfig.json +19 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trends.d.ts","sourceRoot":"","sources":["../../src/cli/trends.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoDpC,eAAO,MAAM,aAAa,SAkDtB,CAAC"}
@@ -0,0 +1,77 @@
1
+ import { Command } from 'commander';
2
+ import { openDb } from '../db/schema.js';
3
+ function fmt1(n) {
4
+ return n.toFixed(1);
5
+ }
6
+ function fmtInt(n) {
7
+ return Math.round(n).toLocaleString();
8
+ }
9
+ function arrow(values) {
10
+ if (values.length < 2)
11
+ return '→';
12
+ const half = Math.floor(values.length / 2);
13
+ const firstHalfAvg = values.slice(0, half).reduce((a, b) => a + b, 0) / half;
14
+ const secondHalfAvg = values.slice(half).reduce((a, b) => a + b, 0) / (values.length - half);
15
+ const delta = secondHalfAvg - firstHalfAvg;
16
+ if (Math.abs(delta) < 0.01 * Math.abs(firstHalfAvg || 1))
17
+ return '→';
18
+ return delta > 0 ? '↑' : '↓';
19
+ }
20
+ function metricTrend(db, metricName, since) {
21
+ const rows = db.prepare(`SELECT date, AVG(qty) as avg_qty FROM metrics
22
+ WHERE metric = ? AND date >= ? AND qty IS NOT NULL
23
+ GROUP BY date ORDER BY date ASC`).all(metricName, since);
24
+ if (rows.length === 0)
25
+ return null;
26
+ const values = rows.map(r => r.avg_qty);
27
+ const avg = values.reduce((a, b) => a + b, 0) / values.length;
28
+ return { values, avg, min: Math.min(...values), max: Math.max(...values) };
29
+ }
30
+ function sleepTrend(db, since) {
31
+ const rows = db.prepare(`SELECT asleep_h FROM sleep WHERE date >= ? AND asleep_h IS NOT NULL ORDER BY date ASC`).all(since);
32
+ if (rows.length === 0)
33
+ return null;
34
+ const values = rows.map(r => r.asleep_h);
35
+ const avg = values.reduce((a, b) => a + b, 0) / values.length;
36
+ return { values, avg, min: Math.min(...values), max: Math.max(...values) };
37
+ }
38
+ export const trendsCommand = new Command('trends')
39
+ .description('Multi-metric trend analysis with averages, ranges, and direction arrows')
40
+ .option('--days <n>', 'Days of history', '7')
41
+ .option('--json', 'Output raw JSON')
42
+ .action((opts) => {
43
+ const db = openDb();
44
+ const days = parseInt(opts.days, 10);
45
+ const since = new Date();
46
+ since.setDate(since.getDate() - days);
47
+ const sinceStr = since.toISOString().slice(0, 10);
48
+ const steps = metricTrend(db, 'step_count', sinceStr);
49
+ const restingHR = metricTrend(db, 'resting_heart_rate', sinceStr);
50
+ const hrv = metricTrend(db, 'heart_rate_variability_sdnn', sinceStr);
51
+ const activeCal = metricTrend(db, 'active_energy_burned', sinceStr);
52
+ const sleep = sleepTrend(db, sinceStr);
53
+ if (opts.json) {
54
+ console.log(JSON.stringify({ days, steps, restingHR, hrv, activeCal, sleep }, null, 2));
55
+ return;
56
+ }
57
+ const lines = [];
58
+ lines.push(`📊 ${days}-Day Trends`);
59
+ lines.push('');
60
+ function row(emoji, label, data, unit, round) {
61
+ if (!data)
62
+ return;
63
+ const dir = arrow(data.values);
64
+ const fmt = round ? fmtInt : fmt1;
65
+ lines.push(`${emoji} ${label}: ${fmt(data.avg)} avg (${fmt(data.min)}–${fmt(data.max)}) ${dir}`);
66
+ }
67
+ row('👟', 'Steps', steps, '', true);
68
+ row('💓', 'Resting HR', restingHR, 'bpm', true);
69
+ row('🧠', 'HRV', hrv, 'ms', true);
70
+ row('😴', 'Sleep', sleep, 'h', false);
71
+ row('🔥', 'Active Cal', activeCal, 'kcal', true);
72
+ if (lines.length === 2) {
73
+ lines.push(' No trend data available');
74
+ }
75
+ console.log(lines.join('\n'));
76
+ });
77
+ //# sourceMappingURL=trends.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trends.js","sourceRoot":"","sources":["../../src/cli/trends.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { Command } from 'commander';
2
+ import type Database from 'better-sqlite3';
3
+ export interface TickResult {
4
+ tick: string;
5
+ dir: string;
6
+ found: number;
7
+ imported: number;
8
+ skipped: number;
9
+ }
10
+ export declare function tick(db: Database.Database, watchDir: string, target: string): TickResult;
11
+ export declare const watchCommand: Command;
12
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/cli/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AA0B3C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAmDxF;AAED,eAAO,MAAM,YAAY,SAmBrB,CAAC"}
@@ -0,0 +1,89 @@
1
+ import { Command } from 'commander';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { join } from 'node:path';
5
+ import { openDb } from '../db/schema.js';
6
+ import { ingest } from '../server/ingest.js';
7
+ import { hasBeenImported, logImport } from '../db/importLog.js';
8
+ import { extractPayloadFromZip } from '../util/zip.js';
9
+ import { config } from '../config.js';
10
+ const HAE_PATTERN = /^HealthAutoExport.*\.(zip|json)$/i;
11
+ function sha256(buf) {
12
+ return createHash('sha256').update(buf).digest('hex');
13
+ }
14
+ function loadBuf(buf, filename) {
15
+ if (filename.toLowerCase().endsWith('.zip')) {
16
+ return extractPayloadFromZip(buf);
17
+ }
18
+ try {
19
+ const p = JSON.parse(buf.toString('utf-8'));
20
+ return p?.data ? p : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ export function tick(db, watchDir, target) {
27
+ const now = new Date().toISOString();
28
+ let found = 0, imported = 0, skipped = 0;
29
+ let files;
30
+ try {
31
+ files = readdirSync(watchDir).filter(f => HAE_PATTERN.test(f));
32
+ }
33
+ catch (err) {
34
+ console.error(JSON.stringify({ error: `Cannot read watch dir: ${String(err)}` }));
35
+ return { tick: now, dir: watchDir, found, imported, skipped };
36
+ }
37
+ found = files.length;
38
+ for (const filename of files) {
39
+ const filepath = join(watchDir, filename);
40
+ let buf;
41
+ try {
42
+ buf = readFileSync(filepath);
43
+ }
44
+ catch {
45
+ skipped++;
46
+ continue;
47
+ }
48
+ const hash = sha256(buf);
49
+ if (hasBeenImported(db, hash)) {
50
+ skipped++;
51
+ continue;
52
+ }
53
+ const payload = loadBuf(buf, filename);
54
+ if (!payload) {
55
+ skipped++;
56
+ continue;
57
+ }
58
+ const result = ingest(db, payload, {
59
+ target,
60
+ sessionId: null,
61
+ automationName: 'watch',
62
+ automationPeriod: 'manual',
63
+ });
64
+ logImport(db, filepath, hash, result);
65
+ console.log(JSON.stringify({ imported: filename, target, ...result }));
66
+ imported++;
67
+ }
68
+ const summary = { tick: now, dir: watchDir, found, imported, skipped };
69
+ console.log(JSON.stringify(summary));
70
+ return summary;
71
+ }
72
+ export const watchCommand = new Command('watch')
73
+ .description('Poll a directory for new HAE exports and auto-import them')
74
+ .option('--dir <path>', 'Directory to watch', config.watchDir)
75
+ .option('--interval <seconds>', 'Poll interval in seconds', String(config.watchInterval))
76
+ .option('--target <name>', 'Target name', config.target)
77
+ .action((opts) => {
78
+ const watchDir = opts.dir;
79
+ if (!watchDir) {
80
+ console.error(JSON.stringify({ error: 'Watch directory required: use --dir or set HVAULT_WATCH_DIR' }));
81
+ process.exit(1);
82
+ }
83
+ const intervalMs = Number(opts.interval) * 1000;
84
+ const db = openDb(config.dbPath);
85
+ console.log(JSON.stringify({ watching: watchDir, intervalSeconds: Number(opts.interval), target: opts.target }));
86
+ tick(db, watchDir, opts.target);
87
+ setInterval(() => tick(db, watchDir, opts.target), intervalMs);
88
+ });
89
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/cli/watch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const workoutsCommand: Command;
3
+ //# sourceMappingURL=workouts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workouts.d.ts","sourceRoot":"","sources":["../../src/cli/workouts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,eAAO,MAAM,eAAe,SAexB,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { Command } from 'commander';
2
+ import { openDb } from '../db/schema.js';
3
+ export const workoutsCommand = new Command('workouts')
4
+ .description('Query workouts')
5
+ .option('--days <n>', 'Last N days', '30')
6
+ .option('--pretty', 'Pretty-print JSON', false)
7
+ .action((opts) => {
8
+ const db = openDb();
9
+ const since = new Date();
10
+ since.setDate(since.getDate() - parseInt(opts.days, 10));
11
+ const rows = db.prepare(`
12
+ SELECT ts, date, name, duration_s, calories_kj, distance, distance_unit, avg_hr, max_hr, target
13
+ FROM workouts
14
+ WHERE date >= ?
15
+ ORDER BY ts ASC
16
+ `).all(since.toISOString().slice(0, 10));
17
+ console.log(opts.pretty ? JSON.stringify(rows, null, 2) : JSON.stringify(rows));
18
+ });
19
+ //# sourceMappingURL=workouts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workouts.js","sourceRoot":"","sources":["../../src/cli/workouts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC;KACnD,WAAW,CAAC,gBAAgB,CAAC;KAC7B,MAAM,CAAC,YAAY,EAAE,aAAa,EAAE,IAAI,CAAC;KACzC,MAAM,CAAC,UAAU,EAAE,mBAAmB,EAAE,KAAK,CAAC;KAC9C,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;IACf,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;IACpB,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;IACzB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;KAKvB,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACzC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAClF,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ export declare const config: {
2
+ readonly dbPath: string;
3
+ readonly port: number;
4
+ readonly token: string | undefined;
5
+ readonly watchDir: string | undefined;
6
+ readonly watchInterval: number;
7
+ readonly target: string;
8
+ };
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,MAAM;;;;;;;CAOT,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,25 @@
1
+ import { config as dotenvLoad } from 'dotenv';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ function expandTilde(p) {
6
+ if (p === '~' || p.startsWith('~/')) {
7
+ return homedir() + p.slice(1);
8
+ }
9
+ return p;
10
+ }
11
+ // Load .env: HVAULT_ENV_FILE env var overrides; fallback to CWD .env
12
+ const envFile = process.env.HVAULT_ENV_FILE ?? join(process.cwd(), '.env');
13
+ if (existsSync(envFile)) {
14
+ dotenvLoad({ path: envFile });
15
+ }
16
+ const DEFAULT_DB_PATH = join(homedir(), '.hae-vault', 'health.db');
17
+ export const config = {
18
+ dbPath: expandTilde(process.env.HVAULT_DB_PATH ?? DEFAULT_DB_PATH),
19
+ port: Number(process.env.HVAULT_PORT ?? 4242),
20
+ token: process.env.HVAULT_TOKEN,
21
+ watchDir: process.env.HVAULT_WATCH_DIR ? expandTilde(process.env.HVAULT_WATCH_DIR) : undefined,
22
+ watchInterval: Number(process.env.HVAULT_WATCH_INTERVAL ?? 60),
23
+ target: process.env.HVAULT_TARGET ?? 'default',
24
+ };
25
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,SAAS,WAAW,CAAC,CAAS;IAC5B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,OAAO,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,qEAAqE;AACrE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;AAC3E,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;AAEnE,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,MAAM,EAAS,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,eAAe,CAAC;IACzE,IAAI,EAAW,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;IACtD,KAAK,EAAU,OAAO,CAAC,GAAG,CAAC,YAAY;IACvC,QAAQ,EAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,SAAS;IACnG,aAAa,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC;IAC9D,MAAM,EAAS,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,SAAS;CAC7C,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { IngestResult } from '../server/ingest.js';
3
+ export declare function hasBeenImported(db: Database.Database, hash: string): boolean;
4
+ export declare function logImport(db: Database.Database, filename: string, hash: string, result: IngestResult): void;
5
+ //# sourceMappingURL=importLog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"importLog.d.ts","sourceRoot":"","sources":["../../src/db/importLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5E;AAED,wBAAgB,SAAS,CACvB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,GACnB,IAAI,CAKN"}
@@ -0,0 +1,10 @@
1
+ export function hasBeenImported(db, hash) {
2
+ return db.prepare('SELECT id FROM import_log WHERE file_hash = ?').get(hash) !== undefined;
3
+ }
4
+ export function logImport(db, filename, hash, result) {
5
+ db.prepare(`
6
+ INSERT OR IGNORE INTO import_log (filename, file_hash, imported_at, metrics_added, sleep_added, workouts_added)
7
+ VALUES (?, ?, ?, ?, ?, ?)
8
+ `).run(filename, hash, new Date().toISOString(), result.metricsAdded, result.sleepAdded, result.workoutsAdded);
9
+ }
10
+ //# sourceMappingURL=importLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"importLog.js","sourceRoot":"","sources":["../../src/db/importLog.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,eAAe,CAAC,EAAqB,EAAE,IAAY;IACjE,OAAO,EAAE,CAAC,OAAO,CAAC,+CAA+C,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;AAC7F,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,EAAqB,EACrB,QAAgB,EAChB,IAAY,EACZ,MAAoB;IAEpB,EAAE,CAAC,OAAO,CAAC;;;GAGV,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;AACjH,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { NormalizedMetric } from '../parse/metrics.js';
3
+ export declare function upsertMetrics(db: Database.Database, rows: NormalizedMetric[]): void;
4
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/db/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAWnF"}
@@ -0,0 +1,14 @@
1
+ export function upsertMetrics(db, rows) {
2
+ const stmt = db.prepare(`
3
+ INSERT OR REPLACE INTO metrics
4
+ (ts, date, metric, qty, min, avg, max, units, source, target, meta, session_id)
5
+ VALUES
6
+ (@ts, @date, @metric, @qty, @min, @avg, @max, @units, @source, @target, @meta, @session_id)
7
+ `);
8
+ const insertMany = db.transaction((rows) => {
9
+ for (const row of rows)
10
+ stmt.run(row);
11
+ });
12
+ insertMany(rows);
13
+ }
14
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/db/metrics.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,aAAa,CAAC,EAAqB,EAAE,IAAwB;IAC3E,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;GAKvB,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,IAAwB,EAAE,EAAE;QAC7D,KAAK,MAAM,GAAG,IAAI,IAAI;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IACH,UAAU,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import Database from 'better-sqlite3';
2
+ export declare const DEFAULT_DB_PATH: string;
3
+ export declare function openDb(dbPath?: string): Database.Database;
4
+ export declare function closeDb(db: Database.Database): void;
5
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAKtC,eAAO,MAAM,eAAe,QAA6C,CAAC;AAE1E,wBAAgB,MAAM,CAAC,MAAM,SAAkB,GAAG,QAAQ,CAAC,QAAQ,CA6FlE;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAEnD"}
@@ -0,0 +1,100 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ export const DEFAULT_DB_PATH = join(homedir(), '.hae-vault', 'health.db');
6
+ export function openDb(dbPath = DEFAULT_DB_PATH) {
7
+ mkdirSync(dirname(dbPath), { recursive: true });
8
+ const db = new Database(dbPath);
9
+ db.pragma('journal_mode = WAL');
10
+ db.pragma('foreign_keys = ON');
11
+ db.exec(`
12
+ CREATE TABLE IF NOT EXISTS metrics (
13
+ id INTEGER PRIMARY KEY,
14
+ ts TEXT NOT NULL,
15
+ date TEXT NOT NULL,
16
+ metric TEXT NOT NULL,
17
+ qty REAL,
18
+ min REAL,
19
+ avg REAL,
20
+ max REAL,
21
+ units TEXT,
22
+ source TEXT,
23
+ target TEXT,
24
+ meta TEXT,
25
+ session_id TEXT,
26
+ UNIQUE(ts, metric, source, target)
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_metrics_date ON metrics(date);
30
+ CREATE INDEX IF NOT EXISTS idx_metrics_metric ON metrics(metric);
31
+
32
+ CREATE TABLE IF NOT EXISTS sleep (
33
+ id INTEGER PRIMARY KEY,
34
+ date TEXT NOT NULL,
35
+ sleep_start TEXT,
36
+ sleep_end TEXT,
37
+ in_bed_start TEXT,
38
+ in_bed_end TEXT,
39
+ core_h REAL,
40
+ deep_h REAL,
41
+ rem_h REAL,
42
+ awake_h REAL,
43
+ asleep_h REAL,
44
+ in_bed_h REAL,
45
+ schema_ver TEXT,
46
+ source TEXT,
47
+ target TEXT,
48
+ meta TEXT,
49
+ session_id TEXT,
50
+ UNIQUE(date, source, target)
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_sleep_date ON sleep(date);
54
+
55
+ CREATE TABLE IF NOT EXISTS workouts (
56
+ id INTEGER PRIMARY KEY,
57
+ ts TEXT NOT NULL,
58
+ date TEXT NOT NULL,
59
+ name TEXT NOT NULL,
60
+ duration_s INTEGER,
61
+ calories_kj REAL,
62
+ distance REAL,
63
+ distance_unit TEXT,
64
+ avg_hr REAL,
65
+ max_hr REAL,
66
+ target TEXT,
67
+ meta TEXT,
68
+ session_id TEXT,
69
+ UNIQUE(ts, name, target)
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_workouts_date ON workouts(date);
73
+
74
+ CREATE TABLE IF NOT EXISTS sync_log (
75
+ id INTEGER PRIMARY KEY,
76
+ received_at TEXT NOT NULL,
77
+ target TEXT,
78
+ session_id TEXT,
79
+ metrics_count INTEGER,
80
+ workouts_count INTEGER,
81
+ automation_name TEXT,
82
+ automation_period TEXT
83
+ );
84
+
85
+ CREATE TABLE IF NOT EXISTS import_log (
86
+ id INTEGER PRIMARY KEY,
87
+ filename TEXT NOT NULL,
88
+ file_hash TEXT NOT NULL UNIQUE,
89
+ imported_at TEXT NOT NULL,
90
+ metrics_added INTEGER,
91
+ sleep_added INTEGER,
92
+ workouts_added INTEGER
93
+ );
94
+ `);
95
+ return db;
96
+ }
97
+ export function closeDb(db) {
98
+ db.close();
99
+ }
100
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;AAE1E,MAAM,UAAU,MAAM,CAAC,MAAM,GAAG,eAAe;IAC7C,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAEhC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAE/B,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFP,CAAC,CAAC;IAEH,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,EAAqB;IAC3C,EAAE,CAAC,KAAK,EAAE,CAAC;AACb,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { NormalizedSleep } from '../parse/sleep.js';
3
+ export declare function upsertSleep(db: Database.Database, row: NormalizedSleep): void;
4
+ //# sourceMappingURL=sleep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/db/sleep.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,eAAe,GAAG,IAAI,CAW7E"}
@@ -0,0 +1,13 @@
1
+ export function upsertSleep(db, row) {
2
+ db.prepare(`
3
+ INSERT OR REPLACE INTO sleep
4
+ (date, sleep_start, sleep_end, in_bed_start, in_bed_end,
5
+ core_h, deep_h, rem_h, awake_h, asleep_h, in_bed_h,
6
+ schema_ver, source, target, meta, session_id)
7
+ VALUES
8
+ (@date, @sleep_start, @sleep_end, @in_bed_start, @in_bed_end,
9
+ @core_h, @deep_h, @rem_h, @awake_h, @asleep_h, @in_bed_h,
10
+ @schema_ver, @source, @target, @meta, @session_id)
11
+ `).run(row);
12
+ }
13
+ //# sourceMappingURL=sleep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../src/db/sleep.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,WAAW,CAAC,EAAqB,EAAE,GAAoB;IACrE,EAAE,CAAC,OAAO,CAAC;;;;;;;;;GASV,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACd,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { NormalizedWorkout } from '../parse/workouts.js';
3
+ export declare function upsertWorkout(db: Database.Database, row: NormalizedWorkout): void;
4
+ //# sourceMappingURL=workouts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workouts.d.ts","sourceRoot":"","sources":["../../src/db/workouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,iBAAiB,GAAG,IAAI,CASjF"}
@@ -0,0 +1,11 @@
1
+ export function upsertWorkout(db, row) {
2
+ db.prepare(`
3
+ INSERT OR REPLACE INTO workouts
4
+ (ts, date, name, duration_s, calories_kj, distance, distance_unit,
5
+ avg_hr, max_hr, target, meta, session_id)
6
+ VALUES
7
+ (@ts, @date, @name, @duration_s, @calories_kj, @distance, @distance_unit,
8
+ @avg_hr, @max_hr, @target, @meta, @session_id)
9
+ `).run(row);
10
+ }
11
+ //# sourceMappingURL=workouts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workouts.js","sourceRoot":"","sources":["../../src/db/workouts.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,aAAa,CAAC,EAAqB,EAAE,GAAsB;IACzE,EAAE,CAAC,OAAO,CAAC;;;;;;;GAOV,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACd,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import './config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import './config.js'; // load dotenv before any command runs
3
+ import { program } from './cli/index.js';
4
+ program.parse();
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,aAAa,CAAC,CAAE,sCAAsC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { MetricData } from '../types/hae.js';
2
+ export interface NormalizedMetric {
3
+ ts: string;
4
+ date: string;
5
+ metric: string;
6
+ qty: number | null;
7
+ min: number | null;
8
+ avg: number | null;
9
+ max: number | null;
10
+ units: string;
11
+ source: string | null;
12
+ target: string;
13
+ meta: string | null;
14
+ session_id: string | null;
15
+ }
16
+ export declare function parseMetric(m: MetricData, target: string, sessionId: string | null): NormalizedMetric[];
17
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/parse/metrics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,iBAAiB,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,gBAAgB,EAAE,CA+BvG"}
@@ -0,0 +1,33 @@
1
+ import { parseHaeTime, toIso, toDateStr } from './time.js';
2
+ export function parseMetric(m, target, sessionId) {
3
+ if (m.name === 'sleep_analysis')
4
+ return [];
5
+ return m.data.map((dp) => {
6
+ const d = parseHaeTime(dp.date);
7
+ const isHeartRate = dp.Min !== undefined || dp.Avg !== undefined || dp.Max !== undefined;
8
+ const isBloodPressure = dp.systolic !== undefined || dp.diastolic !== undefined;
9
+ let qty = null;
10
+ let min = null;
11
+ let avg = null;
12
+ let max = null;
13
+ let meta = null;
14
+ if (isHeartRate) {
15
+ min = dp.Min ?? null;
16
+ avg = dp.Avg ?? null;
17
+ max = dp.Max ?? null;
18
+ }
19
+ else if (isBloodPressure) {
20
+ meta = JSON.stringify({ systolic: dp.systolic, diastolic: dp.diastolic });
21
+ }
22
+ else {
23
+ qty = dp.qty ?? null;
24
+ }
25
+ return {
26
+ ts: toIso(d), date: toDateStr(d),
27
+ metric: m.name, qty, min, avg, max,
28
+ units: m.units, source: dp.source ?? null,
29
+ target, meta, session_id: sessionId,
30
+ };
31
+ });
32
+ }
33
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/parse/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAkB3D,MAAM,UAAU,WAAW,CAAC,CAAa,EAAE,MAAc,EAAE,SAAwB;IACjF,IAAI,CAAC,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,EAAE,CAAC;IAE3C,OAAQ,CAAC,CAAC,IAAuB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAC3C,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,WAAW,GAAG,EAAE,CAAC,GAAG,KAAK,SAAS,IAAI,EAAE,CAAC,GAAG,KAAK,SAAS,IAAI,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC;QACzF,MAAM,eAAe,GAAG,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,CAAC;QAEhF,IAAI,GAAG,GAAkB,IAAI,CAAC;QAC9B,IAAI,GAAG,GAAkB,IAAI,CAAC;QAC9B,IAAI,GAAG,GAAkB,IAAI,CAAC;QAC9B,IAAI,GAAG,GAAkB,IAAI,CAAC;QAC9B,IAAI,IAAI,GAAkB,IAAI,CAAC;QAE/B,IAAI,WAAW,EAAE,CAAC;YAChB,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC;YACrB,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC;YACrB,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC;QACvB,CAAC;aAAM,IAAI,eAAe,EAAE,CAAC;YAC3B,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC;QACvB,CAAC;QAED,OAAO;YACL,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;YAChC,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;YAClC,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,IAAI,IAAI;YACzC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS;SACpC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { SleepDatapoint } from '../types/hae.js';
2
+ export type SleepVariant = 'detailed' | 'aggregated_v2' | 'aggregated_v1';
3
+ export interface NormalizedSleep {
4
+ date: string;
5
+ sleep_start: string | null;
6
+ sleep_end: string | null;
7
+ in_bed_start: string | null;
8
+ in_bed_end: string | null;
9
+ core_h: number | null;
10
+ deep_h: number | null;
11
+ rem_h: number | null;
12
+ awake_h: number | null;
13
+ asleep_h: number | null;
14
+ in_bed_h: number | null;
15
+ schema_ver: SleepVariant;
16
+ source: string | null;
17
+ target: string;
18
+ meta: string | null;
19
+ session_id: string | null;
20
+ }
21
+ export declare function detectSleepVariant(dp: SleepDatapoint): SleepVariant;
22
+ export declare function normalizeSleep(dp: SleepDatapoint, target: string, sessionId: string | null): NormalizedSleep;
23
+ //# sourceMappingURL=sleep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/parse/sleep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAA0D,MAAM,iBAAiB,CAAC;AAE9G,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,eAAe,GAAG,eAAe,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,YAAY,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,GAAG,YAAY,CAInE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,CAmD5G"}