@readwise/cli 0.5.1 → 0.5.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 CHANGED
@@ -125,6 +125,21 @@ Pipe results to `jq`:
125
125
  readwise reader-list-documents --limit 3 --json | jq '.results[].title'
126
126
  ```
127
127
 
128
+ ## Skills
129
+
130
+ Pre-built workflows your AI agent can run. Install them with one command:
131
+
132
+ ```bash
133
+ readwise skills install claude # or codex, opencode
134
+ readwise skills list # see all available skills
135
+ ```
136
+
137
+ Browse and contribute skills at [github.com/readwiseio/readwise-skills](https://github.com/readwiseio/readwise-skills).
138
+
139
+ ## Looking for MCP?
140
+
141
+ Using Claude Desktop, ChatGPT, or another AI app? Connect Readwise via MCP — no terminal needed. [Set up Readwise MCP →](https://readwise.io/mcp)
142
+
128
143
  ## How it works
129
144
 
130
145
  The CLI connects to the [Readwise MCP server](https://mcp2.readwise.io) internally, auto-discovers available tools, and exposes each one as a CLI command. The tool list is cached locally for 24 hours.
package/dist/tui/app.js CHANGED
@@ -387,6 +387,12 @@ function extractDocTitle(obj) {
387
387
  return "Untitled";
388
388
  }
389
389
  function extractDocSummary(obj) {
390
+ // Prefer first matched chunk text (from search results)
391
+ if (Array.isArray(obj.matches) && obj.matches.length > 0) {
392
+ const first = obj.matches[0];
393
+ if (first?.plaintext && typeof first.plaintext === "string")
394
+ return first.plaintext;
395
+ }
390
396
  for (const key of ["summary", "description", "note", "notes", "content"]) {
391
397
  const val = obj[key];
392
398
  if (val && typeof val === "string")
@@ -1333,7 +1339,45 @@ const SUCCESS_ICON = [
1333
1339
  function cardLine(text, innerW, borderFn) {
1334
1340
  return " " + borderFn("│") + " " + fitWidth(text, innerW) + " " + borderFn("│");
1335
1341
  }
1336
- function buildCardLines(card, ci, selected, cardWidth) {
1342
+ /** Build a regex that matches any individual word from the query (case-insensitive) */
1343
+ function searchTermRegex(query) {
1344
+ const words = query.trim().split(/\s+/).filter((w) => w.length >= 3);
1345
+ if (words.length === 0)
1346
+ return null;
1347
+ const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1348
+ return new RegExp(`(${escaped.join("|")})`, "gi");
1349
+ }
1350
+ /** Shift text so the first match is visible, returning a window of ~maxChars */
1351
+ function snippetAroundMatch(text, query, maxChars) {
1352
+ const re = searchTermRegex(query);
1353
+ if (!re)
1354
+ return text.slice(0, maxChars);
1355
+ const m = re.exec(text);
1356
+ if (!m)
1357
+ return text.slice(0, maxChars);
1358
+ const matchStart = m.index;
1359
+ // Start the window a bit before the match
1360
+ let start = Math.max(0, matchStart - Math.floor(maxChars / 4));
1361
+ // Snap forward to a word boundary so we don't cut mid-word
1362
+ if (start > 0) {
1363
+ const nextSpace = text.indexOf(" ", start);
1364
+ if (nextSpace !== -1 && nextSpace < matchStart) {
1365
+ start = nextSpace + 1;
1366
+ }
1367
+ }
1368
+ const snippet = text.slice(start, start + maxChars);
1369
+ // Only add leading ellipsis; trailing truncation is handled by the card renderer
1370
+ return start > 0 ? "…" + snippet : snippet;
1371
+ }
1372
+ /** Bold all occurrences of search terms in a line */
1373
+ function highlightTerms(line, query) {
1374
+ const re = searchTermRegex(query);
1375
+ if (!re)
1376
+ return line;
1377
+ // \x1b[22;1;3;37m = undim+bold+italic+white, \x1b[22;23;2;39m = unbold+unitalic+dim+default color
1378
+ return line.replace(re, (match) => `\x1b[22;1;3;37m${match}\x1b[22;23;2;39m`);
1379
+ }
1380
+ function buildCardLines(card, ci, selected, cardWidth, searchQuery) {
1337
1381
  const borderColor = selected ? style.cyan : style.dim;
1338
1382
  const innerW = Math.max(1, cardWidth - 4);
1339
1383
  const lines = [];
@@ -1383,8 +1427,19 @@ function buildCardLines(card, ci, selected, cardWidth) {
1383
1427
  const titleStyled = selected ? style.bold(style.cyan(titleText)) : style.bold(titleText);
1384
1428
  lines.push(cardLine(titleStyled, innerW, borderColor));
1385
1429
  if (card.summary) {
1386
- const summaryText = truncateVisible(card.summary, innerW);
1387
- lines.push(cardLine(style.dim(summaryText), innerW, borderColor));
1430
+ const snippet = searchQuery
1431
+ ? snippetAroundMatch(card.summary, searchQuery, innerW * 3)
1432
+ : card.summary;
1433
+ const wrapped = wrapText(snippet, innerW);
1434
+ const showLines = wrapped.slice(0, 3);
1435
+ if (wrapped.length > 3) {
1436
+ const last = showLines[2];
1437
+ showLines[2] = truncateVisible(last, innerW - 1) + "…";
1438
+ }
1439
+ for (const sl of showLines) {
1440
+ const highlighted = searchQuery ? highlightTerms(sl, searchQuery) : sl;
1441
+ lines.push(cardLine(style.dim(highlighted), innerW, borderColor));
1442
+ }
1388
1443
  }
1389
1444
  if (card.meta) {
1390
1445
  lines.push(cardLine(style.dim(truncateVisible(card.meta, innerW)), innerW, borderColor));
@@ -1404,10 +1459,12 @@ function renderCardView(state) {
1404
1459
  const countInfo = style.dim(` (${state.resultCursor + 1} of ${cards.length})`);
1405
1460
  content.push(" " + style.bold("Results") + countInfo);
1406
1461
  content.push("");
1462
+ // Extract search query from form values (try common field names)
1463
+ const searchQuery = state.formValues["query"] || state.formValues["vector_search_term"] || state.formValues["search"] || "";
1407
1464
  // Build all card lines
1408
1465
  const allLines = [];
1409
1466
  for (let ci = 0; ci < cards.length; ci++) {
1410
- const cardContentLines = buildCardLines(cards[ci], ci, ci === state.resultCursor, cardWidth);
1467
+ const cardContentLines = buildCardLines(cards[ci], ci, ci === state.resultCursor, cardWidth, searchQuery || undefined);
1411
1468
  for (const line of cardContentLines) {
1412
1469
  allLines.push({ line, cardIdx: ci });
1413
1470
  }
@@ -2408,7 +2465,8 @@ function cardLineCount(card, cardWidth) {
2408
2465
  const textLines = Math.min(wrapped.length, 6);
2409
2466
  return 2 + textLines + (card.note ? 1 : 0) + (card.meta ? 1 : 0); // top + text + note? + meta? + bottom
2410
2467
  }
2411
- return 2 + 1 + (card.summary ? 1 : 0) + (card.meta ? 1 : 0); // top + title + summary? + meta? + bottom
2468
+ const summaryLines = card.summary ? Math.min(wrapText(card.summary, 60).length, 3) : 0;
2469
+ return 2 + 1 + summaryLines + (card.meta ? 1 : 0); // top + title + summary + meta? + bottom
2412
2470
  }
2413
2471
  function computeCardScroll(cards, cursor, currentScroll, availableHeight) {
2414
2472
  const { innerWidth } = getBoxDimensions();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readwise/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Command-line interface for Readwise and Reader",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/app.ts CHANGED
@@ -445,6 +445,11 @@ function extractDocTitle(obj: Record<string, unknown>): string {
445
445
  }
446
446
 
447
447
  function extractDocSummary(obj: Record<string, unknown>): string {
448
+ // Prefer first matched chunk text (from search results)
449
+ if (Array.isArray(obj.matches) && obj.matches.length > 0) {
450
+ const first = obj.matches[0] as Record<string, unknown> | undefined;
451
+ if (first?.plaintext && typeof first.plaintext === "string") return first.plaintext;
452
+ }
448
453
  for (const key of ["summary", "description", "note", "notes", "content"]) {
449
454
  const val = obj[key];
450
455
  if (val && typeof val === "string") return val;
@@ -1420,7 +1425,44 @@ function cardLine(text: string, innerW: number, borderFn: (s: string) => string)
1420
1425
  return " " + borderFn("│") + " " + fitWidth(text, innerW) + " " + borderFn("│");
1421
1426
  }
1422
1427
 
1423
- function buildCardLines(card: CardItem, ci: number, selected: boolean, cardWidth: number): string[] {
1428
+ /** Build a regex that matches any individual word from the query (case-insensitive) */
1429
+ function searchTermRegex(query: string): RegExp | null {
1430
+ const words = query.trim().split(/\s+/).filter((w) => w.length >= 3);
1431
+ if (words.length === 0) return null;
1432
+ const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1433
+ return new RegExp(`(${escaped.join("|")})`, "gi");
1434
+ }
1435
+
1436
+ /** Shift text so the first match is visible, returning a window of ~maxChars */
1437
+ function snippetAroundMatch(text: string, query: string, maxChars: number): string {
1438
+ const re = searchTermRegex(query);
1439
+ if (!re) return text.slice(0, maxChars);
1440
+ const m = re.exec(text);
1441
+ if (!m) return text.slice(0, maxChars);
1442
+ const matchStart = m.index;
1443
+ // Start the window a bit before the match
1444
+ let start = Math.max(0, matchStart - Math.floor(maxChars / 4));
1445
+ // Snap forward to a word boundary so we don't cut mid-word
1446
+ if (start > 0) {
1447
+ const nextSpace = text.indexOf(" ", start);
1448
+ if (nextSpace !== -1 && nextSpace < matchStart) {
1449
+ start = nextSpace + 1;
1450
+ }
1451
+ }
1452
+ const snippet = text.slice(start, start + maxChars);
1453
+ // Only add leading ellipsis; trailing truncation is handled by the card renderer
1454
+ return start > 0 ? "…" + snippet : snippet;
1455
+ }
1456
+
1457
+ /** Bold all occurrences of search terms in a line */
1458
+ function highlightTerms(line: string, query: string): string {
1459
+ const re = searchTermRegex(query);
1460
+ if (!re) return line;
1461
+ // \x1b[22;1;3;37m = undim+bold+italic+white, \x1b[22;23;2;39m = unbold+unitalic+dim+default color
1462
+ return line.replace(re, (match) => `\x1b[22;1;3;37m${match}\x1b[22;23;2;39m`);
1463
+ }
1464
+
1465
+ function buildCardLines(card: CardItem, ci: number, selected: boolean, cardWidth: number, searchQuery?: string): string[] {
1424
1466
  const borderColor = selected ? style.cyan : style.dim;
1425
1467
  const innerW = Math.max(1, cardWidth - 4);
1426
1468
  const lines: string[] = [];
@@ -1471,8 +1513,19 @@ function buildCardLines(card: CardItem, ci: number, selected: boolean, cardWidth
1471
1513
  lines.push(cardLine(titleStyled, innerW, borderColor));
1472
1514
 
1473
1515
  if (card.summary) {
1474
- const summaryText = truncateVisible(card.summary, innerW);
1475
- lines.push(cardLine(style.dim(summaryText), innerW, borderColor));
1516
+ const snippet = searchQuery
1517
+ ? snippetAroundMatch(card.summary, searchQuery, innerW * 3)
1518
+ : card.summary;
1519
+ const wrapped = wrapText(snippet, innerW);
1520
+ const showLines = wrapped.slice(0, 3);
1521
+ if (wrapped.length > 3) {
1522
+ const last = showLines[2]!;
1523
+ showLines[2] = truncateVisible(last, innerW - 1) + "…";
1524
+ }
1525
+ for (const sl of showLines) {
1526
+ const highlighted = searchQuery ? highlightTerms(sl, searchQuery) : sl;
1527
+ lines.push(cardLine(style.dim(highlighted), innerW, borderColor));
1528
+ }
1476
1529
  }
1477
1530
 
1478
1531
  if (card.meta) {
@@ -1498,10 +1551,13 @@ function renderCardView(state: AppState): string[] {
1498
1551
  content.push(" " + style.bold("Results") + countInfo);
1499
1552
  content.push("");
1500
1553
 
1554
+ // Extract search query from form values (try common field names)
1555
+ const searchQuery = state.formValues["query"] || state.formValues["vector_search_term"] || state.formValues["search"] || "";
1556
+
1501
1557
  // Build all card lines
1502
1558
  const allLines: { line: string; cardIdx: number }[] = [];
1503
1559
  for (let ci = 0; ci < cards.length; ci++) {
1504
- const cardContentLines = buildCardLines(cards[ci]!, ci, ci === state.resultCursor, cardWidth);
1560
+ const cardContentLines = buildCardLines(cards[ci]!, ci, ci === state.resultCursor, cardWidth, searchQuery || undefined);
1505
1561
  for (const line of cardContentLines) {
1506
1562
  allLines.push({ line, cardIdx: ci });
1507
1563
  }
@@ -2526,7 +2582,8 @@ function cardLineCount(card: CardItem, cardWidth: number): number {
2526
2582
  const textLines = Math.min(wrapped.length, 6);
2527
2583
  return 2 + textLines + (card.note ? 1 : 0) + (card.meta ? 1 : 0); // top + text + note? + meta? + bottom
2528
2584
  }
2529
- return 2 + 1 + (card.summary ? 1 : 0) + (card.meta ? 1 : 0); // top + title + summary? + meta? + bottom
2585
+ const summaryLines = card.summary ? Math.min(wrapText(card.summary, 60).length, 3) : 0;
2586
+ return 2 + 1 + summaryLines + (card.meta ? 1 : 0); // top + title + summary + meta? + bottom
2530
2587
  }
2531
2588
 
2532
2589
  function computeCardScroll(cards: CardItem[], cursor: number, currentScroll: number, availableHeight: number): number {