@shiori-sh/cli 0.1.0 → 0.2.3
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 +26 -1
- package/dist/index.js +150 -40
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -29,9 +29,16 @@ You can also set `SHIORI_API_KEY` as an environment variable.
|
|
|
29
29
|
```bash
|
|
30
30
|
shiori list # List recent links
|
|
31
31
|
shiori list --read unread # List unread links
|
|
32
|
-
shiori list --
|
|
32
|
+
shiori list --since 7d # Links saved in the last 7 days
|
|
33
|
+
shiori list --content --json # Include full markdown content
|
|
34
|
+
|
|
35
|
+
shiori search "react hooks" # Full-text search
|
|
36
|
+
shiori search "AI" --since 30d # Search within a time window
|
|
37
|
+
shiori search "go" --content --json # Search with full content
|
|
33
38
|
|
|
34
39
|
shiori get <id> # Get a link with full content
|
|
40
|
+
shiori content <id> # Print raw markdown (for piping)
|
|
41
|
+
|
|
35
42
|
shiori save <url> # Save a new link
|
|
36
43
|
shiori save <url> --title "..." # Save with custom title
|
|
37
44
|
|
|
@@ -51,6 +58,24 @@ shiori auth --logout # Remove stored credentials
|
|
|
51
58
|
|
|
52
59
|
Add `--json` to any command for machine-readable output.
|
|
53
60
|
|
|
61
|
+
## AI and Scripting
|
|
62
|
+
|
|
63
|
+
The CLI is designed to make your links accessible to AI tools and scripts:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Pipe link content to an LLM
|
|
67
|
+
shiori content <id> | llm "summarize this"
|
|
68
|
+
|
|
69
|
+
# Get recent links with full content as JSON
|
|
70
|
+
shiori list --since 7d --content --json
|
|
71
|
+
|
|
72
|
+
# Search and pipe results
|
|
73
|
+
shiori search "react" --content --json | jq '.links[].content'
|
|
74
|
+
|
|
75
|
+
# Duration shortcuts: 1h, 7d, 2w, 1m, 1y
|
|
76
|
+
shiori list --since 2w --json
|
|
77
|
+
```
|
|
78
|
+
|
|
54
79
|
## Environment Variables
|
|
55
80
|
|
|
56
81
|
| Variable | Description |
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ function readConfig() {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
function writeConfig(config) {
|
|
20
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
21
21
|
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
22
22
|
mode: 384
|
|
23
23
|
});
|
|
@@ -27,9 +27,7 @@ function getApiKey() {
|
|
|
27
27
|
if (envKey) return envKey;
|
|
28
28
|
const config = readConfig();
|
|
29
29
|
if (config.api_key) return config.api_key;
|
|
30
|
-
console.error(
|
|
31
|
-
"Not authenticated. Run `shiori auth` to set up your API key."
|
|
32
|
-
);
|
|
30
|
+
console.error("Not authenticated. Run `shiori auth` to set up your API key.");
|
|
33
31
|
console.error("Or set the SHIORI_API_KEY environment variable.");
|
|
34
32
|
process.exit(1);
|
|
35
33
|
}
|
|
@@ -198,7 +196,9 @@ function hasFlag(args, ...names) {
|
|
|
198
196
|
function getPositional(args) {
|
|
199
197
|
for (let i = 0; i < args.length; i++) {
|
|
200
198
|
if (args[i].startsWith("--")) {
|
|
201
|
-
if (i + 1 < args.length && !args[i + 1].startsWith("--") && ["--limit", "--offset", "--sort", "--read", "--title", "--summary"].includes(
|
|
199
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("--") && ["--limit", "--offset", "--sort", "--read", "--title", "--summary", "--since"].includes(
|
|
200
|
+
args[i]
|
|
201
|
+
)) {
|
|
202
202
|
i++;
|
|
203
203
|
}
|
|
204
204
|
continue;
|
|
@@ -207,9 +207,66 @@ function getPositional(args) {
|
|
|
207
207
|
}
|
|
208
208
|
return void 0;
|
|
209
209
|
}
|
|
210
|
+
var DURATION_UNITS = {
|
|
211
|
+
h: 36e5,
|
|
212
|
+
d: 864e5,
|
|
213
|
+
w: 6048e5,
|
|
214
|
+
m: 2592e6,
|
|
215
|
+
y: 31536e6
|
|
216
|
+
};
|
|
217
|
+
function parseDuration(str) {
|
|
218
|
+
const match = str.match(/^(\d+)([hdwmy])$/);
|
|
219
|
+
if (!match) {
|
|
220
|
+
throw new Error(`Invalid duration "${str}". Use a number + unit: 1h, 7d, 2w, 1m, 1y`);
|
|
221
|
+
}
|
|
222
|
+
const amount = Number(match[1]);
|
|
223
|
+
const ms = amount * DURATION_UNITS[match[2]];
|
|
224
|
+
return new Date(Date.now() - ms).toISOString();
|
|
225
|
+
}
|
|
226
|
+
function formatLinkList(links, options) {
|
|
227
|
+
for (const [i, link] of links.entries()) {
|
|
228
|
+
const readStatus = link.read_at ? "read" : "unread";
|
|
229
|
+
const title = truncate(link.title || "(untitled)", 60);
|
|
230
|
+
console.log(
|
|
231
|
+
` ${String(options.offset + i + 1).padStart(3)}. ${title} ${link.domain || ""} ${formatDate(link.created_at)} ${readStatus}`
|
|
232
|
+
);
|
|
233
|
+
if (options.showContent && link.content) {
|
|
234
|
+
console.log(` ${truncate(link.content.replace(/\n/g, " "), 500)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
210
238
|
|
|
211
|
-
// src/commands/
|
|
239
|
+
// src/commands/content.ts
|
|
212
240
|
async function run2(args) {
|
|
241
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
242
|
+
console.log(`shiori content - Print link content as markdown
|
|
243
|
+
|
|
244
|
+
Usage: shiori content <id>
|
|
245
|
+
|
|
246
|
+
Prints the extracted markdown content to stdout with no decoration.
|
|
247
|
+
Useful for piping into other tools:
|
|
248
|
+
|
|
249
|
+
shiori content <id> | llm "summarize this"
|
|
250
|
+
shiori content <id> | pbcopy
|
|
251
|
+
|
|
252
|
+
Options:
|
|
253
|
+
--help, -h Show this help`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const id = getPositional(args);
|
|
257
|
+
if (!id) {
|
|
258
|
+
console.error("Usage: shiori content <link-id>");
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const { data } = await api("GET", `/api/links/${id}`);
|
|
262
|
+
const content = data.link?.content;
|
|
263
|
+
if (content) {
|
|
264
|
+
process.stdout.write(content);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/commands/delete.ts
|
|
269
|
+
async function run3(args) {
|
|
213
270
|
if (hasFlag(args, "--help", "-h")) {
|
|
214
271
|
console.log(`shiori delete - Delete a link (move to trash)
|
|
215
272
|
|
|
@@ -234,7 +291,7 @@ Options:
|
|
|
234
291
|
}
|
|
235
292
|
|
|
236
293
|
// src/commands/get.ts
|
|
237
|
-
async function
|
|
294
|
+
async function run4(args) {
|
|
238
295
|
if (hasFlag(args, "--help", "-h")) {
|
|
239
296
|
console.log(`shiori get - Get a link by ID (includes content)
|
|
240
297
|
|
|
@@ -266,9 +323,7 @@ Options:
|
|
|
266
323
|
console.log(` Author: ${link.author || "--"}`);
|
|
267
324
|
console.log(` Published: ${formatDate(link.publication_date)}`);
|
|
268
325
|
console.log(` Saved: ${formatDate(link.created_at)}`);
|
|
269
|
-
console.log(
|
|
270
|
-
` Read: ${link.read_at ? formatDate(link.read_at) : "unread"}`
|
|
271
|
-
);
|
|
326
|
+
console.log(` Read: ${link.read_at ? formatDate(link.read_at) : "unread"}`);
|
|
272
327
|
console.log(` Summary: ${link.summary || "--"}`);
|
|
273
328
|
if (link.content) {
|
|
274
329
|
console.log(`
|
|
@@ -281,7 +336,7 @@ Options:
|
|
|
281
336
|
}
|
|
282
337
|
|
|
283
338
|
// src/commands/list.ts
|
|
284
|
-
async function
|
|
339
|
+
async function run5(args) {
|
|
285
340
|
if (hasFlag(args, "--help", "-h")) {
|
|
286
341
|
console.log(`shiori list - List saved links
|
|
287
342
|
|
|
@@ -292,6 +347,8 @@ Options:
|
|
|
292
347
|
--offset <n> Pagination offset (default: 0)
|
|
293
348
|
--sort <newest|oldest> Sort order (default: newest)
|
|
294
349
|
--read <all|read|unread> Filter by read status (default: all)
|
|
350
|
+
--since <duration> Only links saved within this period (e.g. 1h, 7d, 2w, 1m, 1y)
|
|
351
|
+
--content Include extracted markdown content
|
|
295
352
|
--json Output raw JSON
|
|
296
353
|
--help, -h Show this help`);
|
|
297
354
|
return;
|
|
@@ -300,12 +357,16 @@ Options:
|
|
|
300
357
|
const offset = getFlag(args, "--offset", "0");
|
|
301
358
|
const sort = getFlag(args, "--sort", "newest");
|
|
302
359
|
const read = getFlag(args, "--read", "all");
|
|
360
|
+
const sinceFlag = getFlag(args, "--since");
|
|
361
|
+
const includeContent = hasFlag(args, "--content");
|
|
303
362
|
const params = new URLSearchParams({
|
|
304
363
|
limit,
|
|
305
364
|
offset,
|
|
306
365
|
sort,
|
|
307
366
|
read
|
|
308
367
|
});
|
|
368
|
+
if (includeContent) params.set("include_content", "true");
|
|
369
|
+
if (sinceFlag) params.set("since", parseDuration(sinceFlag));
|
|
309
370
|
const { data } = await api("GET", `/api/links?${params}`);
|
|
310
371
|
if (hasFlag(args, "--json")) {
|
|
311
372
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -314,13 +375,7 @@ Options:
|
|
|
314
375
|
console.log(`
|
|
315
376
|
Links: ${data.links.length} of ${data.total} total
|
|
316
377
|
`);
|
|
317
|
-
|
|
318
|
-
const readStatus = link.read_at ? "read" : "unread";
|
|
319
|
-
const title = truncate(link.title || "(untitled)", 60);
|
|
320
|
-
console.log(
|
|
321
|
-
` ${String(Number(offset) + i + 1).padStart(3)}. ${title} ${link.domain || ""} ${formatDate(link.created_at)} ${readStatus}`
|
|
322
|
-
);
|
|
323
|
-
}
|
|
378
|
+
formatLinkList(data.links, { offset: Number(offset), showContent: includeContent });
|
|
324
379
|
if (data.links.length === 0) {
|
|
325
380
|
console.log(" No links found.");
|
|
326
381
|
}
|
|
@@ -328,7 +383,7 @@ Links: ${data.links.length} of ${data.total} total
|
|
|
328
383
|
}
|
|
329
384
|
|
|
330
385
|
// src/commands/me.ts
|
|
331
|
-
async function
|
|
386
|
+
async function run6(args) {
|
|
332
387
|
if (hasFlag(args, "--help", "-h")) {
|
|
333
388
|
console.log(`shiori me - Show current user info
|
|
334
389
|
|
|
@@ -349,12 +404,14 @@ Options:
|
|
|
349
404
|
Name: ${user.full_name || "--"}`);
|
|
350
405
|
const plan = user.subscription?.plan === "subscription" ? "Pro" : user.subscription?.plan || "Free";
|
|
351
406
|
console.log(` Plan: ${plan}`);
|
|
352
|
-
console.log(
|
|
407
|
+
console.log(
|
|
408
|
+
` Member since: ${new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`
|
|
409
|
+
);
|
|
353
410
|
console.log();
|
|
354
411
|
}
|
|
355
412
|
|
|
356
413
|
// src/commands/save.ts
|
|
357
|
-
async function
|
|
414
|
+
async function run7(args) {
|
|
358
415
|
if (hasFlag(args, "--help", "-h")) {
|
|
359
416
|
console.log(`shiori save - Save a new link
|
|
360
417
|
|
|
@@ -388,8 +445,55 @@ Options:
|
|
|
388
445
|
}
|
|
389
446
|
}
|
|
390
447
|
|
|
448
|
+
// src/commands/search.ts
|
|
449
|
+
async function run8(args) {
|
|
450
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
451
|
+
console.log(`shiori search - Search saved links
|
|
452
|
+
|
|
453
|
+
Usage: shiori search <query> [options]
|
|
454
|
+
|
|
455
|
+
Options:
|
|
456
|
+
--limit <n> Number of links (1-100, default: 25)
|
|
457
|
+
--offset <n> Pagination offset (default: 0)
|
|
458
|
+
--since <duration> Only links saved within this period (e.g. 1h, 7d, 2w, 1m, 1y)
|
|
459
|
+
--content Include extracted markdown content
|
|
460
|
+
--json Output raw JSON
|
|
461
|
+
--help, -h Show this help`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const query = getPositional(args);
|
|
465
|
+
if (!query) {
|
|
466
|
+
console.error("Usage: shiori search <query>");
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
const limit = getFlag(args, "--limit", "25");
|
|
470
|
+
const offset = getFlag(args, "--offset", "0");
|
|
471
|
+
const sinceFlag = getFlag(args, "--since");
|
|
472
|
+
const includeContent = hasFlag(args, "--content");
|
|
473
|
+
const params = new URLSearchParams({
|
|
474
|
+
search: query,
|
|
475
|
+
limit,
|
|
476
|
+
offset
|
|
477
|
+
});
|
|
478
|
+
if (includeContent) params.set("include_content", "true");
|
|
479
|
+
if (sinceFlag) params.set("since", parseDuration(sinceFlag));
|
|
480
|
+
const { data } = await api("GET", `/api/links?${params}`);
|
|
481
|
+
if (hasFlag(args, "--json")) {
|
|
482
|
+
console.log(JSON.stringify(data, null, 2));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
console.log(`
|
|
486
|
+
Results: ${data.links.length} of ${data.total} matches
|
|
487
|
+
`);
|
|
488
|
+
formatLinkList(data.links, { offset: Number(offset), showContent: includeContent });
|
|
489
|
+
if (data.links.length === 0) {
|
|
490
|
+
console.log(" No links found.");
|
|
491
|
+
}
|
|
492
|
+
console.log();
|
|
493
|
+
}
|
|
494
|
+
|
|
391
495
|
// src/commands/trash.ts
|
|
392
|
-
async function
|
|
496
|
+
async function run9(args) {
|
|
393
497
|
if (hasFlag(args, "--help", "-h")) {
|
|
394
498
|
console.log(`shiori trash - List or empty the trash
|
|
395
499
|
|
|
@@ -440,7 +544,7 @@ Trashed links: ${data.links.length} of ${data.total} total
|
|
|
440
544
|
}
|
|
441
545
|
|
|
442
546
|
// src/commands/update.ts
|
|
443
|
-
async function
|
|
547
|
+
async function run10(args) {
|
|
444
548
|
if (hasFlag(args, "--help", "-h")) {
|
|
445
549
|
console.log(`shiori update - Update a link
|
|
446
550
|
|
|
@@ -472,7 +576,9 @@ Options:
|
|
|
472
576
|
const title = getFlag(args, "--title");
|
|
473
577
|
const summary = getFlag(args, "--summary");
|
|
474
578
|
if (!title && !summary) {
|
|
475
|
-
console.error(
|
|
579
|
+
console.error(
|
|
580
|
+
"Provide at least one update flag: --read, --unread, --title, --summary, --restore"
|
|
581
|
+
);
|
|
476
582
|
process.exit(1);
|
|
477
583
|
}
|
|
478
584
|
body = {};
|
|
@@ -489,13 +595,14 @@ Options:
|
|
|
489
595
|
|
|
490
596
|
// src/error-report.ts
|
|
491
597
|
function reportError(command, error) {
|
|
492
|
-
const
|
|
598
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
599
|
+
const message = raw.replace(/shk_[a-zA-Z0-9]+/g, "shk_[REDACTED]");
|
|
493
600
|
const baseUrl = getBaseUrl();
|
|
494
601
|
fetch(`${baseUrl}/api/cli/report`, {
|
|
495
602
|
method: "POST",
|
|
496
603
|
headers: { "Content-Type": "application/json" },
|
|
497
604
|
body: JSON.stringify({
|
|
498
|
-
version: "0.
|
|
605
|
+
version: "0.2.3",
|
|
499
606
|
command,
|
|
500
607
|
error: message,
|
|
501
608
|
platform: process.platform
|
|
@@ -512,8 +619,7 @@ async function checkForUpdate(currentVersion, isJson) {
|
|
|
512
619
|
if (process.env.CI || process.env.NO_UPDATE_NOTIFIER || isJson) return;
|
|
513
620
|
const config = readConfig();
|
|
514
621
|
const now = Date.now();
|
|
515
|
-
if (config.last_update_check && now - config.last_update_check < CHECK_INTERVAL_MS)
|
|
516
|
-
return;
|
|
622
|
+
if (config.last_update_check && now - config.last_update_check < CHECK_INTERVAL_MS) return;
|
|
517
623
|
try {
|
|
518
624
|
const res = await fetch(REGISTRY_URL, {
|
|
519
625
|
signal: AbortSignal.timeout(3e3)
|
|
@@ -537,13 +643,15 @@ async function checkForUpdate(currentVersion, isJson) {
|
|
|
537
643
|
// src/index.ts
|
|
538
644
|
var COMMANDS = {
|
|
539
645
|
auth: { run, desc: "Authenticate with your Shiori API key" },
|
|
540
|
-
list: { run:
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
646
|
+
list: { run: run5, desc: "List saved links" },
|
|
647
|
+
search: { run: run8, desc: "Search saved links" },
|
|
648
|
+
get: { run: run4, desc: "Get a link by ID (includes content)" },
|
|
649
|
+
content: { run: run2, desc: "Print link content as markdown" },
|
|
650
|
+
save: { run: run7, desc: "Save a new link" },
|
|
651
|
+
update: { run: run10, desc: "Update a link" },
|
|
652
|
+
delete: { run: run3, desc: "Delete a link (move to trash)" },
|
|
653
|
+
trash: { run: run9, desc: "List or empty the trash" },
|
|
654
|
+
me: { run: run6, desc: "Show current user info" }
|
|
547
655
|
};
|
|
548
656
|
function printHelp() {
|
|
549
657
|
console.log(`shiori - Manage your Shiori link library from the terminal
|
|
@@ -558,9 +666,11 @@ Options:
|
|
|
558
666
|
--version, -v Show version
|
|
559
667
|
|
|
560
668
|
Get started:
|
|
561
|
-
shiori auth
|
|
562
|
-
shiori list
|
|
563
|
-
shiori save <url>
|
|
669
|
+
shiori auth Authenticate with your API key
|
|
670
|
+
shiori list List your recent links
|
|
671
|
+
shiori save <url> Save a new link
|
|
672
|
+
shiori search <query> Search your links
|
|
673
|
+
shiori content <id> Print markdown content (pipe to other tools)`);
|
|
564
674
|
}
|
|
565
675
|
async function main() {
|
|
566
676
|
const args = process.argv.slice(2);
|
|
@@ -570,7 +680,7 @@ async function main() {
|
|
|
570
680
|
return;
|
|
571
681
|
}
|
|
572
682
|
if (command === "--version" || command === "-v") {
|
|
573
|
-
console.log("0.
|
|
683
|
+
console.log("0.2.3");
|
|
574
684
|
return;
|
|
575
685
|
}
|
|
576
686
|
const cmd = COMMANDS[command];
|
|
@@ -583,7 +693,7 @@ async function main() {
|
|
|
583
693
|
const cmdArgs = args.slice(1);
|
|
584
694
|
const isJson = cmdArgs.includes("--json");
|
|
585
695
|
await cmd.run(cmdArgs);
|
|
586
|
-
checkForUpdate("0.
|
|
696
|
+
checkForUpdate("0.2.3", isJson).catch(() => {
|
|
587
697
|
});
|
|
588
698
|
}
|
|
589
699
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shiori-sh/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "CLI for managing your Shiori link library",
|
|
5
5
|
"author": "Brian Lovin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
"build": "tsup",
|
|
22
22
|
"dev": "tsup --watch",
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
24
25
|
"prepublishOnly": "bun run build"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"tsup": "^8.0.0",
|
|
28
|
-
"typescript": "^5.8.3"
|
|
29
|
+
"typescript": "^5.8.3",
|
|
30
|
+
"vitest": "^3.0.0"
|
|
29
31
|
},
|
|
30
32
|
"engines": {
|
|
31
33
|
"node": ">=18"
|