@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.
@@ -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 (options.query) {
53
- console.log(`Syncing images matching ${JSON.stringify(options.query)}...`);
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 = options.query
61
- ? await (0, api_1.searchImages)(options.query, page, 100)
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 (!options.query && createdAt > endDate) {
98
+ if (!query && createdAt > endDate) {
71
99
  // Skip images newer than target range.
72
100
  continue;
73
101
  }
74
- if (!options.query && createdAt < startDate) {
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
- const cached = (0, storage_1.loadImageCache)(img.image_id);
88
- if (cached && cached.ocr) {
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.8.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.8.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuiseki/gyazocli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Gyazo Memory CLI for AI Secretary",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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. Budget about 40 seconds per page of 100 captures that are
112
- not cached yet.
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 objects, but the API no longer returns the
123
- field it reads, so it exits non-zero with "Object annotations not found" on
124
- every capture tested. Use the OCR text instead.
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.