| name | lifetract |
| description | Query personal life-tracking data: Samsung Health (sleep, steps, heart rate, stress, exercise, weight) + aTimeLogger (18 time categories) + Home Assistant REST (live sensors via ha.junghanacs.com). All records use Denote IDs (YYYYMMDDTHHMMSS) for cross-referencing with denotecli. DB mode (lifetract.db) for instant queries, individual Samsung commands have CSV fallback, and today's DB gaps can use Home Assistant. |
lifetract — Life Tracking CLI
Query and analyze personal health and time-tracking data.
All records carry Denote IDs (YYYYMMDDTHHMMSS) — same axis as denotecli.
Binary is bundled in the skill directory. Invoke via {baseDir}/lifetract.
All output is JSON.
Why This Exists (not sqlite3/pandas)
Do NOT open lifetract.db or CSV files directly with Python/sqlite3/pandas.
- Denote ID mapping — Raw CSVs use Samsung's epoch timestamps. The CLI converts them to
YYYYMMDDTHHMMSS Denote IDs for cross-referencing with denotecli/gitcli.
- Multi-source join — Sleep, heart rate, steps, stress, exercise, time tracking from different tables/sources, unified per-day. Manual SQL gets this wrong.
- JSON for agents — Structured output ready for reasoning. No parsing needed.
When to Use
- "오늘 몸 상태" →
lifetract today
- "어제 뭐 했지?" →
lifetract read 2026-03-09
- "최근 수면 패턴" →
lifetract sleep --days 30 --summary
- "이번 주 시간 사용" →
lifetract time --days 7
- "운동 기록" →
lifetract exercise --days 30
- "30일 추이" →
lifetract timeline --days 30
Quick Start
lifetract status
lifetract import --exec
lifetract today
lifetract read 2025-10-04
lifetract timeline --days 30
Architecture
lifetract.db 있음 → 모든 명령: DB 쿼리 (~90ms) → JSON
lifetract.db 없음 → sleep/steps/heart/stress/exercise: CSV 파싱 (~300ms) → JSON
→ time/timeline/today/read: exit 1
lifetract import --exec 실행 후 모든 조회가 DB 모드
- DB 없으면 개별 Samsung 명령(
sleep/steps/heart/stress/exercise)만 CSV 직접 파싱
time과 시간축을 합치는 timeline/today/read는 DB가 없으면 exit 1 — aTimeLogger를 못 읽은 구멍을 0으로 내보내지 않는다
Commands
status — 데이터 소스 확인
lifetract status
{
"samsung_health": {"path": "...", "available": true, "csv_count": 78},
"atimelogger": {"path": "...", "available": true, "size_mb": 5.0},
"database": {
"path": "...", "available": true, "size_mb": 37.3, "mode": "db",
"last_time_block": "2026-07-13", "last_sleep": "2026-07-13", "last_steps": "2026-07-13",
"stale_days": 1, "freshness_checked": true, "warnings": []
}
}
last_* / stale_days / warnings 가 이 명령의 요점이다. Samsung export 는 사람이
폰에서 손으로 내보내야 흐른다 — 안 넣어주면 조용히 낡는다. 숫자를 저널에 사실로 박기
전에 여기부터 봐라 (§시간 계약 4항).
warnings: [] 는 freshness_checked: true 일 때만 "성하다"는 뜻이다. DB 가 없거나
검사 자체가 실패하면 freshness_checked: false 이고, 그때의 빈 목록은 "이상 없음"이 아니라
"보지 않았음" 이다. 검사하지 않은 것이 합격으로 보이면 그건 검사가 아니다.
import — DB 생성
lifetract import
lifetract import --exec
202,479 rows, ~38MB, ~2s. Active tables: sleep, sleep_stage, heart_rate, steps_daily,
stress, exercise, weight, atl_category, atl_interval. HRV는 은퇴했다: Samsung export 1,058행에
rmssd가 없고 binning_data만 있어, 예전 importer가 전부 0.0으로 넣던 빈 껍데기였다.
import 는 자기가 뭘 잃었는지 말한다. total_rows 말고 status 를 먼저 봐라.
{
"status": "warning",
"warnings": ["stress: 27,598 rows (2026-07-14 12:25) → 0 — stream lost [empty]"],
"total_rows": 175941,
"prev_total_rows": 203539,
"tables": [
{"name": "stress", "rows": 0, "status": "empty", "prev_rows": 27598, "delta": -27598}
]
}
| 낱말 | 뜻 |
|---|
ok | 읽었고, 지난번보다 줄지 않았다 |
empty | 읽히긴 했는데 0 행. 지난번에 행이 있었다면 잃은 것 |
shrunk | 지난 import 보다 적다 — Samsung export 는 누적 덤프라 줄면 이상하다 |
직전 행수는 DB 안 import_log 원장에 산다 (import 가 DB 를 지워도 이월된다).
첫 import 는 비교 대상이 없으니 경고하지 않는다 — note 가 그렇게 말한다.
원장을 직접 읽는다면 GROUP BY import_id 를 써라. imported_at 은 한 import 를
묶지 못한다 (옛 행들은 초 경계를 넘어 2~3 개로 쪼개져 있다).
rows 는 DB 에 실제로 앉은 행수다 — 우리가 건넨 행수가 아니라. 예전엔 INSERT 결과를
안 보고 세서, DB 가 거부한 행도 "imported" 로 원장에 박혔다. 손실 가드가 그 가짜 숫자를
기준으로 삼고 있었다.
rejected 는 승격을 막지 않지만 반드시 보고된다. 재본 적 없는 시각(1970-01-01 epoch,
2000-01-01)을 실은 행과, Samsung 이 같은 날을 다시 동기화해 최신 개정판으로 대체된 행이다.
조용히 버리는 도구는 조용히 잃는 도구와 같은 침묵이라 매 run 이 이유별 개수를 말한다.
거절은 영구적인 손실 예산이 아니다. 정상 baseline 뒤에는 accepted rows 가 하나라도 줄면
현재 rejected 가 몇 개든 shrunk 다. 새 거절 정책은 전수 대조한 명시적 baseline 전환으로만
들어오며, 산술 허용량을 상시 열어두지 않는다.
경고가 하나라도 있으면 승격되지 않는다 — 첫 import 도 예외가 아니다. 우회 플래그는 없다.
예전엔 "빈 것만" 막았는데, Samsung 여러 스트림 성공 + aTimeLogger 실패 같은 부분 성공은 그대로
운영 DB 가 됐다. 그 순간 "못 봤다"를 말할 에러 경로가 사라진다 — DB 가 있으니 이후로는 그냥
[] 다. 불완전한 DB 는 운영 자리에 오지 않는다. 스트림 하나가 빠진 DB 는 그 스트림에
대해 영원히 [] 를 답하고, 소비자(관측소)는 그 구멍을 0 으로 기록한다.
rows 옆의 invalid 를 봐라. 파일에 행은 있는데 필수 필드가 없거나 시각·필수 수치가
파싱 안 되는 행 수다. Samsung 이 헤더 하나만 개명하거나 숫자 자리에 garbage를 넣어도 예전엔
empty 또는 측정값 0으로 보였다 (첫 import 는 baseline 이 없어 경고조차 없었다).
invalid > 0 이면 승격이 막힌다. 빈 선택 필드는 0으로 둘 수 있지만, 값이 있는데 읽히지
않는 것은 0이 아니다.
atl_category 는 독립 스트림이다. time 조회는 카테고리를 INNER JOIN 하므로,
카테고리가 사라지면 블록 수가 그대로여도 시간축이 전멸한다. 예전 원장은 interval 만
세서 이 손실을 못 봤다 (status: ok, warnings: [], time 30일 → 0). 세지 않는 스트림은
잃어도 모른다. 고아 블록(없는 카테고리를 가리키는 블록) 검사도 함께 돈다.
왜 있나: 2026-07-14, 글롭 하나가 7MB stress 대신 1KB histogram 을 집어 27,598 행이
통째로 0 이 됐는데 import 는 "ok" 라고 했다. 테스트는 초록불이었다. 잡은 건 총 행수가
203,539 → 175,941 로 떨어진 걸 사람이 눈으로 본 것뿐이었다. 이제 도구가 말한다.
read — Denote ID로 조회
lifetract read 20250115T000000
lifetract read 2025-01-15
lifetract read 20250115T233000
Day 조회 시 건강 메트릭 + aTimeLogger 시간 카테고리 + 수면 세션 + 운동 모두 포함.
today — 오늘 요약
lifetract today
{"date": "2025-10-04", "steps": 41382, "sleep_hours": 1.5, "avg_hr": 93.1, "stress_avg": 20.9, "time_categories": [...], "source": "db"}
{"date": "2026-05-26", "steps": 7099, "sleep_hours": 4.8, "avg_hr": 137, "stress_avg": 0, "source": "db+ha", "ha_sources": ["steps","heart_rate","sleep"]}
time_categories 가 비면 JSON 에서 키 자체가 빠진다 (omitempty). 데이터 있는 날 vs 없는 날 둘 다 정상 출력.
자동 HA fallback (오늘 자리에 한정): DB 가 오늘 자리를 비웠으면 (Samsung CSV 가 아직 안 들어왔으면) today 와 read <오늘> 이 자동으로 HA 라이브 값으로 채운다. source 가 "db+ha" 로 바뀌고, ha_sources 가 어떤 필드가 HA 에서 왔는지 알려준다. 과거 날짜는 enrichment 안 됨 — HA recorder 는 backfill 자리가 아니다. 끄려면 LIFETRACT_NO_HA=1. Sleep 은 옛 row 가 오늘로 잡히는 stale 자리도 감지해서 HA 로 덮어쓴다 (최근 36h 의 sleep_duration history 를 합산 — main sleep + nap 둘 다 잡음).
timeline — 날짜별 횡단 뷰
lifetract timeline --days 7
lifetract timeline --days 30
denotecli 저널과 같은 날짜 키(YYYYMMDDT000000)로 정렬. 건강+시간+운동 통합.
sleep / steps / heart / stress / exercise
lifetract sleep --days 7
lifetract sleep --days 30 --summary
lifetract steps --days 7
lifetract heart --days 7
lifetract stress --days 7
lifetract exercise --days 30
걸음 수 계약: steps_daily 는 측정일마다 정확히 한 행이다. 날짜는 Samsung의
day_time 만 읽으며 create_time 으로 대신하지 않는다. day_time 은 epoch-ms와
벽시계 문자열 양쪽을 지원하고, 못 읽거나 미래면 invalid로 승격을 막는다. 같은 날의
재동기화 행은 최신 update_time 한 건만 남기며, 동시각에 값이 충돌하면 임의로 고르지
않고 invalid다. DB도 날짜 UNIQUE로 같은 불변식을 강제한다.
time — aTimeLogger 시간 추적
lifetract time --days 7
lifetract time --days 30 --category 본짓
카테고리: 본짓, 수면, 가족, 식사, 독서, 운동, 걷기, 수행, 셀프토크, 낮잠, 준비, 집안일, 이동, 쇼핑, 딴짓, 유튜브, 짧은휴식, 여가활동 (18종)
export — 공개용 내보내기 계획
lifetract export
ha — Home Assistant REST (live sensors)
lifetract ha ping
lifetract ha state heart_rate
lifetract ha state sleep_duration
lifetract ha states
lifetract ha entities
lifetract ha history sleep_duration --days 7
{
"entity_id": "sensor.sm_s942n_s26_glgman_heart_rate",
"kind": "heart_rate",
"state": "111.0",
"value": 111,
"unit": "bpm",
"last_changed": "2026-05-17T22:34:11Z",
"attributes": {...}
}
토큰: pass show 2fa/totp/ha/junghanacs (primary) → env HA_TOKEN (fallback) → ~/.lifetract/ha.env. 토큰값 자체는 절대 commit/push 금지.
도메인 kind: sleep_duration, steps_daily, distance_daily, floors_daily, heart_rate, resting_heart_rate, heart_rate_variability, weight, body_fat, height, calories_burned, active_calories_burned, basal_metabolic_rate, hydration, detected_activity, geocoded_location, battery, sleep_confidence, respiratory_rate, oxygen_saturation, body_temperature, blood_glucose, systolic_blood_pressure, diastolic_blood_pressure (24종).
새 sensor 추가 = lifetract/ha_entities.go 의 KnownEntities 에 한 줄.
ha history 동작: HA recorder 는 state 변화 시점에만 row 저장. recorder 30일 보관은 "있는 데이터 보존" 이지 "없는 데이터 채워줌" 이 아님. HA 인프라가 띄워진 시점 이전 데이터는 영원히 안 잡힘. 과거는 Samsung CSV export 가 유일한 길. HA history = 내일부터의 적립 자리.
{
"entity_id": "sensor.sm_s942n_s26_glgman_sleep_duration",
"kind": "sleep_duration",
"unit": "min",
"days": 7,
"from": "2026-05-11T...+09:00",
"to": "2026-05-18T...+09:00",
"count": 2,
"points": [
{"last_changed": "...", "value": 427, "unit": "min", "attributes": {"endTime": "..."}},
...
]
}
현재 상태 (phase 7 read-only): cmdToday / cmdRead <오늘> 이 DB miss 또는 stale sleep 자리에서 자동으로 HA GetState + GetHistory 를 호출해 응답에 채워준다 (source: "db+ha", ha_sources: [...]). DB upsert 는 의도적으로 하지 않는다. Samsung export 가 영구 SSOT이고 HA recorder 는 30일 보조면이라, 덜 아는 값을 본 DB에 흡수하지 않는다.
Flags
| Flag | Default | 설명 |
|---|
--days N | 7 | 창 길이 (무시되지 않는다 — 아래 조합표) |
--from YYYY-MM-DD | — | 창 시작 (포함) |
--to YYYY-MM-DD | — | 창 끝 (배타적) |
--data-dir DIR | ~/repos/gh/self-tracking-data | 데이터 루트 |
--shealth-dir DIR | 최신 자동감지 | Samsung Health 디렉토리 |
--summary | false | 요약 모드 |
--category CAT | 전체 | 시간 카테고리 필터 |
--exec | false | import 실행 모드 |
창 조합 — 모든 조합이 뜻을 갖는다
| 조합 | 창 |
|---|
--days N | [내일-N, 내일) — 오늘 포함 정확히 N일 (1=오늘) |
--days N --to T | [T-N, T) — T 에 끝나는 N일 |
--days N --from F | [F, F+N) — F 에 시작하는 N일 |
--from F --to T | [F, T) |
--from F | [F, 내일) |