elliot-stack 1.0.22 → 1.0.24
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 +5 -4
- package/bin/install.cjs +234 -37
- package/hooks/repo-search-nudge.js +1 -0
- package/package.json +1 -1
- package/skills/estack-active-learning-tutor/SKILL.md +1 -0
- package/skills/estack-better-title/SKILL.md +1 -0
- package/skills/estack-chris-voss/SKILL.md +1 -0
- package/skills/estack-customer-discovery/SKILL.md +1 -0
- package/skills/estack-flight-planner/SKILL.md +1 -0
- package/skills/estack-github-issue-tracker/SKILL.md +1 -0
- package/skills/estack-prompt-builder-coach/SKILL.md +1 -0
- package/skills/estack-read-claude-session-history/SKILL.md +1 -0
- package/skills/estack-repo-search/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A curated set of Claude Code skills by Elliot Drel. One command installs them al
|
|
|
11
11
|
npx elliot-stack@latest
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
This
|
|
14
|
+
This installs skills to `~/.agents/skills/` and symlinks them into `~/.claude/skills/`, then registers a `SessionStart` hook so your skills stay up to date automatically.
|
|
15
15
|
|
|
16
16
|
## Skills
|
|
17
17
|
|
|
@@ -34,10 +34,11 @@ Hooks install to `~/.claude/hooks/` and are auto-registered in your `~/.claude/s
|
|
|
34
34
|
|
|
35
35
|
## How it works
|
|
36
36
|
|
|
37
|
-
- Skills install to `~/.claude/skills/estack-*/`
|
|
37
|
+
- Skills install to `~/.agents/skills/estack-*/` (symlinked from `~/.claude/skills/estack-*/`)
|
|
38
38
|
- Hooks install to `~/.claude/hooks/` and are registered in `~/.claude/settings.json`
|
|
39
39
|
- A `SessionStart` hook auto-updates both each time you open Claude Code
|
|
40
40
|
- If you've made local edits to a skill or hook, the installer detects the conflict and lets you choose: overwrite, skip, or merge
|
|
41
|
+
- Every skill carries its own semver (`version:` in SKILL.md frontmatter; hooks use a `// @version` comment), independent of the package version — update messages show exactly what moved, e.g. `estack-chris-voss (1.0.0 → 1.1.0)`. Under the hood, updates are detected by content hash, so a change can never be missed; a release-time check (`scripts/check-versions.cjs`) guarantees every content change ships with a version bump.
|
|
41
42
|
|
|
42
43
|
## Updating
|
|
43
44
|
|
|
@@ -62,10 +63,10 @@ Run the installer straight from your checkout to preview what a real install wou
|
|
|
62
63
|
|
|
63
64
|
```bash
|
|
64
65
|
node bin/install.cjs # dry run — previews changes, writes nothing
|
|
65
|
-
node bin/install.cjs --install # actually sync your local edits to ~/.claude/skills/
|
|
66
|
+
node bin/install.cjs --install # actually sync your local edits to ~/.agents/skills/ + ~/.claude/skills/
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
Run from the repo, the installer **dry-runs by default** so testing never clobbers your live
|
|
69
|
+
Run from the repo, the installer **dry-runs by default** so testing never clobbers your live install. Pass `--install` once the preview looks right. (`--dry-run` forces a preview even under `npx`.)
|
|
69
70
|
|
|
70
71
|
See [`docs/publishing.md`](docs/publishing.md) for the release flow and security model.
|
|
71
72
|
|
package/bin/install.cjs
CHANGED
|
@@ -11,6 +11,8 @@ const readline = require('readline');
|
|
|
11
11
|
const HOME = os.homedir();
|
|
12
12
|
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
13
13
|
const SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
|
|
14
|
+
const AGENTS_ROOT = path.join(HOME, '.agents');
|
|
15
|
+
const AGENTS_DIR = path.join(AGENTS_ROOT, 'skills');
|
|
14
16
|
const BACKUP_DIR = path.join(HOME, '.estack-backup');
|
|
15
17
|
const CHECKSUMS_FILE = path.join(CLAUDE_DIR, '.estack-checksums.json');
|
|
16
18
|
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
@@ -60,6 +62,67 @@ const PACKAGE_HOOKS_DIR = path.join(__dirname, '..', 'hooks');
|
|
|
60
62
|
}
|
|
61
63
|
})();
|
|
62
64
|
|
|
65
|
+
// ── Migrate skills from ~/.agents/<name> (v1.0.23 layout) to ~/.agents/skills/ ──
|
|
66
|
+
(function migrateAgentsLayout() {
|
|
67
|
+
let strays;
|
|
68
|
+
try {
|
|
69
|
+
// statSync guards against ~/.agents existing as a plain file
|
|
70
|
+
if (!fs.existsSync(AGENTS_ROOT) || !fs.statSync(AGENTS_ROOT).isDirectory()) return;
|
|
71
|
+
strays = fs.readdirSync(AGENTS_ROOT, { withFileTypes: true })
|
|
72
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('estack-'));
|
|
73
|
+
} catch (_) {
|
|
74
|
+
return; // unreadable — let main() surface a real error if it matters
|
|
75
|
+
}
|
|
76
|
+
if (strays.length === 0) return;
|
|
77
|
+
const silent = process.argv.includes('--silent');
|
|
78
|
+
const isDryRun = process.argv.includes('--dry-run') ||
|
|
79
|
+
(!__dirname.includes('node_modules') && !process.argv.includes('--install'));
|
|
80
|
+
if (isDryRun) {
|
|
81
|
+
if (!silent) {
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
'estack: [dry run] Would move ' + strays.length + ' skill(s) from ~/.agents/ to ~/.agents/skills/\n'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
'estack: WARNING — could not create ~/.agents/skills/: ' + err.message + '\n'
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const e of strays) {
|
|
97
|
+
const oldPath = path.join(AGENTS_ROOT, e.name);
|
|
98
|
+
const newPath = path.join(AGENTS_DIR, e.name);
|
|
99
|
+
try {
|
|
100
|
+
if (fs.existsSync(newPath)) {
|
|
101
|
+
// already migrated — drop the stale copy at the old location
|
|
102
|
+
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
fs.renameSync(oldPath, newPath);
|
|
106
|
+
} catch (_) {
|
|
107
|
+
copyDirRaw(oldPath, newPath);
|
|
108
|
+
removeDirRaw(oldPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// re-point the live symlink — the old junction now dangles
|
|
112
|
+
ensureSymlink(newPath, path.join(SKILLS_DIR, e.name));
|
|
113
|
+
} catch (err) {
|
|
114
|
+
process.stderr.write(
|
|
115
|
+
'estack: WARNING — could not migrate ' + e.name + ' to ~/.agents/skills/: ' + err.message + '\n'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!silent) {
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
'estack: moved ' + strays.length + ' skill(s) from ~/.agents/ to ~/.agents/skills/\n'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
|
|
63
126
|
function copyDirRaw(src, dest) {
|
|
64
127
|
fs.mkdirSync(dest, { recursive: true });
|
|
65
128
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -79,6 +142,38 @@ function removeDirRaw(dir) {
|
|
|
79
142
|
fs.rmdirSync(dir);
|
|
80
143
|
}
|
|
81
144
|
|
|
145
|
+
function isSymlink(p) {
|
|
146
|
+
try { return fs.lstatSync(p).isSymbolicLink(); } catch (_) { return false; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// True only for a real directory at p — not a symlink to one, not a file.
|
|
150
|
+
function isRealDir(p) {
|
|
151
|
+
try {
|
|
152
|
+
const stat = fs.lstatSync(p);
|
|
153
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
154
|
+
} catch (_) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Creates (or updates) a directory symlink at linkPath pointing to target.
|
|
160
|
+
// On Windows uses 'junction' (no elevation required); on Unix uses 'dir'.
|
|
161
|
+
function ensureSymlink(target, linkPath) {
|
|
162
|
+
try {
|
|
163
|
+
const stat = fs.lstatSync(linkPath);
|
|
164
|
+
if (stat.isSymbolicLink()) {
|
|
165
|
+
if (path.resolve(fs.readlinkSync(linkPath)) === path.resolve(target)) return;
|
|
166
|
+
fs.unlinkSync(linkPath);
|
|
167
|
+
} else {
|
|
168
|
+
// real dir, plain file, or anything else occupying the link path
|
|
169
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
} catch (_) {}
|
|
172
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
173
|
+
const type = process.platform === 'win32' ? 'junction' : 'dir';
|
|
174
|
+
fs.symlinkSync(target, linkPath, type);
|
|
175
|
+
}
|
|
176
|
+
|
|
82
177
|
// ── Flags ──────────────────────────────────────────────────────────────────
|
|
83
178
|
const SILENT = process.argv.includes('--silent');
|
|
84
179
|
const STARTUP = process.argv.includes('--startup');
|
|
@@ -127,7 +222,12 @@ function computeFileHash(filePath) {
|
|
|
127
222
|
}
|
|
128
223
|
|
|
129
224
|
function computeSkillHash(skillDir) {
|
|
130
|
-
|
|
225
|
+
// statSync (not lstat) so symlinked dirs hash their contents; plain files → null
|
|
226
|
+
try {
|
|
227
|
+
if (!fs.statSync(skillDir).isDirectory()) return null;
|
|
228
|
+
} catch (_) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
131
231
|
const hash = crypto.createHash('sha256');
|
|
132
232
|
const files = walkDir(skillDir, skillDir);
|
|
133
233
|
for (const relPath of files) {
|
|
@@ -147,7 +247,8 @@ function copyDir(src, dest) {
|
|
|
147
247
|
}
|
|
148
248
|
|
|
149
249
|
function backupSkill(name) {
|
|
150
|
-
const
|
|
250
|
+
const agentsDir = path.join(AGENTS_DIR, name);
|
|
251
|
+
const installedDir = fs.existsSync(agentsDir) ? agentsDir : path.join(SKILLS_DIR, name);
|
|
151
252
|
if (!fs.existsSync(installedDir)) return;
|
|
152
253
|
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
153
254
|
copyDir(installedDir, path.join(BACKUP_DIR, name));
|
|
@@ -201,18 +302,65 @@ function getSkillDescription(skillDir) {
|
|
|
201
302
|
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
202
303
|
if (!fs.existsSync(skillMd)) return '';
|
|
203
304
|
const content = fs.readFileSync(skillMd, 'utf8');
|
|
204
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
305
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
205
306
|
if (!frontmatterMatch) return '';
|
|
206
307
|
const fm = frontmatterMatch[1];
|
|
207
|
-
const singleLine = fm.match(/^description:\s*(
|
|
208
|
-
if (singleLine) return singleLine[1].trim();
|
|
209
|
-
const multiLine = fm.match(/^description:\s
|
|
308
|
+
const singleLine = fm.match(/^description:\s*(\S.*)$/m);
|
|
309
|
+
if (singleLine && !/^[>|]/.test(singleLine[1])) return singleLine[1].trim();
|
|
310
|
+
const multiLine = fm.match(/^description:\s*[>|][->+]?\r?\n((?:[ \t]+.*\r?\n?)+)/m);
|
|
210
311
|
if (multiLine) {
|
|
211
312
|
return multiLine[1].replace(/\s+/g, ' ').trim();
|
|
212
313
|
}
|
|
213
314
|
return '';
|
|
214
315
|
}
|
|
215
316
|
|
|
317
|
+
// Per-skill version from SKILL.md frontmatter (`version: x.y.z`).
|
|
318
|
+
// Versions are the human-readable label; content hashes remain the
|
|
319
|
+
// update-detection source of truth (scripts/check-versions.cjs keeps them in sync).
|
|
320
|
+
function getSkillVersion(skillDir) {
|
|
321
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
322
|
+
if (!fs.existsSync(skillMd)) return null;
|
|
323
|
+
const content = fs.readFileSync(skillMd, 'utf8');
|
|
324
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
325
|
+
if (!frontmatterMatch) return null;
|
|
326
|
+
const m = frontmatterMatch[1].match(/^version:\s*(\S+)\s*$/m);
|
|
327
|
+
return m ? m[1] : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Version of the currently installed copy of a skill (agents dir, falling
|
|
331
|
+
// back to the legacy skills dir for pre-migration installs).
|
|
332
|
+
function getInstalledSkillVersion(name) {
|
|
333
|
+
const agentsDir = path.join(AGENTS_DIR, name);
|
|
334
|
+
if (fs.existsSync(agentsDir)) return getSkillVersion(agentsDir);
|
|
335
|
+
return getSkillVersion(path.join(SKILLS_DIR, name));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Per-hook version from a `// @version x.y.z` comment near the top.
|
|
339
|
+
function getHookVersion(filePath) {
|
|
340
|
+
if (!fs.existsSync(filePath)) return null;
|
|
341
|
+
const m = fs.readFileSync(filePath, 'utf8').match(/^\/\/ @version\s+(\S+)\s*$/m);
|
|
342
|
+
return m ? m[1] : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// "name (1.0.0 → 1.1.0)" for updates, "name (v1.1.0)" for fresh installs.
|
|
346
|
+
function withVersion(name, oldV, newV) {
|
|
347
|
+
if (oldV && newV && oldV !== newV) return name + ' (' + oldV + ' → ' + newV + ')';
|
|
348
|
+
if (newV) return name + ' (v' + newV + ')';
|
|
349
|
+
return name;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Copies a skill to ~/.agents/skills/<name> and creates/updates the symlink at ~/.claude/skills/<name>.
|
|
353
|
+
// If a real (non-symlink) directory already exists at the skills path, it is removed first.
|
|
354
|
+
function installSkillFiles(name) {
|
|
355
|
+
const agentsSkillDir = path.join(AGENTS_DIR, name);
|
|
356
|
+
const skillsLinkDir = path.join(SKILLS_DIR, name);
|
|
357
|
+
if (!isSymlink(skillsLinkDir) && fs.existsSync(skillsLinkDir)) {
|
|
358
|
+
fs.rmSync(skillsLinkDir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
copyDir(path.join(PACKAGE_SKILLS_DIR, name), agentsSkillDir);
|
|
361
|
+
ensureSymlink(agentsSkillDir, skillsLinkDir);
|
|
362
|
+
}
|
|
363
|
+
|
|
216
364
|
// ── Hook setup ─────────────────────────────────────────────────────────────
|
|
217
365
|
|
|
218
366
|
// Returns true if the hook was added (or would be added in dryRun), false if
|
|
@@ -310,15 +458,26 @@ function setupRepoSearchNudgeHook(dryRun) {
|
|
|
310
458
|
|
|
311
459
|
async function main() {
|
|
312
460
|
// 0. Remove deprecated skills (renamed/deleted from the package)
|
|
313
|
-
if (fs.existsSync(SKILLS_DIR)) {
|
|
461
|
+
if (fs.existsSync(SKILLS_DIR) || fs.existsSync(AGENTS_DIR)) {
|
|
314
462
|
const newChecksums0 = fs.existsSync(CHECKSUMS_FILE)
|
|
315
463
|
? (() => { try { return JSON.parse(fs.readFileSync(CHECKSUMS_FILE, 'utf8')); } catch (_) { return {}; } })()
|
|
316
464
|
: {};
|
|
317
465
|
let changed = false;
|
|
318
466
|
for (const name of DEPRECATED_SKILLS) {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
467
|
+
const agentsDir = path.join(AGENTS_DIR, name);
|
|
468
|
+
const skillsDir = path.join(SKILLS_DIR, name);
|
|
469
|
+
let found = false;
|
|
470
|
+
if (fs.existsSync(agentsDir)) {
|
|
471
|
+
if (!DRY_RUN) fs.rmSync(agentsDir, { recursive: true, force: true });
|
|
472
|
+
found = true;
|
|
473
|
+
}
|
|
474
|
+
if (fs.existsSync(skillsDir) || isSymlink(skillsDir)) {
|
|
475
|
+
if (!DRY_RUN) {
|
|
476
|
+
try { fs.unlinkSync(skillsDir); } catch (_) { fs.rmSync(skillsDir, { recursive: true, force: true }); }
|
|
477
|
+
}
|
|
478
|
+
found = true;
|
|
479
|
+
}
|
|
480
|
+
if (found) {
|
|
322
481
|
delete newChecksums0[name];
|
|
323
482
|
changed = true;
|
|
324
483
|
if (!SILENT && !STARTUP) {
|
|
@@ -329,7 +488,10 @@ async function main() {
|
|
|
329
488
|
changed = true;
|
|
330
489
|
}
|
|
331
490
|
}
|
|
332
|
-
if (changed && !DRY_RUN)
|
|
491
|
+
if (changed && !DRY_RUN) {
|
|
492
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
493
|
+
fs.writeFileSync(CHECKSUMS_FILE, JSON.stringify(newChecksums0, null, 2));
|
|
494
|
+
}
|
|
333
495
|
}
|
|
334
496
|
|
|
335
497
|
// 1. Scan package skills
|
|
@@ -377,11 +539,21 @@ async function main() {
|
|
|
377
539
|
}
|
|
378
540
|
|
|
379
541
|
// 4. Detect local modifications and needed updates
|
|
542
|
+
// Real files live in AGENTS_DIR; fall back to SKILLS_DIR for pre-migration installs.
|
|
380
543
|
const modifiedSkills = [];
|
|
381
544
|
const needsUpdate = [];
|
|
382
545
|
for (const name of skillNames) {
|
|
383
|
-
const
|
|
384
|
-
|
|
546
|
+
const agentsSkillDir = path.join(AGENTS_DIR, name);
|
|
547
|
+
let installedDir = null;
|
|
548
|
+
if (fs.existsSync(agentsSkillDir)) {
|
|
549
|
+
installedDir = agentsSkillDir;
|
|
550
|
+
} else {
|
|
551
|
+
const legacyDir = path.join(SKILLS_DIR, name);
|
|
552
|
+
if (isRealDir(legacyDir)) {
|
|
553
|
+
installedDir = legacyDir; // old-style install, will be migrated on next write
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (!installedDir) {
|
|
385
557
|
needsUpdate.push(name);
|
|
386
558
|
continue;
|
|
387
559
|
}
|
|
@@ -437,12 +609,13 @@ async function main() {
|
|
|
437
609
|
hooksNeedingUpdate.length === 0 && modifiedHooks.length === 0) {
|
|
438
610
|
process.exit(0);
|
|
439
611
|
}
|
|
612
|
+
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
440
613
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
441
614
|
const newChecksums = Object.assign({}, storedChecksums);
|
|
442
615
|
for (const name of skillNames) {
|
|
443
616
|
if (modifiedSkills.includes(name)) continue;
|
|
444
|
-
if (!needsUpdate.includes(name) && fs.existsSync(path.join(
|
|
445
|
-
|
|
617
|
+
if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) continue;
|
|
618
|
+
installSkillFiles(name);
|
|
446
619
|
newChecksums[name] = packageHashes[name];
|
|
447
620
|
}
|
|
448
621
|
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
@@ -463,43 +636,54 @@ async function main() {
|
|
|
463
636
|
process.exit(0);
|
|
464
637
|
}
|
|
465
638
|
|
|
639
|
+
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
466
640
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
467
641
|
const newChecksums = Object.assign({}, storedChecksums);
|
|
468
|
-
const updated = [];
|
|
469
|
-
const mergeNeeded = [];
|
|
642
|
+
const updated = []; // display labels with version transitions
|
|
643
|
+
const mergeNeeded = []; // plain names (used in merge instructions)
|
|
644
|
+
const mergeNeededLabels = []; // display labels with version transitions
|
|
470
645
|
|
|
471
646
|
for (const name of skillNames) {
|
|
647
|
+
const newV = getSkillVersion(path.join(PACKAGE_SKILLS_DIR, name));
|
|
472
648
|
if (modifiedSkills.includes(name)) {
|
|
473
649
|
// Backup local version, install new version
|
|
650
|
+
const oldV = getInstalledSkillVersion(name);
|
|
474
651
|
backupSkill(name);
|
|
475
|
-
|
|
652
|
+
installSkillFiles(name);
|
|
476
653
|
newChecksums[name] = packageHashes[name];
|
|
477
654
|
mergeNeeded.push(name);
|
|
655
|
+
mergeNeededLabels.push(withVersion(name, oldV, newV));
|
|
478
656
|
continue;
|
|
479
657
|
}
|
|
480
|
-
if (!needsUpdate.includes(name) && fs.existsSync(path.join(
|
|
481
|
-
|
|
658
|
+
if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) continue;
|
|
659
|
+
const oldV = getInstalledSkillVersion(name);
|
|
660
|
+
installSkillFiles(name);
|
|
482
661
|
newChecksums[name] = packageHashes[name];
|
|
483
|
-
updated.push(name);
|
|
662
|
+
updated.push(withVersion(name, oldV, newV));
|
|
484
663
|
}
|
|
485
664
|
|
|
486
665
|
// Install hooks
|
|
487
666
|
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
488
667
|
const updatedHooks = [];
|
|
489
668
|
const mergeNeededHooks = [];
|
|
669
|
+
const mergeNeededHookLabels = [];
|
|
490
670
|
|
|
491
671
|
for (const filename of hookFilenames) {
|
|
672
|
+
const newV = getHookVersion(path.join(PACKAGE_HOOKS_DIR, filename));
|
|
492
673
|
if (modifiedHooks.includes(filename)) {
|
|
674
|
+
const oldV = getHookVersion(path.join(HOOKS_DIR, filename));
|
|
493
675
|
backupHook(filename);
|
|
494
676
|
fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
|
|
495
677
|
newChecksums['hook:' + filename] = packageHookHashes[filename];
|
|
496
678
|
mergeNeededHooks.push(filename);
|
|
679
|
+
mergeNeededHookLabels.push(withVersion(filename, oldV, newV));
|
|
497
680
|
continue;
|
|
498
681
|
}
|
|
499
682
|
if (!hooksNeedingUpdate.includes(filename) && fs.existsSync(path.join(HOOKS_DIR, filename))) continue;
|
|
683
|
+
const oldV = getHookVersion(path.join(HOOKS_DIR, filename));
|
|
500
684
|
fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
|
|
501
685
|
newChecksums['hook:' + filename] = packageHookHashes[filename];
|
|
502
|
-
updatedHooks.push(filename);
|
|
686
|
+
updatedHooks.push(withVersion(filename, oldV, newV));
|
|
503
687
|
}
|
|
504
688
|
|
|
505
689
|
setupRepoSearchNudgeHook();
|
|
@@ -521,14 +705,15 @@ async function main() {
|
|
|
521
705
|
if (mergeNeeded.length > 0) {
|
|
522
706
|
const backupPath = BACKUP_DIR.replace(HOME, '~');
|
|
523
707
|
msgParts.push(
|
|
524
|
-
'estack: updated ' +
|
|
708
|
+
'estack: updated ' + mergeNeededLabels.join(', ') +
|
|
525
709
|
' (local changes backed up to ' + backupPath + ')'
|
|
526
710
|
);
|
|
527
711
|
output.additionalContext =
|
|
528
712
|
'estack skills were updated but the user had local modifications to: ' +
|
|
529
713
|
mergeNeeded.join(', ') + '. ' +
|
|
530
714
|
'Their previous versions are saved at ' + BACKUP_DIR + '. ' +
|
|
531
|
-
'The new upstream versions are now installed at ' +
|
|
715
|
+
'The new upstream versions are now installed at ' + AGENTS_DIR + ' ' +
|
|
716
|
+
'(symlinked from ' + SKILLS_DIR + '). ' +
|
|
532
717
|
'Offer to merge their customizations from the backup into the updated versions. ' +
|
|
533
718
|
'To merge: read both the backup version and the new version of each skill, ' +
|
|
534
719
|
'identify the user\'s changes, and apply them to the new version where compatible.';
|
|
@@ -537,7 +722,7 @@ async function main() {
|
|
|
537
722
|
if (mergeNeededHooks.length > 0) {
|
|
538
723
|
const backupPath = BACKUP_DIR.replace(HOME, '~');
|
|
539
724
|
msgParts.push(
|
|
540
|
-
'estack: updated hooks ' +
|
|
725
|
+
'estack: updated hooks ' + mergeNeededHookLabels.join(', ') +
|
|
541
726
|
' (local changes backed up to ' + backupPath + '/hooks/)'
|
|
542
727
|
);
|
|
543
728
|
const existingContext = output.additionalContext ? output.additionalContext + ' ' : '';
|
|
@@ -608,7 +793,10 @@ async function main() {
|
|
|
608
793
|
}
|
|
609
794
|
|
|
610
795
|
// 8. Install skills
|
|
611
|
-
if (!DRY_RUN)
|
|
796
|
+
if (!DRY_RUN) {
|
|
797
|
+
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
798
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
799
|
+
}
|
|
612
800
|
const newChecksums = Object.assign({}, storedChecksums);
|
|
613
801
|
let installedCount = 0;
|
|
614
802
|
const mergedSkills = [];
|
|
@@ -617,7 +805,8 @@ async function main() {
|
|
|
617
805
|
if (modifiedSkills.includes(name)) {
|
|
618
806
|
if (modifiedAction === 'skip') {
|
|
619
807
|
console.log(' Skipped ' + name + ' (local modifications preserved)');
|
|
620
|
-
const currentHash = computeSkillHash(path.join(
|
|
808
|
+
const currentHash = computeSkillHash(path.join(AGENTS_DIR, name)) ||
|
|
809
|
+
computeSkillHash(path.join(SKILLS_DIR, name));
|
|
621
810
|
if (currentHash) newChecksums[name] = currentHash;
|
|
622
811
|
continue;
|
|
623
812
|
}
|
|
@@ -627,19 +816,23 @@ async function main() {
|
|
|
627
816
|
console.log((DRY_RUN ? ' [dry run] Would back up ' : ' Backed up ') + name + ' → ~/.estack-backup/' + name);
|
|
628
817
|
}
|
|
629
818
|
// overwrite or merge — fall through to install
|
|
630
|
-
} else if (!needsUpdate.includes(name) && fs.existsSync(path.join(
|
|
819
|
+
} else if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) {
|
|
631
820
|
// Already installed and up-to-date
|
|
632
821
|
if (DRY_RUN) console.log(' [dry run] Up to date (no change): ' + name);
|
|
633
822
|
continue;
|
|
634
823
|
}
|
|
635
|
-
const isUpdate = fs.existsSync(path.join(
|
|
636
|
-
|
|
824
|
+
const isUpdate = fs.existsSync(path.join(AGENTS_DIR, name)) ||
|
|
825
|
+
isRealDir(path.join(SKILLS_DIR, name));
|
|
826
|
+
const label = withVersion(name,
|
|
827
|
+
isUpdate ? getInstalledSkillVersion(name) : null,
|
|
828
|
+
getSkillVersion(path.join(PACKAGE_SKILLS_DIR, name)));
|
|
829
|
+
if (!DRY_RUN) installSkillFiles(name);
|
|
637
830
|
newChecksums[name] = packageHashes[name];
|
|
638
831
|
installedCount++;
|
|
639
832
|
if (DRY_RUN) {
|
|
640
|
-
console.log(' [dry run] Would ' + (isUpdate ? 'update ' : 'install ') +
|
|
833
|
+
console.log(' [dry run] Would ' + (isUpdate ? 'update ' : 'install ') + label);
|
|
641
834
|
} else {
|
|
642
|
-
console.log(' Installed ' +
|
|
835
|
+
console.log(' Installed ' + label);
|
|
643
836
|
}
|
|
644
837
|
}
|
|
645
838
|
|
|
@@ -668,13 +861,16 @@ async function main() {
|
|
|
668
861
|
continue;
|
|
669
862
|
}
|
|
670
863
|
const isHookUpdate = fs.existsSync(path.join(HOOKS_DIR, filename));
|
|
864
|
+
const hookLabel = withVersion(filename,
|
|
865
|
+
isHookUpdate ? getHookVersion(path.join(HOOKS_DIR, filename)) : null,
|
|
866
|
+
getHookVersion(path.join(PACKAGE_HOOKS_DIR, filename)));
|
|
671
867
|
if (!DRY_RUN) fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
|
|
672
868
|
newChecksums['hook:' + filename] = packageHookHashes[filename];
|
|
673
869
|
installedHookCount++;
|
|
674
870
|
if (DRY_RUN) {
|
|
675
|
-
console.log(' [dry run] Would ' + (isHookUpdate ? 'update hook ' : 'install hook ') +
|
|
871
|
+
console.log(' [dry run] Would ' + (isHookUpdate ? 'update hook ' : 'install hook ') + hookLabel);
|
|
676
872
|
} else {
|
|
677
|
-
console.log(' Installed hook ' +
|
|
873
|
+
console.log(' Installed hook ' + hookLabel);
|
|
678
874
|
}
|
|
679
875
|
}
|
|
680
876
|
|
|
@@ -689,13 +885,13 @@ async function main() {
|
|
|
689
885
|
// 11. Summary output
|
|
690
886
|
if (DRY_RUN) {
|
|
691
887
|
console.log('\n[dry run] No files were changed. Run with --install to apply.\n');
|
|
692
|
-
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.claude/skills/');
|
|
888
|
+
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.agents/skills/ (linked from ~/.claude/skills/)');
|
|
693
889
|
if (installedHookCount > 0) {
|
|
694
890
|
console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.claude/hooks/');
|
|
695
891
|
}
|
|
696
892
|
} else {
|
|
697
893
|
console.log('\nestack installed successfully!\n');
|
|
698
|
-
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.claude/skills/');
|
|
894
|
+
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.agents/skills/ (symlinked from ~/.claude/skills/)');
|
|
699
895
|
if (installedHookCount > 0) {
|
|
700
896
|
console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' installed to ~/.claude/hooks/');
|
|
701
897
|
}
|
|
@@ -705,7 +901,8 @@ async function main() {
|
|
|
705
901
|
|
|
706
902
|
for (const name of skillNames) {
|
|
707
903
|
const desc = getSkillDescription(path.join(PACKAGE_SKILLS_DIR, name));
|
|
708
|
-
|
|
904
|
+
const ver = getSkillVersion(path.join(PACKAGE_SKILLS_DIR, name));
|
|
905
|
+
console.log(' /' + name + (ver ? ' v' + ver : '') + (desc ? ' — ' + desc : ''));
|
|
709
906
|
}
|
|
710
907
|
|
|
711
908
|
if (mergedSkills.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: estack-active-learning-tutor
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: (active-learning-tutor) Tutors a student through exam preparation using active learning — questioning, gap diagnosis, and concept mastery tracking. Use when the student says they want to study, learn, prep for an exam, be quizzed on a chapter, work through a practice test together, or be taught a topic conceptually rather than lectured.
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
---
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: estack-customer-discovery
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: (customer-discovery) Guide users through customer discovery — validating business ideas, identifying target customers, crafting outreach, preparing interview questions, and analyzing interview results. Use this skill whenever the user mentions customer discovery, customer interviews, validating an idea, market research, finding product-market fit, talking to customers, outreach messages, interview guides, or analyzing customer feedback. Also use when someone says they have a business idea and want to test it, or when they're preparing to talk to potential customers.
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: estack-flight-planner
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: (flight-planner) Find and rank flights between any two airports with config-driven preferences (budget, airlines, nonstop, time-of-day) and optional ground-shuttle pairing. Uses SerpAPI Google Flights (or WebSearch fallback). Saves preferences to `~/.flight-planner/config.json` and logs every search.
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
---
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: estack-prompt-builder-coach
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: (prompt-builder-coach) Use whenever you or the user need to write, sharpen, audit, or scope a prompt or work request for an AI agent or model. This is a four-part kit covering shaping a fuzzy idea into a decided goal, building a prompt from scratch, auditing a draft request that feels vague, and defining what "done" looks like when the task is fuzzy. Trigger when the user says "help me write a prompt", "build me a prompt", "audit this prompt", "make this request better", "why is the AI giving me generic output", "I don't know what I want", "I have a rough idea", "what should done look like", or when handing a task to another agent and wanting it to land. Use it even when the user did not say the word "prompt" but is clearly trying to get an AI to do consequential work. Do not use for quick factual lookups or for executing an already well-defined task.
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: estack-read-claude-session-history
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: (read-claude-session-history) Invoke for ANY task involving Claude Code session history, transcripts, or .jsonl files — this is the only way to read, parse, or search them; do not attempt to use Bash or Read on .jsonl directly. Use for: recovering context after /compact ("what were we doing before compact"), advisor response retrieval ("what did the advisor say"), subagent output collection ("get all subagent finals"), cross-project session search by keyword, session listing and triage, UUID and title lookup, resume-command generation, file-edit and tool-call forensics, session diff between two sessions or subagents, weekly work journal, day timeline of activity blocks and idle gaps, engagement/attention-time accounting (active vs elapsed time, break detection, parallel-chat-safe totals), recovering from .claude-backups after data loss, session count queries, and reading the last agent message before a crash or interrupt. Trigger phrases: "session history", "before compact", "what did claude do", "what did I work on", "search my sessions", "find that session", "what did the advisor say", "what did the agent edit", "from the backup", "list my sessions", "subagent outputs", "session journal", "resume previous", "which files did claude touch", "go back and look", "what did I do yesterday", "where did my day go", "timeline of my day", "how much time on", "how long did that actually take", "how much did I actually work", "active time", "time I spent".
|
|
4
5
|
---
|
|
5
6
|
|