@ax-llm/ax 25.4.0 → 25.4.2

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
@@ -157,6 +157,35 @@ const analyzer = ax(`
157
157
  npm install @ax-llm/ax
158
158
  ```
159
159
 
160
+ Outside CI, installation sets up one Claude Code skill, `ax-llm`, with a compact
161
+ index and bundled guides your agent reads as needed. To disable future automatic
162
+ setup, run `npx @ax-llm/ax skills-auto-install off` from your project root. This
163
+ saves `ax.skills.autoInstall: false` in your project's `package.json`; commit
164
+ that setting to share it with your team. Use `skills-auto-install on` to re-enable
165
+ automatic setup on future installs. Set `AX_SKIP_SKILL_INSTALL=1` to skip setup
166
+ for a single installation; CI and this environment variable take precedence over
167
+ the saved setting.
168
+
169
+ You can also set the preference before installing Ax for the first time:
170
+
171
+ ```json
172
+ {
173
+ "ax": {
174
+ "skills": {
175
+ "autoInstall": false
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ Merge this field into your existing `package.json`. Manual
182
+ `npx @ax-llm/ax setup-claude` works even when automatic setup is disabled;
183
+ add `--all` to register every specialist or `--skill ax-ai` to install only one.
184
+ Changing the preference leaves installed skills in place, and manual setup or
185
+ removal preserves the saved preference.
186
+ Existing specialists remain installed; use `remove-claude` followed by
187
+ `setup-claude` to switch an older full installation to the compact bundle.
188
+
160
189
  ### Your First AI Feature (2 minutes)
161
190
 
162
191
  ```typescript
package/cli/index.mjs CHANGED
@@ -1,294 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * CLI for @ax-llm/ax
5
- *
6
- * Commands:
7
- * setup-claude [--force] Install/upgrade Claude Code skills to .claude/skills/<name>/SKILL.md (project-local)
8
- * remove-claude Remove all Claude Code skills
9
- *
10
- * Usage:
11
- * npx @ax-llm/ax setup-claude # Install or upgrade if newer version
12
- * npx @ax-llm/ax setup-claude --force # Force overwrite regardless of version
13
- * npx @ax-llm/ax remove-claude # Remove all skills
14
- */
15
-
16
- import {
17
- existsSync,
18
- mkdirSync,
19
- readdirSync,
20
- readFileSync,
21
- rmSync,
22
- writeFileSync,
23
- } from 'node:fs';
24
- import { basename, dirname, join } from 'node:path';
3
+ import { dirname, join } from 'node:path';
25
4
  import { fileURLToPath } from 'node:url';
5
+ import { setAutoSkillInstall } from '../scripts/skill-config.mjs';
6
+ import { installSkills, removeSkills } from '../scripts/skill-install.mjs';
26
7
 
27
- const __filename = fileURLToPath(import.meta.url);
28
- const __dirname = dirname(__filename);
29
-
30
- // Skill files directory in the package
31
- const SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills');
32
-
33
- // Base target location for skills in current working directory (project-local)
34
- const SKILLS_BASE_DIR = join(process.cwd(), '.claude', 'skills');
35
-
36
- /**
37
- * Get all *.md skill files from the source directory
38
- */
39
- function getSkillFiles() {
40
- if (!existsSync(SKILLS_SOURCE_DIR)) {
41
- return [];
42
- }
43
- return readdirSync(SKILLS_SOURCE_DIR).filter((f) => f.endsWith('.md'));
44
- }
45
-
46
- /**
47
- * Get the skill name from YAML frontmatter, falling back to filename without .md
48
- */
49
- function getSkillName(filePath) {
50
- try {
51
- const content = readFileSync(filePath, 'utf8');
52
- const match = content.match(/^name:\s*["']?([^"'\n\r]+)/m);
53
- if (match) return match[1].trim();
54
- } catch {
55
- // Fall through to fallback
56
- }
57
- return basename(filePath, '.md');
58
- }
59
-
60
- /**
61
- * Compare semver versions
62
- * Returns: 1 if a > b, -1 if a < b, 0 if equal
63
- */
64
- function compareSemver(a, b) {
65
- const parseVersion = (v) => {
66
- const match = v.match(/^(\d+)\.(\d+)\.(\d+)/);
67
- if (!match) return [0, 0, 0];
68
- return [
69
- Number.parseInt(match[1], 10),
70
- Number.parseInt(match[2], 10),
71
- Number.parseInt(match[3], 10),
72
- ];
73
- };
74
-
75
- const [aMajor, aMinor, aPatch] = parseVersion(a);
76
- const [bMajor, bMinor, bPatch] = parseVersion(b);
77
-
78
- if (aMajor !== bMajor) return aMajor > bMajor ? 1 : -1;
79
- if (aMinor !== bMinor) return aMinor > bMinor ? 1 : -1;
80
- if (aPatch !== bPatch) return aPatch > bPatch ? 1 : -1;
81
- return 0;
82
- }
83
-
84
- /**
85
- * Get the installed skill version from a file
86
- */
87
- function getInstalledVersion(targetPath) {
88
- if (!existsSync(targetPath)) {
89
- return null;
90
- }
91
-
92
- try {
93
- const content = readFileSync(targetPath, 'utf8');
94
- const match = content.match(/^version:\s*["']?([^"'\n\r]+)/m);
95
- return match ? match[1].trim() : null;
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- /**
102
- * Get the package version from a skill source file
103
- */
104
- function getPackageVersion(sourcePath) {
105
- if (!existsSync(sourcePath)) {
106
- // Fallback: try to read from package.json
107
- const packageJsonPath = join(__dirname, '..', 'package.json');
108
- if (existsSync(packageJsonPath)) {
109
- try {
110
- const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
111
- return pkg.version || null;
112
- } catch {
113
- return null;
114
- }
115
- }
116
- return null;
117
- }
118
-
119
- try {
120
- const content = readFileSync(sourcePath, 'utf8');
121
- const match = content.match(/^version:\s*["']?([^"'\n\r]+)/m);
122
- return match ? match[1].trim() : null;
123
- } catch {
124
- return null;
125
- }
126
- }
127
-
128
- /**
129
- * Install or upgrade Claude Code skills
130
- */
131
- function setupClaude(force = false) {
132
- const skillFiles = getSkillFiles();
133
-
134
- if (skillFiles.length === 0) {
135
- console.error('Error: No skill files found. The package may be corrupted.');
136
- process.exit(1);
137
- }
138
-
139
- // Clean up legacy flat file structure (.claude/skills/ax/*.md except SKILL.md)
140
- const legacyDir = join(SKILLS_BASE_DIR, 'ax');
141
- if (existsSync(legacyDir)) {
142
- try {
143
- const legacyFiles = readdirSync(legacyDir).filter(
144
- (f) => f.endsWith('.md') && f !== 'SKILL.md'
145
- );
146
- for (const f of legacyFiles) {
147
- rmSync(join(legacyDir, f), { force: true });
148
- }
149
- } catch {
150
- // Ignore cleanup errors
151
- }
152
- }
153
-
154
- let allUpToDate = true;
155
-
156
- for (const file of skillFiles) {
157
- const skillSource = join(SKILLS_SOURCE_DIR, file);
158
- const skillName = getSkillName(skillSource);
159
- const skillDir = join(SKILLS_BASE_DIR, skillName);
160
- const skillTarget = join(skillDir, 'SKILL.md');
161
-
162
- const packageVersion = getPackageVersion(skillSource);
163
- const installedVersion = getInstalledVersion(skillTarget);
8
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
9
+ const [command = 'help', ...flags] = process.argv.slice(2);
164
10
 
165
- // Determine if we should install
166
- let shouldInstall = false;
167
- let action = 'Installed';
168
-
169
- if (!existsSync(skillTarget)) {
170
- shouldInstall = true;
171
- } else if (force) {
172
- shouldInstall = true;
173
- action = installedVersion ? 'Reinstalled' : 'Installed';
174
- } else if (installedVersion && packageVersion) {
175
- const comparison = compareSemver(packageVersion, installedVersion);
176
- if (comparison > 0) {
177
- shouldInstall = true;
178
- action = 'Upgraded';
179
- } else if (comparison === 0) {
180
- console.log(`${skillName} is up to date (v${installedVersion})`);
181
- continue;
182
- } else {
183
- console.log(
184
- `${skillName} is already at v${installedVersion} (package has v${packageVersion})`
185
- );
186
- continue;
187
- }
188
- } else if (!installedVersion && existsSync(skillTarget)) {
189
- shouldInstall = true;
190
- action = 'Upgraded';
191
- }
192
-
193
- if (!shouldInstall) {
194
- console.log(
195
- `${skillName} is up to date (v${installedVersion || 'unknown'})`
196
- );
197
- continue;
198
- }
199
-
200
- allUpToDate = false;
201
-
202
- // Create target directory if it doesn't exist
203
- if (!existsSync(skillDir)) {
204
- mkdirSync(skillDir, { recursive: true });
205
- }
206
-
207
- try {
208
- const content = readFileSync(skillSource, 'utf8');
209
- writeFileSync(skillTarget, content, 'utf8');
210
-
211
- if (action === 'Upgraded' && installedVersion && packageVersion) {
212
- console.log(
213
- `\u2713 ${action} ${skillName} (v${installedVersion} \u2192 v${packageVersion})`
214
- );
215
- } else {
216
- console.log(
217
- `\u2713 ${action} ${skillName} (v${packageVersion || 'unknown'})`
218
- );
219
- }
220
- } catch (err) {
221
- console.error(`Error installing ${skillName}: ${err.message}`);
222
- process.exit(1);
223
- }
224
- }
225
-
226
- if (allUpToDate) {
227
- console.log('All Ax Claude Code skills are up to date.');
228
- }
229
- }
230
-
231
- /**
232
- * Remove all Claude Code skills
233
- */
234
- function removeClaude() {
235
- const skillFiles = getSkillFiles();
236
-
237
- if (skillFiles.length === 0) {
238
- console.log('No skill definitions found in package.');
239
- return;
240
- }
241
-
242
- try {
243
- let removedCount = 0;
244
-
245
- for (const file of skillFiles) {
246
- const skillName = getSkillName(join(SKILLS_SOURCE_DIR, file));
247
- const skillDir = join(SKILLS_BASE_DIR, skillName);
248
- const skillTarget = join(skillDir, 'SKILL.md');
249
-
250
- if (existsSync(skillTarget)) {
251
- rmSync(skillTarget, { force: true });
252
- removedCount++;
253
-
254
- // Try to remove the directory if empty
255
- try {
256
- const remaining = readdirSync(skillDir);
257
- if (remaining.length === 0) {
258
- rmSync(skillDir, { recursive: true, force: true });
259
- }
260
- } catch {
261
- // Ignore errors when trying to clean up directory
262
- }
263
- }
264
- }
265
-
266
- // Also clean up legacy flat files from old .claude/skills/ax/ structure
267
- const legacyDir = join(SKILLS_BASE_DIR, 'ax');
268
- if (existsSync(legacyDir)) {
269
- const legacyFiles = readdirSync(legacyDir).filter(
270
- (f) => f.endsWith('.md') && f !== 'SKILL.md'
271
- );
272
- for (const f of legacyFiles) {
273
- rmSync(join(legacyDir, f), { force: true });
274
- }
275
- }
276
-
277
- if (removedCount > 0) {
278
- const noun = removedCount === 1 ? 'skill' : 'skills';
279
- console.log(`\u2713 Removed ${removedCount} Ax Claude Code ${noun}`);
280
- } else {
281
- console.log('Ax Claude Code skills are not installed.');
282
- }
283
- } catch (err) {
284
- console.error(`Error removing skills: ${err.message}`);
285
- process.exit(1);
286
- }
287
- }
288
-
289
- /**
290
- * Show help
291
- */
292
11
  function showHelp() {
293
12
  console.log(`
294
13
  @ax-llm/ax CLI
@@ -297,42 +16,76 @@ Usage:
297
16
  npx @ax-llm/ax <command> [options]
298
17
 
299
18
  Commands:
300
- setup-claude [--force] Install/upgrade Claude Code skills
301
- remove-claude Remove all Claude Code skills
302
- help Show this help message
19
+ setup-claude Install the Ax entry point and bundled guides locally
20
+ remove-claude Remove installed Ax skills and their bundled guides
21
+ skills-auto-install <on|off>
22
+ Save automatic setup preference in project package.json
23
+ help Show this help
303
24
 
304
- Options:
305
- --force Force reinstall regardless of version
25
+ Options for setup-claude:
26
+ --all Install every specialist as a separate skill too
27
+ --skill <name> Install only the named skill (repeatable)
28
+ --force Reinstall even if the installed version is newer
306
29
 
307
30
  Examples:
308
- npx @ax-llm/ax setup-claude # Install or upgrade
309
- npx @ax-llm/ax setup-claude --force # Force reinstall
310
- npx @ax-llm/ax remove-claude # Remove skills
31
+ npx @ax-llm/ax setup-claude
32
+ npx @ax-llm/ax setup-claude --all
33
+ npx @ax-llm/ax setup-claude --skill ax-ai
34
+ npx @ax-llm/ax skills-auto-install off
35
+ npx @ax-llm/ax skills-auto-install on
36
+
37
+ Automatic setup installs only ax-llm in .claude/skills, outside CI.
38
+ Run skills-auto-install off from your project root to disable future automatic setup.
39
+ This saves ax.skills.autoInstall in package.json; on restores automatic setup.
40
+ Manual setup-claude still works while automatic setup is disabled.
41
+ Set AX_SKIP_SKILL_INSTALL=1 to skip setup for a single package installation.
42
+ Existing specialist installs remain until you run remove-claude.
311
43
  `);
312
44
  }
313
45
 
314
- // Parse command line arguments
315
- const args = process.argv.slice(2);
316
- const command = args[0];
317
- const flags = args.slice(1);
318
-
319
- switch (command) {
320
- case 'setup-claude':
321
- setupClaude(flags.includes('--force'));
322
- break;
323
- case 'remove-claude':
324
- removeClaude();
325
- break;
326
- case 'help':
327
- case '--help':
328
- case '-h':
329
- showHelp();
330
- break;
331
- case undefined:
46
+ try {
47
+ if (command === 'setup-claude') {
48
+ const options = { all: false, force: false, names: [] };
49
+ for (let i = 0; i < flags.length; i++) {
50
+ const flag = flags[i];
51
+ if (flag === '--all') options.all = true;
52
+ else if (flag === '--force') options.force = true;
53
+ else if (
54
+ flag === '--skill' &&
55
+ flags[i + 1] &&
56
+ !flags[i + 1].startsWith('--')
57
+ )
58
+ options.names.push(flags[++i]);
59
+ else throw new Error(`Unknown or incomplete option: ${flag}`);
60
+ }
61
+ if (options.all && options.names.length)
62
+ throw new Error('Choose --all or --skill.');
63
+ const installed = installSkills(packageRoot, process.cwd(), options);
64
+ console.log(
65
+ installed.length
66
+ ? `✓ Installed Ax skills: ${installed.join(', ')}`
67
+ : 'Ax skills are up to date.'
68
+ );
69
+ } else if (command === 'remove-claude' && flags.length === 0) {
70
+ console.log(
71
+ `✓ Removed ${removeSkills(packageRoot, process.cwd())} Ax skills.`
72
+ );
73
+ } else if (command === 'skills-auto-install') {
74
+ if (flags.length !== 1 || !['on', 'off'].includes(flags[0]))
75
+ throw new Error('Usage: npx @ax-llm/ax skills-auto-install <on|off>');
76
+ const enabled = flags[0] === 'on';
77
+ setAutoSkillInstall(process.cwd(), enabled);
78
+ console.log(
79
+ `✓ Automatic Ax skill installation ${enabled ? 'enabled' : 'disabled'} in package.json.`
80
+ );
81
+ } else if (['help', '--help', '-h'].includes(command)) {
332
82
  showHelp();
333
- break;
334
- default:
335
- console.error(`Unknown command: ${command}`);
336
- console.error('Run "npx @ax-llm/ax help" for usage information.');
337
- process.exit(1);
83
+ } else {
84
+ throw new Error(
85
+ `Unknown command: ${command}. Run npx @ax-llm/ax help for usage.`
86
+ );
87
+ }
88
+ } catch (error) {
89
+ console.error(error.message);
90
+ process.exitCode = 1;
338
91
  }