@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 +100 -2
- package/dist/tui/app.js +13 -7
- package/package.json +1 -1
- package/src/skills.ts +109 -3
- package/src/tui/app.ts +13 -7
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
|
|
154
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2871
|
-
paint(renderState(state));
|
|
2872
|
-
resetQuitTimer();
|
|
2878
|
+
runTool(state);
|
|
2873
2879
|
return;
|
|
2874
2880
|
}
|
|
2875
2881
|
state = result;
|
package/package.json
CHANGED
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
|
-
.
|
|
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
|
|
167
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2999
|
-
paint(renderState(state));
|
|
3000
|
-
resetQuitTimer();
|
|
3006
|
+
runTool(state);
|
|
3001
3007
|
return;
|
|
3002
3008
|
}
|
|
3003
3009
|
|