@yuiseki/gyazocli 0.0.2 → 0.1.1

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 CHANGED
@@ -16,17 +16,37 @@ gyazo sync --days 10
16
16
  gyazo --help
17
17
  ```
18
18
 
19
+ ### Shorthands
20
+
21
+ The first argument may stand on its own when it is unambiguous. A Gyazo
22
+ image ID is 32 hex characters, so it can never be mistaken for a path.
23
+
24
+ ```bash
25
+ gyazo path/to/image.png # same as: gyazo upload path/to/image.png
26
+ gyazo 49a008e2f254f513063b6ec4d3082940 # same as: gyazo get 49a008e2f254f513063b6ec4d3082940
27
+ gyazo https://gyazo.com/49a008e2f254f513063b6ec4d3082940 # same as above
28
+ gyazo https://gyazo.com/collections/21ca16a1023c667a7a437be561a65018 # same as: gyazo collection <id>
29
+ ```
30
+
31
+ A collection ID is 32 hex characters just like an image ID, so a bare ID is read
32
+ as an image. Only the `/collections/<id>` URL form is unambiguous. When a command
33
+ is given the wrong kind of ID it points at the other one.
34
+
35
+ Subcommand names always win, so `gyazo search` stays `gyazo search` even
36
+ if a file of that name exists in the working directory.
37
+
19
38
  ### Detail
20
39
 
21
40
  - `gyazo config set token <token>`: Save your access token
22
41
  - `gyazo config get token|me`: Show saved token (masked) or `me` profile info
23
42
  - `gyazo ls` (`gyazo list`): List images (`--date`/`--today`, `--photos`, `--uploaded`, `-H` available; `--photos/--uploaded` can be combined with `--date`/`--today`)
24
43
  - `gyazo search <query>`: Search images
25
- - `gyazo get <image_id>`: Show image details (`--ocr`, `--objects`, `-j` available)
44
+ - `gyazo collection <collection_id|url>` (`col`, `cols`, `collections`): Show a collection and the images in it (`--sort added|created|captured`, `-A`, `-j` available)
45
+ - `gyazo get <image_id|url>`: Show image details (`--ocr`, `--objects`, `-j` available). Accepts a bare image ID, a `https://gyazo.com/<id>` permalink, or a `https://i.gyazo.com/<id>.png` URL
26
46
  - `gyazo apps|domains|tags|locations`: Show rankings
27
47
  - `gyazo summary`: Show day-by-day weekly summary in Markdown (`##`/`###` headings, image count, apps, domains, tags, locations per day)
28
48
  - `gyazo stats`: Show weekly summary
29
- - `gyazo upload [path]`: Upload an image (uses stdin when path is omitted)
49
+ - `gyazo upload [path]`: Upload an image (uses stdin when path is omitted). Prints the permalink URL alone; use `-j` for the full response
30
50
  - `gyazo sync`: Sync cache
31
51
 
32
52
  Date range notes:
@@ -34,7 +54,37 @@ Date range notes:
34
54
  - Use `--today` for today only, or `--date <yyyy|yyyy-mm|yyyy-mm-dd>` for a custom range
35
55
 
36
56
  JSON output:
37
- - `-j, --json` is available for `config get`, `ls`, `get`, `search`, `apps`, `domains`, `tags`, `locations`, and `summary`
57
+ - `-j, --json` is available for `config get`, `ls`, `get`, `search`, `apps`, `domains`, `tags`, `locations`, `summary`, and `upload`
58
+
59
+ ### Anonymous access
60
+
61
+ A Gyazo ID is long enough to act as the key to the image, so public images and
62
+ collections read fine with no token. An access token, on the other hand, allows
63
+ things this CLI does not expose (deleting, for one), so the way to give an agent
64
+ read-only access is to give it no token at all.
65
+
66
+ ```bash
67
+ gyazo collection <id> # uses the token if there is one, otherwise reads anonymously
68
+ gyazo collection <id> --anonymous # ignores the token even when one is configured
69
+ ```
70
+
71
+ Anonymous reads have limits worth knowing:
72
+
73
+ - A collection or image set to `only_me` returns 404, indistinguishable from one
74
+ that does not exist. That is deliberate: it keeps the ID from confirming what
75
+ exists.
76
+ - An image with `metadata_is_public: false` still returns 200, but `metadata` and
77
+ `created_at` come back `null`, so OCR, EXIF and location are gone. The web
78
+ endpoint withholds these even from the owner's token; `gyazo get` uses
79
+ `api.gyazo.com` for that reason.
80
+
81
+ Exit codes:
82
+ - `0` on success, `1` on a usage error or a failed API call
83
+
84
+ Environment variables:
85
+ - `GYAZO_ACCESS_TOKEN`: access token (takes precedence over the saved config)
86
+ - `GYAZO_CACHE_DIR`: cache location
87
+ - `GYAZO_API_ORIGIN` / `GYAZO_UPLOAD_ORIGIN` / `GYAZO_WEB_ORIGIN`: override the endpoints (used by the test suite)
38
88
 
39
89
  ## Development
40
90
 
@@ -45,6 +95,12 @@ npm install
45
95
  npm run build
46
96
  ```
47
97
 
98
+ ### Test
99
+
100
+ ```bash
101
+ npm test
102
+ ```
103
+
48
104
  ### Link local CLI with npm link
49
105
 
50
106
  ```bash
package/dist/api.js CHANGED
@@ -7,18 +7,35 @@ exports.listImages = listImages;
7
7
  exports.getImageDetail = getImageDetail;
8
8
  exports.searchImages = searchImages;
9
9
  exports.getCurrentUser = getCurrentUser;
10
+ exports.getCollection = getCollection;
10
11
  exports.uploadImage = uploadImage;
11
12
  const axios_1 = __importDefault(require("axios"));
12
13
  const form_data_1 = __importDefault(require("form-data"));
13
14
  const config_1 = require("./config");
14
- const API_BASE_URL = 'https://api.gyazo.com/api/images';
15
- const API_SEARCH_URL = 'https://api.gyazo.com/api/search';
16
- const API_USERS_ME_URL = 'https://api.gyazo.com/api/users/me';
17
- const API_UPLOAD_URL = 'https://upload.gyazo.com/api/upload';
18
- async function requestWithRetry(url, params = {}) {
19
- const headers = { Authorization: `Bearer ${config_1.config.GYAZO_ACCESS_TOKEN}` };
15
+ const DEFAULT_API_ORIGIN = 'https://api.gyazo.com';
16
+ const DEFAULT_UPLOAD_ORIGIN = 'https://upload.gyazo.com';
17
+ const DEFAULT_WEB_ORIGIN = 'https://gyazo.com';
18
+ function stripTrailingSlash(origin) {
19
+ return origin.replace(/\/+$/, '');
20
+ }
21
+ function apiOrigin() {
22
+ return stripTrailingSlash(config_1.config.GYAZO_API_ORIGIN || DEFAULT_API_ORIGIN);
23
+ }
24
+ function uploadOrigin() {
25
+ return stripTrailingSlash(config_1.config.GYAZO_UPLOAD_ORIGIN || DEFAULT_UPLOAD_ORIGIN);
26
+ }
27
+ function webOrigin() {
28
+ return stripTrailingSlash(config_1.config.GYAZO_WEB_ORIGIN || DEFAULT_WEB_ORIGIN);
29
+ }
30
+ const apiBaseUrl = () => `${apiOrigin()}/api/images`;
31
+ const apiSearchUrl = () => `${apiOrigin()}/api/search`;
32
+ const apiUsersMeUrl = () => `${apiOrigin()}/api/users/me`;
33
+ const apiUploadUrl = () => `${uploadOrigin()}/api/upload`;
34
+ const webCollectionUrl = (id) => `${webOrigin()}/collections/${id}.json`;
35
+ async function requestWithRetry(url, params = {}, headers) {
36
+ const requestHeaders = headers ?? { Authorization: `Bearer ${config_1.config.GYAZO_ACCESS_TOKEN}` };
20
37
  try {
21
- const response = await axios_1.default.get(url, { headers, params });
38
+ const response = await axios_1.default.get(url, { headers: requestHeaders, params });
22
39
  return response.data;
23
40
  }
24
41
  catch (error) {
@@ -26,22 +43,35 @@ async function requestWithRetry(url, params = {}) {
26
43
  const retryAfter = parseInt(error.response.headers['retry-after'] || '5', 10);
27
44
  console.warn(`Rate limited. Retrying after ${retryAfter} seconds...`);
28
45
  await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
29
- return requestWithRetry(url, params);
46
+ return requestWithRetry(url, params, headers);
30
47
  }
31
48
  throw error;
32
49
  }
33
50
  }
34
51
  async function listImages(page = 1, perPage = 20) {
35
- return requestWithRetry(API_BASE_URL, { page, per_page: perPage });
52
+ return requestWithRetry(apiBaseUrl(), { page, per_page: perPage });
36
53
  }
37
54
  async function getImageDetail(imageId) {
38
- return requestWithRetry(`${API_BASE_URL}/${imageId}`);
55
+ return requestWithRetry(`${apiBaseUrl()}/${imageId}`);
39
56
  }
40
57
  async function searchImages(query, page = 1, perPage = 20) {
41
- return requestWithRetry(API_SEARCH_URL, { query, page, per: perPage });
58
+ return requestWithRetry(apiSearchUrl(), { query, page, per: perPage });
42
59
  }
43
60
  async function getCurrentUser() {
44
- return requestWithRetry(API_USERS_ME_URL);
61
+ return requestWithRetry(apiUsersMeUrl());
62
+ }
63
+ /**
64
+ * Collections are read through the public web endpoint. It returns the
65
+ * collection metadata plus the first 100 images with their full detail in a
66
+ * single request, and it works without a token for public collections, which
67
+ * is what lets an agent run read-only with no credentials at all.
68
+ */
69
+ async function getCollection(collectionId, options = {}) {
70
+ const headers = {};
71
+ if (!options.anonymous && config_1.config.GYAZO_ACCESS_TOKEN) {
72
+ headers.Authorization = `Bearer ${config_1.config.GYAZO_ACCESS_TOKEN}`;
73
+ }
74
+ return requestWithRetry(webCollectionUrl(collectionId), {}, headers);
45
75
  }
46
76
  async function uploadImage(options) {
47
77
  const form = new form_data_1.default();
@@ -60,7 +90,7 @@ async function uploadImage(options) {
60
90
  if (typeof options.timestamp === 'number') {
61
91
  form.append('created_at', String(options.timestamp));
62
92
  }
63
- const response = await axios_1.default.post(API_UPLOAD_URL, form, {
93
+ const response = await axios_1.default.post(apiUploadUrl(), form, {
64
94
  headers: form.getHeaders(),
65
95
  });
66
96
  return response.data;
package/dist/config.js CHANGED
@@ -13,12 +13,18 @@ const configSchema = zod_1.z.object({
13
13
  GYAZO_CLIENT_ID: zod_1.z.string().optional(),
14
14
  GYAZO_CLIENT_SECRET: zod_1.z.string().optional(),
15
15
  GYAZO_CACHE_DIR: zod_1.z.string().optional(),
16
+ GYAZO_API_ORIGIN: zod_1.z.string().optional(),
17
+ GYAZO_UPLOAD_ORIGIN: zod_1.z.string().optional(),
18
+ GYAZO_WEB_ORIGIN: zod_1.z.string().optional(),
16
19
  });
17
20
  exports.config = configSchema.parse({
18
21
  GYAZO_ACCESS_TOKEN: process.env.GYAZO_ACCESS_TOKEN,
19
22
  GYAZO_CLIENT_ID: process.env.GYAZO_CLIENT_ID,
20
23
  GYAZO_CLIENT_SECRET: process.env.GYAZO_CLIENT_SECRET,
21
24
  GYAZO_CACHE_DIR: process.env.GYAZO_CACHE_DIR,
25
+ GYAZO_API_ORIGIN: process.env.GYAZO_API_ORIGIN,
26
+ GYAZO_UPLOAD_ORIGIN: process.env.GYAZO_UPLOAD_ORIGIN,
27
+ GYAZO_WEB_ORIGIN: process.env.GYAZO_WEB_ORIGIN,
22
28
  });
23
29
  function setAccessToken(token) {
24
30
  exports.config.GYAZO_ACCESS_TOKEN = token;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getStoredConfig = getStoredConfig;
7
7
  exports.setStoredConfig = setStoredConfig;
8
+ exports.resolveAccessToken = resolveAccessToken;
8
9
  exports.ensureAccessToken = ensureAccessToken;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const path_1 = __importDefault(require("path"));
@@ -44,7 +45,11 @@ function setStoredConfig(key, value) {
44
45
  fs_1.default.writeFileSync(CREDENTIALS_FILE, JSON.stringify(stored, null, 2));
45
46
  console.error(`Config set: ${key}=${value}`);
46
47
  }
47
- async function ensureAccessToken() {
48
+ /**
49
+ * Load the access token if there is one, without failing when there is not.
50
+ * Read-only commands fall back to anonymous access instead of exiting.
51
+ */
52
+ function resolveAccessToken() {
48
53
  if (config_1.config.GYAZO_ACCESS_TOKEN) {
49
54
  return config_1.config.GYAZO_ACCESS_TOKEN;
50
55
  }
@@ -53,6 +58,13 @@ async function ensureAccessToken() {
53
58
  (0, config_1.setAccessToken)(storedToken);
54
59
  return storedToken;
55
60
  }
61
+ return undefined;
62
+ }
63
+ async function ensureAccessToken() {
64
+ const token = resolveAccessToken();
65
+ if (token) {
66
+ return token;
67
+ }
56
68
  console.error('Error: Gyazo Access Token is not set.');
57
69
  console.error('Please run the following command to set your access token:');
58
70
  console.error(' gyazo config set token <your_access_token>');
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.normalizeImageId = normalizeImageId;
8
+ exports.normalizeCollectionId = normalizeCollectionId;
7
9
  const commander_1 = require("commander");
8
10
  const fs_1 = __importDefault(require("fs"));
9
11
  const path_1 = __importDefault(require("path"));
@@ -13,10 +15,123 @@ const credentials_1 = require("./credentials");
13
15
  const program = new commander_1.Command();
14
16
  const UPLOAD_DESC_TAG = '#gyazocli_uploads';
15
17
  const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
18
+ const IMAGE_ID_PATTERN = /^[0-9a-f]{32}$/i;
19
+ const GYAZO_HOST_PATTERN = /(^|\.)gyazo\.com$/i;
20
+ /**
21
+ * Accept either a bare Gyazo image id (32 hex characters) or any Gyazo URL that
22
+ * carries one, and return the canonical lowercase id. Returns null otherwise.
23
+ */
24
+ function normalizeImageId(input) {
25
+ const trimmed = (input || '').trim();
26
+ if (!trimmed)
27
+ return null;
28
+ if (IMAGE_ID_PATTERN.test(trimmed)) {
29
+ return trimmed.toLowerCase();
30
+ }
31
+ if (!/^https?:\/\//i.test(trimmed)) {
32
+ return null;
33
+ }
34
+ let url;
35
+ try {
36
+ url = new URL(trimmed);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ if (!GYAZO_HOST_PATTERN.test(url.hostname)) {
42
+ return null;
43
+ }
44
+ const segments = url.pathname.split('/').filter(Boolean);
45
+ const lastSegment = segments[segments.length - 1];
46
+ if (!lastSegment)
47
+ return null;
48
+ // /collections/<id> is a collection, not an image.
49
+ if (segments[segments.length - 2] === 'collections')
50
+ return null;
51
+ const withoutExtension = lastSegment.replace(/\.[a-z0-9]+$/i, '');
52
+ return IMAGE_ID_PATTERN.test(withoutExtension) ? withoutExtension.toLowerCase() : null;
53
+ }
54
+ const COLLECTION_SORTS = ['added', 'created', 'captured'];
55
+ /**
56
+ * A collection ID looks exactly like an image ID (32 hex characters), so only
57
+ * the URL form tells the two apart. `/collections/<id>` is a collection;
58
+ * `/<id>` is an image.
59
+ */
60
+ function normalizeCollectionId(input) {
61
+ const trimmed = (input || '').trim();
62
+ if (!trimmed)
63
+ return null;
64
+ if (IMAGE_ID_PATTERN.test(trimmed)) {
65
+ return trimmed.toLowerCase();
66
+ }
67
+ if (!/^https?:\/\//i.test(trimmed)) {
68
+ return null;
69
+ }
70
+ let url;
71
+ try {
72
+ url = new URL(trimmed);
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ if (!GYAZO_HOST_PATTERN.test(url.hostname)) {
78
+ return null;
79
+ }
80
+ const segments = url.pathname.split('/').filter(Boolean);
81
+ if (segments.length < 2 || segments[segments.length - 2] !== 'collections') {
82
+ return null;
83
+ }
84
+ const withoutExtension = segments[segments.length - 1].replace(/\.[a-z0-9]+$/i, '');
85
+ return IMAGE_ID_PATTERN.test(withoutExtension) ? withoutExtension.toLowerCase() : null;
86
+ }
87
+ function requireCollectionId(input) {
88
+ const collectionId = normalizeCollectionId(input);
89
+ if (!collectionId) {
90
+ console.error(`Error: '${input}' is not a Gyazo collection ID or URL.`);
91
+ console.error('Hint: pass a 32-character collection ID or a https://gyazo.com/collections/<id> URL.');
92
+ process.exit(1);
93
+ }
94
+ return collectionId;
95
+ }
96
+ function parseCollectionSort(value) {
97
+ if (value === undefined || value === null)
98
+ return 'added';
99
+ if (COLLECTION_SORTS.includes(String(value))) {
100
+ return String(value);
101
+ }
102
+ console.error(`Error: --sort must be one of ${COLLECTION_SORTS.join(', ')}.`);
103
+ process.exit(1);
104
+ }
105
+ function collectionSortKey(image, sort) {
106
+ if (sort === 'captured') {
107
+ return image?.exif_captured_at || image?.metadata?.exif_normalized?.time || image?.created_at || '';
108
+ }
109
+ return image?.created_at || '';
110
+ }
111
+ function sortCollectionImages(images, sort) {
112
+ if (sort === 'added')
113
+ return images;
114
+ // Newest first, matching how `list` presents images.
115
+ return [...images].sort((a, b) => collectionSortKey(b, sort).localeCompare(collectionSortKey(a, sort)));
116
+ }
117
+ function requireImageId(input) {
118
+ const imageId = normalizeImageId(input);
119
+ if (!imageId) {
120
+ console.error(`Error: '${input}' is not a Gyazo image ID or URL.`);
121
+ if (normalizeCollectionId(input)) {
122
+ console.error(`Hint: that looks like a collection. Try \`gyazo collection ${input}\`.`);
123
+ }
124
+ else {
125
+ console.error('Hint: pass a 32-character image ID or a https://gyazo.com/<id> URL.');
126
+ }
127
+ process.exit(1);
128
+ }
129
+ return imageId;
130
+ }
16
131
  program
17
132
  .name('gyazo')
18
133
  .description('Gyazo Memory CLI for AI Secretary')
19
- .version('0.0.2');
134
+ .version('0.1.1');
20
135
  // Config Command
21
136
  const configCmd = program.command('config').description('Manage configuration');
22
137
  configCmd
@@ -1228,6 +1343,42 @@ async function readStdinBuffer() {
1228
1343
  process.stdin.on('error', reject);
1229
1344
  });
1230
1345
  }
1346
+ function printCollectionMarkdown(collection, images) {
1347
+ const lines = [];
1348
+ lines.push('## Gyazo Collection');
1349
+ lines.push('');
1350
+ const name = normalizeText(collection?.name);
1351
+ if (name)
1352
+ lines.push(`- Name: ${name}`);
1353
+ const collectionId = collection?.id;
1354
+ const url = collection?.url || (collectionId ? `https://gyazo.com/collections/${collectionId}` : undefined);
1355
+ if (url)
1356
+ lines.push(`- URL: <${url}>`);
1357
+ const description = normalizeText(collection?.description);
1358
+ if (description)
1359
+ lines.push(`- Description: ${description}`);
1360
+ const owner = normalizeText(collection?.user?.name);
1361
+ if (owner)
1362
+ lines.push(`- Owner: ${owner}`);
1363
+ const total = collection?.total_image_count;
1364
+ const shown = images.length;
1365
+ const truncated = typeof total === 'number' && total > shown;
1366
+ lines.push(`- Images: ${truncated ? `${shown} of ${total}` : shown}`);
1367
+ const updatedAt = normalizeText(collection?.list_updated_at);
1368
+ if (updatedAt)
1369
+ lines.push(`- Updated at: ${formatCreatedAt(updatedAt)}`);
1370
+ console.log(lines.join('\n'));
1371
+ if (truncated) {
1372
+ console.log('');
1373
+ console.log('Note: this endpoint returns only the first 100 images of a collection.');
1374
+ }
1375
+ if (shown > 0) {
1376
+ console.log('');
1377
+ console.log('### Images');
1378
+ console.log('');
1379
+ printListImages(images);
1380
+ }
1381
+ }
1231
1382
  function printGetMarkdown(image, ocrDescription, objects = []) {
1232
1383
  const lines = [];
1233
1384
  lines.push('## Gyazo Image');
@@ -1267,7 +1418,7 @@ function printGetMarkdown(image, ocrDescription, objects = []) {
1267
1418
  function summarizeImageForList(img) {
1268
1419
  const domain = extractDomain(normalizeText(img.metadata?.url));
1269
1420
  const cleanedTitle = sanitizeSummaryText(img.metadata?.title, domain);
1270
- const cleanedDesc = sanitizeSummaryText(img.metadata?.desc, domain);
1421
+ const cleanedDesc = sanitizeSummaryText(img.metadata?.desc ?? img.desc, domain);
1271
1422
  const locationLabel = sanitizeSummaryText(extractImageLocationLabel(img));
1272
1423
  const cleanedAltText = sanitizeSummaryText(img.alt_text);
1273
1424
  let main = '(no title/description)';
@@ -1591,6 +1742,7 @@ program
1591
1742
  }
1592
1743
  catch (error) {
1593
1744
  console.error('Error listing images:', error.message);
1745
+ process.exit(1);
1594
1746
  }
1595
1747
  });
1596
1748
  program
@@ -1611,6 +1763,7 @@ program
1611
1763
  console.error('Error: --ocr and --objects cannot be used together.');
1612
1764
  process.exit(1);
1613
1765
  }
1766
+ imageId = requireImageId(imageId);
1614
1767
  let image = options.cache !== false ? (0, storage_1.loadImageCache)(imageId) : null;
1615
1768
  if (!image) {
1616
1769
  image = await (0, api_1.getImageDetail)(imageId);
@@ -1648,6 +1801,43 @@ program
1648
1801
  }
1649
1802
  catch (error) {
1650
1803
  console.error('Error getting image:', error.message);
1804
+ process.exit(1);
1805
+ }
1806
+ });
1807
+ program
1808
+ .command('collection <collection_id>')
1809
+ .aliases(['col', 'cols', 'collections'])
1810
+ .description('Show a collection and the images in it')
1811
+ .option('-j, --json', 'output as JSON')
1812
+ .option('-A, --anonymous', 'read without an access token, even when one is configured')
1813
+ .option('--sort <added|created|captured>', 'image order (default: added)')
1814
+ .action(async (collectionIdInput, options) => {
1815
+ const collectionId = requireCollectionId(collectionIdInput);
1816
+ const sort = parseCollectionSort(options.sort);
1817
+ // No token is not an error here: public collections read fine anonymously.
1818
+ if (!options.anonymous) {
1819
+ (0, credentials_1.resolveAccessToken)();
1820
+ }
1821
+ try {
1822
+ const collection = await (0, api_1.getCollection)(collectionId, { anonymous: Boolean(options.anonymous) });
1823
+ if (options.json) {
1824
+ console.log(JSON.stringify(collection, null, 2));
1825
+ return;
1826
+ }
1827
+ const images = sortCollectionImages(Array.isArray(collection?.images) ? collection.images : [], sort);
1828
+ printCollectionMarkdown(collection, images);
1829
+ }
1830
+ catch (error) {
1831
+ if (error?.response?.status === 404) {
1832
+ console.error(`Error: collection ${collectionId} was not found or not public.`);
1833
+ console.error('Hint: a private collection returns the same 404 as one that does not exist.');
1834
+ if (options.anonymous) {
1835
+ console.error('Hint: you are running with --anonymous. Drop it to use your access token.');
1836
+ }
1837
+ process.exit(1);
1838
+ }
1839
+ console.error('Error getting collection:', error.message);
1840
+ process.exit(1);
1651
1841
  }
1652
1842
  });
1653
1843
  program
@@ -1680,6 +1870,7 @@ program
1680
1870
  }
1681
1871
  catch (error) {
1682
1872
  console.error('Error searching images:', error.message);
1873
+ process.exit(1);
1683
1874
  }
1684
1875
  });
1685
1876
  program
@@ -2067,6 +2258,7 @@ program
2067
2258
  program
2068
2259
  .command('upload [path]')
2069
2260
  .description('Upload an image file (or read image bytes from stdin)')
2261
+ .option('-j, --json', 'output the upload response as JSON')
2070
2262
  .option('--title <title>', 'image title')
2071
2263
  .option('--app <app>', 'application name', 'gyazocli')
2072
2264
  .option('--url <url>', 'source URL (sent as referer_url)')
@@ -2109,13 +2301,12 @@ program
2109
2301
  desc,
2110
2302
  timestamp,
2111
2303
  });
2112
- console.log(`URL: ${uploaded.permalink_url}`);
2113
- console.log(`ID: ${uploaded.image_id}`);
2114
- if (uploaded.created_at) {
2115
- console.log(`Created at: ${formatCreatedAt(uploaded.created_at)}`);
2304
+ if (options.json) {
2305
+ console.log(JSON.stringify(uploaded, null, 2));
2306
+ }
2307
+ else {
2308
+ console.log(uploaded.permalink_url);
2116
2309
  }
2117
- console.log(`App: ${options.app || 'gyazocli'}`);
2118
- console.log(`Desc: ${desc}`);
2119
2310
  }
2120
2311
  catch (error) {
2121
2312
  console.error('Error uploading image:', error.message);
@@ -2270,4 +2461,42 @@ program
2270
2461
  process.exit(1);
2271
2462
  }
2272
2463
  });
2273
- program.parseAsync(process.argv);
2464
+ /**
2465
+ * Let the first argument stand on its own when it is unambiguous:
2466
+ * a Gyazo image ID or URL means `get`, an existing file means `upload`.
2467
+ * Anything else is left to commander so unknown commands still report as such.
2468
+ */
2469
+ function expandImplicitCommand(argv) {
2470
+ const args = argv.slice(2);
2471
+ const first = args[0];
2472
+ if (!first || first.startsWith('-')) {
2473
+ return argv;
2474
+ }
2475
+ const knownNames = new Set(['help']);
2476
+ for (const command of program.commands) {
2477
+ knownNames.add(command.name());
2478
+ for (const alias of command.aliases()) {
2479
+ knownNames.add(alias);
2480
+ }
2481
+ }
2482
+ if (knownNames.has(first)) {
2483
+ return argv;
2484
+ }
2485
+ let implicitCommand = null;
2486
+ if (normalizeImageId(first)) {
2487
+ // A bare 32-hex ID is ambiguous; treat it as an image.
2488
+ implicitCommand = 'get';
2489
+ }
2490
+ else if (normalizeCollectionId(first)) {
2491
+ // Only the /collections/<id> URL form is unambiguous.
2492
+ implicitCommand = 'collection';
2493
+ }
2494
+ else if (fs_1.default.existsSync(first) && fs_1.default.statSync(first).isFile()) {
2495
+ implicitCommand = 'upload';
2496
+ }
2497
+ if (!implicitCommand) {
2498
+ return argv;
2499
+ }
2500
+ return [...argv.slice(0, 2), implicitCommand, ...args];
2501
+ }
2502
+ program.parseAsync(expandImplicitCommand(process.argv));
@@ -11,7 +11,7 @@ Adopt and document the existing top-level command structure.
11
11
 
12
12
  ### 1. Program Metadata
13
13
  - Binary name: `gyazo`
14
- - Version: `0.0.2`
14
+ - Version: `0.1.1`
15
15
  - Description: `Gyazo Memory CLI for AI Secretary`
16
16
 
17
17
  ### 2. Commands
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@yuiseki/gyazocli",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "description": "Gyazo Memory CLI for AI Secretary",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/yuiseki/gyazocli.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/yuiseki/gyazocli/issues"
11
+ },
12
+ "homepage": "https://github.com/yuiseki/gyazocli#readme",
5
13
  "main": "dist/index.js",
6
14
  "bin": {
7
15
  "gyazo": "dist/index.js"
@@ -31,6 +39,7 @@
31
39
  "axios": "^1.13.5",
32
40
  "commander": "^14.0.3",
33
41
  "dotenv": "^17.3.1",
42
+ "form-data": "^4.0.0",
34
43
  "zod": "^4.3.6"
35
44
  },
36
45
  "devDependencies": {