@yuiseki/gyazocli 0.4.0 → 0.5.1

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
@@ -114,12 +114,44 @@ 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
+ | `has:exif` | photographs rather than screenshots |
126
+ | `has:location` | captures with coordinates |
127
+ | `address:広島`, `address:Hiroshima`, `address:730-0041` | the reverse-geocoded address of a capture with GPS, in any language or case, postal codes included |
128
+ | `app:"Gyazo Android"` | the application the capture came from |
129
+ | `title:`, `url:`, `desc:` | the page it was captured from |
130
+ | `ocr:` | the text in the image |
131
+ | `type:png` | the file type |
132
+ | `since:2026-08-30 until:2026-08-31` | the upload date |
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".
145
+
146
+ There is no coordinate or radius search. `location:`, `geo:`, `near:`,
147
+ `bbox:`, `city:`, `lat:` and the like all return nothing, exactly as an
148
+ invented operator does, so search by place with `address:`.
149
+
117
150
  ### Tools
118
151
 
119
152
  - `gyazo_search`: full-text search over your captures. Arguments: `query`
120
153
  (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`.
154
+ max 100), `include_location`. See the query syntax below.
123
155
  - `gyazo_image`: metadata for one capture. Argument: `id_or_url` (required),
124
156
  which accepts a bare 32-character ID, a `https://gyazo.com/<id>` permalink or
125
157
  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.4.0');
29
+ .version('0.5.1');
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,36 @@ 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 (max length: 200 characters).',
29
- 'Examples: cat | title:cat | app:"Google Chrome" | url:google.com |',
30
- 'cat since:2024-01-01 until:2024-12-31.',
31
- 'If nothing suitable comes back, rephrase the query to match what the user',
32
- 'meant and search again rather than giving up on the first attempt.',
36
+ 'Search keyword, up to 200 characters. Bare words match the OCR text, title and',
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.',
33
57
  ].join(' ');
34
58
  function serverVersion() {
35
59
  // The published tarball always contains package.json, and dist/ sits one
@@ -165,6 +189,15 @@ function logged(name, handler) {
165
189
  function asJsonResult(payload) {
166
190
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
167
191
  }
192
+ const INCLUDE_LOCATION = zod_1.z
193
+ .boolean()
194
+ .default(true)
195
+ .describe('Fill in the location, which the listing and search endpoints leave out. Costs one ' +
196
+ 'extra lookup per capture the local cache does not already hold. Set false when the ' +
197
+ 'coordinates do not matter');
198
+ async function withLocations(images, includeLocation) {
199
+ return includeLocation ? (0, memory_1.enrichImageLocations)(images) : images;
200
+ }
168
201
  function asMetadataListResult(images) {
169
202
  if (!images || images.length === 0) {
170
203
  return NO_IMAGES;
@@ -208,21 +241,15 @@ function createMcpServer() {
208
241
  .max(100)
209
242
  .default(20)
210
243
  .describe('Number of results per page (max: 100)'),
244
+ include_location: INCLUDE_LOCATION,
211
245
  },
212
246
  annotations: { readOnlyHint: true, openWorldHint: true },
213
- }, logged('gyazo_search', async ({ query, page, per }) => {
247
+ }, logged('gyazo_search', async ({ query, page, per, include_location: includeLocation }) => {
214
248
  const images = await (0, api_1.searchImages)(query, page, per);
215
249
  if (!images || images.length === 0) {
216
250
  return NO_IMAGES;
217
251
  }
218
- return {
219
- content: [
220
- {
221
- type: 'text',
222
- text: JSON.stringify(images.map(toMetadata), null, 2),
223
- },
224
- ],
225
- };
252
+ return asMetadataListResult(await withLocations(images, includeLocation));
226
253
  }));
227
254
  server.registerTool('gyazo_image', {
228
255
  title: 'Describe one Gyazo capture',
@@ -309,6 +336,7 @@ function createMcpServer() {
309
336
  .boolean()
310
337
  .default(true)
311
338
  .describe('Answer from the local cache where possible. Set false to force a fetch'),
339
+ include_location: INCLUDE_LOCATION,
312
340
  },
313
341
  annotations: { readOnlyHint: true, openWorldHint: true },
314
342
  }, logged('gyazo_list', async (args) => {
@@ -337,7 +365,7 @@ function createMcpServer() {
337
365
  hour: hour || undefined,
338
366
  alias,
339
367
  });
340
- return asMetadataListResult(images);
368
+ return asMetadataListResult(await withLocations(images, args.include_location));
341
369
  }));
342
370
  server.registerTool('gyazo_summary', {
343
371
  title: 'Summarise a stretch of Gyazo captures',
@@ -469,6 +497,7 @@ function createMcpServer() {
469
497
  .max(20)
470
498
  .default(5)
471
499
  .describe('How many pages of 100 to walk before giving up on the boundary'),
500
+ include_location: INCLUDE_LOCATION,
472
501
  },
473
502
  annotations: { readOnlyHint: true, openWorldHint: true },
474
503
  }, logged('gyazo_recent', async (args) => {
@@ -506,7 +535,7 @@ function createMcpServer() {
506
535
  'recent pages of captures. It may be older than that, or belong to another ' +
507
536
  'account. Ask for a window in minutes instead, or raise max_pages.');
508
537
  }
509
- return asMetadataListResult(result.images);
538
+ return asMetadataListResult(await withLocations(result.images, args.include_location));
510
539
  }));
511
540
  server.registerTool('gyazo_collections', {
512
541
  title: 'Find a Gyazo collection by name',
@@ -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
+ }
@@ -13,7 +13,7 @@ Adopt and document the existing top-level command structure.
13
13
 
14
14
  ### 1. Program Metadata
15
15
  - Binary name: `gyazo`
16
- - Version: `0.4.0`
16
+ - Version: `0.5.1`
17
17
  - Description: `Gyazo Memory CLI for AI Secretary`
18
18
 
19
19
  ### 2. Commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuiseki/gyazocli",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Gyazo Memory CLI for AI Secretary",
5
5
  "repository": {
6
6
  "type": "git",