+23
-3
@@ -494,8 +494,15 @@ #!/usr/bin/env node | ||
| if (bool) { | ||
| installStatusline(); | ||
| const result = installStatusline(); | ||
| console.log(` ✓ statusline = true — wired into ~/.claude/settings.json`); | ||
| if (result.wrapped) { | ||
| console.log(` Existing statusLine detected — wrapping it (you'll see both).`); | ||
| } | ||
| console.log(` Open a new Claude Code session to see it.`); | ||
| } else { | ||
| uninstallStatusline(); | ||
| console.log(` ✓ statusline = false — removed from ~/.claude/settings.json`); | ||
| const result = uninstallStatusline(); | ||
| if (result.restored) { | ||
| console.log(` ✓ statusline = false — your original statusLine restored.`); | ||
| } else { | ||
| console.log(` ✓ statusline = false — removed from ~/.claude/settings.json`); | ||
| } | ||
| } | ||
@@ -521,3 +528,16 @@ } else { | ||
| function registerRawModeExitGuard() { | ||
| process.once('exit', () => { | ||
| try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {} | ||
| }); | ||
| for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) { | ||
| process.once(sig, () => { | ||
| try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {} | ||
| process.exit(130); | ||
| }); | ||
| } | ||
| } | ||
| async function pickColorInteractive() { | ||
| registerRawModeExitGuard(); | ||
| const colors = Object.keys(STATUSLINE_COLORS); | ||
@@ -524,0 +544,0 @@ const currentColor = getConfigValue('statusline_color') || 'cyan'; |
+1
-1
@@ -42,3 +42,3 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'; | ||
| try { | ||
| input = readFileSync('/dev/stdin', 'utf-8'); | ||
| input = readFileSync(0, 'utf-8'); | ||
| } catch { | ||
@@ -45,0 +45,0 @@ console.log(`Reminder: ${REMINDER}`); |
+34
-7
@@ -5,2 +5,3 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs'; | ||
| import { execSync } from 'child_process'; | ||
| import { getConfigValue, setConfig, unsetConfig } from './config.js'; | ||
@@ -97,2 +98,7 @@ const CLAUDE_DIR = join(homedir(), '.claude'); | ||
| function isCloudctxStatusLine(entry) { | ||
| const cmd = entry?.command || ''; | ||
| return cmd.includes('cloudctx') && /\bstatusline\b/.test(cmd); | ||
| } | ||
| export function installStatusline() { | ||
@@ -104,2 +110,12 @@ const binPath = getCloudctxBinPath(); | ||
| } | ||
| // If there's an existing non-cloudctx statusLine, preserve it so our | ||
| // subcommand can wrap it (toolbelt mode). | ||
| const existing = settings.statusLine; | ||
| let wrapped = false; | ||
| if (existing && !isCloudctxStatusLine(existing)) { | ||
| setConfig('wrapped_statusline', existing); | ||
| wrapped = true; | ||
| } | ||
| settings.statusLine = { | ||
@@ -110,14 +126,25 @@ type: 'command', | ||
| writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n'); | ||
| return true; | ||
| return { installed: true, wrapped }; | ||
| } | ||
| export function uninstallStatusline() { | ||
| if (!existsSync(SETTINGS_FILE)) return false; | ||
| if (!existsSync(SETTINGS_FILE)) return { removed: false, restored: false }; | ||
| let settings; | ||
| try { settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8')); } catch { return false; } | ||
| const cmd = settings.statusLine?.command || ''; | ||
| if (!cmd.includes('cloudctx') || !cmd.includes('statusline')) return false; | ||
| delete settings.statusLine; | ||
| try { settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8')); } catch { return { removed: false, restored: false }; } | ||
| const currentIsOurs = isCloudctxStatusLine(settings.statusLine); | ||
| if (!currentIsOurs) return { removed: false, restored: false }; | ||
| // Restore whatever we wrapped, if anything. | ||
| const wrapped = getConfigValue('wrapped_statusline'); | ||
| let restored = false; | ||
| if (wrapped && wrapped.command) { | ||
| settings.statusLine = wrapped; | ||
| unsetConfig('wrapped_statusline'); | ||
| restored = true; | ||
| } else { | ||
| delete settings.statusLine; | ||
| } | ||
| writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n'); | ||
| return true; | ||
| return { removed: true, restored }; | ||
| } | ||
@@ -124,0 +151,0 @@ |
+10
-0
@@ -202,2 +202,12 @@ import { getReadonlyDb, getDb, dbExists, migrate } from './db.js'; | ||
| process.once('exit', () => { | ||
| try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {} | ||
| }); | ||
| for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) { | ||
| process.once(sig, () => { | ||
| try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {} | ||
| process.exit(130); | ||
| }); | ||
| } | ||
| process.stdin.setRawMode(true); | ||
@@ -204,0 +214,0 @@ process.stdin.resume(); |
+39
-22
| import { readFileSync } from 'fs'; | ||
| import { execSync } from 'child_process'; | ||
| import { getReadonlyDb, dbExists } from './db.js'; | ||
@@ -6,14 +7,10 @@ import { getConfigValue, STATUSLINE_COLORS } from './config.js'; | ||
| export function runStatusline() { | ||
| let input = ''; | ||
| let sessionId = ''; | ||
| let ourPrefix = ''; | ||
| try { | ||
| if (!dbExists()) { | ||
| process.exit(0); | ||
| } | ||
| try { input = readFileSync(0, 'utf-8'); } catch {} | ||
| let input = ''; | ||
| try { | ||
| input = readFileSync('/dev/stdin', 'utf-8'); | ||
| } catch {} | ||
| let sessionId = ''; | ||
| try { | ||
| const parsed = JSON.parse(input); | ||
@@ -23,20 +20,40 @@ sessionId = parsed.session_id || parsed.sessionId || ''; | ||
| if (!sessionId) process.exit(0); | ||
| if (sessionId && dbExists()) { | ||
| try { | ||
| const db = getReadonlyDb(); | ||
| const row = db.prepare( | ||
| 'SELECT name FROM saved_threads WHERE session_id = ? ORDER BY saved_at DESC LIMIT 1' | ||
| ).get(sessionId); | ||
| db.close(); | ||
| if (row && row.name) { | ||
| const colorName = getConfigValue('statusline_color') || 'cyan'; | ||
| const code = STATUSLINE_COLORS[colorName] ?? ''; | ||
| const open = code ? `\x1b[1;${code}m` : `\x1b[1m`; | ||
| ourPrefix = `${open}📌 ${row.name}\x1b[0m`; | ||
| } | ||
| } catch {} | ||
| } | ||
| const db = getReadonlyDb(); | ||
| const row = db.prepare( | ||
| 'SELECT name FROM saved_threads WHERE session_id = ? ORDER BY saved_at DESC LIMIT 1' | ||
| ).get(sessionId); | ||
| db.close(); | ||
| const wrapped = getConfigValue('wrapped_statusline'); | ||
| let wrappedOutput = ''; | ||
| if (wrapped && wrapped.command) { | ||
| try { | ||
| wrappedOutput = execSync(wrapped.command, { | ||
| input, | ||
| timeout: 800, | ||
| encoding: 'utf-8', | ||
| stdio: ['pipe', 'pipe', 'ignore'], | ||
| shell: true, | ||
| }).replace(/\r?\n+$/g, ''); | ||
| } catch { | ||
| // wrapped command failed or timed out — fall back to just our prefix | ||
| } | ||
| } | ||
| if (row && row.name) { | ||
| const colorName = getConfigValue('statusline_color') || 'cyan'; | ||
| const code = STATUSLINE_COLORS[colorName] ?? ''; | ||
| const prefix = code ? `\x1b[1;${code}m` : `\x1b[1m`; | ||
| process.stdout.write(`${prefix}📌 ${row.name}\x1b[0m`); | ||
| } | ||
| const parts = [ourPrefix, wrappedOutput].filter(s => s && s.length); | ||
| if (parts.length) process.stdout.write(parts.join(' · ')); | ||
| } catch { | ||
| // silent — statusline should never crash CC | ||
| // never crash Claude Code's status line | ||
| } | ||
| process.exit(0); | ||
| } |
+1
-1
| { | ||
| "name": "cloudctx", | ||
| "version": "0.1.4", | ||
| "version": "0.1.5", | ||
| "description": "Persistent memory for Claude Code. One command, full recall.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
81318
3.27%2085
3.32%14
7.69%5
25%