@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.
@@ -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
- console.log(`Syncing images between ${startDate.toISOString()} and ${endDate.toISOString()}...`);
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 = await (0, api_1.listImages)(page, 100);
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
- if (createdAt > endDate) {
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
- const cached = (0, storage_1.loadImageCache)(img.image_id);
67
- 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) {
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.7.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.7.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuiseki/gyazocli",
3
- "version": "0.7.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
@@ -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 objects, but the API no longer returns the
115
- field it reads, so it exits non-zero with "Object annotations not found" on
116
- 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.
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.