create-claude-cabinet 0.38.0 → 0.40.0

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.
Files changed (114) hide show
  1. package/README.md +15 -4
  2. package/lib/cli.js +71 -2
  3. package/lib/engagement-server-setup.js +189 -0
  4. package/lib/mux-setup.js +193 -0
  5. package/lib/settings-merge.js +95 -1
  6. package/package.json +1 -1
  7. package/templates/cabinet/_cabinet-member-template.md +6 -20
  8. package/templates/cabinet/committees.yaml +1 -0
  9. package/templates/cabinet/watchtower-contracts.md +159 -0
  10. package/templates/engagement/capture-and-encrypt.mjs +14 -5
  11. package/templates/engagement/engagement-schema.md +94 -8
  12. package/templates/engagement/engagement-transport.mjs +99 -2
  13. package/templates/engagement/sql-constants.mjs +1 -1
  14. package/templates/engagement-server/Dockerfile +14 -0
  15. package/templates/engagement-server/engage-server.mjs +211 -0
  16. package/templates/engagement-server/package.json +12 -0
  17. package/templates/engagement-server/railway.toml +10 -0
  18. package/templates/engagement-server/schema.sql +46 -0
  19. package/templates/engagement-server/server.mjs +464 -0
  20. package/templates/hooks/watchtower-session-end.sh +74 -0
  21. package/templates/hooks/watchtower-session-start.sh +41 -0
  22. package/templates/mux/bin/mux +1305 -0
  23. package/templates/mux/config/__pycache__/muxlib.cpython-314.pyc +0 -0
  24. package/templates/mux/config/_mux +70 -0
  25. package/templates/mux/config/context-help.py +49 -0
  26. package/templates/mux/config/dashboard-wrapper.sh +16 -0
  27. package/templates/mux/config/dashboard.py +122 -0
  28. package/templates/mux/config/dx-wrapper.sh +12 -0
  29. package/templates/mux/config/help.txt +43 -0
  30. package/templates/mux/config/manage-dx.py +105 -0
  31. package/templates/mux/config/manage-notes.py +73 -0
  32. package/templates/mux/config/mux-server.py +298 -0
  33. package/templates/mux/config/mux.bash +43 -0
  34. package/templates/mux/config/muxlib.py +425 -0
  35. package/templates/mux/config/show-dx.py +89 -0
  36. package/templates/mux/config/show-notes.py +87 -0
  37. package/templates/mux/config/sidebar-wrapper.sh +56 -0
  38. package/templates/mux/config/status-indicators.sh +44 -0
  39. package/templates/mux/config/status-popup.py +200 -0
  40. package/templates/mux/config/status-set.sh +44 -0
  41. package/templates/mux/config/trail-add.sh +49 -0
  42. package/templates/mux/config/trail-popup.py +210 -0
  43. package/templates/mux/config/worktree-cleanup.sh +86 -0
  44. package/templates/mux/config/worktree-health-popup.sh +23 -0
  45. package/templates/mux/config/worktree-session-health.sh +105 -0
  46. package/templates/rules/maintainability.md +92 -0
  47. package/templates/rules/memory-capture.md +4 -2
  48. package/templates/scripts/watchtower-build-context.mjs +336 -0
  49. package/templates/scripts/watchtower-lib.mjs +118 -0
  50. package/templates/scripts/watchtower-queue.mjs +269 -0
  51. package/templates/scripts/watchtower-ring1-runner.sh +77 -0
  52. package/templates/scripts/watchtower-ring1.mjs +903 -0
  53. package/templates/scripts/watchtower-ring2-runner.sh +92 -0
  54. package/templates/scripts/watchtower-ring2.mjs +1326 -0
  55. package/templates/scripts/watchtower-ring3-close.mjs +1291 -0
  56. package/templates/scripts/watchtower-status.sh +260 -0
  57. package/templates/scripts/watchtower-validate.mjs +134 -0
  58. package/templates/skills/briefing/SKILL.md +281 -0
  59. package/templates/skills/cabinet-accessibility/SKILL.md +58 -223
  60. package/templates/skills/cabinet-anthropic-insider/SKILL.md +63 -296
  61. package/templates/skills/cabinet-anti-confirmation/SKILL.md +36 -152
  62. package/templates/skills/cabinet-architecture/SKILL.md +57 -265
  63. package/templates/skills/cabinet-automation/SKILL.md +75 -398
  64. package/templates/skills/cabinet-boundary-man/SKILL.md +56 -194
  65. package/templates/skills/cabinet-cc-health/SKILL.md +62 -462
  66. package/templates/skills/cabinet-cc-health/migration-reference.md +46 -0
  67. package/templates/skills/cabinet-data-integrity/SKILL.md +51 -142
  68. package/templates/skills/cabinet-debugger/SKILL.md +65 -209
  69. package/templates/skills/cabinet-elegance/SKILL.md +87 -0
  70. package/templates/skills/cabinet-framework-quality/SKILL.md +76 -387
  71. package/templates/skills/cabinet-goal-alignment/SKILL.md +62 -218
  72. package/templates/skills/cabinet-historian/SKILL.md +74 -320
  73. package/templates/skills/cabinet-information-design/SKILL.md +87 -432
  74. package/templates/skills/cabinet-interactive-storyteller/SKILL.md +75 -307
  75. package/templates/skills/cabinet-mantine-quality/SKILL.md +40 -293
  76. package/templates/skills/cabinet-narrative-architect/SKILL.md +65 -254
  77. package/templates/skills/cabinet-organized-mind/SKILL.md +88 -340
  78. package/templates/skills/cabinet-process-therapist/SKILL.md +68 -233
  79. package/templates/skills/cabinet-qa/SKILL.md +55 -195
  80. package/templates/skills/cabinet-record-keeper/SKILL.md +57 -170
  81. package/templates/skills/cabinet-roster-check/SKILL.md +72 -300
  82. package/templates/skills/cabinet-security/SKILL.md +56 -211
  83. package/templates/skills/cabinet-small-screen/SKILL.md +36 -138
  84. package/templates/skills/cabinet-speed-freak/SKILL.md +34 -198
  85. package/templates/skills/cabinet-system-advocate/SKILL.md +60 -170
  86. package/templates/skills/cabinet-technical-debt/SKILL.md +34 -176
  87. package/templates/skills/cabinet-ui-experimentalist/SKILL.md +65 -231
  88. package/templates/skills/cabinet-usability/SKILL.md +57 -175
  89. package/templates/skills/cabinet-user-advocate/SKILL.md +67 -290
  90. package/templates/skills/cabinet-vision/SKILL.md +60 -224
  91. package/templates/skills/cabinet-workflow-cop/SKILL.md +61 -226
  92. package/templates/skills/collab-client/SKILL.md +211 -46
  93. package/templates/skills/collab-consultant/SKILL.md +458 -76
  94. package/templates/skills/decisions/SKILL.md +13 -0
  95. package/templates/skills/dx-feedback/SKILL.md +125 -0
  96. package/templates/skills/inbox/SKILL.md +181 -0
  97. package/templates/skills/investigate/SKILL.md +2 -2
  98. package/templates/skills/orient/phases/dx-captures.md +51 -0
  99. package/templates/skills/plan/phases/verify-plan.md +3 -3
  100. package/templates/skills/setup-accounts/SKILL.md +88 -6
  101. package/templates/skills/watchtower/SKILL.md +253 -0
  102. package/templates/watchtower/com.claude-cabinet.watchtower-ring1.plist +38 -0
  103. package/templates/watchtower/com.claude-cabinet.watchtower-ring2-fast.plist +39 -0
  104. package/templates/watchtower/com.claude-cabinet.watchtower-ring2-slow.plist +39 -0
  105. package/templates/watchtower/config.json.template +25 -0
  106. package/templates/watchtower/queue/items/item.json.schema +78 -0
  107. package/templates/watchtower/state/projects/project.md.template +13 -0
  108. package/templates/watchtower/state/summary.md.template +18 -0
  109. package/templates/watchtower/watchtower-ring1.service +16 -0
  110. package/templates/watchtower/watchtower-ring1.timer +15 -0
  111. package/templates/watchtower/watchtower-ring2-fast.service +16 -0
  112. package/templates/watchtower/watchtower-ring2-fast.timer +15 -0
  113. package/templates/watchtower/watchtower-ring2-slow.service +16 -0
  114. package/templates/watchtower/watchtower-ring2-slow.timer +15 -0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Claude Cabinet
2
2
 
3
3
  A cabinet of expert advisors for your Claude Code project. One command
4
- gives Claude a memory, 31 domain experts, a planning process, and the
4
+ gives Claude a memory, 32 domain experts, a planning process, and the
5
5
  habit of starting sessions informed and ending them properly.
6
6
 
7
7
  Built by a guy who'd rather talk to Claude than write code. Most of it
@@ -12,7 +12,7 @@ was built by Claude. I just complained until it (mostly) worked.
12
12
  Your project gets a cabinet — specialist advisors who each own a domain
13
13
  and weigh in when their expertise matters:
14
14
 
15
- - **Cabinet members** — 31 domain experts (security, accessibility,
15
+ - **Cabinet members** — 32 domain experts (security, accessibility,
16
16
  architecture, QA, etc.) who review your project and surface what
17
17
  you'd miss alone
18
18
  - **Briefings** — project context members read before weighing in
@@ -78,7 +78,7 @@ left off.
78
78
 
79
79
  ### The Cabinet (included in lean)
80
80
 
81
- 31 expert cabinet members who each own a domain and stay in their lane.
81
+ 32 expert cabinet members who each own a domain and stay in their lane.
82
82
  **Speed-freak** watches performance. **Boundary-man** catches edge cases.
83
83
  **Record-keeper** flags when docs drift from code. **Workflow-cop**
84
84
  evaluates whether your process actually works. Each member has a
@@ -221,6 +221,17 @@ the listed modules to what's already there, it doesn't replace your
221
221
  module set. Safe to run on a mature project without losing
222
222
  customization. You can pass multiple modules: `--modules verify,audit`.
223
223
 
224
+ ### Opt-in Modules
225
+
226
+ | Module | What it does |
227
+ |--------|-------------|
228
+ | **verify** | Cucumber + Playwright walkthrough verification harness |
229
+ | **site-audit** | 14-check deployed-site quality audit with HTML reports |
230
+ | **engagement** | Client engagement management — packets, billing, feedback loops |
231
+ | **engagement-server** | Central multi-engagement API server (Railway/Fly deploy) |
232
+ | **watchtower** | Continuous background state management replacing orient/debrief |
233
+ | **mux** | Multi-project terminal manager — desks, auto-worktrees with shared identity, trail logging, DX captures, portal color-switching |
234
+
224
235
  ## CLI Options
225
236
 
226
237
  ```
@@ -240,7 +251,7 @@ source code.
240
251
  ```
241
252
  .claude/
242
253
  ├── skills/ # orient, debrief, plan, execute, audit, etc.
243
- │ └── cabinet-*/ # 31 cabinet member definitions
254
+ │ └── cabinet-*/ # 32 cabinet member definitions
244
255
  ├── cabinet/ # committees, lifecycle, composition patterns
245
256
  │ # (incl. pib-db-access.md, pib-db-triggers.md)
246
257
  ├── briefing/ # project briefing templates
package/lib/cli.js CHANGED
@@ -4,12 +4,14 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const crypto = require('crypto');
6
6
  const { copyTemplates } = require('./copy');
7
- const { mergeSettings, healUserSettings } = require('./settings-merge');
7
+ const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks } = require('./settings-merge');
8
8
  const { create: createMetadata, read: readMetadata } = require('./metadata');
9
9
  const { setupDb } = require('./db-setup');
10
10
  const { setupVerifyRuntime } = require('./verify-setup');
11
11
  const { setupSiteAuditRuntime } = require('./site-audit-setup');
12
12
  const { setupEngagement } = require('./engagement-setup');
13
+ const { setupMux } = require('./mux-setup');
14
+ const { setupEngagementServer } = require('./engagement-server-setup');
13
15
  const { reset } = require('./reset');
14
16
 
15
17
  const VERSION = require('../package.json').version;
@@ -494,7 +496,7 @@ const MODULES = {
494
496
  mandatory: false,
495
497
  default: true,
496
498
  lean: false,
497
- templates: ['rules/enforcement-pipeline.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
499
+ templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
498
500
  },
499
501
  'memory': {
500
502
  name: 'Built-In Memory (cc-remember + reader + validator)',
@@ -543,6 +545,7 @@ const MODULES = {
543
545
  'skills/cabinet-ui-experimentalist', 'skills/cabinet-user-advocate',
544
546
  'skills/cabinet-vision',
545
547
  'skills/cabinet-narrative-architect', 'skills/cabinet-interactive-storyteller',
548
+ 'skills/cabinet-elegance',
546
549
  'scripts/merge-findings.js', 'scripts/load-triage-history.js',
547
550
  'scripts/triage-server.mjs', 'scripts/triage-ui.html',
548
551
  'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
@@ -618,6 +621,60 @@ const MODULES = {
618
621
  'engagement',
619
622
  ],
620
623
  },
624
+ watchtower: {
625
+ name: 'Watchtower (continuous background state)',
626
+ description: 'Replaces orient/debrief with continuous background processing. Three rings (mechanical cron, Claude intelligence, session-aware), ambient state injection via SessionStart hook, and an inbox for extracted knowledge and signals. Sessions start informed with minimal context cost.',
627
+ mandatory: false,
628
+ default: false,
629
+ lean: false,
630
+ templates: [
631
+ 'scripts/watchtower-validate.mjs',
632
+ 'cabinet/watchtower-contracts.md',
633
+ 'scripts/watchtower-lib.mjs',
634
+ 'scripts/watchtower-queue.mjs',
635
+ 'skills/inbox',
636
+ 'skills/decisions',
637
+ 'hooks/watchtower-session-start.sh',
638
+ 'scripts/watchtower-build-context.mjs',
639
+ 'scripts/watchtower-ring1.mjs',
640
+ 'scripts/watchtower-ring1-runner.sh',
641
+ 'skills/watchtower',
642
+ 'watchtower/com.claude-cabinet.watchtower-ring1.plist',
643
+ 'watchtower/watchtower-ring1.service',
644
+ 'watchtower/watchtower-ring1.timer',
645
+ 'scripts/watchtower-ring2.mjs',
646
+ 'scripts/watchtower-ring2-runner.sh',
647
+ 'watchtower/com.claude-cabinet.watchtower-ring2-fast.plist',
648
+ 'watchtower/com.claude-cabinet.watchtower-ring2-slow.plist',
649
+ 'watchtower/watchtower-ring2-fast.service',
650
+ 'watchtower/watchtower-ring2-fast.timer',
651
+ 'watchtower/watchtower-ring2-slow.service',
652
+ 'watchtower/watchtower-ring2-slow.timer',
653
+ 'hooks/watchtower-session-end.sh',
654
+ 'scripts/watchtower-ring3-close.mjs',
655
+ 'scripts/watchtower-status.sh',
656
+ 'skills/briefing',
657
+ ],
658
+ },
659
+ mux: {
660
+ name: 'Mux (tmux project manager)',
661
+ description: 'Multi-project terminal manager. Desks (tmux sessions), auto-worktrees, trail logging, sticky notes, portal color-switching, and an MCP server for Claude. Installs to user-level paths (~/.local/bin/, ~/.config/mux/).',
662
+ mandatory: false,
663
+ default: false,
664
+ lean: false,
665
+ postInstall: 'mux-setup',
666
+ templates: ['skills/orient/phases/dx-captures.md'],
667
+ },
668
+ 'engagement-server': {
669
+ name: 'Engagement Server',
670
+ description: 'Central multi-engagement API server. Deploys once to Railway/Fly, serves all client engagements. User-level infrastructure.',
671
+ mandatory: false,
672
+ default: false,
673
+ lean: false,
674
+ requires: ['engagement', 'work-tracking'],
675
+ postInstall: 'engagement-server-setup',
676
+ templates: [],
677
+ },
621
678
  };
622
679
 
623
680
  /** Recursively collect all relative file paths under a directory. */
@@ -1225,6 +1282,16 @@ async function run() {
1225
1282
  if (selectedModules.includes('hooks') && !flags.dryRun) {
1226
1283
  const settingsPath = mergeSettings(projectDir, { includeDb });
1227
1284
  console.log(` ⚙️ Merged hooks into ${path.relative(projectDir, settingsPath)}`);
1285
+
1286
+ if (selectedModules.includes('watchtower')) {
1287
+ mergeWatchtowerHooks(settingsPath);
1288
+ console.log(' ⚙️ Registered watchtower SessionStart/SessionEnd hooks');
1289
+ }
1290
+
1291
+ if (selectedModules.includes('mux')) {
1292
+ mergeMuxHooks(settingsPath);
1293
+ console.log(' ⚙️ Registered mux worktree health SessionStart hook');
1294
+ }
1228
1295
  }
1229
1296
 
1230
1297
  // --- Heal user-level ~/.claude/settings.json ---
@@ -1276,6 +1343,8 @@ async function run() {
1276
1343
  'verify-setup': setupVerifyRuntime,
1277
1344
  'site-audit-setup': setupSiteAuditRuntime,
1278
1345
  'engagement-setup': setupEngagement,
1346
+ 'mux-setup': setupMux,
1347
+ 'engagement-server-setup': setupEngagementServer,
1279
1348
  };
1280
1349
  for (const moduleKey of selectedModules) {
1281
1350
  const mod = MODULES[moduleKey];
@@ -0,0 +1,189 @@
1
+ /**
2
+ * engagement-server-setup.js — install engagement server to user-level paths.
3
+ *
4
+ * Follows the mux-setup.js pattern: user-level infrastructure managed by CC,
5
+ * tracked via global manifest at ~/.claude-cabinet/global-manifest.json.
6
+ *
7
+ * Installs to ~/.claude-cabinet/engagement-server/. The user deploys to
8
+ * Railway (or similar) separately.
9
+ *
10
+ * Version semantics:
11
+ * - First install: copy all managed files, create data dir, write .cc-version
12
+ * - Same version: skip with log
13
+ * - Newer CC version: upgrade managed files, preserve data dir
14
+ * - Older CC version than installed: skip (don't downgrade)
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const crypto = require('crypto');
21
+
22
+ const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
23
+ const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
24
+ const INSTALL_DIR = path.join(CC_HOME, 'engagement-server');
25
+ const VERSION_FILE = path.join(INSTALL_DIR, '.cc-version');
26
+
27
+ const MANAGED_FILES = [
28
+ 'server.mjs',
29
+ 'engage-server.mjs',
30
+ 'schema.sql',
31
+ 'package.json',
32
+ 'Dockerfile',
33
+ 'railway.toml',
34
+ ];
35
+
36
+ const DATA_DIRS = [
37
+ path.join(INSTALL_DIR, 'data'),
38
+ path.join(INSTALL_DIR, 'migrations'),
39
+ ];
40
+
41
+ function sha256(content) {
42
+ return crypto.createHash('sha256').update(content).digest('hex');
43
+ }
44
+
45
+ function compareVersions(a, b) {
46
+ const pa = a.split('.').map(Number);
47
+ const pb = b.split('.').map(Number);
48
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
49
+ const va = pa[i] || 0;
50
+ const vb = pb[i] || 0;
51
+ if (va > vb) return 1;
52
+ if (va < vb) return -1;
53
+ }
54
+ return 0;
55
+ }
56
+
57
+ function readGlobalManifest() {
58
+ if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
59
+ try {
60
+ return JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
61
+ } catch {
62
+ return { files: {} };
63
+ }
64
+ }
65
+
66
+ function writeGlobalManifest(manifest) {
67
+ fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
68
+ const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
69
+ fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
70
+ fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
71
+ }
72
+
73
+ function readInstalledVersion() {
74
+ if (!fs.existsSync(VERSION_FILE)) return null;
75
+ try {
76
+ return fs.readFileSync(VERSION_FILE, 'utf8').trim();
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function writeVersion(version) {
83
+ fs.mkdirSync(path.dirname(VERSION_FILE), { recursive: true });
84
+ fs.writeFileSync(VERSION_FILE, version + '\n');
85
+ }
86
+
87
+ /**
88
+ * @param {Object} opts
89
+ * @param {boolean} [opts.dryRun]
90
+ * @param {string} [opts.projectDir]
91
+ * @returns {{ results: string[], status: string }}
92
+ */
93
+ function setupEngagementServer(opts = {}) {
94
+ const dryRun = !!opts.dryRun;
95
+ const results = [];
96
+
97
+ const ccVersion = require('../package.json').version;
98
+ const templateDir = path.resolve(__dirname, '..', 'templates', 'engagement-server');
99
+
100
+ if (!fs.existsSync(templateDir)) {
101
+ throw new Error(`engagement-server-setup: ${templateDir} not found.`);
102
+ }
103
+
104
+ const installedVersion = readInstalledVersion();
105
+
106
+ if (installedVersion) {
107
+ const cmp = compareVersions(ccVersion, installedVersion);
108
+ if (cmp === 0) {
109
+ results.push(`engagement-server ${installedVersion} already installed — skipping`);
110
+ return { results, status: 'skipped' };
111
+ }
112
+ if (cmp < 0) {
113
+ results.push(`engagement-server ${installedVersion} is newer than CC ${ccVersion} — skipping (won't downgrade)`);
114
+ return { results, status: 'skipped' };
115
+ }
116
+ results.push(`Upgrading engagement-server from ${installedVersion} to ${ccVersion}`);
117
+ } else {
118
+ results.push(`Installing engagement-server ${ccVersion}`);
119
+ }
120
+
121
+ // Create data directories (preserved across upgrades)
122
+ for (const dir of DATA_DIRS) {
123
+ if (!fs.existsSync(dir)) {
124
+ if (dryRun) {
125
+ results.push(` [dry-run] mkdir -p ${dir}`);
126
+ } else {
127
+ fs.mkdirSync(dir, { recursive: true });
128
+ }
129
+ }
130
+ }
131
+
132
+ // Copy managed files
133
+ const manifest = readGlobalManifest();
134
+ let copiedCount = 0;
135
+
136
+ for (const file of MANAGED_FILES) {
137
+ const srcPath = path.join(templateDir, file);
138
+ const destPath = path.join(INSTALL_DIR, file);
139
+
140
+ if (!fs.existsSync(srcPath)) {
141
+ results.push(` ⚠ Template missing: ${file}`);
142
+ continue;
143
+ }
144
+
145
+ const content = fs.readFileSync(srcPath);
146
+ const hash = sha256(content);
147
+
148
+ if (manifest.files[destPath] === hash) {
149
+ continue;
150
+ }
151
+
152
+ if (dryRun) {
153
+ results.push(` [dry-run] ${file} → ${destPath}`);
154
+ } else {
155
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
156
+ fs.writeFileSync(destPath, content);
157
+ manifest.files[destPath] = hash;
158
+ }
159
+ copiedCount++;
160
+ }
161
+
162
+ if (!dryRun) {
163
+ manifest.version = ccVersion;
164
+ manifest.installedAt = new Date().toISOString();
165
+ writeGlobalManifest(manifest);
166
+ writeVersion(ccVersion);
167
+ }
168
+
169
+ if (copiedCount > 0) {
170
+ results.push(` Copied ${copiedCount} file(s) to ${INSTALL_DIR}`);
171
+ }
172
+
173
+ if (!installedVersion) {
174
+ results.push('');
175
+ results.push(' Next steps:');
176
+ results.push(` cd ${INSTALL_DIR}`);
177
+ results.push(' npm install');
178
+ results.push(' railway init # or railway link');
179
+ results.push(' railway volume add -m /app/data');
180
+ results.push(' railway up --detach');
181
+ } else {
182
+ results.push('');
183
+ results.push(` Re-deploy: cd ${INSTALL_DIR} && railway up --detach`);
184
+ }
185
+
186
+ return { results, status: installedVersion ? 'upgraded' : 'installed' };
187
+ }
188
+
189
+ module.exports = { setupEngagementServer };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * mux-setup.js — install mux files to user-level paths.
3
+ *
4
+ * First user-level module in CC. Unlike project-scoped templates that
5
+ * copy to .claude/ and scripts/, mux installs to ~/.local/bin/,
6
+ * ~/.config/mux/, and ~/.local/share/mux/. Tracked via a global
7
+ * manifest at ~/.claude-cabinet/global-manifest.json.
8
+ *
9
+ * Version semantics:
10
+ * - First install: copy all managed files, write .cc-version
11
+ * - Same version: skip with log
12
+ * - Newer CC version: upgrade managed files, preserve data dirs
13
+ * - Older CC version than installed: skip (don't downgrade)
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const crypto = require('crypto');
20
+
21
+ const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
22
+ const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
23
+ const MUX_VERSION_FILE = path.join(os.homedir(), '.config', 'mux', '.cc-version');
24
+
25
+ const MANAGED_FILES = [
26
+ { src: 'bin/mux', dest: path.join(os.homedir(), '.local', 'bin', 'mux'), mode: 0o755 },
27
+ { src: 'config/muxlib.py', dest: path.join(os.homedir(), '.config', 'mux', 'muxlib.py') },
28
+ { src: 'config/trail-add.sh', dest: path.join(os.homedir(), '.config', 'mux', 'trail-add.sh'), mode: 0o755 },
29
+ { src: 'config/status-set.sh', dest: path.join(os.homedir(), '.config', 'mux', 'status-set.sh'), mode: 0o755 },
30
+ { src: 'config/trail-popup.py', dest: path.join(os.homedir(), '.config', 'mux', 'trail-popup.py') },
31
+ { src: 'config/status-popup.py', dest: path.join(os.homedir(), '.config', 'mux', 'status-popup.py') },
32
+ { src: 'config/dashboard.py', dest: path.join(os.homedir(), '.config', 'mux', 'dashboard.py') },
33
+ { src: 'config/dashboard-wrapper.sh', dest: path.join(os.homedir(), '.config', 'mux', 'dashboard-wrapper.sh'), mode: 0o755 },
34
+ { src: 'config/sidebar-wrapper.sh', dest: path.join(os.homedir(), '.config', 'mux', 'sidebar-wrapper.sh'), mode: 0o755 },
35
+ { src: 'config/status-indicators.sh', dest: path.join(os.homedir(), '.config', 'mux', 'status-indicators.sh'), mode: 0o755 },
36
+ { src: 'config/context-help.py', dest: path.join(os.homedir(), '.config', 'mux', 'context-help.py') },
37
+ { src: 'config/show-notes.py', dest: path.join(os.homedir(), '.config', 'mux', 'show-notes.py') },
38
+ { src: 'config/manage-notes.py', dest: path.join(os.homedir(), '.config', 'mux', 'manage-notes.py') },
39
+ { src: 'config/manage-dx.py', dest: path.join(os.homedir(), '.config', 'mux', 'manage-dx.py') },
40
+ { src: 'config/show-dx.py', dest: path.join(os.homedir(), '.config', 'mux', 'show-dx.py') },
41
+ { src: 'config/mux-server.py', dest: path.join(os.homedir(), '.config', 'mux', 'mux-server.py') },
42
+ { src: 'config/help.txt', dest: path.join(os.homedir(), '.config', 'mux', 'help.txt') },
43
+ { src: 'config/_mux', dest: path.join(os.homedir(), '.config', 'mux', '_mux') },
44
+ { src: 'config/mux.bash', dest: path.join(os.homedir(), '.config', 'mux', 'mux.bash') },
45
+ { src: 'config/worktree-session-health.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-session-health.sh'), mode: 0o755 },
46
+ { src: 'config/worktree-health-popup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-health-popup.sh'), mode: 0o755 },
47
+ { src: 'config/worktree-cleanup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-cleanup.sh'), mode: 0o755 },
48
+ ];
49
+
50
+ const DATA_DIRS = [
51
+ path.join(os.homedir(), '.local', 'share', 'mux', 'trails'),
52
+ path.join(os.homedir(), '.local', 'share', 'mux', 'status'),
53
+ path.join(os.homedir(), '.config', 'mux', 'notes'),
54
+ path.join(os.homedir(), '.config', 'mux', 'dx'),
55
+ path.join(os.homedir(), '.config', 'mux', 'pending-prompts'),
56
+ path.join(os.homedir(), '.local', 'share', 'mux', 'wt-health'),
57
+ ];
58
+
59
+ function sha256(content) {
60
+ return crypto.createHash('sha256').update(content).digest('hex');
61
+ }
62
+
63
+ function compareVersions(a, b) {
64
+ const pa = a.split('.').map(Number);
65
+ const pb = b.split('.').map(Number);
66
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
67
+ const va = pa[i] || 0;
68
+ const vb = pb[i] || 0;
69
+ if (va > vb) return 1;
70
+ if (va < vb) return -1;
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ function readGlobalManifest() {
76
+ if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
77
+ try {
78
+ return JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
79
+ } catch {
80
+ return { files: {} };
81
+ }
82
+ }
83
+
84
+ function writeGlobalManifest(manifest) {
85
+ fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
86
+ const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
87
+ fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
88
+ fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
89
+ }
90
+
91
+ function readInstalledVersion() {
92
+ if (!fs.existsSync(MUX_VERSION_FILE)) return null;
93
+ try {
94
+ return fs.readFileSync(MUX_VERSION_FILE, 'utf8').trim();
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function writeVersion(version) {
101
+ fs.mkdirSync(path.dirname(MUX_VERSION_FILE), { recursive: true });
102
+ fs.writeFileSync(MUX_VERSION_FILE, version + '\n');
103
+ }
104
+
105
+ /**
106
+ * @param {Object} opts
107
+ * @param {boolean} [opts.dryRun]
108
+ * @param {string} [opts.projectDir] — consumer project dir (unused for mux)
109
+ * @returns {{ results: string[], status: string }}
110
+ */
111
+ function setupMux(opts = {}) {
112
+ const dryRun = !!opts.dryRun;
113
+ const results = [];
114
+
115
+ const ccVersion = require('../package.json').version;
116
+ const templateDir = path.resolve(__dirname, '..', 'templates', 'mux');
117
+
118
+ if (!fs.existsSync(templateDir)) {
119
+ throw new Error(`mux-setup: ${templateDir} not found.`);
120
+ }
121
+
122
+ const installedVersion = readInstalledVersion();
123
+
124
+ if (installedVersion) {
125
+ const cmp = compareVersions(ccVersion, installedVersion);
126
+ if (cmp === 0) {
127
+ results.push(`mux ${installedVersion} already installed — skipping`);
128
+ return { results, status: 'skipped' };
129
+ }
130
+ if (cmp < 0) {
131
+ results.push(`mux ${installedVersion} is newer than CC ${ccVersion} — skipping (won't downgrade)`);
132
+ return { results, status: 'skipped' };
133
+ }
134
+ results.push(`Upgrading mux from ${installedVersion} to ${ccVersion}`);
135
+ } else {
136
+ results.push(`Installing mux ${ccVersion}`);
137
+ }
138
+
139
+ // Create data directories
140
+ for (const dir of DATA_DIRS) {
141
+ if (!fs.existsSync(dir)) {
142
+ if (dryRun) {
143
+ results.push(` [dry-run] mkdir -p ${dir}`);
144
+ } else {
145
+ fs.mkdirSync(dir, { recursive: true });
146
+ }
147
+ }
148
+ }
149
+
150
+ // Copy managed files
151
+ const manifest = readGlobalManifest();
152
+ let copiedCount = 0;
153
+
154
+ for (const file of MANAGED_FILES) {
155
+ const srcPath = path.join(templateDir, file.src);
156
+ if (!fs.existsSync(srcPath)) {
157
+ results.push(` ⚠ Template missing: ${file.src}`);
158
+ continue;
159
+ }
160
+
161
+ const content = fs.readFileSync(srcPath);
162
+ const hash = sha256(content);
163
+
164
+ if (manifest.files[file.dest] === hash) {
165
+ continue; // file unchanged
166
+ }
167
+
168
+ if (dryRun) {
169
+ results.push(` [dry-run] ${file.src} → ${file.dest}`);
170
+ } else {
171
+ fs.mkdirSync(path.dirname(file.dest), { recursive: true });
172
+ fs.writeFileSync(file.dest, content);
173
+ if (file.mode) {
174
+ fs.chmodSync(file.dest, file.mode);
175
+ }
176
+ manifest.files[file.dest] = hash;
177
+ }
178
+ copiedCount++;
179
+ }
180
+
181
+ if (!dryRun) {
182
+ manifest.version = ccVersion;
183
+ manifest.installedAt = new Date().toISOString();
184
+ writeGlobalManifest(manifest);
185
+ writeVersion(ccVersion);
186
+ }
187
+
188
+ results.push(` ${copiedCount} file${copiedCount !== 1 ? 's' : ''} installed to user paths`);
189
+
190
+ return { results, status: installedVersion ? 'upgraded' : 'installed' };
191
+ }
192
+
193
+ module.exports = { setupMux };
@@ -82,6 +82,45 @@ const DEFAULT_HOOKS = {
82
82
  ],
83
83
  };
84
84
 
85
+ const WATCHTOWER_HOOKS = {
86
+ SessionStart: [
87
+ {
88
+ matcher: '',
89
+ hooks: [
90
+ {
91
+ type: 'command',
92
+ command: '$HOME/.claude-cabinet/watchtower/hooks/watchtower-session-start.sh',
93
+ },
94
+ ],
95
+ },
96
+ ],
97
+ SessionEnd: [
98
+ {
99
+ matcher: '',
100
+ hooks: [
101
+ {
102
+ type: 'command',
103
+ command: '$HOME/.claude-cabinet/watchtower/hooks/watchtower-session-end.sh',
104
+ },
105
+ ],
106
+ },
107
+ ],
108
+ };
109
+
110
+ const MUX_HOOKS = {
111
+ SessionStart: [
112
+ {
113
+ matcher: '',
114
+ hooks: [
115
+ {
116
+ type: 'command',
117
+ command: '$HOME/.config/mux/worktree-session-health.sh',
118
+ },
119
+ ],
120
+ },
121
+ ],
122
+ };
123
+
85
124
  // Legacy hook script names that should be stripped on any merge.
86
125
  // Centralizes cleanup so a user who skips --migrate-memory but runs
87
126
  // any other CC operation still gets omega-era hooks pruned.
@@ -209,4 +248,59 @@ function healUserSettings() {
209
248
  return removed;
210
249
  }
211
250
 
212
- module.exports = { mergeSettings, healUserSettings, DEFAULT_HOOKS, LEGACY_HOOK_COMMANDS };
251
+ /**
252
+ * Merge watchtower-specific hooks into project settings.
253
+ * Called from the watchtower module's install path in cli.js — only
254
+ * registers SessionStart/SessionEnd hooks when watchtower is installed.
255
+ */
256
+ function mergeWatchtowerHooks(settingsPath) {
257
+ if (!fs.existsSync(settingsPath)) return;
258
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
259
+ if (!settings.hooks) settings.hooks = {};
260
+
261
+ for (const [event, newHooks] of Object.entries(WATCHTOWER_HOOKS)) {
262
+ if (!settings.hooks[event]) {
263
+ settings.hooks[event] = newHooks;
264
+ } else {
265
+ for (const newHook of newHooks) {
266
+ const hookKey = h => h.command || h.prompt || '';
267
+ const existingKeys = settings.hooks[event].flatMap(h =>
268
+ h.hooks.map(hh => hookKey(hh))
269
+ );
270
+ const newKeys = newHook.hooks.map(h => hookKey(h));
271
+ if (!newKeys.every(k => existingKeys.includes(k))) {
272
+ settings.hooks[event].push(newHook);
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
279
+ }
280
+
281
+ function mergeMuxHooks(settingsPath) {
282
+ if (!fs.existsSync(settingsPath)) return;
283
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
284
+ if (!settings.hooks) settings.hooks = {};
285
+
286
+ for (const [event, newHooks] of Object.entries(MUX_HOOKS)) {
287
+ if (!settings.hooks[event]) {
288
+ settings.hooks[event] = newHooks;
289
+ } else {
290
+ for (const newHook of newHooks) {
291
+ const hookKey = h => h.command || h.prompt || '';
292
+ const existingKeys = settings.hooks[event].flatMap(h =>
293
+ h.hooks.map(hh => hookKey(hh))
294
+ );
295
+ const newKeys = newHook.hooks.map(h => hookKey(h));
296
+ if (!newKeys.every(k => existingKeys.includes(k))) {
297
+ settings.hooks[event].push(newHook);
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
304
+ }
305
+
306
+ module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, LEGACY_HOOK_COMMANDS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"