tlc-claude-code 2.0.1 → 2.1.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 (86) hide show
  1. package/.claude/commands/tlc/deploy.md +194 -2
  2. package/.claude/commands/tlc/e2e-verify.md +214 -0
  3. package/.claude/commands/tlc/guard.md +191 -0
  4. package/.claude/commands/tlc/help.md +32 -0
  5. package/.claude/commands/tlc/init.md +73 -37
  6. package/.claude/commands/tlc/llm.md +19 -4
  7. package/.claude/commands/tlc/preflight.md +134 -0
  8. package/.claude/commands/tlc/review.md +17 -4
  9. package/.claude/commands/tlc/watchci.md +159 -0
  10. package/.claude/hooks/tlc-block-tools.sh +41 -0
  11. package/.claude/hooks/tlc-capture-exchange.sh +50 -0
  12. package/.claude/hooks/tlc-post-build.sh +38 -0
  13. package/.claude/hooks/tlc-post-push.sh +22 -0
  14. package/.claude/hooks/tlc-prompt-guard.sh +69 -0
  15. package/.claude/hooks/tlc-session-init.sh +123 -0
  16. package/CLAUDE.md +12 -0
  17. package/bin/install.js +171 -2
  18. package/bin/postinstall.js +45 -26
  19. package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
  20. package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
  21. package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
  22. package/dashboard-web/dist/index.html +2 -2
  23. package/docker-compose.dev.yml +18 -12
  24. package/package.json +3 -1
  25. package/server/index.js +228 -2
  26. package/server/lib/capture-bridge.js +242 -0
  27. package/server/lib/capture-bridge.test.js +363 -0
  28. package/server/lib/capture-guard.js +140 -0
  29. package/server/lib/capture-guard.test.js +182 -0
  30. package/server/lib/command-runner.js +159 -0
  31. package/server/lib/command-runner.test.js +92 -0
  32. package/server/lib/deploy/runners/dependency-runner.js +106 -0
  33. package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
  34. package/server/lib/deploy/runners/secrets-runner.js +174 -0
  35. package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
  36. package/server/lib/deploy/security-gates.js +11 -24
  37. package/server/lib/deploy/security-gates.test.js +9 -2
  38. package/server/lib/deploy-engine.js +182 -0
  39. package/server/lib/deploy-engine.test.js +147 -0
  40. package/server/lib/docker-api.js +137 -0
  41. package/server/lib/docker-api.test.js +202 -0
  42. package/server/lib/docker-client.js +297 -0
  43. package/server/lib/docker-client.test.js +308 -0
  44. package/server/lib/input-sanitizer.js +86 -0
  45. package/server/lib/input-sanitizer.test.js +117 -0
  46. package/server/lib/launchd-agent.js +225 -0
  47. package/server/lib/launchd-agent.test.js +185 -0
  48. package/server/lib/memory-api.js +3 -1
  49. package/server/lib/memory-api.test.js +3 -5
  50. package/server/lib/memory-bridge-e2e.test.js +160 -0
  51. package/server/lib/memory-committer.js +18 -4
  52. package/server/lib/memory-committer.test.js +21 -0
  53. package/server/lib/memory-hooks-capture.test.js +69 -4
  54. package/server/lib/memory-hooks-integration.test.js +98 -0
  55. package/server/lib/memory-hooks.js +42 -4
  56. package/server/lib/memory-store-adapter.js +105 -0
  57. package/server/lib/memory-store-adapter.test.js +141 -0
  58. package/server/lib/memory-wiring-e2e.test.js +93 -0
  59. package/server/lib/nginx-config.js +114 -0
  60. package/server/lib/nginx-config.test.js +82 -0
  61. package/server/lib/ollama-health.js +91 -0
  62. package/server/lib/ollama-health.test.js +74 -0
  63. package/server/lib/port-guard.js +44 -0
  64. package/server/lib/port-guard.test.js +65 -0
  65. package/server/lib/project-scanner.js +37 -2
  66. package/server/lib/project-scanner.test.js +152 -0
  67. package/server/lib/remember-command.js +2 -0
  68. package/server/lib/remember-command.test.js +23 -0
  69. package/server/lib/security/crypto-utils.test.js +2 -2
  70. package/server/lib/semantic-recall.js +1 -1
  71. package/server/lib/semantic-recall.test.js +17 -0
  72. package/server/lib/ssh-client.js +184 -0
  73. package/server/lib/ssh-client.test.js +127 -0
  74. package/server/lib/vps-api.js +184 -0
  75. package/server/lib/vps-api.test.js +208 -0
  76. package/server/lib/vps-bootstrap.js +124 -0
  77. package/server/lib/vps-bootstrap.test.js +79 -0
  78. package/server/lib/vps-monitor.js +126 -0
  79. package/server/lib/vps-monitor.test.js +98 -0
  80. package/server/lib/workspace-api.js +182 -1
  81. package/server/lib/workspace-api.test.js +474 -0
  82. package/server/package-lock.json +737 -0
  83. package/server/package.json +3 -0
  84. package/dashboard-web/dist/assets/index-Uhc49PE-.css +0 -1
  85. package/dashboard-web/dist/assets/index-W36XHPC5.js +0 -431
  86. package/dashboard-web/dist/assets/index-W36XHPC5.js.map +0 -1
@@ -0,0 +1,69 @@
1
+ #!/bin/bash
2
+ # TLC Enforcement Layer 2: Smart intent detection + routing
3
+ # Fires on every UserPromptSubmit. Parses user message for intent and
4
+ # injects specific TLC command routing — not a generic reminder.
5
+ #
6
+ # Output appears as <user-prompt-submit-hook> and Claude treats it as user instruction.
7
+ # Survives context compaction because it is re-injected every turn.
8
+
9
+ # Only active in TLC projects
10
+ [ ! -f ".tlc.json" ] && exit 0
11
+
12
+ # Read user's message from stdin
13
+ INPUT=$(cat)
14
+ MSG=$(echo "$INPUT" | jq -r '.user_prompt // empty' 2>/dev/null)
15
+
16
+ # If we can't parse the message, fall back to generic reminder
17
+ if [ -z "$MSG" ]; then
18
+ echo "[TLC PROJECT] All work uses /tlc commands. Never write code without tests. Run /tlc if unsure."
19
+ exit 0
20
+ fi
21
+
22
+ # Lowercase for matching
23
+ MSG_LOWER=$(echo "$MSG" | tr '[:upper:]' '[:lower:]')
24
+
25
+ # Detect intent and inject specific routing
26
+ # Priority order: most specific first
27
+
28
+ # Plan intent
29
+ if echo "$MSG_LOWER" | grep -qE '\b(plan|break.*(down|into)|design|architect|roadmap)\b'; then
30
+ if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
31
+ echo "[TLC-ENFORCE] Planning detected. You MUST use /tlc:plan for this. Plans go in .planning/phases/ files, not in chat. Invoke Skill(skill=\"tlc:plan\") now."
32
+ exit 0
33
+ fi
34
+ fi
35
+
36
+ # Build/implement intent
37
+ if echo "$MSG_LOWER" | grep -qE '\b(build|implement|create|add|code|write|develop|make)\b.*(feature|function|module|component|endpoint|api|page|service|handler|route|model)'; then
38
+ if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
39
+ echo "[TLC-ENFORCE] Implementation detected. You MUST use /tlc:build for this. Tests before code — Red, Green, Refactor. Run /tlc:progress first, then invoke Skill(skill=\"tlc:build\"). Do NOT write code directly."
40
+ exit 0
41
+ fi
42
+ fi
43
+
44
+ # Fix/bug intent
45
+ if echo "$MSG_LOWER" | grep -qE '\b(fix|bug|broken|not working|failing|crash|error)\b'; then
46
+ if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
47
+ echo "[TLC-ENFORCE] Bug/fix detected. Use /tlc:quick for small fixes (still test-first) or /tlc:autofix if tests are failing. Do NOT write code directly without tests."
48
+ exit 0
49
+ fi
50
+ fi
51
+
52
+ # Refactor intent
53
+ if echo "$MSG_LOWER" | grep -qE '\b(refactor|clean.?up|restructure|reorganize|simplify)\b'; then
54
+ if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
55
+ echo "[TLC-ENFORCE] Refactoring detected. Use /tlc:refactor for step-by-step standards refactoring with tests. Invoke Skill(skill=\"tlc:refactor\")."
56
+ exit 0
57
+ fi
58
+ fi
59
+
60
+ # Deploy intent
61
+ if echo "$MSG_LOWER" | grep -qE '\b(deploy|ship|release|publish|push to prod|staging)\b'; then
62
+ if ! echo "$MSG_LOWER" | grep -qE '/tlc'; then
63
+ echo "[TLC-ENFORCE] Deployment detected. Use /tlc:deploy for deployment. Secrets must come from HashiCorp Vault or environment — never hardcode. Invoke Skill(skill=\"tlc:deploy\")."
64
+ exit 0
65
+ fi
66
+ fi
67
+
68
+ # Generic fallback for TLC projects
69
+ echo "[TLC PROJECT] All work uses /tlc commands. Never write code without tests. Run /tlc if unsure."
@@ -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
@@ -49,6 +49,10 @@ When the user says X → invoke `Skill(skill="tlc:...")`:
49
49
  | "quick task", "small fix" | `/tlc:quick` |
50
50
  | "dashboard" | `/tlc:dashboard` |
51
51
  | "review PR" | `/tlc:review-pr` |
52
+ | "watch ci", "fix ci", "ci failing" | `/tlc:watchci` |
53
+ | "e2e", "screenshot", "visual check" | `/tlc:e2e-verify` |
54
+ | "guard", "check process", "validate" | `/tlc:guard` |
55
+ | "preflight", "am I done", "check gaps" | `/tlc:preflight` |
52
56
 
53
57
  ## TLC File System
54
58
 
@@ -73,6 +77,14 @@ Use `Task` tool to spawn sub-agents for independent work. Keep main conversation
73
77
 
74
78
  Claim tasks before starting: `/tlc:claim`. Release if blocked: `/tlc:release`. Check team: `/tlc:who`. Pull before claiming, push after.
75
79
 
80
+ ## Memory Auto-Capture
81
+
82
+ 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/`.
83
+
84
+ - **Resilience:** If the server is unreachable, exchanges spool to `.tlc/memory/.spool.jsonl` and drain on the next successful capture.
85
+ - **Endpoint hardening:** Payloads are capped at 100KB, deduplicated within a 60s window, and rate-limited to 100 captures/minute per project.
86
+ - **Disable:** Remove the `Stop` hook entry from `.claude/settings.json`.
87
+
76
88
  ---
77
89
 
78
90
  <!-- 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',
@@ -78,10 +79,30 @@ const COMMANDS = [
78
79
  'deploy.md',
79
80
  // Multi-Model
80
81
  'llm.md',
82
+ // Plugins (auto-run via hooks)
83
+ 'watchci.md',
84
+ 'e2e-verify.md',
85
+ 'guard.md',
86
+ 'preflight.md',
87
+ // Memory
88
+ 'remember.md',
89
+ 'recall.md',
90
+ // Dashboard
91
+ 'dashboard.md',
81
92
  // Help
82
93
  'help.md'
83
94
  ];
84
95
 
96
+ // Hook scripts that power the plugin system
97
+ const HOOKS = [
98
+ 'tlc-block-tools.sh',
99
+ 'tlc-prompt-guard.sh',
100
+ 'tlc-session-init.sh',
101
+ 'tlc-post-push.sh',
102
+ 'tlc-post-build.sh',
103
+ 'tlc-capture-exchange.sh'
104
+ ];
105
+
85
106
  function getGlobalDir() {
86
107
  const claudeConfig = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude');
87
108
  return path.join(claudeConfig, 'commands');
@@ -115,15 +136,20 @@ function info(msg) {
115
136
 
116
137
  function install(targetDir, installType) {
117
138
  const commandsDir = path.join(targetDir, 'tlc');
139
+ const packageRoot = path.join(__dirname, '..');
118
140
 
119
141
  // Create directory
120
142
  fs.mkdirSync(commandsDir, { recursive: true });
121
143
 
122
144
  // Copy command files with version injection
123
- const sourceDir = path.join(__dirname, '..');
145
+ // Try .claude/commands/tlc/ first (npm package structure), fall back to root (dev)
146
+ const commandsSrcDir = fs.existsSync(path.join(packageRoot, '.claude', 'commands', 'tlc'))
147
+ ? path.join(packageRoot, '.claude', 'commands', 'tlc')
148
+ : packageRoot;
149
+
124
150
  let installed = 0;
125
151
  for (const file of COMMANDS) {
126
- const src = path.join(sourceDir, file);
152
+ const src = path.join(commandsSrcDir, file);
127
153
  const dest = path.join(commandsDir, file);
128
154
  if (fs.existsSync(src)) {
129
155
  // Read, replace {{VERSION}}, write
@@ -135,6 +161,19 @@ function install(targetDir, installType) {
135
161
  }
136
162
 
137
163
  success(`Installed ${installed} commands to ${c.cyan}${commandsDir}${c.reset}`);
164
+
165
+ // Install hooks (plugin system)
166
+ const hooksInstalled = installHooks(targetDir, packageRoot);
167
+ if (hooksInstalled > 0) {
168
+ success(`Installed ${hooksInstalled} hooks to ${c.cyan}${path.dirname(targetDir)}/.claude/hooks/${c.reset}`);
169
+ }
170
+
171
+ // Install settings template (with hooks wiring)
172
+ const settingsInstalled = installSettings(targetDir);
173
+ if (settingsInstalled) {
174
+ success(`Installed settings template with hook wiring`);
175
+ }
176
+
138
177
  log('');
139
178
  log(`${c.green}Done!${c.reset} Restart Claude Code to load commands.`);
140
179
  log('');
@@ -150,6 +189,136 @@ function install(targetDir, installType) {
150
189
  log('');
151
190
  }
152
191
 
192
+ function installHooks(targetDir, packageRoot) {
193
+ // For local install: .claude/commands -> go up to .claude/hooks
194
+ // For global install: ~/.claude/commands -> go up to ~/.claude/hooks
195
+ const claudeDir = path.dirname(targetDir);
196
+ const hooksDestDir = path.join(claudeDir, 'hooks');
197
+ fs.mkdirSync(hooksDestDir, { recursive: true });
198
+
199
+ // Try .claude/hooks/ first (npm package), fall back to root .claude/hooks/ (dev)
200
+ const hooksSrcDir = fs.existsSync(path.join(packageRoot, '.claude', 'hooks'))
201
+ ? path.join(packageRoot, '.claude', 'hooks')
202
+ : null;
203
+
204
+ if (!hooksSrcDir) return 0;
205
+
206
+ let copied = 0;
207
+ for (const file of HOOKS) {
208
+ const src = path.join(hooksSrcDir, file);
209
+ const dest = path.join(hooksDestDir, file);
210
+ if (fs.existsSync(src)) {
211
+ fs.copyFileSync(src, dest);
212
+ // Make executable
213
+ fs.chmodSync(dest, 0o755);
214
+ copied++;
215
+ }
216
+ }
217
+ return copied;
218
+ }
219
+
220
+ function installSettings(targetDir) {
221
+ // For local install: .claude/commands -> go up to .claude/settings.json
222
+ // For global install: ~/.claude/commands -> go up to ~/.claude/settings.json
223
+ const claudeDir = path.dirname(targetDir);
224
+ const settingsPath = path.join(claudeDir, 'settings.json');
225
+
226
+ // The settings template with full hook wiring
227
+ const settingsTemplate = {
228
+ permissions: {
229
+ allow: [
230
+ "Bash(npm *)", "Bash(npx *)", "Bash(node *)", "Bash(git *)",
231
+ "Bash(gh *)", "Bash(ssh *)", "Bash(scp *)", "Bash(rsync *)",
232
+ "Bash(curl *)", "Bash(wget *)", "Bash(docker *)", "Bash(docker-compose *)",
233
+ "Bash(pytest*)", "Bash(python *)", "Bash(pip *)", "Bash(go *)",
234
+ "Bash(cargo *)", "Bash(make *)", "Bash(cat *)", "Bash(ls *)",
235
+ "Bash(pwd*)", "Bash(cd *)", "Bash(mkdir *)", "Bash(cp *)",
236
+ "Bash(mv *)", "Bash(which *)", "Bash(echo *)", "Bash(jq *)",
237
+ "Bash(wc *)", "Bash(head *)", "Bash(tail *)", "Bash(sort *)",
238
+ "Bash(uniq *)", "Bash(xargs *)"
239
+ ]
240
+ },
241
+ hooks: {
242
+ PreToolUse: [{
243
+ matcher: "EnterPlanMode|TaskCreate|TaskUpdate|TaskList|TaskGet|ExitPlanMode",
244
+ hooks: [{
245
+ type: "command",
246
+ command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-block-tools.sh",
247
+ timeout: 5
248
+ }]
249
+ }],
250
+ UserPromptSubmit: [{
251
+ hooks: [{
252
+ type: "command",
253
+ command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-prompt-guard.sh",
254
+ timeout: 5
255
+ }]
256
+ }],
257
+ SessionStart: [{
258
+ hooks: [{
259
+ type: "command",
260
+ command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-session-init.sh",
261
+ timeout: 5
262
+ }]
263
+ }],
264
+ PostToolUse: [
265
+ {
266
+ matcher: "Bash",
267
+ hooks: [{
268
+ type: "command",
269
+ command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-post-push.sh",
270
+ timeout: 5
271
+ }]
272
+ },
273
+ {
274
+ matcher: "Skill",
275
+ hooks: [{
276
+ type: "command",
277
+ command: "bash $CLAUDE_PROJECT_DIR/.claude/hooks/tlc-post-build.sh",
278
+ timeout: 5
279
+ }]
280
+ }
281
+ ],
282
+ Stop: [{
283
+ hooks: [{
284
+ type: "command",
285
+ command: "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tlc-capture-exchange.sh",
286
+ timeout: 30
287
+ }]
288
+ }]
289
+ }
290
+ };
291
+
292
+ if (fs.existsSync(settingsPath)) {
293
+ // Merge: preserve existing permissions, add missing hooks
294
+ try {
295
+ const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
296
+ // Merge permissions (union)
297
+ if (existing.permissions && existing.permissions.allow) {
298
+ const existingSet = new Set(existing.permissions.allow);
299
+ for (const perm of settingsTemplate.permissions.allow) {
300
+ existingSet.add(perm);
301
+ }
302
+ existing.permissions.allow = [...existingSet];
303
+ } else {
304
+ existing.permissions = settingsTemplate.permissions;
305
+ }
306
+ // Add hooks if not present
307
+ if (!existing.hooks) {
308
+ existing.hooks = settingsTemplate.hooks;
309
+ }
310
+ fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
311
+ return true;
312
+ } catch (err) {
313
+ // If we can't parse existing, don't overwrite
314
+ return false;
315
+ }
316
+ } else {
317
+ fs.writeFileSync(settingsPath, JSON.stringify(settingsTemplate, null, 2) + '\n');
318
+ return true;
319
+ }
320
+ }
321
+
153
322
  async function main() {
154
323
  const args = process.argv.slice(2);
155
324
 
@@ -4,44 +4,63 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
 
7
- // Get source and destination directories
8
- const srcDir = path.join(__dirname, '..', '.claude', 'commands', 'tlc');
9
- const destDir = path.join(os.homedir(), '.claude', 'commands', 'tlc');
7
+ const packageRoot = path.join(__dirname, '..');
8
+ const claudeHome = 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
+
14
+ // Destination directories (user's home)
15
+ const commandsDestDir = path.join(claudeHome, 'commands', 'tlc');
16
+ const hooksDestDir = path.join(claudeHome, 'hooks');
10
17
 
11
- // Create destination directory if it doesn't exist
12
18
  function ensureDir(dir) {
13
19
  if (!fs.existsSync(dir)) {
14
20
  fs.mkdirSync(dir, { recursive: true });
15
21
  }
16
22
  }
17
23
 
18
- // Copy all .md files from source to destination
24
+ // Copy all .md command files
19
25
  function copyCommands() {
20
- try {
21
- // Ensure destination exists
22
- ensureDir(destDir);
26
+ if (!fs.existsSync(commandsSrcDir)) return 0;
27
+ ensureDir(commandsDestDir);
23
28
 
24
- // Check if source exists
25
- if (!fs.existsSync(srcDir)) {
26
- // Silent exit if source doesn't exist (might be dev install)
27
- return;
28
- }
29
+ const files = fs.readdirSync(commandsSrcDir).filter(f => f.endsWith('.md'));
30
+ let copied = 0;
31
+ for (const file of files) {
32
+ fs.copyFileSync(path.join(commandsSrcDir, file), path.join(commandsDestDir, file));
33
+ copied++;
34
+ }
35
+ return copied;
36
+ }
29
37
 
30
- // Get all .md files
31
- const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.md'));
38
+ // Copy all .sh hook files
39
+ function copyHooks() {
40
+ if (!fs.existsSync(hooksSrcDir)) return 0;
41
+ ensureDir(hooksDestDir);
32
42
 
33
- let copied = 0;
34
- for (const file of files) {
35
- const src = path.join(srcDir, file);
36
- const dest = path.join(destDir, file);
43
+ const files = fs.readdirSync(hooksSrcDir).filter(f => f.endsWith('.sh'));
44
+ let copied = 0;
45
+ for (const file of files) {
46
+ const dest = path.join(hooksDestDir, file);
47
+ fs.copyFileSync(path.join(hooksSrcDir, file), dest);
48
+ fs.chmodSync(dest, 0o755);
49
+ copied++;
50
+ }
51
+ return copied;
52
+ }
37
53
 
38
- // Copy file (overwrite if exists)
39
- fs.copyFileSync(src, dest);
40
- copied++;
41
- }
54
+ function postinstall() {
55
+ try {
56
+ const commands = copyCommands();
57
+ const hooks = copyHooks();
42
58
 
43
- if (copied > 0) {
44
- console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${copied} commands to ~/.claude/commands/tlc/`);
59
+ if (commands > 0) {
60
+ console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${commands} commands to ~/.claude/commands/tlc/`);
61
+ }
62
+ if (hooks > 0) {
63
+ console.log(`\x1b[32m✓\x1b[0m TLC: Installed ${hooks} hooks to ~/.claude/hooks/`);
45
64
  }
46
65
  } catch (err) {
47
66
  // Silent fail - don't break npm install
@@ -51,4 +70,4 @@ function copyCommands() {
51
70
  }
52
71
  }
53
72
 
54
- copyCommands();
73
+ postinstall();
@@ -0,0 +1 @@
1
+ :root{--color-bg-primary: #0a0a0b;--color-bg-secondary: #141416;--color-bg-tertiary: #1e1e21;--color-bg-elevated: #252529;--color-text-primary: #fafafa;--color-text-secondary: #a1a1aa;--color-text-muted: #71717a;--color-border: #27272a;--color-border-hover: #3f3f46;--color-accent: #3b82f6;--color-accent-hover: #2563eb;--color-success: #22c55e;--color-warning: #eab308;--color-error: #ef4444;--color-info: #06b6d4;--space-0: 0;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.25rem;--space-6: 1.5rem;--space-8: 2rem;--space-10: 2.5rem;--space-12: 3rem;--space-16: 4rem;--font-sans: "Inter", system-ui, -apple-system, sans-serif;--font-mono: "JetBrains Mono", ui-monospace, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: 1rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.5rem;--text-3xl: 1.875rem;--font-normal: 400;--font-medium: 500;--font-semibold: 600;--font-bold: 700;--leading-tight: 1.25;--leading-normal: 1.5;--leading-relaxed: 1.625;--radius-sm: .25rem;--radius-md: .375rem;--radius-lg: .5rem;--radius-xl: .75rem;--radius-2xl: 1rem;--radius-full: 9999px;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);--shadow-md: 0 4px 6px rgba(0, 0, 0, .3);--shadow-lg: 0 10px 15px rgba(0, 0, 0, .3);--shadow-xl: 0 20px 25px rgba(0, 0, 0, .3);--transition-fast: .1s ease;--transition-base: .2s ease;--transition-slow: .3s ease;--z-dropdown: 1000;--z-sticky: 1020;--z-modal: 1030;--z-popover: 1040;--z-tooltip: 1050;--z-toast: 1060;--sidebar-width: 240px;--sidebar-collapsed-width: 64px;--header-height: 56px;--mobile-nav-height: 64px}[data-theme=light]{--color-bg-primary: #ffffff;--color-bg-secondary: #f4f4f5;--color-bg-tertiary: #e4e4e7;--color-bg-elevated: #ffffff;--color-text-primary: #09090b;--color-text-secondary: #52525b;--color-text-muted: #a1a1aa;--color-border: #e4e4e7;--color-border-hover: #d4d4d8;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .05);--shadow-md: 0 4px 6px rgba(0, 0, 0, .07);--shadow-lg: 0 10px 15px rgba(0, 0, 0, .1);--shadow-xl: 0 20px 25px rgba(0, 0, 0, .1)}@media(prefers-contrast:high){:root{--color-border: #52525b;--color-text-secondary: #d4d4d8}[data-theme=light]{--color-border: #71717a;--color-text-secondary: #3f3f46}}@media(prefers-reduced-motion:reduce){:root{--transition-fast: 0ms;--transition-base: 0ms;--transition-slow: 0ms}}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:var(--font-sans);font-size:16px;line-height:var(--leading-normal);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;padding:0;background-color:var(--color-bg-primary);color:var(--color-text-primary)}:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}.skip-link{position:absolute;top:-40px;left:0;padding:var(--space-2) var(--space-4);background:var(--color-accent);color:#fff;z-index:var(--z-tooltip);transition:top var(--transition-fast)}.skip-link:focus{top:0}::-moz-selection{background-color:var(--color-accent);color:#fff}::selection{background-color:var(--color-accent);color:#fff}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--color-bg-secondary)}::-webkit-scrollbar-thumb{background:var(--color-border-hover);border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--color-text-muted)}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;border-radius:var(--radius-md);padding:.5rem 1rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn:focus-visible{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-color: var(--color-accent);--tw-ring-offset-width: 2px}.btn:disabled{cursor:not-allowed;opacity:.5}.btn-primary{background-color:var(--color-accent);--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-primary:hover{background-color:var(--color-accent-hover)}.btn-secondary{border-width:1px;border-color:var(--color-border);background-color:var(--color-bg-tertiary);color:var(--color-text-primary)}.btn-secondary:hover{background-color:var(--color-bg-elevated)}.btn-ghost{background-color:transparent;color:var(--color-text-secondary)}.btn-ghost:hover{background-color:var(--color-bg-tertiary);color:var(--color-text-primary)}.btn-danger{background-color:var(--color-error);--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-danger:hover{opacity:.9}.btn-sm{padding:.375rem .75rem;font-size:.875rem;line-height:1.25rem}.btn-lg{padding:.75rem 1.5rem;font-size:1.125rem;line-height:1.75rem}.card{border-radius:var(--radius-lg);border-width:1px;border-color:var(--color-border);background-color:var(--color-bg-secondary)}.card-hover{cursor:pointer;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.card-hover:hover{border-color:var(--color-border-hover);background-color:var(--color-bg-tertiary)}.input{width:100%;padding:.5rem .75rem;border-radius:var(--radius-md);border-width:1px;border-color:var(--color-border);background-color:var(--color-bg-tertiary);color:var(--color-text-primary)}.input::-moz-placeholder{color:var(--color-text-muted)}.input::placeholder{color:var(--color-text-muted)}.input:focus{border-color:transparent;outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-color: var(--color-accent)}.input:disabled{cursor:not-allowed;opacity:.5}.badge{display:inline-flex;align-items:center;gap:.25rem;border-radius:9999px;padding:.125rem .5rem;font-size:.75rem;line-height:1rem;font-weight:500}.badge-success{color:var(--color-success);background-color:color-mix(in srgb,var(--color-success) 20%,transparent)}.badge-warning{color:var(--color-warning);background-color:color-mix(in srgb,var(--color-warning) 20%,transparent)}.badge-error{color:var(--color-error);background-color:color-mix(in srgb,var(--color-error) 20%,transparent)}.badge-info{color:var(--color-info);background-color:color-mix(in srgb,var(--color-info) 20%,transparent)}.badge-neutral{background-color:var(--color-bg-tertiary);color:var(--color-text-secondary)}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-1{top:-.25rem}.-top-10{top:-2.5rem}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-1\.5{left:.375rem}.left-2{left:.5rem}.left-3{left:.75rem}.left-4{left:1rem}.left-full{left:100%}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.-mb-px{margin-bottom:-1px}.-ml-2{margin-left:-.5rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-10{margin-left:2.5rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-6{margin-left:1.5rem}.ml-9{margin-left:2.25rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[18px\]{height:18px}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[400px\]{max-height:400px}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.min-h-\[100px\]{min-height:100px}.min-h-\[200px\]{min-height:200px}.min-h-\[400px\]{min-height:400px}.min-h-\[60vh\]{min-height:60vh}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-1\/3{width:33.333333%}.w-1\/4{width:25%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-5\/6{width:83.333333%}.w-56{width:14rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-64{width:16rem}.w-8{width:2rem}.w-96{width:24rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[180px\]{min-width:180px}.min-w-\[18px\]{min-width:18px}.min-w-\[300px\]{min-width:300px}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-\[400px\]{max-width:400px}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-border>:not([hidden])~:not([hidden]){border-color:var(--color-border)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-x{border-left-width:1px;border-right-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-accent{border-color:var(--color-accent)}.border-border{border-color:var(--color-border)}.border-error{border-color:var(--color-error)}.border-info{border-color:var(--color-info)}.border-transparent{border-color:transparent}.border-warning{border-color:var(--color-warning)}.border-l-error{border-left-color:var(--color-error)}.border-l-info{border-left-color:var(--color-info)}.border-l-success{border-left-color:var(--color-success)}.border-l-warning{border-left-color:var(--color-warning)}.bg-accent{background-color:var(--color-accent)}.bg-bg-elevated{background-color:var(--color-bg-elevated)}.bg-bg-primary{background-color:var(--color-bg-primary)}.bg-bg-secondary{background-color:var(--color-bg-secondary)}.bg-bg-tertiary{background-color:var(--color-bg-tertiary)}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/80{background-color:#000c}.bg-border{background-color:var(--color-border)}.bg-error{background-color:var(--color-error)}.bg-info{background-color:var(--color-info)}.bg-success{background-color:var(--color-success)}.bg-text-muted{background-color:var(--color-text-muted)}.bg-transparent{background-color:transparent}.bg-warning{background-color:var(--color-warning)}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-0{padding-bottom:0}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-8{padding-left:2rem}.pl-9{padding-left:2.25rem}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pr-8{padding-right:2rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-\[20vh\]{padding-top:20vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.leading-6{line-height:1.5rem}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-accent{color:var(--color-accent)}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-error{color:var(--color-error)}.text-info{color:var(--color-info)}.text-success{color:var(--color-success)}.text-text-muted{color:var(--color-text-muted)}.text-text-primary{color:var(--color-text-primary)}.text-text-secondary{color:var(--color-text-secondary)}.text-warning{color:var(--color-warning)}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: var(--shadow-lg);--tw-shadow-colored: var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.placeholder\:text-text-muted::-moz-placeholder{color:var(--color-text-muted)}.placeholder\:text-text-muted::placeholder{color:var(--color-text-muted)}.first\:mt-0:first-child{margin-top:0}.hover\:bg-accent-hover:hover{background-color:var(--color-accent-hover)}.hover\:bg-bg-tertiary:hover{background-color:var(--color-bg-tertiary)}.hover\:text-error:hover{color:var(--color-error)}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:text-text-primary:hover{color:var(--color-text-primary)}.hover\:text-text-secondary:hover{color:var(--color-text-secondary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.hover\:shadow-md:hover{--tw-shadow: var(--shadow-md);--tw-shadow-colored: var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow: var(--shadow-sm);--tw-shadow-colored: var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:ring-2:hover{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:border-accent:focus{border-color:var(--color-accent)}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-accent:focus{--tw-ring-color: var(--color-accent)}.focus\:ring-error:focus{--tw-ring-color: var(--color-error)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:640px){.sm\:block{display:block}.sm\:flex-row{flex-direction:row}}@media(min-width:768px){.md\:block{display:block}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}