@readwise/cli 0.5.3 → 0.5.4

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/skills.js CHANGED
@@ -117,6 +117,87 @@ const PLATFORMS = {
117
117
  codex: { name: "Codex CLI", path: join(homedir(), ".codex", "skills") },
118
118
  opencode: { name: "OpenCode", path: join(homedir(), ".opencode", "skills") },
119
119
  };
120
+ /** Interactive checkbox picker — returns selected items */
121
+ async function pickSkills(names, descriptions) {
122
+ const selected = new Set(names.filter((n) => n === "readwise-cli"));
123
+ let cursor = 0;
124
+ const render = () => {
125
+ // Move cursor up to overwrite previous render
126
+ process.stderr.write(`\x1b[${names.length + 2}A\x1b[J`);
127
+ process.stderr.write(" Select skills to install (space toggle, a all/none, enter confirm):\n\n");
128
+ for (let i = 0; i < names.length; i++) {
129
+ const check = selected.has(names[i]) ? "\x1b[32m✔\x1b[0m" : " ";
130
+ const pointer = i === cursor ? "\x1b[36m❯\x1b[0m" : " ";
131
+ const desc = descriptions.get(names[i]) || "";
132
+ const descText = desc ? ` \x1b[2m${desc}\x1b[0m` : "";
133
+ process.stderr.write(` ${pointer} [${check}] ${names[i]}${descText}\n`);
134
+ }
135
+ };
136
+ // Initial render (print blank lines first so the upward cursor move works)
137
+ process.stderr.write("\n".repeat(names.length + 2));
138
+ render();
139
+ return new Promise((resolve) => {
140
+ if (!process.stdin.isTTY) {
141
+ // Non-interactive: install all
142
+ resolve(names);
143
+ return;
144
+ }
145
+ process.stdin.setRawMode(true);
146
+ process.stdin.resume();
147
+ const onData = (data) => {
148
+ const s = data.toString();
149
+ if (s === "\r" || s === "\n") {
150
+ // Enter — confirm
151
+ cleanup();
152
+ resolve([...selected]);
153
+ return;
154
+ }
155
+ if (s === "\x03" || s === "\x1b") {
156
+ // Ctrl+C or Escape — cancel
157
+ cleanup();
158
+ resolve(null);
159
+ return;
160
+ }
161
+ if (s === " ") {
162
+ // Space — toggle current
163
+ const name = names[cursor];
164
+ if (selected.has(name))
165
+ selected.delete(name);
166
+ else
167
+ selected.add(name);
168
+ render();
169
+ return;
170
+ }
171
+ if (s === "a") {
172
+ // 'a' — toggle all/none
173
+ if (selected.size === names.length)
174
+ selected.clear();
175
+ else
176
+ names.forEach((n) => selected.add(n));
177
+ render();
178
+ return;
179
+ }
180
+ if (s === "\x1b[A" || s === "k") {
181
+ // Up
182
+ cursor = (cursor - 1 + names.length) % names.length;
183
+ render();
184
+ return;
185
+ }
186
+ if (s === "\x1b[B" || s === "j") {
187
+ // Down
188
+ cursor = (cursor + 1) % names.length;
189
+ render();
190
+ return;
191
+ }
192
+ };
193
+ const cleanup = () => {
194
+ process.stdin.setRawMode(false);
195
+ process.stdin.pause();
196
+ process.stdin.removeListener("data", onData);
197
+ };
198
+ process.stdin.on("data", onData);
199
+ });
200
+ }
120
201
  export function registerSkillsCommands(program) {
121
202
  const skills = program.command("skills").description("Manage Readwise skills for AI agents");
122
203
  skills
@@ -148,10 +229,11 @@ export function registerSkillsCommands(program) {
148
229
  .description("Install skills to an agent platform (claude, codex, opencode)")
149
230
  .option("--all", "Detect installed agents and install to all")
150
231
  .option("--refresh", "Force refresh from GitHub before installing")
232
+ .option("-y, --yes", "Skip skill selection and install all")
151
233
  .action(async (platform, opts) => {
152
234
  try {
153
- const names = await getSkillNames(!!opts?.refresh);
154
- if (names.length === 0) {
235
+ const allNames = (await getSkillNames(!!opts?.refresh)).filter((n) => n !== "readwise-mcp");
236
+ if (allNames.length === 0) {
155
237
  console.error("No skills found.");
156
238
  process.exitCode = 1;
157
239
  return;
@@ -185,6 +267,22 @@ export function registerSkillsCommands(program) {
185
267
  process.exitCode = 1;
186
268
  return;
187
269
  }
270
+ // Let user pick which skills to install (unless --yes)
271
+ let names = allNames;
272
+ if (!opts?.yes && process.stdin.isTTY) {
273
+ const descs = new Map();
274
+ for (const n of allNames) {
275
+ const fm = await readSkillFrontmatter(n);
276
+ if (fm.description)
277
+ descs.set(n, fm.description);
278
+ }
279
+ const picked = await pickSkills(allNames, descs);
280
+ if (!picked || picked.length === 0) {
281
+ console.log("No skills selected.");
282
+ return;
283
+ }
284
+ names = picked;
285
+ }
188
286
  const cache = cacheDir();
189
287
  for (const target of targets) {
190
288
  console.log(`\nInstalling to ${target.name} (${target.path})...`);
package/dist/tui/app.js CHANGED
@@ -2835,7 +2835,17 @@ export async function runApp(tools) {
2835
2835
  process.stdin.removeListener("data", onData);
2836
2836
  resolve();
2837
2837
  };
2838
- const onData = async (data) => {
2838
+ const runTool = (loadingState) => {
2839
+ executeTool(loadingState).then((resultState) => {
2840
+ // If user quit during loading, don't update state
2841
+ if (state.view !== "loading")
2842
+ return;
2843
+ state = resultState;
2844
+ paint(renderState(state));
2845
+ resetQuitTimer();
2846
+ });
2847
+ };
2848
+ const onData = (data) => {
2839
2849
  const key = parseKey(data);
2840
2850
  if (key.ctrl && key.name === "c") {
2841
2851
  cleanup();
@@ -2859,17 +2869,13 @@ export async function runApp(tools) {
2859
2869
  if (result === "submit") {
2860
2870
  state = { ...state, view: "loading", spinnerFrame: 0 };
2861
2871
  paint(renderState(state));
2862
- state = await executeTool(state);
2863
- paint(renderState(state));
2864
- resetQuitTimer();
2872
+ runTool(state);
2865
2873
  return;
2866
2874
  }
2867
2875
  if (result.view === "loading") {
2868
2876
  state = { ...result, spinnerFrame: 0 };
2869
2877
  paint(renderState(state));
2870
- state = await executeTool(state);
2871
- paint(renderState(state));
2872
- resetQuitTimer();
2878
+ runTool(state);
2873
2879
  return;
2874
2880
  }
2875
2881
  state = result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readwise/cli",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Command-line interface for Readwise and Reader",
5
5
  "type": "module",
6
6
  "bin": {
package/src/skills.ts CHANGED
@@ -129,6 +129,95 @@ const PLATFORMS: Record<string, { name: string; path: string }> = {
129
129
  opencode: { name: "OpenCode", path: join(homedir(), ".opencode", "skills") },
130
130
  };
131
131
 
132
+ /** Interactive checkbox picker — returns selected items */
133
+ async function pickSkills(
134
+ names: string[],
135
+ descriptions: Map<string, string>
136
+ ): Promise<string[] | null> {
137
+ const selected = new Set<string>(names.filter((n) => n === "readwise-cli"));
138
+ let cursor = 0;
139
+
140
+ const render = () => {
141
+ // Move cursor up to overwrite previous render
142
+ process.stderr.write(`\x1b[${names.length + 2}A\x1b[J`);
143
+ process.stderr.write(" Select skills to install (space toggle, a all/none, enter confirm):\n\n");
144
+ for (let i = 0; i < names.length; i++) {
145
+ const check = selected.has(names[i]!) ? "\x1b[32m✔\x1b[0m" : " ";
146
+ const pointer = i === cursor ? "\x1b[36m❯\x1b[0m" : " ";
147
+ const desc = descriptions.get(names[i]!) || "";
148
+ const descText = desc ? ` \x1b[2m${desc}\x1b[0m` : "";
149
+ process.stderr.write(` ${pointer} [${check}] ${names[i]}${descText}\n`);
150
+ }
151
+ };
152
+
153
+ // Initial render (print blank lines first so the upward cursor move works)
154
+ process.stderr.write("\n".repeat(names.length + 2));
155
+ render();
156
+
157
+ return new Promise((resolve) => {
158
+ if (!process.stdin.isTTY) {
159
+ // Non-interactive: install all
160
+ resolve(names);
161
+ return;
162
+ }
163
+
164
+ process.stdin.setRawMode(true);
165
+ process.stdin.resume();
166
+
167
+ const onData = (data: Buffer) => {
168
+ const s = data.toString();
169
+
170
+ if (s === "\r" || s === "\n") {
171
+ // Enter — confirm
172
+ cleanup();
173
+ resolve([...selected]);
174
+ return;
175
+ }
176
+ if (s === "\x03" || s === "\x1b") {
177
+ // Ctrl+C or Escape — cancel
178
+ cleanup();
179
+ resolve(null);
180
+ return;
181
+ }
182
+ if (s === " ") {
183
+ // Space — toggle current
184
+ const name = names[cursor]!;
185
+ if (selected.has(name)) selected.delete(name);
186
+ else selected.add(name);
187
+ render();
188
+ return;
189
+ }
190
+ if (s === "a") {
191
+ // 'a' — toggle all/none
192
+ if (selected.size === names.length) selected.clear();
193
+ else names.forEach((n) => selected.add(n));
194
+ render();
195
+ return;
196
+ }
197
+ if (s === "\x1b[A" || s === "k") {
198
+ // Up
199
+ cursor = (cursor - 1 + names.length) % names.length;
200
+ render();
201
+ return;
202
+ }
203
+ if (s === "\x1b[B" || s === "j") {
204
+ // Down
205
+ cursor = (cursor + 1) % names.length;
206
+ render();
207
+ return;
208
+ }
209
+ };
210
+
211
+ const cleanup = () => {
212
+ process.stdin.setRawMode(false);
213
+ process.stdin.pause();
214
+ process.stdin.removeListener("data", onData);
215
+ };
216
+
217
+ process.stdin.on("data", onData);
218
+ });
219
+ }
220
+
132
221
  export function registerSkillsCommands(program: Command): void {
133
222
  const skills = program.command("skills").description("Manage Readwise skills for AI agents");
134
223
 
@@ -161,10 +250,11 @@ export function registerSkillsCommands(program: Command): void {
161
250
  .description("Install skills to an agent platform (claude, codex, opencode)")
162
251
  .option("--all", "Detect installed agents and install to all")
163
252
  .option("--refresh", "Force refresh from GitHub before installing")
164
- .action(async (platform?: string, opts?: { all?: boolean; refresh?: boolean }) => {
253
+ .option("-y, --yes", "Skip skill selection and install all")
254
+ .action(async (platform?: string, opts?: { all?: boolean; refresh?: boolean; yes?: boolean }) => {
165
255
  try {
166
- const names = await getSkillNames(!!opts?.refresh);
167
- if (names.length === 0) {
256
+ const allNames = (await getSkillNames(!!opts?.refresh)).filter((n) => n !== "readwise-mcp");
257
+ if (allNames.length === 0) {
168
258
  console.error("No skills found.");
169
259
  process.exitCode = 1;
170
260
  return;
@@ -199,6 +289,22 @@ export function registerSkillsCommands(program: Command): void {
199
289
  return;
200
290
  }
201
291
 
292
+ // Let user pick which skills to install (unless --yes)
293
+ let names = allNames;
294
+ if (!opts?.yes && process.stdin.isTTY) {
295
+ const descs = new Map<string, string>();
296
+ for (const n of allNames) {
297
+ const fm = await readSkillFrontmatter(n);
298
+ if (fm.description) descs.set(n, fm.description);
299
+ }
300
+ const picked = await pickSkills(allNames, descs);
301
+ if (!picked || picked.length === 0) {
302
+ console.log("No skills selected.");
303
+ return;
304
+ }
305
+ names = picked;
306
+ }
307
+
202
308
  const cache = cacheDir();
203
309
  for (const target of targets) {
204
310
  console.log(`\nInstalling to ${target.name} (${target.path})...`);
package/src/tui/app.ts CHANGED
@@ -2957,7 +2957,17 @@ export async function runApp(tools: ToolDef[]): Promise<void> {
2957
2957
  resolve();
2958
2958
  };
2959
2959
 
2960
- const onData = async (data: Buffer) => {
2960
+ const runTool = (loadingState: AppState) => {
2961
+ executeTool(loadingState).then((resultState) => {
2962
+ // If user quit during loading, don't update state
2963
+ if (state.view !== "loading") return;
2964
+ state = resultState;
2965
+ paint(renderState(state));
2966
+ resetQuitTimer();
2967
+ });
2968
+ };
2969
+
2970
+ const onData = (data: Buffer) => {
2961
2971
  const key = parseKey(data);
2962
2972
 
2963
2973
  if (key.ctrl && key.name === "c") {
@@ -2986,18 +2996,14 @@ export async function runApp(tools: ToolDef[]): Promise<void> {
2986
2996
  if (result === "submit") {
2987
2997
  state = { ...state, view: "loading", spinnerFrame: 0 };
2988
2998
  paint(renderState(state));
2989
- state = await executeTool(state);
2990
- paint(renderState(state));
2991
- resetQuitTimer();
2999
+ runTool(state);
2992
3000
  return;
2993
3001
  }
2994
3002
 
2995
3003
  if (result.view === "loading") {
2996
3004
  state = { ...result, spinnerFrame: 0 };
2997
3005
  paint(renderState(state));
2998
- state = await executeTool(state);
2999
- paint(renderState(state));
3000
- resetQuitTimer();
3006
+ runTool(state);
3001
3007
  return;
3002
3008
  }
3003
3009