skillio 0.1.9 → 0.1.11
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/cli.js +396 -318
- package/dist/shared/chunk-0qvp6v8g.js +138 -0
- package/dist/shared/chunk-j9jc4e4c.js +163 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
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 writeLock(path, lock) {
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
const tmp = join(dirname(path), `.${Date.now()}.skill-lock.json`);
|
|
19
|
+
writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
|
|
20
|
+
`);
|
|
21
|
+
renameSync(tmp, path);
|
|
22
|
+
}
|
|
23
|
+
function removeSkillFromLock(path, skill) {
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return { removed: false };
|
|
26
|
+
const lock = readLock(path);
|
|
27
|
+
if (!Object.hasOwn(lock.skills, skill))
|
|
28
|
+
return { removed: false };
|
|
29
|
+
delete lock.skills[skill];
|
|
30
|
+
writeLock(path, lock);
|
|
31
|
+
return { removed: true };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/ansi.ts
|
|
35
|
+
var enabled = false;
|
|
36
|
+
function setColorEnabled(value) {
|
|
37
|
+
enabled = value;
|
|
38
|
+
}
|
|
39
|
+
function detectColorSupport() {
|
|
40
|
+
if (process.env.NO_COLOR)
|
|
41
|
+
return false;
|
|
42
|
+
if (process.env.FORCE_COLOR)
|
|
43
|
+
return true;
|
|
44
|
+
return Boolean(process.stdout.isTTY);
|
|
45
|
+
}
|
|
46
|
+
function green(s) {
|
|
47
|
+
return enabled ? `\x1B[32m${s}\x1B[0m` : s;
|
|
48
|
+
}
|
|
49
|
+
function yellow(s) {
|
|
50
|
+
return enabled ? `\x1B[33m${s}\x1B[0m` : s;
|
|
51
|
+
}
|
|
52
|
+
function red(s) {
|
|
53
|
+
return enabled ? `\x1B[31m${s}\x1B[0m` : s;
|
|
54
|
+
}
|
|
55
|
+
function cyan(s) {
|
|
56
|
+
return enabled ? `\x1B[36m${s}\x1B[0m` : s;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/utils/discover-skills.ts
|
|
60
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
61
|
+
import { homedir as homedir3 } from "node:os";
|
|
62
|
+
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
63
|
+
|
|
64
|
+
// src/utils/skill-files.ts
|
|
65
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
66
|
+
import { homedir as homedir2 } from "node:os";
|
|
67
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
68
|
+
var CHARS_PER_TOKEN = 4;
|
|
69
|
+
function extractFrontmatter(content) {
|
|
70
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
71
|
+
return match?.[1];
|
|
72
|
+
}
|
|
73
|
+
function estimateTokens(text) {
|
|
74
|
+
return Math.round(text.length / CHARS_PER_TOKEN);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/utils/discover-skills.ts
|
|
78
|
+
function resolveRoots(input) {
|
|
79
|
+
if (input.isGlobal) {
|
|
80
|
+
return {
|
|
81
|
+
claude: join3(homedir3(), ".claude", "skills"),
|
|
82
|
+
agents: join3(homedir3(), ".agents", "skills")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const repo = dirname3(resolve2(input.lockPath));
|
|
86
|
+
return {
|
|
87
|
+
claude: join3(repo, ".claude", "skills"),
|
|
88
|
+
agents: join3(repo, ".agents", "skills")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function listSkillNames(root) {
|
|
92
|
+
if (!root || !existsSync3(root))
|
|
93
|
+
return [];
|
|
94
|
+
return readdirSync(root).filter((name) => {
|
|
95
|
+
const skill = join3(root, name, "SKILL.md");
|
|
96
|
+
return existsSync3(skill) && statSync(skill).isFile();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function tokensFromFile(path) {
|
|
100
|
+
const content = readFileSync3(path, "utf8");
|
|
101
|
+
const fm = extractFrontmatter(content);
|
|
102
|
+
if (fm === undefined)
|
|
103
|
+
return { status: "no-frontmatter" };
|
|
104
|
+
return { tokens: estimateTokens(fm), status: "ok" };
|
|
105
|
+
}
|
|
106
|
+
function discoverSkills(input) {
|
|
107
|
+
const roots = resolveRoots(input);
|
|
108
|
+
const lock = readLock(input.lockPath);
|
|
109
|
+
const lockNames = Object.keys(lock.skills);
|
|
110
|
+
const claudeNames = listSkillNames(roots.claude);
|
|
111
|
+
const agentsNames = listSkillNames(roots.agents);
|
|
112
|
+
const all = new Set([...lockNames, ...claudeNames, ...agentsNames]);
|
|
113
|
+
const out = new Map;
|
|
114
|
+
for (const name of all) {
|
|
115
|
+
const sources = [];
|
|
116
|
+
if (lockNames.includes(name))
|
|
117
|
+
sources.push("lock");
|
|
118
|
+
if (claudeNames.includes(name))
|
|
119
|
+
sources.push(".claude");
|
|
120
|
+
if (agentsNames.includes(name))
|
|
121
|
+
sources.push(".agents");
|
|
122
|
+
let skillFile;
|
|
123
|
+
if (claudeNames.includes(name) && roots.claude) {
|
|
124
|
+
skillFile = join3(roots.claude, name, "SKILL.md");
|
|
125
|
+
} else if (agentsNames.includes(name) && roots.agents) {
|
|
126
|
+
skillFile = join3(roots.agents, name, "SKILL.md");
|
|
127
|
+
}
|
|
128
|
+
if (!skillFile) {
|
|
129
|
+
out.set(name, { name, sources, status: "missing" });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const { tokens, status } = tokensFromFile(skillFile);
|
|
133
|
+
out.set(name, { name, sources, skillFile, frontmatterTokens: tokens, status });
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { __require, getLockPath, readLock, removeSkillFromLock, setColorEnabled, detectColorSupport, green, yellow, red, cyan, discoverSkills };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cyan,
|
|
3
|
+
discoverSkills,
|
|
4
|
+
getLockPath,
|
|
5
|
+
red
|
|
6
|
+
} from "./chunk-0qvp6v8g.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
|
+
|
|
103
|
+
// src/commands/picker.ts
|
|
104
|
+
var CANCEL = "__cancel__";
|
|
105
|
+
async function pickRemoveTarget(args) {
|
|
106
|
+
const lockPath = getLockPath(args.global);
|
|
107
|
+
const { inLock, orphan } = listRemovableTargets({
|
|
108
|
+
isGlobal: args.global,
|
|
109
|
+
cwd: process.cwd(),
|
|
110
|
+
lockPath
|
|
111
|
+
});
|
|
112
|
+
if (inLock.length === 0 && orphan.length === 0) {
|
|
113
|
+
console.log("No skills found in scope.");
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const options = [
|
|
117
|
+
...inLock.map((name) => ({ value: name, label: name })),
|
|
118
|
+
...orphan.map((name) => ({ value: name, label: `${name} ${red("(orphan)")}` })),
|
|
119
|
+
{ value: CANCEL, label: "cancel" }
|
|
120
|
+
];
|
|
121
|
+
const choice = await select({ title: "skillio — pick a skill to remove", options });
|
|
122
|
+
if (choice === null || choice === CANCEL)
|
|
123
|
+
return null;
|
|
124
|
+
return choice;
|
|
125
|
+
}
|
|
126
|
+
async function runPicker(args) {
|
|
127
|
+
const choice = await select({
|
|
128
|
+
title: "skillio — pick a command",
|
|
129
|
+
options: [
|
|
130
|
+
{ value: "usage", label: "usage — count of skill invocations" },
|
|
131
|
+
{ value: "cost", label: "cost — per-skill ambient tokens" },
|
|
132
|
+
{ value: "list", label: "list — installed skills per source" },
|
|
133
|
+
{ value: "remove", label: "remove — delete a skill (disk-only; lock with --force-lock)" },
|
|
134
|
+
{ value: "quit", label: "quit" }
|
|
135
|
+
]
|
|
136
|
+
});
|
|
137
|
+
if (choice === null || choice === "quit")
|
|
138
|
+
return 0;
|
|
139
|
+
const cliPath = process.argv[1];
|
|
140
|
+
if (!cliPath) {
|
|
141
|
+
console.error("skillio: cannot resolve CLI path (process.argv[1] missing)");
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
let argv;
|
|
145
|
+
if (choice === "remove") {
|
|
146
|
+
const target = await pickRemoveTarget(args);
|
|
147
|
+
if (target === null)
|
|
148
|
+
return 0;
|
|
149
|
+
argv = ["rm", target];
|
|
150
|
+
} else {
|
|
151
|
+
argv = [choice];
|
|
152
|
+
}
|
|
153
|
+
if (args.global)
|
|
154
|
+
argv.push("-g");
|
|
155
|
+
const r = spawnSync(process.execPath, [cliPath, ...argv], {
|
|
156
|
+
stdio: "inherit",
|
|
157
|
+
env: process.env
|
|
158
|
+
});
|
|
159
|
+
return r.status ?? 0;
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
runPicker
|
|
163
|
+
};
|