skillview 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 myskills contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # skillview
2
+
3
+ An interactive terminal manager for the AI agent skills on your machine.
4
+
5
+ `skillview` scans the skill folders of your local AI agents — **Claude** (`~/.claude`),
6
+ **Cursor** (`~/.cursor`), and **Agents** (`~/.agents`) — and lets you browse them by agent,
7
+ sorted by how recently they changed. View a skill's full `SKILL.md`, search across titles and
8
+ descriptions, and delete skills you no longer want, all without leaving the terminal.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -g skillview
14
+ ```
15
+
16
+ Then run:
17
+
18
+ ```bash
19
+ skillview # launch the interactive browser
20
+ skillview --help # show usage and keybindings
21
+ skillview --version # print the version
22
+ ```
23
+
24
+ Requires Node.js >= 18.
25
+
26
+ ## How it works
27
+
28
+ Skills are expected at:
29
+
30
+ ```
31
+ ~/.claude/skills/<skill>/SKILL.md
32
+ ~/.cursor/skills/<skill>/SKILL.md
33
+ ~/.agents/skills/<skill>/SKILL.md
34
+ ```
35
+
36
+ Each `SKILL.md` starts with YAML frontmatter holding `name` and `description`. `skillview` reads
37
+ that to show a colored title and a short description for every skill.
38
+
39
+ ## Navigation
40
+
41
+ The UI has three levels:
42
+
43
+ 1. **Agents** — pick `.claude`, `.cursor`, or `.agents` (shown with skill counts).
44
+ 2. **Skills** — skills in that agent, newest first. Each shows an orange title and a two-line
45
+ description.
46
+ 3. **Skill detail** — the full, scrollable `SKILL.md`.
47
+
48
+ ### Keys
49
+
50
+ | Key | Action |
51
+ | --------------- | --------------------------------------- |
52
+ | `↑` / `↓` | Move selection / scroll |
53
+ | `Enter` | Open the selected agent or skill |
54
+ | `Esc` / `←` | Go back a level |
55
+ | `/` | Search skills (in the skills view) |
56
+ | `d` | Delete the selected skill (asks first) |
57
+ | `PgUp` / `PgDn` | Page through a long skill in detail view |
58
+ | `q` / `Ctrl+C` | Quit |
59
+
60
+ ## License
61
+
62
+ MIT
package/dist/app.js ADDED
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback } from "react";
3
+ import { Box, Text, useApp } from "ink";
4
+ import { listAgents, listSkills } from "./lib/scanner.js";
5
+ import { AgentList } from "./components/AgentList.js";
6
+ import { SkillList } from "./components/SkillList.js";
7
+ import { SkillView } from "./components/SkillView.js";
8
+ import { ORANGE } from "./theme.js";
9
+ export function App() {
10
+ const { exit } = useApp();
11
+ const [view, setView] = useState("agents");
12
+ const [agents, setAgents] = useState(() => listAgents());
13
+ const [agent, setAgent] = useState(null);
14
+ const [skills, setSkills] = useState([]);
15
+ const [skill, setSkill] = useState(null);
16
+ // Quit ('q') is handled per-view so it never fires while typing in the search
17
+ // box. Ctrl+C always exits via Ink's default handler.
18
+ const openAgent = useCallback((a) => {
19
+ setAgent(a);
20
+ setSkills(listSkills(a));
21
+ setView("skills");
22
+ }, []);
23
+ const refreshSkills = useCallback(() => {
24
+ if (agent) {
25
+ // Re-read agent metadata (counts) and skills after a delete.
26
+ const fresh = listAgents().find((a) => a.id === agent.id) ?? agent;
27
+ setAgent(fresh);
28
+ setSkills(listSkills(fresh));
29
+ setAgents(listAgents());
30
+ }
31
+ }, [agent]);
32
+ const openSkill = useCallback((s) => {
33
+ setSkill(s);
34
+ setView("detail");
35
+ }, []);
36
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: ORANGE, bold: true, children: "skillview" }), _jsx(Text, { color: "gray", children: " \u00B7 local AI skill manager" })] }), view === "agents" && (_jsx(AgentList, { agents: agents, onSelect: openAgent, onQuit: exit })), view === "skills" && agent && (_jsx(SkillList, { agent: agent, skills: skills, onOpen: openSkill, onBack: () => setView("agents"), onChanged: refreshSkills })), view === "detail" && skill && (_jsx(SkillView, { skill: skill, onBack: () => setView("skills") }))] }));
37
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /// <reference types="node" />
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { render } from "ink";
8
+ import { App } from "./app.js";
9
+ function readVersion() {
10
+ try {
11
+ const here = path.dirname(fileURLToPath(import.meta.url));
12
+ const pkg = JSON.parse(fs.readFileSync(path.join(here, "..", "package.json"), "utf8"));
13
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
14
+ }
15
+ catch {
16
+ return "unknown";
17
+ }
18
+ }
19
+ const HELP = `skillview — interactive manager for your local AI agent skills
20
+
21
+ Usage:
22
+ skillview Launch the interactive browser
23
+ skillview --help Show this help
24
+ skillview --version Show the version
25
+
26
+ Scans skills under ~/.claude/skills, ~/.cursor/skills, and ~/.agents/skills.
27
+
28
+ Navigation:
29
+ ↑/↓ Move selection / scroll
30
+ Enter Open the selected agent or skill
31
+ Esc / ← Go back a level
32
+ / Search skills (in the skills view)
33
+ d Delete the selected skill (asks first)
34
+ PgUp / PgDn Page through a skill in the detail view
35
+ q / Ctrl+C Quit`;
36
+ const args = process.argv.slice(2);
37
+ if (args.includes("--help") || args.includes("-h")) {
38
+ console.log(HELP);
39
+ process.exit(0);
40
+ }
41
+ if (args.includes("--version") || args.includes("-v")) {
42
+ console.log(readVersion());
43
+ process.exit(0);
44
+ }
45
+ render(_jsx(App, {}));
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { ORANGE } from "../theme.js";
5
+ export function AgentList({ agents, onSelect, onQuit }) {
6
+ const [index, setIndex] = useState(0);
7
+ useInput((input, key) => {
8
+ if (input === "q") {
9
+ onQuit();
10
+ }
11
+ else if (key.upArrow || input === "k") {
12
+ setIndex((i) => (i - 1 + agents.length) % agents.length);
13
+ }
14
+ else if (key.downArrow || input === "j") {
15
+ setIndex((i) => (i + 1) % agents.length);
16
+ }
17
+ else if (key.return) {
18
+ const a = agents[index];
19
+ if (a && a.exists && a.count > 0)
20
+ onSelect(a);
21
+ }
22
+ });
23
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "Select an agent:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: agents.map((a, i) => {
24
+ const selected = i === index;
25
+ const enterable = a.exists && a.count > 0;
26
+ const status = !a.exists
27
+ ? "not found"
28
+ : a.count === 0
29
+ ? "empty"
30
+ : `${a.count} skill${a.count === 1 ? "" : "s"}`;
31
+ return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? ORANGE : undefined, children: selected ? "❯ " : " " }), _jsx(Text, { color: selected ? ORANGE : enterable ? undefined : "gray", bold: selected, children: a.label }), _jsxs(Text, { color: "gray", children: [" ", a.id, " \u00B7 ", status] })] }, a.id));
32
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "\u2191/\u2193 move \u00B7 Enter open \u00B7 q quit" }) })] }));
33
+ }
@@ -0,0 +1,13 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ export function ConfirmDelete({ skill, onConfirm, onCancel }) {
4
+ useInput((input, key) => {
5
+ if (input === "y" || input === "Y") {
6
+ onConfirm();
7
+ }
8
+ else if (input === "n" || input === "N" || key.escape || key.return) {
9
+ onCancel();
10
+ }
11
+ });
12
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["Delete skill \"", skill.title, "\"?"] }), _jsx(Text, { color: "gray", children: "This permanently removes:" }), _jsx(Text, { color: "gray", children: skill.path }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Press ", _jsx(Text, { color: "red", bold: true, children: "y" }), " to delete \u00B7 any other key to cancel"] }) })] }));
13
+ }
@@ -0,0 +1,105 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo, useEffect } from "react";
3
+ import { Box, Text, useApp, useInput, useStdout } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ import { deleteSkill } from "../lib/scanner.js";
6
+ import { wrapToLines } from "../lib/wrap.js";
7
+ import { ConfirmDelete } from "./ConfirmDelete.js";
8
+ import { ORANGE } from "../theme.js";
9
+ const ITEM_ROWS = 4; // title + 2 description lines + blank separator
10
+ export function SkillList({ agent, skills, onOpen, onBack, onChanged }) {
11
+ const { exit } = useApp();
12
+ const { stdout } = useStdout();
13
+ const rows = stdout?.rows ?? 24;
14
+ const columns = stdout?.columns ?? 80;
15
+ const [mode, setMode] = useState("list");
16
+ const [query, setQuery] = useState("");
17
+ const [index, setIndex] = useState(0);
18
+ const [offset, setOffset] = useState(0);
19
+ const [message, setMessage] = useState(null);
20
+ const filtered = useMemo(() => {
21
+ const q = query.trim().toLowerCase();
22
+ if (!q)
23
+ return skills;
24
+ return skills.filter((s) => s.title.toLowerCase().includes(q) ||
25
+ s.description.toLowerCase().includes(q));
26
+ }, [skills, query]);
27
+ // Keep selection in range when the list changes.
28
+ useEffect(() => {
29
+ setIndex((i) => Math.min(i, Math.max(0, filtered.length - 1)));
30
+ }, [filtered.length]);
31
+ const perPage = Math.max(1, Math.floor((rows - 8) / ITEM_ROWS));
32
+ // Scroll so the selected item stays visible.
33
+ useEffect(() => {
34
+ setOffset((o) => {
35
+ if (index < o)
36
+ return index;
37
+ if (index >= o + perPage)
38
+ return index - perPage + 1;
39
+ return o;
40
+ });
41
+ }, [index, perPage]);
42
+ const selected = filtered[index];
43
+ useInput((input, key) => {
44
+ if (input === "q") {
45
+ exit();
46
+ }
47
+ else if (key.upArrow || input === "k") {
48
+ setIndex((i) => (i - 1 + filtered.length) % Math.max(1, filtered.length));
49
+ setMessage(null);
50
+ }
51
+ else if (key.downArrow || input === "j") {
52
+ setIndex((i) => (i + 1) % Math.max(1, filtered.length));
53
+ setMessage(null);
54
+ }
55
+ else if (key.return) {
56
+ if (selected)
57
+ onOpen(selected);
58
+ }
59
+ else if (input === "d") {
60
+ if (selected)
61
+ setMode("confirm");
62
+ }
63
+ else if (input === "/") {
64
+ setMode("search");
65
+ setMessage(null);
66
+ }
67
+ else if (key.escape || key.leftArrow) {
68
+ if (query) {
69
+ setQuery("");
70
+ }
71
+ else {
72
+ onBack();
73
+ }
74
+ }
75
+ }, { isActive: mode === "list" });
76
+ // Esc inside the search box clears the query and returns to the list.
77
+ useInput((_input, key) => {
78
+ if (key.escape) {
79
+ setQuery("");
80
+ setMode("list");
81
+ }
82
+ }, { isActive: mode === "search" });
83
+ if (mode === "confirm" && selected) {
84
+ return (_jsx(ConfirmDelete, { skill: selected, onConfirm: () => {
85
+ const title = selected.title;
86
+ try {
87
+ deleteSkill(selected);
88
+ setMessage(`Deleted "${title}"`);
89
+ }
90
+ catch (err) {
91
+ setMessage(`Failed to delete: ${String(err)}`);
92
+ }
93
+ setMode("list");
94
+ onChanged();
95
+ }, onCancel: () => setMode("list") }));
96
+ }
97
+ const descWidth = columns - 6;
98
+ const visible = filtered.slice(offset, offset + perPage);
99
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: ORANGE, bold: true, children: agent.label }), _jsxs(Text, { color: "gray", children: [" ", filtered.length, query ? `/${skills.length}` : "", " skill", skills.length === 1 ? "" : "s", " \u00B7 newest first"] })] }), (mode === "search" || query) && (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "search: " }), mode === "search" ? (_jsx(TextInput, { value: query, onChange: setQuery, onSubmit: () => setMode("list") })) : (_jsx(Text, { children: query }))] })), message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: message }) })), _jsx(Box, { flexDirection: "column", marginTop: 1, children: filtered.length === 0 ? (_jsx(Text, { color: "gray", children: query ? "No skills match your search." : "No skills here." })) : (visible.map((s, i) => {
100
+ const realIndex = offset + i;
101
+ const isSel = realIndex === index;
102
+ const descLines = wrapToLines(s.description || "(no description)", descWidth, 2);
103
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: isSel ? ORANGE : "gray", children: isSel ? "❯ " : " " }), _jsx(Text, { color: ORANGE, bold: isSel, children: s.title })] }), descLines.map((line, li) => (_jsxs(Text, { color: s.description ? undefined : "gray", children: [" ", line] }, li)))] }, s.path));
104
+ })) }), _jsx(Box, { children: _jsx(Text, { color: "gray", children: "\u2191/\u2193 move \u00B7 Enter view \u00B7 / search \u00B7 d delete \u00B7 Esc back \u00B7 q quit" }) })] }));
105
+ }
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo, useEffect } from "react";
3
+ import { Box, Text, useApp, useInput, useStdout } from "ink";
4
+ import { readSkill } from "../lib/scanner.js";
5
+ import { ORANGE } from "../theme.js";
6
+ export function SkillView({ skill, onBack }) {
7
+ const { exit } = useApp();
8
+ const { stdout } = useStdout();
9
+ const rows = stdout?.rows ?? 24;
10
+ const content = useMemo(() => readSkill(skill), [skill]);
11
+ const lines = useMemo(() => content.replace(/\t/g, " ").split("\n"), [
12
+ content,
13
+ ]);
14
+ const viewport = Math.max(3, rows - 6); // reserve header + footer
15
+ const maxOffset = Math.max(0, lines.length - viewport);
16
+ const [offset, setOffset] = useState(0);
17
+ useEffect(() => {
18
+ setOffset(0);
19
+ }, [skill]);
20
+ useInput((input, key) => {
21
+ if (key.escape || key.leftArrow || input === "b") {
22
+ onBack();
23
+ }
24
+ else if (input === "q") {
25
+ exit();
26
+ }
27
+ else if (key.upArrow || input === "k") {
28
+ setOffset((o) => Math.max(0, o - 1));
29
+ }
30
+ else if (key.downArrow || input === "j") {
31
+ setOffset((o) => Math.min(maxOffset, o + 1));
32
+ }
33
+ else if (key.pageUp) {
34
+ setOffset((o) => Math.max(0, o - viewport));
35
+ }
36
+ else if (key.pageDown || input === " ") {
37
+ setOffset((o) => Math.min(maxOffset, o + viewport));
38
+ }
39
+ else if (input === "g") {
40
+ setOffset(0);
41
+ }
42
+ else if (input === "G") {
43
+ setOffset(maxOffset);
44
+ }
45
+ });
46
+ const visible = lines.slice(offset, offset + viewport);
47
+ const atEnd = offset >= maxOffset;
48
+ const pct = maxOffset === 0 ? 100 : Math.round((offset / maxOffset) * 100);
49
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: ORANGE, bold: true, children: skill.title }), _jsxs(Text, { color: "gray", children: [" ", skill.file] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((line, i) => (_jsx(Text, { children: line.length ? line : " " }, offset + i))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: [atEnd ? "END" : `${pct}%`, " \u00B7 \u2191/\u2193 scroll \u00B7 PgUp/PgDn page \u00B7 g/G top/bottom \u00B7 Esc back \u00B7 q quit"] }) })] }));
50
+ }
@@ -0,0 +1,109 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import matter from "gray-matter";
5
+ /** The agent directories we manage, in display order. */
6
+ const AGENT_DEFS = [
7
+ { id: ".claude", label: "Claude" },
8
+ { id: ".cursor", label: "Cursor" },
9
+ { id: ".agents", label: "Agents" },
10
+ ];
11
+ const HOME = os.homedir();
12
+ function skillsDirFor(id) {
13
+ return path.join(HOME, id, "skills");
14
+ }
15
+ /** Collapse any run of whitespace (incl. newlines) into single spaces. */
16
+ function collapse(text) {
17
+ return text.replace(/\s+/g, " ").trim();
18
+ }
19
+ /** A skill entry may be a real directory or a symlink to one. */
20
+ function isDirLike(entry) {
21
+ return entry.isDirectory() || entry.isSymbolicLink();
22
+ }
23
+ function countSkills(skillsDir) {
24
+ try {
25
+ return fs
26
+ .readdirSync(skillsDir, { withFileTypes: true })
27
+ .filter((e) => isDirLike(e) &&
28
+ fs.existsSync(path.join(skillsDir, e.name, "SKILL.md"))).length;
29
+ }
30
+ catch {
31
+ return 0;
32
+ }
33
+ }
34
+ /** List the agent directories with their skill counts. */
35
+ export function listAgents() {
36
+ return AGENT_DEFS.map(({ id, label }) => {
37
+ const skillsDir = skillsDirFor(id);
38
+ const exists = fs.existsSync(skillsDir);
39
+ return {
40
+ id,
41
+ label,
42
+ skillsDir,
43
+ exists,
44
+ count: exists ? countSkills(skillsDir) : 0,
45
+ };
46
+ });
47
+ }
48
+ /** List the skills inside an agent, sorted by SKILL.md mtime (newest first). */
49
+ export function listSkills(agent) {
50
+ let entries;
51
+ try {
52
+ entries = fs.readdirSync(agent.skillsDir, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ const skills = [];
58
+ for (const entry of entries) {
59
+ if (!isDirLike(entry))
60
+ continue;
61
+ const dirPath = path.join(agent.skillsDir, entry.name);
62
+ const file = path.join(dirPath, "SKILL.md");
63
+ let stat;
64
+ try {
65
+ stat = fs.statSync(file);
66
+ }
67
+ catch {
68
+ continue; // no SKILL.md -> not a skill
69
+ }
70
+ let title = entry.name;
71
+ let description = "";
72
+ try {
73
+ const raw = fs.readFileSync(file, "utf8");
74
+ const { data } = matter(raw);
75
+ if (typeof data.name === "string" && data.name.trim()) {
76
+ title = data.name.trim();
77
+ }
78
+ if (typeof data.description === "string") {
79
+ description = collapse(data.description);
80
+ }
81
+ }
82
+ catch {
83
+ // keep dir-name title, empty description
84
+ }
85
+ skills.push({
86
+ dir: entry.name,
87
+ path: dirPath,
88
+ file,
89
+ title,
90
+ description,
91
+ mtime: stat.mtimeMs,
92
+ });
93
+ }
94
+ skills.sort((a, b) => b.mtime - a.mtime);
95
+ return skills;
96
+ }
97
+ /** Read the raw SKILL.md contents for the detail view. */
98
+ export function readSkill(skill) {
99
+ try {
100
+ return fs.readFileSync(skill.file, "utf8");
101
+ }
102
+ catch (err) {
103
+ return `Failed to read ${skill.file}\n\n${String(err)}`;
104
+ }
105
+ }
106
+ /** Permanently delete a skill's directory. */
107
+ export function deleteSkill(skill) {
108
+ fs.rmSync(skill.path, { recursive: true, force: true });
109
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Wrap a single-line string into at most `maxLines` lines of `width` columns,
3
+ * appending an ellipsis if the text is truncated. Returns at least one line.
4
+ */
5
+ export function wrapToLines(text, width, maxLines) {
6
+ const w = Math.max(8, width);
7
+ if (!text)
8
+ return [""];
9
+ const words = text.split(" ");
10
+ const lines = [];
11
+ let current = "";
12
+ for (const word of words) {
13
+ const candidate = current ? `${current} ${word}` : word;
14
+ if (candidate.length <= w) {
15
+ current = candidate;
16
+ continue;
17
+ }
18
+ // candidate too long
19
+ if (current)
20
+ lines.push(current);
21
+ current = word;
22
+ // a single word longer than the width: hard-split it
23
+ while (current.length > w) {
24
+ lines.push(current.slice(0, w));
25
+ current = current.slice(w);
26
+ }
27
+ if (lines.length >= maxLines)
28
+ break;
29
+ }
30
+ if (current && lines.length < maxLines)
31
+ lines.push(current);
32
+ if (lines.length > maxLines)
33
+ lines.length = maxLines;
34
+ // If we ran out of room but there was more text, mark truncation.
35
+ const consumed = lines.join(" ").length;
36
+ if (consumed < text.length && lines.length > 0) {
37
+ const last = lines[maxLines - 1] ?? lines[lines.length - 1] ?? "";
38
+ const trimmed = last.length >= w ? `${last.slice(0, w - 1)}…` : `${last}…`;
39
+ lines[lines.length - 1] = trimmed;
40
+ }
41
+ return lines.length ? lines : [""];
42
+ }
package/dist/theme.js ADDED
@@ -0,0 +1,4 @@
1
+ /** Claude-style orange used for skill/agent titles. */
2
+ export const ORANGE = "#d97757";
3
+ /** Dim gray used for help bars and secondary text. */
4
+ export const DIM = "gray";
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "skillview",
3
+ "version": "0.1.0",
4
+ "description": "Interactive terminal manager for local AI agent skills (Claude, Cursor, Agents). Browse, search, view, and delete SKILL.md files by agent and recency.",
5
+ "keywords": [
6
+ "ai",
7
+ "skills",
8
+ "claude",
9
+ "cursor",
10
+ "agents",
11
+ "cli",
12
+ "tui",
13
+ "ink",
14
+ "skill-manager"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Jianyu",
18
+ "type": "module",
19
+ "bin": {
20
+ "skillview": "dist/cli.js"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "dev": "tsx src/cli.tsx",
31
+ "start": "node dist/cli.js",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "gray-matter": "^4.0.3",
36
+ "ink": "^5.1.0",
37
+ "ink-text-input": "^6.0.0",
38
+ "react": "^18.3.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.10.0",
42
+ "@types/react": "^18.3.12",
43
+ "tsx": "^4.19.2",
44
+ "typescript": "^5.7.2"
45
+ }
46
+ }