@remember-md/remember
Advanced tools
+1
-1
@@ -6,3 +6,3 @@ const fs = require('fs'); | ||
| const MAX_EVIDENCE_LINES = 0; | ||
| const MAX_EVIDENCE_LINES = 20; | ||
@@ -9,0 +9,0 @@ function truncateEvidence(persona) { |
@@ -5,3 +5,3 @@ { | ||
| "description": "Portable knowledge base — one brain, every AI tool. Extract decisions, people, and insights from your AI sessions into organized local markdown.", | ||
| "version": "2.0.1", | ||
| "version": "2.0.5", | ||
| "repository": "github:remember-md/remember", | ||
@@ -8,0 +8,0 @@ "skills": ["./skills"], |
+1
-1
| { | ||
| "name": "@remember-md/remember", | ||
| "version": "2.0.4", | ||
| "version": "2.0.5", | ||
| "description": "Portable knowledge base — one brain, every AI tool. Extract decisions, people, and insights from your AI sessions into organized local markdown.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
+196
-40
@@ -10,3 +10,3 @@ #!/usr/bin/env node | ||
| const BRAIN_ROOT = getBrainRoot(); | ||
| const PROCESSED_FILE = path.join(BRAIN_ROOT, '.processed_sessions'); | ||
| const STATE_DIR = path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'), 'remember'); | ||
| const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects'); | ||
@@ -16,7 +16,30 @@ const MAX_ASSISTANT_TEXT_LEN = loadConfig().extract?.max_assistant_text_len || 500; | ||
| // --- Source definitions --- | ||
| const SOURCES = { | ||
| 'claude-code': { | ||
| processedFile: path.join(STATE_DIR, 'processed-claude-code'), | ||
| }, | ||
| 'openclaw': { | ||
| processedFile: path.join(STATE_DIR, 'processed-openclaw'), | ||
| }, | ||
| }; | ||
| function getDefaultSource() { | ||
| // Auto-detect: if OPENCLAW env or running inside OpenClaw, default to openclaw | ||
| if (process.env.OPENCLAW || process.env.OPENCLAW_WORKSPACE) return 'openclaw'; | ||
| return 'claude-code'; | ||
| } | ||
| // --- Session tracking --- | ||
| function getProcessedSessions() { | ||
| function getProcessedFile(source) { | ||
| const src = SOURCES[source]; | ||
| if (!src) throw new Error(`Unknown source: ${source}. Valid: ${Object.keys(SOURCES).join(', ')}`); | ||
| return src.processedFile; | ||
| } | ||
| function getProcessedSessions(source) { | ||
| try { | ||
| return new Set(fs.readFileSync(PROCESSED_FILE, 'utf-8').trim().split('\n').filter(Boolean)); | ||
| return new Set(fs.readFileSync(getProcessedFile(source), 'utf-8').trim().split('\n').filter(Boolean)); | ||
| } catch { | ||
@@ -27,9 +50,94 @@ return new Set(); | ||
| function markSessionProcessed(sessionId) { | ||
| fs.mkdirSync(path.dirname(PROCESSED_FILE), { recursive: true }); | ||
| fs.appendFileSync(PROCESSED_FILE, sessionId + '\n', 'utf-8'); | ||
| function markSessionProcessed(sessionId, source) { | ||
| const file = getProcessedFile(source); | ||
| fs.mkdirSync(path.dirname(file), { recursive: true }); | ||
| fs.appendFileSync(file, sessionId + '\n', 'utf-8'); | ||
| } | ||
| // --- Transcript discovery --- | ||
| // --- OpenClaw memory file discovery --- | ||
| function getOpenClawMemoryDir() { | ||
| // Check common locations for OpenClaw memory files | ||
| const candidates = [ | ||
| process.env.OPENCLAW_WORKSPACE && path.join(process.env.OPENCLAW_WORKSPACE, 'memory'), | ||
| path.join(os.homedir(), '.openclaw', 'workspace', 'memory'), | ||
| ].filter(Boolean); | ||
| for (const dir of candidates) { | ||
| if (fs.existsSync(dir)) return dir; | ||
| } | ||
| return null; | ||
| } | ||
| function findOpenClawMemoryFiles() { | ||
| const memDir = getOpenClawMemoryDir(); | ||
| if (!memDir) return []; | ||
| const files = []; | ||
| for (const file of fs.readdirSync(memDir)) { | ||
| if (!file.endsWith('.md')) continue; | ||
| const filePath = path.join(memDir, file); | ||
| let stat; | ||
| try { stat = fs.statSync(filePath); } catch { continue; } | ||
| if (!stat.isFile()) continue; | ||
| files.push(filePath); | ||
| } | ||
| return files.sort(); | ||
| } | ||
| function getOpenClawSessionId(filePath) { | ||
| return path.basename(filePath, '.md'); | ||
| } | ||
| function extractOpenClawSession(filePath) { | ||
| const sessionId = getOpenClawSessionId(filePath); | ||
| const content = fs.readFileSync(filePath, 'utf-8'); | ||
| // Extract date from filename (YYYY-MM-DD.md or YYYY-MM-DD-topic.md) | ||
| const dateMatch = sessionId.match(/^(\d{4}-\d{2}-\d{2})/); | ||
| const dateStr = dateMatch ? dateMatch[1] : ''; | ||
| const firstTs = dateStr ? `${dateStr}T00:00:00Z` : null; | ||
| // Parse the markdown into pseudo-messages (sections become context) | ||
| const messages = []; | ||
| const lines = content.split('\n'); | ||
| let currentSection = ''; | ||
| let sectionContent = []; | ||
| for (const line of lines) { | ||
| const headingMatch = line.match(/^#{1,3}\s+(.+)/); | ||
| if (headingMatch) { | ||
| // Flush previous section | ||
| if (currentSection && sectionContent.length) { | ||
| const text = sectionContent.join('\n').trim(); | ||
| if (text) { | ||
| messages.push({ role: 'user', text: `## ${currentSection}\n${text}`, time: '', timestamp: firstTs || '' }); | ||
| } | ||
| } | ||
| currentSection = headingMatch[1]; | ||
| sectionContent = []; | ||
| } else { | ||
| sectionContent.push(line); | ||
| } | ||
| } | ||
| // Flush last section | ||
| if (currentSection && sectionContent.length) { | ||
| const text = sectionContent.join('\n').trim(); | ||
| if (text) { | ||
| messages.push({ role: 'user', text: `## ${currentSection}\n${text}`, time: '', timestamp: firstTs || '' }); | ||
| } | ||
| } | ||
| return { | ||
| session_id: sessionId, | ||
| project: 'openclaw', | ||
| cwd: null, | ||
| first_ts: firstTs, | ||
| last_ts: firstTs, | ||
| spans_multiple_days: false, | ||
| session_date: firstTs, | ||
| messages, | ||
| }; | ||
| } | ||
| // --- Claude Code transcript discovery --- | ||
| function findAllTranscripts() { | ||
@@ -259,3 +367,3 @@ const transcripts = []; | ||
| function cmdExtract(filePath) { | ||
| function cmdExtract(filePath, source) { | ||
| if (!fs.existsSync(filePath)) { | ||
@@ -265,20 +373,40 @@ process.stderr.write(`Error: file not found: ${filePath}\n`); | ||
| } | ||
| const session = extractSession(filePath); | ||
| const session = source === 'openclaw' | ||
| ? extractOpenClawSession(filePath) | ||
| : extractSession(filePath); | ||
| console.log(formatSessionMarkdown(session)); | ||
| } | ||
| function cmdListUnprocessed(projectFilter) { | ||
| const processed = getProcessedSessions(); | ||
| const transcripts = findAllTranscripts(); | ||
| function cmdListUnprocessed(projectFilter, source) { | ||
| // If source specified, only that source. Otherwise, all available sources. | ||
| const sourcesToCheck = source ? [source] : Object.keys(SOURCES); | ||
| const unprocessed = []; | ||
| for (const t of transcripts) { | ||
| const sid = getSessionId(t); | ||
| if (processed.has(sid)) continue; | ||
| const project = getProjectName(t); | ||
| if (projectFilter && !project.toLowerCase().includes(projectFilter.toLowerCase())) continue; | ||
| let size; | ||
| try { size = fs.statSync(t).size; } catch { continue; } | ||
| if (size < MIN_SESSION_SIZE) continue; | ||
| unprocessed.push({ path: t, project, size }); | ||
| for (const src of sourcesToCheck) { | ||
| const processed = getProcessedSessions(src); | ||
| if (src === 'openclaw') { | ||
| const memFiles = findOpenClawMemoryFiles(); | ||
| for (const f of memFiles) { | ||
| const sid = getOpenClawSessionId(f); | ||
| if (processed.has(sid)) continue; | ||
| let size; | ||
| try { size = fs.statSync(f).size; } catch { continue; } | ||
| if (size < MIN_SESSION_SIZE) continue; | ||
| unprocessed.push({ path: f, project: 'openclaw', size, source: src }); | ||
| } | ||
| } else if (src === 'claude-code') { | ||
| const transcripts = findAllTranscripts(); | ||
| for (const t of transcripts) { | ||
| const sid = getSessionId(t); | ||
| if (processed.has(sid)) continue; | ||
| const project = getProjectName(t); | ||
| if (projectFilter && !project.toLowerCase().includes(projectFilter.toLowerCase())) continue; | ||
| let size; | ||
| try { size = fs.statSync(t).size; } catch { continue; } | ||
| if (size < MIN_SESSION_SIZE) continue; | ||
| unprocessed.push({ path: t, project, size, source: src }); | ||
| } | ||
| } | ||
| } | ||
@@ -293,9 +421,11 @@ | ||
| if (!unprocessed.length) { | ||
| console.log('No unprocessed sessions found.'); | ||
| const label = source || 'all sources'; | ||
| console.log(`No unprocessed sessions found (${label}).`); | ||
| return; | ||
| } | ||
| console.log(`Found ${unprocessed.length} unprocessed session(s):\n`); | ||
| const label = source || 'all sources'; | ||
| console.log(`Found ${unprocessed.length} unprocessed session(s) [${label}]:\n`); | ||
| for (const u of unprocessed) { | ||
| const sid = getSessionId(u.path); | ||
| const sid = u.source === 'openclaw' ? getOpenClawSessionId(u.path) : getSessionId(u.path); | ||
| let mtime; | ||
@@ -310,2 +440,3 @@ try { | ||
| console.log(` ${sid}`); | ||
| console.log(` Source: ${u.source}`); | ||
| console.log(` Project: ${u.project}`); | ||
@@ -319,5 +450,5 @@ console.log(` Modified: ${mtime}`); | ||
| function cmdMarkProcessed(sessionId) { | ||
| markSessionProcessed(sessionId); | ||
| console.log(`Marked as processed: ${sessionId}`); | ||
| function cmdMarkProcessed(sessionId, source) { | ||
| markSessionProcessed(sessionId, source); | ||
| console.log(`Marked as processed: ${sessionId} (source: ${source})`); | ||
| } | ||
@@ -327,24 +458,49 @@ | ||
| function detectSourceFromPath(filePath) { | ||
| if (filePath.endsWith('.md')) return 'openclaw'; | ||
| if (filePath.endsWith('.jsonl')) return 'claude-code'; | ||
| return getDefaultSource(); | ||
| } | ||
| function main() { | ||
| const args = process.argv.slice(2); | ||
| if (!args.length) { | ||
| // Parse --source flag (can appear anywhere; optional) | ||
| const srcIdx = args.indexOf('--source'); | ||
| const explicitSource = srcIdx !== -1 && args[srcIdx + 1] ? args[srcIdx + 1] : null; | ||
| // Remove --source and its value from args | ||
| const cleanArgs = srcIdx !== -1 | ||
| ? args.filter((_, i) => i !== srcIdx && i !== srcIdx + 1) | ||
| : [...args]; | ||
| if (!cleanArgs.length) { | ||
| console.log('Usage:'); | ||
| console.log(' node extract.js <transcript.jsonl>'); | ||
| console.log(' node extract.js --unprocessed [--project <name>]'); | ||
| console.log(' node extract.js --mark-processed <session_id>'); | ||
| console.log(' node extract.js [--source claude-code|openclaw] <file>'); | ||
| console.log(' node extract.js [--source claude-code|openclaw] --unprocessed [--project <name>]'); | ||
| console.log(' node extract.js [--source claude-code|openclaw] --mark-processed <session_id>'); | ||
| console.log(''); | ||
| console.log('Without --source:'); | ||
| console.log(' --unprocessed scans ALL sources'); | ||
| console.log(' <file> auto-detects from extension (.md → openclaw, .jsonl → claude-code)'); | ||
| console.log(' --mark-processed requires --source'); | ||
| console.log(''); | ||
| console.log(`Sources: ${Object.keys(SOURCES).join(', ')}`); | ||
| console.log(`State dir: ${STATE_DIR}`); | ||
| process.exit(1); | ||
| } | ||
| if (args[0] === '--unprocessed') { | ||
| const projIdx = args.indexOf('--project'); | ||
| const projectFilter = projIdx !== -1 && args[projIdx + 1] ? args[projIdx + 1] : null; | ||
| cmdListUnprocessed(projectFilter); | ||
| } else if (args[0] === '--mark-processed') { | ||
| if (!args[1]) { | ||
| process.stderr.write('Usage: extract.js --mark-processed <session-id>\n'); | ||
| if (cleanArgs[0] === '--unprocessed') { | ||
| const projIdx = cleanArgs.indexOf('--project'); | ||
| const projectFilter = projIdx !== -1 && cleanArgs[projIdx + 1] ? cleanArgs[projIdx + 1] : null; | ||
| cmdListUnprocessed(projectFilter, explicitSource); | ||
| } else if (cleanArgs[0] === '--mark-processed') { | ||
| if (!cleanArgs[1]) { | ||
| process.stderr.write('Usage: extract.js --mark-processed <session-id> --source <source>\n'); | ||
| process.exit(1); | ||
| } | ||
| cmdMarkProcessed(args[1]); | ||
| const source = explicitSource || getDefaultSource(); | ||
| cmdMarkProcessed(cleanArgs[1], source); | ||
| } else { | ||
| cmdExtract(args[0]); | ||
| const source = explicitSource || detectSourceFromPath(cleanArgs[0]); | ||
| cmdExtract(cleanArgs[0], source); | ||
| } | ||
@@ -351,0 +507,0 @@ } |
+2
-25
@@ -73,29 +73,6 @@ --- | ||
| Create `{brain_path}/REMEMBER.md` with starter content: | ||
| **Skip if file exists** — user may have already customized it. | ||
| ```markdown | ||
| # REMEMBER.md | ||
| Read the template from `assets/templates/remember.md` (relative to plugin root) and write it **verbatim** to `{brain_path}/REMEMBER.md`. Do NOT add examples, explanations, or any other content — the sections must remain empty so the user fills them in. | ||
| Instructions for how your Second Brain captures and processes knowledge. | ||
| All sections are optional. | ||
| --- | ||
| ## Capture Rules | ||
| ## Processing | ||
| ## Custom Types | ||
| ## Connections | ||
| ## Language | ||
| ## Templates | ||
| ## Notes | ||
| ``` | ||
| **Skip if file exists** — user may have already customized it. | ||
| **Note:** This creates the global REMEMBER.md. Users can also create a project-level `REMEMBER.md` in any project root for project-specific rules that layer on top of global preferences. | ||
@@ -102,0 +79,0 @@ |
@@ -62,3 +62,5 @@ --- | ||
| With project filter: | ||
| This scans **all available sources** (Claude Code transcripts + OpenClaw memory files) and lists everything unprocessed. Each entry shows its `Source:` label. | ||
| With project filter (Claude Code only): | ||
| ```bash | ||
@@ -68,2 +70,8 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/extract.js --unprocessed --project <name> | ||
| To filter by source: | ||
| ```bash | ||
| node ${CLAUDE_PLUGIN_ROOT}/scripts/extract.js --source openclaw --unprocessed | ||
| node ${CLAUDE_PLUGIN_ROOT}/scripts/extract.js --source claude-code --unprocessed | ||
| ``` | ||
| Show the list. Ask user which to process: **All**, **specific sessions by number**, or **Skip**. | ||
@@ -75,5 +83,7 @@ | ||
| ```bash | ||
| node ${CLAUDE_PLUGIN_ROOT}/scripts/extract.js <transcript_path> | ||
| node ${CLAUDE_PLUGIN_ROOT}/scripts/extract.js <file_path> | ||
| ``` | ||
| Source is auto-detected from file extension (`.md` → OpenClaw, `.jsonl` → Claude Code). | ||
| Note the `**Session date (use for journal/tasks):**` line — use THAT date for everything (journal filenames, frontmatter, last_contact, task dates). Never use today's date. | ||
@@ -134,5 +144,7 @@ | ||
| ```bash | ||
| node ${CLAUDE_PLUGIN_ROOT}/scripts/extract.js --mark-processed <session_id> | ||
| node ${CLAUDE_PLUGIN_ROOT}/scripts/extract.js --source <source> --mark-processed <session_id> | ||
| ``` | ||
| Use `--source openclaw` for OpenClaw memory files, `--source claude-code` for Claude Code transcripts. | ||
| Report summary: | ||
@@ -139,0 +151,0 @@ ``` |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances
71581
9.28%1043
15.25%17
41.67%