memoir-cli
Advanced tools
+1
-0
@@ -115,2 +115,3 @@ #!/usr/bin/env node | ||
| .option('-p, --profile <name>', 'Use a specific profile') | ||
| .option('--redact', 'Strip detected secrets from synced files before they are backed up') | ||
| .action(async (options) => { | ||
@@ -117,0 +118,0 @@ try { |
+2
-2
| { | ||
| "name": "memoir-cli", | ||
| "version": "3.7.1", | ||
| "version": "3.8.0", | ||
| "mcpName": "io.github.camgitt/memoir", | ||
@@ -23,3 +23,3 @@ "description": "MCP server that gives Claude, Cursor, and Gemini long-term memory across sessions. Your AI remembers your codebase, decisions, and preferences — across tools and machines.", | ||
| "type": "git", | ||
| "url": "https://github.com/camgitt/memoir.git" | ||
| "url": "git+https://github.com/camgitt/memoir.git" | ||
| }, | ||
@@ -26,0 +26,0 @@ "homepage": "https://memoir.sh", |
+10
-3
@@ -18,3 +18,3 @@ <div align="center"> | ||
| One command. No install, no config, no API keys. Your AI now has persistent memory across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, and 8 more tools. | ||
| One command. No install, no config, no API keys. Your AI now has persistent memory across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, and 6 more tools. | ||
@@ -48,3 +48,3 @@ --- | ||
| Your AI gets 7 memory tools: | ||
| Your AI gets 14 memory tools: | ||
@@ -60,2 +60,9 @@ | MCP Tool | What it does | | ||
| | `memoir_profiles` | Switch between work/personal | | ||
| | `memoir_set_goal` | Set the current session goal (pinned into CLAUDE.md) | | ||
| | `memoir_add_next` | Add a next action to the current session | | ||
| | `memoir_complete_next` | Mark a next action as done | | ||
| | `memoir_note` | Record a decision with its rationale | | ||
| | `memoir_ask` | Capture an open question for later | | ||
| | `memoir_session` | Show goals, next actions, decisions, and recent sessions | | ||
| | `memoir_why` | Look up why a past decision was made | | ||
@@ -68,3 +75,3 @@ ## Why memoir | ||
| **13 tools supported:** Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, OpenAI Codex, ChatGPT, Aider, Zed, Cline, Continue.dev, Augment, Trae. | ||
| **11 tools supported:** Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, OpenAI Codex, ChatGPT, Aider, Zed, Cline, Continue.dev. | ||
@@ -71,0 +78,0 @@ ## Sync across machines |
@@ -11,12 +11,4 @@ import chalk from 'chalk'; | ||
| import { adapters } from '../adapters/index.js'; | ||
| import { scanForSecrets as scanTextForSecrets } from '../security/scanner.js'; | ||
| const SECRET_PATTERNS = [ | ||
| { pattern: /sk-[a-zA-Z0-9]{20,}/, label: 'OpenAI/Stripe secret key' }, | ||
| { pattern: /key-[a-zA-Z0-9]{20,}/, label: 'API key' }, | ||
| { pattern: /ghp_[a-zA-Z0-9]{36,}/, label: 'GitHub personal access token' }, | ||
| { pattern: /gho_[a-zA-Z0-9]{36,}/, label: 'GitHub OAuth token' }, | ||
| { pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS access key' }, | ||
| { pattern: /Bearer\s+[a-zA-Z0-9._\-]{20,}/, label: 'Bearer token' }, | ||
| ]; | ||
| const SENSITIVE_FILENAMES = ['.env', 'credentials', 'token.json']; | ||
@@ -65,7 +57,5 @@ | ||
| const content = await fs.readFile(filePath, 'utf-8'); | ||
| for (const { pattern, label } of SECRET_PATTERNS) { | ||
| if (pattern.test(content)) { | ||
| warnings.push({ file: filePath, reason: label }); | ||
| break; | ||
| } | ||
| const { found } = scanTextForSecrets(content); | ||
| if (found.length > 0) { | ||
| warnings.push({ file: filePath, reason: found[0].label }); | ||
| } | ||
@@ -72,0 +62,0 @@ } catch { |
+87
-0
@@ -22,2 +22,48 @@ import chalk from 'chalk'; | ||
| // Recursively scan every staged file (the REAL tool memory/config files about | ||
| // to be uploaded — CLAUDE.md, .cursorrules, settings.json, project configs, | ||
| // etc.) for secrets. When `redact` is true, rewrite each offending file in | ||
| // place so the cleaned version is what gets uploaded (and encrypted, if on). | ||
| // Returns { findings, scanned } where findings is a flat list of detections | ||
| // keyed by file. Best-effort: unreadable/binary files are skipped. | ||
| export async function scanStagedFiles(dir, { redact = false } = {}) { | ||
| const findings = []; | ||
| let scanned = 0; | ||
| const walk = async (d) => { | ||
| let entries; | ||
| try { | ||
| entries = await fs.readdir(d, { withFileTypes: true }); | ||
| } catch { return; } | ||
| for (const entry of entries) { | ||
| const fullPath = path.join(d, entry.name); | ||
| if (entry.isDirectory()) { | ||
| await walk(fullPath); | ||
| continue; | ||
| } | ||
| try { | ||
| const stat = await fs.stat(fullPath); | ||
| // Skip files larger than 1MB — same threshold as doctor's scan | ||
| if (stat.size > 1024 * 1024) continue; | ||
| const content = await fs.readFile(fullPath, 'utf-8'); | ||
| scanned++; | ||
| const { found, clean } = scanForSecrets(content); | ||
| if (found.length > 0) { | ||
| for (const f of found) { | ||
| findings.push({ file: fullPath, label: f.label, redacted: f.redacted }); | ||
| } | ||
| if (redact && clean !== content) { | ||
| await fs.writeFile(fullPath, clean); | ||
| } | ||
| } | ||
| } catch { | ||
| // Skip unreadable / non-text files | ||
| } | ||
| } | ||
| }; | ||
| await walk(dir); | ||
| return { findings, scanned }; | ||
| } | ||
| export async function pushCommand(options = {}) { | ||
@@ -209,2 +255,43 @@ let config = await getConfig(options.profile); | ||
| // Scan the REAL files being synced (the staged tool memory/config files, | ||
| // not just the handoff blob) for secrets before they leave the machine. | ||
| // • --redact → strip secrets in place, then upload (sanitized) | ||
| // • otherwise → WARN and continue | ||
| // • background autopush → stay silent and continue | ||
| // We deliberately do NOT hard-block. This is a zero-knowledge encrypted | ||
| // backup of the user's OWN files; silently refusing to back up — which the | ||
| // detached `autopush` Stop-hook path (stdio:'ignore', MEMOIR_AUTOPUSH=1, no | ||
| // TTY) would hit on any false-positive match — is a worse failure than | ||
| // backing up. A future `--strict` flag could fail-closed for the | ||
| // encrypt-off / shared-destination case. Wrapped so a scanner error can | ||
| // never break the push. | ||
| const background = process.env.MEMOIR_AUTOPUSH === '1'; | ||
| try { | ||
| const { findings } = await scanStagedFiles(stagingDir, { redact: options.redact === true }); | ||
| if (findings.length > 0) { | ||
| if (options.redact === true) { | ||
| spinner.stop(); | ||
| console.log(chalk.yellow(`\n 🔒 Redacted ${findings.length} secret(s) from synced files before upload:`)); | ||
| for (const f of findings.slice(0, 5)) { | ||
| console.log(chalk.gray(` ${path.basename(f.file)}: ${f.label} (${f.redacted})`)); | ||
| } | ||
| if (findings.length > 5) console.log(chalk.gray(` ...and ${findings.length - 5} more`)); | ||
| spinner.start(); | ||
| } else if (!background) { | ||
| // Warn (interactive or piped) but never block — the backup proceeds. | ||
| spinner.stop(); | ||
| console.log(chalk.yellow(`\n ⚠️ ${findings.length} potential secret(s) in synced files (backed up as-is):`)); | ||
| for (const f of findings.slice(0, 5)) { | ||
| console.log(chalk.gray(` ${path.basename(f.file)}: ${f.label} (${f.redacted})`)); | ||
| } | ||
| if (findings.length > 5) console.log(chalk.gray(` ...and ${findings.length - 5} more`)); | ||
| console.log(chalk.gray(' Re-run with ') + chalk.cyan('--redact') + chalk.gray(' to strip them from the backup.')); | ||
| spinner.start(); | ||
| } | ||
| // background autopush: silent, continue — never block the auto-backup | ||
| } | ||
| } catch { | ||
| // Secret scan is best-effort — never let it break the push. | ||
| } | ||
| // Encrypt if enabled (or ask on first push if not configured) | ||
@@ -211,0 +298,0 @@ let uploadDir = stagingDir; |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
370652
1.13%8989
0.84%151
4.86%34
3.03%