@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 +2 -1
- package/dist/index.js +1 -1
- package/dist/mcp.js +69 -26
- package/docs/ADR/003-cli-structure.md +1 -1
- package/docs/ADR/004-mcp-server.md +7 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
54
|
-
...(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 !==
|
|
57
|
-
...(
|
|
58
|
-
...(
|
|
59
|
-
...(image.
|
|
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() {
|
|
@@ -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`, `
|
|
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`
|