@maccesar/aiskills 1.15.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/README.md CHANGED
@@ -74,6 +74,7 @@ All three platforms use the same Agent Skills format: a `SKILL.md` file with YAM
74
74
  | audit-codebase | Auditing | Evidence-based audit methodology | 2 files |
75
75
  | vscode-extension-dev | VS Code | VS Code Extension API docs | 14 files |
76
76
  | stitch-showcase | Design Tools | Google Stitch export workflow | 16 files |
77
+ | session-log | Project | Convention + 3 A/B rounds | 2 files |
77
78
 
78
79
  Use `aiskills list` to see available skills from the command line. Pull requests are welcome.
79
80
 
@@ -133,8 +134,6 @@ Hard restrictions:
133
134
  Distribution note:
134
135
  - Available via the plugin install (Option A above). Slash commands are not distributed by the npm CLI (Option B) because they are a Claude Code feature.
135
136
 
136
- ---
137
-
138
137
  ## How skills work
139
138
 
140
139
  Skills activate based on what you ask. You can write prompts normally:
@@ -308,6 +307,52 @@ Reference files:
308
307
 
309
308
  ---
310
309
 
310
+ ### session-log
311
+
312
+ Gives a project one predictable place for its working state, so both you and any assistant know where to look instead of hunting through scattered notes. It installs a fixed four-file convention under `docs/project/` and writes a short pointer into every context file the repo has — `CLAUDE.md`, `AGENTS.md`, `GEMINI.md` — so the notes stay findable no matter which assistant opens the project next.
313
+
314
+ The convention:
315
+
316
+ | File | Holds | Loaded at startup |
317
+ | --- | --- | --- |
318
+ | `status.md` | Where the work stands: half-done things, next step, what's blocked, deployment state | **No** |
319
+ | `requirements.md` | What the system must do, and the acceptance criterion for each item | Yes |
320
+ | `decisions.md` | What was chosen and why. Append-only, dated | Yes |
321
+ | `context.md` | Documentation map, architecture, conventions, traps | Yes |
322
+
323
+ **Why `status.md` is excluded from startup.** Cached context is matched as a prefix — the first byte that differs invalidates everything after it. Status written inside a startup-loaded file means every update throws away the cache for all the stable content behind it. The file you edit most often is the one that must not load at startup.
324
+
325
+ How to use it — just say it, in whatever words you'd use anyway:
326
+
327
+ ```
328
+ "set up the project notes here — the mobile app lives at ../../Apps/MyApp"
329
+ "ya me voy, déjame anotado dónde quedé"
330
+ "where did we leave off? I haven't touched this repo in weeks"
331
+ "my CLAUDE.md has the progress and a date inside it — should I move that?"
332
+ ```
333
+
334
+ Closing a session and resuming one are different jobs and it treats them differently. On the way out it writes; on the way back in it reads `status.md` and then checks it against the repo before repeating it to you — what landed since the file was written, whether the branch it names still exists, what's uncommitted that it never mentioned. A three-week-old note is a snapshot, and the most expensive way to use one is to trust it.
335
+
336
+ There is no slash command, by design: a command and a skill doing the same job means two copies of the logic that drift apart, and a command only works in Claude Code. This is one file that Claude, Codex and Gemini all read the same way — point any of them at `skills/session-log/SKILL.md` if it doesn't pick it up on its own.
337
+
338
+ Once the convention is installed, finding the notes no longer depends on the skill at all — the pointer in `CLAUDE.md`, `AGENTS.md` and `GEMINI.md` is what any assistant reads at startup.
339
+
340
+ What it will not do:
341
+ - Commit, tag, push, or write CHANGELOG entries — that is a release, and releasing assumes the work is finished, which is the opposite of why this exists. Use `/release` for that.
342
+ - Edit your uncommitted code. It reports what it finds broken and leaves it alone.
343
+ - Invent a completion percentage. Without a fixed denominator any number is made up, so it counts what is enumerable or describes status in words.
344
+ - Write a token, a password or a client's private details into the files. They get committed, and a secret deleted in a later commit is still in the history — it records where the credential lives instead.
345
+ - Overwrite the record on arrival. If a resumed file turns out to be badly out of date it says so and offers; rewriting is your call.
346
+
347
+ Measured behaviour, across three A/B rounds against a no-skill baseline (18 runs, adversarially graded):
348
+
349
+ | | With skill | Without |
350
+ | --- | --- | --- |
351
+ | Kept volatile status out of the startup chain | 9 / 9 | 0 / 9 |
352
+ | Left the user's broken uncommitted code untouched | yes | no — fixed it unasked |
353
+
354
+ Token cost is 3–13% higher per run. **Those rounds graded an earlier layout** — a single status file versus an imported memory index — so what they establish is the split itself, not the four filenames. The paths added since (resuming against a stale file, upgrading an earlier install, monorepos, a gitignored `docs/`) have prompts written for them and have not been run. The grading notes, and an explicit account of what is and isn't measured, are in `skills/session-log/evals/`.
355
+
311
356
  ### stitch-showcase
312
357
 
313
358
  A workflow skill for processing Google Stitch design exports. It handles the full lifecycle: from raw zips to a navigable showcase, component standardization, and a visual component catalog.
@@ -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
+ };
@@ -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 linkedCount = SKILLS.length - missing.length - broken.length;
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
- console.log(` ${CHECK} ${platform.displayName}: ${SKILLS.length}/${SKILLS.length} skills linked`);
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(` ${CROSS} ${platform.displayName}: ${linkedCount}/${SKILLS.length} skills linked (${problems.join('; ')})`);
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('');
@@ -79,10 +79,12 @@ export async function skillsCommand(options) {
79
79
  chalk.dim('(agentskills.io standard)')
80
80
  );
81
81
  console.log(
82
- ' ' + chalk.dim('Read directly by Gemini, Codex, Cursor, Cline, Amp, GitHub Copilot, +more.')
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('Below: extra symlink mirrors for Claude Code.')
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(chalk.green('✓'), `${platform.displayName} detected`);
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
  }
@@ -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
@@ -30,6 +30,7 @@ export const SKILLS = [
30
30
  'audit-codebase',
31
31
  'humaniza',
32
32
  'refactoring-ui',
33
+ 'session-log',
33
34
  'stitch-showcase',
34
35
  'vscode-extension-dev',
35
36
  ];
@@ -63,6 +64,17 @@ export const CLAUDE_PLUGIN_NAME = 'aiskills';
63
64
  export const getClaudePluginSkillsPath = (baseDir = os.homedir()) =>
64
65
  path.join(baseDir, '.claude', 'plugins', 'cache', CLAUDE_PLUGIN_MARKETPLACE, CLAUDE_PLUGIN_NAME);
65
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
+
66
78
  // AI platform detection
67
79
  //
68
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 { readdir } = await import('fs/promises');
84
- const { getClaudePluginSkillsPath } = await import('./config.js');
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
  /**
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@maccesar/aiskills",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
4
4
  "description": "AI coding assistant skills for Claude Code, Gemini CLI, and Codex CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "aiskills": "./bin/aiskills.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "node --test test/**/*.test.js",
10
+ "test": "node --test test/*.test.js",
11
11
  "lint": "eslint lib/**/*.js",
12
12
  "format": "prettier --write lib/**/*.js"
13
13
  },