@yuiseki/gyazocli 0.5.0 → 0.5.2
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 +13 -2
- package/dist/api.js +5 -1
- package/dist/index.js +1 -1
- package/dist/mcp.js +50 -22
- package/dist/services/memory.js +10 -13
- package/docs/ADR/003-cli-structure.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,15 +122,26 @@ results back from the detail endpoint to confirm the filter had applied:
|
|
|
122
122
|
|
|
123
123
|
| Operator | Matches |
|
|
124
124
|
| --- | --- |
|
|
125
|
+
| `has:exif` | photographs rather than screenshots |
|
|
126
|
+
| `has:location` | captures with coordinates |
|
|
125
127
|
| `address:広島`, `address:Hiroshima`, `address:730-0041` | the reverse-geocoded address of a capture with GPS, in any language or case, postal codes included |
|
|
126
128
|
| `app:"Gyazo Android"` | the application the capture came from |
|
|
127
129
|
| `title:`, `url:`, `desc:` | the page it was captured from |
|
|
128
130
|
| `ocr:` | the text in the image |
|
|
129
131
|
| `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
132
|
| `since:2026-08-30 until:2026-08-31` | the upload date |
|
|
133
133
|
| `-address:広島` | negation |
|
|
134
|
+
| `OR` | alternation; terms are ANDed otherwise |
|
|
135
|
+
|
|
136
|
+
To narrow to photographs, reach for `has:exif`. The application does not tell
|
|
137
|
+
them apart: `app:"Gyazo Android"` includes screenshots and screen recordings
|
|
138
|
+
from the same phone, and one page of `app:"Gyazo Android" -has:exif` came back
|
|
139
|
+
as 68 gif and 30 png against 2 jpg.
|
|
140
|
+
|
|
141
|
+
`has:exif` and `has:location` overlap without either containing the other. A
|
|
142
|
+
photo taken indoors has EXIF and no coordinates; 86 captures here carry
|
|
143
|
+
coordinates without the EXIF flag. `has:exif OR has:location` is the widest
|
|
144
|
+
reading of "a photo".
|
|
134
145
|
|
|
135
146
|
There is no coordinate or radius search. `location:`, `geo:`, `near:`,
|
|
136
147
|
`bbox:`, `city:`, `lat:` and the like all return nothing, exactly as an
|
package/dist/api.js
CHANGED
|
@@ -119,7 +119,11 @@ async function listCollectionImages(collectionId, page = 1, per = 100) {
|
|
|
119
119
|
*/
|
|
120
120
|
async function fetchImageRendition(imageId, width, format = 'webp') {
|
|
121
121
|
const extension = format === 'jpeg' ? 'jpg' : 'webp';
|
|
122
|
-
|
|
122
|
+
// The `-jpg` before the extension is a source-type marker, and it has to be
|
|
123
|
+
// there: without it the route answers 404 for any capture whose derivative
|
|
124
|
+
// is not already stored, which is most of them. Its value is ignored, and
|
|
125
|
+
// the extension alone decides what comes back, so a constant will do.
|
|
126
|
+
const url = `${imageOrigin()}/thumb/${width}_w/${imageId}-jpg.${extension}`;
|
|
123
127
|
const response = await axios_1.default.get(url, { responseType: 'arraybuffer' });
|
|
124
128
|
const data = Buffer.from(response.data);
|
|
125
129
|
const contentType = String(response.headers['content-type'] || '').split(';')[0].trim();
|
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.5.
|
|
29
|
+
.version('0.5.2');
|
|
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
|
@@ -34,20 +34,26 @@ const collections_1 = require("./services/collections");
|
|
|
34
34
|
*/
|
|
35
35
|
const SEARCH_QUERY_DESCRIPTION = [
|
|
36
36
|
'Search keyword, up to 200 characters. Bare words match the OCR text, title and',
|
|
37
|
-
'description.',
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
37
|
+
'description. Terms are ANDed; OR in capitals is honoured, and a leading - negates.',
|
|
38
|
+
'To narrow to photographs rather than screenshots, use has:exif. The application is',
|
|
39
|
+
'not the distinction: app:"Gyazo Android" includes screenshots and screen recordings',
|
|
40
|
+
'from the same phone, and of one such page 68 were gif and 30 png against 2 jpg.',
|
|
41
|
+
'has:location narrows to captures with coordinates. It is not a subset of has:exif',
|
|
42
|
+
'and does not contain it either: a photo taken indoors has EXIF and no coordinates,',
|
|
43
|
+
'and some photos carry coordinates without the EXIF flag, so has:exif OR has:location',
|
|
44
|
+
'is the widest reading of "a photo".',
|
|
45
|
+
'address: matches the reverse-geocoded address of a capture with GPS, in any language',
|
|
46
|
+
'and case, and matches postal codes too, so address:広島 and address:Hiroshima and',
|
|
47
|
+
'address:730-0041 all find the same photos.',
|
|
48
|
+
'Also: app: the application it came from; title:, url: and desc: the page it was',
|
|
49
|
+
'captured from; ocr: the text in the image; type: the file type, as type:png; since:',
|
|
50
|
+
'and until: bound the upload date, as since:2026-08-30 until:2026-08-31. Quote a value',
|
|
51
|
+
'that contains spaces.',
|
|
52
|
+
'There is no coordinate or radius search: location:, geo:, near:, bbox:, city: and the',
|
|
53
|
+
'like all return nothing. To search by place, use address: with a place name.',
|
|
54
|
+
'An operator Gyazo does not know returns nothing rather than matching as text, so an',
|
|
55
|
+
'invented operator looks exactly like "no such captures". If nothing suitable comes',
|
|
56
|
+
'back, rephrase the query rather than giving up on the first attempt.',
|
|
51
57
|
].join(' ');
|
|
52
58
|
function serverVersion() {
|
|
53
59
|
// The published tarball always contains package.json, and dist/ sits one
|
|
@@ -187,16 +193,35 @@ const INCLUDE_LOCATION = zod_1.z
|
|
|
187
193
|
.boolean()
|
|
188
194
|
.default(true)
|
|
189
195
|
.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
|
|
191
|
-
'
|
|
196
|
+
'extra lookup per capture the local cache does not already hold, so a page of 40 ' +
|
|
197
|
+
'uncached captures adds a few seconds; the next look at the same captures is free. ' +
|
|
198
|
+
'Set false when the coordinates do not matter');
|
|
199
|
+
/**
|
|
200
|
+
* Fill in the locations, and say so when the per-call limit stopped it part
|
|
201
|
+
* way: past that point a capture without a location is indistinguishable from
|
|
202
|
+
* one that has none, which is exactly the sort of silence worth breaking.
|
|
203
|
+
*/
|
|
192
204
|
async function withLocations(images, includeLocation) {
|
|
193
|
-
|
|
205
|
+
if (!includeLocation)
|
|
206
|
+
return { images };
|
|
207
|
+
const result = await (0, memory_1.enrichImageLocations)(images);
|
|
208
|
+
if (result.skipped === 0)
|
|
209
|
+
return { images: result.images };
|
|
210
|
+
return {
|
|
211
|
+
images: result.images,
|
|
212
|
+
note: `The location was filled in for ${result.considered} captures; ${result.skipped} were ` +
|
|
213
|
+
'left as the listing returned them, so their missing location means "not looked up" ' +
|
|
214
|
+
'rather than "no location". Ask for fewer at a time to cover them all.',
|
|
215
|
+
};
|
|
194
216
|
}
|
|
195
|
-
function asMetadataListResult(images) {
|
|
217
|
+
function asMetadataListResult(images, note) {
|
|
196
218
|
if (!images || images.length === 0) {
|
|
197
219
|
return NO_IMAGES;
|
|
198
220
|
}
|
|
199
|
-
|
|
221
|
+
const json = asJsonResult(images.map(toMetadata));
|
|
222
|
+
if (!note)
|
|
223
|
+
return json;
|
|
224
|
+
return { content: [...json.content, { type: 'text', text: note }] };
|
|
200
225
|
}
|
|
201
226
|
/**
|
|
202
227
|
* A date argument, refused by throwing. The CLI reports and exits here, which
|
|
@@ -243,7 +268,8 @@ function createMcpServer() {
|
|
|
243
268
|
if (!images || images.length === 0) {
|
|
244
269
|
return NO_IMAGES;
|
|
245
270
|
}
|
|
246
|
-
|
|
271
|
+
const withLocation = await withLocations(images, includeLocation);
|
|
272
|
+
return asMetadataListResult(withLocation.images, withLocation.note);
|
|
247
273
|
}));
|
|
248
274
|
server.registerTool('gyazo_image', {
|
|
249
275
|
title: 'Describe one Gyazo capture',
|
|
@@ -359,7 +385,8 @@ function createMcpServer() {
|
|
|
359
385
|
hour: hour || undefined,
|
|
360
386
|
alias,
|
|
361
387
|
});
|
|
362
|
-
|
|
388
|
+
const withLocation = await withLocations(images, args.include_location);
|
|
389
|
+
return asMetadataListResult(withLocation.images, withLocation.note);
|
|
363
390
|
}));
|
|
364
391
|
server.registerTool('gyazo_summary', {
|
|
365
392
|
title: 'Summarise a stretch of Gyazo captures',
|
|
@@ -529,7 +556,8 @@ function createMcpServer() {
|
|
|
529
556
|
'recent pages of captures. It may be older than that, or belong to another ' +
|
|
530
557
|
'account. Ask for a window in minutes instead, or raise max_pages.');
|
|
531
558
|
}
|
|
532
|
-
|
|
559
|
+
const withLocation = await withLocations(result.images, args.include_location);
|
|
560
|
+
return asMetadataListResult(withLocation.images, withLocation.note);
|
|
533
561
|
}));
|
|
534
562
|
server.registerTool('gyazo_collections', {
|
|
535
563
|
title: 'Find a Gyazo collection by name',
|
package/dist/services/memory.js
CHANGED
|
@@ -416,26 +416,23 @@ async function listCapturesSince(options) {
|
|
|
416
416
|
}
|
|
417
417
|
return { images: collected.slice(0, limit), pagesWalked };
|
|
418
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
419
|
async function enrichImageLocations(images, options = {}) {
|
|
430
420
|
const useCache = options.useCache !== false;
|
|
431
421
|
const limit = options.limit ?? 40;
|
|
432
|
-
const concurrency = Math.max(1, options.concurrency ??
|
|
422
|
+
const concurrency = Math.max(1, options.concurrency ?? 10);
|
|
433
423
|
const enriched = [...images];
|
|
434
424
|
const pending = [];
|
|
435
|
-
|
|
425
|
+
let considered = 0;
|
|
426
|
+
let skipped = 0;
|
|
427
|
+
for (let index = 0; index < enriched.length; index++) {
|
|
436
428
|
const image = enriched[index];
|
|
437
429
|
if (image?.metadata?.exif_normalized || image?.metadata?.exif_address)
|
|
438
430
|
continue;
|
|
431
|
+
if (considered >= limit) {
|
|
432
|
+
skipped++;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
considered++;
|
|
439
436
|
if (useCache) {
|
|
440
437
|
const cached = (0, storage_1.loadImageCache)(image?.image_id);
|
|
441
438
|
if (cached) {
|
|
@@ -459,5 +456,5 @@ async function enrichImageLocations(images, options = {}) {
|
|
|
459
456
|
}
|
|
460
457
|
}));
|
|
461
458
|
}
|
|
462
|
-
return enriched;
|
|
459
|
+
return { images: enriched, considered, skipped };
|
|
463
460
|
}
|