@yuiseki/gyazocli 0.8.0 → 0.9.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/dist/commands/sync.js +60 -8
- package/dist/index.js +1 -1
- package/dist/storage.js +25 -0
- package/docs/ADR/003-cli-structure.md +3 -1
- package/package.json +1 -1
- package/skills/gyazo/SKILL.md +16 -5
package/dist/commands/sync.js
CHANGED
|
@@ -14,6 +14,8 @@ function registerSyncCommand(program) {
|
|
|
14
14
|
.option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'sync only this date/month/year range')
|
|
15
15
|
.option('--max-pages <number>', 'max pages to fetch', '10')
|
|
16
16
|
.option('--query <query>', 'fill the cache from a search instead of the listing')
|
|
17
|
+
.option('--refresh', 'fetch every capture again, even one already cached')
|
|
18
|
+
.option('--continue', 'resume the last walk of this query instead of starting at the top')
|
|
17
19
|
.action(async (options) => {
|
|
18
20
|
await (0, credentials_1.ensureAccessToken)();
|
|
19
21
|
if (options.date && options.days) {
|
|
@@ -31,7 +33,32 @@ function registerSyncCommand(program) {
|
|
|
31
33
|
console.error(' --query "has:exif since:2026-08-01 until:2026-08-31"');
|
|
32
34
|
process.exit(1);
|
|
33
35
|
}
|
|
36
|
+
if (options.continue && !options.query) {
|
|
37
|
+
console.error('Error: --continue needs --query, because it resumes a query.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
34
40
|
const maxPages = (0, options_1.parsePositiveIntegerOption)(options.maxPages, '--max-pages');
|
|
41
|
+
/** The day part of an instant, in local time, as the operators want it. */
|
|
42
|
+
const dayOf = (date) => {
|
|
43
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
44
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
45
|
+
};
|
|
46
|
+
let query = options.query;
|
|
47
|
+
if (options.continue) {
|
|
48
|
+
if (/\b(date|since|until):/i.test(query)) {
|
|
49
|
+
console.error('Error: --continue cannot resume a query that bounds its own dates.');
|
|
50
|
+
console.error('Hint: drop date:, since: and until: from --query, or drop --continue.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const state = (0, storage_1.loadSyncState)(query);
|
|
54
|
+
if (state) {
|
|
55
|
+
query = `${query} until:${state.oldestDay}`;
|
|
56
|
+
console.log(`Resuming from ${state.oldestDay} (walked ${state.updatedAt}).`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log('Nothing walked for this query yet; starting at the top.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
35
62
|
let startDate;
|
|
36
63
|
let endDate;
|
|
37
64
|
if (options.date) {
|
|
@@ -49,16 +76,17 @@ function registerSyncCommand(program) {
|
|
|
49
76
|
startDate.setDate(startDate.getDate() - days - 1);
|
|
50
77
|
startDate.setHours(0, 0, 0, 0);
|
|
51
78
|
}
|
|
52
|
-
if (
|
|
53
|
-
console.log(`Syncing images matching ${JSON.stringify(
|
|
79
|
+
if (query) {
|
|
80
|
+
console.log(`Syncing images matching ${JSON.stringify(query)}...`);
|
|
54
81
|
}
|
|
55
82
|
else {
|
|
56
83
|
console.log(`Syncing images between ${startDate.toISOString()} and ${endDate.toISOString()}...`);
|
|
57
84
|
}
|
|
58
85
|
const hourlyIndices = new Map();
|
|
86
|
+
let oldestSeen = null;
|
|
59
87
|
for (let page = 1; page <= maxPages; page++) {
|
|
60
|
-
const images =
|
|
61
|
-
? await (0, api_1.searchImages)(
|
|
88
|
+
const images = query
|
|
89
|
+
? await (0, api_1.searchImages)(query, page, 100)
|
|
62
90
|
: await (0, api_1.listImages)(page, 100);
|
|
63
91
|
if (images.length === 0)
|
|
64
92
|
break;
|
|
@@ -67,14 +95,17 @@ function registerSyncCommand(program) {
|
|
|
67
95
|
const createdAt = new Date(img.created_at);
|
|
68
96
|
// A query says for itself what it covers, and the results are not
|
|
69
97
|
// ordered predictably enough to stop early on a date.
|
|
70
|
-
if (!
|
|
98
|
+
if (!query && createdAt > endDate) {
|
|
71
99
|
// Skip images newer than target range.
|
|
72
100
|
continue;
|
|
73
101
|
}
|
|
74
|
-
if (!
|
|
102
|
+
if (!query && createdAt < startDate) {
|
|
75
103
|
reachedLimit = true;
|
|
76
104
|
break;
|
|
77
105
|
}
|
|
106
|
+
if (!oldestSeen || createdAt < oldestSeen) {
|
|
107
|
+
oldestSeen = createdAt;
|
|
108
|
+
}
|
|
78
109
|
// Add to hourly index
|
|
79
110
|
const y = createdAt.getFullYear().toString();
|
|
80
111
|
const m = (createdAt.getMonth() + 1).toString().padStart(2, '0');
|
|
@@ -84,8 +115,13 @@ function registerSyncCommand(program) {
|
|
|
84
115
|
if (!hourlyIndices.has(key))
|
|
85
116
|
hourlyIndices.set(key, new Set());
|
|
86
117
|
hourlyIndices.get(key)?.add(img.image_id);
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
// Already fetched is already fetched. This used to test `cached.ocr`,
|
|
119
|
+
// which is null in every response the API returns -- the OCR text
|
|
120
|
+
// lives under `metadata` -- so the check never fired and every sync
|
|
121
|
+
// re-fetched everything it had. On an API with undocumented rate
|
|
122
|
+
// limits that is the expensive kind of mistake.
|
|
123
|
+
const cached = options.refresh ? null : (0, storage_1.loadImageCache)(img.image_id);
|
|
124
|
+
if (cached) {
|
|
89
125
|
process.stdout.write(`s`);
|
|
90
126
|
continue;
|
|
91
127
|
}
|
|
@@ -102,6 +138,12 @@ function registerSyncCommand(program) {
|
|
|
102
138
|
console.log(`\nPage ${page} processed.`);
|
|
103
139
|
if (reachedLimit)
|
|
104
140
|
break;
|
|
141
|
+
// A breath between pages. The rate limits here are real and
|
|
142
|
+
// undocumented, and a walk of a hundred pages is exactly the shape
|
|
143
|
+
// that finds them.
|
|
144
|
+
if (page < maxPages) {
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
146
|
+
}
|
|
105
147
|
}
|
|
106
148
|
// Save hourly indices
|
|
107
149
|
console.log(`Updating hourly indices...`);
|
|
@@ -111,6 +153,16 @@ function registerSyncCommand(program) {
|
|
|
111
153
|
const merged = Array.from(new Set([...existing, ...ids]));
|
|
112
154
|
(0, storage_1.saveHourlyCache)(y, m, d, h, merged);
|
|
113
155
|
}
|
|
156
|
+
// Remember how far back this query got, so a later --continue can pick
|
|
157
|
+
// up there rather than walking the same pages again. Only for a query
|
|
158
|
+
// the caller did not bound itself: a bounded one says what it covers.
|
|
159
|
+
if (options.query && oldestSeen && !/\b(date|since|until):/i.test(options.query)) {
|
|
160
|
+
(0, storage_1.saveSyncState)({
|
|
161
|
+
query: options.query,
|
|
162
|
+
oldestDay: dayOf(oldestSeen),
|
|
163
|
+
updatedAt: new Date().toISOString(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
114
166
|
console.log(`Sync complete.`);
|
|
115
167
|
});
|
|
116
168
|
}
|
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.9.0');
|
|
30
30
|
(0, config_1.registerConfigCommand)(program);
|
|
31
31
|
(0, list_1.registerListCommand)(program);
|
|
32
32
|
(0, get_1.registerGetCommand)(program);
|
package/dist/storage.js
CHANGED
|
@@ -15,8 +15,11 @@ exports.saveHourlyCache = saveHourlyCache;
|
|
|
15
15
|
exports.loadHourlyCache = loadHourlyCache;
|
|
16
16
|
exports.saveHourlyMetadataCache = saveHourlyMetadataCache;
|
|
17
17
|
exports.loadHourlyMetadataCache = loadHourlyMetadataCache;
|
|
18
|
+
exports.loadSyncState = loadSyncState;
|
|
19
|
+
exports.saveSyncState = saveSyncState;
|
|
18
20
|
const fs_1 = __importDefault(require("fs"));
|
|
19
21
|
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
20
23
|
const os_1 = __importDefault(require("os"));
|
|
21
24
|
function getCacheDir() {
|
|
22
25
|
if (process.env.GYAZO_CACHE_DIR) {
|
|
@@ -105,3 +108,25 @@ function loadHourlyMetadataCache(kind, year, month, day, hour) {
|
|
|
105
108
|
}
|
|
106
109
|
return null;
|
|
107
110
|
}
|
|
111
|
+
function getSyncStatePath(query) {
|
|
112
|
+
const dir = path_1.default.join(getCacheDir(), 'sync');
|
|
113
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
114
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
const key = crypto_1.default.createHash('sha1').update(query).digest('hex');
|
|
117
|
+
return path_1.default.join(dir, `${key}.json`);
|
|
118
|
+
}
|
|
119
|
+
function loadSyncState(query) {
|
|
120
|
+
const file = getSyncStatePath(query);
|
|
121
|
+
if (!fs_1.default.existsSync(file))
|
|
122
|
+
return null;
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function saveSyncState(state) {
|
|
131
|
+
fs_1.default.writeFileSync(getSyncStatePath(state.query), JSON.stringify(state, null, 2));
|
|
132
|
+
}
|
|
@@ -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.
|
|
16
|
+
- Version: `0.9.0`
|
|
17
17
|
- Description: `Gyazo Memory CLI for AI Secretary`
|
|
18
18
|
|
|
19
19
|
### 2. Commands
|
|
@@ -120,6 +120,8 @@ Adopt and document the existing top-level command structure.
|
|
|
120
120
|
- `--max-pages <number>` (default: `10`)
|
|
121
121
|
- `--query <query>` (fill from a search instead of the listing; not with
|
|
122
122
|
`--date` or `--days`, because the range belongs inside the query)
|
|
123
|
+
- `--refresh` (fetch every capture again, even one already cached)
|
|
124
|
+
- `--continue` (resume the last walk of this query, with `--query`)
|
|
123
125
|
- `gyazo import <type> <dir>`
|
|
124
126
|
- Supported types: `json`, `hourly`
|
|
125
127
|
|
package/package.json
CHANGED
package/skills/gyazo/SKILL.md
CHANGED
|
@@ -25,6 +25,7 @@ gyazo ls --hour 2026-08-30-14 # one hour, from the cache only
|
|
|
25
25
|
gyazo ls --photos # shorthand for has:location
|
|
26
26
|
gyazo get <image_id> # one capture in detail
|
|
27
27
|
gyazo get <image_id> --ocr # just the OCR text
|
|
28
|
+
gyazo get <image_id> --objects # just what was detected in it
|
|
28
29
|
gyazo <image_id> # same as get
|
|
29
30
|
gyazo <https://gyazo.com/...> # same as get
|
|
30
31
|
gyazo ./screenshot.png # an existing file uploads instead
|
|
@@ -98,6 +99,7 @@ endpoint returns the first 100 and cannot page.
|
|
|
98
99
|
gyazo sync --days 7 # yesterday back through 7 days
|
|
99
100
|
gyazo sync --date 2026-08 # a whole month
|
|
100
101
|
gyazo sync --query "has:exif OR has:location" --max-pages 20
|
|
102
|
+
gyazo sync --query "has:exif" --max-pages 20 --continue # carry on from last time
|
|
101
103
|
```
|
|
102
104
|
|
|
103
105
|
`sync` covers yesterday backwards and never today, because today is still
|
|
@@ -108,8 +110,16 @@ command with `--today`.
|
|
|
108
110
|
gather one kind of capture without walking past everything else: photographs
|
|
109
111
|
are a small fraction of a day's screenshots. Put any date range inside the
|
|
110
112
|
query (`date:2026-08`, `since:... until:...`) rather than in `--date`, which
|
|
111
|
-
`--query` refuses.
|
|
112
|
-
|
|
113
|
+
`--query` refuses.
|
|
114
|
+
|
|
115
|
+
A capture already in the cache is not fetched again, so a repeated sync costs
|
|
116
|
+
only the search pages: 100 cached captures take under a second, 100 new ones
|
|
117
|
+
about 40 seconds. `--continue` remembers how far back the last walk of that
|
|
118
|
+
query reached and resumes with `until:<that day>`, which is how to backfill a
|
|
119
|
+
long history a few hundred pages at a time without asking for the same pages
|
|
120
|
+
twice. Gyazo rate-limits without documenting it, so prefer resuming over
|
|
121
|
+
re-walking, and leave `--refresh` alone unless a capture really needs
|
|
122
|
+
re-fetching.
|
|
113
123
|
|
|
114
124
|
## Answering questions with captures
|
|
115
125
|
|
|
@@ -119,9 +129,10 @@ not cached yet.
|
|
|
119
129
|
work, and `ls --date` over a wide range walks many pages.
|
|
120
130
|
- OCR text is noisy: it comes from screenshots at whatever resolution, and
|
|
121
131
|
`locale` is often `und`. Treat it as a hint, not a transcript.
|
|
122
|
-
- `get --objects` prints detected
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
- `get --objects` prints what was detected in the image, with a confidence.
|
|
133
|
+
About 59% of captures carry annotations; the rest exit non-zero with "Object
|
|
134
|
+
annotations not found", which means this capture has none, not that the
|
|
135
|
+
command is broken. `--ocr` and `--objects` cannot be combined.
|
|
125
136
|
- **Do not turn a capture into a claim it does not support.** A product page or
|
|
126
137
|
a cart is interest; an order confirmation or a payment receipt is a purchase.
|
|
127
138
|
Say which capture the conclusion rests on.
|