@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 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). 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`.
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.4.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 (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.',
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',
@@ -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.0`
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.0",
4
4
  "description": "Gyazo Memory CLI for AI Secretary",
5
5
  "repository": {
6
6
  "type": "git",