@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 +59 -3
- package/dist/api.js +43 -13
- package/dist/config.js +6 -0
- package/dist/credentials.js +13 -1
- package/dist/index.js +238 -9
- package/docs/ADR/003-cli-structure.md +1 -1
- package/package.json +10 -1
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
|
|
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 `
|
|
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
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
52
|
+
return requestWithRetry(apiBaseUrl(), { page, per_page: perPage });
|
|
36
53
|
}
|
|
37
54
|
async function getImageDetail(imageId) {
|
|
38
|
-
return requestWithRetry(`${
|
|
55
|
+
return requestWithRetry(`${apiBaseUrl()}/${imageId}`);
|
|
39
56
|
}
|
|
40
57
|
async function searchImages(query, page = 1, perPage = 20) {
|
|
41
|
-
return requestWithRetry(
|
|
58
|
+
return requestWithRetry(apiSearchUrl(), { query, page, per: perPage });
|
|
42
59
|
}
|
|
43
60
|
async function getCurrentUser() {
|
|
44
|
-
return requestWithRetry(
|
|
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(
|
|
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;
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
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
|
-
|
|
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));
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yuiseki/gyazocli",
|
|
3
|
-
"version": "0.
|
|
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": {
|