@readwise/cli 0.5.0 → 0.5.2
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 +24 -9
- package/dist/index.js +4 -1
- package/dist/skills.d.ts +2 -0
- package/dist/skills.js +246 -0
- package/dist/tui/app.js +384 -10
- package/package.json +1 -1
- package/src/index.ts +5 -1
- package/src/skills.ts +260 -0
- package/src/tui/app.ts +424 -12
package/dist/tui/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
1
2
|
import { resolveProperty } from "../commands.js";
|
|
2
3
|
import { callTool } from "../mcp.js";
|
|
3
4
|
import { ensureValidToken } from "../auth.js";
|
|
@@ -288,6 +289,182 @@ function wrapText(text, width) {
|
|
|
288
289
|
lines.push(current);
|
|
289
290
|
return lines.length > 0 ? lines : [""];
|
|
290
291
|
}
|
|
292
|
+
// --- Card parsing helpers ---
|
|
293
|
+
function extractCards(data) {
|
|
294
|
+
let items;
|
|
295
|
+
if (Array.isArray(data)) {
|
|
296
|
+
items = data;
|
|
297
|
+
}
|
|
298
|
+
else if (typeof data === "object" && data !== null) {
|
|
299
|
+
// Look for a "results" array or similar
|
|
300
|
+
const obj = data;
|
|
301
|
+
const arrayKey = Object.keys(obj).find((k) => Array.isArray(obj[k]) && obj[k].length > 0);
|
|
302
|
+
if (arrayKey) {
|
|
303
|
+
items = obj[arrayKey];
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (items.length === 0)
|
|
313
|
+
return null;
|
|
314
|
+
// Only works for arrays of objects
|
|
315
|
+
if (typeof items[0] !== "object" || items[0] === null || Array.isArray(items[0]))
|
|
316
|
+
return null;
|
|
317
|
+
const cards = items.map((item) => {
|
|
318
|
+
const obj = item;
|
|
319
|
+
const kind = isHighlightObj(obj) ? "highlight" : "document";
|
|
320
|
+
if (kind === "highlight") {
|
|
321
|
+
const attrs = (typeof obj.attributes === "object" && obj.attributes !== null)
|
|
322
|
+
? obj.attributes : null;
|
|
323
|
+
return {
|
|
324
|
+
kind,
|
|
325
|
+
title: str(attrs?.document_title || obj.title || obj.readable_title || ""),
|
|
326
|
+
summary: str(attrs?.highlight_plaintext || obj.text || obj.summary || obj.content || ""),
|
|
327
|
+
note: str(attrs?.highlight_note || obj.note || obj.notes || ""),
|
|
328
|
+
meta: extractHighlightMeta(obj),
|
|
329
|
+
url: obj.id ? `https://readwise.io/open/${obj.id}` : extractCardUrl(obj),
|
|
330
|
+
raw: obj,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
kind,
|
|
335
|
+
title: extractDocTitle(obj),
|
|
336
|
+
summary: extractDocSummary(obj),
|
|
337
|
+
note: "",
|
|
338
|
+
meta: extractDocMeta(obj),
|
|
339
|
+
url: extractCardUrl(obj),
|
|
340
|
+
raw: obj,
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
// Skip card view if most items have no meaningful content (e.g. just URLs)
|
|
344
|
+
const hasContent = cards.filter((c) => c.summary || c.note || (c.kind === "document" && c.title !== "Untitled" && !c.raw.url?.toString().includes(c.title)));
|
|
345
|
+
if (hasContent.length < cards.length / 2)
|
|
346
|
+
return null;
|
|
347
|
+
return cards;
|
|
348
|
+
}
|
|
349
|
+
function str(val) {
|
|
350
|
+
if (val === null || val === undefined)
|
|
351
|
+
return "";
|
|
352
|
+
return String(val);
|
|
353
|
+
}
|
|
354
|
+
function isHighlightObj(obj) {
|
|
355
|
+
// Reader docs with category "highlight"
|
|
356
|
+
if (obj.category === "highlight")
|
|
357
|
+
return true;
|
|
358
|
+
// Readwise search highlights: nested attributes with highlight_plaintext
|
|
359
|
+
const attrs = obj.attributes;
|
|
360
|
+
if (typeof attrs === "object" && attrs !== null && "highlight_plaintext" in attrs)
|
|
361
|
+
return true;
|
|
362
|
+
// Readwise highlights: have text + highlight-specific fields
|
|
363
|
+
if (typeof obj.text === "string" &&
|
|
364
|
+
("highlighted_at" in obj || "color" in obj || "book_id" in obj || "location_type" in obj)) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
// Has text + note fields (common highlight shape even without highlighted_at)
|
|
368
|
+
if (typeof obj.text === "string" && "note" in obj)
|
|
369
|
+
return true;
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
// --- Document card helpers ---
|
|
373
|
+
function extractDocTitle(obj) {
|
|
374
|
+
for (const key of ["title", "readable_title", "name"]) {
|
|
375
|
+
const val = obj[key];
|
|
376
|
+
if (val && typeof val === "string" && !String(val).startsWith("http"))
|
|
377
|
+
return val;
|
|
378
|
+
}
|
|
379
|
+
// Last resort: show domain from URL
|
|
380
|
+
const url = str(obj.url || obj.source_url);
|
|
381
|
+
if (url) {
|
|
382
|
+
try {
|
|
383
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
384
|
+
}
|
|
385
|
+
catch { /* */ }
|
|
386
|
+
}
|
|
387
|
+
return "Untitled";
|
|
388
|
+
}
|
|
389
|
+
function extractDocSummary(obj) {
|
|
390
|
+
for (const key of ["summary", "description", "note", "notes", "content"]) {
|
|
391
|
+
const val = obj[key];
|
|
392
|
+
if (val && typeof val === "string")
|
|
393
|
+
return val;
|
|
394
|
+
}
|
|
395
|
+
return "";
|
|
396
|
+
}
|
|
397
|
+
function extractDocMeta(obj) {
|
|
398
|
+
const parts = [];
|
|
399
|
+
const siteName = str(obj.site_name);
|
|
400
|
+
if (siteName)
|
|
401
|
+
parts.push(siteName);
|
|
402
|
+
const author = str(obj.author);
|
|
403
|
+
if (author && author !== siteName)
|
|
404
|
+
parts.push(author);
|
|
405
|
+
const category = str(obj.category);
|
|
406
|
+
if (category)
|
|
407
|
+
parts.push(category);
|
|
408
|
+
const wordCount = Number(obj.word_count);
|
|
409
|
+
if (wordCount > 0) {
|
|
410
|
+
const mins = Math.ceil(wordCount / 250);
|
|
411
|
+
parts.push(`${mins} min`);
|
|
412
|
+
}
|
|
413
|
+
const progress = Number(obj.reading_progress);
|
|
414
|
+
if (progress > 0 && progress < 1) {
|
|
415
|
+
parts.push(`${Math.round(progress * 100)}% read`);
|
|
416
|
+
}
|
|
417
|
+
else if (progress >= 1) {
|
|
418
|
+
parts.push("finished");
|
|
419
|
+
}
|
|
420
|
+
const date = str(obj.created_at || obj.saved_at || obj.published_date);
|
|
421
|
+
if (date) {
|
|
422
|
+
const d = new Date(date);
|
|
423
|
+
if (!isNaN(d.getTime())) {
|
|
424
|
+
parts.push(d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return parts.join(" · ");
|
|
428
|
+
}
|
|
429
|
+
// --- Highlight card helpers ---
|
|
430
|
+
function extractHighlightMeta(obj) {
|
|
431
|
+
const attrs = (typeof obj.attributes === "object" && obj.attributes !== null)
|
|
432
|
+
? obj.attributes : null;
|
|
433
|
+
const parts = [];
|
|
434
|
+
// Source book/article author
|
|
435
|
+
const author = str(attrs?.document_author || obj.author || obj.book_author);
|
|
436
|
+
if (author && !author.startsWith("http"))
|
|
437
|
+
parts.push(author);
|
|
438
|
+
// Category
|
|
439
|
+
const category = str(attrs?.document_category || obj.category);
|
|
440
|
+
if (category)
|
|
441
|
+
parts.push(category);
|
|
442
|
+
const color = str(obj.color);
|
|
443
|
+
if (color)
|
|
444
|
+
parts.push(color);
|
|
445
|
+
// Tags (from attributes or top-level)
|
|
446
|
+
const tags = attrs?.highlight_tags || obj.tags;
|
|
447
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
448
|
+
const tagNames = tags.map((t) => typeof t === "object" && t !== null ? str(t.name) : str(t)).filter(Boolean);
|
|
449
|
+
if (tagNames.length > 0)
|
|
450
|
+
parts.push(tagNames.join(", "));
|
|
451
|
+
}
|
|
452
|
+
const date = str(obj.highlighted_at || obj.created_at);
|
|
453
|
+
if (date) {
|
|
454
|
+
const d = new Date(date);
|
|
455
|
+
if (!isNaN(d.getTime())) {
|
|
456
|
+
parts.push(d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return parts.join(" · ");
|
|
460
|
+
}
|
|
461
|
+
function extractCardUrl(obj) {
|
|
462
|
+
for (const key of ["url", "source_url", "reader_url", "readwise_url"]) {
|
|
463
|
+
if (obj[key] && typeof obj[key] === "string")
|
|
464
|
+
return obj[key];
|
|
465
|
+
}
|
|
466
|
+
return "";
|
|
467
|
+
}
|
|
291
468
|
// --- Word boundary helpers ---
|
|
292
469
|
function prevWordBoundary(buf, pos) {
|
|
293
470
|
if (pos <= 0)
|
|
@@ -1153,6 +1330,111 @@ const SUCCESS_ICON = [
|
|
|
1153
1330
|
"╚██████╔╝██║ ██╗",
|
|
1154
1331
|
" ╚═════╝ ╚═╝ ╚═╝",
|
|
1155
1332
|
];
|
|
1333
|
+
function cardLine(text, innerW, borderFn) {
|
|
1334
|
+
return " " + borderFn("│") + " " + fitWidth(text, innerW) + " " + borderFn("│");
|
|
1335
|
+
}
|
|
1336
|
+
function buildCardLines(card, ci, selected, cardWidth) {
|
|
1337
|
+
const borderColor = selected ? style.cyan : style.dim;
|
|
1338
|
+
const innerW = Math.max(1, cardWidth - 4);
|
|
1339
|
+
const lines = [];
|
|
1340
|
+
lines.push(" " + borderColor("╭" + "─".repeat(cardWidth - 2) + "╮"));
|
|
1341
|
+
if (card.kind === "highlight") {
|
|
1342
|
+
// Highlight card: quote-style passage, optional note, meta
|
|
1343
|
+
const quotePrefix = "\u201c ";
|
|
1344
|
+
const quoteSuffix = "\u201d";
|
|
1345
|
+
const passage = card.summary || "\u2026";
|
|
1346
|
+
const maxQuoteW = innerW - quotePrefix.length;
|
|
1347
|
+
const wrapped = wrapText(passage, maxQuoteW);
|
|
1348
|
+
// Cap at 6 lines
|
|
1349
|
+
const showLines = wrapped.slice(0, 6);
|
|
1350
|
+
if (wrapped.length > 6) {
|
|
1351
|
+
const last = showLines[5];
|
|
1352
|
+
showLines[5] = truncateVisible(last, maxQuoteW - 1) + "…";
|
|
1353
|
+
}
|
|
1354
|
+
for (let i = 0; i < showLines.length; i++) {
|
|
1355
|
+
let lineText;
|
|
1356
|
+
if (i === 0) {
|
|
1357
|
+
lineText = quotePrefix + showLines[i];
|
|
1358
|
+
if (showLines.length === 1)
|
|
1359
|
+
lineText += quoteSuffix;
|
|
1360
|
+
}
|
|
1361
|
+
else if (i === showLines.length - 1) {
|
|
1362
|
+
lineText = " " + showLines[i] + quoteSuffix;
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
lineText = " " + showLines[i];
|
|
1366
|
+
}
|
|
1367
|
+
const styled = selected ? style.cyan(lineText) : lineText;
|
|
1368
|
+
lines.push(cardLine(styled, innerW, borderColor));
|
|
1369
|
+
}
|
|
1370
|
+
// Note (if present)
|
|
1371
|
+
if (card.note) {
|
|
1372
|
+
const noteText = "✏ " + truncateVisible(card.note, innerW - 2);
|
|
1373
|
+
lines.push(cardLine(style.yellow(noteText), innerW, borderColor));
|
|
1374
|
+
}
|
|
1375
|
+
// Meta line
|
|
1376
|
+
if (card.meta) {
|
|
1377
|
+
lines.push(cardLine(style.dim(truncateVisible(card.meta, innerW)), innerW, borderColor));
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
// Document card: title, summary, meta
|
|
1382
|
+
const titleText = truncateVisible(card.title || "Untitled", innerW);
|
|
1383
|
+
const titleStyled = selected ? style.bold(style.cyan(titleText)) : style.bold(titleText);
|
|
1384
|
+
lines.push(cardLine(titleStyled, innerW, borderColor));
|
|
1385
|
+
if (card.summary) {
|
|
1386
|
+
const summaryText = truncateVisible(card.summary, innerW);
|
|
1387
|
+
lines.push(cardLine(style.dim(summaryText), innerW, borderColor));
|
|
1388
|
+
}
|
|
1389
|
+
if (card.meta) {
|
|
1390
|
+
lines.push(cardLine(style.dim(truncateVisible(card.meta, innerW)), innerW, borderColor));
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
lines.push(" " + borderColor("╰" + "─".repeat(cardWidth - 2) + "╯"));
|
|
1394
|
+
return lines;
|
|
1395
|
+
}
|
|
1396
|
+
function renderCardView(state) {
|
|
1397
|
+
const { contentHeight, innerWidth } = getBoxDimensions();
|
|
1398
|
+
const tool = state.selectedTool;
|
|
1399
|
+
const title = tool ? humanLabel(tool.name, toolPrefix(tool)) : "";
|
|
1400
|
+
const cards = state.resultCards;
|
|
1401
|
+
const cardWidth = Math.min(innerWidth - 4, 72);
|
|
1402
|
+
const content = [];
|
|
1403
|
+
// Header with count
|
|
1404
|
+
const countInfo = style.dim(` (${state.resultCursor + 1} of ${cards.length})`);
|
|
1405
|
+
content.push(" " + style.bold("Results") + countInfo);
|
|
1406
|
+
content.push("");
|
|
1407
|
+
// Build all card lines
|
|
1408
|
+
const allLines = [];
|
|
1409
|
+
for (let ci = 0; ci < cards.length; ci++) {
|
|
1410
|
+
const cardContentLines = buildCardLines(cards[ci], ci, ci === state.resultCursor, cardWidth);
|
|
1411
|
+
for (const line of cardContentLines) {
|
|
1412
|
+
allLines.push({ line, cardIdx: ci });
|
|
1413
|
+
}
|
|
1414
|
+
if (ci < cards.length - 1) {
|
|
1415
|
+
allLines.push({ line: "", cardIdx: ci });
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
// Scroll so selected card is visible
|
|
1419
|
+
const availableHeight = Math.max(1, contentHeight - content.length);
|
|
1420
|
+
const scroll = state.resultCardScroll;
|
|
1421
|
+
const visible = allLines.slice(scroll, scroll + availableHeight);
|
|
1422
|
+
for (const entry of visible) {
|
|
1423
|
+
content.push(entry.line);
|
|
1424
|
+
}
|
|
1425
|
+
const hasUrl = cards[state.resultCursor]?.url;
|
|
1426
|
+
const footerParts = ["↑↓ navigate"];
|
|
1427
|
+
if (hasUrl)
|
|
1428
|
+
footerParts.push("enter open");
|
|
1429
|
+
footerParts.push("esc back", "q quit");
|
|
1430
|
+
return renderLayout({
|
|
1431
|
+
breadcrumb: style.boldYellow("Readwise") + style.dim(" › ") + style.bold(title) + style.dim(" › results"),
|
|
1432
|
+
content,
|
|
1433
|
+
footer: state.quitConfirm
|
|
1434
|
+
? style.yellow("Press q again to quit")
|
|
1435
|
+
: style.dim(footerParts.join(" · ")),
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1156
1438
|
function renderResults(state) {
|
|
1157
1439
|
const { contentHeight, innerWidth } = getBoxDimensions();
|
|
1158
1440
|
const tool = state.selectedTool;
|
|
@@ -1160,6 +1442,10 @@ function renderResults(state) {
|
|
|
1160
1442
|
const isError = !!state.error;
|
|
1161
1443
|
const isEmptyList = !isError && state.result === EMPTY_LIST_SENTINEL;
|
|
1162
1444
|
const isEmpty = !isError && !isEmptyList && !state.result.trim();
|
|
1445
|
+
// Card view for list results
|
|
1446
|
+
if (!isError && !isEmptyList && !isEmpty && state.resultCards.length > 0) {
|
|
1447
|
+
return renderCardView(state);
|
|
1448
|
+
}
|
|
1163
1449
|
// No results screen for empty lists
|
|
1164
1450
|
if (isEmptyList) {
|
|
1165
1451
|
const ghost = [
|
|
@@ -2113,10 +2399,40 @@ function handleFormEditInput(state, key) {
|
|
|
2113
2399
|
}
|
|
2114
2400
|
return state;
|
|
2115
2401
|
}
|
|
2402
|
+
function cardLineCount(card, cardWidth) {
|
|
2403
|
+
const innerW = Math.max(1, cardWidth - 4);
|
|
2404
|
+
if (card.kind === "highlight") {
|
|
2405
|
+
const quoteW = innerW - 2; // account for quote prefix
|
|
2406
|
+
const passage = card.summary || "\u2026";
|
|
2407
|
+
const wrapped = wrapText(passage, quoteW);
|
|
2408
|
+
const textLines = Math.min(wrapped.length, 6);
|
|
2409
|
+
return 2 + textLines + (card.note ? 1 : 0) + (card.meta ? 1 : 0); // top + text + note? + meta? + bottom
|
|
2410
|
+
}
|
|
2411
|
+
return 2 + 1 + (card.summary ? 1 : 0) + (card.meta ? 1 : 0); // top + title + summary? + meta? + bottom
|
|
2412
|
+
}
|
|
2413
|
+
function computeCardScroll(cards, cursor, currentScroll, availableHeight) {
|
|
2414
|
+
const { innerWidth } = getBoxDimensions();
|
|
2415
|
+
const cardWidth = Math.min(innerWidth - 4, 72);
|
|
2416
|
+
let lineStart = 0;
|
|
2417
|
+
for (let ci = 0; ci < cards.length; ci++) {
|
|
2418
|
+
const card = cards[ci];
|
|
2419
|
+
const height = cardLineCount(card, cardWidth);
|
|
2420
|
+
const spacing = ci < cards.length - 1 ? 1 : 0;
|
|
2421
|
+
if (ci === cursor) {
|
|
2422
|
+
const lineEnd = lineStart + height + spacing;
|
|
2423
|
+
if (lineStart < currentScroll)
|
|
2424
|
+
return lineStart;
|
|
2425
|
+
if (lineEnd > currentScroll + availableHeight)
|
|
2426
|
+
return Math.max(0, lineEnd - availableHeight);
|
|
2427
|
+
return currentScroll;
|
|
2428
|
+
}
|
|
2429
|
+
lineStart += height + spacing;
|
|
2430
|
+
}
|
|
2431
|
+
return currentScroll;
|
|
2432
|
+
}
|
|
2116
2433
|
function handleResultsInput(state, key) {
|
|
2117
2434
|
const { contentHeight } = getBoxDimensions();
|
|
2118
|
-
const
|
|
2119
|
-
const visibleCount = Math.max(1, contentHeight - 3);
|
|
2435
|
+
const inCardMode = state.resultCards.length > 0 && !state.error;
|
|
2120
2436
|
if (key.ctrl && key.name === "c") {
|
|
2121
2437
|
if (state.quitConfirm)
|
|
2122
2438
|
return "exit";
|
|
@@ -2129,7 +2445,7 @@ function handleResultsInput(state, key) {
|
|
|
2129
2445
|
}
|
|
2130
2446
|
// Any other key cancels quit confirm
|
|
2131
2447
|
const s = state.quitConfirm ? { ...state, quitConfirm: false } : state;
|
|
2132
|
-
const resultsClear = { result: "", error: "", resultScroll: 0, resultScrollX: 0 };
|
|
2448
|
+
const resultsClear = { result: "", error: "", resultScroll: 0, resultScrollX: 0, resultCards: [], resultCursor: 0, resultCardScroll: 0 };
|
|
2133
2449
|
const goBack = () => {
|
|
2134
2450
|
const isEmpty = !s.error && s.result !== EMPTY_LIST_SENTINEL && !s.result.trim();
|
|
2135
2451
|
const hasParams = !isEmpty && s.selectedTool && Object.keys(s.selectedTool.inputSchema.properties || {}).length > 0;
|
|
@@ -2145,7 +2461,51 @@ function handleResultsInput(state, key) {
|
|
|
2145
2461
|
}
|
|
2146
2462
|
return { ...s, ...commandListReset(s.tools), ...resultsClear };
|
|
2147
2463
|
};
|
|
2148
|
-
if (key.name === "
|
|
2464
|
+
if (key.name === "escape") {
|
|
2465
|
+
return goBack();
|
|
2466
|
+
}
|
|
2467
|
+
if (inCardMode) {
|
|
2468
|
+
const cards = s.resultCards;
|
|
2469
|
+
// 2 lines used by header + blank
|
|
2470
|
+
const availableHeight = Math.max(1, contentHeight - 2);
|
|
2471
|
+
if (key.name === "return") {
|
|
2472
|
+
const card = cards[s.resultCursor];
|
|
2473
|
+
if (card?.url)
|
|
2474
|
+
return "openUrl";
|
|
2475
|
+
return goBack();
|
|
2476
|
+
}
|
|
2477
|
+
if (key.name === "up") {
|
|
2478
|
+
if (s.resultCursor > 0) {
|
|
2479
|
+
const newCursor = s.resultCursor - 1;
|
|
2480
|
+
const newScroll = computeCardScroll(cards, newCursor, s.resultCardScroll, availableHeight);
|
|
2481
|
+
return { ...s, resultCursor: newCursor, resultCardScroll: newScroll };
|
|
2482
|
+
}
|
|
2483
|
+
return s;
|
|
2484
|
+
}
|
|
2485
|
+
if (key.name === "down") {
|
|
2486
|
+
if (s.resultCursor < cards.length - 1) {
|
|
2487
|
+
const newCursor = s.resultCursor + 1;
|
|
2488
|
+
const newScroll = computeCardScroll(cards, newCursor, s.resultCardScroll, availableHeight);
|
|
2489
|
+
return { ...s, resultCursor: newCursor, resultCardScroll: newScroll };
|
|
2490
|
+
}
|
|
2491
|
+
return s;
|
|
2492
|
+
}
|
|
2493
|
+
if (key.name === "pageup") {
|
|
2494
|
+
const newCursor = Math.max(0, s.resultCursor - 5);
|
|
2495
|
+
const newScroll = computeCardScroll(cards, newCursor, s.resultCardScroll, availableHeight);
|
|
2496
|
+
return { ...s, resultCursor: newCursor, resultCardScroll: newScroll };
|
|
2497
|
+
}
|
|
2498
|
+
if (key.name === "pagedown") {
|
|
2499
|
+
const newCursor = Math.min(cards.length - 1, s.resultCursor + 5);
|
|
2500
|
+
const newScroll = computeCardScroll(cards, newCursor, s.resultCardScroll, availableHeight);
|
|
2501
|
+
return { ...s, resultCursor: newCursor, resultCardScroll: newScroll };
|
|
2502
|
+
}
|
|
2503
|
+
return s;
|
|
2504
|
+
}
|
|
2505
|
+
// Text mode fallback
|
|
2506
|
+
const contentLines = (state.error || state.result).split("\n");
|
|
2507
|
+
const visibleCount = Math.max(1, contentHeight - 3);
|
|
2508
|
+
if (key.name === "return") {
|
|
2149
2509
|
return goBack();
|
|
2150
2510
|
}
|
|
2151
2511
|
if (key.name === "up") {
|
|
@@ -2310,31 +2670,34 @@ async function executeTool(state) {
|
|
|
2310
2670
|
const res = await callTool(token, authType, tool.name, args);
|
|
2311
2671
|
if (res.isError) {
|
|
2312
2672
|
const errMsg = res.content.map((c) => c.text || "").filter(Boolean).join("\n");
|
|
2313
|
-
return { ...state, view: "results", error: errMsg || "Unknown error", result: "", resultScroll: 0, resultScrollX: 0 };
|
|
2673
|
+
return { ...state, view: "results", error: errMsg || "Unknown error", result: "", resultScroll: 0, resultScrollX: 0, resultCards: [], resultCursor: 0, resultCardScroll: 0 };
|
|
2314
2674
|
}
|
|
2315
2675
|
const text = res.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
|
|
2316
2676
|
// Check structuredContent if text content is empty
|
|
2317
2677
|
const structured = res.structuredContent;
|
|
2318
2678
|
let formatted;
|
|
2319
2679
|
let emptyList = false;
|
|
2680
|
+
let parsedData = undefined;
|
|
2320
2681
|
if (!text && structured !== undefined) {
|
|
2682
|
+
parsedData = structured;
|
|
2321
2683
|
emptyList = isEmptyListResult(structured);
|
|
2322
2684
|
formatted = formatJsonPretty(structured);
|
|
2323
2685
|
}
|
|
2324
2686
|
else {
|
|
2325
2687
|
try {
|
|
2326
|
-
|
|
2327
|
-
emptyList = isEmptyListResult(
|
|
2328
|
-
formatted = formatJsonPretty(
|
|
2688
|
+
parsedData = JSON.parse(text);
|
|
2689
|
+
emptyList = isEmptyListResult(parsedData);
|
|
2690
|
+
formatted = formatJsonPretty(parsedData);
|
|
2329
2691
|
}
|
|
2330
2692
|
catch {
|
|
2331
2693
|
formatted = text;
|
|
2332
2694
|
}
|
|
2333
2695
|
}
|
|
2334
|
-
|
|
2696
|
+
const cards = parsedData !== undefined ? extractCards(parsedData) || [] : [];
|
|
2697
|
+
return { ...state, view: "results", result: emptyList ? EMPTY_LIST_SENTINEL : formatted, error: "", resultScroll: 0, resultScrollX: 0, resultCards: cards, resultCursor: 0, resultCardScroll: 0 };
|
|
2335
2698
|
}
|
|
2336
2699
|
catch (err) {
|
|
2337
|
-
return { ...state, view: "results", error: err.message, result: "", resultScroll: 0, resultScrollX: 0 };
|
|
2700
|
+
return { ...state, view: "results", error: err.message, result: "", resultScroll: 0, resultScrollX: 0, resultCards: [], resultCursor: 0, resultCardScroll: 0 };
|
|
2338
2701
|
}
|
|
2339
2702
|
}
|
|
2340
2703
|
// --- Main loop ---
|
|
@@ -2373,6 +2736,9 @@ export async function runApp(tools) {
|
|
|
2373
2736
|
error: "",
|
|
2374
2737
|
resultScroll: 0,
|
|
2375
2738
|
resultScrollX: 0,
|
|
2739
|
+
resultCards: [],
|
|
2740
|
+
resultCursor: 0,
|
|
2741
|
+
resultCardScroll: 0,
|
|
2376
2742
|
spinnerFrame: 0,
|
|
2377
2743
|
};
|
|
2378
2744
|
paint(renderState(state));
|
|
@@ -2424,6 +2790,14 @@ export async function runApp(tools) {
|
|
|
2424
2790
|
cleanup();
|
|
2425
2791
|
return;
|
|
2426
2792
|
}
|
|
2793
|
+
if (result === "openUrl") {
|
|
2794
|
+
const card = state.resultCards[state.resultCursor];
|
|
2795
|
+
if (card?.url) {
|
|
2796
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2797
|
+
exec(`${cmd} ${JSON.stringify(card.url)}`);
|
|
2798
|
+
}
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2427
2801
|
if (result === "submit") {
|
|
2428
2802
|
state = { ...state, view: "loading", spinnerFrame: 0 };
|
|
2429
2803
|
paint(renderState(state));
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getTools } from "./mcp.js";
|
|
|
6
6
|
import { registerTools } from "./commands.js";
|
|
7
7
|
import { loadConfig } from "./config.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
9
|
+
import { registerSkillsCommands } from "./skills.js";
|
|
9
10
|
|
|
10
11
|
function readHiddenInput(prompt: string): Promise<string> {
|
|
11
12
|
return new Promise((resolve, reject) => {
|
|
@@ -116,8 +117,11 @@ async function main() {
|
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// Register skills commands (works without auth)
|
|
121
|
+
registerSkillsCommands(program);
|
|
122
|
+
|
|
119
123
|
// If not authenticated and trying a non-login command, tell user to log in
|
|
120
|
-
if (!config.access_token && hasSubcommand && positionalArgs[0] !== "login" && positionalArgs[0] !== "login-with-token") {
|
|
124
|
+
if (!config.access_token && hasSubcommand && positionalArgs[0] !== "login" && positionalArgs[0] !== "login-with-token" && positionalArgs[0] !== "skills") {
|
|
121
125
|
process.stderr.write("\x1b[31mNot logged in.\x1b[0m Run `readwise login` or `readwise login-with-token` to authenticate.\n");
|
|
122
126
|
process.exitCode = 1;
|
|
123
127
|
return;
|