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 +117 -2
- package/hooks/up-context-monitor.js +112 -0
- package/hooks/up-statusline.js +88 -0
- package/package.json +3 -2
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.
|
|
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
|
-
//
|
|
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.
|
|
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",
|