vibechk 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.
package/dist/index.js ADDED
@@ -0,0 +1,1654 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/storage/atomic.ts
12
+ import { writeFileSync, readFileSync, existsSync, renameSync, appendFileSync } from "fs";
13
+ import { randomBytes } from "crypto";
14
+ function writeJson(filePath, data) {
15
+ const tmp = filePath + ".tmp." + randomBytes(6).toString("hex");
16
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
17
+ renameSync(tmp, filePath);
18
+ }
19
+ function readJson(filePath) {
20
+ if (!existsSync(filePath)) return null;
21
+ try {
22
+ return JSON.parse(readFileSync(filePath, "utf8"));
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ var init_atomic = __esm({
28
+ "src/storage/atomic.ts"() {
29
+ "use strict";
30
+ }
31
+ });
32
+
33
+ // src/storage/paths.ts
34
+ import { homedir } from "os";
35
+ import { join } from "path";
36
+ import { mkdirSync, existsSync as existsSync2 } from "fs";
37
+ function ensureDir() {
38
+ if (!existsSync2(VIBECHK_DIR)) {
39
+ mkdirSync(VIBECHK_DIR, { recursive: true, mode: 448 });
40
+ }
41
+ }
42
+ var VIBECHK_SERVER, VIBECHK_DIR, PROFILE_PATH, STREAK_PATH, ACTIVITY_PATH, BADGES_PATH, LEADERBOARD_CACHE_PATH, FRIENDS_PATH, GIST_TOKEN_PATH;
43
+ var init_paths = __esm({
44
+ "src/storage/paths.ts"() {
45
+ "use strict";
46
+ VIBECHK_SERVER = process.env.VIBECHK_SERVER ? process.env.VIBECHK_SERVER.replace(/\/$/, "") : null;
47
+ VIBECHK_DIR = join(homedir(), ".vibechk");
48
+ PROFILE_PATH = join(VIBECHK_DIR, "profile.json");
49
+ STREAK_PATH = join(VIBECHK_DIR, "streak.json");
50
+ ACTIVITY_PATH = join(VIBECHK_DIR, "activity.jsonl");
51
+ BADGES_PATH = join(VIBECHK_DIR, "badges.json");
52
+ LEADERBOARD_CACHE_PATH = join(VIBECHK_DIR, "leaderboard.json");
53
+ FRIENDS_PATH = join(VIBECHK_DIR, "friends.json");
54
+ GIST_TOKEN_PATH = join(VIBECHK_DIR, "gist-token");
55
+ }
56
+ });
57
+
58
+ // src/storage/profile-store.ts
59
+ function loadProfile() {
60
+ return readJson(PROFILE_PATH);
61
+ }
62
+ function requireProfile() {
63
+ const profile = loadProfile();
64
+ if (!profile) {
65
+ throw new Error(
66
+ "No vibechk profile found. Run `vibechk init` to get started."
67
+ );
68
+ }
69
+ return profile;
70
+ }
71
+ var init_profile_store = __esm({
72
+ "src/storage/profile-store.ts"() {
73
+ "use strict";
74
+ init_atomic();
75
+ init_paths();
76
+ }
77
+ });
78
+
79
+ // src/storage/streak-store.ts
80
+ function loadStreak() {
81
+ return readJson(STREAK_PATH) ?? { ...DEFAULT_STREAK };
82
+ }
83
+ function saveStreak(streak) {
84
+ ensureDir();
85
+ writeJson(STREAK_PATH, streak);
86
+ }
87
+ var DEFAULT_STREAK;
88
+ var init_streak_store = __esm({
89
+ "src/storage/streak-store.ts"() {
90
+ "use strict";
91
+ init_atomic();
92
+ init_paths();
93
+ DEFAULT_STREAK = {
94
+ currentStreak: 0,
95
+ longestStreak: 0,
96
+ lastActivityDate: null,
97
+ lastCheckInAt: null,
98
+ freezeTokens: 2,
99
+ totalCheckIns: 0,
100
+ graceUsedAt: null,
101
+ status: "new"
102
+ };
103
+ }
104
+ });
105
+
106
+ // src/storage/activity-store.ts
107
+ import { appendFileSync as appendFileSync2, existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
108
+ function appendActivity(entry) {
109
+ ensureDir();
110
+ const line = JSON.stringify(entry) + "\n";
111
+ appendFileSync2(ACTIVITY_PATH, line, { encoding: "utf8", mode: 384 });
112
+ }
113
+ function loadActivity() {
114
+ if (!existsSync3(ACTIVITY_PATH)) return [];
115
+ try {
116
+ const content = readFileSync2(ACTIVITY_PATH, "utf8");
117
+ return content.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
122
+ var init_activity_store = __esm({
123
+ "src/storage/activity-store.ts"() {
124
+ "use strict";
125
+ init_paths();
126
+ }
127
+ });
128
+
129
+ // src/storage/badge-store.ts
130
+ function loadBadges() {
131
+ return readJson(BADGES_PATH) ?? { ...DEFAULT_BADGES };
132
+ }
133
+ function saveBadges(badges) {
134
+ ensureDir();
135
+ writeJson(BADGES_PATH, badges);
136
+ }
137
+ function appendBadges(newBadges) {
138
+ if (newBadges.length === 0) return;
139
+ const current = loadBadges();
140
+ current.earned.push(...newBadges);
141
+ saveBadges(current);
142
+ }
143
+ var DEFAULT_BADGES;
144
+ var init_badge_store = __esm({
145
+ "src/storage/badge-store.ts"() {
146
+ "use strict";
147
+ init_atomic();
148
+ init_paths();
149
+ DEFAULT_BADGES = { earned: [] };
150
+ }
151
+ });
152
+
153
+ // src/core/date-utils.ts
154
+ import dayjs from "dayjs";
155
+ import utc from "dayjs/plugin/utc.js";
156
+ import timezone from "dayjs/plugin/timezone.js";
157
+ function todayInTz(tz) {
158
+ return dayjs().tz(tz).format("YYYY-MM-DD");
159
+ }
160
+ function dateInTz(isoTimestamp, tz) {
161
+ return dayjs(isoTimestamp).tz(tz).format("YYYY-MM-DD");
162
+ }
163
+ function daysBetween(a, b) {
164
+ const da = dayjs(a, "YYYY-MM-DD");
165
+ const db = dayjs(b, "YYYY-MM-DD");
166
+ return db.diff(da, "day");
167
+ }
168
+ function nowIso() {
169
+ return (/* @__PURE__ */ new Date()).toISOString();
170
+ }
171
+ var init_date_utils = __esm({
172
+ "src/core/date-utils.ts"() {
173
+ "use strict";
174
+ dayjs.extend(utc);
175
+ dayjs.extend(timezone);
176
+ }
177
+ });
178
+
179
+ // src/core/milestone-checker.ts
180
+ function getAllMilestones() {
181
+ return MILESTONES;
182
+ }
183
+ function checkNewMilestones(newStreak, badges) {
184
+ const all = getAllMilestones();
185
+ const earnedIds = new Set(badges.earned.map((b) => b.milestoneId));
186
+ return all.filter((m) => m.streakRequired <= newStreak && !earnedIds.has(m.id));
187
+ }
188
+ function createEarnedBadges(milestones, streak, nowIso2) {
189
+ return milestones.map((m) => ({
190
+ milestoneId: m.id,
191
+ earnedAt: nowIso2,
192
+ streakAtEarning: streak
193
+ }));
194
+ }
195
+ function totalFreezeReward(milestones) {
196
+ return milestones.reduce((sum, m) => sum + m.freezeTokenReward, 0);
197
+ }
198
+ var MILESTONES;
199
+ var init_milestone_checker = __esm({
200
+ "src/core/milestone-checker.ts"() {
201
+ "use strict";
202
+ MILESTONES = [
203
+ { id: "streak_3", name: "Warming Up", description: "3 days of vibe coding", streakRequired: 3, icon: "\u{1F331}", rarity: "common", freezeTokenReward: 0 },
204
+ { id: "streak_7", name: "Week Warrior", description: "7 days straight \u2014 that's a real habit", streakRequired: 7, icon: "\u26A1", rarity: "common", freezeTokenReward: 0 },
205
+ { id: "streak_14", name: "Fortnight Coder", description: "Two weeks of consistent vibe coding", streakRequired: 14, icon: "\u{1F680}", rarity: "common", freezeTokenReward: 0 },
206
+ { id: "streak_30", name: "Monthly Builder", description: "A full month of showing up every day", streakRequired: 30, icon: "\u{1F3C6}", rarity: "rare", freezeTokenReward: 1 },
207
+ { id: "streak_60", name: "Two Month Grind", description: "60 days \u2014 you're not messing around", streakRequired: 60, icon: "\u{1F48E}", rarity: "rare", freezeTokenReward: 1 },
208
+ { id: "streak_90", name: "Quarter Strong", description: "Three months of daily vibe coding", streakRequired: 90, icon: "\u{1F525}", rarity: "epic", freezeTokenReward: 1 },
209
+ { id: "streak_100", name: "Triple Digits", description: "100 days. This is who you are.", streakRequired: 100, icon: "\u{1F4AF}", rarity: "epic", freezeTokenReward: 0 },
210
+ { id: "streak_180", name: "Half Year Vibe", description: "Six months of consistent AI-assisted coding", streakRequired: 180, icon: "\u{1F31F}", rarity: "legendary", freezeTokenReward: 0 },
211
+ { id: "streak_365", name: "Year of the Vibe", description: "365 days. Legendary status achieved.", streakRequired: 365, icon: "\u{1F451}", rarity: "legendary", freezeTokenReward: 0 }
212
+ ];
213
+ }
214
+ });
215
+
216
+ // src/storage/friends-store.ts
217
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync6 } from "fs";
218
+ function getPublishEndpoint() {
219
+ if (VIBECHK_SERVER) return VIBECHK_SERVER;
220
+ return loadFriends().publishEndpoint ?? null;
221
+ }
222
+ function loadFriends() {
223
+ if (!existsSync6(FRIENDS_PATH)) return { ...DEFAULT };
224
+ try {
225
+ return JSON.parse(readFileSync4(FRIENDS_PATH, "utf8"));
226
+ } catch {
227
+ return { ...DEFAULT };
228
+ }
229
+ }
230
+ function saveFriends(data) {
231
+ ensureDir();
232
+ writeFileSync2(FRIENDS_PATH, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
233
+ }
234
+ function getFriendByAlias(alias) {
235
+ const { friends } = loadFriends();
236
+ return friends.find((f) => f.alias.toLowerCase() === alias.toLowerCase()) ?? null;
237
+ }
238
+ function loadGistToken() {
239
+ if (!existsSync6(GIST_TOKEN_PATH)) return null;
240
+ try {
241
+ const t = readFileSync4(GIST_TOKEN_PATH, "utf8").trim();
242
+ return t || null;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ function saveGistToken(token) {
248
+ ensureDir();
249
+ writeFileSync2(GIST_TOKEN_PATH, token, { encoding: "utf8", mode: 384 });
250
+ }
251
+ var DEFAULT;
252
+ var init_friends_store = __esm({
253
+ "src/storage/friends-store.ts"() {
254
+ "use strict";
255
+ init_paths();
256
+ DEFAULT = {
257
+ version: 1,
258
+ friends: [],
259
+ myPublishUrl: null,
260
+ gistId: null,
261
+ publishEndpoint: null
262
+ };
263
+ }
264
+ });
265
+
266
+ // src/commands/publish.ts
267
+ var publish_exports = {};
268
+ __export(publish_exports, {
269
+ buildPublicProfile: () => buildPublicProfile,
270
+ runPublish: () => runPublish
271
+ });
272
+ import chalk2 from "chalk";
273
+ import { input } from "@inquirer/prompts";
274
+ function buildPublicProfile(username, currentStreak, longestStreak, lastActiveDate, totalCheckIns, badges, activeLast30) {
275
+ return {
276
+ version: 1,
277
+ username,
278
+ currentStreak,
279
+ longestStreak,
280
+ lastActiveDate,
281
+ consistencyLast30: Math.round(activeLast30 / 30 * 100),
282
+ totalCheckIns,
283
+ badges,
284
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString()
285
+ };
286
+ }
287
+ async function runPublish(options = {}) {
288
+ const profile = requireProfile();
289
+ const streak = loadStreak();
290
+ const activity = loadActivity();
291
+ const badges = loadBadges();
292
+ const today = todayInTz(profile.timezone);
293
+ const thirtyDaysAgo = /* @__PURE__ */ new Date();
294
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
295
+ const cutoff = thirtyDaysAgo.toISOString().slice(0, 10);
296
+ const activeLast30 = activity.filter((a) => a.date >= cutoff && a.date <= today).length;
297
+ const payload = buildPublicProfile(
298
+ profile.username,
299
+ streak.currentStreak,
300
+ streak.longestStreak,
301
+ streak.lastActivityDate,
302
+ streak.totalCheckIns,
303
+ badges.earned.map((b) => b.milestoneId),
304
+ activeLast30
305
+ );
306
+ const json = JSON.stringify(payload, null, 2);
307
+ if (options.stdout) {
308
+ console.log(json);
309
+ return null;
310
+ }
311
+ if (options.gist) {
312
+ return publishToGist(profile, json, options);
313
+ }
314
+ const endpoint = options.endpoint ? options.endpoint.replace(/\/$/, "") : getPublishEndpoint();
315
+ if (endpoint) {
316
+ return publishToEndpoint(profile, json, endpoint, options.silent ?? false);
317
+ }
318
+ return publishToGist(profile, json, options);
319
+ }
320
+ async function publishToEndpoint(profile, json, endpoint, silent) {
321
+ try {
322
+ if (!silent) process.stdout.write(chalk2.dim(` Publishing to ${endpoint}...`));
323
+ const res = await fetch(`${endpoint}/api/users/${profile.id}`, {
324
+ method: "PUT",
325
+ headers: {
326
+ "Content-Type": "application/json",
327
+ Authorization: `Bearer ${profile.id}`,
328
+ "User-Agent": "vibechk/0.1.0"
329
+ },
330
+ body: json,
331
+ signal: AbortSignal.timeout(1e4)
332
+ });
333
+ if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
334
+ const data = await res.json();
335
+ const friends = loadFriends();
336
+ friends.myPublishUrl = data.jsonUrl;
337
+ friends.publishEndpoint = endpoint;
338
+ saveFriends(friends);
339
+ if (!silent) {
340
+ console.log(chalk2.green(" \u2713"));
341
+ console.log("");
342
+ console.log(` ${chalk2.bold("Your profile:")} ${chalk2.cyan(data.url)}`);
343
+ console.log("");
344
+ console.log(chalk2.dim(" Friends on the same server can add you by username:"));
345
+ console.log(chalk2.dim(` vibechk friend add ${profile.username}`));
346
+ console.log("");
347
+ console.log(chalk2.dim(" Or share your direct URL:"));
348
+ console.log(chalk2.dim(` vibechk friend add ${profile.username} ${data.jsonUrl}`));
349
+ console.log("");
350
+ }
351
+ return data.jsonUrl;
352
+ } catch (err) {
353
+ if (!silent) {
354
+ console.log(chalk2.red(` \u2717 ${err.message}`));
355
+ console.log(chalk2.dim("\n Check that the endpoint is reachable and supports the vibechk API."));
356
+ console.log(chalk2.dim(" Use --gist to publish via GitHub Gist instead."));
357
+ }
358
+ return null;
359
+ }
360
+ }
361
+ async function publishToGist(profile, json, options) {
362
+ let token = options.token ?? loadGistToken();
363
+ if (!token) {
364
+ if (options.silent) return null;
365
+ console.log("");
366
+ console.log(chalk2.bold(" Publish your streak via GitHub Gist"));
367
+ console.log("");
368
+ console.log(chalk2.dim(" This creates a public JSON file that friends can subscribe to."));
369
+ console.log(chalk2.dim(" You need a GitHub personal access token with the `gist` scope."));
370
+ console.log("");
371
+ console.log(chalk2.dim(" 1. Go to https://github.com/settings/tokens/new?scopes=gist"));
372
+ console.log(chalk2.dim(' 2. Create a token with just the "gist" scope'));
373
+ console.log(chalk2.dim(" 3. Paste it below"));
374
+ console.log("");
375
+ token = await input({ message: "GitHub token (gist scope):" });
376
+ if (!token.trim()) {
377
+ console.log(chalk2.dim(" Skipped. Run `vibechk publish` again when ready."));
378
+ return null;
379
+ }
380
+ saveGistToken(token.trim());
381
+ token = token.trim();
382
+ }
383
+ const friends = loadFriends();
384
+ try {
385
+ if (!options.silent) process.stdout.write(chalk2.dim(" Publishing to Gist..."));
386
+ const { gistId, rawUrl } = friends.gistId ? await updateGist(friends.gistId, json, token) : await createGist(json, token);
387
+ friends.gistId = gistId;
388
+ friends.myPublishUrl = rawUrl;
389
+ saveFriends(friends);
390
+ if (!options.silent) {
391
+ console.log(chalk2.green(" \u2713"));
392
+ console.log("");
393
+ console.log(` ${chalk2.bold("Your friend URL:")} ${chalk2.cyan(rawUrl)}`);
394
+ console.log("");
395
+ console.log(chalk2.dim(" Share this URL with friends so they can follow your streak:"));
396
+ console.log(chalk2.dim(` vibechk friend add ${profile.username} ${rawUrl}`));
397
+ console.log("");
398
+ }
399
+ return rawUrl;
400
+ } catch (err) {
401
+ if (!options.silent) {
402
+ console.log(chalk2.red(` \u2717 ${err.message}`));
403
+ if (err.message.includes("401") || err.message.includes("403")) {
404
+ console.log(chalk2.dim("\n Token may be expired or missing the gist scope."));
405
+ console.log(chalk2.dim(" Delete ~/.vibechk/gist-token and run `vibechk publish` again."));
406
+ }
407
+ }
408
+ return null;
409
+ }
410
+ }
411
+ async function createGist(json, token) {
412
+ const res = await fetch(`${GIST_API}/gists`, {
413
+ method: "POST",
414
+ headers: gistHeaders(token),
415
+ body: JSON.stringify({
416
+ description: "vibechk \u2014 my vibe coding streak",
417
+ public: true,
418
+ files: { [GIST_FILENAME]: { content: json } }
419
+ }),
420
+ signal: AbortSignal.timeout(1e4)
421
+ });
422
+ if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`);
423
+ const data = await res.json();
424
+ const login = data.owner?.login ?? "me";
425
+ const rawUrl = `https://gist.githubusercontent.com/${login}/${data.id}/raw/${GIST_FILENAME}`;
426
+ return { gistId: data.id, rawUrl, login };
427
+ }
428
+ async function updateGist(gistId, json, token) {
429
+ const res = await fetch(`${GIST_API}/gists/${gistId}`, {
430
+ method: "PATCH",
431
+ headers: gistHeaders(token),
432
+ body: JSON.stringify({
433
+ files: { [GIST_FILENAME]: { content: json } }
434
+ }),
435
+ signal: AbortSignal.timeout(1e4)
436
+ });
437
+ if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`);
438
+ const data = await res.json();
439
+ const login = data.owner?.login ?? "me";
440
+ const rawUrl = `https://gist.githubusercontent.com/${login}/${data.id}/raw/${GIST_FILENAME}`;
441
+ return { gistId: data.id, rawUrl, login };
442
+ }
443
+ function gistHeaders(token) {
444
+ return {
445
+ Authorization: `Bearer ${token}`,
446
+ Accept: "application/vnd.github+json",
447
+ "Content-Type": "application/json",
448
+ "User-Agent": "vibechk/0.1.0",
449
+ "X-GitHub-Api-Version": "2022-11-28"
450
+ };
451
+ }
452
+ var GIST_API, GIST_FILENAME;
453
+ var init_publish = __esm({
454
+ "src/commands/publish.ts"() {
455
+ "use strict";
456
+ init_profile_store();
457
+ init_streak_store();
458
+ init_activity_store();
459
+ init_badge_store();
460
+ init_friends_store();
461
+ init_date_utils();
462
+ GIST_API = "https://api.github.com";
463
+ GIST_FILENAME = "vibechk.json";
464
+ }
465
+ });
466
+
467
+ // src/commands/friend.ts
468
+ var friend_exports = {};
469
+ __export(friend_exports, {
470
+ runFriendAdd: () => runFriendAdd,
471
+ runFriendList: () => runFriendList,
472
+ runFriendPull: () => runFriendPull,
473
+ runFriendRemove: () => runFriendRemove
474
+ });
475
+ import chalk3 from "chalk";
476
+ async function runFriendAdd(alias, url) {
477
+ const normalized = alias.trim().toLowerCase();
478
+ if (!normalized || !/^[\w\-\.]+$/.test(normalized)) {
479
+ console.error(chalk3.red(" Alias must be letters, numbers, - or _ only."));
480
+ process.exit(1);
481
+ }
482
+ const existing = getFriendByAlias(normalized);
483
+ if (existing) {
484
+ console.log(chalk3.yellow(` Already following "${normalized}" at ${existing.url}`));
485
+ console.log(chalk3.dim(` Use \`vibechk friend remove ${normalized}\` first to replace them.`));
486
+ return;
487
+ }
488
+ let resolvedUrl;
489
+ if (url?.trim()) {
490
+ resolvedUrl = url.trim();
491
+ } else {
492
+ const endpoint = getPublishEndpoint();
493
+ if (endpoint) {
494
+ resolvedUrl = `${endpoint}/u/${normalized}.json`;
495
+ } else {
496
+ console.error(chalk3.red(" No URL provided and no remote endpoint configured."));
497
+ console.log("");
498
+ console.log(chalk3.dim(" Provide the friend's profile URL:"));
499
+ console.log(chalk3.dim(` vibechk friend add ${normalized} https://...`));
500
+ console.log("");
501
+ console.log(chalk3.dim(" Or set a remote endpoint via the VIBECHK_SERVER env var."));
502
+ process.exit(1);
503
+ }
504
+ }
505
+ const data = loadFriends();
506
+ const entry = {
507
+ alias: normalized,
508
+ url: resolvedUrl,
509
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
510
+ lastFetchedAt: null,
511
+ cached: null
512
+ };
513
+ data.friends.push(entry);
514
+ saveFriends(data);
515
+ console.log(chalk3.green(`
516
+ \u2713 Added ${normalized}.`));
517
+ console.log(chalk3.dim(" Fetching their streak now...\n"));
518
+ const result = await fetchOneFriend(entry);
519
+ if (result.cached) {
520
+ printFriendRow(result, todayInTz(requireProfile().timezone), true);
521
+ } else {
522
+ console.log(chalk3.yellow(` Could not fetch their data: ${result.lastFetchError ?? "unknown error"}`));
523
+ console.log(chalk3.dim(" Their data will be retried on the next `vibechk friend pull`."));
524
+ }
525
+ console.log("");
526
+ }
527
+ function runFriendRemove(alias) {
528
+ const normalized = alias.trim().toLowerCase();
529
+ const data = loadFriends();
530
+ const idx = data.friends.findIndex((f) => f.alias.toLowerCase() === normalized);
531
+ if (idx === -1) {
532
+ console.log(chalk3.yellow(` No friend found with alias "${normalized}".`));
533
+ return;
534
+ }
535
+ data.friends.splice(idx, 1);
536
+ saveFriends(data);
537
+ console.log(chalk3.green(`
538
+ \u2713 Removed ${normalized}.
539
+ `));
540
+ }
541
+ async function runFriendPull(options = {}) {
542
+ const data = loadFriends();
543
+ if (data.friends.length === 0) {
544
+ if (!options.quiet) {
545
+ console.log(chalk3.dim("\n No friends added yet. Run `vibechk friend add <alias> <url>`.\n"));
546
+ }
547
+ return;
548
+ }
549
+ if (!options.quiet) process.stdout.write(chalk3.dim(` Syncing ${data.friends.length} friend(s)...`));
550
+ const updated = await Promise.all(data.friends.map(fetchOneFriend));
551
+ data.friends = updated;
552
+ saveFriends(data);
553
+ if (!options.quiet) {
554
+ const ok = updated.filter((f) => f.cached !== null).length;
555
+ console.log(chalk3.green(` ${ok}/${updated.length} updated.
556
+ `));
557
+ }
558
+ }
559
+ async function runFriendList() {
560
+ const profile = requireProfile();
561
+ const myStreak = loadStreak();
562
+ const today = todayInTz(profile.timezone);
563
+ let data = loadFriends();
564
+ if (data.friends.length === 0) {
565
+ console.log("");
566
+ console.log(chalk3.dim(" No friends yet.\n"));
567
+ console.log(" Share your streak URL with friends and add theirs:");
568
+ if (data.myPublishUrl) {
569
+ console.log("");
570
+ console.log(` Your URL: ${chalk3.cyan(data.myPublishUrl)}`);
571
+ } else {
572
+ console.log(chalk3.dim(" Run `vibechk publish` to generate your shareable URL first."));
573
+ }
574
+ console.log(chalk3.dim("\n vibechk friend add <alias> <their-url>\n"));
575
+ return;
576
+ }
577
+ const needsRefresh = data.friends.some((f) => isStale(f) || !f.lastFetchedAt);
578
+ if (needsRefresh) {
579
+ process.stdout.write(chalk3.dim(` Refreshing ${data.friends.length} friend(s)...`));
580
+ const updated = await Promise.all(data.friends.map(fetchOneFriend));
581
+ data.friends = updated;
582
+ saveFriends(data);
583
+ const ok = updated.filter((f) => f.cached !== null).length;
584
+ console.log(chalk3.green(` ${ok}/${updated.length} updated.`));
585
+ }
586
+ console.log("");
587
+ const sorted = [...data.friends].sort((a, b) => {
588
+ const aToday = a.cached?.lastActiveDate === today ? 1 : 0;
589
+ const bToday = b.cached?.lastActiveDate === today ? 1 : 0;
590
+ if (bToday !== aToday) return bToday - aToday;
591
+ return (b.cached?.currentStreak ?? -1) - (a.cached?.currentStreak ?? -1);
592
+ });
593
+ const COL = { alias: 14, streak: 9, today: 8, consistency: 8, best: 7 };
594
+ const header = chalk3.dim(padR(" name", COL.alias)) + chalk3.dim(padR("streak", COL.streak)) + chalk3.dim(padR("today", COL.today)) + chalk3.dim(padR("30d%", COL.consistency)) + chalk3.dim("best");
595
+ console.log(header);
596
+ console.log(chalk3.dim(" " + "\u2500".repeat(48)));
597
+ for (const friend of sorted) {
598
+ printFriendRow(friend, today, false);
599
+ }
600
+ console.log(chalk3.dim(" " + "\u2500".repeat(48)));
601
+ const myCheckedIn = myStreak.lastActivityDate === today;
602
+ const myEmoji = myStreak.currentStreak >= 30 ? "\u{1F3C6}" : myStreak.currentStreak > 0 ? "\u{1F525}" : "\u{1F331}";
603
+ const myTodayLabel = myCheckedIn ? chalk3.green("\u2713") : chalk3.yellow("\u25CB");
604
+ console.log(
605
+ padR(` ${chalk3.bold("you")}`, COL.alias) + padR(`${myEmoji} ${myStreak.currentStreak}d`, COL.streak) + padR(myTodayLabel, COL.today) + padR(chalk3.dim("\u2014"), COL.consistency) + chalk3.dim(`${myStreak.longestStreak}d`)
606
+ );
607
+ console.log("");
608
+ if (data.friends.some((f) => f.lastFetchedAt)) {
609
+ const oldest = data.friends.filter((f) => f.lastFetchedAt).sort((a, b) => a.lastFetchedAt.localeCompare(b.lastFetchedAt)).at(0);
610
+ const age = Math.round((Date.now() - new Date(oldest.lastFetchedAt).getTime()) / 6e4);
611
+ console.log(chalk3.dim(` Last synced: ${age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`}`));
612
+ }
613
+ if (data.myPublishUrl) {
614
+ console.log(chalk3.dim(` Your URL: ${data.myPublishUrl}`));
615
+ } else {
616
+ console.log(chalk3.dim(" Run `vibechk publish` so friends can follow you."));
617
+ }
618
+ console.log("");
619
+ }
620
+ async function fetchOneFriend(entry) {
621
+ try {
622
+ const res = await fetch(entry.url, {
623
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
624
+ headers: { "User-Agent": "vibechk/0.1.0", Accept: "application/json" }
625
+ });
626
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
627
+ const json = await res.json();
628
+ if (typeof json.username !== "string" || typeof json.currentStreak !== "number") {
629
+ throw new Error("Invalid profile format");
630
+ }
631
+ return {
632
+ ...entry,
633
+ lastFetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
634
+ lastFetchError: void 0,
635
+ cached: json
636
+ };
637
+ } catch (err) {
638
+ return {
639
+ ...entry,
640
+ lastFetchError: err.message ?? "fetch failed"
641
+ // preserve old cached data — stale is better than empty
642
+ };
643
+ }
644
+ }
645
+ function printFriendRow(friend, today, verbose) {
646
+ const c = friend.cached;
647
+ const COL = { alias: 14, streak: 9, today: 8, consistency: 8 };
648
+ if (!c) {
649
+ const errLabel = friend.lastFetchError ? chalk3.red(`\u26A0 ${friend.lastFetchError.slice(0, 30)}`) : chalk3.dim("fetching...");
650
+ console.log(padR(` ${chalk3.bold(friend.alias)}`, COL.alias) + errLabel);
651
+ return;
652
+ }
653
+ const streakEmoji = c.currentStreak >= 30 ? "\u{1F3C6}" : c.currentStreak > 0 ? "\u{1F525}" : "\u{1F331}";
654
+ const checkedInToday = c.lastActiveDate === today;
655
+ const todayLabel = checkedInToday ? chalk3.green("\u2713") : chalk3.dim("\u25CB");
656
+ const consistencyColor = c.consistencyLast30 >= 80 ? chalk3.green : c.consistencyLast30 >= 50 ? chalk3.yellow : chalk3.red;
657
+ const prevNote = c.longestStreak > c.currentStreak && c.longestStreak > 0 ? chalk3.dim(` prev ${c.longestStreak}d`) : "";
658
+ const staleNote = isStale(friend) ? chalk3.yellow(" \u26A0") : "";
659
+ const theirName = c.username !== friend.alias ? chalk3.dim(` (${c.username})`) : "";
660
+ if (verbose) {
661
+ console.log(` ${streakEmoji} ${chalk3.bold(friend.alias)}${theirName}: ${chalk3.yellow.bold(c.currentStreak + " days")}`);
662
+ console.log(` Today: ${checkedInToday ? chalk3.green("checked in \u2713") : chalk3.dim("not yet \u25CB")}`);
663
+ console.log(` 30-day consistency: ${consistencyColor(c.consistencyLast30 + "%")}`);
664
+ console.log(` Best streak: ${c.longestStreak}d Check-ins: ${c.totalCheckIns}`);
665
+ const milestones = getAllMilestones();
666
+ const earned = c.badges.map((id) => milestones.find((m) => m.id === id)?.icon ?? "").filter(Boolean);
667
+ if (earned.length) console.log(` Badges: ${earned.join(" ")}`);
668
+ } else {
669
+ console.log(
670
+ padR(` ${chalk3.bold(friend.alias)}${theirName}`, COL.alias) + padR(`${streakEmoji} ${c.currentStreak}d${prevNote}`, COL.streak) + padR(todayLabel, COL.today) + padR(consistencyColor(c.consistencyLast30 + "%"), COL.consistency) + chalk3.dim(c.longestStreak + "d") + staleNote
671
+ );
672
+ }
673
+ }
674
+ function isStale(friend) {
675
+ if (!friend.lastFetchedAt) return false;
676
+ const ageMs = Date.now() - new Date(friend.lastFetchedAt).getTime();
677
+ return ageMs > STALE_HOURS * 3600 * 1e3;
678
+ }
679
+ function padR(s, width) {
680
+ const plainLen = s.replace(/\x1B\[[0-9;]*m/g, "").length;
681
+ const pad = Math.max(0, width - plainLen);
682
+ return s + " ".repeat(pad);
683
+ }
684
+ var FETCH_TIMEOUT_MS, STALE_HOURS;
685
+ var init_friend = __esm({
686
+ "src/commands/friend.ts"() {
687
+ "use strict";
688
+ init_friends_store();
689
+ init_profile_store();
690
+ init_streak_store();
691
+ init_date_utils();
692
+ init_milestone_checker();
693
+ FETCH_TIMEOUT_MS = 8e3;
694
+ STALE_HOURS = 25;
695
+ }
696
+ });
697
+
698
+ // src/commands/check-in.ts
699
+ init_profile_store();
700
+ init_streak_store();
701
+ init_activity_store();
702
+ init_badge_store();
703
+ init_badge_store();
704
+ init_date_utils();
705
+ import chalk4 from "chalk";
706
+ import ora from "ora";
707
+ import { confirm } from "@inquirer/prompts";
708
+ import { v4 as uuidv4 } from "uuid";
709
+
710
+ // src/core/streak-calculator.ts
711
+ init_date_utils();
712
+ var GRACE_COOLDOWN_DAYS = 14;
713
+ var MAX_FREEZE_TOKENS = 5;
714
+ function calculateStreakImpact(streak, today, useFreeze = false) {
715
+ const { lastActivityDate, currentStreak, graceUsedAt, freezeTokens } = streak;
716
+ if (!lastActivityDate) {
717
+ return { action: "started", newStreak: 1 };
718
+ }
719
+ const daysDiff = daysBetween(lastActivityDate, today);
720
+ if (daysDiff === 0) {
721
+ return { action: "already_checked_in", newStreak: currentStreak };
722
+ }
723
+ if (daysDiff === 1) {
724
+ return { action: "continued", newStreak: currentStreak + 1 };
725
+ }
726
+ if (daysDiff === 2) {
727
+ const graceAvailable = !graceUsedAt || daysBetween(graceUsedAt, today) >= GRACE_COOLDOWN_DAYS;
728
+ if (graceAvailable) {
729
+ return { action: "grace", newStreak: currentStreak + 1, usedGrace: true };
730
+ }
731
+ if (useFreeze && freezeTokens > 0) {
732
+ return { action: "frozen", newStreak: currentStreak + 1, usedFreeze: true };
733
+ }
734
+ if (freezeTokens > 0 && !useFreeze) {
735
+ return {
736
+ action: "broken",
737
+ newStreak: 1,
738
+ previousStreak: currentStreak
739
+ };
740
+ }
741
+ }
742
+ return {
743
+ action: "broken",
744
+ newStreak: 1,
745
+ previousStreak: currentStreak
746
+ };
747
+ }
748
+ function applyStreakImpact(current, impact, today, nowIso2) {
749
+ const newStreak = impact.newStreak;
750
+ const newLongest = Math.max(current.longestStreak, newStreak);
751
+ let freezeTokens = current.freezeTokens;
752
+ if (impact.usedFreeze) {
753
+ freezeTokens = Math.max(0, freezeTokens - 1);
754
+ }
755
+ let graceUsedAt = current.graceUsedAt;
756
+ if (impact.usedGrace) {
757
+ graceUsedAt = today;
758
+ }
759
+ let status = "active";
760
+ if (impact.action === "frozen") status = "frozen";
761
+ else if (impact.action === "grace") status = "grace";
762
+ else if (impact.action === "broken") status = "broken";
763
+ else if (impact.action === "started" || impact.action === "continued" || impact.action === "already_checked_in") status = "active";
764
+ return {
765
+ ...current,
766
+ currentStreak: newStreak,
767
+ longestStreak: newLongest,
768
+ lastActivityDate: impact.action === "already_checked_in" ? current.lastActivityDate : today,
769
+ lastCheckInAt: nowIso2,
770
+ freezeTokens,
771
+ totalCheckIns: impact.action === "already_checked_in" ? current.totalCheckIns : current.totalCheckIns + 1,
772
+ graceUsedAt,
773
+ status
774
+ };
775
+ }
776
+ function addFreezeTokens(current, toAdd) {
777
+ return Math.min(MAX_FREEZE_TOKENS, current + toAdd);
778
+ }
779
+
780
+ // src/commands/check-in.ts
781
+ init_milestone_checker();
782
+
783
+ // src/detection/claude-code.ts
784
+ init_date_utils();
785
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
786
+ import { join as join2 } from "path";
787
+ import { homedir as homedir2 } from "os";
788
+ import { execSync } from "child_process";
789
+ var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".config", "claude", "projects");
790
+ async function detectClaudeCodeUsage(date, timezone3) {
791
+ const ccusageResult = tryCcusageCli(date);
792
+ if (ccusageResult) return ccusageResult;
793
+ return scanJsonlFiles(date, timezone3);
794
+ }
795
+ function tryCcusageCli(date) {
796
+ try {
797
+ const output = execSync(`ccusage daily --date ${date} --json 2>/dev/null`, {
798
+ timeout: 5e3,
799
+ encoding: "utf8"
800
+ });
801
+ const data = JSON.parse(output);
802
+ const entries = Array.isArray(data) ? data : [data];
803
+ const total = entries.reduce((sum, e) => sum + (e.totalTokens || e.tokens || 0), 0);
804
+ if (total > 0) {
805
+ return {
806
+ detected: true,
807
+ source: "ccusage-cli",
808
+ agentData: {
809
+ sessions: entries.length,
810
+ tokensApprox: total,
811
+ models: [...new Set(entries.map((e) => e.model).filter(Boolean))]
812
+ }
813
+ };
814
+ }
815
+ return { detected: false, source: "ccusage-cli" };
816
+ } catch {
817
+ return null;
818
+ }
819
+ }
820
+ function scanJsonlFiles(date, timezone3) {
821
+ if (!existsSync4(CLAUDE_PROJECTS_DIR)) {
822
+ return { detected: false, source: "none" };
823
+ }
824
+ let sessionCount = 0;
825
+ let tokenCount = 0;
826
+ try {
827
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR);
828
+ for (const project of projectDirs) {
829
+ const projectPath = join2(CLAUDE_PROJECTS_DIR, project);
830
+ if (!statSync(projectPath).isDirectory()) continue;
831
+ const files = readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
832
+ for (const file of files) {
833
+ const filePath = join2(projectPath, file);
834
+ const content = readFileSync3(filePath, "utf8");
835
+ const lines = content.split("\n").filter((l) => l.trim());
836
+ let fileHasActivity = false;
837
+ for (const line of lines) {
838
+ try {
839
+ const entry = JSON.parse(line);
840
+ const ts = entry.timestamp || entry.createdAt || entry.created_at;
841
+ if (!ts) continue;
842
+ const entryDate = dateInTz(ts, timezone3);
843
+ if (entryDate === date) {
844
+ fileHasActivity = true;
845
+ if (typeof entry.usage?.input_tokens === "number") {
846
+ tokenCount += entry.usage.input_tokens + (entry.usage.output_tokens || 0);
847
+ } else if (typeof entry.content === "string") {
848
+ tokenCount += Math.floor(entry.content.length / 4);
849
+ }
850
+ }
851
+ } catch {
852
+ }
853
+ }
854
+ if (fileHasActivity) sessionCount++;
855
+ }
856
+ }
857
+ } catch {
858
+ return { detected: false, source: "none" };
859
+ }
860
+ if (sessionCount > 0) {
861
+ return {
862
+ detected: true,
863
+ source: "jsonl-scan",
864
+ agentData: { sessions: sessionCount, tokensApprox: tokenCount }
865
+ };
866
+ }
867
+ return { detected: false, source: "jsonl-scan" };
868
+ }
869
+
870
+ // src/detection/git.ts
871
+ import { execSync as execSync2 } from "child_process";
872
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
873
+ import { join as join3 } from "path";
874
+ import { homedir as homedir3 } from "os";
875
+ async function detectGitActivity(date, timezone3, watchedRepos) {
876
+ const repoPaths = watchedRepos.length > 0 ? watchedRepos.map(expandHome) : discoverRepos();
877
+ for (const repoPath of repoPaths) {
878
+ if (!existsSync5(join3(repoPath, ".git"))) continue;
879
+ try {
880
+ const since = `${date} 00:00:00`;
881
+ const until = `${date} 23:59:59`;
882
+ const result = execSync2(
883
+ `git -C ${JSON.stringify(repoPath)} log --oneline --after=${JSON.stringify(since)} --before=${JSON.stringify(until)} 2>/dev/null`,
884
+ { encoding: "utf8", timeout: 5e3, env: { ...process.env, TZ: timezone3 } }
885
+ ).trim();
886
+ if (result.length > 0) {
887
+ return { detected: true, repoPath };
888
+ }
889
+ } catch {
890
+ }
891
+ }
892
+ return { detected: false };
893
+ }
894
+ function expandHome(p) {
895
+ if (p.startsWith("~/") || p === "~") {
896
+ return join3(homedir3(), p.slice(2));
897
+ }
898
+ return p;
899
+ }
900
+ function discoverRepos() {
901
+ const candidates = [
902
+ homedir3(),
903
+ join3(homedir3(), "code"),
904
+ join3(homedir3(), "projects"),
905
+ join3(homedir3(), "dev"),
906
+ join3(homedir3(), "workspace"),
907
+ join3(homedir3(), "src")
908
+ ];
909
+ const repos = [];
910
+ for (const dir of candidates) {
911
+ if (!existsSync5(dir)) continue;
912
+ if (existsSync5(join3(dir, ".git"))) {
913
+ repos.push(dir);
914
+ continue;
915
+ }
916
+ try {
917
+ const entries = readdirSync2(dir);
918
+ for (const entry of entries) {
919
+ const full = join3(dir, entry);
920
+ try {
921
+ if (statSync2(full).isDirectory() && existsSync5(join3(full, ".git"))) {
922
+ repos.push(full);
923
+ }
924
+ } catch {
925
+ }
926
+ }
927
+ } catch {
928
+ }
929
+ if (repos.length >= 50) break;
930
+ }
931
+ return repos;
932
+ }
933
+
934
+ // src/detection/index.ts
935
+ async function detectTodaySession(date, timezone3, enabledSources, watchedRepos = []) {
936
+ if (enabledSources.includes("claude-code")) {
937
+ const result = await detectClaudeCodeUsage(date, timezone3);
938
+ if (result.detected) {
939
+ return { detected: true, source: "claude-code", agentData: result.agentData };
940
+ }
941
+ }
942
+ if (enabledSources.includes("git")) {
943
+ const result = await detectGitActivity(date, timezone3, watchedRepos);
944
+ if (result.detected) {
945
+ return { detected: true, source: "git" };
946
+ }
947
+ }
948
+ return { detected: false, source: "manual" };
949
+ }
950
+
951
+ // src/sync/client.ts
952
+ function makeHeaders(apiKey) {
953
+ return {
954
+ "Content-Type": "application/json",
955
+ Authorization: `Bearer ${apiKey}`,
956
+ "User-Agent": "vibechk/0.1.0"
957
+ };
958
+ }
959
+ function buildSyncPayload(profile, streak, badges) {
960
+ const totalFreezesUsed = streak.totalCheckIns - streak.currentStreak;
961
+ return {
962
+ userId: profile.id,
963
+ username: profile.username,
964
+ currentStreak: streak.currentStreak,
965
+ longestStreak: streak.longestStreak,
966
+ lastActiveDate: streak.lastActivityDate,
967
+ freezesUsed: totalFreezesUsed > 0 ? totalFreezesUsed : 0,
968
+ badges: badges.earned.map((b) => b.milestoneId),
969
+ shareOnLeaderboard: profile.preferences.shareOnLeaderboard
970
+ };
971
+ }
972
+ async function pushStreak(profile, streak, badges) {
973
+ const sync = profile.cloudSync;
974
+ if (!sync) return false;
975
+ const payload = buildSyncPayload(profile, streak, badges);
976
+ try {
977
+ const response = await fetch(`${sync.endpoint}/users/${profile.id}`, {
978
+ method: "PUT",
979
+ headers: makeHeaders(sync.apiKey),
980
+ body: JSON.stringify(payload),
981
+ signal: AbortSignal.timeout(8e3)
982
+ });
983
+ return response.ok;
984
+ } catch {
985
+ return false;
986
+ }
987
+ }
988
+
989
+ // src/ui/renderer.ts
990
+ init_date_utils();
991
+ init_milestone_checker();
992
+ import chalk from "chalk";
993
+ import boxen from "boxen";
994
+ import dayjs2 from "dayjs";
995
+ import utc2 from "dayjs/plugin/utc.js";
996
+ import timezone2 from "dayjs/plugin/timezone.js";
997
+ dayjs2.extend(utc2);
998
+ dayjs2.extend(timezone2);
999
+ function renderCheckIn(action, streak, previousStreak, newMilestones = [], quiet = false) {
1000
+ if (quiet) return "";
1001
+ const lines = [];
1002
+ if (action === "already_checked_in") {
1003
+ lines.push(chalk.green(`\u2713 Already checked in today \u2014 ${streak}-day streak protected.`));
1004
+ return lines.join("\n");
1005
+ }
1006
+ if (action === "broken") {
1007
+ if (previousStreak && previousStreak > 0) {
1008
+ lines.push(chalk.cyan(`Your ${previousStreak}-day streak was real.`));
1009
+ lines.push(chalk.dim(` Every day you showed up counted. That doesn't disappear.`));
1010
+ lines.push(``);
1011
+ }
1012
+ lines.push(chalk.yellow(`\u{1F331} Day 1. Fresh start.`));
1013
+ return boxen(lines.join("\n"), { padding: 1, borderColor: "cyan", borderStyle: "round" });
1014
+ }
1015
+ const actionEmoji = action === "started" ? "\u{1F331}" : action === "grace" ? "\u2728" : action === "frozen" ? "\u2744\uFE0F" : "\u{1F525}";
1016
+ const actionLabel = action === "started" ? "Day 1! The journey begins." : action === "grace" ? `Grace period used \u2014 streak preserved!` : action === "frozen" ? `Freeze token used \u2014 streak preserved!` : `Day ${streak}! Streak protected.`;
1017
+ lines.push(`${actionEmoji} ${chalk.bold(actionLabel)}`);
1018
+ lines.push(chalk.dim(`\u{1F525} ${streak}-day streak`));
1019
+ return lines.join("\n");
1020
+ }
1021
+ function renderMilestone(milestone, streak) {
1022
+ const border = milestone.rarity === "legendary" ? "double" : milestone.rarity === "epic" ? "bold" : "round";
1023
+ const color = milestone.rarity === "legendary" ? "magentaBright" : milestone.rarity === "epic" ? "yellowBright" : "cyan";
1024
+ const lines = [
1025
+ chalk.bold.yellow("\u2B50 ACHIEVEMENT UNLOCKED!"),
1026
+ ``,
1027
+ `${milestone.icon} ${chalk.bold(milestone.name)}`,
1028
+ chalk.dim(milestone.description),
1029
+ ``,
1030
+ chalk.green(`${streak}-day streak reached!`)
1031
+ ];
1032
+ if (milestone.freezeTokenReward > 0) {
1033
+ lines.push(``);
1034
+ lines.push(chalk.cyan(`+ ${milestone.freezeTokenReward} freeze token earned!`));
1035
+ }
1036
+ return boxen(lines.join("\n"), {
1037
+ padding: 1,
1038
+ borderStyle: border,
1039
+ borderColor: color,
1040
+ title: ` ${milestone.rarity.toUpperCase()} `,
1041
+ titleAlignment: "center"
1042
+ });
1043
+ }
1044
+
1045
+ // src/web/server.ts
1046
+ init_profile_store();
1047
+ init_streak_store();
1048
+ init_activity_store();
1049
+ init_badge_store();
1050
+ import { createServer } from "http";
1051
+
1052
+ // src/storage/leaderboard-cache.ts
1053
+ init_atomic();
1054
+ init_paths();
1055
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
1056
+ function loadLeaderboardCache() {
1057
+ return readJson(LEADERBOARD_CACHE_PATH);
1058
+ }
1059
+
1060
+ // src/web/server.ts
1061
+ init_milestone_checker();
1062
+ init_date_utils();
1063
+ async function startDashboard(autoClose = true) {
1064
+ const profile = requireProfile();
1065
+ const streak = loadStreak();
1066
+ const activity = loadActivity();
1067
+ const badges = loadBadges();
1068
+ const leaderboard = loadLeaderboardCache();
1069
+ const milestones = getAllMilestones();
1070
+ const today = todayInTz(profile.timezone);
1071
+ const dashboardData = {
1072
+ profile: { username: profile.username, timezone: profile.timezone },
1073
+ streak,
1074
+ recentActivity: activity.slice(-90),
1075
+ badges: badges.earned,
1076
+ milestones,
1077
+ leaderboard,
1078
+ today
1079
+ };
1080
+ const html = generateDashboardHtml(dashboardData);
1081
+ const server = createServer((req, res) => {
1082
+ if (req.url === "/data") {
1083
+ res.writeHead(200, { "Content-Type": "application/json" });
1084
+ res.end(JSON.stringify(dashboardData));
1085
+ return;
1086
+ }
1087
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1088
+ res.end(html);
1089
+ if (autoClose) {
1090
+ setTimeout(() => server.close(), 500);
1091
+ }
1092
+ });
1093
+ return new Promise((resolve) => {
1094
+ server.listen(0, "127.0.0.1", () => {
1095
+ const addr = server.address();
1096
+ const url = `http://127.0.0.1:${addr.port}`;
1097
+ resolve(url);
1098
+ });
1099
+ });
1100
+ }
1101
+ function generateDashboardHtml(data) {
1102
+ const { profile, streak, recentActivity, badges, milestones, leaderboard, today } = data;
1103
+ const activityMap = {};
1104
+ for (const a of recentActivity) {
1105
+ activityMap[a.date] = { checked: true, frozen: a.isFrozen, grace: a.isGrace, source: a.source };
1106
+ }
1107
+ const cells = [];
1108
+ for (let i = 89; i >= 0; i--) {
1109
+ const d = /* @__PURE__ */ new Date();
1110
+ d.setDate(d.getDate() - i);
1111
+ const dateStr = d.toISOString().slice(0, 10);
1112
+ const act = activityMap[dateStr];
1113
+ const isToday = dateStr === today;
1114
+ let cls = "day-empty";
1115
+ if (act?.frozen) cls = "day-frozen";
1116
+ else if (act?.grace) cls = "day-grace";
1117
+ else if (act?.checked) cls = "day-active";
1118
+ if (isToday) cls += " day-today";
1119
+ cells.push(`<div class="day ${cls}" title="${dateStr}"></div>`);
1120
+ }
1121
+ const lbRows = leaderboard ? leaderboard.entries.slice(0, 20).map((e) => {
1122
+ const isYou = e.userId === profile.id || e.displayName === profile.username;
1123
+ const badges_icons = (e.badges || []).slice(0, 3).map((id) => milestones.find((m) => m.id === id)?.icon ?? "").join("");
1124
+ return `<tr class="${isYou ? "you" : ""}">
1125
+ <td class="rank">${e.rank ?? "?"}</td>
1126
+ <td class="name">${isYou ? "\u2192 " : ""}${e.displayName}</td>
1127
+ <td class="streak">${e.currentStreak}d</td>
1128
+ <td class="longest">${e.longestStreak}d</td>
1129
+ <td class="badges">${badges_icons}${e.freezesUsed > 0 ? `<span class="freeze-used">${e.freezesUsed}\u2744</span>` : ""}</td>
1130
+ </tr>`;
1131
+ }).join("\n") : '<tr><td colspan="5" class="empty">No leaderboard data \u2014 run <code>vibechk sync</code></td></tr>';
1132
+ const earnedIds = new Set(badges.map((b) => b.milestoneId));
1133
+ const milestoneCards = milestones.map((m) => {
1134
+ const earned = earnedIds.has(m.id);
1135
+ return `<div class="milestone-card ${earned ? "earned" : "locked"} rarity-${m.rarity}">
1136
+ <div class="ms-icon">${earned ? m.icon : "\u{1F512}"}</div>
1137
+ <div class="ms-name">${m.name}</div>
1138
+ <div class="ms-req">${m.streakRequired}d</div>
1139
+ </div>`;
1140
+ }).join("\n");
1141
+ const isCheckedInToday = streak.lastActivityDate === today;
1142
+ const streakEmoji = streak.currentStreak >= 100 ? "\u{1F4AF}" : streak.currentStreak >= 30 ? "\u{1F3C6}" : "\u{1F525}";
1143
+ return `<!DOCTYPE html>
1144
+ <html lang="en">
1145
+ <head>
1146
+ <meta charset="UTF-8">
1147
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1148
+ <title>vibechk \u2014 ${profile.username}</title>
1149
+ <style>
1150
+ :root {
1151
+ --bg: #0d1117;
1152
+ --surface: #161b22;
1153
+ --border: #30363d;
1154
+ --text: #e6edf3;
1155
+ --muted: #8b949e;
1156
+ --accent: #f78166;
1157
+ --green: #3fb950;
1158
+ --yellow: #d29922;
1159
+ --blue: #58a6ff;
1160
+ --frozen: #79c0ff;
1161
+ --grace: #ffa657;
1162
+ --purple: #bc8cff;
1163
+ }
1164
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1165
+ body {
1166
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1167
+ background: var(--bg);
1168
+ color: var(--text);
1169
+ min-height: 100vh;
1170
+ padding: 24px;
1171
+ }
1172
+ h1, h2, h3 { font-weight: 600; }
1173
+ .container { max-width: 900px; margin: 0 auto; }
1174
+ header {
1175
+ display: flex;
1176
+ align-items: center;
1177
+ gap: 12px;
1178
+ margin-bottom: 32px;
1179
+ }
1180
+ header .logo {
1181
+ font-size: 1.5rem;
1182
+ font-weight: 700;
1183
+ background: linear-gradient(135deg, #f78166, #d29922);
1184
+ -webkit-background-clip: text;
1185
+ -webkit-text-fill-color: transparent;
1186
+ }
1187
+ header .username {
1188
+ color: var(--muted);
1189
+ font-size: 0.9rem;
1190
+ }
1191
+
1192
+ /* Streak hero card */
1193
+ .streak-hero {
1194
+ background: var(--surface);
1195
+ border: 1px solid var(--border);
1196
+ border-radius: 12px;
1197
+ padding: 28px 32px;
1198
+ margin-bottom: 24px;
1199
+ display: grid;
1200
+ grid-template-columns: 1fr auto;
1201
+ gap: 16px;
1202
+ align-items: center;
1203
+ }
1204
+ .streak-number {
1205
+ font-size: 4rem;
1206
+ font-weight: 700;
1207
+ line-height: 1;
1208
+ color: var(--accent);
1209
+ }
1210
+ .streak-label {
1211
+ color: var(--muted);
1212
+ font-size: 0.9rem;
1213
+ margin-top: 4px;
1214
+ }
1215
+ .streak-stats {
1216
+ display: flex;
1217
+ gap: 24px;
1218
+ }
1219
+ .stat { text-align: center; }
1220
+ .stat-value { font-size: 1.5rem; font-weight: 600; color: var(--blue); }
1221
+ .stat-label { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }
1222
+
1223
+ .status-badge {
1224
+ display: inline-flex;
1225
+ align-items: center;
1226
+ gap: 6px;
1227
+ padding: 4px 12px;
1228
+ border-radius: 20px;
1229
+ font-size: 0.8rem;
1230
+ font-weight: 500;
1231
+ margin-top: 8px;
1232
+ }
1233
+ .status-badge.protected { background: rgba(63, 185, 80, 0.15); color: var(--green); border: 1px solid rgba(63,185,80,0.3); }
1234
+ .status-badge.at-risk { background: rgba(210, 153, 34, 0.15); color: var(--yellow); border: 1px solid rgba(210,153,34,0.3); }
1235
+ .status-badge.broken { background: rgba(247, 129, 102, 0.15); color: var(--accent); border: 1px solid rgba(247,129,102,0.3); }
1236
+
1237
+ .freeze-tokens {
1238
+ font-size: 1.2rem;
1239
+ margin-top: 8px;
1240
+ }
1241
+
1242
+ /* Progress bar */
1243
+ .progress-section {
1244
+ margin-top: 16px;
1245
+ grid-column: 1 / -1;
1246
+ }
1247
+ .progress-label {
1248
+ font-size: 0.75rem;
1249
+ color: var(--muted);
1250
+ margin-bottom: 6px;
1251
+ }
1252
+ .progress-bar-track {
1253
+ height: 8px;
1254
+ background: var(--border);
1255
+ border-radius: 4px;
1256
+ overflow: hidden;
1257
+ }
1258
+ .progress-bar-fill {
1259
+ height: 100%;
1260
+ border-radius: 4px;
1261
+ background: linear-gradient(90deg, #f78166, #d29922);
1262
+ transition: width 0.6s ease;
1263
+ }
1264
+
1265
+ /* Calendar grid */
1266
+ .section {
1267
+ background: var(--surface);
1268
+ border: 1px solid var(--border);
1269
+ border-radius: 12px;
1270
+ padding: 20px 24px;
1271
+ margin-bottom: 24px;
1272
+ }
1273
+ .section h2 {
1274
+ font-size: 0.85rem;
1275
+ text-transform: uppercase;
1276
+ letter-spacing: 0.08em;
1277
+ color: var(--muted);
1278
+ margin-bottom: 16px;
1279
+ }
1280
+ .calendar-grid {
1281
+ display: grid;
1282
+ grid-template-columns: repeat(13, 1fr);
1283
+ gap: 3px;
1284
+ }
1285
+ .day {
1286
+ aspect-ratio: 1;
1287
+ border-radius: 3px;
1288
+ cursor: default;
1289
+ transition: transform 0.1s;
1290
+ }
1291
+ .day:hover { transform: scale(1.3); }
1292
+ .day-empty { background: var(--border); }
1293
+ .day-active { background: var(--green); }
1294
+ .day-frozen { background: var(--frozen); }
1295
+ .day-grace { background: var(--grace); }
1296
+ .day-today { outline: 2px solid var(--text); outline-offset: 1px; }
1297
+ .calendar-legend {
1298
+ display: flex;
1299
+ gap: 16px;
1300
+ margin-top: 12px;
1301
+ flex-wrap: wrap;
1302
+ }
1303
+ .legend-item {
1304
+ display: flex;
1305
+ align-items: center;
1306
+ gap: 6px;
1307
+ font-size: 0.75rem;
1308
+ color: var(--muted);
1309
+ }
1310
+ .legend-dot {
1311
+ width: 12px;
1312
+ height: 12px;
1313
+ border-radius: 3px;
1314
+ }
1315
+
1316
+ /* Leaderboard */
1317
+ table { width: 100%; border-collapse: collapse; }
1318
+ th, td { padding: 8px 12px; text-align: left; font-size: 0.875rem; }
1319
+ th { color: var(--muted); font-weight: 500; border-bottom: 1px solid var(--border); }
1320
+ tr.you { background: rgba(88, 166, 255, 0.08); }
1321
+ tr.you td { color: var(--blue); font-weight: 600; }
1322
+ td.rank { color: var(--muted); width: 40px; }
1323
+ td.streak { color: var(--accent); font-weight: 600; }
1324
+ td.longest { color: var(--muted); }
1325
+ td.badges { font-size: 1rem; letter-spacing: 2px; }
1326
+ td.empty { color: var(--muted); font-style: italic; padding: 20px 12px; }
1327
+ .freeze-used { font-size: 0.7rem; color: var(--frozen); margin-left: 4px; }
1328
+ tr:hover:not(.you) { background: rgba(255,255,255,0.03); }
1329
+
1330
+ /* Milestones */
1331
+ .milestones-grid {
1332
+ display: grid;
1333
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
1334
+ gap: 12px;
1335
+ }
1336
+ .milestone-card {
1337
+ text-align: center;
1338
+ padding: 16px 8px;
1339
+ border-radius: 10px;
1340
+ border: 1px solid var(--border);
1341
+ transition: transform 0.2s;
1342
+ }
1343
+ .milestone-card:hover { transform: translateY(-2px); }
1344
+ .milestone-card.locked { opacity: 0.35; filter: grayscale(1); }
1345
+ .milestone-card.earned { border-color: transparent; }
1346
+ .milestone-card.rarity-common.earned { background: rgba(63,185,80,0.1); border-color: rgba(63,185,80,0.3); }
1347
+ .milestone-card.rarity-rare.earned { background: rgba(88,166,255,0.1); border-color: rgba(88,166,255,0.3); }
1348
+ .milestone-card.rarity-epic.earned { background: rgba(210,153,34,0.1); border-color: rgba(210,153,34,0.3); }
1349
+ .milestone-card.rarity-legendary.earned { background: rgba(188,140,255,0.1); border-color: rgba(188,140,255,0.3); }
1350
+ .ms-icon { font-size: 1.75rem; margin-bottom: 6px; }
1351
+ .ms-name { font-size: 0.7rem; font-weight: 600; color: var(--text); }
1352
+ .ms-req { font-size: 0.65rem; color: var(--muted); margin-top: 2px; }
1353
+
1354
+ /* Animations */
1355
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
1356
+ .streak-number { animation: ${isCheckedInToday ? "none" : "pulse 2s ease-in-out infinite"}; }
1357
+
1358
+ @keyframes celebrate {
1359
+ 0% { transform: scale(1); }
1360
+ 50% { transform: scale(1.05); }
1361
+ 100% { transform: scale(1); }
1362
+ }
1363
+ .celebrate { animation: celebrate 0.5s ease-in-out; }
1364
+ </style>
1365
+ </head>
1366
+ <body>
1367
+ <div class="container">
1368
+ <header>
1369
+ <div class="logo">vibechk</div>
1370
+ <div class="username">@${profile.username}</div>
1371
+ </header>
1372
+
1373
+ <!-- Streak Hero -->
1374
+ <div class="streak-hero" id="heroCard">
1375
+ <div>
1376
+ <div class="streak-number">${streakEmoji} ${streak.currentStreak}</div>
1377
+ <div class="streak-label">day streak</div>
1378
+ <div class="status-badge ${isCheckedInToday ? "protected" : streak.currentStreak > 0 ? "at-risk" : "broken"}">
1379
+ ${isCheckedInToday ? "\u2713 Protected for today" : streak.currentStreak > 0 ? "\u25CB Check in to protect" : "\u25CF Start your streak"}
1380
+ </div>
1381
+ <div class="freeze-tokens" title="Freeze tokens available">
1382
+ ${"\u2744\uFE0F".repeat(streak.freezeTokens)}${streak.freezeTokens === 0 ? '<span style="color:var(--muted);font-size:0.8rem">No freezes</span>' : ""}
1383
+ </div>
1384
+ </div>
1385
+ <div class="streak-stats">
1386
+ <div class="stat">
1387
+ <div class="stat-value">${streak.longestStreak}d</div>
1388
+ <div class="stat-label">Longest</div>
1389
+ </div>
1390
+ <div class="stat">
1391
+ <div class="stat-value">${streak.totalCheckIns}</div>
1392
+ <div class="stat-label">Total days</div>
1393
+ </div>
1394
+ </div>
1395
+ </div>
1396
+
1397
+ <!-- Activity Calendar -->
1398
+ <div class="section">
1399
+ <h2>Last 90 Days</h2>
1400
+ <div class="calendar-grid">
1401
+ ${cells.join("\n ")}
1402
+ </div>
1403
+ <div class="calendar-legend">
1404
+ <div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> Coded</div>
1405
+ <div class="legend-item"><div class="legend-dot" style="background:var(--frozen)"></div> Frozen</div>
1406
+ <div class="legend-item"><div class="legend-dot" style="background:var(--grace)"></div> Grace</div>
1407
+ <div class="legend-item"><div class="legend-dot" style="background:var(--border)"></div> Missed</div>
1408
+ </div>
1409
+ </div>
1410
+
1411
+ <!-- Leaderboard -->
1412
+ <div class="section">
1413
+ <h2>Leaderboard</h2>
1414
+ ${leaderboard ? `<table>
1415
+ <thead>
1416
+ <tr>
1417
+ <th>Rank</th>
1418
+ <th>Name</th>
1419
+ <th>Streak</th>
1420
+ <th>Longest</th>
1421
+ <th>Badges</th>
1422
+ </tr>
1423
+ </thead>
1424
+ <tbody>
1425
+ ${lbRows}
1426
+ </tbody>
1427
+ </table>` : '<p style="color:var(--muted);font-size:0.875rem">No leaderboard data yet. Configure cloud sync to compare with friends.</p>'}
1428
+ </div>
1429
+
1430
+ <!-- Milestones -->
1431
+ <div class="section">
1432
+ <h2>Milestones</h2>
1433
+ <div class="milestones-grid">
1434
+ ${milestoneCards}
1435
+ </div>
1436
+ </div>
1437
+ </div>
1438
+
1439
+ <script>
1440
+ // Celebrate if just reached a new milestone
1441
+ const urlParams = new URLSearchParams(window.location.search);
1442
+ if (urlParams.get('celebrate')) {
1443
+ document.getElementById('heroCard').classList.add('celebrate');
1444
+ }
1445
+
1446
+ // Auto-refresh every 60s
1447
+ setTimeout(() => window.location.reload(), 60000);
1448
+ </script>
1449
+ </body>
1450
+ </html>`;
1451
+ }
1452
+
1453
+ // src/commands/check-in.ts
1454
+ import openBrowser from "open";
1455
+ async function runCheckIn(options = {}) {
1456
+ const profile = requireProfile();
1457
+ const today = todayInTz(profile.timezone);
1458
+ const now = nowIso();
1459
+ let source = options.source ?? "manual";
1460
+ let agentData = void 0;
1461
+ if (!options.source || options.source === "claude-code") {
1462
+ const spinner = ora({ text: "Detecting coding session\u2026", isSilent: options.quiet }).start();
1463
+ const detection = await detectTodaySession(
1464
+ today,
1465
+ profile.timezone,
1466
+ profile.preferences.sessionSources,
1467
+ profile.preferences.watchedRepos ?? []
1468
+ );
1469
+ spinner.stop();
1470
+ if (detection.detected) {
1471
+ source = detection.source;
1472
+ agentData = detection.agentData;
1473
+ } else if (options.noInteractive) {
1474
+ if (!options.quiet) {
1475
+ console.log(chalk4.dim(" No coding session detected today. Streak unchanged."));
1476
+ }
1477
+ const currentStreak2 = loadStreak();
1478
+ return {
1479
+ action: "skipped",
1480
+ streak: currentStreak2.currentStreak,
1481
+ previousStreak: void 0,
1482
+ newMilestones: [],
1483
+ freezeTokensRemaining: currentStreak2.freezeTokens,
1484
+ agentData: void 0
1485
+ };
1486
+ } else if (!options.quiet) {
1487
+ const doManual = await confirm({
1488
+ message: chalk4.dim("No AI session detected. Check in manually anyway?"),
1489
+ default: true
1490
+ });
1491
+ if (!doManual) {
1492
+ console.log(chalk4.dim(" Skipped. Run vibechk again when you've coded today."));
1493
+ process.exit(0);
1494
+ }
1495
+ source = "manual";
1496
+ }
1497
+ }
1498
+ const currentStreak = loadStreak();
1499
+ const currentBadges = loadBadges();
1500
+ let useFreeze = options.autoFreeze ?? false;
1501
+ const impact = calculateStreakImpact(currentStreak, today, false);
1502
+ if (impact.action === "broken" && impact.previousStreak && impact.previousStreak > 0 && currentStreak.freezeTokens > 0 && !options.noInteractive && !options.quiet) {
1503
+ const daysDiff = currentStreak.lastActivityDate ? Math.abs(new Date(today).getTime() - new Date(currentStreak.lastActivityDate).getTime()) / 864e5 : 99;
1504
+ if (daysDiff <= 2) {
1505
+ console.log(chalk4.yellow(`
1506
+ Missed yesterday. You have ${currentStreak.freezeTokens} freeze token(s).`));
1507
+ useFreeze = await confirm({
1508
+ message: `Use a freeze token to preserve your ${impact.previousStreak}-day streak?`,
1509
+ default: true
1510
+ });
1511
+ }
1512
+ }
1513
+ const finalImpact = calculateStreakImpact(currentStreak, today, useFreeze);
1514
+ const newStreakRecord = applyStreakImpact(currentStreak, finalImpact, today, now);
1515
+ const newMilestones = checkNewMilestones(newStreakRecord.currentStreak, currentBadges);
1516
+ const newBadges = createEarnedBadges(newMilestones, newStreakRecord.currentStreak, now);
1517
+ const freezeReward = totalFreezeReward(newMilestones);
1518
+ if (freezeReward > 0) {
1519
+ newStreakRecord.freezeTokens = addFreezeTokens(newStreakRecord.freezeTokens, freezeReward);
1520
+ }
1521
+ if (!options.dryRun) {
1522
+ saveStreak(newStreakRecord);
1523
+ appendBadges(newBadges);
1524
+ if (finalImpact.action !== "already_checked_in") {
1525
+ appendActivity({
1526
+ date: today,
1527
+ checkedInAt: now,
1528
+ source,
1529
+ isFrozen: finalImpact.action === "frozen",
1530
+ isGrace: finalImpact.action === "grace",
1531
+ sessionId: uuidv4(),
1532
+ agentData,
1533
+ notes: options.notes
1534
+ });
1535
+ }
1536
+ }
1537
+ if (!options.quiet && !options.json) {
1538
+ console.log("");
1539
+ console.log(renderCheckIn(finalImpact.action, newStreakRecord.currentStreak, finalImpact.previousStreak, newMilestones));
1540
+ for (const milestone of newMilestones) {
1541
+ console.log("");
1542
+ console.log(renderMilestone(milestone, newStreakRecord.currentStreak));
1543
+ }
1544
+ console.log("");
1545
+ } else if (options.json) {
1546
+ const result = {
1547
+ action: finalImpact.action,
1548
+ streak: newStreakRecord.currentStreak,
1549
+ previousStreak: finalImpact.previousStreak,
1550
+ newMilestones: newMilestones.map((m) => m.id),
1551
+ freezeTokensRemaining: newStreakRecord.freezeTokens
1552
+ };
1553
+ console.log(JSON.stringify(result));
1554
+ }
1555
+ const shouldOpenBrowser = options.openDashboard || newMilestones.length > 0 && !options.quiet;
1556
+ if (shouldOpenBrowser && !options.dryRun) {
1557
+ try {
1558
+ const url = await startDashboard(true);
1559
+ const celebrateUrl = newMilestones.length > 0 ? `${url}?celebrate=1` : url;
1560
+ await openBrowser(celebrateUrl);
1561
+ if (!options.quiet) console.log(chalk4.dim(` Opening dashboard in browser\u2026`));
1562
+ } catch {
1563
+ }
1564
+ }
1565
+ if (!options.dryRun && profile.cloudSync) {
1566
+ const badges = loadBadges();
1567
+ pushStreak(profile, newStreakRecord, badges).catch(() => {
1568
+ });
1569
+ }
1570
+ if (!options.dryRun && options.noInteractive && finalImpact.action !== "already_checked_in") {
1571
+ Promise.resolve().then(() => (init_publish(), publish_exports)).then(({ runPublish: runPublish2 }) => runPublish2({ silent: true })).catch(() => {
1572
+ });
1573
+ Promise.resolve().then(() => (init_friend(), friend_exports)).then(({ runFriendPull: runFriendPull2 }) => runFriendPull2({ quiet: true })).catch(() => {
1574
+ });
1575
+ }
1576
+ return {
1577
+ action: finalImpact.action,
1578
+ streak: newStreakRecord.currentStreak,
1579
+ previousStreak: finalImpact.previousStreak,
1580
+ newMilestones,
1581
+ freezeTokensRemaining: newStreakRecord.freezeTokens,
1582
+ agentData
1583
+ };
1584
+ }
1585
+
1586
+ // src/commands/status.ts
1587
+ init_profile_store();
1588
+ init_streak_store();
1589
+ init_activity_store();
1590
+ init_date_utils();
1591
+ import openBrowser2 from "open";
1592
+ import chalk5 from "chalk";
1593
+ function getStreak() {
1594
+ return loadStreak();
1595
+ }
1596
+
1597
+ // src/commands/leaderboard.ts
1598
+ init_profile_store();
1599
+ import chalk7 from "chalk";
1600
+ import ora2 from "ora";
1601
+
1602
+ // src/ui/leaderboard-view.ts
1603
+ init_milestone_checker();
1604
+ import chalk6 from "chalk";
1605
+ import dayjs3 from "dayjs";
1606
+
1607
+ // src/commands/leaderboard.ts
1608
+ import openBrowser3 from "open";
1609
+ function getLeaderboard() {
1610
+ return loadLeaderboardCache();
1611
+ }
1612
+
1613
+ // src/index.ts
1614
+ init_profile_store();
1615
+
1616
+ // src/commands/export.ts
1617
+ init_profile_store();
1618
+ init_streak_store();
1619
+ init_activity_store();
1620
+ init_badge_store();
1621
+ async function runExport(options = {}) {
1622
+ const profile = requireProfile();
1623
+ const streak = loadStreak();
1624
+ const activity = loadActivity();
1625
+ const badges = loadBadges();
1626
+ const data = {
1627
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1628
+ version: "0.1.0",
1629
+ profile: {
1630
+ username: profile.username,
1631
+ timezone: profile.timezone,
1632
+ createdAt: profile.createdAt
1633
+ },
1634
+ streak,
1635
+ activity,
1636
+ badges
1637
+ };
1638
+ if (options.format === "csv") {
1639
+ const rows = ["date,source,isFrozen,isGrace"];
1640
+ for (const a of activity) {
1641
+ rows.push(`${a.date},${a.source},${a.isFrozen},${a.isGrace}`);
1642
+ }
1643
+ process.stdout.write(rows.join("\n") + "\n");
1644
+ } else {
1645
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
1646
+ }
1647
+ }
1648
+ export {
1649
+ runCheckIn as checkIn,
1650
+ runExport as exportData,
1651
+ getLeaderboard,
1652
+ loadProfile as getProfile,
1653
+ getStreak
1654
+ };