@yuiseki/gyazocli 0.2.0 → 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 CHANGED
@@ -135,7 +135,8 @@ Configured in a client:
135
135
  `id_or_url` (required) and `sort` (`added`, `created` or `captured`).
136
136
 
137
137
  All of them are read-only, and all of them return metadata rather than image
138
- bytes: URLs, timestamp, OCR text, title, source application and page, and
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
139
140
  location when the capture carries one. Use the URLs in a result to show the
140
141
  capture itself. Handing base64 image data to a model turned out not to work
141
142
  well in practice, and describing a capture does.
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.2.0');
29
+ .version('0.3.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
@@ -35,31 +35,74 @@ function serverVersion() {
35
35
  // level below it, so this holds both in the repository and once installed.
36
36
  return require('../package.json').version;
37
37
  }
38
+ /** null and undefined both mean the capture does not carry the field. */
39
+ function present(value) {
40
+ return value !== null && value !== undefined;
41
+ }
38
42
  /**
39
- * The fields worth handing to a model: enough to cite a capture and to open it,
40
- * without the parts of the API response it cannot act on. Absent fields stay
41
- * absent rather than becoming null.
42
- *
43
- * Metadata only, on purpose. Handing image bytes to a model as base64 was the
44
- * ambitious part of the upstream design and it did not hold up in use, so a
45
- * capture is described here and its URLs are given for anything that wants the
46
- * pixels.
43
+ * Where a capture was taken. This lives under `metadata`, and the top-level
44
+ * `exif_normalized` is null in every response this CLI reads, which is why
45
+ * coordinates were missing from all of the tool output until now. The
46
+ * top-level shape is still read, in case an endpoint starts filling it.
47
+ */
48
+ function readLocation(image) {
49
+ const source = image?.metadata?.exif_normalized ?? image?.exif_normalized;
50
+ const latitude = source?.latitude;
51
+ const longitude = source?.longitude;
52
+ if (typeof latitude !== 'number' || typeof longitude !== 'number') {
53
+ return undefined;
54
+ }
55
+ return { latitude, longitude };
56
+ }
57
+ /**
58
+ * The OCR text, from wherever this response carries it. Same mistake as the
59
+ * coordinates: the responses that have OCR keep it under `metadata`, and the
60
+ * top-level field comes back null.
47
61
  */
62
+ function readOcr(image) {
63
+ const ocr = present(image?.ocr) ? image.ocr : image?.metadata?.ocr;
64
+ return present(ocr) && present(ocr.description) ? ocr : undefined;
65
+ }
48
66
  function toMetadata(image) {
67
+ const location = readLocation(image);
68
+ const ocr = readOcr(image);
49
69
  return {
50
70
  image_id: image.image_id,
51
71
  permalink_url: image.permalink_url,
52
72
  url: image.url,
53
- ...(image.thumb_url !== undefined ? { thumb_url: image.thumb_url } : {}),
54
- ...(image.type !== undefined ? { mimeType: `image/${image.type}` } : {}),
73
+ ...(present(image.thumb_url) ? { thumb_url: image.thumb_url } : {}),
74
+ ...(present(image.type) ? { mimeType: `image/${image.type}` } : {}),
55
75
  created_at: image.created_at,
56
- ...(image.alt_text !== undefined ? { alt_text: image.alt_text } : {}),
57
- ...(image.ocr !== undefined ? { ocr: image.ocr } : {}),
58
- ...(image.metadata !== undefined ? { metadata: image.metadata } : {}),
59
- ...(image.exif_normalized !== undefined ? { exif_normalized: image.exif_normalized } : {}),
76
+ ...(present(image.alt_text) && image.alt_text !== '' ? { alt_text: image.alt_text } : {}),
77
+ ...(ocr !== undefined ? { ocr } : {}),
78
+ ...(location !== undefined ? { location } : {}),
79
+ ...(present(image.metadata) ? { metadata: image.metadata } : {}),
60
80
  };
61
81
  }
62
82
  const NO_IMAGES = { content: [{ type: 'text', text: 'No images found' }] };
83
+ /**
84
+ * Every call, with how long it took, on stderr. stdout belongs to the
85
+ * protocol, and the host that starts this server is where its stderr ends up,
86
+ * which is the only place an operator can see that one tool is slow.
87
+ */
88
+ function logged(name, handler) {
89
+ return async (args) => {
90
+ const startedAt = Date.now();
91
+ const given = Object.entries((args || {}))
92
+ .filter(([, value]) => value !== undefined && value !== false)
93
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
94
+ .join(' ');
95
+ try {
96
+ const result = await handler(args);
97
+ console.error(`[gyazo-mcp] ${name} ok ${Date.now() - startedAt}ms ${given}`.trimEnd());
98
+ return result;
99
+ }
100
+ catch (error) {
101
+ console.error(`[gyazo-mcp] ${name} failed ${Date.now() - startedAt}ms ${given}`.trimEnd(), `- ${error?.message || error}`);
102
+ throw error;
103
+ }
104
+ };
105
+ }
63
106
  function asJsonResult(payload) {
64
107
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
65
108
  }
@@ -108,7 +151,7 @@ function createMcpServer() {
108
151
  .describe('Number of results per page (max: 100)'),
109
152
  },
110
153
  annotations: { readOnlyHint: true, openWorldHint: true },
111
- }, async ({ query, page, per }) => {
154
+ }, logged('gyazo_search', async ({ query, page, per }) => {
112
155
  const images = await (0, api_1.searchImages)(query, page, per);
113
156
  if (!images || images.length === 0) {
114
157
  return NO_IMAGES;
@@ -121,7 +164,7 @@ function createMcpServer() {
121
164
  },
122
165
  ],
123
166
  };
124
- });
167
+ }));
125
168
  server.registerTool('gyazo_image', {
126
169
  title: 'Describe one Gyazo capture',
127
170
  description: 'Fetch the metadata of one capture on Gyazo: its URLs, timestamp, OCR text, ' +
@@ -135,7 +178,7 @@ function createMcpServer() {
135
178
  'https://gyazo.com/<id> permalink, or a direct image URL all work.'),
136
179
  },
137
180
  annotations: { readOnlyHint: true, openWorldHint: true },
138
- }, async ({ id_or_url }) => {
181
+ }, logged('gyazo_image', async ({ id_or_url }) => {
139
182
  const imageId = (0, ids_1.normalizeImageId)(id_or_url);
140
183
  if (!imageId) {
141
184
  throw new Error(`'${id_or_url}' is not a Gyazo image ID or URL. Pass a 32-character ID or a ` +
@@ -147,7 +190,7 @@ function createMcpServer() {
147
190
  return NO_IMAGES;
148
191
  }
149
192
  return asMetadataResult(image);
150
- });
193
+ }));
151
194
  server.registerTool('gyazo_latest_image', {
152
195
  title: 'Describe the most recent Gyazo capture',
153
196
  description: 'Fetch the metadata of the capture the user uploaded most recently. Useful when ' +
@@ -157,14 +200,14 @@ function createMcpServer() {
157
200
  // are dropped rather than refused.
158
201
  inputSchema: {},
159
202
  annotations: { readOnlyHint: true, openWorldHint: true },
160
- }, async () => {
203
+ }, logged('gyazo_latest_image', async () => {
161
204
  const images = await (0, api_1.listImages)(1, 1);
162
205
  const latest = images && images[0];
163
206
  if (!latest) {
164
207
  return NO_IMAGES;
165
208
  }
166
209
  return asMetadataResult(latest);
167
- });
210
+ }));
168
211
  server.registerTool('gyazo_list', {
169
212
  title: 'List Gyazo captures',
170
213
  description: 'List the captures the user uploaded, newest first, taking the same options as ' +
@@ -209,7 +252,7 @@ function createMcpServer() {
209
252
  .describe('Answer from the local cache where possible. Set false to force a fetch'),
210
253
  },
211
254
  annotations: { readOnlyHint: true, openWorldHint: true },
212
- }, async (args) => {
255
+ }, logged('gyazo_list', async (args) => {
213
256
  const { page, limit, today, photos, uploaded, max_pages: maxPages, use_cache: useCache } = args;
214
257
  if (photos && uploaded) {
215
258
  throw new Error('photos and uploaded cannot be used together.');
@@ -236,7 +279,7 @@ function createMcpServer() {
236
279
  alias,
237
280
  });
238
281
  return asMetadataListResult(images);
239
- });
282
+ }));
240
283
  server.registerTool('gyazo_summary', {
241
284
  title: 'Summarise a stretch of Gyazo captures',
242
285
  description: 'What a day or a range adds up to: how many captures each day, and which ' +
@@ -267,12 +310,12 @@ function createMcpServer() {
267
310
  .describe('Answer from the local cache where possible. Set false to force a fetch'),
268
311
  },
269
312
  annotations: { readOnlyHint: true, openWorldHint: true },
270
- }, async (args) => {
313
+ }, logged('gyazo_summary', async (args) => {
271
314
  const { today, limit, max_pages: maxPages, use_cache: useCache } = args;
272
315
  const targetDate = requireDate(args.date, today) || (0, dates_1.buildRecentWeekRangeUntilYesterday)();
273
316
  const dailySummaries = await (0, analytics_1.buildSummary)({ targetDate, maxPages, useCache });
274
317
  return asJsonResult((0, analytics_1.toSummaryJson)(targetDate.dateKey, dailySummaries, limit));
275
- });
318
+ }));
276
319
  server.registerTool('gyazo_collection', {
277
320
  title: 'Read a Gyazo collection',
278
321
  description: 'The metadata of a collection and of the captures in it. A collection ID looks ' +
@@ -290,7 +333,7 @@ function createMcpServer() {
290
333
  'captured (when the photo was taken)'),
291
334
  },
292
335
  annotations: { readOnlyHint: true, openWorldHint: true },
293
- }, async ({ id_or_url, sort }) => {
336
+ }, logged('gyazo_collection', async ({ id_or_url, sort }) => {
294
337
  const collectionId = (0, ids_1.normalizeCollectionId)(id_or_url);
295
338
  if (!collectionId) {
296
339
  throw new Error(`'${id_or_url}' is not a Gyazo collection ID or URL. Pass a 32-character ID or a ` +
@@ -311,7 +354,7 @@ function createMcpServer() {
311
354
  ...(collection?.user !== undefined ? { user: collection.user } : {}),
312
355
  images: images.map(toMetadata),
313
356
  });
314
- });
357
+ }));
315
358
  return server;
316
359
  }
317
360
  async function runMcpServer() {
@@ -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.2.0`
16
+ - Version: `0.3.0`
17
17
  - Description: `Gyazo Memory CLI for AI Secretary`
18
18
 
19
19
  ### 2. Commands
@@ -74,7 +74,13 @@ exiting ones are now thin wrappers over them.
74
74
 
75
75
  - The result payload is the fields a model can act on: `image_id`,
76
76
  `permalink_url`, `url`, `thumb_url`, `mimeType`, `created_at`, `alt_text`,
77
- `ocr`, `metadata`, `exif_normalized`. Absent fields stay absent.
77
+ `ocr`, `location`, `metadata`. Absent fields stay absent, and a null field
78
+ counts as absent.
79
+ - `location` and `ocr` are read from under `metadata`, which is where Gyazo
80
+ puts them. The top-level `exif_normalized` and `ocr` are null in every
81
+ response this CLI receives, and reading those was why coordinates never
82
+ appeared in any payload. Fixtures written from the shape the code expected
83
+ hid it; they now come from real responses.
78
84
  - No `uri` field, unlike upstream: it points at an MCP resource, and this
79
85
  server does not serve resources yet.
80
86
  - `gyazo_latest_image` takes no arguments, while upstream declared a `name`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuiseki/gyazocli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Gyazo Memory CLI for AI Secretary",
5
5
  "repository": {
6
6
  "type": "git",