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 +20 -0
- package/SKILL.md +2 -0
- package/dist/index.js +62 -3
- package/package.json +2 -2
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
|
-
|
|
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.
|
|
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": "
|
|
8
|
+
"health-agent": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "tsup --watch",
|