@yuiseki/gyazocli 0.0.2 → 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.
package/README.md CHANGED
@@ -16,17 +16,37 @@ gyazo sync --days 10
16
16
  gyazo --help
17
17
  ```
18
18
 
19
+ ### Shorthands
20
+
21
+ The first argument may stand on its own when it is unambiguous. A Gyazo
22
+ image ID is 32 hex characters, so it can never be mistaken for a path.
23
+
24
+ ```bash
25
+ gyazo path/to/image.png # same as: gyazo upload path/to/image.png
26
+ gyazo 49a008e2f254f513063b6ec4d3082940 # same as: gyazo get 49a008e2f254f513063b6ec4d3082940
27
+ gyazo https://gyazo.com/49a008e2f254f513063b6ec4d3082940 # same as above
28
+ gyazo https://gyazo.com/collections/21ca16a1023c667a7a437be561a65018 # same as: gyazo collection <id>
29
+ ```
30
+
31
+ A collection ID is 32 hex characters just like an image ID, so a bare ID is read
32
+ as an image. Only the `/collections/<id>` URL form is unambiguous. When a command
33
+ is given the wrong kind of ID it points at the other one.
34
+
35
+ Subcommand names always win, so `gyazo search` stays `gyazo search` even
36
+ if a file of that name exists in the working directory.
37
+
19
38
  ### Detail
20
39
 
21
40
  - `gyazo config set token <token>`: Save your access token
22
41
  - `gyazo config get token|me`: Show saved token (masked) or `me` profile info
23
42
  - `gyazo ls` (`gyazo list`): List images (`--date`/`--today`, `--photos`, `--uploaded`, `-H` available; `--photos/--uploaded` can be combined with `--date`/`--today`)
24
43
  - `gyazo search <query>`: Search images
25
- - `gyazo get <image_id>`: Show image details (`--ocr`, `--objects`, `-j` available)
44
+ - `gyazo collection <collection_id|url>` (`col`, `cols`, `collections`): Show a collection and the images in it (`--sort added|created|captured`, `-A`, `-j` available)
45
+ - `gyazo get <image_id|url>`: Show image details (`--ocr`, `--objects`, `-j` available). Accepts a bare image ID, a `https://gyazo.com/<id>` permalink, or a `https://i.gyazo.com/<id>.png` URL
26
46
  - `gyazo apps|domains|tags|locations`: Show rankings
27
47
  - `gyazo summary`: Show day-by-day weekly summary in Markdown (`##`/`###` headings, image count, apps, domains, tags, locations per day)
28
48
  - `gyazo stats`: Show weekly summary
29
- - `gyazo upload [path]`: Upload an image (uses stdin when path is omitted)
49
+ - `gyazo upload [path]`: Upload an image (uses stdin when path is omitted). Prints the permalink URL alone; use `-j` for the full response
30
50
  - `gyazo sync`: Sync cache
31
51
 
32
52
  Date range notes:
@@ -34,7 +54,97 @@ Date range notes:
34
54
  - Use `--today` for today only, or `--date <yyyy|yyyy-mm|yyyy-mm-dd>` for a custom range
35
55
 
36
56
  JSON output:
37
- - `-j, --json` is available for `config get`, `ls`, `get`, `search`, `apps`, `domains`, `tags`, `locations`, and `summary`
57
+ - `-j, --json` is available for `config get`, `ls`, `get`, `search`, `apps`, `domains`, `tags`, `locations`, `summary`, and `upload`
58
+
59
+ ### Anonymous access
60
+
61
+ A Gyazo ID is long enough to act as the key to the image, so public images and
62
+ collections read fine with no token. An access token, on the other hand, allows
63
+ things this CLI does not expose (deleting, for one), so the way to give an agent
64
+ read-only access is to give it no token at all.
65
+
66
+ ```bash
67
+ gyazo collection <id> # uses the token if there is one, otherwise reads anonymously
68
+ gyazo collection <id> --anonymous # ignores the token even when one is configured
69
+ ```
70
+
71
+ Anonymous reads have limits worth knowing:
72
+
73
+ - A collection or image set to `only_me` returns 404, indistinguishable from one
74
+ that does not exist. That is deliberate: it keeps the ID from confirming what
75
+ exists.
76
+ - An image with `metadata_is_public: false` still returns 200, but `metadata` and
77
+ `created_at` come back `null`, so OCR, EXIF and location are gone. The web
78
+ endpoint withholds these even from the owner's token; `gyazo get` uses
79
+ `api.gyazo.com` for that reason.
80
+
81
+ Exit codes:
82
+ - `0` on success, `1` on a usage error or a failed API call
83
+
84
+ Environment variables:
85
+ - `GYAZO_ACCESS_TOKEN`: access token (takes precedence over the saved config)
86
+ - `GYAZO_CACHE_DIR`: cache location
87
+ - `GYAZO_API_ORIGIN` / `GYAZO_UPLOAD_ORIGIN` / `GYAZO_WEB_ORIGIN`: override the endpoints (used by the test suite)
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: URLs, timestamp, OCR text, title, source application and page, and
139
+ location when the capture carries one. Use the URLs in a result to show the
140
+ capture itself. Handing base64 image data to a model turned out not to work
141
+ well in practice, and describing a capture does.
142
+
143
+ Tool names and arguments follow
144
+ [nota/gyazo-mcp-server](https://github.com/nota/gyazo-mcp-server), so a client
145
+ already configured against that server can point at this one instead. Its
146
+ `gyazo_upload` is deliberately absent: nothing here can write to your Gyazo
147
+ account until there is a reason for it to.
38
148
 
39
149
  ## Development
40
150
 
@@ -45,6 +155,17 @@ npm install
45
155
  npm run build
46
156
  ```
47
157
 
158
+ ### Test
159
+
160
+ ```bash
161
+ npm test
162
+ ```
163
+
164
+ ### Release
165
+
166
+ Push a `v*` tag and Actions stages the package on npm, where a maintainer
167
+ approves it with 2FA. See [RELEASE.md](RELEASE.md).
168
+
48
169
  ### Link local CLI with npm link
49
170
 
50
171
  ```bash
package/dist/api.js CHANGED
@@ -7,18 +7,35 @@ exports.listImages = listImages;
7
7
  exports.getImageDetail = getImageDetail;
8
8
  exports.searchImages = searchImages;
9
9
  exports.getCurrentUser = getCurrentUser;
10
+ exports.getCollection = getCollection;
10
11
  exports.uploadImage = uploadImage;
11
12
  const axios_1 = __importDefault(require("axios"));
12
13
  const form_data_1 = __importDefault(require("form-data"));
13
14
  const config_1 = require("./config");
14
- const API_BASE_URL = 'https://api.gyazo.com/api/images';
15
- const API_SEARCH_URL = 'https://api.gyazo.com/api/search';
16
- const API_USERS_ME_URL = 'https://api.gyazo.com/api/users/me';
17
- const API_UPLOAD_URL = 'https://upload.gyazo.com/api/upload';
18
- async function requestWithRetry(url, params = {}) {
19
- const headers = { Authorization: `Bearer ${config_1.config.GYAZO_ACCESS_TOKEN}` };
15
+ const DEFAULT_API_ORIGIN = 'https://api.gyazo.com';
16
+ const DEFAULT_UPLOAD_ORIGIN = 'https://upload.gyazo.com';
17
+ const DEFAULT_WEB_ORIGIN = 'https://gyazo.com';
18
+ function stripTrailingSlash(origin) {
19
+ return origin.replace(/\/+$/, '');
20
+ }
21
+ function apiOrigin() {
22
+ return stripTrailingSlash(config_1.config.GYAZO_API_ORIGIN || DEFAULT_API_ORIGIN);
23
+ }
24
+ function uploadOrigin() {
25
+ return stripTrailingSlash(config_1.config.GYAZO_UPLOAD_ORIGIN || DEFAULT_UPLOAD_ORIGIN);
26
+ }
27
+ function webOrigin() {
28
+ return stripTrailingSlash(config_1.config.GYAZO_WEB_ORIGIN || DEFAULT_WEB_ORIGIN);
29
+ }
30
+ const apiBaseUrl = () => `${apiOrigin()}/api/images`;
31
+ const apiSearchUrl = () => `${apiOrigin()}/api/search`;
32
+ const apiUsersMeUrl = () => `${apiOrigin()}/api/users/me`;
33
+ const apiUploadUrl = () => `${uploadOrigin()}/api/upload`;
34
+ const webCollectionUrl = (id) => `${webOrigin()}/collections/${id}.json`;
35
+ async function requestWithRetry(url, params = {}, headers) {
36
+ const requestHeaders = headers ?? { Authorization: `Bearer ${config_1.config.GYAZO_ACCESS_TOKEN}` };
20
37
  try {
21
- const response = await axios_1.default.get(url, { headers, params });
38
+ const response = await axios_1.default.get(url, { headers: requestHeaders, params });
22
39
  return response.data;
23
40
  }
24
41
  catch (error) {
@@ -26,22 +43,35 @@ async function requestWithRetry(url, params = {}) {
26
43
  const retryAfter = parseInt(error.response.headers['retry-after'] || '5', 10);
27
44
  console.warn(`Rate limited. Retrying after ${retryAfter} seconds...`);
28
45
  await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
29
- return requestWithRetry(url, params);
46
+ return requestWithRetry(url, params, headers);
30
47
  }
31
48
  throw error;
32
49
  }
33
50
  }
34
51
  async function listImages(page = 1, perPage = 20) {
35
- return requestWithRetry(API_BASE_URL, { page, per_page: perPage });
52
+ return requestWithRetry(apiBaseUrl(), { page, per_page: perPage });
36
53
  }
37
54
  async function getImageDetail(imageId) {
38
- return requestWithRetry(`${API_BASE_URL}/${imageId}`);
55
+ return requestWithRetry(`${apiBaseUrl()}/${imageId}`);
39
56
  }
40
57
  async function searchImages(query, page = 1, perPage = 20) {
41
- return requestWithRetry(API_SEARCH_URL, { query, page, per: perPage });
58
+ return requestWithRetry(apiSearchUrl(), { query, page, per: perPage });
42
59
  }
43
60
  async function getCurrentUser() {
44
- return requestWithRetry(API_USERS_ME_URL);
61
+ return requestWithRetry(apiUsersMeUrl());
62
+ }
63
+ /**
64
+ * Collections are read through the public web endpoint. It returns the
65
+ * collection metadata plus the first 100 images with their full detail in a
66
+ * single request, and it works without a token for public collections, which
67
+ * is what lets an agent run read-only with no credentials at all.
68
+ */
69
+ async function getCollection(collectionId, options = {}) {
70
+ const headers = {};
71
+ if (!options.anonymous && config_1.config.GYAZO_ACCESS_TOKEN) {
72
+ headers.Authorization = `Bearer ${config_1.config.GYAZO_ACCESS_TOKEN}`;
73
+ }
74
+ return requestWithRetry(webCollectionUrl(collectionId), {}, headers);
45
75
  }
46
76
  async function uploadImage(options) {
47
77
  const form = new form_data_1.default();
@@ -60,7 +90,7 @@ async function uploadImage(options) {
60
90
  if (typeof options.timestamp === 'number') {
61
91
  form.append('created_at', String(options.timestamp));
62
92
  }
63
- const response = await axios_1.default.post(API_UPLOAD_URL, form, {
93
+ const response = await axios_1.default.post(apiUploadUrl(), form, {
64
94
  headers: form.getHeaders(),
65
95
  });
66
96
  return response.data;
@@ -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
+ }