+67
-25
@@ -14,5 +14,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'; | ||
| const SAVE_PATTERN_LITERAL = /^\/cloudctx\s+save\s+(?:"([^"]+)"|'([^']+)'|(\S[^\n]*?))\s*$/im; | ||
| // /cloudctx-rename "old" "new" or /cloudctx rename "old" "new" | ||
| const RENAME_PATTERN_SLASH = /^\/cloudctx-rename\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+(?:"([^"]+)"|'([^']+)'|(\S[^\n]*?))\s*$/im; | ||
| const RENAME_PATTERN_LITERAL = /^\/cloudctx\s+rename\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+(?:"([^"]+)"|'([^']+)'|(\S[^\n]*?))\s*$/im; | ||
| // /cloudctx-rename <args> — captures everything after the command; routing decides 1-arg vs 2-arg | ||
| const RENAME_PATTERN_SLASH = /^\/cloudctx-rename\s+(\S[^\n]*?)\s*$/im; | ||
| const RENAME_PATTERN_LITERAL = /^\/cloudctx\s+rename\s+(\S[^\n]*?)\s*$/im; | ||
| // Used inside the routing decision to try parsing as "old new" | ||
| const TWO_ARG_PARSE = /^(?:"([^"]+)"|'([^']+)'|(\S+))\s+(?:"([^"]+)"|'([^']+)'|(\S[^\n]*?))\s*$/; | ||
| const COMMANDS_DIR = join(homedir(), '.claude', 'commands'); | ||
@@ -81,5 +83,3 @@ const SLASH_COMMAND_FILE = join(COMMANDS_DIR, 'cloudctx-save.md'); | ||
| if (renameMatch) { | ||
| const oldName = renameMatch[1] || renameMatch[2] || renameMatch[3]; | ||
| const newName = renameMatch[4] || renameMatch[5] || renameMatch[6]; | ||
| handleRenameCommand(oldName, newName); | ||
| handleRename(sessionId, renameMatch[1]); | ||
| process.exit(0); | ||
@@ -137,30 +137,72 @@ } | ||
| function handleRenameCommand(rawOld, rawNew) { | ||
| const oldName = (rawOld || '').trim(); | ||
| const newName = (rawNew || '').trim(); | ||
| function stripOuterQuotes(s) { | ||
| const q = s.match(/^"(.*)"$/) || s.match(/^'(.*)'$/); | ||
| return q ? q[1] : s; | ||
| } | ||
| function reply(reason) { | ||
| console.log(JSON.stringify({ decision: 'block', reason: `[CloudCtx] ${reason}` })); | ||
| } | ||
| function handleRename(sessionId, argsStr) { | ||
| try { | ||
| if (!oldName || !newName) throw new Error('Both old and new names are required'); | ||
| const args = (argsStr || '').trim(); | ||
| if (!args) return reply('Usage: /cloudctx-rename <new-name> (or) /cloudctx-rename <old> <new>'); | ||
| const db = getDb(); | ||
| migrate(db); | ||
| const exists = db.prepare('SELECT name FROM saved_threads WHERE name = ?').get(newName); | ||
| if (exists) { | ||
| // Try to parse as two-arg form | ||
| const two = args.match(TWO_ARG_PARSE); | ||
| if (two) { | ||
| const maybeOld = two[1] || two[2] || two[3]; | ||
| const maybeNew = two[4] || two[5] || two[6]; | ||
| const oldExists = db.prepare('SELECT name FROM saved_threads WHERE name = ?').get(maybeOld); | ||
| if (oldExists) { | ||
| if (maybeOld === maybeNew) { | ||
| db.close(); | ||
| return reply(`Already named "${maybeNew}" — no change.`); | ||
| } | ||
| const collides = db.prepare('SELECT name FROM saved_threads WHERE name = ?').get(maybeNew); | ||
| if (collides) { | ||
| db.close(); | ||
| return reply(`Cannot rename: "${maybeNew}" already exists`); | ||
| } | ||
| db.prepare('UPDATE saved_threads SET name = ? WHERE name = ?').run(maybeNew, maybeOld); | ||
| db.close(); | ||
| return reply(`✓ Renamed "${maybeOld}" → "${maybeNew}"`); | ||
| } | ||
| // First token wasn't a real thread — fall through to single-arg mode using the full args string | ||
| } | ||
| // Single-arg mode: rename THIS session's saved thread to the whole args string | ||
| const newName = stripOuterQuotes(args).trim(); | ||
| if (!newName) { | ||
| db.close(); | ||
| const out = { decision: 'block', reason: `[CloudCtx] Cannot rename: "${newName}" already exists` }; | ||
| console.log(JSON.stringify(out)); | ||
| return; | ||
| return reply('New name cannot be empty'); | ||
| } | ||
| const result = db.prepare('UPDATE saved_threads SET name = ? WHERE name = ?').run(newName, oldName); | ||
| db.close(); | ||
| if (result.changes === 0) { | ||
| const out = { decision: 'block', reason: `[CloudCtx] No saved thread named "${oldName}"` }; | ||
| console.log(JSON.stringify(out)); | ||
| return; | ||
| const current = db.prepare( | ||
| 'SELECT name FROM saved_threads WHERE session_id = ? ORDER BY saved_at DESC LIMIT 1' | ||
| ).get(sessionId); | ||
| if (!current) { | ||
| db.close(); | ||
| return reply(`This session has no saved thread. Save it first with /cloudctx-save <name>`); | ||
| } | ||
| const out = { decision: 'block', reason: `[CloudCtx] ✓ Renamed "${oldName}" → "${newName}"` }; | ||
| console.log(JSON.stringify(out)); | ||
| if (current.name === newName) { | ||
| db.close(); | ||
| return reply(`Already named "${newName}" — no change.`); | ||
| } | ||
| const collides = db.prepare('SELECT name FROM saved_threads WHERE name = ?').get(newName); | ||
| if (collides) { | ||
| db.close(); | ||
| return reply(`Cannot rename: "${newName}" already exists`); | ||
| } | ||
| db.prepare('UPDATE saved_threads SET name = ? WHERE name = ?').run(newName, current.name); | ||
| db.close(); | ||
| return reply(`✓ Renamed "${current.name}" → "${newName}"`); | ||
| } catch (e) { | ||
| const out = { decision: 'block', reason: `[CloudCtx] Rename failed: ${e.message}` }; | ||
| console.log(JSON.stringify(out)); | ||
| return reply(`Rename failed: ${e.message}`); | ||
| } | ||
@@ -167,0 +209,0 @@ } |
+2
-2
@@ -23,4 +23,4 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs'; | ||
| const RENAME_COMMAND_BODY = `--- | ||
| description: Rename a saved CloudCtx thread | ||
| argument-hint: "<old-name>" "<new-name>" | ||
| description: Rename the current session's saved thread (or an older one via two args) | ||
| argument-hint: <new-name> — or: <old-name> <new-name> | ||
| --- | ||
@@ -27,0 +27,0 @@ |
+1
-1
| { | ||
| "name": "cloudctx", | ||
| "version": "0.1.6", | ||
| "version": "0.1.7", | ||
| "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
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
84559
1.68%2161
1.69%