@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.
Files changed (3) hide show
  1. package/README.md +26 -1
  2. package/dist/index.js +150 -40
  3. 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 --limit 5 --sort oldest # Paginate and sort
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(args[i])) {
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/delete.ts
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 run3(args) {
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 run4(args) {
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
- for (const [i, link] of data.links.entries()) {
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 run5(args) {
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(` Member since: ${new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`);
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 run6(args) {
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 run7(args) {
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 run8(args) {
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("Provide at least one update flag: --read, --unread, --title, --summary, --restore");
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 message = error instanceof Error ? error.message : String(error);
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.1.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: run4, desc: "List saved links" },
541
- get: { run: run3, desc: "Get a link by ID (includes content)" },
542
- save: { run: run6, desc: "Save a new link" },
543
- update: { run: run8, desc: "Update a link" },
544
- delete: { run: run2, desc: "Delete a link (move to trash)" },
545
- trash: { run: run7, desc: "List or empty the trash" },
546
- me: { run: run5, desc: "Show current user info" }
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 Authenticate with your API key
562
- shiori list List your recent links
563
- shiori save <url> Save a new link`);
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.1.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.1.0", isJson).catch(() => {
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.1.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"