@yuiseki/gyazocli 0.7.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 +79 -6
- package/dist/index.js +1 -1
- package/dist/storage.js +25 -0
- package/docs/ADR/003-cli-structure.md +5 -1
- package/package.json +1 -1
- package/skills/gyazo/SKILL.md +22 -3
package/dist/commands/sync.js
CHANGED
|
@@ -13,13 +13,52 @@ function registerSyncCommand(program) {
|
|
|
13
13
|
.option('--days <number>', 'number of days to sync (used when --date is omitted)')
|
|
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
|
+
.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')
|
|
16
19
|
.action(async (options) => {
|
|
17
20
|
await (0, credentials_1.ensureAccessToken)();
|
|
18
21
|
if (options.date && options.days) {
|
|
19
22
|
console.error('Error: --date and --days cannot be used together.');
|
|
20
23
|
process.exit(1);
|
|
21
24
|
}
|
|
25
|
+
if (options.query && (options.date || options.days)) {
|
|
26
|
+
// Search results are not ordered the same way for every query: a plain
|
|
27
|
+
// query comes back newest first, while a `date:` one starts at the
|
|
28
|
+
// beginning of its range. Nothing here can bound a walk by date
|
|
29
|
+
// safely, and the query language can: put the range in the query.
|
|
30
|
+
console.error('Error: --query cannot be used with --date or --days.');
|
|
31
|
+
console.error('Hint: bound the range inside the query, as');
|
|
32
|
+
console.error(' --query "has:exif date:2026-08"');
|
|
33
|
+
console.error(' --query "has:exif since:2026-08-01 until:2026-08-31"');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
if (options.continue && !options.query) {
|
|
37
|
+
console.error('Error: --continue needs --query, because it resumes a query.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
22
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
|
+
}
|
|
23
62
|
let startDate;
|
|
24
63
|
let endDate;
|
|
25
64
|
if (options.date) {
|
|
@@ -37,23 +76,36 @@ function registerSyncCommand(program) {
|
|
|
37
76
|
startDate.setDate(startDate.getDate() - days - 1);
|
|
38
77
|
startDate.setHours(0, 0, 0, 0);
|
|
39
78
|
}
|
|
40
|
-
|
|
79
|
+
if (query) {
|
|
80
|
+
console.log(`Syncing images matching ${JSON.stringify(query)}...`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(`Syncing images between ${startDate.toISOString()} and ${endDate.toISOString()}...`);
|
|
84
|
+
}
|
|
41
85
|
const hourlyIndices = new Map();
|
|
86
|
+
let oldestSeen = null;
|
|
42
87
|
for (let page = 1; page <= maxPages; page++) {
|
|
43
|
-
const images =
|
|
88
|
+
const images = query
|
|
89
|
+
? await (0, api_1.searchImages)(query, page, 100)
|
|
90
|
+
: await (0, api_1.listImages)(page, 100);
|
|
44
91
|
if (images.length === 0)
|
|
45
92
|
break;
|
|
46
93
|
let reachedLimit = false;
|
|
47
94
|
for (const img of images) {
|
|
48
95
|
const createdAt = new Date(img.created_at);
|
|
49
|
-
|
|
96
|
+
// A query says for itself what it covers, and the results are not
|
|
97
|
+
// ordered predictably enough to stop early on a date.
|
|
98
|
+
if (!query && createdAt > endDate) {
|
|
50
99
|
// Skip images newer than target range.
|
|
51
100
|
continue;
|
|
52
101
|
}
|
|
53
|
-
if (createdAt < startDate) {
|
|
102
|
+
if (!query && createdAt < startDate) {
|
|
54
103
|
reachedLimit = true;
|
|
55
104
|
break;
|
|
56
105
|
}
|
|
106
|
+
if (!oldestSeen || createdAt < oldestSeen) {
|
|
107
|
+
oldestSeen = createdAt;
|
|
108
|
+
}
|
|
57
109
|
// Add to hourly index
|
|
58
110
|
const y = createdAt.getFullYear().toString();
|
|
59
111
|
const m = (createdAt.getMonth() + 1).toString().padStart(2, '0');
|
|
@@ -63,8 +115,13 @@ function registerSyncCommand(program) {
|
|
|
63
115
|
if (!hourlyIndices.has(key))
|
|
64
116
|
hourlyIndices.set(key, new Set());
|
|
65
117
|
hourlyIndices.get(key)?.add(img.image_id);
|
|
66
|
-
|
|
67
|
-
|
|
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) {
|
|
68
125
|
process.stdout.write(`s`);
|
|
69
126
|
continue;
|
|
70
127
|
}
|
|
@@ -81,6 +138,12 @@ function registerSyncCommand(program) {
|
|
|
81
138
|
console.log(`\nPage ${page} processed.`);
|
|
82
139
|
if (reachedLimit)
|
|
83
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
|
+
}
|
|
84
147
|
}
|
|
85
148
|
// Save hourly indices
|
|
86
149
|
console.log(`Updating hourly indices...`);
|
|
@@ -90,6 +153,16 @@ function registerSyncCommand(program) {
|
|
|
90
153
|
const merged = Array.from(new Set([...existing, ...ids]));
|
|
91
154
|
(0, storage_1.saveHourlyCache)(y, m, d, h, merged);
|
|
92
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
|
+
}
|
|
93
166
|
console.log(`Sync complete.`);
|
|
94
167
|
});
|
|
95
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
|
|
@@ -118,6 +118,10 @@ Adopt and document the existing top-level command structure.
|
|
|
118
118
|
- `--days <number>` (default: `1`, used when `--date` is omitted)
|
|
119
119
|
- `--date <yyyy|yyyy-mm|yyyy-mm-dd>`
|
|
120
120
|
- `--max-pages <number>` (default: `10`)
|
|
121
|
+
- `--query <query>` (fill from a search instead of the listing; not with
|
|
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`)
|
|
121
125
|
- `gyazo import <type> <dir>`
|
|
122
126
|
- Supported types: `json`, `hourly`
|
|
123
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
|
|
@@ -97,12 +98,29 @@ endpoint returns the first 100 and cannot page.
|
|
|
97
98
|
```bash
|
|
98
99
|
gyazo sync --days 7 # yesterday back through 7 days
|
|
99
100
|
gyazo sync --date 2026-08 # a whole month
|
|
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
|
|
100
103
|
```
|
|
101
104
|
|
|
102
105
|
`sync` covers yesterday backwards and never today, because today is still
|
|
103
106
|
happening. For anything from today use `ls --today`, `search`, or a ranking
|
|
104
107
|
command with `--today`.
|
|
105
108
|
|
|
109
|
+
`--query` fills the cache from a search instead of the listing, which is how to
|
|
110
|
+
gather one kind of capture without walking past everything else: photographs
|
|
111
|
+
are a small fraction of a day's screenshots. Put any date range inside the
|
|
112
|
+
query (`date:2026-08`, `since:... until:...`) rather than in `--date`, which
|
|
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.
|
|
123
|
+
|
|
106
124
|
## Answering questions with captures
|
|
107
125
|
|
|
108
126
|
- A day is a local day. `--date 2026-08-30` means that date in this machine's
|
|
@@ -111,9 +129,10 @@ command with `--today`.
|
|
|
111
129
|
work, and `ls --date` over a wide range walks many pages.
|
|
112
130
|
- OCR text is noisy: it comes from screenshots at whatever resolution, and
|
|
113
131
|
`locale` is often `und`. Treat it as a hint, not a transcript.
|
|
114
|
-
- `get --objects` prints detected
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
117
136
|
- **Do not turn a capture into a claim it does not support.** A product page or
|
|
118
137
|
a cart is interest; an order confirmation or a payment receipt is a purchase.
|
|
119
138
|
Say which capture the conclusion rests on.
|