@milldr/crono 0.1.0 → 0.2.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 (50) hide show
  1. package/README.md +128 -0
  2. package/dist/commands/add.d.ts +9 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +65 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/export.d.ts +8 -0
  7. package/dist/commands/export.d.ts.map +1 -0
  8. package/dist/commands/export.js +142 -0
  9. package/dist/commands/export.js.map +1 -0
  10. package/dist/commands/log.d.ts +6 -0
  11. package/dist/commands/log.d.ts.map +1 -0
  12. package/dist/commands/log.js +40 -0
  13. package/dist/commands/log.js.map +1 -0
  14. package/dist/config.d.ts +2 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/cronometer/auth.d.ts +31 -0
  18. package/dist/cronometer/auth.d.ts.map +1 -0
  19. package/dist/cronometer/auth.js +151 -0
  20. package/dist/cronometer/auth.js.map +1 -0
  21. package/dist/cronometer/export.d.ts +22 -0
  22. package/dist/cronometer/export.d.ts.map +1 -0
  23. package/dist/cronometer/export.js +83 -0
  24. package/dist/cronometer/export.js.map +1 -0
  25. package/dist/cronometer/parse.d.ts +35 -0
  26. package/dist/cronometer/parse.d.ts.map +1 -0
  27. package/dist/cronometer/parse.js +158 -0
  28. package/dist/cronometer/parse.js.map +1 -0
  29. package/dist/index.js +33 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/kernel/add-custom-food.d.ts +22 -0
  32. package/dist/kernel/add-custom-food.d.ts.map +1 -0
  33. package/dist/kernel/add-custom-food.js +314 -0
  34. package/dist/kernel/add-custom-food.js.map +1 -0
  35. package/dist/kernel/client.d.ts +15 -0
  36. package/dist/kernel/client.d.ts.map +1 -1
  37. package/dist/kernel/client.js +92 -1
  38. package/dist/kernel/client.js.map +1 -1
  39. package/dist/kernel/log-food.d.ts +17 -0
  40. package/dist/kernel/log-food.d.ts.map +1 -0
  41. package/dist/kernel/log-food.js +230 -0
  42. package/dist/kernel/log-food.js.map +1 -0
  43. package/dist/kernel/login.d.ts.map +1 -1
  44. package/dist/kernel/login.js +24 -1
  45. package/dist/kernel/login.js.map +1 -1
  46. package/package.json +5 -1
  47. package/dist/debug-nav.d.ts +0 -2
  48. package/dist/debug-nav.d.ts.map +0 -1
  49. package/dist/debug-nav.js +0 -99
  50. package/dist/debug-nav.js.map +0 -1
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Direct HTTP authentication with Cronometer.
3
+ *
4
+ * Three-step flow:
5
+ * 1. GET /login/ — parse anticsrf token and JSESSIONID cookie
6
+ * 2. POST /login — form-encoded login, get sesnonce cookie
7
+ * 3. POST /cronometer/app — GWT RPC authenticate, get user ID
8
+ */
9
+ const BASE_URL = "https://cronometer.com";
10
+ // GWT permutation and header hashes are public client-side artifacts visible to
11
+ // anyone inspecting Cronometer's network requests — they are not secrets.
12
+ // These may need updating if Cronometer deploys a new GWT build.
13
+ const DEFAULT_GWT_PERMUTATION = "7B121DC5483BF272B1BC1916DA9FA963";
14
+ const DEFAULT_GWT_HEADER = "2D6A926E3729946302DC68073CB0D550";
15
+ /** Extract a named cookie value from a set-cookie header array. */
16
+ export function extractCookie(headers, name) {
17
+ const cookies = headers.getSetCookie();
18
+ for (const cookie of cookies) {
19
+ const match = cookie.match(new RegExp(`${name}=([^;]+)`));
20
+ if (match)
21
+ return match[1];
22
+ }
23
+ return null;
24
+ }
25
+ /** Collect all cookies from set-cookie headers as "name=value; name2=value2" string. */
26
+ export function collectCookies(headers) {
27
+ const cookies = headers.getSetCookie();
28
+ const pairs = [];
29
+ for (const cookie of cookies) {
30
+ const match = cookie.match(/^([^=]+)=([^;]+)/);
31
+ if (match)
32
+ pairs.push(`${match[1]}=${match[2]}`);
33
+ }
34
+ return pairs.join("; ");
35
+ }
36
+ /** Merge two cookie strings, with later values overriding earlier ones. */
37
+ export function mergeCookies(a, b) {
38
+ const map = new Map();
39
+ for (const str of [a, b]) {
40
+ for (const pair of str.split("; ")) {
41
+ const eq = pair.indexOf("=");
42
+ if (eq > 0)
43
+ map.set(pair.substring(0, eq), pair.substring(eq + 1));
44
+ }
45
+ }
46
+ return [...map.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
47
+ }
48
+ /** Parse the anticsrf token from Cronometer's login page HTML. */
49
+ export function parseAnticsrf(html) {
50
+ const match = html.match(/name=["']anticsrf["']\s+value=["']([^"']+)["']/);
51
+ return match ? match[1] : null;
52
+ }
53
+ /** Extract user ID from GWT authenticate response. */
54
+ export function parseUserId(body) {
55
+ // GWT response format: //OK[<id>,...]
56
+ // The user ID is a large negative number in the response array
57
+ const match = body.match(/\/\/OK\[(-?\d+)/);
58
+ return match ? parseInt(match[1], 10) : null;
59
+ }
60
+ /** Build GWT RPC body for the authenticate call. */
61
+ export function buildAuthenticateBody(gwtHeader) {
62
+ const tzOffset = new Date().getTimezoneOffset();
63
+ return `7|0|5|https://cronometer.com/cronometer/|${gwtHeader}|com.cronometer.shared.rpc.CronometerService|authenticate|java.lang.Integer/3438268394|1|2|3|4|1|5|5|${tzOffset}|`;
64
+ }
65
+ function gwtHeaders(gwtPermutation, cookies) {
66
+ return {
67
+ "Content-Type": "text/x-gwt-rpc; charset=UTF-8",
68
+ "X-GWT-Module-Base": "https://cronometer.com/cronometer/",
69
+ "X-GWT-Permutation": gwtPermutation,
70
+ Cookie: cookies,
71
+ };
72
+ }
73
+ /**
74
+ * Authenticate with Cronometer via HTTP.
75
+ * Returns a session object with cookies and user ID.
76
+ */
77
+ export async function login(username, password, gwtPermutation, gwtHeader) {
78
+ const permutation = gwtPermutation || DEFAULT_GWT_PERMUTATION;
79
+ const header = gwtHeader || DEFAULT_GWT_HEADER;
80
+ // Step 1: GET /login/ — get anticsrf + JSESSIONID
81
+ const loginPageRes = await fetch(`${BASE_URL}/login/`, {
82
+ redirect: "manual",
83
+ });
84
+ const loginPageHtml = await loginPageRes.text();
85
+ const anticsrf = parseAnticsrf(loginPageHtml);
86
+ if (!anticsrf) {
87
+ throw new Error("Failed to parse anticsrf token from login page");
88
+ }
89
+ const step1Cookies = collectCookies(loginPageRes.headers);
90
+ const jsessionId = extractCookie(loginPageRes.headers, "JSESSIONID");
91
+ if (!jsessionId) {
92
+ throw new Error("No JSESSIONID cookie received from login page");
93
+ }
94
+ // Step 2: POST /login — form login (send all step 1 cookies including anticsrf cookie)
95
+ const formBody = new URLSearchParams({
96
+ username,
97
+ password,
98
+ anticsrf,
99
+ });
100
+ const loginRes = await fetch(`${BASE_URL}/login`, {
101
+ method: "POST",
102
+ headers: {
103
+ "Content-Type": "application/x-www-form-urlencoded",
104
+ Cookie: step1Cookies,
105
+ },
106
+ body: formBody.toString(),
107
+ redirect: "manual",
108
+ });
109
+ const sesnonce = extractCookie(loginRes.headers, "sesnonce");
110
+ // Check for login errors — parse body as JSON
111
+ const loginText = await loginRes.text();
112
+ try {
113
+ const loginJson = JSON.parse(loginText);
114
+ if (loginJson.error) {
115
+ throw new Error(loginJson.error.includes("Too Many Attempts")
116
+ ? "Cronometer rate limit hit. Please wait a minute and try again."
117
+ : "Cronometer login failed. Check your credentials with: crono login");
118
+ }
119
+ }
120
+ catch (e) {
121
+ if (e instanceof Error && e.message.includes("Cronometer"))
122
+ throw e;
123
+ // Non-JSON response (e.g. redirect body) — check sesnonce as success indicator
124
+ if (!sesnonce) {
125
+ throw new Error("Cronometer login failed. Check your credentials with: crono login");
126
+ }
127
+ }
128
+ if (!sesnonce) {
129
+ throw new Error("No sesnonce cookie received after login");
130
+ }
131
+ // Step 3: POST /cronometer/app — GWT authenticate
132
+ const step2Cookies = collectCookies(loginRes.headers);
133
+ const cookies = mergeCookies(step1Cookies, step2Cookies);
134
+ const authBody = buildAuthenticateBody(header);
135
+ const authRes = await fetch(`${BASE_URL}/cronometer/app`, {
136
+ method: "POST",
137
+ headers: gwtHeaders(permutation, cookies),
138
+ body: authBody,
139
+ });
140
+ const authText = await authRes.text();
141
+ const userId = parseUserId(authText);
142
+ if (userId === null) {
143
+ throw new Error("Cronometer API error. GWT values may be outdated. See docs for override instructions.");
144
+ }
145
+ // Step 3 rotates the sesnonce — use the new one if present
146
+ const step3Cookies = collectCookies(authRes.headers);
147
+ const finalCookies = mergeCookies(cookies, step3Cookies);
148
+ const newSesnonce = extractCookie(authRes.headers, "sesnonce") || sesnonce;
149
+ return { cookies: finalCookies, sesnonce: newSesnonce, userId };
150
+ }
151
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/cronometer/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Cronometer export API client.
3
+ *
4
+ * Generates a single-use nonce via GWT RPC, then fetches CSV data
5
+ * from the /export endpoint. No browser automation required.
6
+ */
7
+ import { type CronometerSession } from "./auth.js";
8
+ export type ExportType = "nutrition" | "exercises" | "biometrics";
9
+ /** Build GWT RPC body for generateAuthorizationToken. */
10
+ export declare function buildNonceBody(gwtHeader: string, sesnonce: string, userId: number): string;
11
+ /** Extract the nonce from a GWT RPC response. */
12
+ export declare function parseNonce(body: string): string | null;
13
+ /** Generate a single-use authorization nonce. */
14
+ export declare function generateNonce(session: CronometerSession, gwtPermutation?: string, gwtHeader?: string): Promise<string>;
15
+ /** Fetch CSV export data from Cronometer. */
16
+ export declare function fetchExport(session: CronometerSession, type: ExportType, start: string, end: string, gwtPermutation?: string, gwtHeader?: string): Promise<string>;
17
+ /**
18
+ * Top-level export orchestrator.
19
+ * Reads credentials, authenticates, fetches and returns raw CSV.
20
+ */
21
+ export declare function exportData(type: ExportType, start: string, end: string, onStatus?: (msg: string) => void): Promise<string>;
22
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/cronometer/export.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAS,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAO1D,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;AAQlE,yDAAyD;AACzD,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED,iDAAiD;AACjD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAItD;AAED,iDAAiD;AACjD,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,EAC1B,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAED,6CAA6C;AAC7C,wBAAsB,WAAW,CAC/B,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAC/B,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Cronometer export API client.
3
+ *
4
+ * Generates a single-use nonce via GWT RPC, then fetches CSV data
5
+ * from the /export endpoint. No browser automation required.
6
+ */
7
+ import { getCredential } from "../credentials.js";
8
+ import { loadConfig } from "../config.js";
9
+ import { login } from "./auth.js";
10
+ const BASE_URL = "https://cronometer.com";
11
+ const DEFAULT_GWT_PERMUTATION = "7B121DC5483BF272B1BC1916DA9FA963";
12
+ const DEFAULT_GWT_HEADER = "2D6A926E3729946302DC68073CB0D550";
13
+ const EXPORT_TYPE_MAP = {
14
+ nutrition: "dailySummary",
15
+ exercises: "exercises",
16
+ biometrics: "biometrics",
17
+ };
18
+ /** Build GWT RPC body for generateAuthorizationToken. */
19
+ export function buildNonceBody(gwtHeader, sesnonce, userId) {
20
+ return `7|0|8|https://cronometer.com/cronometer/|${gwtHeader}|com.cronometer.shared.rpc.CronometerService|generateAuthorizationToken|java.lang.String/2004016611|I|com.cronometer.shared.user.AuthScope/2065601159|${sesnonce}|1|2|3|4|4|5|6|6|7|8|${userId}|3600|7|2|`;
21
+ }
22
+ /** Extract the nonce from a GWT RPC response. */
23
+ export function parseNonce(body) {
24
+ // Response format: //OK[1,["<32-char-hex>"],...] or //OK["<32-char-hex>",...]
25
+ const match = body.match(/"([a-f0-9]{32,})"/);
26
+ return match ? match[1] : null;
27
+ }
28
+ /** Generate a single-use authorization nonce. */
29
+ export async function generateNonce(session, gwtPermutation, gwtHeader) {
30
+ const permutation = gwtPermutation || DEFAULT_GWT_PERMUTATION;
31
+ const header = gwtHeader || DEFAULT_GWT_HEADER;
32
+ const body = buildNonceBody(header, session.sesnonce, session.userId);
33
+ const res = await fetch(`${BASE_URL}/cronometer/app`, {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "text/x-gwt-rpc; charset=UTF-8",
37
+ "X-GWT-Module-Base": "https://cronometer.com/cronometer/",
38
+ "X-GWT-Permutation": permutation,
39
+ Cookie: session.cookies,
40
+ },
41
+ body,
42
+ });
43
+ const text = await res.text();
44
+ const nonce = parseNonce(text);
45
+ if (!nonce) {
46
+ throw new Error("Cronometer API error. GWT values may be outdated. See docs for override instructions.");
47
+ }
48
+ return nonce;
49
+ }
50
+ /** Fetch CSV export data from Cronometer. */
51
+ export async function fetchExport(session, type, start, end, gwtPermutation, gwtHeader) {
52
+ const nonce = await generateNonce(session, gwtPermutation, gwtHeader);
53
+ const generate = EXPORT_TYPE_MAP[type];
54
+ const url = `${BASE_URL}/export?nonce=${nonce}&generate=${generate}&start=${start}&end=${end}`;
55
+ const res = await fetch(url, {
56
+ headers: { Cookie: session.cookies },
57
+ });
58
+ if (!res.ok) {
59
+ throw new Error(`Export failed: ${res.status}`);
60
+ }
61
+ return res.text();
62
+ }
63
+ /**
64
+ * Top-level export orchestrator.
65
+ * Reads credentials, authenticates, fetches and returns raw CSV.
66
+ */
67
+ export async function exportData(type, start, end, onStatus) {
68
+ const username = getCredential("cronometer-username");
69
+ const password = getCredential("cronometer-password");
70
+ if (!username || !password) {
71
+ throw new Error("No Cronometer credentials found. Run: crono login");
72
+ }
73
+ // Resolve GWT overrides: env vars > config > defaults
74
+ const config = loadConfig();
75
+ const gwtPermutation = process.env.CRONO_GWT_PERMUTATION || config.gwtPermutation;
76
+ const gwtHeader = process.env.CRONO_GWT_HEADER || config.gwtHeader;
77
+ onStatus?.("Logging in...");
78
+ const session = await login(username, password, gwtPermutation, gwtHeader);
79
+ onStatus?.("Fetching data...");
80
+ const csv = await fetchExport(session, type, start, end, gwtPermutation, gwtHeader);
81
+ return csv;
82
+ }
83
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/cronometer/export.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CSV parsing for Cronometer export data.
3
+ *
4
+ * Hand-rolled parser — Cronometer's CSV format is simple and predictable.
5
+ * No external library needed.
6
+ */
7
+ export interface NutritionEntry {
8
+ date: string;
9
+ calories: number;
10
+ protein: number;
11
+ carbs: number;
12
+ fat: number;
13
+ [key: string]: string | number;
14
+ }
15
+ export interface ExerciseEntry {
16
+ date: string;
17
+ time: string;
18
+ exercise: string;
19
+ minutes: number;
20
+ caloriesBurned: number;
21
+ group: string;
22
+ }
23
+ export interface BiometricEntry {
24
+ date: string;
25
+ time: string;
26
+ metric: string;
27
+ unit: string;
28
+ amount: number | string;
29
+ }
30
+ /** Parse a CSV string into rows of string arrays. Handles quoted fields. */
31
+ export declare function parseCSV(csv: string): string[][];
32
+ export declare function parseNutrition(csv: string): NutritionEntry[];
33
+ export declare function parseExercises(csv: string): ExerciseEntry[];
34
+ export declare function parseBiometrics(csv: string): BiometricEntry[];
35
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/cronometer/parse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAgChD;AAgBD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,EAAE,CA0C5D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CA+B3D;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,EAAE,CAkC7D"}
@@ -0,0 +1,158 @@
1
+ /**
2
+ * CSV parsing for Cronometer export data.
3
+ *
4
+ * Hand-rolled parser — Cronometer's CSV format is simple and predictable.
5
+ * No external library needed.
6
+ */
7
+ /** Parse a CSV string into rows of string arrays. Handles quoted fields. */
8
+ export function parseCSV(csv) {
9
+ const lines = csv.trim().split("\n");
10
+ return lines.map((line) => {
11
+ const fields = [];
12
+ let current = "";
13
+ let inQuotes = false;
14
+ for (let i = 0; i < line.length; i++) {
15
+ const char = line[i];
16
+ if (inQuotes) {
17
+ if (char === '"') {
18
+ if (i + 1 < line.length && line[i + 1] === '"') {
19
+ current += '"';
20
+ i++;
21
+ }
22
+ else {
23
+ inQuotes = false;
24
+ }
25
+ }
26
+ else {
27
+ current += char;
28
+ }
29
+ }
30
+ else if (char === '"') {
31
+ inQuotes = true;
32
+ }
33
+ else if (char === ",") {
34
+ fields.push(current);
35
+ current = "";
36
+ }
37
+ else {
38
+ current += char;
39
+ }
40
+ }
41
+ fields.push(current);
42
+ return fields;
43
+ });
44
+ }
45
+ /** Find the index of a column by name (case-insensitive). */
46
+ function colIndex(headers, name) {
47
+ return headers.findIndex((h) => h.trim().toLowerCase() === name.toLowerCase());
48
+ }
49
+ /** Parse a numeric field, returning 0 if empty or NaN. */
50
+ function num(value) {
51
+ if (!value || value.trim() === "")
52
+ return 0;
53
+ const n = parseFloat(value.trim());
54
+ return isNaN(n) ? 0 : n;
55
+ }
56
+ export function parseNutrition(csv) {
57
+ const rows = parseCSV(csv);
58
+ if (rows.length < 2)
59
+ return [];
60
+ const headers = rows[0];
61
+ const dateIdx = colIndex(headers, "Date");
62
+ const energyIdx = colIndex(headers, "Energy (kcal)");
63
+ const proteinIdx = colIndex(headers, "Protein (g)");
64
+ const carbsIdx = colIndex(headers, "Carbs (g)");
65
+ const fatIdx = colIndex(headers, "Fat (g)");
66
+ if (dateIdx === -1)
67
+ return [];
68
+ const entries = [];
69
+ for (let i = 1; i < rows.length; i++) {
70
+ const row = rows[i];
71
+ const date = row[dateIdx]?.trim();
72
+ if (!date)
73
+ continue;
74
+ const entry = {
75
+ date,
76
+ calories: num(row[energyIdx]),
77
+ protein: num(row[proteinIdx]),
78
+ carbs: num(row[carbsIdx]),
79
+ fat: num(row[fatIdx]),
80
+ };
81
+ // Include all columns for JSON/CSV output
82
+ for (let j = 0; j < headers.length; j++) {
83
+ if (j === dateIdx)
84
+ continue;
85
+ const key = headers[j].trim();
86
+ const val = row[j]?.trim() ?? "";
87
+ if (key && val !== "") {
88
+ const parsed = parseFloat(val);
89
+ entry[key] = isNaN(parsed) ? val : parsed;
90
+ }
91
+ }
92
+ entries.push(entry);
93
+ }
94
+ return entries;
95
+ }
96
+ export function parseExercises(csv) {
97
+ const rows = parseCSV(csv);
98
+ if (rows.length < 2)
99
+ return [];
100
+ const headers = rows[0];
101
+ const dayIdx = colIndex(headers, "Day");
102
+ const timeIdx = colIndex(headers, "Time");
103
+ const exerciseIdx = colIndex(headers, "Exercise");
104
+ const minutesIdx = colIndex(headers, "Minutes");
105
+ const caloriesIdx = colIndex(headers, "Calories Burned");
106
+ const groupIdx = colIndex(headers, "Group");
107
+ if (dayIdx === -1)
108
+ return [];
109
+ const entries = [];
110
+ for (let i = 1; i < rows.length; i++) {
111
+ const row = rows[i];
112
+ const date = row[dayIdx]?.trim();
113
+ if (!date)
114
+ continue;
115
+ entries.push({
116
+ date,
117
+ time: row[timeIdx]?.trim() ?? "",
118
+ exercise: row[exerciseIdx]?.trim() ?? "",
119
+ minutes: num(row[minutesIdx]),
120
+ caloriesBurned: num(row[caloriesIdx]),
121
+ group: row[groupIdx]?.trim() ?? "",
122
+ });
123
+ }
124
+ return entries;
125
+ }
126
+ export function parseBiometrics(csv) {
127
+ const rows = parseCSV(csv);
128
+ if (rows.length < 2)
129
+ return [];
130
+ const headers = rows[0];
131
+ const dayIdx = colIndex(headers, "Day");
132
+ const timeIdx = colIndex(headers, "Time");
133
+ const metricIdx = colIndex(headers, "Metric");
134
+ const unitIdx = colIndex(headers, "Unit");
135
+ const amountIdx = colIndex(headers, "Amount");
136
+ if (dayIdx === -1)
137
+ return [];
138
+ const entries = [];
139
+ for (let i = 1; i < rows.length; i++) {
140
+ const row = rows[i];
141
+ const date = row[dayIdx]?.trim();
142
+ if (!date)
143
+ continue;
144
+ const rawAmount = row[amountIdx]?.trim() ?? "";
145
+ // Keep as string if it contains non-numeric chars (e.g. blood pressure "120/80")
146
+ const isNumeric = rawAmount !== "" && /^-?\d+(\.\d+)?$/.test(rawAmount);
147
+ const amount = isNumeric ? parseFloat(rawAmount) : rawAmount;
148
+ entries.push({
149
+ date,
150
+ time: row[timeIdx]?.trim() ?? "",
151
+ metric: row[metricIdx]?.trim() ?? "",
152
+ unit: row[unitIdx]?.trim() ?? "",
153
+ amount,
154
+ });
155
+ }
156
+ return entries;
157
+ }
158
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/cronometer/parse.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -2,8 +2,11 @@
2
2
  import { Command } from "commander";
3
3
  import { login } from "./commands/login.js";
4
4
  import { quickAdd } from "./commands/quick-add.js";
5
+ import { addCustomFood } from "./commands/add.js";
6
+ import { log } from "./commands/log.js";
5
7
  import { diary } from "./commands/diary.js";
6
8
  import { weight } from "./commands/weight.js";
9
+ import { exportCmd } from "./commands/export.js";
7
10
  const program = new Command();
8
11
  program
9
12
  .name("crono")
@@ -25,6 +28,26 @@ program
25
28
  .action(async (options) => {
26
29
  await quickAdd(options);
27
30
  });
31
+ const addCmd = program.command("add").description("Add items to Cronometer");
32
+ addCmd
33
+ .command("custom-food <name>")
34
+ .description("Create a custom food with macros")
35
+ .option("-p, --protein <grams>", "Grams of protein", parseFloat)
36
+ .option("-c, --carbs <grams>", "Grams of carbohydrates", parseFloat)
37
+ .option("-f, --fat <grams>", "Grams of fat", parseFloat)
38
+ .option("-t, --total <calories>", "Total calories (auto-calculated from macros if omitted)", parseFloat)
39
+ .option("--log [meal]", "Also log to diary (optionally specify meal)")
40
+ .action(async (name, options) => {
41
+ await addCustomFood(name, options);
42
+ });
43
+ program
44
+ .command("log <name>")
45
+ .description("Log a food to your diary by name")
46
+ .option("-m, --meal <name>", "Meal category (Breakfast, Lunch, Dinner, Snacks)")
47
+ .option("-s, --servings <count>", "Number of servings", parseFloat)
48
+ .action(async (name, options) => {
49
+ await log(name, options);
50
+ });
28
51
  program
29
52
  .command("weight")
30
53
  .description("Check your weight from Cronometer")
@@ -43,5 +66,15 @@ program
43
66
  .action(async (options) => {
44
67
  await diary(options);
45
68
  });
69
+ program
70
+ .command("export <type>")
71
+ .description("Export data from Cronometer (nutrition, exercises, biometrics)")
72
+ .option("-d, --date <date>", "Date (YYYY-MM-DD)")
73
+ .option("-r, --range <range>", "Range (7d, 30d, or YYYY-MM-DD:YYYY-MM-DD)")
74
+ .option("--csv", "Output as CSV")
75
+ .option("--json", "Output as JSON")
76
+ .action(async (type, options) => {
77
+ await exportCmd(type, options);
78
+ });
46
79
  program.parse();
47
80
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAE9C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,OAAO,CAAC;KACb,WAAW,CAAC,6CAA6C,CAAC;KAC1D,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,kDAAkD,CAAC;KAC/D,MAAM,CAAC,KAAK,IAAI,EAAE;IACjB,MAAM,KAAK,EAAE,CAAC;AAChB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,uCAAuC,CAAC;KACpD,MAAM,CAAC,uBAAuB,EAAE,kBAAkB,EAAE,UAAU,CAAC;KAC/D,MAAM,CAAC,qBAAqB,EAAE,wBAAwB,EAAE,UAAU,CAAC;KACnE,MAAM,CAAC,mBAAmB,EAAE,cAAc,EAAE,UAAU,CAAC;KACvD,MAAM,CACL,mBAAmB,EACnB,kDAAkD,CACnD;KACA,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC1B,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,mCAAmC,CAAC;KAChD,MAAM,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;KAChD,MAAM,CAAC,qBAAqB,EAAE,2CAA2C,CAAC;KAC1E,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;KAClC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;AACxB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,6CAA6C,CAAC;KAC1D,MAAM,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;KAChD,MAAM,CAAC,qBAAqB,EAAE,2CAA2C,CAAC;KAC1E,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;KAClC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Playwright code generator for Cronometer custom food creation.
3
+ *
4
+ * Returns a code string that executes remotely via
5
+ * kernel.browsers.playwright.execute(). The code has access to
6
+ * `page`, `context`, and `browser` from the Playwright environment.
7
+ */
8
+ import type { CustomFoodEntry } from "./client.js";
9
+ /**
10
+ * Nutrient labels as they appear in Cronometer's nutrition facts editor.
11
+ */
12
+ export declare const NUTRIENT_LABELS: Record<string, string>;
13
+ /**
14
+ * Generate Playwright code for creating a custom food in Cronometer.
15
+ *
16
+ * Flow:
17
+ * navigate to #foods → expand sidebar → click Custom Foods →
18
+ * click CREATE FOOD → fill name → click "-" to reveal nutrient inputs →
19
+ * fill macros → Save Changes → optionally log to diary
20
+ */
21
+ export declare function buildAddCustomFoodCode(entry: CustomFoodEntry): string;
22
+ //# sourceMappingURL=add-custom-food.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add-custom-food.d.ts","sourceRoot":"","sources":["../../src/kernel/add-custom-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKlD,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAuSrE"}