skillmaxxing 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.
Files changed (64) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/LICENSE +21 -0
  4. package/README.md +152 -0
  5. package/dist/agents/claude.js +12 -0
  6. package/dist/agents/codex.js +12 -0
  7. package/dist/agents/cursor.js +12 -0
  8. package/dist/agents/hermes.js +12 -0
  9. package/dist/agents/opencode.js +12 -0
  10. package/dist/agents/registry.js +22 -0
  11. package/dist/agents/types.js +1 -0
  12. package/dist/cli.js +291 -0
  13. package/dist/commands/discover.js +76 -0
  14. package/dist/commands/doctor.js +84 -0
  15. package/dist/commands/init.js +47 -0
  16. package/dist/commands/install.js +74 -0
  17. package/dist/commands/list.js +74 -0
  18. package/dist/commands/optimize.js +152 -0
  19. package/dist/commands/plugin.js +232 -0
  20. package/dist/commands/remove.js +48 -0
  21. package/dist/commands/skillify.js +74 -0
  22. package/dist/commands/update.js +52 -0
  23. package/dist/commands/workspace.js +117 -0
  24. package/dist/create/match.js +23 -0
  25. package/dist/create/reflect.js +49 -0
  26. package/dist/create/skillify.js +117 -0
  27. package/dist/discover/collect.js +40 -0
  28. package/dist/discover/github.js +27 -0
  29. package/dist/discover/index.js +39 -0
  30. package/dist/discover/local.js +55 -0
  31. package/dist/discover/rank.js +63 -0
  32. package/dist/discover/types.js +1 -0
  33. package/dist/eval/runner.js +81 -0
  34. package/dist/eval/schema.js +78 -0
  35. package/dist/eval/scorers.js +19 -0
  36. package/dist/lock/global.js +53 -0
  37. package/dist/lock/project.js +67 -0
  38. package/dist/optimize/budget.js +22 -0
  39. package/dist/optimize/buffer.js +33 -0
  40. package/dist/optimize/diff.js +89 -0
  41. package/dist/optimize/loop.js +49 -0
  42. package/dist/plugin/guidance.js +30 -0
  43. package/dist/plugin/reflect.js +63 -0
  44. package/dist/plugin/sessions.js +58 -0
  45. package/dist/source/parser.js +63 -0
  46. package/dist/source/resolver.js +120 -0
  47. package/dist/state/store.js +120 -0
  48. package/dist/state/trust.js +31 -0
  49. package/dist/types.js +1 -0
  50. package/dist/util/collision.js +46 -0
  51. package/dist/util/exec.js +78 -0
  52. package/dist/util/frontmatter.js +72 -0
  53. package/dist/util/fs.js +77 -0
  54. package/dist/util/git.js +35 -0
  55. package/dist/util/log.js +33 -0
  56. package/dist/util/sanitize.js +36 -0
  57. package/dist/util/similarity.js +27 -0
  58. package/dist/util/versions.js +104 -0
  59. package/dist/workspace/channels.js +14 -0
  60. package/dist/workspace/collab.js +103 -0
  61. package/dist/workspace/registry.js +113 -0
  62. package/hooks/hooks.json +26 -0
  63. package/index/index.json +5 -0
  64. package/package.json +53 -0
@@ -0,0 +1,35 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import * as fs from 'node:fs';
6
+ const execFileAsync = promisify(execFile);
7
+ export async function gitClone(url, dest, ref) {
8
+ const args = ['clone', '--depth', '1'];
9
+ if (ref)
10
+ args.push('--branch', ref);
11
+ args.push(url, dest);
12
+ await execFileAsync('git', args, {
13
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_LFS_SKIP_SMUDGE: '1' },
14
+ timeout: 60_000,
15
+ });
16
+ }
17
+ export async function gitGetHeadSha(dir) {
18
+ const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: dir });
19
+ return stdout.trim();
20
+ }
21
+ export function makeTempDir(prefix) {
22
+ return fs.mkdtempSync(path.join(os.tmpdir(), `skillmax-${prefix}-`));
23
+ }
24
+ export function cleanTempDir(dir) {
25
+ const resolved = path.resolve(dir);
26
+ const tmpdir = path.resolve(os.tmpdir());
27
+ if (!resolved.startsWith(tmpdir + path.sep))
28
+ return;
29
+ try {
30
+ fs.rmSync(resolved, { recursive: true });
31
+ }
32
+ catch {
33
+ // best-effort cleanup
34
+ }
35
+ }
@@ -0,0 +1,33 @@
1
+ const RESET = '\x1b[0m';
2
+ const BOLD = '\x1b[1m';
3
+ const DIM = '\x1b[2m';
4
+ const GREEN = '\x1b[32m';
5
+ const YELLOW = '\x1b[33m';
6
+ const RED = '\x1b[31m';
7
+ const CYAN = '\x1b[36m';
8
+ export function info(msg) {
9
+ console.log(`${CYAN}i${RESET} ${msg}`);
10
+ }
11
+ export function success(msg) {
12
+ console.log(`${GREEN}✓${RESET} ${msg}`);
13
+ }
14
+ export function warn(msg) {
15
+ console.log(`${YELLOW}!${RESET} ${msg}`);
16
+ }
17
+ export function error(msg) {
18
+ console.error(`${RED}✗${RESET} ${msg}`);
19
+ }
20
+ export function heading(msg) {
21
+ console.log(`\n${BOLD}${msg}${RESET}`);
22
+ }
23
+ export function dim(msg) {
24
+ console.log(`${DIM}${msg}${RESET}`);
25
+ }
26
+ export function table(rows) {
27
+ if (rows.length === 0)
28
+ return;
29
+ const widths = rows[0].map((_, i) => Math.max(...rows.map(r => (r[i] ?? '').length)));
30
+ for (const row of rows) {
31
+ console.log(row.map((cell, i) => cell.padEnd(widths[i])).join(' '));
32
+ }
33
+ }
@@ -0,0 +1,36 @@
1
+ import * as path from 'node:path';
2
+ const NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
3
+ const MAX_NAME_LEN = 64;
4
+ export function sanitizeName(raw) {
5
+ return raw
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, '-')
8
+ .replace(/^-+|-+$/g, '')
9
+ .substring(0, MAX_NAME_LEN) || 'unnamed-skill';
10
+ }
11
+ export function validateName(name) {
12
+ if (!name)
13
+ return 'name is required';
14
+ if (name.length > MAX_NAME_LEN)
15
+ return `name exceeds ${MAX_NAME_LEN} characters`;
16
+ if (!NAME_RE.test(name))
17
+ return 'name must be lowercase alphanumeric with single hyphens, starting with a letter';
18
+ return null;
19
+ }
20
+ export function isPathSafe(basePath, targetPath) {
21
+ const resolvedBase = path.resolve(basePath);
22
+ const resolvedTarget = path.resolve(targetPath);
23
+ return resolvedTarget.startsWith(resolvedBase + path.sep) || resolvedTarget === resolvedBase;
24
+ }
25
+ export function sanitizeSubpath(subpath) {
26
+ const segments = subpath.split(/[/\\]/);
27
+ if (segments.some(s => s === '..'))
28
+ return null;
29
+ return segments.filter(s => s && s !== '.').join('/');
30
+ }
31
+ export function stripTerminalEscapes(str) {
32
+ return str
33
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '') // CSI sequences
34
+ .replace(/\x1b\][^\x07]*\x07/g, '') // OSC sequences
35
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, ''); // control chars (keep \t \n \r)
36
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared text similarity helpers used by discovery ranking, the prefer-update
3
+ * matcher, and in-session reflection. Single source of truth so tokenization
4
+ * stays consistent across all three (and future changes — stemming, stopwords —
5
+ * land in one place).
6
+ */
7
+ /** Lowercase, split on non-alphanumeric runs, drop empties. */
8
+ export function tokenize(text) {
9
+ return text
10
+ .toLowerCase()
11
+ .split(/[^a-z0-9]+/)
12
+ .filter(Boolean);
13
+ }
14
+ /** Token set of a string. */
15
+ export function tokenSet(text) {
16
+ return new Set(tokenize(text));
17
+ }
18
+ /** Jaccard similarity of two sets (0 when either is empty). */
19
+ export function jaccard(a, b) {
20
+ if (a.size === 0 || b.size === 0)
21
+ return 0;
22
+ let inter = 0;
23
+ for (const t of a)
24
+ if (b.has(t))
25
+ inter++;
26
+ return inter / (a.size + b.size - inter);
27
+ }
@@ -0,0 +1,104 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { ensureDir, copyDir, removeDir } from './fs.js';
5
+ const VERSIONS_ROOT = path.join(os.homedir(), '.skillmax', 'versions');
6
+ /** Default number of prior versions retained per skill (review SG5: bound growth). */
7
+ export const MAX_RETAINED_VERSIONS = 5;
8
+ function skillVersionsDir(id) {
9
+ return path.join(VERSIONS_ROOT, id);
10
+ }
11
+ /**
12
+ * Atomically replace `target`'s contents with a copy of `source`.
13
+ *
14
+ * Crash-safety (review C4): `source` is first copied into a sibling staged dir on
15
+ * the SAME filesystem as `target`, so the two renames below are intra-filesystem
16
+ * and atomic (sidesteps the cross-device EXDEV concern, review F5). If the swap
17
+ * rename fails, the prior `target` is rolled back from its backup — a failed swap
18
+ * never destroys the previous version.
19
+ */
20
+ export function atomicReplaceDir(target, source) {
21
+ if (!fs.existsSync(source)) {
22
+ throw new Error(`source directory not found: ${source}`);
23
+ }
24
+ // Refuse to replace a symlink: renaming the link (not its target) would leave
25
+ // the real upstream skill untouched and silently break the install topology
26
+ // (review: optimize/promote against a symlinked install dir). The caller must
27
+ // pass the resolved managed-copy directory.
28
+ if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
29
+ throw new Error(`refusing to replace a symlink: ${target} (pass the resolved skill directory)`);
30
+ }
31
+ const parent = path.dirname(target);
32
+ ensureDir(parent);
33
+ const base = path.basename(target);
34
+ const staged = path.join(parent, `.${base}.staged-${process.pid}`);
35
+ const backup = path.join(parent, `.${base}.old-${process.pid}`);
36
+ removeDir(staged);
37
+ copyDir(source, staged);
38
+ removeDir(backup);
39
+ let backedUp = false;
40
+ if (fs.existsSync(target)) {
41
+ fs.renameSync(target, backup);
42
+ backedUp = true;
43
+ }
44
+ try {
45
+ fs.renameSync(staged, target);
46
+ }
47
+ catch (err) {
48
+ if (backedUp)
49
+ fs.renameSync(backup, target); // roll back
50
+ removeDir(staged);
51
+ throw err;
52
+ }
53
+ if (backedUp)
54
+ removeDir(backup);
55
+ }
56
+ /** Copy a skill dir into the retained-versions store under <id>/<version>/. */
57
+ export function snapshot(id, version, srcDir) {
58
+ const dir = path.join(skillVersionsDir(id), version);
59
+ removeDir(dir);
60
+ copyDir(srcDir, dir);
61
+ pruneVersions(id);
62
+ return dir;
63
+ }
64
+ /** Retained version names for a skill, newest first. */
65
+ export function listVersions(id) {
66
+ const dir = skillVersionsDir(id);
67
+ let entries;
68
+ try {
69
+ entries = fs.readdirSync(dir, { withFileTypes: true });
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ return entries
75
+ .filter((e) => e.isDirectory())
76
+ .map((e) => ({ name: e.name, mtime: fs.statSync(path.join(dir, e.name)).mtimeMs }))
77
+ .sort((a, b) => b.mtime - a.mtime)
78
+ .map((e) => e.name);
79
+ }
80
+ /** Remove the oldest retained versions beyond MAX_RETAINED_VERSIONS. */
81
+ export function pruneVersions(id, keep = MAX_RETAINED_VERSIONS) {
82
+ const versions = listVersions(id); // newest first
83
+ for (const stale of versions.slice(keep)) {
84
+ removeDir(path.join(skillVersionsDir(id), stale));
85
+ }
86
+ }
87
+ /**
88
+ * Promote a candidate into the live location: retain the current live version,
89
+ * then atomically swap in the candidate. Reversible via `revert`.
90
+ */
91
+ export function promote(params) {
92
+ if (fs.existsSync(params.liveDir)) {
93
+ snapshot(params.id, params.priorVersion, params.liveDir);
94
+ }
95
+ atomicReplaceDir(params.liveDir, params.candidateDir);
96
+ }
97
+ /** Restore a retained version into the live location atomically. */
98
+ export function revert(id, version, liveDir) {
99
+ const vdir = path.join(skillVersionsDir(id), version);
100
+ if (!fs.existsSync(vdir)) {
101
+ throw new Error(`version not retained: ${id}@${version}`);
102
+ }
103
+ atomicReplaceDir(liveDir, vdir);
104
+ }
@@ -0,0 +1,14 @@
1
+ /** Release channels in promotion order: dev → beta → stable. */
2
+ export const CHANNELS = ['dev', 'beta', 'stable'];
3
+ export function isValidChannel(value) {
4
+ return CHANNELS.includes(value);
5
+ }
6
+ /** Ordinal position of a channel (dev=0, beta=1, stable=2). */
7
+ export function channelRank(channel) {
8
+ return CHANNELS.indexOf(channel);
9
+ }
10
+ /** The next channel up the promotion ladder, or null if already at stable. */
11
+ export function nextChannel(channel) {
12
+ const i = CHANNELS.indexOf(channel);
13
+ return i >= 0 && i < CHANNELS.length - 1 ? CHANNELS[i + 1] : null;
14
+ }
@@ -0,0 +1,103 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { ensureDir, removeDir, copyDir, fileExists } from '../util/fs.js';
4
+ import { ensureValidName } from '../util/collision.js';
5
+ import { readRegistry, writeRegistry } from './registry.js';
6
+ /**
7
+ * Collaborative optimization + review/promote for the shared registry (U15).
8
+ *
9
+ * - Pooled evals are append-only JSONL (merge-friendly across contributors).
10
+ * - Promotion to a higher channel REQUIRES explicit review/approval (review S2):
11
+ * the gate refuses without `approve` + an `approver`, and records a receipt.
12
+ * - Divergent versions of the same skill in the target channel are surfaced as a
13
+ * CONFLICT, never silently merged (review R22/SG7: detection, not auto-resolve).
14
+ */
15
+ function appendJsonl(file, record) {
16
+ ensureDir(path.dirname(file));
17
+ fs.appendFileSync(file, JSON.stringify(record) + '\n');
18
+ }
19
+ function readJsonl(file) {
20
+ try {
21
+ return fs
22
+ .readFileSync(file, 'utf-8')
23
+ .split('\n')
24
+ .filter((l) => l.trim())
25
+ .map((l) => JSON.parse(l));
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ }
31
+ export function poolEval(registryDir, entry) {
32
+ appendJsonl(path.join(registryDir, 'evals', `${entry.skill}.jsonl`), entry);
33
+ }
34
+ export function pooledScores(registryDir, skill) {
35
+ return readJsonl(path.join(registryDir, 'evals', `${skill}.jsonl`));
36
+ }
37
+ export function reviewPromote(registryDir, params) {
38
+ if (!params.approve) {
39
+ return {
40
+ ok: false,
41
+ reason: `promotion to ${params.toChannel} requires review and approval (pass approve + an approver)`,
42
+ };
43
+ }
44
+ if (!params.approver) {
45
+ return { ok: false, reason: 'an approver is required for promotion' };
46
+ }
47
+ // Validate the skill name before it is joined into filesystem paths (review:
48
+ // path traversal via params.skill).
49
+ const nameCheck = ensureValidName(params.skill);
50
+ if (!nameCheck.ok) {
51
+ return { ok: false, reason: nameCheck.reason };
52
+ }
53
+ const idx = readRegistry(registryDir);
54
+ const candidates = idx.skills.filter((e) => e.name === params.skill);
55
+ if (candidates.length === 0) {
56
+ return { ok: false, reason: `"${params.skill}" is not in the registry` };
57
+ }
58
+ // Source = the entry being promoted INTO the target: the highest-versioned
59
+ // entry NOT already in the target channel (falls back to the target's own
60
+ // entry if it only exists there — a harmless no-op promote).
61
+ const promotable = candidates.filter((e) => e.channel !== params.toChannel);
62
+ const source = [...(promotable.length > 0 ? promotable : candidates)].sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }))[0];
63
+ // Conflict detection: a different version already occupies the target channel.
64
+ const targetExisting = candidates.find((e) => e.channel === params.toChannel);
65
+ if (targetExisting && targetExisting.version !== source.version) {
66
+ return {
67
+ ok: false,
68
+ reason: `conflict: ${params.skill} is already ${targetExisting.version} in ${params.toChannel} (source ${source.version}); resolve manually`,
69
+ };
70
+ }
71
+ // Already in the target channel (only a target-channel entry exists): nothing
72
+ // to copy. Short-circuit BEFORE any removeDir/copyDir — otherwise src === dst
73
+ // and removeDir(dst) would delete the only copy (review: self-channel data loss).
74
+ if (source.channel === params.toChannel) {
75
+ return { ok: true };
76
+ }
77
+ const srcDir = path.join(registryDir, 'skills', source.channel, params.skill);
78
+ if (!fileExists(path.join(srcDir, 'SKILL.md'))) {
79
+ return { ok: false, reason: `registry files missing for ${params.skill} in ${source.channel}` };
80
+ }
81
+ const dstDir = path.join(registryDir, 'skills', params.toChannel, params.skill);
82
+ removeDir(dstDir);
83
+ ensureDir(path.dirname(dstDir));
84
+ copyDir(srcDir, dstDir);
85
+ idx.skills = idx.skills.filter((e) => !(e.name === params.skill && e.channel === params.toChannel));
86
+ idx.skills.push({
87
+ name: params.skill,
88
+ channel: params.toChannel,
89
+ version: source.version,
90
+ publishedBy: source.publishedBy,
91
+ publishedAt: source.publishedAt,
92
+ });
93
+ writeRegistry(registryDir, idx);
94
+ // Append-only approval receipt (auditability; review S10 notes signing as a follow-up).
95
+ appendJsonl(path.join(registryDir, 'approvals.jsonl'), {
96
+ skill: params.skill,
97
+ toChannel: params.toChannel,
98
+ version: source.version,
99
+ approver: params.approver,
100
+ at: params.at,
101
+ });
102
+ return { ok: true };
103
+ }
@@ -0,0 +1,113 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { ensureDir, copyDir, fileExists, removeDir } from '../util/fs.js';
5
+ import { readSkillMeta } from '../util/frontmatter.js';
6
+ import { ensureValidName, namespacedName } from '../util/collision.js';
7
+ import { isPathSafe } from '../util/sanitize.js';
8
+ import { isValidChannel } from './channels.js';
9
+ import { ensureState, loadState, saveState } from '../state/store.js';
10
+ const WORKSPACE_DIR = path.join(os.homedir(), '.skillmax', 'workspace');
11
+ function indexPath(registryDir) {
12
+ return path.join(registryDir, 'registry.json');
13
+ }
14
+ export function readRegistry(registryDir) {
15
+ try {
16
+ const data = JSON.parse(fs.readFileSync(indexPath(registryDir), 'utf-8'));
17
+ if (!data || !Array.isArray(data.skills))
18
+ return { version: 1, skills: [] };
19
+ // registry.json is an UNTRUSTED shared file. Drop entries whose name or
20
+ // channel is invalid before any entry value is joined into a filesystem path
21
+ // (review: path traversal via crafted entry.name / entry.channel in sync()).
22
+ const skills = data.skills.filter((e) => e &&
23
+ typeof e.name === 'string' &&
24
+ ensureValidName(e.name).ok &&
25
+ typeof e.channel === 'string' &&
26
+ isValidChannel(e.channel));
27
+ return { version: 1, skills };
28
+ }
29
+ catch {
30
+ return { version: 1, skills: [] };
31
+ }
32
+ }
33
+ export function writeRegistry(registryDir, idx) {
34
+ ensureDir(registryDir);
35
+ // Sort entries for merge-friendliness (review C5).
36
+ const sorted = [...idx.skills].sort((a, b) => a.name.localeCompare(b.name) || a.channel.localeCompare(b.channel));
37
+ const out = { version: 1, skills: sorted };
38
+ const p = indexPath(registryDir);
39
+ const tmp = p + '.tmp';
40
+ fs.writeFileSync(tmp, JSON.stringify(out, null, 2) + '\n');
41
+ fs.renameSync(tmp, p);
42
+ }
43
+ function skillStorePath(registryDir, channel, name) {
44
+ return path.join(registryDir, 'skills', channel, name);
45
+ }
46
+ /** Publish a local skill directory into the registry under a channel. */
47
+ export function publish(skillDir, registryDir, opts) {
48
+ const skillMd = path.join(skillDir, 'SKILL.md');
49
+ if (!fileExists(skillMd))
50
+ throw new Error(`no SKILL.md in ${skillDir}`);
51
+ const meta = readSkillMeta(fs.readFileSync(skillMd, 'utf-8'));
52
+ if (!meta)
53
+ throw new Error(`invalid SKILL.md in ${skillDir}`);
54
+ const nameCheck = ensureValidName(meta.name);
55
+ if (!nameCheck.ok)
56
+ throw new Error(nameCheck.reason);
57
+ const dest = skillStorePath(registryDir, opts.channel, meta.name);
58
+ removeDir(dest);
59
+ ensureDir(path.dirname(dest));
60
+ copyDir(skillDir, dest);
61
+ const entry = {
62
+ name: meta.name,
63
+ channel: opts.channel,
64
+ version: opts.version ?? (typeof meta.version === 'string' ? meta.version : '1.0.0'),
65
+ publishedBy: opts.publishedBy,
66
+ publishedAt: opts.at,
67
+ };
68
+ const idx = readRegistry(registryDir);
69
+ idx.skills = idx.skills.filter((e) => !(e.name === entry.name && e.channel === entry.channel));
70
+ idx.skills.push(entry);
71
+ writeRegistry(registryDir, idx);
72
+ return entry;
73
+ }
74
+ /** List registry entries, optionally filtered by channel. */
75
+ export function listRegistry(registryDir, channel) {
76
+ const entries = readRegistry(registryDir).skills;
77
+ return channel ? entries.filter((e) => e.channel === channel) : entries;
78
+ }
79
+ /**
80
+ * Materialize registry skills into the local managed workspace area and record
81
+ * their state (origin: workspace, trusted: false). Synced skills are keyed by an
82
+ * origin-namespaced id when they collide with an existing local skill, so the
83
+ * local skill's state and history are never overwritten (review A5/AE2).
84
+ */
85
+ export function sync(registryDir, opts) {
86
+ const registryId = opts.registryId ?? path.basename(path.resolve(registryDir));
87
+ const entries = listRegistry(registryDir, opts.channel);
88
+ const synced = [];
89
+ for (const entry of entries) {
90
+ const src = skillStorePath(registryDir, entry.channel, entry.name);
91
+ if (!fileExists(path.join(src, 'SKILL.md')))
92
+ continue;
93
+ const existing = loadState(entry.name);
94
+ const collided = !!existing && existing.origin !== 'workspace';
95
+ const id = collided ? namespacedName(registryId, entry.name) : entry.name;
96
+ const dir = path.join(WORKSPACE_DIR, registryId, entry.name);
97
+ // Defense-in-depth: never remove/write outside the managed workspace area.
98
+ if (!isPathSafe(WORKSPACE_DIR, dir))
99
+ continue;
100
+ removeDir(dir);
101
+ ensureDir(path.dirname(dir));
102
+ copyDir(src, dir);
103
+ const state = ensureState({ name: entry.name, id, origin: 'workspace', version: entry.version, source: registryId }, opts.at);
104
+ state.origin = 'workspace';
105
+ state.channel = entry.channel;
106
+ state.version = entry.version;
107
+ state.source = registryId;
108
+ state.updatedAt = opts.at;
109
+ saveState(state);
110
+ synced.push({ name: entry.name, id, channel: entry.channel, dir, collided });
111
+ }
112
+ return synced;
113
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ { "type": "command", "command": "npx -y skillmaxxing plugin guidance" }
7
+ ]
8
+ }
9
+ ],
10
+ "PostToolUse": [
11
+ {
12
+ "matcher": "*",
13
+ "hooks": [
14
+ { "type": "command", "command": "npx -y skillmaxxing plugin on-tool" }
15
+ ]
16
+ }
17
+ ],
18
+ "Stop": [
19
+ {
20
+ "hooks": [
21
+ { "type": "command", "command": "npx -y skillmaxxing plugin on-stop --agent claude --mode auto --threshold 10" }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "_note": "Starter seed for the curated skill index. Populate with verified public skills after auditing the ecosystem (plan open item; review P5f/A9). The discover command degrades to local + explicit repo sources when this is empty.",
4
+ "skills": []
5
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "skillmaxxing",
3
+ "version": "0.1.0",
4
+ "description": "Self-evolving skills for your coding agent — auto-create and auto-improve skills as you work, no trigger needed.",
5
+ "type": "module",
6
+ "bin": {
7
+ "skillmaxxing": "./dist/cli.js",
8
+ "skill-maxing": "./dist/cli.js",
9
+ "skillmax": "./dist/cli.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsx src/cli.ts",
14
+ "check": "tsc --noEmit",
15
+ "test": "tsx --test test/**/*.test.ts",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "index",
21
+ "hooks",
22
+ ".claude-plugin",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Bennyoooo/skillmaxxing.git"
29
+ },
30
+ "homepage": "https://github.com/Bennyoooo/skillmaxxing#readme",
31
+ "bugs": "https://github.com/Bennyoooo/skillmaxxing/issues",
32
+ "keywords": [
33
+ "ai",
34
+ "agents",
35
+ "agent-skills",
36
+ "skills",
37
+ "skill-management",
38
+ "self-improving-agents"
39
+ ],
40
+ "author": "Benny Jiang",
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.0.0",
47
+ "tsx": "^4.20.0",
48
+ "typescript": "^5.8.0"
49
+ },
50
+ "dependencies": {
51
+ "yaml": "^2.9.0"
52
+ }
53
+ }