@readwise/cli 0.3.1 → 0.5.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.
package/dist/tui/term.js CHANGED
@@ -17,11 +17,15 @@ export const style = {
17
17
  // --- Screen control ---
18
18
  export function enterFullScreen() {
19
19
  process.stdout.write(`${ESC}[?1049h`); // alternate screen buffer
20
+ process.stdout.write(`${ESC}[?2004h`); // enable bracketed paste mode
21
+ process.stdout.write(`${ESC}[>1u`); // enable kitty keyboard protocol (disambiguate mode)
20
22
  process.stdout.write(`${ESC}[?25l`); // hide cursor
21
23
  process.stdout.write(`${ESC}[H`); // cursor home
22
24
  }
23
25
  export function exitFullScreen() {
24
26
  process.stdout.write(`${ESC}[?25h`); // show cursor
27
+ process.stdout.write(`${ESC}[<u`); // disable kitty keyboard protocol
28
+ process.stdout.write(`${ESC}[?2004l`); // disable bracketed paste mode
25
29
  process.stdout.write(`${ESC}[?1049l`); // restore screen buffer
26
30
  }
27
31
  /** Paint lines to terminal without flicker: cursor home → overwrite each line → clear remainder */
@@ -30,11 +34,13 @@ export function paint(lines) {
30
34
  let out = `${ESC}[H`; // cursor home
31
35
  const count = Math.min(lines.length, rows);
32
36
  for (let i = 0; i < count; i++) {
33
- out += lines[i] + `${ESC}[K\n`; // line content + clear to end of line
37
+ out += lines[i] + `${ESC}[K`; // line content + clear to end of line
38
+ if (i < count - 1)
39
+ out += "\n";
34
40
  }
35
41
  // Clear any remaining lines below content
36
42
  if (count < rows) {
37
- out += `${ESC}[J`; // clear from cursor to end of screen
43
+ out += `\n${ESC}[J`; // clear from cursor to end of screen
38
44
  }
39
45
  process.stdout.write(out);
40
46
  }
@@ -43,6 +49,24 @@ export function screenSize() {
43
49
  }
44
50
  export function parseKey(data) {
45
51
  const s = data.toString("utf-8");
52
+ // Bracketed paste: \x1b[200~ ... \x1b[201~
53
+ if (s.startsWith(`${ESC}[200~`)) {
54
+ const content = s.replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
55
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
56
+ return { raw: normalized, name: "paste", shift: false, ctrl: false };
57
+ }
58
+ // Alt+Enter (ESC + CR/LF) — reliable newline insertion across all terminals
59
+ if (s === `${ESC}\r` || s === `${ESC}\n`) {
60
+ return { raw: s, name: "return", shift: true, ctrl: false };
61
+ }
62
+ // Alt+Arrow (word navigation)
63
+ if (s === `${ESC}[1;3D` || s === `${ESC}b`)
64
+ return { raw: s, name: "wordLeft", shift: false, ctrl: false };
65
+ if (s === `${ESC}[1;3C` || s === `${ESC}f`)
66
+ return { raw: s, name: "wordRight", shift: false, ctrl: false };
67
+ // Alt+Backspace (word delete)
68
+ if (s === `${ESC}\x7f`)
69
+ return { raw: s, name: "wordBackspace", shift: false, ctrl: false };
46
70
  const ctrl = s.length === 1 && s.charCodeAt(0) < 32;
47
71
  // Escape sequences
48
72
  if (s === `${ESC}[A`)
@@ -61,13 +85,23 @@ export function parseKey(data) {
61
85
  return { raw: s, name: "tab", shift: true, ctrl: false };
62
86
  if (s === ESC || s === `${ESC}${ESC}`)
63
87
  return { raw: s, name: "escape", shift: false, ctrl: false };
64
- // Shift+Enter sequences
88
+ // Kitty keyboard protocol CSI-u encodings (when disambiguate mode is active)
65
89
  if (s === `${ESC}[13;2u`)
66
- return { raw: s, name: "return", shift: true, ctrl: false }; // CSI u / kitty
90
+ return { raw: s, name: "return", shift: true, ctrl: false }; // Shift+Enter
67
91
  if (s === `${ESC}[27;2;13~`)
68
- return { raw: s, name: "return", shift: true, ctrl: false }; // xterm
92
+ return { raw: s, name: "return", shift: true, ctrl: false }; // Shift+Enter (xterm)
69
93
  if (s === `${ESC}OM`)
70
- return { raw: s, name: "return", shift: true, ctrl: false }; // misc terminals
94
+ return { raw: s, name: "return", shift: true, ctrl: false }; // Shift+Enter (misc)
95
+ if (s === `${ESC}[27u`)
96
+ return { raw: s, name: "escape", shift: false, ctrl: false };
97
+ if (s === `${ESC}[13u`)
98
+ return { raw: s, name: "return", shift: false, ctrl: false };
99
+ if (s === `${ESC}[9u`)
100
+ return { raw: s, name: "tab", shift: false, ctrl: false };
101
+ if (s === `${ESC}[9;2u`)
102
+ return { raw: s, name: "tab", shift: true, ctrl: false };
103
+ if (s === `${ESC}[127u`)
104
+ return { raw: s, name: "backspace", shift: false, ctrl: false };
71
105
  // Single characters
72
106
  if (s === "\r" || s === "\n")
73
107
  return { raw: s, name: "return", shift: false, ctrl: false };
@@ -82,6 +116,11 @@ export function parseKey(data) {
82
116
  if (ctrl) {
83
117
  return { raw: s, name: String.fromCharCode(s.charCodeAt(0) + 96), shift: false, ctrl: true };
84
118
  }
119
+ // Multi-character non-escape input = paste (terminal without bracketed paste support)
120
+ if (s.length > 1 && !s.startsWith(ESC)) {
121
+ const normalized = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
122
+ return { raw: normalized, name: "paste", shift: false, ctrl: false };
123
+ }
85
124
  return { raw: s, name: s, shift: false, ctrl: false };
86
125
  }
87
126
  /** Strip ANSI escape codes to get visible character count */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readwise/cli",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Command-line interface for Readwise and Reader",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,8 +23,8 @@
23
23
  "open": "^10"
24
24
  },
25
25
  "devDependencies": {
26
- "typescript": "^5",
27
26
  "@types/node": "^22",
28
- "tsx": "^4"
27
+ "tsx": "^4",
28
+ "typescript": "^5"
29
29
  }
30
30
  }
package/src/config.ts CHANGED
@@ -20,6 +20,7 @@ export interface SchemaProperty {
20
20
  enum?: string[];
21
21
  items?: SchemaProperty;
22
22
  default?: unknown;
23
+ examples?: unknown[];
23
24
  anyOf?: SchemaProperty[];
24
25
  $ref?: string;
25
26
  properties?: Record<string, SchemaProperty>;
package/src/index.ts CHANGED
@@ -93,9 +93,10 @@ async function main() {
93
93
  const forceRefresh = process.argv.includes("--refresh");
94
94
  const positionalArgs = process.argv.slice(2).filter((a) => !a.startsWith("--"));
95
95
  const hasSubcommand = positionalArgs.length > 0;
96
+ const wantsHelp = process.argv.includes("--help") || process.argv.includes("-h");
96
97
 
97
- // If no subcommand, TTY, and authenticated → launch TUI
98
- if (!hasSubcommand && process.stdout.isTTY && config.access_token) {
98
+ // If no subcommand, TTY, and authenticated → launch TUI (unless --help)
99
+ if (!hasSubcommand && !wantsHelp && process.stdout.isTTY && config.access_token) {
99
100
  try {
100
101
  const { token, authType } = await ensureValidToken();
101
102
  const tools = await getTools(token, authType, forceRefresh);
@@ -115,6 +116,13 @@ async function main() {
115
116
  return;
116
117
  }
117
118
 
119
+ // 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") {
121
+ process.stderr.write("\x1b[31mNot logged in.\x1b[0m Run `readwise login` or `readwise login-with-token` to authenticate.\n");
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+
118
126
  // Try to load tools if we have a token (for subcommand mode)
119
127
  if (config.access_token) {
120
128
  try {
@@ -123,7 +131,6 @@ async function main() {
123
131
  registerTools(program, tools);
124
132
  } catch (err) {
125
133
  // Don't fail — login command should still work
126
- // Only warn if user is trying to run a non-login command
127
134
  if (hasSubcommand && positionalArgs[0] !== "login" && positionalArgs[0] !== "login-with-token") {
128
135
  process.stderr.write(`\x1b[33mWarning: Could not fetch tools: ${(err as Error).message}\x1b[0m\n`);
129
136
  }