@yuiseki/gyazocli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSearchCommand = registerSearchCommand;
4
+ const api_1 = require("../api");
5
+ const credentials_1 = require("../credentials");
6
+ const format_1 = require("../format");
7
+ const memory_1 = require("../services/memory");
8
+ const images_1 = require("../services/images");
9
+ function registerSearchCommand(program) {
10
+ program
11
+ .command('search [query]')
12
+ .description('Search images')
13
+ .option('-j, --json', 'output as JSON')
14
+ .option('--no-cache', 'force fetch from API')
15
+ .action(async (query, options) => {
16
+ await (0, credentials_1.ensureAccessToken)();
17
+ try {
18
+ if (!(0, format_1.normalizeText)(query)) {
19
+ console.error('Error: Query is required.');
20
+ console.error('Hint: Run `gyazo search -h` for usage.');
21
+ process.exit(1);
22
+ }
23
+ const images = await (0, api_1.searchImages)(query);
24
+ const useCache = options.cache !== false;
25
+ if (options.json) {
26
+ (0, memory_1.cacheSearchResultImages)(images);
27
+ console.log(JSON.stringify(images, null, 2));
28
+ }
29
+ else {
30
+ const imagesForDisplay = await (0, images_1.prepareImagesForDisplay)(images, {
31
+ cacheSearchResults: true,
32
+ enrichLocation: true,
33
+ useCache,
34
+ });
35
+ (0, images_1.printListImages)(imagesForDisplay);
36
+ }
37
+ }
38
+ catch (error) {
39
+ console.error('Error searching images:', error.message);
40
+ process.exit(1);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerStatsCommand = registerStatsCommand;
4
+ const credentials_1 = require("../credentials");
5
+ const dates_1 = require("../dates");
6
+ const options_1 = require("../options");
7
+ const memory_1 = require("../services/memory");
8
+ const analytics_1 = require("../services/analytics");
9
+ function registerStatsCommand(program) {
10
+ program
11
+ .command('stats')
12
+ .description('Show weekly stats summary in Markdown')
13
+ .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'window end date anchor (default: yesterday)')
14
+ .option('--days <number>', 'window length in days', '7')
15
+ .option('--top <number>', 'rows per section', '10')
16
+ .option('--max-pages <number>', 'max pages to fetch when warming cache', '10')
17
+ .option('--no-cache', 'force fetch from API')
18
+ .action(async (options) => {
19
+ await (0, credentials_1.ensureAccessToken)();
20
+ try {
21
+ const { range, days, startLabel, endLabel } = (0, dates_1.buildStatsDateRange)(options.date, options.days || '7');
22
+ const top = Math.min((0, options_1.parsePositiveIntegerOption)(options.top, '--top'), 20);
23
+ const maxPages = (0, options_1.parsePositiveIntegerOption)(options.maxPages, '--max-pages');
24
+ const useCache = options.cache !== false;
25
+ let uploadTime;
26
+ let apps = [];
27
+ let domains = [];
28
+ let tags = [];
29
+ let totalUploads = 0;
30
+ if (useCache) {
31
+ uploadTime = (0, analytics_1.buildUploadTimeSummaryFromHourlyCache)(range);
32
+ if (uploadTime.totalImages === 0) {
33
+ await (0, memory_1.warmDateCacheForApps)(range, maxPages, true);
34
+ uploadTime = (0, analytics_1.buildUploadTimeSummaryFromHourlyCache)(range);
35
+ }
36
+ let appsSummary = (0, analytics_1.buildAppsRankingFromHourlyCache)(range);
37
+ if (uploadTime.totalImages > 0 && appsSummary.totalImages === 0) {
38
+ await (0, memory_1.warmDateCacheForApps)(range, maxPages, true);
39
+ appsSummary = (0, analytics_1.buildAppsRankingFromHourlyCache)(range);
40
+ }
41
+ let domainsSummary = (0, analytics_1.buildDomainsRankingFromHourlyCache)(range);
42
+ if (uploadTime.totalImages > 0 && domainsSummary.totalImages === 0) {
43
+ await (0, memory_1.warmDateCacheForDomains)(range, maxPages, true);
44
+ domainsSummary = (0, analytics_1.buildDomainsRankingFromHourlyCache)(range);
45
+ }
46
+ let tagsSummary = (0, analytics_1.buildTagsRankingFromHourlyCache)(range);
47
+ if (uploadTime.totalImages > 0 && tagsSummary.totalImages === 0) {
48
+ await (0, memory_1.warmDateCacheForTags)(range, maxPages, true);
49
+ tagsSummary = (0, analytics_1.buildTagsRankingFromHourlyCache)(range);
50
+ }
51
+ apps = appsSummary.ranking;
52
+ domains = domainsSummary.ranking;
53
+ tags = tagsSummary.ranking;
54
+ totalUploads = uploadTime.totalImages;
55
+ }
56
+ else {
57
+ const imageIds = await (0, memory_1.warmDateCacheForTags)(range, maxPages, false);
58
+ uploadTime = (0, analytics_1.buildUploadTimeSummaryFromImageCache)(imageIds, range);
59
+ apps = (0, analytics_1.buildAppsRankingFromCache)(imageIds);
60
+ domains = (0, analytics_1.buildDomainsRankingFromCache)(imageIds);
61
+ tags = (0, analytics_1.buildTagsRankingFromCache)(imageIds).ranking;
62
+ totalUploads = uploadTime.totalImages;
63
+ }
64
+ console.log((0, analytics_1.renderStatsMarkdown)({
65
+ startLabel,
66
+ endLabel,
67
+ days,
68
+ totalUploads,
69
+ uploadTime,
70
+ apps,
71
+ domains,
72
+ tags,
73
+ top,
74
+ }));
75
+ }
76
+ catch (error) {
77
+ console.error('Error building stats:', error.message);
78
+ process.exit(1);
79
+ }
80
+ });
81
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSummaryCommand = registerSummaryCommand;
4
+ const credentials_1 = require("../credentials");
5
+ const dates_1 = require("../dates");
6
+ const options_1 = require("../options");
7
+ const analytics_1 = require("../services/analytics");
8
+ function registerSummaryCommand(program) {
9
+ program
10
+ .command('summary')
11
+ .description('Show weekly summary with daily uploads and metadata rankings')
12
+ .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
13
+ .option('--today', 'target today only (overrides default weekly range)')
14
+ .option('-l, --limit <number>', 'maximum ranking rows (max: 10)', '10')
15
+ .option('--max-pages <number>', 'max pages to scan before stopping', '10')
16
+ .option('-j, --json', 'output as JSON')
17
+ .option('--no-cache', 'force fetch from API')
18
+ .action(async (options) => {
19
+ await (0, credentials_1.ensureAccessToken)();
20
+ try {
21
+ const targetDate = (0, dates_1.resolveRankingRangeOption)(options);
22
+ const requestedLimit = (0, options_1.parsePositiveIntegerOption)(options.limit, '--limit');
23
+ const limit = Math.min(requestedLimit, 10);
24
+ const maxPages = (0, options_1.parsePositiveIntegerOption)(options.maxPages, '--max-pages');
25
+ const useCache = options.cache !== false;
26
+ const dailySummaries = await (0, analytics_1.buildSummary)({ targetDate, maxPages, useCache });
27
+ if (options.json) {
28
+ console.log(JSON.stringify((0, analytics_1.toSummaryJson)(targetDate.dateKey, dailySummaries, limit), null, 2));
29
+ return;
30
+ }
31
+ console.log((0, analytics_1.renderSummaryText)({
32
+ dateKey: targetDate.dateKey,
33
+ dailySummaries,
34
+ limit,
35
+ }));
36
+ }
37
+ catch (error) {
38
+ console.error('Error building summary:', error.message);
39
+ process.exit(1);
40
+ }
41
+ });
42
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSyncCommand = registerSyncCommand;
4
+ const api_1 = require("../api");
5
+ const storage_1 = require("../storage");
6
+ const credentials_1 = require("../credentials");
7
+ const dates_1 = require("../dates");
8
+ const options_1 = require("../options");
9
+ function registerSyncCommand(program) {
10
+ program
11
+ .command('sync')
12
+ .description('Sync images from yesterday back to N days')
13
+ .option('--days <number>', 'number of days to sync (used when --date is omitted)')
14
+ .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'sync only this date/month/year range')
15
+ .option('--max-pages <number>', 'max pages to fetch', '10')
16
+ .action(async (options) => {
17
+ await (0, credentials_1.ensureAccessToken)();
18
+ if (options.date && options.days) {
19
+ console.error('Error: --date and --days cannot be used together.');
20
+ process.exit(1);
21
+ }
22
+ const maxPages = (0, options_1.parsePositiveIntegerOption)(options.maxPages, '--max-pages');
23
+ let startDate;
24
+ let endDate;
25
+ if (options.date) {
26
+ const parsed = (0, dates_1.parseDateOption)(options.date);
27
+ startDate = parsed.start;
28
+ endDate = parsed.end;
29
+ }
30
+ else {
31
+ const days = options.days ? (0, options_1.parsePositiveIntegerOption)(options.days, '--days') : 1;
32
+ const now = new Date();
33
+ endDate = new Date(now);
34
+ endDate.setDate(endDate.getDate() - 1);
35
+ endDate.setHours(23, 59, 59, 999);
36
+ startDate = new Date(now);
37
+ startDate.setDate(startDate.getDate() - days - 1);
38
+ startDate.setHours(0, 0, 0, 0);
39
+ }
40
+ console.log(`Syncing images between ${startDate.toISOString()} and ${endDate.toISOString()}...`);
41
+ const hourlyIndices = new Map();
42
+ for (let page = 1; page <= maxPages; page++) {
43
+ const images = await (0, api_1.listImages)(page, 100);
44
+ if (images.length === 0)
45
+ break;
46
+ let reachedLimit = false;
47
+ for (const img of images) {
48
+ const createdAt = new Date(img.created_at);
49
+ if (createdAt > endDate) {
50
+ // Skip images newer than target range.
51
+ continue;
52
+ }
53
+ if (createdAt < startDate) {
54
+ reachedLimit = true;
55
+ break;
56
+ }
57
+ // Add to hourly index
58
+ const y = createdAt.getFullYear().toString();
59
+ const m = (createdAt.getMonth() + 1).toString().padStart(2, '0');
60
+ const d = createdAt.getDate().toString().padStart(2, '0');
61
+ const h = createdAt.getHours().toString().padStart(2, '0');
62
+ const key = `${y}-${m}-${d}-${h}`;
63
+ if (!hourlyIndices.has(key))
64
+ hourlyIndices.set(key, new Set());
65
+ hourlyIndices.get(key)?.add(img.image_id);
66
+ const cached = (0, storage_1.loadImageCache)(img.image_id);
67
+ if (cached && cached.ocr) {
68
+ process.stdout.write(`s`);
69
+ continue;
70
+ }
71
+ process.stdout.write(`.`);
72
+ try {
73
+ const detail = await (0, api_1.getImageDetail)(img.image_id);
74
+ (0, storage_1.saveImageCache)(img.image_id, detail);
75
+ await new Promise(resolve => setTimeout(resolve, 200));
76
+ }
77
+ catch (e) {
78
+ process.stdout.write(`x`);
79
+ }
80
+ }
81
+ console.log(`\nPage ${page} processed.`);
82
+ if (reachedLimit)
83
+ break;
84
+ }
85
+ // Save hourly indices
86
+ console.log(`Updating hourly indices...`);
87
+ for (const [key, ids] of hourlyIndices.entries()) {
88
+ const [y, m, d, h] = key.split('-');
89
+ const existing = (0, storage_1.loadHourlyCache)(y, m, d, h) || [];
90
+ const merged = Array.from(new Set([...existing, ...ids]));
91
+ (0, storage_1.saveHourlyCache)(y, m, d, h, merged);
92
+ }
93
+ console.log(`Sync complete.`);
94
+ });
95
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTagsCommand = registerTagsCommand;
4
+ const credentials_1 = require("../credentials");
5
+ const dates_1 = require("../dates");
6
+ const options_1 = require("../options");
7
+ const memory_1 = require("../services/memory");
8
+ const analytics_1 = require("../services/analytics");
9
+ function registerTagsCommand(program) {
10
+ program
11
+ .command('tags')
12
+ .description('Rank metadata tags for a specific date')
13
+ .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
14
+ .option('--today', 'target today only (overrides default weekly range)')
15
+ .option('-l, --limit <number>', 'maximum ranking rows (max: 10)', '10')
16
+ .option('--max-pages <number>', 'max pages to scan before stopping', '10')
17
+ .option('-j, --json', 'output as JSON')
18
+ .option('--no-cache', 'force fetch from API')
19
+ .action(async (options) => {
20
+ await (0, credentials_1.ensureAccessToken)();
21
+ try {
22
+ const targetDate = (0, dates_1.resolveRankingRangeOption)(options);
23
+ const requestedLimit = (0, options_1.parsePositiveIntegerOption)(options.limit, '--limit');
24
+ const limit = Math.min(requestedLimit, 10);
25
+ const maxPages = (0, options_1.parsePositiveIntegerOption)(options.maxPages, '--max-pages');
26
+ const useCache = options.cache !== false;
27
+ let summary;
28
+ let totalImages = 0;
29
+ if (useCache) {
30
+ let cacheSummary = (0, analytics_1.buildTagsRankingFromHourlyCache)(targetDate);
31
+ if (cacheSummary.totalImages === 0) {
32
+ await (0, memory_1.warmDateCacheForTags)(targetDate, maxPages, true);
33
+ cacheSummary = (0, analytics_1.buildTagsRankingFromHourlyCache)(targetDate);
34
+ }
35
+ summary = cacheSummary;
36
+ totalImages = cacheSummary.totalImages;
37
+ }
38
+ else {
39
+ const imageIds = await (0, memory_1.warmDateCacheForTags)(targetDate, maxPages, false);
40
+ summary = (0, analytics_1.buildTagsRankingFromCache)(imageIds);
41
+ totalImages = imageIds.length;
42
+ }
43
+ const displayedRanking = summary.ranking.slice(0, limit);
44
+ if (options.json) {
45
+ console.log(JSON.stringify({
46
+ date: targetDate.dateKey,
47
+ image_count: totalImages,
48
+ image_count_with_tags: summary.imageCountWithTags,
49
+ total_tag_assignments: summary.totalTagAssignments,
50
+ total_tags: summary.ranking.length,
51
+ ranking: displayedRanking,
52
+ }, null, 2));
53
+ return;
54
+ }
55
+ if (summary.ranking.length === 0) {
56
+ console.log(`No tag metadata found for ${targetDate.dateKey}.`);
57
+ return;
58
+ }
59
+ console.log(`Tags on ${targetDate.dateKey}`);
60
+ displayedRanking.forEach((item, index) => {
61
+ console.log(`${index + 1}. #${item.tag}: ${item.count}`);
62
+ });
63
+ console.log(`Total images with tag metadata: ${summary.imageCountWithTags}`);
64
+ }
65
+ catch (error) {
66
+ console.error('Error ranking tags:', error.message);
67
+ process.exit(1);
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerUploadCommand = registerUploadCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const api_1 = require("../api");
10
+ const credentials_1 = require("../credentials");
11
+ const dates_1 = require("../dates");
12
+ const images_1 = require("../services/images");
13
+ function registerUploadCommand(program) {
14
+ program
15
+ .command('upload [path]')
16
+ .description('Upload an image file (or read image bytes from stdin)')
17
+ .option('-j, --json', 'output the upload response as JSON')
18
+ .option('--title <title>', 'image title')
19
+ .option('--app <app>', 'application name', 'gyazocli')
20
+ .option('--url <url>', 'source URL (sent as referer_url)')
21
+ .option('--timestamp <unix_timestamp>', 'created_at unix timestamp (current or past)')
22
+ .option('--desc <desc>', 'image description')
23
+ .action(async (inputPath, options) => {
24
+ await (0, credentials_1.ensureAccessToken)();
25
+ let imageData;
26
+ let filename = 'stdin-upload.bin';
27
+ if (inputPath && inputPath !== '-') {
28
+ const resolvedPath = path_1.default.resolve(inputPath);
29
+ if (!fs_1.default.existsSync(resolvedPath)) {
30
+ console.error(`Error: File not found: ${resolvedPath}`);
31
+ process.exit(1);
32
+ }
33
+ imageData = fs_1.default.readFileSync(resolvedPath);
34
+ filename = path_1.default.basename(resolvedPath);
35
+ }
36
+ else {
37
+ if (process.stdin.isTTY) {
38
+ console.error('Error: Provide an image path or pipe image data via stdin.');
39
+ console.error('Hint: Run `gyazo upload -h` for usage.');
40
+ process.exit(1);
41
+ }
42
+ imageData = await (0, images_1.readStdinBuffer)();
43
+ if (imageData.length === 0) {
44
+ console.error('Error: No image data received from stdin.');
45
+ process.exit(1);
46
+ }
47
+ }
48
+ const desc = (0, images_1.ensureUploadDescTag)(options.desc);
49
+ const timestamp = (0, dates_1.parseUploadTimestamp)(options.timestamp);
50
+ try {
51
+ const uploaded = await (0, api_1.uploadImage)({
52
+ imageData,
53
+ filename,
54
+ title: options.title,
55
+ app: options.app || 'gyazocli',
56
+ refererUrl: options.url,
57
+ desc,
58
+ timestamp,
59
+ });
60
+ if (options.json) {
61
+ console.log(JSON.stringify(uploaded, null, 2));
62
+ }
63
+ else {
64
+ console.log(uploaded.permalink_url);
65
+ }
66
+ }
67
+ catch (error) {
68
+ console.error('Error uploading image:', error.message);
69
+ process.exit(1);
70
+ }
71
+ });
72
+ }
package/dist/dates.js ADDED
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ /**
3
+ * Dates, as this CLI means them: a day is a local day, because that is how the
4
+ * cache is laid out and how a person asking for "today" means it. Parses what
5
+ * --date and --days accept, builds the ranges the aggregations walk, and keys
6
+ * the hourly cache buckets.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.DATE_OPTION_PROBLEMS = exports.WEEKDAY_LABELS = void 0;
10
+ exports.isToday = isToday;
11
+ exports.parseUploadTimestamp = parseUploadTimestamp;
12
+ exports.tryParseDateOption = tryParseDateOption;
13
+ exports.parseDateOption = parseDateOption;
14
+ exports.formatDateYmd = formatDateYmd;
15
+ exports.buildRecentWeekRangeUntilYesterday = buildRecentWeekRangeUntilYesterday;
16
+ exports.resolveRankingRangeOption = resolveRankingRangeOption;
17
+ exports.buildStatsDateRange = buildStatsDateRange;
18
+ exports.getDateHourStrings = getDateHourStrings;
19
+ exports.buildHourlyBucketKey = buildHourlyBucketKey;
20
+ exports.splitHourlyBucketKey = splitHourlyBucketKey;
21
+ exports.toDateParts = toDateParts;
22
+ exports.getDatePartsInRange = getDatePartsInRange;
23
+ exports.parseHourOption = parseHourOption;
24
+ const options_1 = require("./options");
25
+ exports.WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
26
+ function isToday(date) {
27
+ const today = new Date();
28
+ return date.getDate() === today.getDate() &&
29
+ date.getMonth() === today.getMonth() &&
30
+ date.getFullYear() === today.getFullYear();
31
+ }
32
+ function parseUploadTimestamp(value) {
33
+ if (!value)
34
+ return undefined;
35
+ if (!/^\d+$/.test(value)) {
36
+ console.error('Error: --timestamp must be a unix timestamp in seconds.');
37
+ process.exit(1);
38
+ }
39
+ const parsed = Number(value);
40
+ if (!Number.isSafeInteger(parsed)) {
41
+ console.error('Error: --timestamp is out of range.');
42
+ process.exit(1);
43
+ }
44
+ const now = Math.floor(Date.now() / 1000);
45
+ if (parsed > now) {
46
+ console.error('Error: --timestamp must be current time or in the past.');
47
+ process.exit(1);
48
+ }
49
+ return parsed;
50
+ }
51
+ /**
52
+ * Reads a date the way --date accepts it, without deciding what to do when it
53
+ * cannot: the CLI reports and exits, the MCP server throws.
54
+ */
55
+ function tryParseDateOption(value) {
56
+ if (!value) {
57
+ const today = new Date();
58
+ const year = String(today.getFullYear());
59
+ const month = String(today.getMonth() + 1).padStart(2, '0');
60
+ const day = String(today.getDate()).padStart(2, '0');
61
+ return {
62
+ ok: true,
63
+ value: {
64
+ granularity: 'day',
65
+ dateKey: `${year}-${month}-${day}`,
66
+ start: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0),
67
+ end: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999),
68
+ },
69
+ };
70
+ }
71
+ if (/^\d{4}$/.test(value)) {
72
+ const year = Number(value);
73
+ return {
74
+ ok: true,
75
+ value: {
76
+ granularity: 'year',
77
+ dateKey: value,
78
+ start: new Date(year, 0, 1, 0, 0, 0, 0),
79
+ end: new Date(year, 11, 31, 23, 59, 59, 999),
80
+ },
81
+ };
82
+ }
83
+ if (/^\d{4}-\d{2}$/.test(value)) {
84
+ const [yearText, monthText] = value.split('-');
85
+ const year = Number(yearText);
86
+ const month = Number(monthText);
87
+ const probe = new Date(year, month - 1, 1);
88
+ if (probe.getFullYear() !== year || probe.getMonth() !== month - 1) {
89
+ return { ok: false, problem: 'month' };
90
+ }
91
+ return {
92
+ ok: true,
93
+ value: {
94
+ granularity: 'month',
95
+ dateKey: value,
96
+ start: new Date(year, month - 1, 1, 0, 0, 0, 0),
97
+ end: new Date(year, month, 0, 23, 59, 59, 999),
98
+ },
99
+ };
100
+ }
101
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
102
+ const [yearText, monthText, dayText] = value.split('-');
103
+ const year = Number(yearText);
104
+ const month = Number(monthText);
105
+ const day = Number(dayText);
106
+ const probe = new Date(year, month - 1, day);
107
+ if (probe.getFullYear() !== year ||
108
+ probe.getMonth() !== month - 1 ||
109
+ probe.getDate() !== day) {
110
+ return { ok: false, problem: 'day' };
111
+ }
112
+ return {
113
+ ok: true,
114
+ value: {
115
+ granularity: 'day',
116
+ dateKey: value,
117
+ start: new Date(year, month - 1, day, 0, 0, 0, 0),
118
+ end: new Date(year, month - 1, day, 23, 59, 59, 999),
119
+ },
120
+ };
121
+ }
122
+ return { ok: false, problem: 'format' };
123
+ }
124
+ exports.DATE_OPTION_PROBLEMS = {
125
+ month: '--date month is invalid.',
126
+ day: '--date day is invalid.',
127
+ format: '--date format must be yyyy or yyyy-mm or yyyy-mm-dd.',
128
+ };
129
+ function parseDateOption(value) {
130
+ const parsed = tryParseDateOption(value);
131
+ if (!parsed.ok) {
132
+ console.error(`Error: ${exports.DATE_OPTION_PROBLEMS[parsed.problem]}`);
133
+ process.exit(1);
134
+ }
135
+ return parsed.value;
136
+ }
137
+ function formatDateYmd(date) {
138
+ const year = String(date.getFullYear());
139
+ const month = String(date.getMonth() + 1).padStart(2, '0');
140
+ const day = String(date.getDate()).padStart(2, '0');
141
+ return `${year}-${month}-${day}`;
142
+ }
143
+ function buildRecentWeekRangeUntilYesterday() {
144
+ const today = new Date();
145
+ const end = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1, 23, 59, 59, 999);
146
+ const start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 8, 0, 0, 0, 0);
147
+ return {
148
+ granularity: 'day',
149
+ dateKey: `${formatDateYmd(start)}..${formatDateYmd(end)}`,
150
+ start,
151
+ end,
152
+ };
153
+ }
154
+ function resolveRankingRangeOption(options) {
155
+ if (options.today && options.date) {
156
+ console.error('Error: --today and --date cannot be used together.');
157
+ process.exit(1);
158
+ }
159
+ if (options.today) {
160
+ return parseDateOption();
161
+ }
162
+ if (options.date) {
163
+ return parseDateOption(options.date);
164
+ }
165
+ return buildRecentWeekRangeUntilYesterday();
166
+ }
167
+ function buildStatsDateRange(dateOption, daysOption) {
168
+ if (!dateOption && daysOption === '7') {
169
+ const weekly = buildRecentWeekRangeUntilYesterday();
170
+ return {
171
+ range: weekly,
172
+ days: 7,
173
+ startLabel: formatDateYmd(weekly.start),
174
+ endLabel: formatDateYmd(weekly.end),
175
+ };
176
+ }
177
+ const days = (0, options_1.parsePositiveIntegerOption)(daysOption, '--days');
178
+ let endDate;
179
+ if (dateOption) {
180
+ const parsed = parseDateOption(dateOption);
181
+ endDate = new Date(parsed.end);
182
+ }
183
+ else {
184
+ const now = new Date();
185
+ endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59, 999);
186
+ }
187
+ const startDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate(), 0, 0, 0, 0);
188
+ startDate.setDate(startDate.getDate() - (days - 1));
189
+ const startLabel = formatDateYmd(startDate);
190
+ const endLabel = formatDateYmd(endDate);
191
+ return {
192
+ range: {
193
+ granularity: 'day',
194
+ dateKey: `${startLabel}..${endLabel}`,
195
+ start: startDate,
196
+ end: endDate,
197
+ },
198
+ days,
199
+ startLabel,
200
+ endLabel,
201
+ };
202
+ }
203
+ function getDateHourStrings() {
204
+ const hours = [];
205
+ for (let hour = 0; hour < 24; hour++) {
206
+ hours.push(String(hour).padStart(2, '0'));
207
+ }
208
+ return hours;
209
+ }
210
+ function buildHourlyBucketKey(year, month, day, hour) {
211
+ return `${year}-${month}-${day}-${hour}`;
212
+ }
213
+ function splitHourlyBucketKey(key) {
214
+ const [year, month, day, hour] = key.split('-');
215
+ return { year, month, day, hour };
216
+ }
217
+ function toDateParts(date) {
218
+ return {
219
+ year: String(date.getFullYear()),
220
+ month: String(date.getMonth() + 1).padStart(2, '0'),
221
+ day: String(date.getDate()).padStart(2, '0'),
222
+ hour: String(date.getHours()).padStart(2, '0'),
223
+ };
224
+ }
225
+ function getDatePartsInRange(start, end) {
226
+ const dates = [];
227
+ const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0, 0);
228
+ const last = new Date(end.getFullYear(), end.getMonth(), end.getDate(), 0, 0, 0, 0);
229
+ while (cursor.getTime() <= last.getTime()) {
230
+ dates.push({
231
+ year: String(cursor.getFullYear()),
232
+ month: String(cursor.getMonth() + 1).padStart(2, '0'),
233
+ day: String(cursor.getDate()).padStart(2, '0'),
234
+ });
235
+ cursor.setDate(cursor.getDate() + 1);
236
+ }
237
+ return dates;
238
+ }
239
+ /**
240
+ * `yyyy-mm-dd-hh`, the shape the hourly cache is keyed by. Returns null rather
241
+ * than reporting, because how to report differs between the CLI and the MCP
242
+ * server.
243
+ */
244
+ function parseHourOption(value) {
245
+ const parts = (value || '').split('-');
246
+ if (parts.length !== 4)
247
+ return null;
248
+ const [year, month, day, hour] = parts;
249
+ return { year, month, day, hour };
250
+ }