@shiori-sh/cli 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +33 -1
  2. package/dist/index.js +317 -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
 
@@ -44,6 +51,13 @@ shiori delete <id> # Move to trash
44
51
  shiori trash # List trashed links
45
52
  shiori trash --empty # Permanently delete all trash
46
53
 
54
+ shiori subscriptions list # List RSS subscriptions
55
+ shiori subscriptions add <url> # Subscribe to an RSS feed
56
+ shiori subscriptions add <url> --sync # Subscribe and sync recent items
57
+ shiori subscriptions remove <id> # Remove a subscription
58
+ shiori subscriptions sync <id> # Sync a subscription now
59
+ shiori subscriptions sync <id> --limit 5 # Sync with item limit
60
+
47
61
  shiori me # Show account info
48
62
  shiori auth --status # Check auth status
49
63
  shiori auth --logout # Remove stored credentials
@@ -51,6 +65,24 @@ shiori auth --logout # Remove stored credentials
51
65
 
52
66
  Add `--json` to any command for machine-readable output.
53
67
 
68
+ ## AI and Scripting
69
+
70
+ The CLI is designed to make your links accessible to AI tools and scripts:
71
+
72
+ ```bash
73
+ # Pipe link content to an LLM
74
+ shiori content <id> | llm "summarize this"
75
+
76
+ # Get recent links with full content as JSON
77
+ shiori list --since 7d --content --json
78
+
79
+ # Search and pipe results
80
+ shiori search "react" --content --json | jq '.links[].content'
81
+
82
+ # Duration shortcuts: 1h, 7d, 2w, 1m, 1y
83
+ shiori list --since 2w --json
84
+ ```
85
+
54
86
  ## Environment Variables
55
87
 
56
88
  | 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,16 @@ 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("--") && [
200
+ "--limit",
201
+ "--offset",
202
+ "--sort",
203
+ "--read",
204
+ "--title",
205
+ "--summary",
206
+ "--since",
207
+ "--format"
208
+ ].includes(args[i])) {
202
209
  i++;
203
210
  }
204
211
  continue;
@@ -207,9 +214,66 @@ function getPositional(args) {
207
214
  }
208
215
  return void 0;
209
216
  }
217
+ var DURATION_UNITS = {
218
+ h: 36e5,
219
+ d: 864e5,
220
+ w: 6048e5,
221
+ m: 2592e6,
222
+ y: 31536e6
223
+ };
224
+ function parseDuration(str) {
225
+ const match = str.match(/^(\d+)([hdwmy])$/);
226
+ if (!match) {
227
+ throw new Error(`Invalid duration "${str}". Use a number + unit: 1h, 7d, 2w, 1m, 1y`);
228
+ }
229
+ const amount = Number(match[1]);
230
+ const ms = amount * DURATION_UNITS[match[2]];
231
+ return new Date(Date.now() - ms).toISOString();
232
+ }
233
+ function formatLinkList(links, options) {
234
+ for (const [i, link] of links.entries()) {
235
+ const readStatus = link.read_at ? "read" : "unread";
236
+ const title = truncate(link.title || "(untitled)", 60);
237
+ console.log(
238
+ ` ${String(options.offset + i + 1).padStart(3)}. ${title} ${link.domain || ""} ${formatDate(link.created_at)} ${readStatus}`
239
+ );
240
+ if (options.showContent && link.content) {
241
+ console.log(` ${truncate(link.content.replace(/\n/g, " "), 500)}`);
242
+ }
243
+ }
244
+ }
210
245
 
211
- // src/commands/delete.ts
246
+ // src/commands/content.ts
212
247
  async function run2(args) {
248
+ if (hasFlag(args, "--help", "-h")) {
249
+ console.log(`shiori content - Print link content as markdown
250
+
251
+ Usage: shiori content <id>
252
+
253
+ Prints the extracted markdown content to stdout with no decoration.
254
+ Useful for piping into other tools:
255
+
256
+ shiori content <id> | llm "summarize this"
257
+ shiori content <id> | pbcopy
258
+
259
+ Options:
260
+ --help, -h Show this help`);
261
+ return;
262
+ }
263
+ const id = getPositional(args);
264
+ if (!id) {
265
+ console.error("Usage: shiori content <link-id>");
266
+ process.exit(1);
267
+ }
268
+ const { data } = await api("GET", `/api/links/${id}`);
269
+ const content = data.link?.content;
270
+ if (content) {
271
+ process.stdout.write(content);
272
+ }
273
+ }
274
+
275
+ // src/commands/delete.ts
276
+ async function run3(args) {
213
277
  if (hasFlag(args, "--help", "-h")) {
214
278
  console.log(`shiori delete - Delete a link (move to trash)
215
279
 
@@ -234,7 +298,7 @@ Options:
234
298
  }
235
299
 
236
300
  // src/commands/get.ts
237
- async function run3(args) {
301
+ async function run4(args) {
238
302
  if (hasFlag(args, "--help", "-h")) {
239
303
  console.log(`shiori get - Get a link by ID (includes content)
240
304
 
@@ -266,9 +330,7 @@ Options:
266
330
  console.log(` Author: ${link.author || "--"}`);
267
331
  console.log(` Published: ${formatDate(link.publication_date)}`);
268
332
  console.log(` Saved: ${formatDate(link.created_at)}`);
269
- console.log(
270
- ` Read: ${link.read_at ? formatDate(link.read_at) : "unread"}`
271
- );
333
+ console.log(` Read: ${link.read_at ? formatDate(link.read_at) : "unread"}`);
272
334
  console.log(` Summary: ${link.summary || "--"}`);
273
335
  if (link.content) {
274
336
  console.log(`
@@ -281,7 +343,7 @@ Options:
281
343
  }
282
344
 
283
345
  // src/commands/list.ts
284
- async function run4(args) {
346
+ async function run5(args) {
285
347
  if (hasFlag(args, "--help", "-h")) {
286
348
  console.log(`shiori list - List saved links
287
349
 
@@ -292,6 +354,8 @@ Options:
292
354
  --offset <n> Pagination offset (default: 0)
293
355
  --sort <newest|oldest> Sort order (default: newest)
294
356
  --read <all|read|unread> Filter by read status (default: all)
357
+ --since <duration> Only links saved within this period (e.g. 1h, 7d, 2w, 1m, 1y)
358
+ --content Include extracted markdown content
295
359
  --json Output raw JSON
296
360
  --help, -h Show this help`);
297
361
  return;
@@ -300,12 +364,16 @@ Options:
300
364
  const offset = getFlag(args, "--offset", "0");
301
365
  const sort = getFlag(args, "--sort", "newest");
302
366
  const read = getFlag(args, "--read", "all");
367
+ const sinceFlag = getFlag(args, "--since");
368
+ const includeContent = hasFlag(args, "--content");
303
369
  const params = new URLSearchParams({
304
370
  limit,
305
371
  offset,
306
372
  sort,
307
373
  read
308
374
  });
375
+ if (includeContent) params.set("include_content", "true");
376
+ if (sinceFlag) params.set("since", parseDuration(sinceFlag));
309
377
  const { data } = await api("GET", `/api/links?${params}`);
310
378
  if (hasFlag(args, "--json")) {
311
379
  console.log(JSON.stringify(data, null, 2));
@@ -314,13 +382,7 @@ Options:
314
382
  console.log(`
315
383
  Links: ${data.links.length} of ${data.total} total
316
384
  `);
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
- }
385
+ formatLinkList(data.links, { offset: Number(offset), showContent: includeContent });
324
386
  if (data.links.length === 0) {
325
387
  console.log(" No links found.");
326
388
  }
@@ -328,7 +390,7 @@ Links: ${data.links.length} of ${data.total} total
328
390
  }
329
391
 
330
392
  // src/commands/me.ts
331
- async function run5(args) {
393
+ async function run6(args) {
332
394
  if (hasFlag(args, "--help", "-h")) {
333
395
  console.log(`shiori me - Show current user info
334
396
 
@@ -349,12 +411,14 @@ Options:
349
411
  Name: ${user.full_name || "--"}`);
350
412
  const plan = user.subscription?.plan === "subscription" ? "Pro" : user.subscription?.plan || "Free";
351
413
  console.log(` Plan: ${plan}`);
352
- console.log(` Member since: ${new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`);
414
+ console.log(
415
+ ` Member since: ${new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`
416
+ );
353
417
  console.log();
354
418
  }
355
419
 
356
420
  // src/commands/save.ts
357
- async function run6(args) {
421
+ async function run7(args) {
358
422
  if (hasFlag(args, "--help", "-h")) {
359
423
  console.log(`shiori save - Save a new link
360
424
 
@@ -388,8 +452,214 @@ Options:
388
452
  }
389
453
  }
390
454
 
455
+ // src/commands/search.ts
456
+ async function run8(args) {
457
+ if (hasFlag(args, "--help", "-h")) {
458
+ console.log(`shiori search - Search saved links
459
+
460
+ Usage: shiori search <query> [options]
461
+
462
+ Options:
463
+ --limit <n> Number of links (1-100, default: 25)
464
+ --offset <n> Pagination offset (default: 0)
465
+ --since <duration> Only links saved within this period (e.g. 1h, 7d, 2w, 1m, 1y)
466
+ --content Include extracted markdown content
467
+ --json Output raw JSON
468
+ --help, -h Show this help`);
469
+ return;
470
+ }
471
+ const query = getPositional(args);
472
+ if (!query) {
473
+ console.error("Usage: shiori search <query>");
474
+ process.exit(1);
475
+ }
476
+ const limit = getFlag(args, "--limit", "25");
477
+ const offset = getFlag(args, "--offset", "0");
478
+ const sinceFlag = getFlag(args, "--since");
479
+ const includeContent = hasFlag(args, "--content");
480
+ const params = new URLSearchParams({
481
+ search: query,
482
+ limit,
483
+ offset
484
+ });
485
+ if (includeContent) params.set("include_content", "true");
486
+ if (sinceFlag) params.set("since", parseDuration(sinceFlag));
487
+ const { data } = await api("GET", `/api/links?${params}`);
488
+ if (hasFlag(args, "--json")) {
489
+ console.log(JSON.stringify(data, null, 2));
490
+ return;
491
+ }
492
+ console.log(`
493
+ Results: ${data.links.length} of ${data.total} matches
494
+ `);
495
+ formatLinkList(data.links, { offset: Number(offset), showContent: includeContent });
496
+ if (data.links.length === 0) {
497
+ console.log(" No links found.");
498
+ }
499
+ console.log();
500
+ }
501
+
502
+ // src/commands/subscriptions.ts
503
+ var HELP = `shiori subscriptions - Manage RSS subscriptions
504
+
505
+ Usage: shiori subscriptions <subcommand> [options]
506
+
507
+ Subcommands:
508
+ list List your subscriptions
509
+ add <url> Subscribe to an RSS feed
510
+ remove <id> Remove a subscription
511
+ sync <id> Sync a subscription now
512
+
513
+ Options:
514
+ --json Output raw JSON
515
+ --help, -h Show this help
516
+
517
+ Examples:
518
+ shiori subscriptions list
519
+ shiori subscriptions add https://example.com/feed.xml
520
+ shiori subscriptions add https://example.com --sync
521
+ shiori subscriptions remove <id>
522
+ shiori subscriptions sync <id> --limit 5`;
523
+ async function run9(args) {
524
+ if (hasFlag(args, "--help", "-h") && !args[0]) {
525
+ console.log(HELP);
526
+ return;
527
+ }
528
+ const subcommand = args[0];
529
+ const subArgs = args.slice(1);
530
+ switch (subcommand) {
531
+ case "list":
532
+ case "ls":
533
+ return listSubscriptions(subArgs);
534
+ case "add":
535
+ return addSubscription(subArgs);
536
+ case "remove":
537
+ case "rm":
538
+ return removeSubscription(subArgs);
539
+ case "sync":
540
+ return syncSubscription(subArgs);
541
+ default:
542
+ if (subcommand) {
543
+ console.error(`Unknown subcommand: ${subcommand}
544
+ `);
545
+ }
546
+ console.log(HELP);
547
+ if (subcommand) process.exit(1);
548
+ }
549
+ }
550
+ async function listSubscriptions(args) {
551
+ if (hasFlag(args, "--help", "-h")) {
552
+ console.log(`shiori subscriptions list - List your RSS subscriptions
553
+
554
+ Usage: shiori subscriptions list [options]
555
+
556
+ Options:
557
+ --json Output raw JSON
558
+ --help, -h Show this help`);
559
+ return;
560
+ }
561
+ const { data } = await api("GET", "/api/subscriptions/");
562
+ if (hasFlag(args, "--json")) {
563
+ console.log(JSON.stringify(data, null, 2));
564
+ return;
565
+ }
566
+ const subs = data.subscriptions;
567
+ if (subs.length === 0) {
568
+ console.log("\n No subscriptions. Add one with: shiori subscriptions add <url>\n");
569
+ return;
570
+ }
571
+ console.log(`
572
+ Subscriptions: ${subs.length}
573
+ `);
574
+ for (const sub of subs) {
575
+ const title = truncate(sub.title || sub.feed_url, 50);
576
+ const synced = sub.last_synced_at ? `synced ${formatDate(sub.last_synced_at)}` : "never synced";
577
+ console.log(` ${sub.id} ${title} ${synced}`);
578
+ }
579
+ console.log();
580
+ }
581
+ async function addSubscription(args) {
582
+ if (hasFlag(args, "--help", "-h")) {
583
+ console.log(`shiori subscriptions add - Subscribe to an RSS feed
584
+
585
+ Usage: shiori subscriptions add <url> [options]
586
+
587
+ Options:
588
+ --sync Sync the 3 most recent items immediately
589
+ --json Output raw JSON
590
+ --help, -h Show this help`);
591
+ return;
592
+ }
593
+ const url = getPositional(args);
594
+ if (!url) {
595
+ console.error("Usage: shiori subscriptions add <url>");
596
+ process.exit(1);
597
+ }
598
+ const body = { feedUrl: url };
599
+ if (hasFlag(args, "--sync")) {
600
+ body.initialSync = true;
601
+ }
602
+ const { data } = await api("POST", "/api/subscriptions/", body);
603
+ if (hasFlag(args, "--json")) {
604
+ console.log(JSON.stringify(data, null, 2));
605
+ return;
606
+ }
607
+ const sub = data.subscription;
608
+ console.log(`Subscribed: ${sub.title || sub.feed_url} (${sub.id})`);
609
+ }
610
+ async function removeSubscription(args) {
611
+ if (hasFlag(args, "--help", "-h")) {
612
+ console.log(`shiori subscriptions remove - Remove a subscription
613
+
614
+ Usage: shiori subscriptions remove <id> [options]
615
+
616
+ Options:
617
+ --json Output raw JSON
618
+ --help, -h Show this help`);
619
+ return;
620
+ }
621
+ const id = getPositional(args);
622
+ if (!id) {
623
+ console.error("Usage: shiori subscriptions remove <id>");
624
+ process.exit(1);
625
+ }
626
+ const { data } = await api("DELETE", `/api/subscriptions/${id}`);
627
+ if (hasFlag(args, "--json")) {
628
+ console.log(JSON.stringify(data, null, 2));
629
+ return;
630
+ }
631
+ console.log("Subscription removed.");
632
+ }
633
+ async function syncSubscription(args) {
634
+ if (hasFlag(args, "--help", "-h")) {
635
+ console.log(`shiori subscriptions sync - Sync a subscription now
636
+
637
+ Usage: shiori subscriptions sync <id> [options]
638
+
639
+ Options:
640
+ --limit <n> Max items to sync (1-100)
641
+ --json Output raw JSON
642
+ --help, -h Show this help`);
643
+ return;
644
+ }
645
+ const id = getPositional(args);
646
+ if (!id) {
647
+ console.error("Usage: shiori subscriptions sync <id>");
648
+ process.exit(1);
649
+ }
650
+ const body = {};
651
+ const limit = getFlag(args, "--limit");
652
+ if (limit) body.limit = Number(limit);
653
+ const { data } = await api("POST", `/api/subscriptions/sync/${id}`, body);
654
+ if (hasFlag(args, "--json")) {
655
+ console.log(JSON.stringify(data, null, 2));
656
+ return;
657
+ }
658
+ console.log(`Synced: ${data.newItems} new, ${data.skipped} skipped, ${data.errors} errors`);
659
+ }
660
+
391
661
  // src/commands/trash.ts
392
- async function run7(args) {
662
+ async function run10(args) {
393
663
  if (hasFlag(args, "--help", "-h")) {
394
664
  console.log(`shiori trash - List or empty the trash
395
665
 
@@ -440,7 +710,7 @@ Trashed links: ${data.links.length} of ${data.total} total
440
710
  }
441
711
 
442
712
  // src/commands/update.ts
443
- async function run8(args) {
713
+ async function run11(args) {
444
714
  if (hasFlag(args, "--help", "-h")) {
445
715
  console.log(`shiori update - Update a link
446
716
 
@@ -472,7 +742,9 @@ Options:
472
742
  const title = getFlag(args, "--title");
473
743
  const summary = getFlag(args, "--summary");
474
744
  if (!title && !summary) {
475
- console.error("Provide at least one update flag: --read, --unread, --title, --summary, --restore");
745
+ console.error(
746
+ "Provide at least one update flag: --read, --unread, --title, --summary, --restore"
747
+ );
476
748
  process.exit(1);
477
749
  }
478
750
  body = {};
@@ -489,13 +761,14 @@ Options:
489
761
 
490
762
  // src/error-report.ts
491
763
  function reportError(command, error) {
492
- const message = error instanceof Error ? error.message : String(error);
764
+ const raw = error instanceof Error ? error.message : String(error);
765
+ const message = raw.replace(/shk_[a-zA-Z0-9]+/g, "shk_[REDACTED]");
493
766
  const baseUrl = getBaseUrl();
494
767
  fetch(`${baseUrl}/api/cli/report`, {
495
768
  method: "POST",
496
769
  headers: { "Content-Type": "application/json" },
497
770
  body: JSON.stringify({
498
- version: "0.1.0",
771
+ version: "0.3.0",
499
772
  command,
500
773
  error: message,
501
774
  platform: process.platform
@@ -512,8 +785,7 @@ async function checkForUpdate(currentVersion, isJson) {
512
785
  if (process.env.CI || process.env.NO_UPDATE_NOTIFIER || isJson) return;
513
786
  const config = readConfig();
514
787
  const now = Date.now();
515
- if (config.last_update_check && now - config.last_update_check < CHECK_INTERVAL_MS)
516
- return;
788
+ if (config.last_update_check && now - config.last_update_check < CHECK_INTERVAL_MS) return;
517
789
  try {
518
790
  const res = await fetch(REGISTRY_URL, {
519
791
  signal: AbortSignal.timeout(3e3)
@@ -537,13 +809,16 @@ async function checkForUpdate(currentVersion, isJson) {
537
809
  // src/index.ts
538
810
  var COMMANDS = {
539
811
  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" }
812
+ list: { run: run5, desc: "List saved links" },
813
+ search: { run: run8, desc: "Search saved links" },
814
+ get: { run: run4, desc: "Get a link by ID (includes content)" },
815
+ content: { run: run2, desc: "Print link content as markdown" },
816
+ save: { run: run7, desc: "Save a new link" },
817
+ update: { run: run11, desc: "Update a link" },
818
+ delete: { run: run3, desc: "Delete a link (move to trash)" },
819
+ trash: { run: run10, desc: "List or empty the trash" },
820
+ subscriptions: { run: run9, desc: "Manage RSS subscriptions" },
821
+ me: { run: run6, desc: "Show current user info" }
547
822
  };
548
823
  function printHelp() {
549
824
  console.log(`shiori - Manage your Shiori link library from the terminal
@@ -558,9 +833,11 @@ Options:
558
833
  --version, -v Show version
559
834
 
560
835
  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`);
836
+ shiori auth Authenticate with your API key
837
+ shiori list List your recent links
838
+ shiori save <url> Save a new link
839
+ shiori search <query> Search your links
840
+ shiori content <id> Print markdown content (pipe to other tools)`);
564
841
  }
565
842
  async function main() {
566
843
  const args = process.argv.slice(2);
@@ -570,7 +847,7 @@ async function main() {
570
847
  return;
571
848
  }
572
849
  if (command === "--version" || command === "-v") {
573
- console.log("0.1.0");
850
+ console.log("0.3.0");
574
851
  return;
575
852
  }
576
853
  const cmd = COMMANDS[command];
@@ -583,7 +860,7 @@ async function main() {
583
860
  const cmdArgs = args.slice(1);
584
861
  const isJson = cmdArgs.includes("--json");
585
862
  await cmd.run(cmdArgs);
586
- checkForUpdate("0.1.0", isJson).catch(() => {
863
+ checkForUpdate("0.3.0", isJson).catch(() => {
587
864
  });
588
865
  }
589
866
  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.3.0",
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"