@yuiseki/gyazocli 0.1.1 → 0.3.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/README.md CHANGED
@@ -86,6 +86,67 @@ Environment variables:
86
86
  - `GYAZO_CACHE_DIR`: cache location
87
87
  - `GYAZO_API_ORIGIN` / `GYAZO_UPLOAD_ORIGIN` / `GYAZO_WEB_ORIGIN`: override the endpoints (used by the test suite)
88
88
 
89
+ ## MCP server
90
+
91
+ `gyazo --mcp-server` runs the CLI as a Model Context Protocol server over
92
+ stdio, so an MCP client can search your captures. `--mcp`, `mcp-server` and
93
+ `mcp` start the same thing.
94
+
95
+ ```bash
96
+ gyazo --mcp-server
97
+ ```
98
+
99
+ It needs an access token before it starts, from `gyazo config set token` or
100
+ from `GYAZO_ACCESS_TOKEN` in the client's environment. stdout carries only
101
+ JSON-RPC; anything meant for a human goes to stderr.
102
+
103
+ Configured in a client:
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "gyazo": {
109
+ "command": "npx",
110
+ "args": ["-y", "@yuiseki/gyazocli", "--mcp-server"],
111
+ "env": { "GYAZO_ACCESS_TOKEN": "your_access_token" }
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Tools
118
+
119
+ - `gyazo_search`: full-text search over your captures. Arguments: `query`
120
+ (required, up to 200 characters), `page` (default 1), `per` (default 20,
121
+ max 100). Search syntax is the same as Gyazo's: `cat`, `title:cat`,
122
+ `app:"Google Chrome"`, `url:google.com`, `cat since:2024-01-01 until:2024-12-31`.
123
+ - `gyazo_image`: metadata for one capture. Argument: `id_or_url` (required),
124
+ which accepts a bare 32-character ID, a `https://gyazo.com/<id>` permalink or
125
+ a direct image URL.
126
+ - `gyazo_latest_image`: metadata for the capture uploaded most recently. No
127
+ arguments.
128
+ - `gyazo_list`: the captures, newest first, with the same options as
129
+ `gyazo list`: `page`, `limit`, `date`, `today`, `hour`, `photos`, `uploaded`,
130
+ `max_pages`, `use_cache`. No arguments means the most recent page.
131
+ - `gyazo_summary`: what a day or a range adds up to, with the same options as
132
+ `gyazo summary`: `date`, `today`, `limit`, `max_pages`, `use_cache`. No
133
+ arguments means the week up to yesterday.
134
+ - `gyazo_collection`: a collection and the captures in it. Arguments:
135
+ `id_or_url` (required) and `sort` (`added`, `created` or `captured`).
136
+
137
+ All of them are read-only, and all of them return metadata rather than image
138
+ bytes. A capture that carries coordinates gets a `location: {latitude,
139
+ longitude}`, and OCR text is reported wherever the response carries it: URLs, timestamp, OCR text, title, source application and page, and
140
+ location when the capture carries one. Use the URLs in a result to show the
141
+ capture itself. Handing base64 image data to a model turned out not to work
142
+ well in practice, and describing a capture does.
143
+
144
+ Tool names and arguments follow
145
+ [nota/gyazo-mcp-server](https://github.com/nota/gyazo-mcp-server), so a client
146
+ already configured against that server can point at this one instead. Its
147
+ `gyazo_upload` is deliberately absent: nothing here can write to your Gyazo
148
+ account until there is a reason for it to.
149
+
89
150
  ## Development
90
151
 
91
152
  ### Build
@@ -101,6 +162,11 @@ npm run build
101
162
  npm test
102
163
  ```
103
164
 
165
+ ### Release
166
+
167
+ Push a `v*` tag and Actions stages the package on npm, where a maintainer
168
+ approves it with 2FA. See [RELEASE.md](RELEASE.md).
169
+
104
170
  ### Link local CLI with npm link
105
171
 
106
172
  ```bash
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAppsCommand = registerAppsCommand;
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 registerAppsCommand(program) {
10
+ program
11
+ .command('apps')
12
+ .description('Rank metadata app names 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 ranking = [];
28
+ let totalWithApp = 0;
29
+ let totalImages = 0;
30
+ if (useCache) {
31
+ let cacheSummary = (0, analytics_1.buildAppsRankingFromHourlyCache)(targetDate);
32
+ if (cacheSummary.totalImages === 0) {
33
+ await (0, memory_1.warmDateCacheForApps)(targetDate, maxPages, true);
34
+ cacheSummary = (0, analytics_1.buildAppsRankingFromHourlyCache)(targetDate);
35
+ }
36
+ ranking = cacheSummary.ranking;
37
+ totalWithApp = cacheSummary.imageCountWithApps;
38
+ totalImages = cacheSummary.totalImages;
39
+ }
40
+ else {
41
+ const imageIds = await (0, memory_1.warmDateCacheForApps)(targetDate, maxPages, false);
42
+ ranking = (0, analytics_1.buildAppsRankingFromCache)(imageIds);
43
+ totalWithApp = ranking.reduce((sum, item) => sum + item.count, 0);
44
+ totalImages = imageIds.length;
45
+ }
46
+ const displayedRanking = ranking.slice(0, limit);
47
+ if (options.json) {
48
+ console.log(JSON.stringify({
49
+ date: targetDate.dateKey,
50
+ image_count: totalImages,
51
+ app_image_count: totalWithApp,
52
+ total_apps: ranking.length,
53
+ ranking: displayedRanking,
54
+ }, null, 2));
55
+ return;
56
+ }
57
+ if (ranking.length === 0) {
58
+ console.log(`No app metadata found for ${targetDate.dateKey}.`);
59
+ return;
60
+ }
61
+ console.log(`Apps on ${targetDate.dateKey}`);
62
+ displayedRanking.forEach((item, index) => {
63
+ console.log(`${index + 1}. ${item.app}: ${item.count}`);
64
+ });
65
+ console.log(`Total images with app metadata: ${totalWithApp}`);
66
+ }
67
+ catch (error) {
68
+ console.error('Error ranking apps:', error.message);
69
+ process.exit(1);
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCollectionCommand = registerCollectionCommand;
4
+ const credentials_1 = require("../credentials");
5
+ const collections_1 = require("../services/collections");
6
+ function registerCollectionCommand(program) {
7
+ program
8
+ .command('collection <collection_id>')
9
+ .aliases(['col', 'cols', 'collections'])
10
+ .description('Show a collection and the images in it')
11
+ .option('-j, --json', 'output as JSON')
12
+ .option('-A, --anonymous', 'read without an access token, even when one is configured')
13
+ .option('--sort <added|created|captured>', 'image order (default: added)')
14
+ .action(async (collectionIdInput, options) => {
15
+ const collectionId = (0, collections_1.requireCollectionId)(collectionIdInput);
16
+ const sort = (0, collections_1.parseCollectionSort)(options.sort);
17
+ // No token is not an error here: public collections read fine anonymously.
18
+ if (!options.anonymous) {
19
+ (0, credentials_1.resolveAccessToken)();
20
+ }
21
+ try {
22
+ const { collection, images } = await (0, collections_1.readCollection)(collectionId, {
23
+ anonymous: Boolean(options.anonymous),
24
+ sort,
25
+ });
26
+ if (options.json) {
27
+ console.log(JSON.stringify(collection, null, 2));
28
+ return;
29
+ }
30
+ (0, collections_1.printCollectionMarkdown)(collection, images);
31
+ }
32
+ catch (error) {
33
+ if (error?.response?.status === 404) {
34
+ console.error(`Error: collection ${collectionId} was not found or not public.`);
35
+ console.error('Hint: a private collection returns the same 404 as one that does not exist.');
36
+ if (options.anonymous) {
37
+ console.error('Hint: you are running with --anonymous. Drop it to use your access token.');
38
+ }
39
+ process.exit(1);
40
+ }
41
+ console.error('Error getting collection:', error.message);
42
+ process.exit(1);
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerConfigCommand = registerConfigCommand;
4
+ const api_1 = require("../api");
5
+ const credentials_1 = require("../credentials");
6
+ function registerConfigCommand(program) {
7
+ const configCmd = program.command('config').description('Manage configuration');
8
+ configCmd
9
+ .command('set <key> <value>')
10
+ .description('Set a configuration value')
11
+ .action((key, value) => {
12
+ (0, credentials_1.setStoredConfig)(key, value);
13
+ });
14
+ configCmd
15
+ .command('get <key>')
16
+ .description('Get a configuration value')
17
+ .option('-j, --json', 'output as JSON')
18
+ .action(async (key, options) => {
19
+ if (key === 'me') {
20
+ await (0, credentials_1.ensureAccessToken)();
21
+ try {
22
+ const me = await (0, api_1.getCurrentUser)();
23
+ if (options.json) {
24
+ console.log(JSON.stringify(me, null, 2));
25
+ return;
26
+ }
27
+ const user = me?.user || {};
28
+ if (user.uid)
29
+ console.log(`UID: ${user.uid}`);
30
+ if (user.name)
31
+ console.log(`Name: ${user.name}`);
32
+ if (user.email)
33
+ console.log(`Email: ${user.email}`);
34
+ if (typeof user.is_pro === 'boolean')
35
+ console.log(`Plan: ${user.is_pro ? 'Pro' : 'Free'}`);
36
+ if (typeof user.is_team === 'boolean')
37
+ console.log(`Team: ${user.is_team ? 'Yes' : 'No'}`);
38
+ if (user.profile_image)
39
+ console.log(`Profile image: ${user.profile_image}`);
40
+ }
41
+ catch (error) {
42
+ console.error('Error getting current user:', error.message);
43
+ process.exit(1);
44
+ }
45
+ return;
46
+ }
47
+ const value = (0, credentials_1.getStoredConfig)(key);
48
+ if (value) {
49
+ if (key === 'token') {
50
+ const masked = value.length > 8
51
+ ? `${value.substring(0, 4)}...${value.substring(value.length - 4)}`
52
+ : '********';
53
+ console.log(masked);
54
+ }
55
+ else {
56
+ console.log(value);
57
+ }
58
+ }
59
+ else {
60
+ console.error(`Config key '${key}' not found.`);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerDomainsCommand = registerDomainsCommand;
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 registerDomainsCommand(program) {
10
+ program
11
+ .command('domains')
12
+ .description('Rank metadata URL domains 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 ranking = [];
28
+ let totalWithDomain = 0;
29
+ let totalImages = 0;
30
+ if (useCache) {
31
+ let cacheSummary = (0, analytics_1.buildDomainsRankingFromHourlyCache)(targetDate);
32
+ if (cacheSummary.totalImages === 0) {
33
+ await (0, memory_1.warmDateCacheForDomains)(targetDate, maxPages, true);
34
+ cacheSummary = (0, analytics_1.buildDomainsRankingFromHourlyCache)(targetDate);
35
+ }
36
+ ranking = cacheSummary.ranking;
37
+ totalWithDomain = cacheSummary.imageCountWithDomains;
38
+ totalImages = cacheSummary.totalImages;
39
+ }
40
+ else {
41
+ const imageIds = await (0, memory_1.warmDateCacheForDomains)(targetDate, maxPages, false);
42
+ ranking = (0, analytics_1.buildDomainsRankingFromCache)(imageIds);
43
+ totalWithDomain = ranking.reduce((sum, item) => sum + item.count, 0);
44
+ totalImages = imageIds.length;
45
+ }
46
+ const displayedRanking = ranking.slice(0, limit);
47
+ if (options.json) {
48
+ console.log(JSON.stringify({
49
+ date: targetDate.dateKey,
50
+ image_count: totalImages,
51
+ domain_image_count: totalWithDomain,
52
+ total_domains: ranking.length,
53
+ ranking: displayedRanking,
54
+ }, null, 2));
55
+ return;
56
+ }
57
+ if (ranking.length === 0) {
58
+ console.log(`No domain metadata found for ${targetDate.dateKey}.`);
59
+ return;
60
+ }
61
+ console.log(`Domains on ${targetDate.dateKey}`);
62
+ displayedRanking.forEach((item, index) => {
63
+ console.log(`${index + 1}. ${item.domain}: ${item.count}`);
64
+ });
65
+ console.log(`Total images with domain metadata: ${totalWithDomain}`);
66
+ }
67
+ catch (error) {
68
+ console.error('Error ranking domains:', error.message);
69
+ process.exit(1);
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerGetCommand = registerGetCommand;
4
+ const api_1 = require("../api");
5
+ const storage_1 = require("../storage");
6
+ const credentials_1 = require("../credentials");
7
+ const format_1 = require("../format");
8
+ const memory_1 = require("../services/memory");
9
+ const images_1 = require("../services/images");
10
+ function registerGetCommand(program) {
11
+ program
12
+ .command('get <image_id>')
13
+ .description('Get detailed metadata for an image')
14
+ .option('-j, --json', 'output as JSON')
15
+ .option('--ocr', 'output OCR text only')
16
+ .option('--objects', 'output object annotations only')
17
+ .option('--no-cache', 'force fetch from API')
18
+ .action(async (imageId, options) => {
19
+ await (0, credentials_1.ensureAccessToken)();
20
+ try {
21
+ if (options.json && (options.ocr || options.objects)) {
22
+ console.error('Error: --json cannot be used with --ocr or --objects.');
23
+ process.exit(1);
24
+ }
25
+ if (options.ocr && options.objects) {
26
+ console.error('Error: --ocr and --objects cannot be used together.');
27
+ process.exit(1);
28
+ }
29
+ imageId = (0, images_1.requireImageId)(imageId);
30
+ let image = options.cache !== false ? (0, storage_1.loadImageCache)(imageId) : null;
31
+ if (!image) {
32
+ image = await (0, api_1.getImageDetail)(imageId);
33
+ (0, storage_1.saveImageCache)(imageId, image);
34
+ }
35
+ const supplemented = (0, memory_1.supplementAltTextFromSearchCache)(image);
36
+ image = supplemented.image;
37
+ if (supplemented.supplemented) {
38
+ (0, storage_1.saveImageCache)(imageId, image);
39
+ }
40
+ const ocrDescription = (0, format_1.extractOcrDescription)(image);
41
+ const objects = (0, format_1.extractObjectAnnotations)(image);
42
+ if (options.ocr) {
43
+ if (!ocrDescription) {
44
+ console.error('OCR not found for this image.');
45
+ process.exit(1);
46
+ }
47
+ console.log(ocrDescription);
48
+ return;
49
+ }
50
+ if (options.objects) {
51
+ if (objects.length === 0) {
52
+ console.error('Object annotations not found for this image.');
53
+ process.exit(1);
54
+ }
55
+ console.log(objects.map(format_1.formatObjectAnnotationLine).join('\n'));
56
+ return;
57
+ }
58
+ if (options.json) {
59
+ console.log(JSON.stringify(image, null, 2));
60
+ }
61
+ else {
62
+ (0, images_1.printGetMarkdown)(image, ocrDescription, objects);
63
+ }
64
+ }
65
+ catch (error) {
66
+ console.error('Error getting image:', error.message);
67
+ process.exit(1);
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,74 @@
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.registerImportCommand = registerImportCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const storage_1 = require("../storage");
10
+ function registerImportCommand(program) {
11
+ program
12
+ .command('import <type> <dir>')
13
+ .description('Import legacy data (type: json|hourly)')
14
+ .action(async (type, dir) => {
15
+ const sourceDir = path_1.default.resolve(dir);
16
+ if (!fs_1.default.existsSync(sourceDir)) {
17
+ console.error(`Error: Source directory ${sourceDir} does not exist.`);
18
+ process.exit(1);
19
+ }
20
+ if (type === 'json') {
21
+ const targetDir = path_1.default.join((0, storage_1.getCacheDir)(), 'images');
22
+ console.log(`Importing legacy Gyazo JSON from ${sourceDir}...`);
23
+ let total = 0;
24
+ const walk = (d) => {
25
+ fs_1.default.readdirSync(d, { withFileTypes: true }).forEach(e => {
26
+ const p = path_1.default.join(d, e.name);
27
+ if (e.isDirectory())
28
+ walk(p);
29
+ else if (e.name.endsWith('.json')) {
30
+ const id = e.name.replace('.json', '');
31
+ const p1 = id[0] || '_', p2 = id[1] || '_';
32
+ const dest = path_1.default.join(targetDir, p1, p2);
33
+ if (!fs_1.default.existsSync(dest))
34
+ fs_1.default.mkdirSync(dest, { recursive: true });
35
+ fs_1.default.copyFileSync(p, path_1.default.join(dest, e.name));
36
+ total++;
37
+ if (total % 100 === 0)
38
+ process.stdout.write('.');
39
+ }
40
+ });
41
+ };
42
+ walk(sourceDir);
43
+ console.log(`\nImport complete. Copied ${total} files.`);
44
+ }
45
+ else if (type === 'hourly') {
46
+ console.log(`Importing legacy Gyazo hourly data from ${sourceDir}...`);
47
+ let total = 0;
48
+ const years = fs_1.default.readdirSync(sourceDir).filter(f => /^[0-9]{4}$/.test(f));
49
+ for (const y of years) {
50
+ const months = fs_1.default.readdirSync(path_1.default.join(sourceDir, y)).filter(f => /^[0-9]{2}$/.test(f));
51
+ for (const m of months) {
52
+ const days = fs_1.default.readdirSync(path_1.default.join(sourceDir, y, m)).filter(f => /^[0-9]{2}$/.test(f));
53
+ for (const d of days) {
54
+ const hours = fs_1.default.readdirSync(path_1.default.join(sourceDir, y, m, d)).filter(f => /^[0-9]{2}$/.test(f));
55
+ for (const h of hours) {
56
+ const txt = path_1.default.join(sourceDir, y, m, d, h, 'image_ids.txt');
57
+ if (fs_1.default.existsSync(txt)) {
58
+ const ids = fs_1.default.readFileSync(txt, 'utf-8').split('\n').map(id => id.trim()).filter(id => id.length > 0);
59
+ (0, storage_1.saveHourlyCache)(y, m, d, h, ids);
60
+ total++;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ process.stdout.write('.');
66
+ }
67
+ console.log(`\nImport complete. Copied ${total} hourly index files.`);
68
+ }
69
+ else {
70
+ console.error('Error: type must be "json" or "hourly"');
71
+ process.exit(1);
72
+ }
73
+ });
74
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerListCommand = registerListCommand;
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 images_1 = require("../services/images");
9
+ function registerListCommand(program) {
10
+ program
11
+ .command('list')
12
+ .alias('ls')
13
+ .description('List recent images')
14
+ .option('-p, --page <number>', 'page number', '1')
15
+ .option('-l, --limit <number>', 'items per page', '20')
16
+ .option('-j, --json', 'output as JSON')
17
+ .option('-H, --hour <yyyy-mm-dd-hh>', 'target hour')
18
+ .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
19
+ .option('--today', 'target today only')
20
+ .option('--max-pages <number>', 'max pages to scan for --date/--today mode', '100')
21
+ .option('--photos', 'alias of search "has:location"')
22
+ .option('--uploaded', 'alias of search "gyazocli_uploads"')
23
+ .option('--no-cache', 'force fetch from API')
24
+ .action(async (options) => {
25
+ await (0, credentials_1.ensureAccessToken)();
26
+ try {
27
+ const useCache = options.cache !== false;
28
+ const page = (0, options_1.parsePositiveIntegerOption)(options.page, '--page');
29
+ const limit = (0, options_1.parsePositiveIntegerOption)(options.limit, '--limit');
30
+ const maxPages = (0, options_1.parsePositiveIntegerOption)(options.maxPages, '--max-pages');
31
+ const hasDateRange = Boolean(options.date || options.today);
32
+ if (options.photos && options.uploaded) {
33
+ console.error('Error: --photos and --uploaded cannot be used together.');
34
+ process.exit(1);
35
+ }
36
+ if (options.today && options.date) {
37
+ console.error('Error: --today and --date cannot be used together.');
38
+ process.exit(1);
39
+ }
40
+ if ((options.photos || options.uploaded) && options.hour) {
41
+ console.error('Error: --photos/--uploaded and --hour cannot be used together.');
42
+ process.exit(1);
43
+ }
44
+ if (options.hour && hasDateRange) {
45
+ console.error('Error: --hour and --date/--today cannot be used together.');
46
+ process.exit(1);
47
+ }
48
+ const targetDate = hasDateRange
49
+ ? options.today
50
+ ? (0, dates_1.parseDateOption)()
51
+ : (0, dates_1.parseDateOption)(options.date)
52
+ : undefined;
53
+ let hour = null;
54
+ if (options.hour) {
55
+ hour = (0, dates_1.parseHourOption)(options.hour);
56
+ if (!hour) {
57
+ console.error('Error: hour format must be yyyy-mm-dd-hh');
58
+ process.exit(1);
59
+ }
60
+ }
61
+ const alias = options.photos
62
+ ? 'photos'
63
+ : options.uploaded
64
+ ? 'uploaded'
65
+ : undefined;
66
+ const { images, empty } = await (0, memory_1.listCaptures)({
67
+ page,
68
+ limit,
69
+ maxPages,
70
+ useCache,
71
+ date: targetDate,
72
+ hour: hour || undefined,
73
+ alias,
74
+ });
75
+ if (empty === 'date' && targetDate) {
76
+ console.log(`No images found for ${targetDate.dateKey}.`);
77
+ return;
78
+ }
79
+ if (empty === 'hour') {
80
+ console.log(`No images found for ${options.hour} in cache.`);
81
+ return;
82
+ }
83
+ if (options.json) {
84
+ console.log(JSON.stringify(images, null, 2));
85
+ return;
86
+ }
87
+ // A search result carries less than a listing does, so the alias paths
88
+ // ask for their results to be cached as they are shown.
89
+ const imagesForDisplay = await (0, images_1.prepareImagesForDisplay)(images, {
90
+ cacheSearchResults: Boolean(alias),
91
+ enrichLocation: true,
92
+ useCache,
93
+ });
94
+ (0, images_1.printListImages)(imagesForDisplay);
95
+ }
96
+ catch (error) {
97
+ console.error('Error listing images:', error.message);
98
+ process.exit(1);
99
+ }
100
+ });
101
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerLocationsCommand = registerLocationsCommand;
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 registerLocationsCommand(program) {
10
+ program
11
+ .command('locations')
12
+ .description('Rank metadata locations 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 ranking = [];
28
+ let totalWithLocation = 0;
29
+ let totalImages = 0;
30
+ if (useCache) {
31
+ let cacheSummary = (0, analytics_1.buildLocationsRankingFromHourlyCache)(targetDate);
32
+ if (cacheSummary.totalImages === 0) {
33
+ await (0, memory_1.warmDateCacheForLocations)(targetDate, maxPages, true);
34
+ cacheSummary = (0, analytics_1.buildLocationsRankingFromHourlyCache)(targetDate);
35
+ }
36
+ ranking = cacheSummary.ranking;
37
+ totalWithLocation = cacheSummary.imageCountWithLocations;
38
+ totalImages = cacheSummary.totalImages;
39
+ }
40
+ else {
41
+ const imageIds = await (0, memory_1.warmDateCacheForLocations)(targetDate, maxPages, false);
42
+ ranking = (0, analytics_1.buildLocationsRankingFromCache)(imageIds);
43
+ totalWithLocation = ranking.reduce((sum, item) => sum + item.count, 0);
44
+ totalImages = imageIds.length;
45
+ }
46
+ const displayedRanking = ranking.slice(0, limit);
47
+ if (options.json) {
48
+ console.log(JSON.stringify({
49
+ date: targetDate.dateKey,
50
+ image_count: totalImages,
51
+ location_image_count: totalWithLocation,
52
+ total_locations: ranking.length,
53
+ ranking: displayedRanking,
54
+ }, null, 2));
55
+ return;
56
+ }
57
+ if (ranking.length === 0) {
58
+ console.log(`No location metadata found for ${targetDate.dateKey}.`);
59
+ return;
60
+ }
61
+ console.log(`Locations on ${targetDate.dateKey}`);
62
+ displayedRanking.forEach((item, index) => {
63
+ console.log(`${index + 1}. ${item.location}: ${item.count}`);
64
+ });
65
+ console.log(`Total images with location metadata: ${totalWithLocation}`);
66
+ }
67
+ catch (error) {
68
+ console.error('Error ranking locations:', error.message);
69
+ process.exit(1);
70
+ }
71
+ });
72
+ }