skillio 0.1.13 → 0.1.15

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.
@@ -0,0 +1,252 @@
1
+ import {
2
+ cyan,
3
+ discoverSkills,
4
+ getLockPath,
5
+ red
6
+ } from "./chunk-nkpp3h85.js";
7
+
8
+ // src/commands/picker.ts
9
+ import { spawnSync } from "node:child_process";
10
+
11
+ // src/utils/list-removable.ts
12
+ function listRemovableTargets(input) {
13
+ const records = [...discoverSkills(input).values()];
14
+ const inLock = [];
15
+ const orphan = [];
16
+ for (const r of records) {
17
+ if (r.sources.includes("lock"))
18
+ inLock.push(r.name);
19
+ else
20
+ orphan.push(r.name);
21
+ }
22
+ inLock.sort();
23
+ orphan.sort();
24
+ return { inLock, orphan };
25
+ }
26
+
27
+ // src/utils/prompt.ts
28
+ import { emitKeypressEvents } from "node:readline";
29
+ async function select(params) {
30
+ const input = params.input ?? process.stdin;
31
+ const output = params.output ?? process.stdout;
32
+ if (!input.isTTY || !output.isTTY)
33
+ return null;
34
+ let cursor = 0;
35
+ const total = params.options.length;
36
+ function render() {
37
+ output.write(`${params.title}
38
+ `);
39
+ for (let i = 0;i < total; i++) {
40
+ const opt = params.options[i];
41
+ if (!opt)
42
+ continue;
43
+ const marker = i === cursor ? cyan(">") : " ";
44
+ output.write(`${marker} ${opt.label}
45
+ `);
46
+ }
47
+ }
48
+ function clear() {
49
+ output.write(`\x1B[${total + 1}A\x1B[J`);
50
+ }
51
+ emitKeypressEvents(input);
52
+ if (input.setRawMode)
53
+ input.setRawMode(true);
54
+ input.resume();
55
+ render();
56
+ return await new Promise((resolve) => {
57
+ const onKey = (_str, key) => {
58
+ if (key.ctrl && key.name === "c") {
59
+ cleanup();
60
+ resolve(null);
61
+ return;
62
+ }
63
+ if (key.name === "escape" || key.name === "q") {
64
+ cleanup();
65
+ resolve(null);
66
+ return;
67
+ }
68
+ if (key.name === "up" && cursor > 0) {
69
+ cursor--;
70
+ clear();
71
+ render();
72
+ return;
73
+ }
74
+ if (key.name === "down" && cursor < total - 1) {
75
+ cursor++;
76
+ clear();
77
+ render();
78
+ return;
79
+ }
80
+ if (key.name === "return") {
81
+ cleanup();
82
+ const chosen = params.options[cursor]?.value ?? null;
83
+ resolve(chosen);
84
+ return;
85
+ }
86
+ };
87
+ function onSigterm() {
88
+ cleanup();
89
+ resolve(null);
90
+ }
91
+ function cleanup() {
92
+ input.removeListener("keypress", onKey);
93
+ process.removeListener("SIGTERM", onSigterm);
94
+ if (input.setRawMode)
95
+ input.setRawMode(false);
96
+ input.pause();
97
+ }
98
+ process.once("SIGTERM", onSigterm);
99
+ input.on("keypress", onKey);
100
+ });
101
+ }
102
+ async function multiSelect(params) {
103
+ const input = params.input ?? process.stdin;
104
+ const output = params.output ?? process.stdout;
105
+ if (!input.isTTY || !output.isTTY)
106
+ return null;
107
+ let cursor = 0;
108
+ const total = params.options.length;
109
+ const selected = new Set;
110
+ function render() {
111
+ output.write(`${params.title}
112
+ `);
113
+ for (let i = 0;i < total; i++) {
114
+ const opt = params.options[i];
115
+ if (!opt)
116
+ continue;
117
+ const cursorMark = i === cursor ? cyan(">") : " ";
118
+ const checkbox = selected.has(i) ? "[x]" : "[ ]";
119
+ output.write(`${cursorMark} ${checkbox} ${opt.label}
120
+ `);
121
+ }
122
+ }
123
+ function clear() {
124
+ output.write(`\x1B[${total + 1}A\x1B[J`);
125
+ }
126
+ emitKeypressEvents(input);
127
+ if (input.setRawMode)
128
+ input.setRawMode(true);
129
+ input.resume();
130
+ render();
131
+ return await new Promise((resolve) => {
132
+ const onKey = (_str, key) => {
133
+ if (key.ctrl && key.name === "c") {
134
+ cleanup();
135
+ resolve(null);
136
+ return;
137
+ }
138
+ if (key.name === "escape" || key.name === "q") {
139
+ cleanup();
140
+ resolve(null);
141
+ return;
142
+ }
143
+ if (key.name === "up" && cursor > 0) {
144
+ cursor--;
145
+ clear();
146
+ render();
147
+ return;
148
+ }
149
+ if (key.name === "down" && cursor < total - 1) {
150
+ cursor++;
151
+ clear();
152
+ render();
153
+ return;
154
+ }
155
+ if (key.name === "space") {
156
+ if (selected.has(cursor))
157
+ selected.delete(cursor);
158
+ else
159
+ selected.add(cursor);
160
+ clear();
161
+ render();
162
+ return;
163
+ }
164
+ if (key.name === "return") {
165
+ cleanup();
166
+ const values = [];
167
+ for (let i = 0;i < total; i++) {
168
+ if (selected.has(i)) {
169
+ const v = params.options[i]?.value;
170
+ if (v !== undefined)
171
+ values.push(v);
172
+ }
173
+ }
174
+ resolve(values);
175
+ return;
176
+ }
177
+ };
178
+ function onSigterm() {
179
+ cleanup();
180
+ resolve(null);
181
+ }
182
+ function cleanup() {
183
+ input.removeListener("keypress", onKey);
184
+ process.removeListener("SIGTERM", onSigterm);
185
+ if (input.setRawMode)
186
+ input.setRawMode(false);
187
+ input.pause();
188
+ }
189
+ process.once("SIGTERM", onSigterm);
190
+ input.on("keypress", onKey);
191
+ });
192
+ }
193
+
194
+ // src/commands/picker.ts
195
+ async function pickRemoveTargets(args) {
196
+ const lockPath = getLockPath(args.global);
197
+ const { inLock, orphan } = listRemovableTargets({
198
+ isGlobal: args.global,
199
+ cwd: process.cwd(),
200
+ lockPath
201
+ });
202
+ if (inLock.length === 0 && orphan.length === 0) {
203
+ console.log("No skills found in scope.");
204
+ return [];
205
+ }
206
+ const options = [
207
+ ...inLock.map((name) => ({ value: name, label: name })),
208
+ ...orphan.map((name) => ({ value: name, label: `${name} ${red("(orphan)")}` }))
209
+ ];
210
+ return await multiSelect({
211
+ title: "skillio — pick skills to remove (Space toggle, Enter confirm)",
212
+ options
213
+ });
214
+ }
215
+ async function runPicker(args) {
216
+ const choice = await select({
217
+ title: "skillio — pick a command",
218
+ options: [
219
+ { value: "usage", label: "usage — count of skill invocations" },
220
+ { value: "cost", label: "cost — per-skill ambient tokens" },
221
+ { value: "list", label: "list — installed skills per source" },
222
+ { value: "remove", label: "remove — delete a skill (asks about lock cleanup)" },
223
+ { value: "quit", label: "quit" }
224
+ ]
225
+ });
226
+ if (choice === null || choice === "quit")
227
+ return 0;
228
+ const cliPath = process.argv[1];
229
+ if (!cliPath) {
230
+ console.error("skillio: cannot resolve CLI path (process.argv[1] missing)");
231
+ return 1;
232
+ }
233
+ let argv;
234
+ if (choice === "remove") {
235
+ const targets = await pickRemoveTargets(args);
236
+ if (targets === null || targets.length === 0)
237
+ return 0;
238
+ argv = ["rm", ...targets];
239
+ } else {
240
+ argv = [choice];
241
+ }
242
+ if (args.global)
243
+ argv.push("-g");
244
+ const r = spawnSync(process.execPath, [cliPath, ...argv], {
245
+ stdio: "inherit",
246
+ env: process.env
247
+ });
248
+ return r.status ?? 0;
249
+ }
250
+ export {
251
+ runPicker
252
+ };
@@ -0,0 +1,153 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/lock/file.ts
5
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ function getLockPath(global) {
9
+ return global ? join(homedir(), ".agents", ".skill-lock.json") : "skills-lock.json";
10
+ }
11
+ function readLock(path) {
12
+ if (!existsSync(path))
13
+ return { skills: {} };
14
+ return JSON.parse(readFileSync(path, "utf8"));
15
+ }
16
+ function serialize(lock) {
17
+ return `${JSON.stringify(lock, null, 2)}
18
+ `;
19
+ }
20
+ function writeLock(path, lock) {
21
+ mkdirSync(dirname(path), { recursive: true });
22
+ const tmp = join(dirname(path), `.${Date.now()}.skill-lock.json`);
23
+ writeFileSync(tmp, serialize(lock));
24
+ renameSync(tmp, path);
25
+ }
26
+ function countLockLinesToRemove(path, names) {
27
+ if (!existsSync(path))
28
+ return 0;
29
+ const lock = readLock(path);
30
+ const before = serialize(lock);
31
+ const after = { skills: { ...lock.skills } };
32
+ for (const name of names)
33
+ delete after.skills[name];
34
+ return before.split(`
35
+ `).length - serialize(after).split(`
36
+ `).length;
37
+ }
38
+ function removeSkillFromLock(path, skill) {
39
+ if (!existsSync(path))
40
+ return { removed: false };
41
+ const lock = readLock(path);
42
+ if (!Object.hasOwn(lock.skills, skill))
43
+ return { removed: false };
44
+ delete lock.skills[skill];
45
+ writeLock(path, lock);
46
+ return { removed: true };
47
+ }
48
+
49
+ // src/utils/ansi.ts
50
+ var enabled = false;
51
+ function setColorEnabled(value) {
52
+ enabled = value;
53
+ }
54
+ function detectColorSupport() {
55
+ if (process.env.NO_COLOR)
56
+ return false;
57
+ if (process.env.FORCE_COLOR)
58
+ return true;
59
+ return Boolean(process.stdout.isTTY);
60
+ }
61
+ function green(s) {
62
+ return enabled ? `\x1B[32m${s}\x1B[0m` : s;
63
+ }
64
+ function yellow(s) {
65
+ return enabled ? `\x1B[33m${s}\x1B[0m` : s;
66
+ }
67
+ function red(s) {
68
+ return enabled ? `\x1B[31m${s}\x1B[0m` : s;
69
+ }
70
+ function cyan(s) {
71
+ return enabled ? `\x1B[36m${s}\x1B[0m` : s;
72
+ }
73
+
74
+ // src/utils/discover-skills.ts
75
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
76
+ import { homedir as homedir3 } from "node:os";
77
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
78
+
79
+ // src/utils/skill-files.ts
80
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
81
+ import { homedir as homedir2 } from "node:os";
82
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
83
+ var CHARS_PER_TOKEN = 4;
84
+ function extractFrontmatter(content) {
85
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
86
+ return match?.[1];
87
+ }
88
+ function estimateTokens(text) {
89
+ return Math.round(text.length / CHARS_PER_TOKEN);
90
+ }
91
+
92
+ // src/utils/discover-skills.ts
93
+ function resolveRoots(input) {
94
+ if (input.isGlobal) {
95
+ return {
96
+ claude: join3(homedir3(), ".claude", "skills"),
97
+ agents: join3(homedir3(), ".agents", "skills")
98
+ };
99
+ }
100
+ const repo = dirname3(resolve2(input.lockPath));
101
+ return {
102
+ claude: join3(repo, ".claude", "skills"),
103
+ agents: join3(repo, ".agents", "skills")
104
+ };
105
+ }
106
+ function listSkillNames(root) {
107
+ if (!root || !existsSync3(root))
108
+ return [];
109
+ return readdirSync(root).filter((name) => {
110
+ const skill = join3(root, name, "SKILL.md");
111
+ return existsSync3(skill) && statSync(skill).isFile();
112
+ });
113
+ }
114
+ function tokensFromFile(path) {
115
+ const content = readFileSync3(path, "utf8");
116
+ const fm = extractFrontmatter(content);
117
+ if (fm === undefined)
118
+ return { status: "no-frontmatter" };
119
+ return { tokens: estimateTokens(fm), status: "ok" };
120
+ }
121
+ function discoverSkills(input) {
122
+ const roots = resolveRoots(input);
123
+ const lock = readLock(input.lockPath);
124
+ const lockNames = Object.keys(lock.skills);
125
+ const claudeNames = listSkillNames(roots.claude);
126
+ const agentsNames = listSkillNames(roots.agents);
127
+ const all = new Set([...lockNames, ...claudeNames, ...agentsNames]);
128
+ const out = new Map;
129
+ for (const name of all) {
130
+ const sources = [];
131
+ if (lockNames.includes(name))
132
+ sources.push("lock");
133
+ if (claudeNames.includes(name))
134
+ sources.push(".claude");
135
+ if (agentsNames.includes(name))
136
+ sources.push(".agents");
137
+ let skillFile;
138
+ if (claudeNames.includes(name) && roots.claude) {
139
+ skillFile = join3(roots.claude, name, "SKILL.md");
140
+ } else if (agentsNames.includes(name) && roots.agents) {
141
+ skillFile = join3(roots.agents, name, "SKILL.md");
142
+ }
143
+ if (!skillFile) {
144
+ out.set(name, { name, sources, status: "missing" });
145
+ continue;
146
+ }
147
+ const { tokens, status } = tokensFromFile(skillFile);
148
+ out.set(name, { name, sources, skillFile, frontmatterTokens: tokens, status });
149
+ }
150
+ return out;
151
+ }
152
+
153
+ export { __require, getLockPath, readLock, countLockLinesToRemove, removeSkillFromLock, setColorEnabled, detectColorSupport, green, yellow, red, cyan, discoverSkills };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",