health-agent 0.1.1 → 0.1.3

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.
package/README.md CHANGED
@@ -12,6 +12,12 @@ npm install -g health-agent # installs the `health-agent` command
12
12
  pnpm install -g health-agent
13
13
  ```
14
14
 
15
+ As a Claude Code / agent skill (teaches the agent how to drive the CLI):
16
+
17
+ ```bash
18
+ npx skills add code-vagabond/health-agent
19
+ ```
20
+
15
21
  As a Claude Code plugin (bundles the skill so Claude knows how to drive the CLI):
16
22
 
17
23
  ```bash
@@ -64,6 +70,20 @@ health-agent raw 'users/me/dataTypes/steps/dataPoints' # escape hatch
64
70
 
65
71
  JSON goes to stdout, diagnostics to stderr — pipe to `jq` freely.
66
72
 
73
+ ## Use cases — let the agent cross-reference your life
74
+
75
+ The CLI just emits JSON. The interesting part is handing that to an agent (Claude Code) that *also* has access to your other data, so it can correlate signals you'd never line up by hand. A few that work today:
76
+
77
+ - **Sedentary reminder** — flag when heart rate has stayed at a flat resting level for hours with no active spike, and ping you to move.
78
+ - **"How did my week actually go?"** — the agent pulls `steps`, `sleep`, and `exercise`, then cross-references your calendar (Google Calendar / Outlook MCP) to explain the dips: *"Your sleep dropped to 5h on the three nights before the investor call, and step count cratered the day after."*
79
+ - **Sleep vs. focus** — combine `sleep` with your ScreenTimerAI activity log (or any time-tracking export). Ask *"do I ship more on days after 7h+ of sleep?"* and let the agent regress one against the other.
80
+ - **Workout log → training insight** — `get exercise` carries the full Hevy set/rep/weight log in `.exercise.notes`. The agent can chart progressive overload, flag stalled lifts, or suggest next-session weights — no separate fitness app needed.
81
+ - **Stress / health journal correlation** — pair `health_metrics` (resting HR, body metrics) with your personal journal entries. *"Resting HR was elevated every day you logged feeling 'overwhelmed' — and those clustered around two specific deadlines."*
82
+ - **Standup / weekly review** — at the end of the week, have the agent fold health into your work recap: a one-paragraph "you" report that treats sleep and movement as first-class inputs alongside commits and meetings.
83
+ - **Recovery-aware planning** — before the agent plans tomorrow, it checks last night's `sleep` and today's `steps`. Slept badly? It front-loads lighter tasks and pushes the deep-work block.
84
+
85
+ Because everything is JSON on stdout, you can also wire it into a cron/`/schedule` job that drops a daily digest into a file, a Slack message, or a note — without the agent ever touching your Google login.
86
+
67
87
  ## Data model notes
68
88
 
69
89
  - **Reconciled stream** (`--reconcile` / `-w`): deduped data matching what the Fitbit app shows. The plain list returns raw per-source data points.
package/SKILL.md CHANGED
@@ -86,6 +86,8 @@ At the browser screen, tell the user to:
86
86
  health-agent steps
87
87
  health-agent sleep
88
88
  health-agent exercise
89
+ health-agent resting-hr # daily RESTING heart rate (one value/day, calc'd from sleep). Aliases: resting-heart-rate, rhr
90
+ health-agent bpm # raw live heart-rate samples (latest spot reading, NOT resting). Aliases: heart-rate, hr
89
91
 
90
92
  # Any data type (kebab-case id): steps, sleep, exercise, body-fat, ...
91
93
  health-agent get steps --wearables --limit 50
package/dist/index.js CHANGED
@@ -278,6 +278,46 @@ async function rawGet(pathAndQuery) {
278
278
  return authedGet(pathAndQuery.replace(/^\/+/, ""));
279
279
  }
280
280
 
281
+ // src/localize.ts
282
+ function parseOffsetSeconds(raw) {
283
+ if (typeof raw !== "string") return null;
284
+ const m = /^(-?\d+)s$/.exec(raw.trim());
285
+ return m ? parseInt(m[1], 10) : null;
286
+ }
287
+ function toLocalIso(utc, offsetSeconds) {
288
+ const ms = Date.parse(utc);
289
+ if (Number.isNaN(ms)) return null;
290
+ const shifted = new Date(ms + offsetSeconds * 1e3).toISOString().replace(/(\.\d+)?Z$/, "");
291
+ const sign = offsetSeconds < 0 ? "-" : "+";
292
+ const abs = Math.abs(offsetSeconds);
293
+ const hh = String(Math.floor(abs / 3600)).padStart(2, "0");
294
+ const mm = String(Math.floor(abs % 3600 / 60)).padStart(2, "0");
295
+ return `${shifted}${sign}${hh}:${mm}`;
296
+ }
297
+ function localizeTimes(node) {
298
+ if (Array.isArray(node)) {
299
+ for (const item of node) localizeTimes(item);
300
+ return node;
301
+ }
302
+ if (node && typeof node === "object") {
303
+ const obj = node;
304
+ for (const key of Object.keys(obj)) {
305
+ const m = /^(.*)Time$/.exec(key);
306
+ if (m && typeof obj[key] === "string") {
307
+ const base = m[1];
308
+ const offset = parseOffsetSeconds(obj[`${base}UtcOffset`]);
309
+ if (offset !== null) {
310
+ const local = toLocalIso(obj[key], offset);
311
+ if (local) obj[`${base}TimeLocal`] = local;
312
+ }
313
+ }
314
+ localizeTimes(obj[key]);
315
+ }
316
+ return node;
317
+ }
318
+ return node;
319
+ }
320
+
281
321
  // src/commands/data.ts
282
322
  function toOpts(args) {
283
323
  const reconcile = args.reconcile || args.wearables || !!args.source;
@@ -294,7 +334,8 @@ function emit(data) {
294
334
  }
295
335
  async function getData(dataType, args) {
296
336
  try {
297
- emit(await getDataPoints(dataType, toOpts(args)));
337
+ const data = await getDataPoints(dataType, toOpts(args));
338
+ emit(args.local ? localizeTimes(data) : data);
298
339
  } catch (e) {
299
340
  console.error(`\u274C ${e.message}`);
300
341
  process.exit(1);
@@ -329,7 +370,11 @@ var dataOpts = (y) => y.option("reconcile", {
329
370
  alias: "f",
330
371
  describe: `Google Health filter, e.g. 'sleep.interval.civil_end_time >= "2026-03-03"'`,
331
372
  type: "string"
332
- }).option("limit", { alias: "n", describe: "Max data points (pageSize)", type: "number" }).option("page-token", { describe: "Pagination token from a previous response", type: "string" });
373
+ }).option("limit", { alias: "n", describe: "Max data points (pageSize)", type: "number" }).option("page-token", { describe: "Pagination token from a previous response", type: "string" }).option("local", {
374
+ describe: "Add <field>Local wall-clock times (UTC + the record's UtcOffset) next to each UTC timestamp",
375
+ type: "boolean",
376
+ default: false
377
+ });
333
378
  yargs(hideBin(process.argv)).scriptName("health-agent").usage("$0 <command> [options]").command(
334
379
  "auth:setup",
335
380
  "Store your Google Cloud OAuth client (Desktop app type)",
@@ -350,7 +395,8 @@ yargs(hideBin(process.argv)).scriptName("health-agent").usage("$0 <command> [opt
350
395
  source: a.source,
351
396
  filter: a.filter,
352
397
  limit: a.limit,
353
- pageToken: a.pageToken
398
+ pageToken: a.pageToken,
399
+ local: a.local
354
400
  })
355
401
  ).command(
356
402
  "steps",
@@ -367,6 +413,19 @@ yargs(hideBin(process.argv)).scriptName("health-agent").usage("$0 <command> [opt
367
413
  "Fetch exercise/activity sessions (reconciled, Fitbit/Pixel)",
368
414
  dataOpts,
369
415
  (a) => getData("exercise", { ...a, wearables: a.wearables || !a.reconcile && !a.source })
416
+ ).command(
417
+ ["resting-hr", "resting-heart-rate", "rhr"],
418
+ "Fetch daily resting heart rate (one value per day, calculated from sleep)",
419
+ dataOpts,
420
+ (a) => getData("daily-resting-heart-rate", {
421
+ ...a,
422
+ wearables: a.wearables || !a.reconcile && !a.source
423
+ })
424
+ ).command(
425
+ ["heart-rate", "bpm", "hr"],
426
+ "Fetch raw heart-rate samples (latest live BPM readings, not resting HR)",
427
+ dataOpts,
428
+ (a) => getData("heart-rate", { ...a, wearables: a.wearables || !a.reconcile && !a.source })
370
429
  ).command("profile", "Fetch your user profile", {}, () => getProfile()).command(
371
430
  "raw <path>",
372
431
  'GET any path under the v4 base, e.g. "users/me/dataTypes/steps/dataPoints"',
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "health-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI + Claude skill to authenticate with the Google Health API and pull your Fitbit / Pixel health data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
- "health-agent": "./dist/index.js"
8
+ "health-agent": "dist/index.js"
9
9
  },
10
10
  "scripts": {
11
11
  "dev": "tsup --watch",