+101
| import { readFileSync, writeFileSync, existsSync } from 'fs'; | ||
| import { join } from 'path'; | ||
| import { getDataDir, ensureDataDir } from './db.js'; | ||
| const CONFIG_PATH = join(getDataDir(), 'config.json'); | ||
| const DEFAULTS = { | ||
| statusline: false, | ||
| statusline_color: 'cyan', | ||
| }; | ||
| const KNOWN_KEYS = Object.keys(DEFAULTS); | ||
| const KEY_DESCRIPTIONS = { | ||
| statusline: 'Show saved thread name in Claude Code status line', | ||
| statusline_color: 'Color for statusline text (see: cloudctx config colors)', | ||
| }; | ||
| const BOOL_KEYS = new Set(['statusline']); | ||
| const STRING_KEYS = new Set(['statusline_color']); | ||
| export const STATUSLINE_COLORS = { | ||
| default: '', | ||
| black: '30', | ||
| red: '31', | ||
| green: '32', | ||
| yellow: '33', | ||
| blue: '34', | ||
| magenta: '35', | ||
| cyan: '36', | ||
| white: '37', | ||
| bright_black: '90', | ||
| bright_red: '91', | ||
| bright_green: '92', | ||
| bright_yellow: '93', | ||
| bright_blue: '94', | ||
| bright_magenta: '95', | ||
| bright_cyan: '96', | ||
| bright_white: '97', | ||
| }; | ||
| export function isBoolKey(key) { return BOOL_KEYS.has(key); } | ||
| export function isStringKey(key) { return STRING_KEYS.has(key); } | ||
| export function getConfigPath() { | ||
| return CONFIG_PATH; | ||
| } | ||
| function readRaw() { | ||
| if (!existsSync(CONFIG_PATH)) return {}; | ||
| try { | ||
| return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| export function getConfig() { | ||
| return { ...DEFAULTS, ...readRaw() }; | ||
| } | ||
| export function getConfigValue(key) { | ||
| return getConfig()[key]; | ||
| } | ||
| export function setConfig(key, value) { | ||
| ensureDataDir(); | ||
| const current = readRaw(); | ||
| current[key] = value; | ||
| writeFileSync(CONFIG_PATH, JSON.stringify(current, null, 2) + '\n'); | ||
| } | ||
| export function unsetConfig(key) { | ||
| if (!existsSync(CONFIG_PATH)) return; | ||
| const current = readRaw(); | ||
| delete current[key]; | ||
| writeFileSync(CONFIG_PATH, JSON.stringify(current, null, 2) + '\n'); | ||
| } | ||
| export function parseBool(val) { | ||
| const v = String(val).toLowerCase().trim(); | ||
| if (['true', 'on', '1', 'yes', 'enable', 'enabled'].includes(v)) return true; | ||
| if (['false', 'off', '0', 'no', 'disable', 'disabled'].includes(v)) return false; | ||
| return null; | ||
| } | ||
| export function isKnownKey(key) { | ||
| return KNOWN_KEYS.includes(key); | ||
| } | ||
| export function listKnownKeys() { | ||
| return [...KNOWN_KEYS]; | ||
| } | ||
| export function describeKey(key) { | ||
| return KEY_DESCRIPTIONS[key] || ''; | ||
| } | ||
| export function getDefaults() { | ||
| return { ...DEFAULTS }; | ||
| } |
| import { readFileSync } from 'fs'; | ||
| import { getReadonlyDb, dbExists } from './db.js'; | ||
| import { getConfigValue, STATUSLINE_COLORS } from './config.js'; | ||
| export function runStatusline() { | ||
| try { | ||
| if (!dbExists()) { | ||
| process.exit(0); | ||
| } | ||
| let input = ''; | ||
| try { | ||
| input = readFileSync('/dev/stdin', 'utf-8'); | ||
| } catch {} | ||
| let sessionId = ''; | ||
| try { | ||
| const parsed = JSON.parse(input); | ||
| sessionId = parsed.session_id || parsed.sessionId || ''; | ||
| } catch {} | ||
| if (!sessionId) process.exit(0); | ||
| 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 prefix = code ? `\x1b[1;${code}m` : `\x1b[1m`; | ||
| process.stdout.write(`${prefix}📌 ${row.name}\x1b[0m`); | ||
| } | ||
| } catch { | ||
| // silent — statusline should never crash CC | ||
| } | ||
| process.exit(0); | ||
| } |
+208
-1
@@ -6,5 +6,7 @@ #!/usr/bin/env node | ||
| import { runHook } from '../lib/hook.js'; | ||
| import { installHook, uninstallHook, installClaudeMd, uninstallClaudeMd, installSlashCommand, uninstallSlashCommand } from '../lib/install.js'; | ||
| import { runStatusline } from '../lib/statusline.js'; | ||
| import { installHook, uninstallHook, installClaudeMd, uninstallClaudeMd, installSlashCommand, uninstallSlashCommand, installStatusline, uninstallStatusline } from '../lib/install.js'; | ||
| import { saveThread, removeThread, renameThread, listThreads, interactiveLaunch } from '../lib/launch.js'; | ||
| import { ingestDoc, listDocs, searchDocs, deleteDoc } from '../lib/docs.js'; | ||
| import { getConfig, getConfigValue, setConfig, unsetConfig, parseBool, isKnownKey, listKnownKeys, describeKey, getConfigPath, isBoolKey, isStringKey, STATUSLINE_COLORS } from '../lib/config.js'; | ||
| import { existsSync, rmSync, statSync } from 'fs'; | ||
@@ -28,2 +30,10 @@ | ||
| case 'statusline': | ||
| runStatusline(); | ||
| break; | ||
| case 'config': | ||
| await cmdConfig(args.slice(1)); | ||
| break; | ||
| case 'sync': | ||
@@ -300,2 +310,7 @@ case 'seed': | ||
| // Remove statusline if it was installed | ||
| if (uninstallStatusline()) { | ||
| console.log(' ✓ Status line removed'); | ||
| } | ||
| // Delete data dir | ||
@@ -385,2 +400,187 @@ if (existsSync(getDataDir())) { | ||
| async function cmdConfig(subArgs) { | ||
| const sub = subArgs[0] || 'list'; | ||
| if (sub === 'list') { | ||
| const config = getConfig(); | ||
| console.log(''); | ||
| console.log(' CloudCtx Config'); | ||
| console.log(' ' + '─'.repeat(50)); | ||
| for (const key of listKnownKeys()) { | ||
| const val = config[key]; | ||
| const desc = describeKey(key); | ||
| console.log(` ${key.padEnd(18)} ${String(val).padEnd(6)} ${desc}`); | ||
| } | ||
| console.log(''); | ||
| console.log(` File: ${getConfigPath()}`); | ||
| console.log(''); | ||
| console.log(' cloudctx config set <key> <true|false>'); | ||
| console.log(' cloudctx config unset <key>'); | ||
| console.log(''); | ||
| return; | ||
| } | ||
| if (sub === 'get') { | ||
| const key = subArgs[1]; | ||
| if (!key) { console.error('Usage: cloudctx config get <key>'); process.exit(1); } | ||
| if (!isKnownKey(key)) { | ||
| console.error(`Unknown key: ${key}`); | ||
| console.error(`Known keys: ${listKnownKeys().join(', ')}`); | ||
| process.exit(1); | ||
| } | ||
| console.log(getConfigValue(key)); | ||
| return; | ||
| } | ||
| if (sub === 'color' || sub === 'colors') { | ||
| if (!process.stdin.isTTY) { | ||
| console.log(''); | ||
| console.log(' Valid statusline_color values:'); | ||
| for (const name of Object.keys(STATUSLINE_COLORS)) { | ||
| const code = STATUSLINE_COLORS[name]; | ||
| const preview = code ? `\x1b[1;${code}m📌 ${name}\x1b[0m` : `\x1b[1m📌 ${name}\x1b[0m`; | ||
| console.log(` ${preview}`); | ||
| } | ||
| console.log(''); | ||
| console.log(' cloudctx config set statusline_color <name>'); | ||
| console.log(''); | ||
| return; | ||
| } | ||
| const chosen = await pickColorInteractive(); | ||
| if (chosen) { | ||
| setConfig('statusline_color', chosen); | ||
| const code = STATUSLINE_COLORS[chosen]; | ||
| const preview = code ? `\x1b[1;${code}m📌 ${chosen}\x1b[0m` : `\x1b[1m📌 ${chosen}\x1b[0m`; | ||
| console.log(` ✓ statusline_color = ${preview}`); | ||
| } else { | ||
| console.log(' (cancelled)'); | ||
| } | ||
| return; | ||
| } | ||
| if (sub === 'set') { | ||
| const key = subArgs[1]; | ||
| const value = subArgs[2]; | ||
| if (!key || value === undefined) { | ||
| console.error('Usage: cloudctx config set <key> <value>'); | ||
| process.exit(1); | ||
| } | ||
| if (!isKnownKey(key)) { | ||
| console.error(`Unknown key: ${key}`); | ||
| console.error(`Known keys: ${listKnownKeys().join(', ')}`); | ||
| process.exit(1); | ||
| } | ||
| let storeValue; | ||
| if (isBoolKey(key)) { | ||
| const bool = parseBool(value); | ||
| if (bool === null) { | ||
| console.error(`Value must be true/false (or on/off, yes/no) — got: ${value}`); | ||
| process.exit(1); | ||
| } | ||
| storeValue = bool; | ||
| } else if (isStringKey(key)) { | ||
| if (key === 'statusline_color' && !(value in STATUSLINE_COLORS)) { | ||
| console.error(`Unknown color: ${value}`); | ||
| console.error(`Run: cloudctx config colors`); | ||
| process.exit(1); | ||
| } | ||
| storeValue = value; | ||
| } else { | ||
| storeValue = value; | ||
| } | ||
| setConfig(key, storeValue); | ||
| const bool = storeValue; | ||
| if (key === 'statusline') { | ||
| if (bool) { | ||
| installStatusline(); | ||
| console.log(` ✓ statusline = true — wired into ~/.claude/settings.json`); | ||
| console.log(` Open a new Claude Code session to see it.`); | ||
| } else { | ||
| uninstallStatusline(); | ||
| console.log(` ✓ statusline = false — removed from ~/.claude/settings.json`); | ||
| } | ||
| } else { | ||
| console.log(` ✓ ${key} = ${storeValue}`); | ||
| } | ||
| return; | ||
| } | ||
| if (sub === 'unset') { | ||
| const key = subArgs[1]; | ||
| if (!key) { console.error('Usage: cloudctx config unset <key>'); process.exit(1); } | ||
| unsetConfig(key); | ||
| if (key === 'statusline') uninstallStatusline(); | ||
| console.log(` ✓ unset ${key}`); | ||
| return; | ||
| } | ||
| console.error('Usage: cloudctx config [list|get|set|unset] ...'); | ||
| process.exit(1); | ||
| } | ||
| async function pickColorInteractive() { | ||
| const colors = Object.keys(STATUSLINE_COLORS); | ||
| const currentColor = getConfigValue('statusline_color') || 'cyan'; | ||
| let cursor = Math.max(0, colors.indexOf(currentColor)); | ||
| const render = () => { | ||
| process.stdout.write('\x1b[2J\x1b[H'); | ||
| console.log(''); | ||
| console.log(' \x1b[1mCloudCtx — Choose statusline color\x1b[0m'); | ||
| console.log(' \x1b[2m↑↓ navigate ⏎ save q cancel\x1b[0m'); | ||
| console.log(''); | ||
| for (let i = 0; i < colors.length; i++) { | ||
| const name = colors[i]; | ||
| const code = STATUSLINE_COLORS[name]; | ||
| const preview = code ? `\x1b[1;${code}m📌 ${name}\x1b[0m` : `\x1b[1m📌 ${name}\x1b[0m`; | ||
| const pointer = i === cursor ? ' \x1b[36m❯\x1b[0m ' : ' '; | ||
| const mark = name === currentColor ? ' \x1b[2m(current)\x1b[0m' : ''; | ||
| console.log(`${pointer}${preview}${mark}`); | ||
| } | ||
| console.log(''); | ||
| }; | ||
| process.stdin.setRawMode(true); | ||
| process.stdin.resume(); | ||
| process.stdin.setEncoding('utf-8'); | ||
| render(); | ||
| return new Promise((resolve) => { | ||
| const cleanup = () => { | ||
| process.stdout.write('\x1b[2J\x1b[H'); | ||
| process.stdin.setRawMode(false); | ||
| process.stdin.removeListener('data', onKey); | ||
| process.stdin.pause(); | ||
| }; | ||
| const onKey = (key) => { | ||
| if (key === '\x03' || key === 'q' || key === '\x1b') { | ||
| cleanup(); | ||
| resolve(null); | ||
| return; | ||
| } | ||
| if (key === '\x1b[A' || key === 'k') { | ||
| cursor = (cursor - 1 + colors.length) % colors.length; | ||
| render(); | ||
| return; | ||
| } | ||
| if (key === '\x1b[B' || key === 'j') { | ||
| cursor = (cursor + 1) % colors.length; | ||
| render(); | ||
| return; | ||
| } | ||
| if (key === '\r' || key === '\n') { | ||
| cleanup(); | ||
| resolve(colors[cursor]); | ||
| return; | ||
| } | ||
| }; | ||
| process.stdin.on('data', onKey); | ||
| }); | ||
| } | ||
| function cmdImport(dbPath) { | ||
@@ -473,5 +673,12 @@ if (!dbPath) { | ||
| config List all config values | ||
| config color Interactive color picker for statusline | ||
| config get <key> Get one value | ||
| config set <key> <value> Set a value (known keys: statusline, statusline_color) | ||
| config unset <key> Remove a value | ||
| hook (internal) UserPromptSubmit handler | ||
| statusline (internal) Claude Code statusLine handler | ||
| help Show this help | ||
| `); | ||
| } |
+25
-0
@@ -96,2 +96,27 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs'; | ||
| export function installStatusline() { | ||
| const binPath = getCloudctxBinPath(); | ||
| let settings = {}; | ||
| if (existsSync(SETTINGS_FILE)) { | ||
| try { settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8')); } catch {} | ||
| } | ||
| settings.statusLine = { | ||
| type: 'command', | ||
| command: `${binPath} statusline`, | ||
| }; | ||
| writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n'); | ||
| return true; | ||
| } | ||
| export function uninstallStatusline() { | ||
| if (!existsSync(SETTINGS_FILE)) return 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; | ||
| writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n'); | ||
| return true; | ||
| } | ||
| export function uninstallHook() { | ||
@@ -98,0 +123,0 @@ if (!existsSync(SETTINGS_FILE)) return false; |
+1
-1
| { | ||
| "name": "cloudctx", | ||
| "version": "0.1.3", | ||
| "version": "0.1.4", | ||
| "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
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
78742
16.06%12
20%2018
19.34%13
18.18%