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,65 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { detectSleepVariant, normalizeSleep } from '../src/parse/sleep.js';
4
+ import type { SleepDatapoint } from '../src/types/hae.js';
5
+
6
+ const nonAgg: SleepDatapoint = {
7
+ startDate: '2026-01-15 22:00:00 +0000',
8
+ endDate: '2026-01-16 06:00:00 +0000',
9
+ value: 'ASLEEP_CORE',
10
+ source: 'Apple Watch',
11
+ qty: 1.5
12
+ };
13
+
14
+ const aggV2: SleepDatapoint = {
15
+ sleepStart: '2026-01-15 22:00:00 +0000',
16
+ sleepEnd: '2026-01-16 06:00:00 +0000',
17
+ core: 3.0, deep: 1.0, rem: 1.5, awake: 0.5, asleep: 5.5, inBed: 6.0,
18
+ source: 'Apple Watch'
19
+ };
20
+
21
+ const aggV1: SleepDatapoint = {
22
+ sleepStart: '2026-01-15 22:00:00 +0000',
23
+ sleepEnd: '2026-01-16 06:00:00 +0000',
24
+ inBedStart: '2026-01-15 21:50:00 +0000',
25
+ inBedEnd: '2026-01-16 06:10:00 +0000',
26
+ asleep: 5.5, inBed: 6.0,
27
+ sleepSource: "Ruben's Apple Watch",
28
+ inBedSource: 'iPhone'
29
+ };
30
+
31
+ test('detects non-aggregated variant', () => {
32
+ assert.equal(detectSleepVariant(nonAgg), 'detailed');
33
+ });
34
+
35
+ test('detects aggregated v2 variant', () => {
36
+ assert.equal(detectSleepVariant(aggV2), 'aggregated_v2');
37
+ });
38
+
39
+ test('detects aggregated v1 variant', () => {
40
+ assert.equal(detectSleepVariant(aggV1), 'aggregated_v1');
41
+ });
42
+
43
+ test('normalizes aggregated v2', () => {
44
+ const row = normalizeSleep(aggV2, 'default', 'sess-1');
45
+ assert.equal(row.schema_ver, 'aggregated_v2');
46
+ assert.equal(row.core_h, 3.0);
47
+ assert.equal(row.source, 'Apple Watch');
48
+ assert.ok(row.sleep_start);
49
+ assert.ok(row.date);
50
+ });
51
+
52
+ test('normalizes aggregated v1', () => {
53
+ const row = normalizeSleep(aggV1, 'default', 'sess-1');
54
+ assert.equal(row.schema_ver, 'aggregated_v1');
55
+ assert.equal(row.asleep_h, 5.5);
56
+ assert.ok(row.in_bed_start);
57
+ });
58
+
59
+ test('normalizes non-aggregated — stores raw in meta', () => {
60
+ const row = normalizeSleep(nonAgg, 'default', 'sess-1');
61
+ assert.equal(row.schema_ver, 'detailed');
62
+ assert.ok(row.meta);
63
+ const meta = JSON.parse(row.meta!);
64
+ assert.equal(meta.value, 'ASLEEP_CORE');
65
+ });
@@ -0,0 +1,48 @@
1
+ // tests/parse-time.test.ts
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { parseHaeTime, toIso, toDateStr } from '../src/parse/time.js';
5
+
6
+ test('parses 24-hour format', () => {
7
+ const result = parseHaeTime('2026-01-15 14:30:00 +0000');
8
+ assert.equal(toIso(result), '2026-01-15T14:30:00.000Z');
9
+ });
10
+
11
+ test('parses 12-hour uppercase PM format', () => {
12
+ const result = parseHaeTime('2026-01-15 2:30:00 PM +0000');
13
+ assert.equal(toIso(result), '2026-01-15T14:30:00.000Z');
14
+ });
15
+
16
+ test('parses 12-hour lowercase pm format', () => {
17
+ const result = parseHaeTime('2026-01-15 2:30:00 pm +0000');
18
+ assert.equal(toIso(result), '2026-01-15T14:30:00.000Z');
19
+ });
20
+
21
+ test('parses narrow non-breaking space before PM (\\u202f)', () => {
22
+ const result = parseHaeTime('2026-01-15 2:30:00\u202fPM +0000');
23
+ assert.equal(toIso(result), '2026-01-15T14:30:00.000Z');
24
+ });
25
+
26
+ test('parses narrow non-breaking space before pm (\\u202f)', () => {
27
+ const result = parseHaeTime('2026-01-15 2:30:00\u202fpm +0000');
28
+ assert.equal(toIso(result), '2026-01-15T14:30:00.000Z');
29
+ });
30
+
31
+ test('throws on unrecognised format', () => {
32
+ assert.throws(() => parseHaeTime('not-a-date'), /Failed to parse/);
33
+ });
34
+
35
+ test('toDateStr extracts YYYY-MM-DD', () => {
36
+ const result = parseHaeTime('2026-01-15 14:30:00 +0000');
37
+ assert.equal(toDateStr(result), '2026-01-15');
38
+ });
39
+
40
+ test('handles midnight correctly', () => {
41
+ const result = parseHaeTime('2026-01-15 12:00:00 AM +0000');
42
+ assert.equal(toIso(result), '2026-01-15T00:00:00.000Z');
43
+ });
44
+
45
+ test('handles noon correctly', () => {
46
+ const result = parseHaeTime('2026-01-15 12:00:00 PM +0000');
47
+ assert.equal(toIso(result), '2026-01-15T12:00:00.000Z');
48
+ });
@@ -0,0 +1,43 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { parseWorkout } from '../src/parse/workouts.js';
4
+ import type { WorkoutData } from '../src/types/hae.js';
5
+
6
+ const run: WorkoutData = {
7
+ name: 'Running',
8
+ start: '2026-01-15 07:00:00 +0000',
9
+ end: '2026-01-15 07:45:00 +0000',
10
+ activeEnergyBurned: { qty: 450, units: 'kJ' },
11
+ distance: { qty: 5.2, units: 'km' },
12
+ heartRateData: [
13
+ { date: '2026-01-15 07:01:00 +0000', qty: 140, units: 'bpm' },
14
+ { date: '2026-01-15 07:44:00 +0000', qty: 155, units: 'bpm' },
15
+ ],
16
+ };
17
+
18
+ test('parses workout basics', () => {
19
+ const row = parseWorkout(run, 'default', 'sess-1');
20
+ assert.equal(row.name, 'Running');
21
+ assert.equal(row.duration_s, 45 * 60);
22
+ assert.equal(row.calories_kj, 450);
23
+ assert.equal(row.distance, 5.2);
24
+ assert.equal(row.distance_unit, 'km');
25
+ });
26
+
27
+ test('computes avg and max heart rate', () => {
28
+ const row = parseWorkout(run, 'default', 'sess-1');
29
+ assert.equal(row.avg_hr, (140 + 155) / 2);
30
+ assert.equal(row.max_hr, 155);
31
+ });
32
+
33
+ test('stores full workout JSON in meta', () => {
34
+ const row = parseWorkout(run, 'default', 'sess-1');
35
+ assert.ok(row.meta);
36
+ const meta = JSON.parse(row.meta);
37
+ assert.equal(meta.name, 'Running');
38
+ });
39
+
40
+ test('derives date from start timestamp', () => {
41
+ const row = parseWorkout(run, 'default', 'sess-1');
42
+ assert.equal(row.date, '2026-01-15');
43
+ });
@@ -0,0 +1,27 @@
1
+ // tests/types.test.ts
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import type { HaePayload, MetricData, WorkoutData } from '../src/types/hae.js';
5
+
6
+ test('HaePayload shape is correct', () => {
7
+ const payload: HaePayload = {
8
+ data: {
9
+ metrics: [
10
+ { name: 'step_count', units: 'count', data: [{ date: '2026-01-15 10:00:00 +0000', qty: 5000 }] }
11
+ ],
12
+ workouts: []
13
+ }
14
+ };
15
+ assert.equal(payload.data.metrics![0].name, 'step_count');
16
+ });
17
+
18
+ test('SleepAnalysis non-aggregated shape', () => {
19
+ const row = { startDate: '2026-01-15 22:00:00 +0000', endDate: '2026-01-16 06:00:00 +0000', value: 'ASLEEP_CORE', source: 'Apple Watch', qty: 1.5 };
20
+ assert.ok('startDate' in row);
21
+ assert.ok('value' in row);
22
+ });
23
+
24
+ test('AggregatedSleepV2 shape', () => {
25
+ const row = { sleepStart: '2026-01-15 22:00:00 +0000', sleepEnd: '2026-01-16 06:00:00 +0000', core: 3.0, deep: 1.0, rem: 1.5, awake: 0.5, asleep: 5.5, inBed: 6.0, source: 'Apple Watch' };
26
+ assert.ok('source' in row && 'core' in row);
27
+ });
@@ -0,0 +1,46 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import AdmZip from 'adm-zip';
4
+ import { extractPayloadFromZip } from '../src/util/zip.js';
5
+
6
+ function makeZip(filename: string, content: string): Buffer {
7
+ const zip = new AdmZip();
8
+ zip.addFile(filename, Buffer.from(content, 'utf-8'));
9
+ return zip.toBuffer();
10
+ }
11
+
12
+ const validPayload = JSON.stringify({ data: { metrics: [], workouts: [] } });
13
+
14
+ test('extracts payload from zip containing HealthAutoExport-*.json', () => {
15
+ const buf = makeZip('HealthAutoExport-test-2026-01-15.json', validPayload);
16
+ const result = extractPayloadFromZip(buf);
17
+ assert.ok(result);
18
+ assert.deepEqual(result!.data.metrics, []);
19
+ });
20
+
21
+ test('returns null when zip has no HealthAutoExport-*.json', () => {
22
+ const buf = makeZip('workout.gpx', '<gpx/>');
23
+ const result = extractPayloadFromZip(buf);
24
+ assert.equal(result, null);
25
+ });
26
+
27
+ test('returns null when matching entry contains invalid JSON', () => {
28
+ const buf = makeZip('HealthAutoExport-bad.json', 'not json {{{');
29
+ const result = extractPayloadFromZip(buf);
30
+ assert.equal(result, null);
31
+ });
32
+
33
+ test('returns null when JSON lacks data field', () => {
34
+ const buf = makeZip('HealthAutoExport-nodatafield.json', JSON.stringify({ other: true }));
35
+ const result = extractPayloadFromZip(buf);
36
+ assert.equal(result, null);
37
+ });
38
+
39
+ test('ignores .gpx entries and finds the .json', () => {
40
+ const zip = new AdmZip();
41
+ zip.addFile('route.gpx', Buffer.from('<gpx/>', 'utf-8'));
42
+ zip.addFile('HealthAutoExport-2026.json', Buffer.from(validPayload, 'utf-8'));
43
+ const buf = zip.toBuffer();
44
+ const result = extractPayloadFromZip(buf);
45
+ assert.ok(result);
46
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }