@readwise/cli 0.3.1 → 0.5.1
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 +10 -7
- package/dist/config.d.ts +1 -0
- package/dist/index.js +12 -3
- package/dist/skills.d.ts +2 -0
- package/dist/skills.js +246 -0
- package/dist/tui/app.js +1254 -237
- package/dist/tui/term.js +45 -6
- package/package.json +3 -3
- package/src/config.ts +1 -0
- package/src/index.ts +14 -3
- package/src/skills.ts +260 -0
- package/src/tui/app.ts +1268 -231
- package/src/tui/term.ts +41 -6
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
|
|
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 +=
|
|
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
|
-
//
|
|
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 }; //
|
|
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
|
|
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
|
+
"version": "0.5.1",
|
|
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
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) => {
|
|
@@ -93,9 +94,10 @@ async function main() {
|
|
|
93
94
|
const forceRefresh = process.argv.includes("--refresh");
|
|
94
95
|
const positionalArgs = process.argv.slice(2).filter((a) => !a.startsWith("--"));
|
|
95
96
|
const hasSubcommand = positionalArgs.length > 0;
|
|
97
|
+
const wantsHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
96
98
|
|
|
97
|
-
// If no subcommand, TTY, and authenticated → launch TUI
|
|
98
|
-
if (!hasSubcommand && process.stdout.isTTY && config.access_token) {
|
|
99
|
+
// If no subcommand, TTY, and authenticated → launch TUI (unless --help)
|
|
100
|
+
if (!hasSubcommand && !wantsHelp && process.stdout.isTTY && config.access_token) {
|
|
99
101
|
try {
|
|
100
102
|
const { token, authType } = await ensureValidToken();
|
|
101
103
|
const tools = await getTools(token, authType, forceRefresh);
|
|
@@ -115,6 +117,16 @@ async function main() {
|
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
// Register skills commands (works without auth)
|
|
121
|
+
registerSkillsCommands(program);
|
|
122
|
+
|
|
123
|
+
// If not authenticated and trying a non-login command, tell user to log in
|
|
124
|
+
if (!config.access_token && hasSubcommand && positionalArgs[0] !== "login" && positionalArgs[0] !== "login-with-token" && positionalArgs[0] !== "skills") {
|
|
125
|
+
process.stderr.write("\x1b[31mNot logged in.\x1b[0m Run `readwise login` or `readwise login-with-token` to authenticate.\n");
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
// Try to load tools if we have a token (for subcommand mode)
|
|
119
131
|
if (config.access_token) {
|
|
120
132
|
try {
|
|
@@ -123,7 +135,6 @@ async function main() {
|
|
|
123
135
|
registerTools(program, tools);
|
|
124
136
|
} catch (err) {
|
|
125
137
|
// Don't fail — login command should still work
|
|
126
|
-
// Only warn if user is trying to run a non-login command
|
|
127
138
|
if (hasSubcommand && positionalArgs[0] !== "login" && positionalArgs[0] !== "login-with-token") {
|
|
128
139
|
process.stderr.write(`\x1b[33mWarning: Could not fetch tools: ${(err as Error).message}\x1b[0m\n`);
|
|
129
140
|
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
|
|
7
|
+
const REPO_OWNER = "readwiseio";
|
|
8
|
+
const REPO_NAME = "readwise-skills";
|
|
9
|
+
const SKILLS_SUBDIR = "skills";
|
|
10
|
+
const GITHUB_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}`;
|
|
11
|
+
const GITHUB_RAW = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/master`;
|
|
12
|
+
|
|
13
|
+
/** Local cache directory for fetched skills */
|
|
14
|
+
function cacheDir(): string {
|
|
15
|
+
return join(homedir(), ".readwise", "skills-cache");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Staleness threshold — refetch if cache is older than this */
|
|
19
|
+
const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
20
|
+
|
|
21
|
+
function cacheMetaPath(): string {
|
|
22
|
+
return join(cacheDir(), ".fetched_at");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function isCacheFresh(): Promise<boolean> {
|
|
26
|
+
const metaPath = cacheMetaPath();
|
|
27
|
+
if (!existsSync(metaPath)) return false;
|
|
28
|
+
try {
|
|
29
|
+
const ts = Number(await readFile(metaPath, "utf-8"));
|
|
30
|
+
return Date.now() - ts < CACHE_MAX_AGE_MS;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function touchCache(): Promise<void> {
|
|
37
|
+
await writeFile(cacheMetaPath(), String(Date.now()));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fetch the list of skill directory names from GitHub API */
|
|
41
|
+
async function fetchSkillNames(): Promise<string[]> {
|
|
42
|
+
const res = await fetch(`${GITHUB_API}/contents/${SKILLS_SUBDIR}`, {
|
|
43
|
+
headers: { Accept: "application/vnd.github.v3+json", "User-Agent": "readwise-cli" },
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
|
|
46
|
+
const entries = (await res.json()) as { name: string; type: string }[];
|
|
47
|
+
return entries.filter((e) => e.type === "dir").map((e) => e.name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Fetch a single SKILL.md from GitHub */
|
|
51
|
+
async function fetchSkillFile(skillName: string): Promise<string> {
|
|
52
|
+
const url = `${GITHUB_RAW}/${SKILLS_SUBDIR}/${skillName}/SKILL.md`;
|
|
53
|
+
const res = await fetch(url, { headers: { "User-Agent": "readwise-cli" } });
|
|
54
|
+
if (!res.ok) throw new Error(`Failed to fetch ${skillName}: ${res.status}`);
|
|
55
|
+
return res.text();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Fetch all skills from GitHub and write to cache */
|
|
59
|
+
async function refreshCache(): Promise<string[]> {
|
|
60
|
+
const names = await fetchSkillNames();
|
|
61
|
+
const cache = cacheDir();
|
|
62
|
+
|
|
63
|
+
// Clear old cache
|
|
64
|
+
if (existsSync(cache)) await rm(cache, { recursive: true });
|
|
65
|
+
await mkdir(cache, { recursive: true });
|
|
66
|
+
|
|
67
|
+
// Fetch all in parallel
|
|
68
|
+
const results = await Promise.allSettled(
|
|
69
|
+
names.map(async (name) => {
|
|
70
|
+
const content = await fetchSkillFile(name);
|
|
71
|
+
const dir = join(cache, name);
|
|
72
|
+
await mkdir(dir, { recursive: true });
|
|
73
|
+
await writeFile(join(dir, "SKILL.md"), content);
|
|
74
|
+
return name;
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const fetched = results
|
|
79
|
+
.filter((r): r is PromiseFulfilledResult<string> => r.status === "fulfilled")
|
|
80
|
+
.map((r) => r.value);
|
|
81
|
+
|
|
82
|
+
await touchCache();
|
|
83
|
+
return fetched;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Get cached skill names, refreshing if stale */
|
|
87
|
+
async function getSkillNames(forceRefresh: boolean): Promise<string[]> {
|
|
88
|
+
if (!forceRefresh && (await isCacheFresh())) {
|
|
89
|
+
return listCachedSkills();
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return await refreshCache();
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Fall back to cache if network fails
|
|
95
|
+
const cached = await listCachedSkills();
|
|
96
|
+
if (cached.length > 0) {
|
|
97
|
+
console.error(`\x1b[33mWarning: Could not fetch from GitHub (${(err as Error).message}), using cached skills.\x1b[0m`);
|
|
98
|
+
return cached;
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function listCachedSkills(): Promise<string[]> {
|
|
105
|
+
const cache = cacheDir();
|
|
106
|
+
if (!existsSync(cache)) return [];
|
|
107
|
+
const entries = await readdir(cache, { withFileTypes: true });
|
|
108
|
+
return entries
|
|
109
|
+
.filter((e) => e.isDirectory() && existsSync(join(cache, e.name, "SKILL.md")))
|
|
110
|
+
.map((e) => e.name);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function readSkillFrontmatter(skillName: string): Promise<{ name?: string; description?: string }> {
|
|
114
|
+
const content = await readFile(join(cacheDir(), skillName, "SKILL.md"), "utf-8");
|
|
115
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
116
|
+
if (!match) return {};
|
|
117
|
+
const fm: Record<string, string> = {};
|
|
118
|
+
for (const line of match[1]!.split("\n")) {
|
|
119
|
+
const [key, ...rest] = line.split(":");
|
|
120
|
+
if (key && rest.length) fm[key.trim()] = rest.join(":").trim();
|
|
121
|
+
}
|
|
122
|
+
return fm;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Known agent platforms and their skills directories */
|
|
126
|
+
const PLATFORMS: Record<string, { name: string; path: string }> = {
|
|
127
|
+
claude: { name: "Claude Code", path: join(homedir(), ".claude", "skills") },
|
|
128
|
+
codex: { name: "Codex CLI", path: join(homedir(), ".codex", "skills") },
|
|
129
|
+
opencode: { name: "OpenCode", path: join(homedir(), ".opencode", "skills") },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export function registerSkillsCommands(program: Command): void {
|
|
133
|
+
const skills = program.command("skills").description("Manage Readwise skills for AI agents");
|
|
134
|
+
|
|
135
|
+
skills
|
|
136
|
+
.command("list")
|
|
137
|
+
.description("List available skills (fetched from github.com/readwiseio/readwise-skills)")
|
|
138
|
+
.option("--refresh", "Force refresh from GitHub")
|
|
139
|
+
.action(async (opts: { refresh?: boolean }) => {
|
|
140
|
+
try {
|
|
141
|
+
const names = await getSkillNames(!!opts.refresh);
|
|
142
|
+
if (names.length === 0) {
|
|
143
|
+
console.log("No skills found.");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
console.log("Available skills:\n");
|
|
147
|
+
for (const name of names) {
|
|
148
|
+
const fm = await readSkillFrontmatter(name);
|
|
149
|
+
const desc = fm.description || "";
|
|
150
|
+
console.log(` ${name.padEnd(22)} ${desc}`);
|
|
151
|
+
}
|
|
152
|
+
console.log(`\nRun \`readwise skills install <platform>\` to install. Platforms: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`\x1b[31mFailed to list skills: ${(err as Error).message}\x1b[0m`);
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
skills
|
|
160
|
+
.command("install [platform]")
|
|
161
|
+
.description("Install skills to an agent platform (claude, codex, opencode)")
|
|
162
|
+
.option("--all", "Detect installed agents and install to all")
|
|
163
|
+
.option("--refresh", "Force refresh from GitHub before installing")
|
|
164
|
+
.action(async (platform?: string, opts?: { all?: boolean; refresh?: boolean }) => {
|
|
165
|
+
try {
|
|
166
|
+
const names = await getSkillNames(!!opts?.refresh);
|
|
167
|
+
if (names.length === 0) {
|
|
168
|
+
console.error("No skills found.");
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let targets: { key: string; name: string; path: string }[] = [];
|
|
174
|
+
|
|
175
|
+
if (opts?.all) {
|
|
176
|
+
for (const [key, info] of Object.entries(PLATFORMS)) {
|
|
177
|
+
const parentDir = dirname(info.path);
|
|
178
|
+
if (existsSync(parentDir)) {
|
|
179
|
+
targets.push({ key, ...info });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (targets.length === 0) {
|
|
183
|
+
console.log("No supported agents detected. Supported platforms: " + Object.keys(PLATFORMS).join(", "));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
} else if (platform) {
|
|
187
|
+
const info = PLATFORMS[platform.toLowerCase()];
|
|
188
|
+
if (!info) {
|
|
189
|
+
console.error(`Unknown platform: ${platform}`);
|
|
190
|
+
console.error(`Supported platforms: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
targets = [{ key: platform.toLowerCase(), ...info }];
|
|
195
|
+
} else {
|
|
196
|
+
console.error("Specify a platform or use --all.");
|
|
197
|
+
console.error(`Supported platforms: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const cache = cacheDir();
|
|
203
|
+
for (const target of targets) {
|
|
204
|
+
console.log(`\nInstalling to ${target.name} (${target.path})...`);
|
|
205
|
+
await mkdir(target.path, { recursive: true });
|
|
206
|
+
|
|
207
|
+
for (const skillName of names) {
|
|
208
|
+
const srcFile = join(cache, skillName, "SKILL.md");
|
|
209
|
+
const destDir = join(target.path, skillName);
|
|
210
|
+
await mkdir(destDir, { recursive: true });
|
|
211
|
+
const content = await readFile(srcFile, "utf-8");
|
|
212
|
+
await writeFile(join(destDir, "SKILL.md"), content);
|
|
213
|
+
console.log(` \u2713 ${skillName}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log("\nDone!");
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error(`\x1b[31mFailed to install skills: ${(err as Error).message}\x1b[0m`);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
skills
|
|
225
|
+
.command("show <name>")
|
|
226
|
+
.description("Print the raw SKILL.md for a skill")
|
|
227
|
+
.option("--refresh", "Force refresh from GitHub")
|
|
228
|
+
.action(async (name: string, opts: { refresh?: boolean }) => {
|
|
229
|
+
try {
|
|
230
|
+
await getSkillNames(!!opts.refresh);
|
|
231
|
+
const skillPath = join(cacheDir(), name, "SKILL.md");
|
|
232
|
+
if (!existsSync(skillPath)) {
|
|
233
|
+
console.error(`Skill not found: ${name}`);
|
|
234
|
+
const available = await listCachedSkills();
|
|
235
|
+
if (available.length) console.error(`Available: ${available.join(", ")}`);
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const content = await readFile(skillPath, "utf-8");
|
|
240
|
+
process.stdout.write(content);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error(`\x1b[31mFailed: ${(err as Error).message}\x1b[0m`);
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
skills
|
|
248
|
+
.command("update")
|
|
249
|
+
.description("Force refresh skills from GitHub")
|
|
250
|
+
.action(async () => {
|
|
251
|
+
try {
|
|
252
|
+
console.log("Fetching skills from github.com/readwiseio/readwise-skills...");
|
|
253
|
+
const names = await refreshCache();
|
|
254
|
+
console.log(`Updated ${names.length} skills: ${names.join(", ")}`);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.error(`\x1b[31mFailed to update: ${(err as Error).message}\x1b[0m`);
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|