@maccesar/aiskills 1.16.0 → 1.16.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/lib/claude-plugin.js +105 -0
- package/lib/commands/doctor.js +50 -3
- package/lib/commands/skills.js +19 -4
- package/lib/commands/update.js +4 -0
- package/lib/config.js +11 -0
- package/lib/installer.js +18 -0
- package/lib/symlink.js +5 -10
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of the maccesar-aiskills Claude Code marketplace plugin.
|
|
3
|
+
*
|
|
4
|
+
* When the plugin is installed, Claude Code already lists our skills and slash
|
|
5
|
+
* commands from its plugin cache, so the CLI must not also install its own copy
|
|
6
|
+
* into ~/.claude/ — that produces duplicate entries in the autocomplete.
|
|
7
|
+
*
|
|
8
|
+
* The subtlety, and the reason this lives in its own module: **the cache on disk
|
|
9
|
+
* does not mean the plugin is installed.** Uninstalling a plugin removes it from
|
|
10
|
+
* `enabledPlugins` in settings.json but leaves the cache directory behind.
|
|
11
|
+
* Treating that leftover directory as proof of installation makes the CLI skip
|
|
12
|
+
* work it should do, which leaves Claude Code with no skills at all and no way
|
|
13
|
+
* for the user to repair it by re-running install. So the question we answer here
|
|
14
|
+
* is "is the plugin enabled AND does it carry this file", never just the latter.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import {
|
|
20
|
+
CLAUDE_PLUGIN_KEY,
|
|
21
|
+
getClaudePluginSkillsPath,
|
|
22
|
+
getClaudeSettingsPaths,
|
|
23
|
+
} from './config.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether Claude Code currently has the aiskills plugin enabled.
|
|
27
|
+
* @param {string} baseDir - Optional base directory (defaults to homedir via config)
|
|
28
|
+
* @returns {boolean} True only when a settings file explicitly enables the plugin
|
|
29
|
+
*/
|
|
30
|
+
export function isClaudePluginEnabled(baseDir) {
|
|
31
|
+
for (const settingsPath of getClaudeSettingsPaths(baseDir)) {
|
|
32
|
+
try {
|
|
33
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
34
|
+
if (settings?.enabledPlugins?.[CLAUDE_PLUGIN_KEY] === true) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Missing or malformed settings: nothing here says the plugin is enabled.
|
|
39
|
+
// Falling through to `false` is the safe direction — the cost of a wrong
|
|
40
|
+
// `false` is a duplicate entry, the cost of a wrong `true` is a user with
|
|
41
|
+
// no skills installed.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a plugin cache directory exists at all, regardless of whether the
|
|
49
|
+
* plugin is enabled. An enabled plugin implies a cache; a cache implies nothing,
|
|
50
|
+
* because uninstalling leaves it behind. Diagnostics use this to tell "never
|
|
51
|
+
* installed" apart from "uninstalled, leftovers on disk".
|
|
52
|
+
* @param {string} baseDir - Optional base directory
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function hasClaudePluginCache(baseDir) {
|
|
56
|
+
return existsSync(getClaudePluginSkillsPath(baseDir));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether any cached version of the plugin carries the given file.
|
|
61
|
+
* @param {string} kind - Subdirectory inside the plugin ('skills' or 'commands')
|
|
62
|
+
* @param {string} entry - Entry to look for (skill directory or command file)
|
|
63
|
+
* @param {string} baseDir - Optional base directory
|
|
64
|
+
* @returns {boolean} True if a cached version contains it
|
|
65
|
+
*/
|
|
66
|
+
function pluginCacheContains(kind, entry, baseDir) {
|
|
67
|
+
const pluginBase = getClaudePluginSkillsPath(baseDir);
|
|
68
|
+
if (!existsSync(pluginBase)) return false;
|
|
69
|
+
try {
|
|
70
|
+
return readdirSync(pluginBase).some((version) =>
|
|
71
|
+
existsSync(join(pluginBase, version, kind, entry))
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether the installed plugin already provides this skill to Claude Code.
|
|
80
|
+
* @param {string} skillName - Skill name (e.g. 'session-log')
|
|
81
|
+
* @param {string} baseDir - Optional base directory
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
export function pluginProvidesSkill(skillName, baseDir) {
|
|
85
|
+
return isClaudePluginEnabled(baseDir) && pluginCacheContains('skills', skillName, baseDir);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether the installed plugin already provides this slash command.
|
|
90
|
+
* @param {string} commandName - Command name without the .md extension
|
|
91
|
+
* @param {string} baseDir - Optional base directory
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
export function pluginProvidesCommand(commandName, baseDir) {
|
|
95
|
+
return (
|
|
96
|
+
isClaudePluginEnabled(baseDir) && pluginCacheContains('commands', `${commandName}.md`, baseDir)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default {
|
|
101
|
+
isClaudePluginEnabled,
|
|
102
|
+
hasClaudePluginCache,
|
|
103
|
+
pluginProvidesSkill,
|
|
104
|
+
pluginProvidesCommand,
|
|
105
|
+
};
|
package/lib/commands/doctor.js
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
} from '../config.js';
|
|
17
17
|
import { hasHook } from '../hooks.js';
|
|
18
18
|
import { readLastCheck } from '../cache.js';
|
|
19
|
+
import {
|
|
20
|
+
isClaudePluginEnabled,
|
|
21
|
+
hasClaudePluginCache,
|
|
22
|
+
pluginProvidesSkill,
|
|
23
|
+
} from '../claude-plugin.js';
|
|
19
24
|
|
|
20
25
|
const CHECK = chalk.green('✓');
|
|
21
26
|
const CROSS = chalk.red('✗');
|
|
@@ -79,8 +84,18 @@ export async function doctorCommand() {
|
|
|
79
84
|
for (const platform of platforms) {
|
|
80
85
|
const missing = [];
|
|
81
86
|
const broken = [];
|
|
87
|
+
const servedByPlugin = [];
|
|
82
88
|
|
|
83
89
|
for (const skill of SKILLS) {
|
|
90
|
+
// A skill the marketplace plugin provides is *supposed* to have no mirror
|
|
91
|
+
// here — the CLI removes it on purpose to avoid a duplicate entry. Counting
|
|
92
|
+
// it as missing turns a healthy marketplace install into a wall of errors
|
|
93
|
+
// telling the user to run a command that will correctly do nothing.
|
|
94
|
+
if (platform.name === 'claude' && pluginProvidesSkill(skill, homeDir)) {
|
|
95
|
+
servedByPlugin.push(skill);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
const linkPath = join(platform.skillsDir, skill);
|
|
85
100
|
try {
|
|
86
101
|
const stat = lstatSync(linkPath);
|
|
@@ -96,15 +111,26 @@ export async function doctorCommand() {
|
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
const
|
|
114
|
+
const expected = SKILLS.length - servedByPlugin.length;
|
|
115
|
+
const linkedCount = expected - missing.length - broken.length;
|
|
116
|
+
const pluginNote =
|
|
117
|
+
servedByPlugin.length > 0
|
|
118
|
+
? chalk.dim(` (+${servedByPlugin.length} served by the marketplace plugin)`)
|
|
119
|
+
: '';
|
|
100
120
|
|
|
101
121
|
if (missing.length === 0 && broken.length === 0) {
|
|
102
|
-
|
|
122
|
+
const summary =
|
|
123
|
+
expected === 0
|
|
124
|
+
? `all ${servedByPlugin.length} skills served by the marketplace plugin`
|
|
125
|
+
: `${linkedCount}/${expected} skills linked${pluginNote}`;
|
|
126
|
+
console.log(` ${CHECK} ${platform.displayName}: ${summary}`);
|
|
103
127
|
} else {
|
|
104
128
|
const problems = [];
|
|
105
129
|
if (missing.length > 0) problems.push(`missing: ${missing.join(', ')}`);
|
|
106
130
|
if (broken.length > 0) problems.push(`broken: ${broken.join(', ')}`);
|
|
107
|
-
console.log(
|
|
131
|
+
console.log(
|
|
132
|
+
` ${CROSS} ${platform.displayName}: ${linkedCount}/${expected} skills linked${pluginNote} (${problems.join('; ')})`
|
|
133
|
+
);
|
|
108
134
|
issues += missing.length + broken.length;
|
|
109
135
|
|
|
110
136
|
// Collect symlink issues for detailed report
|
|
@@ -117,6 +143,27 @@ export async function doctorCommand() {
|
|
|
117
143
|
}
|
|
118
144
|
}
|
|
119
145
|
|
|
146
|
+
// Marketplace plugin
|
|
147
|
+
//
|
|
148
|
+
// Worth its own section because the two channels look identical from the
|
|
149
|
+
// outside and produce opposite expectations: with the plugin enabled, absent
|
|
150
|
+
// mirrors are correct; without it, absent mirrors mean no skills at all.
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(' Marketplace plugin:');
|
|
153
|
+
const pluginEnabled = isClaudePluginEnabled(homeDir);
|
|
154
|
+
const pluginCached = hasClaudePluginCache(homeDir);
|
|
155
|
+
|
|
156
|
+
if (pluginEnabled) {
|
|
157
|
+
console.log(` ${CHECK} Enabled — Claude Code is served by the plugin, mirrors intentionally absent`);
|
|
158
|
+
} else if (pluginCached) {
|
|
159
|
+
console.log(` ${WARN} Not enabled, but a cache directory remains from a previous install`);
|
|
160
|
+
console.log(` ${chalk.dim('Harmless on this version. On aiskills < 1.16.1 it made install skip')}`);
|
|
161
|
+
console.log(` ${chalk.dim('every symlink, leaving Claude Code with no skills. Remove it with:')}`);
|
|
162
|
+
console.log(` ${chalk.cyan('rm -rf ~/.claude/plugins/cache/maccesar-aiskills')}`);
|
|
163
|
+
} else {
|
|
164
|
+
console.log(` ${CHECK} Not installed — skills reach Claude Code through npm mirrors`);
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
// Symlink issues detail
|
|
121
168
|
if (symlinkIssues.length > 0) {
|
|
122
169
|
console.log('');
|
package/lib/commands/skills.js
CHANGED
|
@@ -79,10 +79,12 @@ export async function skillsCommand(options) {
|
|
|
79
79
|
chalk.dim('(agentskills.io standard)')
|
|
80
80
|
);
|
|
81
81
|
console.log(
|
|
82
|
-
' ' + chalk.
|
|
82
|
+
' ' + chalk.green('✓'),
|
|
83
|
+
chalk.dim('Gemini, Codex, Cursor, Cline, Amp, GitHub Copilot +more read it directly —')
|
|
83
84
|
);
|
|
85
|
+
console.log(' ' + chalk.dim('nothing else to configure for them.'));
|
|
84
86
|
console.log(
|
|
85
|
-
' ' + chalk.dim('
|
|
87
|
+
' ' + chalk.dim('Claude Code needs symlink mirrors, created below.')
|
|
86
88
|
);
|
|
87
89
|
console.log('');
|
|
88
90
|
|
|
@@ -112,10 +114,19 @@ export async function skillsCommand(options) {
|
|
|
112
114
|
process.exit(1);
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
// Show detected platforms
|
|
117
|
+
// Show detected platforms.
|
|
118
|
+
//
|
|
119
|
+
// Only assistants that need aiskills-managed mirrors appear here, so a bare
|
|
120
|
+
// "Claude Code detected" reads as if the other assistants were not found at
|
|
121
|
+
// all. They were never looked for: Gemini, Codex and the rest read
|
|
122
|
+
// ~/.agents/skills/ directly and are already served by the install itself.
|
|
116
123
|
if (detectedPlatforms.length > 0) {
|
|
117
124
|
for (const platform of detectedPlatforms) {
|
|
118
|
-
console.log(
|
|
125
|
+
console.log(
|
|
126
|
+
chalk.green('✓'),
|
|
127
|
+
`${platform.displayName} detected`,
|
|
128
|
+
chalk.dim('— needs mirrors, linked below')
|
|
129
|
+
);
|
|
119
130
|
}
|
|
120
131
|
console.log('');
|
|
121
132
|
} else if (isLocal) {
|
|
@@ -269,6 +280,10 @@ export async function skillsCommand(options) {
|
|
|
269
280
|
spinner.succeed(
|
|
270
281
|
`${commandsResult.installed.length} slash command${commandsResult.installed.length !== 1 ? 's' : ''} installed`
|
|
271
282
|
);
|
|
283
|
+
} else if (commandsResult.skipped.length > 0) {
|
|
284
|
+
spinner.info(
|
|
285
|
+
`${commandsResult.skipped.length} slash command${commandsResult.skipped.length !== 1 ? 's' : ''} already provided by the marketplace plugin`
|
|
286
|
+
);
|
|
272
287
|
} else {
|
|
273
288
|
spinner.info('No slash commands to install');
|
|
274
289
|
}
|
package/lib/commands/update.js
CHANGED
|
@@ -67,6 +67,10 @@ async function performUpdate(baseDir, repoDir, spinner) {
|
|
|
67
67
|
spinner.succeed(
|
|
68
68
|
`${commandsResult.installed.length} slash command${commandsResult.installed.length !== 1 ? 's' : ''} synced`
|
|
69
69
|
);
|
|
70
|
+
} else if (commandsResult.skipped.length > 0) {
|
|
71
|
+
spinner.info(
|
|
72
|
+
`${commandsResult.skipped.length} slash command${commandsResult.skipped.length !== 1 ? 's' : ''} already provided by the marketplace plugin`
|
|
73
|
+
);
|
|
70
74
|
} else {
|
|
71
75
|
spinner.info('No slash commands to sync');
|
|
72
76
|
}
|
package/lib/config.js
CHANGED
|
@@ -64,6 +64,17 @@ export const CLAUDE_PLUGIN_NAME = 'aiskills';
|
|
|
64
64
|
export const getClaudePluginSkillsPath = (baseDir = os.homedir()) =>
|
|
65
65
|
path.join(baseDir, '.claude', 'plugins', 'cache', CLAUDE_PLUGIN_MARKETPLACE, CLAUDE_PLUGIN_NAME);
|
|
66
66
|
|
|
67
|
+
// The key Claude Code writes under "enabledPlugins" when the plugin is installed.
|
|
68
|
+
export const CLAUDE_PLUGIN_KEY = `${CLAUDE_PLUGIN_NAME}@${CLAUDE_PLUGIN_MARKETPLACE}`;
|
|
69
|
+
|
|
70
|
+
// Where Claude Code records which plugins are enabled. Both files are consulted
|
|
71
|
+
// because the local variant overrides the shared one, and either may carry the
|
|
72
|
+
// entry depending on how the plugin was installed.
|
|
73
|
+
export const getClaudeSettingsPaths = (baseDir = os.homedir()) => [
|
|
74
|
+
path.join(baseDir, '.claude', 'settings.json'),
|
|
75
|
+
path.join(baseDir, '.claude', 'settings.local.json'),
|
|
76
|
+
];
|
|
77
|
+
|
|
67
78
|
// AI platform detection
|
|
68
79
|
//
|
|
69
80
|
// Only platforms that need aiskills-managed symlinks appear here.
|
package/lib/installer.js
CHANGED
|
@@ -135,6 +135,7 @@ export async function installCommands(repoDir, baseDir = os.homedir()) {
|
|
|
135
135
|
installed: [],
|
|
136
136
|
failed: [],
|
|
137
137
|
removed: [],
|
|
138
|
+
skipped: [],
|
|
138
139
|
};
|
|
139
140
|
|
|
140
141
|
const legacyLocal = removeCommands(baseDir, { legacyOnly: true });
|
|
@@ -147,7 +148,24 @@ export async function installCommands(repoDir, baseDir = os.homedir()) {
|
|
|
147
148
|
results.failed.push(...legacyGlobal.failed);
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
const { pluginProvidesCommand } = await import('./claude-plugin.js');
|
|
152
|
+
const commandsDir = getClaudeCommandsDir(baseDir);
|
|
153
|
+
|
|
150
154
|
for (const cmd of COMMANDS) {
|
|
155
|
+
// Same rule the symlink step applies to skills: when the marketplace plugin
|
|
156
|
+
// already provides the command, installing our own copy makes it show up
|
|
157
|
+
// twice in the autocomplete. Clean up any copy left from before the plugin
|
|
158
|
+
// was installed.
|
|
159
|
+
if (pluginProvidesCommand(cmd, baseDir)) {
|
|
160
|
+
const stalePath = join(commandsDir, `${cmd}.md`);
|
|
161
|
+
if (existsSync(stalePath)) {
|
|
162
|
+
await remove(stalePath);
|
|
163
|
+
results.removed.push(cmd);
|
|
164
|
+
}
|
|
165
|
+
results.skipped.push(cmd);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
if (await installCommand(repoDir, cmd, baseDir)) {
|
|
152
170
|
results.installed.push(cmd);
|
|
153
171
|
} else {
|
package/lib/symlink.js
CHANGED
|
@@ -75,21 +75,16 @@ async function removePath(path) {
|
|
|
75
75
|
* and an additional symlink at ~/.claude/skills/<skill> produces a duplicate
|
|
76
76
|
* entry in the slash-command autocomplete. This helper lets the symlink step
|
|
77
77
|
* skip Claude when the plugin already covers it.
|
|
78
|
+
*
|
|
79
|
+
* Requires the plugin to be *enabled*, not merely cached — see `claude-plugin.js`
|
|
80
|
+
* for why the distinction matters.
|
|
78
81
|
* @param {string} skillName - Skill name (e.g. 'stitch-showcase')
|
|
79
82
|
* @param {string} baseDir - Optional base directory (defaults to homedir via config)
|
|
80
83
|
* @returns {Promise<boolean>} True if the plugin provides this skill
|
|
81
84
|
*/
|
|
82
85
|
export async function isClaudePluginSkillInstalled(skillName, baseDir) {
|
|
83
|
-
const {
|
|
84
|
-
|
|
85
|
-
const pluginBase = getClaudePluginSkillsPath(baseDir);
|
|
86
|
-
if (!existsSync(pluginBase)) return false;
|
|
87
|
-
try {
|
|
88
|
-
const versions = await readdir(pluginBase);
|
|
89
|
-
return versions.some((v) => existsSync(join(pluginBase, v, 'skills', skillName)));
|
|
90
|
-
} catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
86
|
+
const { pluginProvidesSkill } = await import('./claude-plugin.js');
|
|
87
|
+
return pluginProvidesSkill(skillName, baseDir);
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
/**
|