tlc-claude-code 2.0.1 → 2.2.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/.claude/agents/builder.md +144 -0
- package/.claude/agents/planner.md +143 -0
- package/.claude/agents/reviewer.md +160 -0
- package/.claude/commands/tlc/build.md +4 -0
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/review-plan.md +363 -0
- package/.claude/commands/tlc/review.md +172 -57
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +13 -0
- package/bin/install.js +268 -2
- package/bin/postinstall.js +102 -24
- package/bin/setup-autoupdate.js +206 -0
- package/bin/setup-autoupdate.test.js +124 -0
- package/bin/tlc.js +0 -0
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +4 -2
- package/scripts/project-docs.js +1 -1
- package/server/index.js +228 -2
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/cost-tracker.test.js +49 -12
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +3 -1
- package/server/lib/memory-api.test.js +3 -5
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +69 -4
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +42 -4
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/orchestration/agent-dispatcher.js +114 -0
- package/server/lib/orchestration/agent-dispatcher.test.js +110 -0
- package/server/lib/orchestration/orchestrator.js +130 -0
- package/server/lib/orchestration/orchestrator.test.js +192 -0
- package/server/lib/orchestration/tmux-manager.js +101 -0
- package/server/lib/orchestration/tmux-manager.test.js +109 -0
- package/server/lib/orchestration/worktree-manager.js +132 -0
- package/server/lib/orchestration/worktree-manager.test.js +129 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +37 -2
- package/server/lib/project-scanner.test.js +152 -0
- package/server/lib/remember-command.js +2 -0
- package/server/lib/remember-command.test.js +23 -0
- package/server/lib/review/plan-reviewer.js +260 -0
- package/server/lib/review/plan-reviewer.test.js +269 -0
- package/server/lib/review/review-schemas.js +173 -0
- package/server/lib/review/review-schemas.test.js +152 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +1 -1
- package/server/lib/semantic-recall.test.js +17 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +182 -1
- package/server/lib/workspace-api.test.js +474 -0
- package/server/package-lock.json +737 -0
- package/server/package.json +3 -0
- package/server/setup.sh +271 -271
- package/dashboard-web/dist/assets/index-Uhc49PE-.css +0 -1
- package/dashboard-web/dist/assets/index-W36XHPC5.js +0 -431
- package/dashboard-web/dist/assets/index-W36XHPC5.js.map +0 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TLC Enforcement Layer 2b: Session initialization
|
|
3
|
+
# 1. Inject TLC awareness
|
|
4
|
+
# 2. Ensure TLC server is running
|
|
5
|
+
# 3. Probe LLM providers and write persistent router state
|
|
6
|
+
|
|
7
|
+
if [ ! -f ".tlc.json" ]; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
echo "TLC project detected. All work goes through /tlc commands. Run /tlc for current status and next action."
|
|
12
|
+
|
|
13
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
14
|
+
|
|
15
|
+
# ─── TLC Server ───────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
TLC_PORT="${TLC_PORT:-3147}"
|
|
18
|
+
if curl -sf --max-time 1 "http://localhost:${TLC_PORT}/api/health" > /dev/null 2>&1; then
|
|
19
|
+
: # Server is running
|
|
20
|
+
else
|
|
21
|
+
PLIST="$HOME/Library/LaunchAgents/com.tlc.server.plist"
|
|
22
|
+
|
|
23
|
+
if [ -f "$PLIST" ]; then
|
|
24
|
+
launchctl kickstart -k "gui/$(id -u)/com.tlc.server" 2>/dev/null
|
|
25
|
+
elif [ -f "$PROJECT_DIR/server/index.js" ]; then
|
|
26
|
+
nohup node "$PROJECT_DIR/server/index.js" > "$HOME/.tlc/logs/server.log" 2>&1 &
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
for i in 1 2 3; do
|
|
30
|
+
sleep 1
|
|
31
|
+
curl -sf --max-time 1 "http://localhost:${TLC_PORT}/api/health" > /dev/null 2>&1 && break
|
|
32
|
+
done
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# ─── LLM Router: Probe Providers ─────────────────────────
|
|
36
|
+
#
|
|
37
|
+
# Writes .tlc/.router-state.json with provider availability.
|
|
38
|
+
# Skills read this file instead of probing from scratch.
|
|
39
|
+
# State has a TTL — re-probed if older than 1 hour.
|
|
40
|
+
|
|
41
|
+
STATE_DIR="$PROJECT_DIR/.tlc"
|
|
42
|
+
STATE_FILE="$STATE_DIR/.router-state.json"
|
|
43
|
+
mkdir -p "$STATE_DIR"
|
|
44
|
+
|
|
45
|
+
# Check if state is fresh (less than 1 hour old)
|
|
46
|
+
STALE=true
|
|
47
|
+
if [ -f "$STATE_FILE" ]; then
|
|
48
|
+
STATE_AGE=$(( $(date +%s) - $(stat -f %m "$STATE_FILE" 2>/dev/null || stat -c %Y "$STATE_FILE" 2>/dev/null || echo 0) ))
|
|
49
|
+
if [ "$STATE_AGE" -lt 3600 ]; then
|
|
50
|
+
STALE=false
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ "$STALE" = true ]; then
|
|
55
|
+
# Probe each provider
|
|
56
|
+
CLAUDE_PATH=$(which claude 2>/dev/null || echo "")
|
|
57
|
+
CODEX_PATH=$(which codex 2>/dev/null || echo "")
|
|
58
|
+
GEMINI_PATH=$(which gemini 2>/dev/null || echo "")
|
|
59
|
+
|
|
60
|
+
CLAUDE_OK="false"
|
|
61
|
+
CODEX_OK="false"
|
|
62
|
+
GEMINI_OK="false"
|
|
63
|
+
|
|
64
|
+
[ -n "$CLAUDE_PATH" ] && CLAUDE_OK="true"
|
|
65
|
+
[ -n "$CODEX_PATH" ] && CODEX_OK="true"
|
|
66
|
+
[ -n "$GEMINI_PATH" ] && GEMINI_OK="true"
|
|
67
|
+
|
|
68
|
+
# Count available
|
|
69
|
+
AVAILABLE=0
|
|
70
|
+
[ "$CLAUDE_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
71
|
+
[ "$CODEX_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
72
|
+
[ "$GEMINI_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
73
|
+
|
|
74
|
+
# Read configured providers from .tlc.json
|
|
75
|
+
CONFIGURED_PROVIDERS=""
|
|
76
|
+
if command -v jq >/dev/null 2>&1; then
|
|
77
|
+
CONFIGURED_PROVIDERS=$(jq -r '.router.providers // {} | keys[]' .tlc.json 2>/dev/null | tr '\n' ',' | sed 's/,$//')
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Write state file
|
|
81
|
+
cat > "$STATE_FILE" <<STATEEOF
|
|
82
|
+
{
|
|
83
|
+
"probed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
84
|
+
"ttl_seconds": 3600,
|
|
85
|
+
"providers": {
|
|
86
|
+
"claude": {
|
|
87
|
+
"available": $CLAUDE_OK,
|
|
88
|
+
"path": "$CLAUDE_PATH"
|
|
89
|
+
},
|
|
90
|
+
"codex": {
|
|
91
|
+
"available": $CODEX_OK,
|
|
92
|
+
"path": "$CODEX_PATH"
|
|
93
|
+
},
|
|
94
|
+
"gemini": {
|
|
95
|
+
"available": $GEMINI_OK,
|
|
96
|
+
"path": "$GEMINI_PATH"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"summary": {
|
|
100
|
+
"available_count": $AVAILABLE,
|
|
101
|
+
"configured": "$CONFIGURED_PROVIDERS"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
STATEEOF
|
|
105
|
+
|
|
106
|
+
# Report to Claude
|
|
107
|
+
if [ "$AVAILABLE" -gt 0 ]; then
|
|
108
|
+
PROVIDERS_LIST=""
|
|
109
|
+
[ "$CLAUDE_OK" = "true" ] && PROVIDERS_LIST="${PROVIDERS_LIST}claude, "
|
|
110
|
+
[ "$CODEX_OK" = "true" ] && PROVIDERS_LIST="${PROVIDERS_LIST}codex, "
|
|
111
|
+
[ "$GEMINI_OK" = "true" ] && PROVIDERS_LIST="${PROVIDERS_LIST}gemini, "
|
|
112
|
+
PROVIDERS_LIST=$(echo "$PROVIDERS_LIST" | sed 's/, $//')
|
|
113
|
+
echo "LLM Router: ${AVAILABLE} providers available (${PROVIDERS_LIST}). State written to .tlc/.router-state.json. All routing skills MUST read this file for provider availability — do not probe manually."
|
|
114
|
+
else
|
|
115
|
+
echo "LLM Router: No external providers detected. Running Claude-only mode. Install codex or gemini for multi-LLM reviews."
|
|
116
|
+
fi
|
|
117
|
+
else
|
|
118
|
+
# State is fresh — just report it
|
|
119
|
+
if command -v jq >/dev/null 2>&1 && [ -f "$STATE_FILE" ]; then
|
|
120
|
+
COUNT=$(jq -r '.summary.available_count' "$STATE_FILE" 2>/dev/null)
|
|
121
|
+
echo "LLM Router: ${COUNT} providers available (cached). State at .tlc/.router-state.json."
|
|
122
|
+
fi
|
|
123
|
+
fi
|
package/CLAUDE.md
CHANGED
|
@@ -19,6 +19,7 @@ When the user says X → invoke `Skill(skill="tlc:...")`:
|
|
|
19
19
|
| "plan", "break this down" | `/tlc:plan` |
|
|
20
20
|
| "build", "implement", "add feature" | `/tlc:build` |
|
|
21
21
|
| "review", "check code" | `/tlc:review` |
|
|
22
|
+
| "review plan", "check plan" | `/tlc:review-plan` |
|
|
22
23
|
| "status", "what's next", "where are we" | `/tlc:progress` |
|
|
23
24
|
| "discuss", "talk about approach" | `/tlc:discuss` |
|
|
24
25
|
| "test", "run tests" | `/tlc:status` |
|
|
@@ -49,6 +50,10 @@ When the user says X → invoke `Skill(skill="tlc:...")`:
|
|
|
49
50
|
| "quick task", "small fix" | `/tlc:quick` |
|
|
50
51
|
| "dashboard" | `/tlc:dashboard` |
|
|
51
52
|
| "review PR" | `/tlc:review-pr` |
|
|
53
|
+
| "watch ci", "fix ci", "ci failing" | `/tlc:watchci` |
|
|
54
|
+
| "e2e", "screenshot", "visual check" | `/tlc:e2e-verify` |
|
|
55
|
+
| "guard", "check process", "validate" | `/tlc:guard` |
|
|
56
|
+
| "preflight", "am I done", "check gaps" | `/tlc:preflight` |
|
|
52
57
|
|
|
53
58
|
## TLC File System
|
|
54
59
|
|
|
@@ -73,6 +78,14 @@ Use `Task` tool to spawn sub-agents for independent work. Keep main conversation
|
|
|
73
78
|
|
|
74
79
|
Claim tasks before starting: `/tlc:claim`. Release if blocked: `/tlc:release`. Check team: `/tlc:who`. Pull before claiming, push after.
|
|
75
80
|
|
|
81
|
+
## Memory Auto-Capture
|
|
82
|
+
|
|
83
|
+
Conversations are automatically captured via the Claude Code `Stop` hook. After each response, the hook POSTs the exchange to the TLC server's capture endpoint. The pattern detector classifies decisions, gotchas, and preferences into team memory files under `.tlc/memory/team/`.
|
|
84
|
+
|
|
85
|
+
- **Resilience:** If the server is unreachable, exchanges spool to `.tlc/memory/.spool.jsonl` and drain on the next successful capture.
|
|
86
|
+
- **Endpoint hardening:** Payloads are capped at 100KB, deduplicated within a 60s window, and rate-limited to 100 captures/minute per project.
|
|
87
|
+
- **Disable:** Remove the `Stop` hook entry from `.claude/settings.json`.
|
|
88
|
+
|
|
76
89
|
---
|
|
77
90
|
|
|
78
91
|
<!-- TLC-STANDARDS -->
|
package/bin/install.js
CHANGED
|
@@ -33,6 +33,7 @@ const COMMANDS = [
|
|
|
33
33
|
'sync.md',
|
|
34
34
|
'new-project.md',
|
|
35
35
|
'init.md',
|
|
36
|
+
'bootstrap.md',
|
|
36
37
|
'import-project.md',
|
|
37
38
|
'discuss.md',
|
|
38
39
|
'plan.md',
|
|
@@ -71,6 +72,7 @@ const COMMANDS = [
|
|
|
71
72
|
// Review
|
|
72
73
|
'review.md',
|
|
73
74
|
'review-pr.md',
|
|
75
|
+
'review-plan.md',
|
|
74
76
|
// Documentation
|
|
75
77
|
'docs.md',
|
|
76
78
|
// Multi-Tool & Deployment
|
|
@@ -78,10 +80,37 @@ const COMMANDS = [
|
|
|
78
80
|
'deploy.md',
|
|
79
81
|
// Multi-Model
|
|
80
82
|
'llm.md',
|
|
83
|
+
// Plugins (auto-run via hooks)
|
|
84
|
+
'watchci.md',
|
|
85
|
+
'e2e-verify.md',
|
|
86
|
+
'guard.md',
|
|
87
|
+
'preflight.md',
|
|
88
|
+
// Memory
|
|
89
|
+
'remember.md',
|
|
90
|
+
'recall.md',
|
|
91
|
+
// Dashboard
|
|
92
|
+
'dashboard.md',
|
|
81
93
|
// Help
|
|
82
94
|
'help.md'
|
|
83
95
|
];
|
|
84
96
|
|
|
97
|
+
// Hook scripts that power the plugin system
|
|
98
|
+
const HOOKS = [
|
|
99
|
+
'tlc-block-tools.sh',
|
|
100
|
+
'tlc-prompt-guard.sh',
|
|
101
|
+
'tlc-session-init.sh',
|
|
102
|
+
'tlc-post-push.sh',
|
|
103
|
+
'tlc-post-build.sh',
|
|
104
|
+
'tlc-capture-exchange.sh'
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// Claude Code agent definitions (managed by TLC — identified by '# TLC Agent:' header)
|
|
108
|
+
const AGENTS = [
|
|
109
|
+
'reviewer.md',
|
|
110
|
+
'builder.md',
|
|
111
|
+
'planner.md'
|
|
112
|
+
];
|
|
113
|
+
|
|
85
114
|
function getGlobalDir() {
|
|
86
115
|
const claudeConfig = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude');
|
|
87
116
|
return path.join(claudeConfig, 'commands');
|
|
@@ -115,15 +144,20 @@ function info(msg) {
|
|
|
115
144
|
|
|
116
145
|
function install(targetDir, installType) {
|
|
117
146
|
const commandsDir = path.join(targetDir, 'tlc');
|
|
147
|
+
const packageRoot = path.join(__dirname, '..');
|
|
118
148
|
|
|
119
149
|
// Create directory
|
|
120
150
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
121
151
|
|
|
122
152
|
// Copy command files with version injection
|
|
123
|
-
|
|
153
|
+
// Try .claude/commands/tlc/ first (npm package structure), fall back to root (dev)
|
|
154
|
+
const commandsSrcDir = fs.existsSync(path.join(packageRoot, '.claude', 'commands', 'tlc'))
|
|
155
|
+
? path.join(packageRoot, '.claude', 'commands', 'tlc')
|
|
156
|
+
: packageRoot;
|
|
157
|
+
|
|
124
158
|
let installed = 0;
|
|
125
159
|
for (const file of COMMANDS) {
|
|
126
|
-
const src = path.join(
|
|
160
|
+
const src = path.join(commandsSrcDir, file);
|
|
127
161
|
const dest = path.join(commandsDir, file);
|
|
128
162
|
if (fs.existsSync(src)) {
|
|
129
163
|
// Read, replace {{VERSION}}, write
|
|
@@ -135,6 +169,35 @@ function install(targetDir, installType) {
|
|
|
135
169
|
}
|
|
136
170
|
|
|
137
171
|
success(`Installed ${installed} commands to ${c.cyan}${commandsDir}${c.reset}`);
|
|
172
|
+
|
|
173
|
+
// Install hooks (plugin system)
|
|
174
|
+
const hooksInstalled = installHooks(targetDir, packageRoot);
|
|
175
|
+
if (hooksInstalled > 0) {
|
|
176
|
+
success(`Installed ${hooksInstalled} hooks to ${c.cyan}${path.dirname(targetDir)}/.claude/hooks/${c.reset}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Install agent definitions (Claude Code sub-agents)
|
|
180
|
+
const agentsInstalled = installAgents(targetDir, packageRoot);
|
|
181
|
+
if (agentsInstalled > 0) {
|
|
182
|
+
success(`Installed ${agentsInstalled} agents to ${c.cyan}${path.dirname(targetDir)}/.claude/agents/${c.reset}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Install settings template (with hooks wiring)
|
|
186
|
+
const settingsInstalled = installSettings(targetDir, installType);
|
|
187
|
+
if (settingsInstalled) {
|
|
188
|
+
success(`Installed settings template with hook wiring`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Fix ownership if running under sudo
|
|
192
|
+
if (isRunningAsSudo()) {
|
|
193
|
+
const claudeDir = path.dirname(targetDir);
|
|
194
|
+
fixOwnership(commandsDir);
|
|
195
|
+
fixOwnership(path.join(claudeDir, 'hooks'));
|
|
196
|
+
fixOwnership(path.join(claudeDir, 'agents'));
|
|
197
|
+
fixOwnership(path.join(claudeDir, 'settings.json'));
|
|
198
|
+
success(`Fixed file ownership for ${c.cyan}${process.env.SUDO_USER}${c.reset}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
138
201
|
log('');
|
|
139
202
|
log(`${c.green}Done!${c.reset} Restart Claude Code to load commands.`);
|
|
140
203
|
log('');
|
|
@@ -150,6 +213,203 @@ function install(targetDir, installType) {
|
|
|
150
213
|
log('');
|
|
151
214
|
}
|
|
152
215
|
|
|
216
|
+
function installHooks(targetDir, packageRoot) {
|
|
217
|
+
// For local install: .claude/commands -> go up to .claude/hooks
|
|
218
|
+
// For global install: ~/.claude/commands -> go up to ~/.claude/hooks
|
|
219
|
+
const claudeDir = path.dirname(targetDir);
|
|
220
|
+
const hooksDestDir = path.join(claudeDir, 'hooks');
|
|
221
|
+
fs.mkdirSync(hooksDestDir, { recursive: true });
|
|
222
|
+
|
|
223
|
+
// Try .claude/hooks/ first (npm package), fall back to root .claude/hooks/ (dev)
|
|
224
|
+
const hooksSrcDir = fs.existsSync(path.join(packageRoot, '.claude', 'hooks'))
|
|
225
|
+
? path.join(packageRoot, '.claude', 'hooks')
|
|
226
|
+
: null;
|
|
227
|
+
|
|
228
|
+
if (!hooksSrcDir) return 0;
|
|
229
|
+
|
|
230
|
+
let copied = 0;
|
|
231
|
+
for (const file of HOOKS) {
|
|
232
|
+
const src = path.join(hooksSrcDir, file);
|
|
233
|
+
const dest = path.join(hooksDestDir, file);
|
|
234
|
+
if (fs.existsSync(src)) {
|
|
235
|
+
fs.copyFileSync(src, dest);
|
|
236
|
+
// Make executable
|
|
237
|
+
fs.chmodSync(dest, 0o755);
|
|
238
|
+
copied++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return copied;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function installAgents(targetDir, packageRoot) {
|
|
245
|
+
// targetDir is .claude/commands (or ~/.claude/commands)
|
|
246
|
+
// agents go into the sibling .claude/agents/ directory
|
|
247
|
+
const claudeDir = path.dirname(targetDir);
|
|
248
|
+
const agentsDestDir = path.join(claudeDir, 'agents');
|
|
249
|
+
fs.mkdirSync(agentsDestDir, { recursive: true });
|
|
250
|
+
|
|
251
|
+
// Try .claude/agents/ first (npm package structure), fall back to nothing
|
|
252
|
+
const agentsSrcDir = fs.existsSync(path.join(packageRoot, '.claude', 'agents'))
|
|
253
|
+
? path.join(packageRoot, '.claude', 'agents')
|
|
254
|
+
: null;
|
|
255
|
+
|
|
256
|
+
if (!agentsSrcDir) return 0;
|
|
257
|
+
|
|
258
|
+
const TLC_AGENT_MARKER = '# TLC Agent:';
|
|
259
|
+
|
|
260
|
+
let copied = 0;
|
|
261
|
+
for (const file of AGENTS) {
|
|
262
|
+
const src = path.join(agentsSrcDir, file);
|
|
263
|
+
const dest = path.join(agentsDestDir, file);
|
|
264
|
+
if (!fs.existsSync(src)) continue;
|
|
265
|
+
|
|
266
|
+
// Only overwrite if the destination either doesn't exist or is TLC-managed
|
|
267
|
+
if (fs.existsSync(dest)) {
|
|
268
|
+
const existing = fs.readFileSync(dest, 'utf8');
|
|
269
|
+
if (!existing.startsWith(TLC_AGENT_MARKER)) {
|
|
270
|
+
// User has customized this agent — leave it alone
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fs.copyFileSync(src, dest);
|
|
276
|
+
copied++;
|
|
277
|
+
}
|
|
278
|
+
return copied;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function installSettings(targetDir, installType) {
|
|
282
|
+
// For local install: .claude/commands -> go up to .claude/settings.json
|
|
283
|
+
// For global install: ~/.claude/commands -> go up to ~/.claude/settings.json
|
|
284
|
+
const claudeDir = path.dirname(targetDir);
|
|
285
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
286
|
+
|
|
287
|
+
// Global installs put hooks in ~/.claude/hooks/ (or $CLAUDE_CONFIG_DIR/hooks/ if set)
|
|
288
|
+
// Local installs put hooks in .claude/hooks/ (use $CLAUDE_PROJECT_DIR)
|
|
289
|
+
const hooksBase = installType === 'global'
|
|
290
|
+
? '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks'
|
|
291
|
+
: '$CLAUDE_PROJECT_DIR/.claude/hooks';
|
|
292
|
+
|
|
293
|
+
// The settings template with full hook wiring
|
|
294
|
+
const settingsTemplate = {
|
|
295
|
+
permissions: {
|
|
296
|
+
allow: [
|
|
297
|
+
"Bash(npm *)", "Bash(npx *)", "Bash(node *)", "Bash(git *)",
|
|
298
|
+
"Bash(gh *)", "Bash(ssh *)", "Bash(scp *)", "Bash(rsync *)",
|
|
299
|
+
"Bash(curl *)", "Bash(wget *)", "Bash(docker *)", "Bash(docker-compose *)",
|
|
300
|
+
"Bash(pytest*)", "Bash(python *)", "Bash(pip *)", "Bash(go *)",
|
|
301
|
+
"Bash(cargo *)", "Bash(make *)", "Bash(cat *)", "Bash(ls *)",
|
|
302
|
+
"Bash(pwd*)", "Bash(cd *)", "Bash(mkdir *)", "Bash(cp *)",
|
|
303
|
+
"Bash(mv *)", "Bash(which *)", "Bash(echo *)", "Bash(jq *)",
|
|
304
|
+
"Bash(wc *)", "Bash(head *)", "Bash(tail *)", "Bash(sort *)",
|
|
305
|
+
"Bash(uniq *)", "Bash(xargs *)"
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
hooks: {
|
|
309
|
+
PreToolUse: [{
|
|
310
|
+
matcher: "EnterPlanMode|TaskCreate|TaskUpdate|TaskList|TaskGet|ExitPlanMode",
|
|
311
|
+
hooks: [{
|
|
312
|
+
type: "command",
|
|
313
|
+
command: `bash ${hooksBase}/tlc-block-tools.sh`,
|
|
314
|
+
timeout: 5
|
|
315
|
+
}]
|
|
316
|
+
}],
|
|
317
|
+
UserPromptSubmit: [{
|
|
318
|
+
hooks: [{
|
|
319
|
+
type: "command",
|
|
320
|
+
command: `bash ${hooksBase}/tlc-prompt-guard.sh`,
|
|
321
|
+
timeout: 5
|
|
322
|
+
}]
|
|
323
|
+
}],
|
|
324
|
+
SessionStart: [{
|
|
325
|
+
hooks: [{
|
|
326
|
+
type: "command",
|
|
327
|
+
command: `bash ${hooksBase}/tlc-session-init.sh`,
|
|
328
|
+
timeout: 5
|
|
329
|
+
}]
|
|
330
|
+
}],
|
|
331
|
+
PostToolUse: [
|
|
332
|
+
{
|
|
333
|
+
matcher: "Bash",
|
|
334
|
+
hooks: [{
|
|
335
|
+
type: "command",
|
|
336
|
+
command: `bash ${hooksBase}/tlc-post-push.sh`,
|
|
337
|
+
timeout: 5
|
|
338
|
+
}]
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
matcher: "Skill",
|
|
342
|
+
hooks: [{
|
|
343
|
+
type: "command",
|
|
344
|
+
command: `bash ${hooksBase}/tlc-post-build.sh`,
|
|
345
|
+
timeout: 5
|
|
346
|
+
}]
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
Stop: [{
|
|
350
|
+
hooks: [{
|
|
351
|
+
type: "command",
|
|
352
|
+
command: `bash "${hooksBase}/tlc-capture-exchange.sh"`,
|
|
353
|
+
timeout: 30
|
|
354
|
+
}]
|
|
355
|
+
}]
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
if (fs.existsSync(settingsPath)) {
|
|
360
|
+
// Merge: preserve existing permissions, add missing hooks
|
|
361
|
+
try {
|
|
362
|
+
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
363
|
+
// Merge permissions (union)
|
|
364
|
+
if (existing.permissions && existing.permissions.allow) {
|
|
365
|
+
const existingSet = new Set(existing.permissions.allow);
|
|
366
|
+
for (const perm of settingsTemplate.permissions.allow) {
|
|
367
|
+
existingSet.add(perm);
|
|
368
|
+
}
|
|
369
|
+
existing.permissions.allow = [...existingSet];
|
|
370
|
+
} else {
|
|
371
|
+
existing.permissions = settingsTemplate.permissions;
|
|
372
|
+
}
|
|
373
|
+
// Add hooks if not present
|
|
374
|
+
if (!existing.hooks) {
|
|
375
|
+
existing.hooks = settingsTemplate.hooks;
|
|
376
|
+
}
|
|
377
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
378
|
+
return true;
|
|
379
|
+
} catch (err) {
|
|
380
|
+
// If we can't parse existing, don't overwrite
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settingsTemplate, null, 2) + '\n');
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isRunningAsSudo() {
|
|
390
|
+
return process.getuid && process.getuid() === 0 && process.env.SUDO_USER;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function fixOwnership(targetPath) {
|
|
394
|
+
const sudoUid = parseInt(process.env.SUDO_UID, 10);
|
|
395
|
+
const sudoGid = parseInt(process.env.SUDO_GID, 10);
|
|
396
|
+
if (isNaN(sudoUid) || isNaN(sudoGid)) return;
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const stat = fs.statSync(targetPath);
|
|
400
|
+
if (stat.isDirectory()) {
|
|
401
|
+
fs.chownSync(targetPath, sudoUid, sudoGid);
|
|
402
|
+
for (const entry of fs.readdirSync(targetPath)) {
|
|
403
|
+
fixOwnership(path.join(targetPath, entry));
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
fs.chownSync(targetPath, sudoUid, sudoGid);
|
|
407
|
+
}
|
|
408
|
+
} catch (err) {
|
|
409
|
+
// Best effort
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
153
413
|
async function main() {
|
|
154
414
|
const args = process.argv.slice(2);
|
|
155
415
|
|
|
@@ -161,6 +421,12 @@ async function main() {
|
|
|
161
421
|
|
|
162
422
|
printBanner();
|
|
163
423
|
|
|
424
|
+
// Warn if running under sudo — files will be owned by root
|
|
425
|
+
if (isRunningAsSudo()) {
|
|
426
|
+
log(`${c.yellow}⚠ Running under sudo — will fix file ownership for ${process.env.SUDO_USER}${c.reset}`);
|
|
427
|
+
log('');
|
|
428
|
+
}
|
|
429
|
+
|
|
164
430
|
if (args.includes('--global') || args.includes('-g')) {
|
|
165
431
|
info(`Installing ${c.bold}globally${c.reset} to ~/.claude/commands/tlc`);
|
|
166
432
|
log('');
|
package/bin/postinstall.js
CHANGED
|
@@ -4,44 +4,122 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
7
|
+
const packageRoot = path.join(__dirname, '..');
|
|
8
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
9
|
+
|
|
10
|
+
// Source directories (inside npm package)
|
|
11
|
+
const commandsSrcDir = path.join(packageRoot, '.claude', 'commands', 'tlc');
|
|
12
|
+
const hooksSrcDir = path.join(packageRoot, '.claude', 'hooks');
|
|
13
|
+
const agentsSrcDir = path.join(packageRoot, '.claude', 'agents');
|
|
14
|
+
|
|
15
|
+
// Destination directories (user's home)
|
|
16
|
+
const commandsDestDir = path.join(claudeHome, 'commands', 'tlc');
|
|
17
|
+
const hooksDestDir = path.join(claudeHome, 'hooks');
|
|
18
|
+
const agentsDestDir = path.join(claudeHome, 'agents');
|
|
10
19
|
|
|
11
|
-
// Create destination directory if it doesn't exist
|
|
12
20
|
function ensureDir(dir) {
|
|
13
21
|
if (!fs.existsSync(dir)) {
|
|
14
22
|
fs.mkdirSync(dir, { recursive: true });
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
// Copy all .md files
|
|
26
|
+
// Copy all .md command files
|
|
19
27
|
function copyCommands() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
if (!fs.existsSync(commandsSrcDir)) return 0;
|
|
29
|
+
ensureDir(commandsDestDir);
|
|
30
|
+
|
|
31
|
+
const files = fs.readdirSync(commandsSrcDir).filter(f => f.endsWith('.md'));
|
|
32
|
+
let copied = 0;
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
fs.copyFileSync(path.join(commandsSrcDir, file), path.join(commandsDestDir, file));
|
|
35
|
+
copied++;
|
|
36
|
+
}
|
|
37
|
+
return copied;
|
|
38
|
+
}
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
// Copy all .sh hook files
|
|
41
|
+
function copyHooks() {
|
|
42
|
+
if (!fs.existsSync(hooksSrcDir)) return 0;
|
|
43
|
+
ensureDir(hooksDestDir);
|
|
44
|
+
|
|
45
|
+
const files = fs.readdirSync(hooksSrcDir).filter(f => f.endsWith('.sh'));
|
|
46
|
+
let copied = 0;
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const dest = path.join(hooksDestDir, file);
|
|
49
|
+
fs.copyFileSync(path.join(hooksSrcDir, file), dest);
|
|
50
|
+
fs.chmodSync(dest, 0o755);
|
|
51
|
+
copied++;
|
|
52
|
+
}
|
|
53
|
+
return copied;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Copy all .md agent files (only overwrite TLC-managed ones)
|
|
57
|
+
function copyAgents() {
|
|
58
|
+
if (!fs.existsSync(agentsSrcDir)) return 0;
|
|
59
|
+
ensureDir(agentsDestDir);
|
|
60
|
+
|
|
61
|
+
const files = fs.readdirSync(agentsSrcDir).filter(f => f.endsWith('.md'));
|
|
62
|
+
let copied = 0;
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const dest = path.join(agentsDestDir, file);
|
|
65
|
+
// Don't overwrite user-customized agents
|
|
66
|
+
if (fs.existsSync(dest)) {
|
|
67
|
+
const content = fs.readFileSync(dest, 'utf8');
|
|
68
|
+
if (!content.startsWith('# TLC Agent:')) continue;
|
|
28
69
|
}
|
|
70
|
+
fs.copyFileSync(path.join(agentsSrcDir, file), dest);
|
|
71
|
+
copied++;
|
|
72
|
+
}
|
|
73
|
+
return copied;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isRunningAsSudo() {
|
|
77
|
+
return process.getuid && process.getuid() === 0 && process.env.SUDO_USER;
|
|
78
|
+
}
|
|
29
79
|
|
|
30
|
-
|
|
31
|
-
|
|
80
|
+
function fixOwnership(targetPath) {
|
|
81
|
+
// After writing files as root, fix ownership to the actual user
|
|
82
|
+
const sudoUid = parseInt(process.env.SUDO_UID, 10);
|
|
83
|
+
const sudoGid = parseInt(process.env.SUDO_GID, 10);
|
|
84
|
+
if (isNaN(sudoUid) || isNaN(sudoGid)) return;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const stat = fs.statSync(targetPath);
|
|
88
|
+
if (stat.isDirectory()) {
|
|
89
|
+
fs.chownSync(targetPath, sudoUid, sudoGid);
|
|
90
|
+
for (const entry of fs.readdirSync(targetPath)) {
|
|
91
|
+
fixOwnership(path.join(targetPath, entry));
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
fs.chownSync(targetPath, sudoUid, sudoGid);
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Best effort
|
|
98
|
+
}
|
|
99
|
+
}
|
|
32
100
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
101
|
+
function postinstall() {
|
|
102
|
+
try {
|
|
103
|
+
const commands = copyCommands();
|
|
104
|
+
const hooks = copyHooks();
|
|
105
|
+
const agents = copyAgents();
|
|
37
106
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
107
|
+
if (commands > 0) {
|
|
108
|
+
console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${commands} commands to ~/.claude/commands/tlc/`);
|
|
109
|
+
}
|
|
110
|
+
if (hooks > 0) {
|
|
111
|
+
console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${hooks} hooks to ~/.claude/hooks/`);
|
|
112
|
+
}
|
|
113
|
+
if (agents > 0) {
|
|
114
|
+
console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${agents} agents to ~/.claude/agents/`);
|
|
41
115
|
}
|
|
42
116
|
|
|
43
|
-
if
|
|
44
|
-
|
|
117
|
+
// Fix ownership if running under sudo
|
|
118
|
+
if (isRunningAsSudo()) {
|
|
119
|
+
console.log(`\x1b[33m⚠\x1b[0m TLC: Detected sudo — fixing file ownership for ${process.env.SUDO_USER}`);
|
|
120
|
+
fixOwnership(commandsDestDir);
|
|
121
|
+
fixOwnership(hooksDestDir);
|
|
122
|
+
fixOwnership(agentsDestDir);
|
|
45
123
|
}
|
|
46
124
|
} catch (err) {
|
|
47
125
|
// Silent fail - don't break npm install
|
|
@@ -51,4 +129,4 @@ function copyCommands() {
|
|
|
51
129
|
}
|
|
52
130
|
}
|
|
53
131
|
|
|
54
|
-
|
|
132
|
+
postinstall();
|