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