@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 +66 -0
- package/dist/commands/apps.js +72 -0
- package/dist/commands/collection.js +45 -0
- package/dist/commands/config.js +64 -0
- package/dist/commands/domains.js +72 -0
- package/dist/commands/get.js +70 -0
- package/dist/commands/import.js +74 -0
- package/dist/commands/list.js +101 -0
- package/dist/commands/locations.js +72 -0
- package/dist/commands/search.js +43 -0
- package/dist/commands/stats.js +81 -0
- package/dist/commands/summary.js +42 -0
- package/dist/commands/sync.js +95 -0
- package/dist/commands/tags.js +70 -0
- package/dist/commands/upload.js +72 -0
- package/dist/dates.js +250 -0
- package/dist/format.js +375 -0
- package/dist/ids.js +77 -0
- package/dist/index.js +55 -2454
- package/dist/mcp.js +371 -0
- package/dist/options.js +20 -0
- package/dist/services/analytics.js +474 -0
- package/dist/services/collections.js +97 -0
- package/dist/services/images.js +184 -0
- package/dist/services/memory.js +373 -0
- package/docs/ADR/003-cli-structure.md +3 -1
- package/docs/ADR/004-mcp-server.md +94 -0
- package/docs/ADR/005-module-layout.md +72 -0
- package/package.json +2 -1
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
|
+
}
|