up-cc 0.1.0 → 0.1.1

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/bin/install.js CHANGED
@@ -507,6 +507,49 @@ function uninstall(targetDir, runtime) {
507
507
  }
508
508
  }
509
509
 
510
+ // Remove UP hooks
511
+ const hooksDir = path.join(targetDir, 'hooks');
512
+ if (fs.existsSync(hooksDir)) {
513
+ for (const file of fs.readdirSync(hooksDir)) {
514
+ if (file.startsWith('up-') && file.endsWith('.js')) {
515
+ fs.unlinkSync(path.join(hooksDir, file));
516
+ removed++;
517
+ }
518
+ }
519
+ console.log(` ${green}✓${reset} Removed UP hooks`);
520
+ }
521
+
522
+ // Clean settings.json references
523
+ if (runtime === 'claude') {
524
+ const settingsPath = path.join(targetDir, 'settings.json');
525
+ if (fs.existsSync(settingsPath)) {
526
+ try {
527
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
528
+ let changed = false;
529
+
530
+ if (settings.statusLine && settings.statusLine.command && settings.statusLine.command.includes('up-statusline')) {
531
+ delete settings.statusLine;
532
+ changed = true;
533
+ }
534
+
535
+ if (settings.hooks && settings.hooks.PostToolUse) {
536
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
537
+ const hooks = entry.hooks || [];
538
+ return !hooks.some(h => h.command && h.command.includes('up-context-monitor'));
539
+ });
540
+ if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
541
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
542
+ changed = true;
543
+ }
544
+
545
+ if (changed) {
546
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
547
+ console.log(` ${green}✓${reset} Cleaned settings.json`);
548
+ }
549
+ } catch (e) {}
550
+ }
551
+ }
552
+
510
553
  if (removed === 0) {
511
554
  console.log(` ${dim}Nothing to remove — UP is not installed here.${reset}`);
512
555
  } else {
@@ -600,12 +643,84 @@ function install(isGlobal, runtime) {
600
643
  }
601
644
  }
602
645
 
603
- // 4. Write VERSION file
646
+ // 4. Install hooks (Claude Code only)
647
+ if (runtime === 'claude') {
648
+ const hooksSrc = path.join(packageRoot, 'hooks');
649
+ if (fs.existsSync(hooksSrc)) {
650
+ const hooksDest = path.join(targetDir, 'hooks');
651
+ fs.mkdirSync(hooksDest, { recursive: true });
652
+
653
+ // Remove old UP hooks
654
+ if (fs.existsSync(hooksDest)) {
655
+ for (const file of fs.readdirSync(hooksDest)) {
656
+ if (file.startsWith('up-') && file.endsWith('.js')) {
657
+ fs.unlinkSync(path.join(hooksDest, file));
658
+ }
659
+ }
660
+ }
661
+
662
+ // Copy new UP hooks
663
+ let hookCount = 0;
664
+ for (const file of fs.readdirSync(hooksSrc)) {
665
+ if (file.endsWith('.js')) {
666
+ fs.copyFileSync(path.join(hooksSrc, file), path.join(hooksDest, file));
667
+ hookCount++;
668
+ }
669
+ }
670
+
671
+ if (hookCount > 0) {
672
+ console.log(` ${green}✓${reset} Installed ${hookCount} hooks`);
673
+ }
674
+
675
+ // Configure statusLine and hooks in settings.json
676
+ const settingsPath = path.join(targetDir, 'settings.json');
677
+ let settings = {};
678
+ if (fs.existsSync(settingsPath)) {
679
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
680
+ }
681
+
682
+ const statuslineCmd = `node "${path.join(hooksDest, 'up-statusline.js')}"`;
683
+ const contextMonitorCmd = `node "${path.join(hooksDest, 'up-context-monitor.js')}"`;
684
+
685
+ // Set statusLine
686
+ settings.statusLine = { type: 'command', command: statuslineCmd };
687
+
688
+ // Set PostToolUse hook for context monitor
689
+ if (!settings.hooks) settings.hooks = {};
690
+ const postToolHooks = settings.hooks.PostToolUse || [];
691
+ // Remove old GSD/UP context monitor entries
692
+ const filtered = postToolHooks.filter(entry => {
693
+ const hooks = entry.hooks || [];
694
+ return !hooks.some(h => h.command && (h.command.includes('gsd-context-monitor') || h.command.includes('up-context-monitor')));
695
+ });
696
+ filtered.push({ hooks: [{ type: 'command', command: contextMonitorCmd }] });
697
+ settings.hooks.PostToolUse = filtered;
698
+
699
+ // Remove old GSD SessionStart hook if present
700
+ if (settings.hooks.SessionStart) {
701
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => {
702
+ const hooks = entry.hooks || [];
703
+ return !hooks.some(h => h.command && h.command.includes('gsd-'));
704
+ });
705
+ if (settings.hooks.SessionStart.length === 0) delete settings.hooks.SessionStart;
706
+ }
707
+
708
+ // Clean old GSD statusLine reference
709
+ if (settings.statusLine && settings.statusLine.command && settings.statusLine.command.includes('gsd-statusline')) {
710
+ settings.statusLine = { type: 'command', command: statuslineCmd };
711
+ }
712
+
713
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
714
+ console.log(` ${green}✓${reset} Configured statusLine and context monitor`);
715
+ }
716
+ }
717
+
718
+ // 5. Write VERSION file
604
719
  const versionDest = path.join(upDest, 'VERSION');
605
720
  fs.writeFileSync(versionDest, VERSION);
606
721
  console.log(` ${green}✓${reset} Wrote VERSION (${VERSION})`);
607
722
 
608
- // 5. Write package.json for CommonJS mode (prevents ESM conflicts)
723
+ // 6. Write package.json for CommonJS mode (prevents ESM conflicts)
609
724
  if (runtime !== 'opencode') {
610
725
  const pkgDest = path.join(targetDir, 'package.json');
611
726
  if (!fs.existsSync(pkgDest)) {
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ // Context Monitor - PostToolUse hook
3
+ // Reads context metrics from the statusline bridge file and injects
4
+ // warnings when context usage is high.
5
+
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
10
+ const WARNING_THRESHOLD = 35;
11
+ const CRITICAL_THRESHOLD = 25;
12
+ const STALE_SECONDS = 60;
13
+ const DEBOUNCE_CALLS = 5;
14
+
15
+ let input = '';
16
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
17
+ process.stdin.setEncoding('utf8');
18
+ process.stdin.on('data', chunk => input += chunk);
19
+ process.stdin.on('end', () => {
20
+ clearTimeout(stdinTimeout);
21
+ try {
22
+ const data = JSON.parse(input);
23
+ const sessionId = data.session_id;
24
+
25
+ if (!sessionId) {
26
+ process.exit(0);
27
+ }
28
+
29
+ const tmpDir = os.tmpdir();
30
+ const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
31
+
32
+ if (!fs.existsSync(metricsPath)) {
33
+ process.exit(0);
34
+ }
35
+
36
+ const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
37
+ const now = Math.floor(Date.now() / 1000);
38
+
39
+ if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
40
+ process.exit(0);
41
+ }
42
+
43
+ const remaining = metrics.remaining_percentage;
44
+ const usedPct = metrics.used_pct;
45
+
46
+ if (remaining > WARNING_THRESHOLD) {
47
+ process.exit(0);
48
+ }
49
+
50
+ // Debounce
51
+ const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
52
+ let warnData = { callsSinceWarn: 0, lastLevel: null };
53
+ let firstWarn = true;
54
+
55
+ if (fs.existsSync(warnPath)) {
56
+ try {
57
+ warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
58
+ firstWarn = false;
59
+ } catch (e) {}
60
+ }
61
+
62
+ warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
63
+
64
+ const isCritical = remaining <= CRITICAL_THRESHOLD;
65
+ const currentLevel = isCritical ? 'critical' : 'warning';
66
+
67
+ const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
68
+ if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
69
+ fs.writeFileSync(warnPath, JSON.stringify(warnData));
70
+ process.exit(0);
71
+ }
72
+
73
+ warnData.callsSinceWarn = 0;
74
+ warnData.lastLevel = currentLevel;
75
+ fs.writeFileSync(warnPath, JSON.stringify(warnData));
76
+
77
+ // Detect if UP is active (has .plano/STATE.md in working directory)
78
+ const cwd = data.cwd || process.cwd();
79
+ const isUpActive = fs.existsSync(path.join(cwd, '.plano', 'STATE.md'));
80
+
81
+ let message;
82
+ if (isCritical) {
83
+ message = isUpActive
84
+ ? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
85
+ 'Context is nearly exhausted. Do NOT start new complex work or write handoff files -- ' +
86
+ 'UP state is already tracked in STATE.md. Inform the user so they can run ' +
87
+ '/up:pausar at the next natural stopping point.'
88
+ : `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
89
+ 'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
90
+ 'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
91
+ } else {
92
+ message = isUpActive
93
+ ? `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
94
+ 'Context is getting limited. Avoid starting new complex work. If not between ' +
95
+ 'defined plan steps, inform the user so they can prepare to pause.'
96
+ : `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
97
+ 'Be aware that context is getting limited. Avoid unnecessary exploration or ' +
98
+ 'starting new complex work.';
99
+ }
100
+
101
+ const output = {
102
+ hookSpecificOutput: {
103
+ hookEventName: "PostToolUse",
104
+ additionalContext: message
105
+ }
106
+ };
107
+
108
+ process.stdout.write(JSON.stringify(output));
109
+ } catch (e) {
110
+ process.exit(0);
111
+ }
112
+ });
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code Statusline - UP Edition
3
+ // Shows: model | current task | directory | context usage
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ let input = '';
10
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
11
+ process.stdin.setEncoding('utf8');
12
+ process.stdin.on('data', chunk => input += chunk);
13
+ process.stdin.on('end', () => {
14
+ clearTimeout(stdinTimeout);
15
+ try {
16
+ const data = JSON.parse(input);
17
+ const model = data.model?.display_name || 'Claude';
18
+ const dir = data.workspace?.current_dir || process.cwd();
19
+ const session = data.session_id || '';
20
+ const remaining = data.context_window?.remaining_percentage;
21
+
22
+ // Context window display (shows USED percentage scaled to usable context)
23
+ const AUTO_COMPACT_BUFFER_PCT = 16.5;
24
+ let ctx = '';
25
+ if (remaining != null) {
26
+ const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
27
+ const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
28
+
29
+ // Write context metrics to bridge file for the context-monitor hook
30
+ if (session) {
31
+ try {
32
+ const bridgePath = path.join(os.tmpdir(), `claude-ctx-${session}.json`);
33
+ const bridgeData = JSON.stringify({
34
+ session_id: session,
35
+ remaining_percentage: remaining,
36
+ used_pct: used,
37
+ timestamp: Math.floor(Date.now() / 1000)
38
+ });
39
+ fs.writeFileSync(bridgePath, bridgeData);
40
+ } catch (e) {}
41
+ }
42
+
43
+ // Build progress bar (10 segments)
44
+ const filled = Math.floor(used / 10);
45
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled);
46
+
47
+ if (used < 50) {
48
+ ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
49
+ } else if (used < 65) {
50
+ ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
51
+ } else if (used < 80) {
52
+ ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
53
+ } else {
54
+ ctx = ` \x1b[5;31m${bar} ${used}%\x1b[0m`;
55
+ }
56
+ }
57
+
58
+ // Current task from todos
59
+ let task = '';
60
+ const homeDir = os.homedir();
61
+ const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude');
62
+ const todosDir = path.join(claudeDir, 'todos');
63
+ if (session && fs.existsSync(todosDir)) {
64
+ try {
65
+ const files = fs.readdirSync(todosDir)
66
+ .filter(f => f.startsWith(session) && f.includes('-agent-') && f.endsWith('.json'))
67
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime }))
68
+ .sort((a, b) => b.mtime - a.mtime);
69
+
70
+ if (files.length > 0) {
71
+ try {
72
+ const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8'));
73
+ const inProgress = todos.find(t => t.status === 'in_progress');
74
+ if (inProgress) task = inProgress.activeForm || '';
75
+ } catch (e) {}
76
+ }
77
+ } catch (e) {}
78
+ }
79
+
80
+ // Output
81
+ const dirname = path.basename(dir);
82
+ if (task) {
83
+ process.stdout.write(`\x1b[2m${model}\x1b[0m \u2502 \x1b[1m${task}\x1b[0m \u2502 \x1b[2m${dirname}\x1b[0m${ctx}`);
84
+ } else {
85
+ process.stdout.write(`\x1b[2m${model}\x1b[0m \u2502 \x1b[2m${dirname}\x1b[0m${ctx}`);
86
+ }
87
+ } catch (e) {}
88
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "up-cc",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Simplified spec-driven development for Claude Code, Gemini and OpenCode.",
5
5
  "bin": {
6
6
  "up-cc": "bin/install.js"
@@ -11,7 +11,8 @@
11
11
  "commands",
12
12
  "workflows",
13
13
  "templates",
14
- "references"
14
+ "references",
15
+ "hooks"
15
16
  ],
16
17
  "keywords": [
17
18
  "claude",