@yuiseki/gyazocli 0.4.0 → 0.5.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 +23 -2
- package/dist/index.js +1 -1
- package/dist/mcp.js +39 -16
- package/dist/services/memory.js +46 -0
- package/docs/ADR/003-cli-structure.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,12 +114,33 @@ Configured in a client:
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
### Query syntax
|
|
118
|
+
|
|
119
|
+
Bare words match the OCR text, title and description. These operators were
|
|
120
|
+
checked against the live API, each with a value that should match, reading the
|
|
121
|
+
results back from the detail endpoint to confirm the filter had applied:
|
|
122
|
+
|
|
123
|
+
| Operator | Matches |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `address:広島`, `address:Hiroshima`, `address:730-0041` | the reverse-geocoded address of a capture with GPS, in any language or case, postal codes included |
|
|
126
|
+
| `app:"Gyazo Android"` | the application the capture came from |
|
|
127
|
+
| `title:`, `url:`, `desc:` | the page it was captured from |
|
|
128
|
+
| `ocr:` | the text in the image |
|
|
129
|
+
| `type:png` | the file type |
|
|
130
|
+
| `has:location` | only captures with coordinates |
|
|
131
|
+
| `has:exif` | only captures with EXIF, which is not the same thing |
|
|
132
|
+
| `since:2026-08-30 until:2026-08-31` | the upload date |
|
|
133
|
+
| `-address:広島` | negation |
|
|
134
|
+
|
|
135
|
+
There is no coordinate or radius search. `location:`, `geo:`, `near:`,
|
|
136
|
+
`bbox:`, `city:`, `lat:` and the like all return nothing, exactly as an
|
|
137
|
+
invented operator does, so search by place with `address:`.
|
|
138
|
+
|
|
117
139
|
### Tools
|
|
118
140
|
|
|
119
141
|
- `gyazo_search`: full-text search over your captures. Arguments: `query`
|
|
120
142
|
(required, up to 200 characters), `page` (default 1), `per` (default 20,
|
|
121
|
-
max 100)
|
|
122
|
-
`app:"Google Chrome"`, `url:google.com`, `cat since:2024-01-01 until:2024-12-31`.
|
|
143
|
+
max 100), `include_location`. See the query syntax below.
|
|
123
144
|
- `gyazo_image`: metadata for one capture. Argument: `id_or_url` (required),
|
|
124
145
|
which accepts a bare 32-character ID, a `https://gyazo.com/<id>` permalink or
|
|
125
146
|
a direct image URL.
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ program
|
|
|
26
26
|
.name('gyazo')
|
|
27
27
|
.description('Gyazo Memory CLI for AI Secretary')
|
|
28
28
|
.option('--mcp-server', 'run as a Model Context Protocol server over stdio')
|
|
29
|
-
.version('0.
|
|
29
|
+
.version('0.5.0');
|
|
30
30
|
(0, config_1.registerConfigCommand)(program);
|
|
31
31
|
(0, list_1.registerListCommand)(program);
|
|
32
32
|
(0, get_1.registerGetCommand)(program);
|
package/dist/mcp.js
CHANGED
|
@@ -24,12 +24,30 @@ const format_1 = require("./format");
|
|
|
24
24
|
const memory_1 = require("./services/memory");
|
|
25
25
|
const analytics_1 = require("./services/analytics");
|
|
26
26
|
const collections_1 = require("./services/collections");
|
|
27
|
+
/**
|
|
28
|
+
* The operators below were checked against the live API rather than taken from
|
|
29
|
+
* documentation: each one was run with a value that should match, and the
|
|
30
|
+
* results were read back from the detail endpoint to confirm the filter had
|
|
31
|
+
* actually applied. An operator Gyazo does not know returns nothing at all
|
|
32
|
+
* rather than falling back to a text search, so an untested guess costs the
|
|
33
|
+
* model a turn.
|
|
34
|
+
*/
|
|
27
35
|
const SEARCH_QUERY_DESCRIPTION = [
|
|
28
|
-
'Search keyword
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
36
|
+
'Search keyword, up to 200 characters. Bare words match the OCR text, title and',
|
|
37
|
+
'description.',
|
|
38
|
+
'Operators, all confirmed to work: address: matches the reverse-geocoded address of',
|
|
39
|
+
'a capture with GPS, in any language and case, and also matches postal codes, so',
|
|
40
|
+
'address:広島 and address:Hiroshima and address:730-0041 all find the same photos;',
|
|
41
|
+
'app: the application it came from, as app:"Gyazo Android"; title:, url: and desc:',
|
|
42
|
+
'the page it was captured from; ocr: the text in the image; type: the file type, as',
|
|
43
|
+
'type:png; has:location only captures with coordinates; has:exif only captures with',
|
|
44
|
+
'EXIF, which is not the same thing; since: and until: bound the upload date, as',
|
|
45
|
+
'since:2026-08-30 until:2026-08-31. A leading - negates, as -address:広島. Quote a',
|
|
46
|
+
'value that contains spaces.',
|
|
47
|
+
'There is no coordinate or radius search: location:, geo:, near:, bbox:, city: and',
|
|
48
|
+
'the like all return nothing. To search by place, use address: with a place name.',
|
|
49
|
+
'If nothing suitable comes back, rephrase the query to match what the user meant and',
|
|
50
|
+
'search again rather than giving up on the first attempt.',
|
|
33
51
|
].join(' ');
|
|
34
52
|
function serverVersion() {
|
|
35
53
|
// The published tarball always contains package.json, and dist/ sits one
|
|
@@ -165,6 +183,15 @@ function logged(name, handler) {
|
|
|
165
183
|
function asJsonResult(payload) {
|
|
166
184
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
167
185
|
}
|
|
186
|
+
const INCLUDE_LOCATION = zod_1.z
|
|
187
|
+
.boolean()
|
|
188
|
+
.default(true)
|
|
189
|
+
.describe('Fill in the location, which the listing and search endpoints leave out. Costs one ' +
|
|
190
|
+
'extra lookup per capture the local cache does not already hold. Set false when the ' +
|
|
191
|
+
'coordinates do not matter');
|
|
192
|
+
async function withLocations(images, includeLocation) {
|
|
193
|
+
return includeLocation ? (0, memory_1.enrichImageLocations)(images) : images;
|
|
194
|
+
}
|
|
168
195
|
function asMetadataListResult(images) {
|
|
169
196
|
if (!images || images.length === 0) {
|
|
170
197
|
return NO_IMAGES;
|
|
@@ -208,21 +235,15 @@ function createMcpServer() {
|
|
|
208
235
|
.max(100)
|
|
209
236
|
.default(20)
|
|
210
237
|
.describe('Number of results per page (max: 100)'),
|
|
238
|
+
include_location: INCLUDE_LOCATION,
|
|
211
239
|
},
|
|
212
240
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
213
|
-
}, logged('gyazo_search', async ({ query, page, per }) => {
|
|
241
|
+
}, logged('gyazo_search', async ({ query, page, per, include_location: includeLocation }) => {
|
|
214
242
|
const images = await (0, api_1.searchImages)(query, page, per);
|
|
215
243
|
if (!images || images.length === 0) {
|
|
216
244
|
return NO_IMAGES;
|
|
217
245
|
}
|
|
218
|
-
return
|
|
219
|
-
content: [
|
|
220
|
-
{
|
|
221
|
-
type: 'text',
|
|
222
|
-
text: JSON.stringify(images.map(toMetadata), null, 2),
|
|
223
|
-
},
|
|
224
|
-
],
|
|
225
|
-
};
|
|
246
|
+
return asMetadataListResult(await withLocations(images, includeLocation));
|
|
226
247
|
}));
|
|
227
248
|
server.registerTool('gyazo_image', {
|
|
228
249
|
title: 'Describe one Gyazo capture',
|
|
@@ -309,6 +330,7 @@ function createMcpServer() {
|
|
|
309
330
|
.boolean()
|
|
310
331
|
.default(true)
|
|
311
332
|
.describe('Answer from the local cache where possible. Set false to force a fetch'),
|
|
333
|
+
include_location: INCLUDE_LOCATION,
|
|
312
334
|
},
|
|
313
335
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
314
336
|
}, logged('gyazo_list', async (args) => {
|
|
@@ -337,7 +359,7 @@ function createMcpServer() {
|
|
|
337
359
|
hour: hour || undefined,
|
|
338
360
|
alias,
|
|
339
361
|
});
|
|
340
|
-
return asMetadataListResult(images);
|
|
362
|
+
return asMetadataListResult(await withLocations(images, args.include_location));
|
|
341
363
|
}));
|
|
342
364
|
server.registerTool('gyazo_summary', {
|
|
343
365
|
title: 'Summarise a stretch of Gyazo captures',
|
|
@@ -469,6 +491,7 @@ function createMcpServer() {
|
|
|
469
491
|
.max(20)
|
|
470
492
|
.default(5)
|
|
471
493
|
.describe('How many pages of 100 to walk before giving up on the boundary'),
|
|
494
|
+
include_location: INCLUDE_LOCATION,
|
|
472
495
|
},
|
|
473
496
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
474
497
|
}, logged('gyazo_recent', async (args) => {
|
|
@@ -506,7 +529,7 @@ function createMcpServer() {
|
|
|
506
529
|
'recent pages of captures. It may be older than that, or belong to another ' +
|
|
507
530
|
'account. Ask for a window in minutes instead, or raise max_pages.');
|
|
508
531
|
}
|
|
509
|
-
return asMetadataListResult(result.images);
|
|
532
|
+
return asMetadataListResult(await withLocations(result.images, args.include_location));
|
|
510
533
|
}));
|
|
511
534
|
server.registerTool('gyazo_collections', {
|
|
512
535
|
title: 'Find a Gyazo collection by name',
|
package/dist/services/memory.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.supplementAltTextFromSearchCache = supplementAltTextFromSearchCache;
|
|
|
15
15
|
exports.supplementAltTextForDisplay = supplementAltTextForDisplay;
|
|
16
16
|
exports.listCaptures = listCaptures;
|
|
17
17
|
exports.listCapturesSince = listCapturesSince;
|
|
18
|
+
exports.enrichImageLocations = enrichImageLocations;
|
|
18
19
|
/**
|
|
19
20
|
* The memory this CLI keeps: the local cache of captures, and the walks over
|
|
20
21
|
* the Gyazo API that fill it. A command asks for a day or a range, and this
|
|
@@ -415,3 +416,48 @@ async function listCapturesSince(options) {
|
|
|
415
416
|
}
|
|
416
417
|
return { images: collected.slice(0, limit), pagesWalked };
|
|
417
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Fill in what the lean endpoints leave out.
|
|
421
|
+
*
|
|
422
|
+
* The listing and the search endpoints return an image without its
|
|
423
|
+
* coordinates or its address, whatever the capture actually carries; only the
|
|
424
|
+
* detail endpoint has them. So a caller that needs a location has to ask again
|
|
425
|
+
* per image, which is what this does: the cache first, the API for the rest,
|
|
426
|
+
* a few at a time, writing what it fetches back to the cache so the next look
|
|
427
|
+
* is free.
|
|
428
|
+
*/
|
|
429
|
+
async function enrichImageLocations(images, options = {}) {
|
|
430
|
+
const useCache = options.useCache !== false;
|
|
431
|
+
const limit = options.limit ?? 40;
|
|
432
|
+
const concurrency = Math.max(1, options.concurrency ?? 5);
|
|
433
|
+
const enriched = [...images];
|
|
434
|
+
const pending = [];
|
|
435
|
+
for (let index = 0; index < enriched.length && pending.length < limit; index++) {
|
|
436
|
+
const image = enriched[index];
|
|
437
|
+
if (image?.metadata?.exif_normalized || image?.metadata?.exif_address)
|
|
438
|
+
continue;
|
|
439
|
+
if (useCache) {
|
|
440
|
+
const cached = (0, storage_1.loadImageCache)(image?.image_id);
|
|
441
|
+
if (cached) {
|
|
442
|
+
enriched[index] = (0, format_1.mergeImageForDisplay)(image, cached);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
pending.push(index);
|
|
447
|
+
}
|
|
448
|
+
for (let start = 0; start < pending.length; start += concurrency) {
|
|
449
|
+
const batch = pending.slice(start, start + concurrency);
|
|
450
|
+
await Promise.all(batch.map(async (index) => {
|
|
451
|
+
const image = enriched[index];
|
|
452
|
+
try {
|
|
453
|
+
const detail = await (0, api_1.getImageDetail)(image.image_id);
|
|
454
|
+
(0, storage_1.saveImageCache)(image.image_id, detail);
|
|
455
|
+
enriched[index] = (0, format_1.mergeImageForDisplay)(image, detail);
|
|
456
|
+
}
|
|
457
|
+
catch (_error) {
|
|
458
|
+
// A capture that cannot be fetched keeps what the listing said.
|
|
459
|
+
}
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
return enriched;
|
|
463
|
+
}
|