elliot-stack 1.0.21 → 1.0.23
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 +4 -4
- package/bin/install.cjs +89 -22
- 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/` 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,7 +34,7 @@ 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/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
|
|
@@ -62,10 +62,10 @@ Run the installer straight from your checkout to preview what a real install wou
|
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
node bin/install.cjs # dry run — previews changes, writes nothing
|
|
65
|
-
node bin/install.cjs --install # actually sync your local edits to ~/.claude/skills/
|
|
65
|
+
node bin/install.cjs --install # actually sync your local edits to ~/.agents/ + ~/.claude/skills/
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Run from the repo, the installer **dry-runs by default** so testing never clobbers your live
|
|
68
|
+
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
69
|
|
|
70
70
|
See [`docs/publishing.md`](docs/publishing.md) for the release flow and security model.
|
|
71
71
|
|
package/bin/install.cjs
CHANGED
|
@@ -11,6 +11,7 @@ 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_DIR = path.join(HOME, '.agents');
|
|
14
15
|
const BACKUP_DIR = path.join(HOME, '.estack-backup');
|
|
15
16
|
const CHECKSUMS_FILE = path.join(CLAUDE_DIR, '.estack-checksums.json');
|
|
16
17
|
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
@@ -79,6 +80,26 @@ function removeDirRaw(dir) {
|
|
|
79
80
|
fs.rmdirSync(dir);
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
function isSymlink(p) {
|
|
84
|
+
try { return fs.lstatSync(p).isSymbolicLink(); } catch (_) { return false; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Creates (or updates) a directory symlink at linkPath pointing to target.
|
|
88
|
+
// On Windows uses 'junction' (no elevation required); on Unix uses 'dir'.
|
|
89
|
+
function ensureSymlink(target, linkPath) {
|
|
90
|
+
try {
|
|
91
|
+
const stat = fs.lstatSync(linkPath);
|
|
92
|
+
if (stat.isSymbolicLink()) {
|
|
93
|
+
if (path.resolve(fs.readlinkSync(linkPath)) === path.resolve(target)) return;
|
|
94
|
+
fs.unlinkSync(linkPath);
|
|
95
|
+
} else if (stat.isDirectory()) {
|
|
96
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
} catch (_) {}
|
|
99
|
+
const type = process.platform === 'win32' ? 'junction' : 'dir';
|
|
100
|
+
fs.symlinkSync(target, linkPath, type);
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
// ── Flags ──────────────────────────────────────────────────────────────────
|
|
83
104
|
const SILENT = process.argv.includes('--silent');
|
|
84
105
|
const STARTUP = process.argv.includes('--startup');
|
|
@@ -98,6 +119,9 @@ const DEPRECATED_SKILLS = [
|
|
|
98
119
|
|
|
99
120
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
100
121
|
|
|
122
|
+
const HASH_IGNORE_DIRS = new Set(['__pycache__', '.git', 'node_modules']);
|
|
123
|
+
const HASH_IGNORE_EXTS = new Set(['.pyc', '.pyo']);
|
|
124
|
+
|
|
101
125
|
function walkDir(dir, base) {
|
|
102
126
|
base = base || dir;
|
|
103
127
|
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
|
|
@@ -107,8 +131,8 @@ function walkDir(dir, base) {
|
|
|
107
131
|
for (const entry of entries) {
|
|
108
132
|
const full = path.join(dir, entry.name);
|
|
109
133
|
if (entry.isDirectory()) {
|
|
110
|
-
files.push(...walkDir(full, base));
|
|
111
|
-
} else {
|
|
134
|
+
if (!HASH_IGNORE_DIRS.has(entry.name)) files.push(...walkDir(full, base));
|
|
135
|
+
} else if (!HASH_IGNORE_EXTS.has(path.extname(entry.name))) {
|
|
112
136
|
files.push(path.relative(base, full));
|
|
113
137
|
}
|
|
114
138
|
}
|
|
@@ -144,7 +168,8 @@ function copyDir(src, dest) {
|
|
|
144
168
|
}
|
|
145
169
|
|
|
146
170
|
function backupSkill(name) {
|
|
147
|
-
const
|
|
171
|
+
const agentsDir = path.join(AGENTS_DIR, name);
|
|
172
|
+
const installedDir = fs.existsSync(agentsDir) ? agentsDir : path.join(SKILLS_DIR, name);
|
|
148
173
|
if (!fs.existsSync(installedDir)) return;
|
|
149
174
|
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
150
175
|
copyDir(installedDir, path.join(BACKUP_DIR, name));
|
|
@@ -210,6 +235,18 @@ function getSkillDescription(skillDir) {
|
|
|
210
235
|
return '';
|
|
211
236
|
}
|
|
212
237
|
|
|
238
|
+
// Copies a skill to ~/.agents/<name> and creates/updates the symlink at ~/.claude/skills/<name>.
|
|
239
|
+
// If a real (non-symlink) directory already exists at the skills path, it is removed first.
|
|
240
|
+
function installSkillFiles(name) {
|
|
241
|
+
const agentsSkillDir = path.join(AGENTS_DIR, name);
|
|
242
|
+
const skillsLinkDir = path.join(SKILLS_DIR, name);
|
|
243
|
+
if (!isSymlink(skillsLinkDir) && fs.existsSync(skillsLinkDir)) {
|
|
244
|
+
fs.rmSync(skillsLinkDir, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
copyDir(path.join(PACKAGE_SKILLS_DIR, name), agentsSkillDir);
|
|
247
|
+
ensureSymlink(agentsSkillDir, skillsLinkDir);
|
|
248
|
+
}
|
|
249
|
+
|
|
213
250
|
// ── Hook setup ─────────────────────────────────────────────────────────────
|
|
214
251
|
|
|
215
252
|
// Returns true if the hook was added (or would be added in dryRun), false if
|
|
@@ -307,15 +344,26 @@ function setupRepoSearchNudgeHook(dryRun) {
|
|
|
307
344
|
|
|
308
345
|
async function main() {
|
|
309
346
|
// 0. Remove deprecated skills (renamed/deleted from the package)
|
|
310
|
-
if (fs.existsSync(SKILLS_DIR)) {
|
|
347
|
+
if (fs.existsSync(SKILLS_DIR) || fs.existsSync(AGENTS_DIR)) {
|
|
311
348
|
const newChecksums0 = fs.existsSync(CHECKSUMS_FILE)
|
|
312
349
|
? (() => { try { return JSON.parse(fs.readFileSync(CHECKSUMS_FILE, 'utf8')); } catch (_) { return {}; } })()
|
|
313
350
|
: {};
|
|
314
351
|
let changed = false;
|
|
315
352
|
for (const name of DEPRECATED_SKILLS) {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
353
|
+
const agentsDir = path.join(AGENTS_DIR, name);
|
|
354
|
+
const skillsDir = path.join(SKILLS_DIR, name);
|
|
355
|
+
let found = false;
|
|
356
|
+
if (fs.existsSync(agentsDir)) {
|
|
357
|
+
if (!DRY_RUN) fs.rmSync(agentsDir, { recursive: true, force: true });
|
|
358
|
+
found = true;
|
|
359
|
+
}
|
|
360
|
+
if (fs.existsSync(skillsDir) || isSymlink(skillsDir)) {
|
|
361
|
+
if (!DRY_RUN) {
|
|
362
|
+
try { fs.unlinkSync(skillsDir); } catch (_) { fs.rmSync(skillsDir, { recursive: true, force: true }); }
|
|
363
|
+
}
|
|
364
|
+
found = true;
|
|
365
|
+
}
|
|
366
|
+
if (found) {
|
|
319
367
|
delete newChecksums0[name];
|
|
320
368
|
changed = true;
|
|
321
369
|
if (!SILENT && !STARTUP) {
|
|
@@ -374,11 +422,21 @@ async function main() {
|
|
|
374
422
|
}
|
|
375
423
|
|
|
376
424
|
// 4. Detect local modifications and needed updates
|
|
425
|
+
// Real files live in AGENTS_DIR; fall back to SKILLS_DIR for pre-migration installs.
|
|
377
426
|
const modifiedSkills = [];
|
|
378
427
|
const needsUpdate = [];
|
|
379
428
|
for (const name of skillNames) {
|
|
380
|
-
const
|
|
381
|
-
|
|
429
|
+
const agentsSkillDir = path.join(AGENTS_DIR, name);
|
|
430
|
+
let installedDir = null;
|
|
431
|
+
if (fs.existsSync(agentsSkillDir)) {
|
|
432
|
+
installedDir = agentsSkillDir;
|
|
433
|
+
} else {
|
|
434
|
+
const legacyDir = path.join(SKILLS_DIR, name);
|
|
435
|
+
if (fs.existsSync(legacyDir) && !isSymlink(legacyDir)) {
|
|
436
|
+
installedDir = legacyDir; // old-style install, will be migrated on next write
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!installedDir) {
|
|
382
440
|
needsUpdate.push(name);
|
|
383
441
|
continue;
|
|
384
442
|
}
|
|
@@ -434,12 +492,13 @@ async function main() {
|
|
|
434
492
|
hooksNeedingUpdate.length === 0 && modifiedHooks.length === 0) {
|
|
435
493
|
process.exit(0);
|
|
436
494
|
}
|
|
495
|
+
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
437
496
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
438
497
|
const newChecksums = Object.assign({}, storedChecksums);
|
|
439
498
|
for (const name of skillNames) {
|
|
440
499
|
if (modifiedSkills.includes(name)) continue;
|
|
441
|
-
if (!needsUpdate.includes(name) && fs.existsSync(path.join(
|
|
442
|
-
|
|
500
|
+
if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) continue;
|
|
501
|
+
installSkillFiles(name);
|
|
443
502
|
newChecksums[name] = packageHashes[name];
|
|
444
503
|
}
|
|
445
504
|
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
@@ -460,6 +519,7 @@ async function main() {
|
|
|
460
519
|
process.exit(0);
|
|
461
520
|
}
|
|
462
521
|
|
|
522
|
+
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
463
523
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
464
524
|
const newChecksums = Object.assign({}, storedChecksums);
|
|
465
525
|
const updated = [];
|
|
@@ -469,13 +529,13 @@ async function main() {
|
|
|
469
529
|
if (modifiedSkills.includes(name)) {
|
|
470
530
|
// Backup local version, install new version
|
|
471
531
|
backupSkill(name);
|
|
472
|
-
|
|
532
|
+
installSkillFiles(name);
|
|
473
533
|
newChecksums[name] = packageHashes[name];
|
|
474
534
|
mergeNeeded.push(name);
|
|
475
535
|
continue;
|
|
476
536
|
}
|
|
477
|
-
if (!needsUpdate.includes(name) && fs.existsSync(path.join(
|
|
478
|
-
|
|
537
|
+
if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) continue;
|
|
538
|
+
installSkillFiles(name);
|
|
479
539
|
newChecksums[name] = packageHashes[name];
|
|
480
540
|
updated.push(name);
|
|
481
541
|
}
|
|
@@ -525,7 +585,8 @@ async function main() {
|
|
|
525
585
|
'estack skills were updated but the user had local modifications to: ' +
|
|
526
586
|
mergeNeeded.join(', ') + '. ' +
|
|
527
587
|
'Their previous versions are saved at ' + BACKUP_DIR + '. ' +
|
|
528
|
-
'The new upstream versions are now installed at ' +
|
|
588
|
+
'The new upstream versions are now installed at ' + AGENTS_DIR + ' ' +
|
|
589
|
+
'(symlinked from ' + SKILLS_DIR + '). ' +
|
|
529
590
|
'Offer to merge their customizations from the backup into the updated versions. ' +
|
|
530
591
|
'To merge: read both the backup version and the new version of each skill, ' +
|
|
531
592
|
'identify the user\'s changes, and apply them to the new version where compatible.';
|
|
@@ -605,7 +666,10 @@ async function main() {
|
|
|
605
666
|
}
|
|
606
667
|
|
|
607
668
|
// 8. Install skills
|
|
608
|
-
if (!DRY_RUN)
|
|
669
|
+
if (!DRY_RUN) {
|
|
670
|
+
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
671
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
672
|
+
}
|
|
609
673
|
const newChecksums = Object.assign({}, storedChecksums);
|
|
610
674
|
let installedCount = 0;
|
|
611
675
|
const mergedSkills = [];
|
|
@@ -614,7 +678,8 @@ async function main() {
|
|
|
614
678
|
if (modifiedSkills.includes(name)) {
|
|
615
679
|
if (modifiedAction === 'skip') {
|
|
616
680
|
console.log(' Skipped ' + name + ' (local modifications preserved)');
|
|
617
|
-
const currentHash = computeSkillHash(path.join(
|
|
681
|
+
const currentHash = computeSkillHash(path.join(AGENTS_DIR, name)) ||
|
|
682
|
+
computeSkillHash(path.join(SKILLS_DIR, name));
|
|
618
683
|
if (currentHash) newChecksums[name] = currentHash;
|
|
619
684
|
continue;
|
|
620
685
|
}
|
|
@@ -624,13 +689,15 @@ async function main() {
|
|
|
624
689
|
console.log((DRY_RUN ? ' [dry run] Would back up ' : ' Backed up ') + name + ' → ~/.estack-backup/' + name);
|
|
625
690
|
}
|
|
626
691
|
// overwrite or merge — fall through to install
|
|
627
|
-
} else if (!needsUpdate.includes(name) && fs.existsSync(path.join(
|
|
692
|
+
} else if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) {
|
|
628
693
|
// Already installed and up-to-date
|
|
629
694
|
if (DRY_RUN) console.log(' [dry run] Up to date (no change): ' + name);
|
|
630
695
|
continue;
|
|
631
696
|
}
|
|
632
|
-
const
|
|
633
|
-
|
|
697
|
+
const skillsLegacyDir = path.join(SKILLS_DIR, name);
|
|
698
|
+
const isUpdate = fs.existsSync(path.join(AGENTS_DIR, name)) ||
|
|
699
|
+
(fs.existsSync(skillsLegacyDir) && !isSymlink(skillsLegacyDir));
|
|
700
|
+
if (!DRY_RUN) installSkillFiles(name);
|
|
634
701
|
newChecksums[name] = packageHashes[name];
|
|
635
702
|
installedCount++;
|
|
636
703
|
if (DRY_RUN) {
|
|
@@ -686,13 +753,13 @@ async function main() {
|
|
|
686
753
|
// 11. Summary output
|
|
687
754
|
if (DRY_RUN) {
|
|
688
755
|
console.log('\n[dry run] No files were changed. Run with --install to apply.\n');
|
|
689
|
-
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.claude/skills/');
|
|
756
|
+
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.agents/ (linked from ~/.claude/skills/)');
|
|
690
757
|
if (installedHookCount > 0) {
|
|
691
758
|
console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.claude/hooks/');
|
|
692
759
|
}
|
|
693
760
|
} else {
|
|
694
761
|
console.log('\nestack installed successfully!\n');
|
|
695
|
-
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.claude/skills/');
|
|
762
|
+
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.agents/ (symlinked from ~/.claude/skills/)');
|
|
696
763
|
if (installedHookCount > 0) {
|
|
697
764
|
console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' installed to ~/.claude/hooks/');
|
|
698
765
|
}
|
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
|
|