elliot-stack 1.0.22 → 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 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 copies skills to `~/.claude/skills/` and registers a `SessionStart` hook so your skills stay up to date automatically.
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 `~/.claude/skills/` install. Pass `--install` once the preview looks right. (`--dry-run` forces a preview even under `npx`.)
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');
@@ -147,7 +168,8 @@ function copyDir(src, dest) {
147
168
  }
148
169
 
149
170
  function backupSkill(name) {
150
- const installedDir = path.join(SKILLS_DIR, name);
171
+ const agentsDir = path.join(AGENTS_DIR, name);
172
+ const installedDir = fs.existsSync(agentsDir) ? agentsDir : path.join(SKILLS_DIR, name);
151
173
  if (!fs.existsSync(installedDir)) return;
152
174
  fs.mkdirSync(BACKUP_DIR, { recursive: true });
153
175
  copyDir(installedDir, path.join(BACKUP_DIR, name));
@@ -213,6 +235,18 @@ function getSkillDescription(skillDir) {
213
235
  return '';
214
236
  }
215
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
+
216
250
  // ── Hook setup ─────────────────────────────────────────────────────────────
217
251
 
218
252
  // Returns true if the hook was added (or would be added in dryRun), false if
@@ -310,15 +344,26 @@ function setupRepoSearchNudgeHook(dryRun) {
310
344
 
311
345
  async function main() {
312
346
  // 0. Remove deprecated skills (renamed/deleted from the package)
313
- if (fs.existsSync(SKILLS_DIR)) {
347
+ if (fs.existsSync(SKILLS_DIR) || fs.existsSync(AGENTS_DIR)) {
314
348
  const newChecksums0 = fs.existsSync(CHECKSUMS_FILE)
315
349
  ? (() => { try { return JSON.parse(fs.readFileSync(CHECKSUMS_FILE, 'utf8')); } catch (_) { return {}; } })()
316
350
  : {};
317
351
  let changed = false;
318
352
  for (const name of DEPRECATED_SKILLS) {
319
- const dir = path.join(SKILLS_DIR, name);
320
- if (fs.existsSync(dir)) {
321
- if (!DRY_RUN) fs.rmSync(dir, { recursive: true, force: true });
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) {
322
367
  delete newChecksums0[name];
323
368
  changed = true;
324
369
  if (!SILENT && !STARTUP) {
@@ -377,11 +422,21 @@ async function main() {
377
422
  }
378
423
 
379
424
  // 4. Detect local modifications and needed updates
425
+ // Real files live in AGENTS_DIR; fall back to SKILLS_DIR for pre-migration installs.
380
426
  const modifiedSkills = [];
381
427
  const needsUpdate = [];
382
428
  for (const name of skillNames) {
383
- const installedDir = path.join(SKILLS_DIR, name);
384
- if (!fs.existsSync(installedDir)) {
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) {
385
440
  needsUpdate.push(name);
386
441
  continue;
387
442
  }
@@ -437,12 +492,13 @@ async function main() {
437
492
  hooksNeedingUpdate.length === 0 && modifiedHooks.length === 0) {
438
493
  process.exit(0);
439
494
  }
495
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
440
496
  fs.mkdirSync(SKILLS_DIR, { recursive: true });
441
497
  const newChecksums = Object.assign({}, storedChecksums);
442
498
  for (const name of skillNames) {
443
499
  if (modifiedSkills.includes(name)) continue;
444
- if (!needsUpdate.includes(name) && fs.existsSync(path.join(SKILLS_DIR, name))) continue;
445
- copyDir(path.join(PACKAGE_SKILLS_DIR, name), path.join(SKILLS_DIR, name));
500
+ if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) continue;
501
+ installSkillFiles(name);
446
502
  newChecksums[name] = packageHashes[name];
447
503
  }
448
504
  fs.mkdirSync(HOOKS_DIR, { recursive: true });
@@ -463,6 +519,7 @@ async function main() {
463
519
  process.exit(0);
464
520
  }
465
521
 
522
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
466
523
  fs.mkdirSync(SKILLS_DIR, { recursive: true });
467
524
  const newChecksums = Object.assign({}, storedChecksums);
468
525
  const updated = [];
@@ -472,13 +529,13 @@ async function main() {
472
529
  if (modifiedSkills.includes(name)) {
473
530
  // Backup local version, install new version
474
531
  backupSkill(name);
475
- copyDir(path.join(PACKAGE_SKILLS_DIR, name), path.join(SKILLS_DIR, name));
532
+ installSkillFiles(name);
476
533
  newChecksums[name] = packageHashes[name];
477
534
  mergeNeeded.push(name);
478
535
  continue;
479
536
  }
480
- if (!needsUpdate.includes(name) && fs.existsSync(path.join(SKILLS_DIR, name))) continue;
481
- copyDir(path.join(PACKAGE_SKILLS_DIR, name), path.join(SKILLS_DIR, name));
537
+ if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) continue;
538
+ installSkillFiles(name);
482
539
  newChecksums[name] = packageHashes[name];
483
540
  updated.push(name);
484
541
  }
@@ -528,7 +585,8 @@ async function main() {
528
585
  'estack skills were updated but the user had local modifications to: ' +
529
586
  mergeNeeded.join(', ') + '. ' +
530
587
  'Their previous versions are saved at ' + BACKUP_DIR + '. ' +
531
- 'The new upstream versions are now installed at ' + SKILLS_DIR + '. ' +
588
+ 'The new upstream versions are now installed at ' + AGENTS_DIR + ' ' +
589
+ '(symlinked from ' + SKILLS_DIR + '). ' +
532
590
  'Offer to merge their customizations from the backup into the updated versions. ' +
533
591
  'To merge: read both the backup version and the new version of each skill, ' +
534
592
  'identify the user\'s changes, and apply them to the new version where compatible.';
@@ -608,7 +666,10 @@ async function main() {
608
666
  }
609
667
 
610
668
  // 8. Install skills
611
- if (!DRY_RUN) fs.mkdirSync(SKILLS_DIR, { recursive: true });
669
+ if (!DRY_RUN) {
670
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
671
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
672
+ }
612
673
  const newChecksums = Object.assign({}, storedChecksums);
613
674
  let installedCount = 0;
614
675
  const mergedSkills = [];
@@ -617,7 +678,8 @@ async function main() {
617
678
  if (modifiedSkills.includes(name)) {
618
679
  if (modifiedAction === 'skip') {
619
680
  console.log(' Skipped ' + name + ' (local modifications preserved)');
620
- const currentHash = computeSkillHash(path.join(SKILLS_DIR, name));
681
+ const currentHash = computeSkillHash(path.join(AGENTS_DIR, name)) ||
682
+ computeSkillHash(path.join(SKILLS_DIR, name));
621
683
  if (currentHash) newChecksums[name] = currentHash;
622
684
  continue;
623
685
  }
@@ -627,13 +689,15 @@ async function main() {
627
689
  console.log((DRY_RUN ? ' [dry run] Would back up ' : ' Backed up ') + name + ' → ~/.estack-backup/' + name);
628
690
  }
629
691
  // overwrite or merge — fall through to install
630
- } else if (!needsUpdate.includes(name) && fs.existsSync(path.join(SKILLS_DIR, name))) {
692
+ } else if (!needsUpdate.includes(name) && fs.existsSync(path.join(AGENTS_DIR, name))) {
631
693
  // Already installed and up-to-date
632
694
  if (DRY_RUN) console.log(' [dry run] Up to date (no change): ' + name);
633
695
  continue;
634
696
  }
635
- const isUpdate = fs.existsSync(path.join(SKILLS_DIR, name));
636
- if (!DRY_RUN) copyDir(path.join(PACKAGE_SKILLS_DIR, name), path.join(SKILLS_DIR, name));
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);
637
701
  newChecksums[name] = packageHashes[name];
638
702
  installedCount++;
639
703
  if (DRY_RUN) {
@@ -689,13 +753,13 @@ async function main() {
689
753
  // 11. Summary output
690
754
  if (DRY_RUN) {
691
755
  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/');
756
+ console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.agents/ (linked from ~/.claude/skills/)');
693
757
  if (installedHookCount > 0) {
694
758
  console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' would be installed/updated in ~/.claude/hooks/');
695
759
  }
696
760
  } else {
697
761
  console.log('\nestack installed successfully!\n');
698
- 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/)');
699
763
  if (installedHookCount > 0) {
700
764
  console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' installed to ~/.claude/hooks/');
701
765
  }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @version 1.0.0
2
3
  // PostToolUse hook: nudges toward the repo-search skill when GitHub is involved.
3
4
  // Fires on every WebFetch or WebSearch that touches a github.com URL.
4
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elliot-stack",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Elliot's skill stack for Claude Code — install via npx elliot-stack@latest",
5
5
  "bin": {
6
6
  "elliot-stack": "bin/install.cjs"
@@ -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-better-title
3
+ version: 1.0.0
3
4
  description: (better-title) Suggest better chat session titles and rename the session
4
5
  disable-model-invocation: true
5
6
  allowed-tools: Bash, AskUserQuestion
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: estack-chris-voss
3
+ version: 1.0.0
3
4
  description: >
4
5
  (chris-voss) Applies Chris Voss negotiation principles from *Never Split the Difference* to any situation
5
6
  where understanding human psychology, persuasion, or influence would improve the output. Use
@@ -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-github-issue-tracker
3
+ version: 1.0.0
3
4
  description: >
4
5
  (github-issue-tracker) GitHub issue tracker management. Checks all open issues the user is involved in,
5
6
  finds related/duplicate issues, reports what changed, and recommends next steps.
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: estack-repo-search
3
+ version: 1.0.0
3
4
  description: >-
4
5
  (repo-search) Clone and search external GitHub repositories to answer questions about their
5
6
  code. Use this skill whenever the user references a repo you don't have local