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.
- package/README.md +15 -4
- package/lib/cli.js +71 -2
- package/lib/engagement-server-setup.js +189 -0
- package/lib/mux-setup.js +193 -0
- package/lib/settings-merge.js +95 -1
- package/package.json +1 -1
- package/templates/cabinet/_cabinet-member-template.md +6 -20
- package/templates/cabinet/committees.yaml +1 -0
- package/templates/cabinet/watchtower-contracts.md +159 -0
- package/templates/engagement/capture-and-encrypt.mjs +14 -5
- package/templates/engagement/engagement-schema.md +94 -8
- package/templates/engagement/engagement-transport.mjs +99 -2
- package/templates/engagement/sql-constants.mjs +1 -1
- package/templates/engagement-server/Dockerfile +14 -0
- package/templates/engagement-server/engage-server.mjs +211 -0
- package/templates/engagement-server/package.json +12 -0
- package/templates/engagement-server/railway.toml +10 -0
- package/templates/engagement-server/schema.sql +46 -0
- package/templates/engagement-server/server.mjs +464 -0
- package/templates/hooks/watchtower-session-end.sh +74 -0
- package/templates/hooks/watchtower-session-start.sh +41 -0
- package/templates/mux/bin/mux +1305 -0
- package/templates/mux/config/__pycache__/muxlib.cpython-314.pyc +0 -0
- package/templates/mux/config/_mux +70 -0
- package/templates/mux/config/context-help.py +49 -0
- package/templates/mux/config/dashboard-wrapper.sh +16 -0
- package/templates/mux/config/dashboard.py +122 -0
- package/templates/mux/config/dx-wrapper.sh +12 -0
- package/templates/mux/config/help.txt +43 -0
- package/templates/mux/config/manage-dx.py +105 -0
- package/templates/mux/config/manage-notes.py +73 -0
- package/templates/mux/config/mux-server.py +298 -0
- package/templates/mux/config/mux.bash +43 -0
- package/templates/mux/config/muxlib.py +425 -0
- package/templates/mux/config/show-dx.py +89 -0
- package/templates/mux/config/show-notes.py +87 -0
- package/templates/mux/config/sidebar-wrapper.sh +56 -0
- package/templates/mux/config/status-indicators.sh +44 -0
- package/templates/mux/config/status-popup.py +200 -0
- package/templates/mux/config/status-set.sh +44 -0
- package/templates/mux/config/trail-add.sh +49 -0
- package/templates/mux/config/trail-popup.py +210 -0
- package/templates/mux/config/worktree-cleanup.sh +86 -0
- package/templates/mux/config/worktree-health-popup.sh +23 -0
- package/templates/mux/config/worktree-session-health.sh +105 -0
- package/templates/rules/maintainability.md +92 -0
- package/templates/rules/memory-capture.md +4 -2
- package/templates/scripts/watchtower-build-context.mjs +336 -0
- package/templates/scripts/watchtower-lib.mjs +118 -0
- package/templates/scripts/watchtower-queue.mjs +269 -0
- package/templates/scripts/watchtower-ring1-runner.sh +77 -0
- package/templates/scripts/watchtower-ring1.mjs +903 -0
- package/templates/scripts/watchtower-ring2-runner.sh +92 -0
- package/templates/scripts/watchtower-ring2.mjs +1326 -0
- package/templates/scripts/watchtower-ring3-close.mjs +1291 -0
- package/templates/scripts/watchtower-status.sh +260 -0
- package/templates/scripts/watchtower-validate.mjs +134 -0
- package/templates/skills/briefing/SKILL.md +281 -0
- package/templates/skills/cabinet-accessibility/SKILL.md +58 -223
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +63 -296
- package/templates/skills/cabinet-anti-confirmation/SKILL.md +36 -152
- package/templates/skills/cabinet-architecture/SKILL.md +57 -265
- package/templates/skills/cabinet-automation/SKILL.md +75 -398
- package/templates/skills/cabinet-boundary-man/SKILL.md +56 -194
- package/templates/skills/cabinet-cc-health/SKILL.md +62 -462
- package/templates/skills/cabinet-cc-health/migration-reference.md +46 -0
- package/templates/skills/cabinet-data-integrity/SKILL.md +51 -142
- package/templates/skills/cabinet-debugger/SKILL.md +65 -209
- package/templates/skills/cabinet-elegance/SKILL.md +87 -0
- package/templates/skills/cabinet-framework-quality/SKILL.md +76 -387
- package/templates/skills/cabinet-goal-alignment/SKILL.md +62 -218
- package/templates/skills/cabinet-historian/SKILL.md +74 -320
- package/templates/skills/cabinet-information-design/SKILL.md +87 -432
- package/templates/skills/cabinet-interactive-storyteller/SKILL.md +75 -307
- package/templates/skills/cabinet-mantine-quality/SKILL.md +40 -293
- package/templates/skills/cabinet-narrative-architect/SKILL.md +65 -254
- package/templates/skills/cabinet-organized-mind/SKILL.md +88 -340
- package/templates/skills/cabinet-process-therapist/SKILL.md +68 -233
- package/templates/skills/cabinet-qa/SKILL.md +55 -195
- package/templates/skills/cabinet-record-keeper/SKILL.md +57 -170
- package/templates/skills/cabinet-roster-check/SKILL.md +72 -300
- package/templates/skills/cabinet-security/SKILL.md +56 -211
- package/templates/skills/cabinet-small-screen/SKILL.md +36 -138
- package/templates/skills/cabinet-speed-freak/SKILL.md +34 -198
- package/templates/skills/cabinet-system-advocate/SKILL.md +60 -170
- package/templates/skills/cabinet-technical-debt/SKILL.md +34 -176
- package/templates/skills/cabinet-ui-experimentalist/SKILL.md +65 -231
- package/templates/skills/cabinet-usability/SKILL.md +57 -175
- package/templates/skills/cabinet-user-advocate/SKILL.md +67 -290
- package/templates/skills/cabinet-vision/SKILL.md +60 -224
- package/templates/skills/cabinet-workflow-cop/SKILL.md +61 -226
- package/templates/skills/collab-client/SKILL.md +211 -46
- package/templates/skills/collab-consultant/SKILL.md +458 -76
- package/templates/skills/decisions/SKILL.md +13 -0
- package/templates/skills/dx-feedback/SKILL.md +125 -0
- package/templates/skills/inbox/SKILL.md +181 -0
- package/templates/skills/investigate/SKILL.md +2 -2
- package/templates/skills/orient/phases/dx-captures.md +51 -0
- package/templates/skills/plan/phases/verify-plan.md +3 -3
- package/templates/skills/setup-accounts/SKILL.md +88 -6
- package/templates/skills/watchtower/SKILL.md +253 -0
- package/templates/watchtower/com.claude-cabinet.watchtower-ring1.plist +38 -0
- package/templates/watchtower/com.claude-cabinet.watchtower-ring2-fast.plist +39 -0
- package/templates/watchtower/com.claude-cabinet.watchtower-ring2-slow.plist +39 -0
- package/templates/watchtower/config.json.template +25 -0
- package/templates/watchtower/queue/items/item.json.schema +78 -0
- package/templates/watchtower/state/projects/project.md.template +13 -0
- package/templates/watchtower/state/summary.md.template +18 -0
- package/templates/watchtower/watchtower-ring1.service +16 -0
- package/templates/watchtower/watchtower-ring1.timer +15 -0
- package/templates/watchtower/watchtower-ring2-fast.service +16 -0
- package/templates/watchtower/watchtower-ring2-fast.timer +15 -0
- package/templates/watchtower/watchtower-ring2-slow.service +16 -0
- 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,
|
|
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** —
|
|
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
|
-
|
|
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-*/ #
|
|
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 };
|
package/lib/mux-setup.js
ADDED
|
@@ -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 };
|
package/lib/settings-merge.js
CHANGED
|
@@ -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
|
-
|
|
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