truememory-mirror 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/mirror.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { scan } from '../src/scanner.js';
4
4
  import { parseClaudeCodeSession } from '../src/parsers/claude-code.js';
5
5
  import { parseCodexSession } from '../src/parsers/codex.js';
6
+ import { parseOpenCodeSession } from '../src/parsers/opencode.js';
6
7
  import { sanitizeMessages } from '../src/sanitizer.js';
7
8
  import { unifyMessages } from '../src/unified.js';
8
9
  import { submitForExtraction, pollStatus } from '../src/api-client.js';
@@ -34,7 +35,7 @@ ${DIM} ▓ truememory ▓${R}
34
35
 
35
36
  function parseArgs(argv) {
36
37
  const args = {
37
- limit: 500,
38
+ limit: 750,
38
39
  source: 'all',
39
40
  json: false,
40
41
  noOpen: false,
@@ -45,7 +46,7 @@ function parseArgs(argv) {
45
46
  for (let i = 2; i < argv.length; i++) {
46
47
  switch (argv[i]) {
47
48
  case '--limit':
48
- args.limit = parseInt(argv[++i], 10) || 500;
49
+ args.limit = parseInt(argv[++i], 10) || 750;
49
50
  break;
50
51
  case '--source':
51
52
  args.source = argv[++i] || 'all';
@@ -79,8 +80,8 @@ ${BANNER}
79
80
  ${WHITE}usage:${R} npx truememory-mirror [options]
80
81
 
81
82
  ${WHITE}options:${R}
82
- --limit <n> max sessions to analyze (default: 500)
83
- --source <type> claude, codex, or all (default: all)
83
+ --limit <n> max sessions to analyze (default: 750)
84
+ --source <type> claude, codex, opencode, or all (default: all)
84
85
  --json output raw profile JSON
85
86
  --no-open don't open browser automatically
86
87
  --delete <id> delete a profile by ID
@@ -185,6 +186,7 @@ async function main() {
185
186
 
186
187
  let claudeSessions = [];
187
188
  let codexSessions = [];
189
+ let opencodeSessions = [];
188
190
 
189
191
  if (sources.claude_code) {
190
192
  const files = sources.claude_code.sessions;
@@ -208,12 +210,24 @@ async function main() {
208
210
  }
209
211
  }
210
212
 
213
+ if (sources.opencode) {
214
+ const files = sources.opencode.sessions;
215
+ const storageDir = sources.opencode.path;
216
+ const parseLimit = Math.min(files.length, args.limit);
217
+ for (let i = 0; i < parseLimit; i++) {
218
+ try {
219
+ const session = await parseOpenCodeSession(files[i].path, storageDir);
220
+ if (session.messages.length > 0) opencodeSessions.push(session);
221
+ } catch {}
222
+ }
223
+ }
224
+
211
225
  clearInterval(spinTimer);
212
226
 
213
- const totalSessions = claudeSessions.length + codexSessions.length;
227
+ const totalSessions = claudeSessions.length + codexSessions.length + opencodeSessions.length;
214
228
  if (totalSessions === 0) {
215
229
  clearLine();
216
- write(` no AI conversations found.\n make sure you have Claude Code or Codex CLI installed.\n\n`);
230
+ write(` no AI conversations found.\n make sure you have Claude Code, Codex, or OpenCode installed.\n\n`);
217
231
  process.exit(1);
218
232
  }
219
233
 
@@ -224,8 +238,11 @@ async function main() {
224
238
  for (const session of codexSessions) {
225
239
  session.messages = sanitizeMessages(session.messages);
226
240
  }
241
+ for (const session of opencodeSessions) {
242
+ session.messages = sanitizeMessages(session.messages);
243
+ }
227
244
 
228
- const payload = unifyMessages(claudeSessions, codexSessions, args.limit);
245
+ const payload = unifyMessages(claudeSessions, codexSessions, opencodeSessions, args.limit);
229
246
 
230
247
  if (args.json) {
231
248
  process.stdout.write(JSON.stringify(payload, null, 2));
@@ -256,9 +273,11 @@ async function main() {
256
273
  const smoothTimer = setInterval(() => {
257
274
  if (done) return;
258
275
  if (displayPct < serverPct) {
259
- displayPct += Math.max(1, (serverPct - displayPct) * 0.3);
260
- } else if (displayPct < 95) {
261
- displayPct += 0.7;
276
+ displayPct += Math.max(0.5, (serverPct - displayPct) * 0.15);
277
+ } else {
278
+ const remaining = 99 - displayPct;
279
+ const crawl = remaining * 0.008;
280
+ if (crawl > 0.05) displayPct += crawl;
262
281
  }
263
282
  displayPct = Math.min(displayPct, 99);
264
283
  renderProgress();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truememory-mirror",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Behavioral prediction engine for how you think, decide, and react",
5
5
  "bin": {
6
6
  "truememory-mirror": "./bin/mirror.js"
@@ -0,0 +1,60 @@
1
+ import { readFile, readdir } from 'fs/promises';
2
+ import { join } from 'path';
3
+
4
+ export async function parseOpenCodeSession(sessionPath, storageDir) {
5
+ const raw = await readFile(sessionPath, 'utf-8');
6
+ const session = JSON.parse(raw);
7
+ const sessionId = session.id;
8
+ const project = session.directory || '';
9
+
10
+ const msgDir = join(storageDir, 'message', sessionId);
11
+ let msgFiles;
12
+ try {
13
+ msgFiles = await readdir(msgDir);
14
+ } catch {
15
+ return { session_id: sessionId, source: 'opencode', project, messages: [] };
16
+ }
17
+
18
+ const msgEntries = [];
19
+ for (const f of msgFiles) {
20
+ if (!f.endsWith('.json')) continue;
21
+ try {
22
+ const msgRaw = await readFile(join(msgDir, f), 'utf-8');
23
+ const msg = JSON.parse(msgRaw);
24
+ if (msg.role !== 'user' && msg.role !== 'assistant') continue;
25
+
26
+ const partDir = join(storageDir, 'part', msg.id);
27
+ let text = '';
28
+ try {
29
+ const partFiles = await readdir(partDir);
30
+ for (const pf of partFiles) {
31
+ if (!pf.endsWith('.json')) continue;
32
+ const partRaw = await readFile(join(partDir, pf), 'utf-8');
33
+ const part = JSON.parse(partRaw);
34
+ if (part.type === 'text' && part.text) {
35
+ text += (text ? '\n' : '') + part.text;
36
+ }
37
+ }
38
+ } catch {
39
+ // no parts directory
40
+ }
41
+
42
+ if (!text) continue;
43
+
44
+ const ts = msg.time?.created;
45
+ msgEntries.push({
46
+ role: msg.role,
47
+ content: text,
48
+ timestamp: ts ? new Date(ts).toISOString() : '',
49
+ _created: ts || 0,
50
+ });
51
+ } catch {
52
+ continue;
53
+ }
54
+ }
55
+
56
+ msgEntries.sort((a, b) => a._created - b._created);
57
+ const messages = msgEntries.map(({ _created, ...rest }) => rest);
58
+
59
+ return { session_id: sessionId, source: 'opencode', project, messages };
60
+ }
package/src/sanitizer.js CHANGED
@@ -30,7 +30,7 @@ export function sanitize(text) {
30
30
  text = text.replace(/\b[a-fA-F0-9]{40,}\b/g, '[TOKEN]');
31
31
 
32
32
  // --- API keys with known prefixes ---
33
- text = text.replace(/\b(sk-ant-api03-|sk-ant-|sk-proj-|sk-or-v1-|ghp_|gho_|github_pat_|ghu_|ghs_|pk_live_|sk_live_|pk_test_|sk_test_|xoxb-|xoxp-|xapp-|AKIA[A-Z0-9]|AGE-SECRET-KEY-|glpat-|pypi-|npm_)[a-zA-Z0-9_/+=.-]{10,}/g, '[API_KEY]');
33
+ text = text.replace(/\b(sk-ant-api03-|sk-ant-|sk-proj-|sk-or-v1-|ghp_|gho_|github_pat_|ghu_|ghs_|pk_live_|sk_live_|pk_test_|sk_test_|xoxb-|xoxp-|xapp-|AKIA[A-Z0-9]|AGE-SECRET-KEY-|glpat-|pypi-|npm_)\s*[a-zA-Z0-9_/+=.-]{10,}/g, '[API_KEY]');
34
34
  text = text.replace(/\bAIza[a-zA-Z0-9_-]{30,}\b/g, '[API_KEY]');
35
35
  text = text.replace(/\b(sk-|pk-)[a-zA-Z0-9_-]{20,}\b/g, '[API_KEY]');
36
36
  // AWS secret access keys (40-char base64 with slashes and plus)
package/src/scanner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readdir, stat } from 'fs/promises';
1
+ import { readdir, stat } from 'node:fs/promises';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
4
 
@@ -24,7 +24,7 @@ async function findJsonlFiles(dir) {
24
24
 
25
25
  export async function scan(sourceFilter = 'all') {
26
26
  const home = homedir();
27
- const result = { claude_code: null, codex: null };
27
+ const result = { claude_code: null, codex: null, opencode: null };
28
28
 
29
29
  if (sourceFilter === 'all' || sourceFilter === 'claude') {
30
30
  const claudeProjectsDir = join(home, '.claude', 'projects');
@@ -58,5 +58,27 @@ export async function scan(sourceFilter = 'all') {
58
58
  }
59
59
  }
60
60
 
61
+ if (sourceFilter === 'all' || sourceFilter === 'opencode') {
62
+ const opencodeStorageDir = join(home, '.local', 'share', 'opencode', 'storage');
63
+ const opencodeSessionDir = join(opencodeStorageDir, 'session', 'global');
64
+ try {
65
+ await stat(opencodeSessionDir);
66
+ const entries = await readdir(opencodeSessionDir);
67
+ const sessions = [];
68
+ for (const entry of entries) {
69
+ if (!entry.endsWith('.json')) continue;
70
+ const fullPath = join(opencodeSessionDir, entry);
71
+ const info = await stat(fullPath);
72
+ sessions.push({ path: fullPath, mtime: info.mtimeMs, size: info.size, storageDir: opencodeStorageDir });
73
+ }
74
+ sessions.sort((a, b) => b.mtime - a.mtime);
75
+ if (sessions.length > 0) {
76
+ result.opencode = { path: opencodeStorageDir, sessions };
77
+ }
78
+ } catch {
79
+ // ~/.local/share/opencode/storage doesn't exist
80
+ }
81
+ }
82
+
61
83
  return result;
62
84
  }
package/src/unified.js CHANGED
@@ -1,7 +1,8 @@
1
- const MAX_PAYLOAD_BYTES = 5 * 1024 * 1024; // 5MB
1
+ const MAX_PAYLOAD_BYTES = 10 * 1024 * 1024; // 10MB
2
+ const MIN_SESSIONS = 500;
2
3
 
3
- export function unifyMessages(claudeSessions, codexSessions, limit = 500) {
4
- const allSessions = [...claudeSessions, ...codexSessions];
4
+ export function unifyMessages(claudeSessions, codexSessions, opencodeSessions = [], limit = 750) {
5
+ const allSessions = [...claudeSessions, ...codexSessions, ...opencodeSessions];
5
6
 
6
7
  allSessions.sort((a, b) => {
7
8
  const aTime = a.messages[a.messages.length - 1]?.timestamp || '';
@@ -10,10 +11,10 @@ export function unifyMessages(claudeSessions, codexSessions, limit = 500) {
10
11
  });
11
12
 
12
13
  let selected = allSessions.slice(0, limit);
13
-
14
14
  let payload = buildPayload(selected);
15
- while (JSON.stringify(payload).length > MAX_PAYLOAD_BYTES && selected.length > 10) {
16
- selected = selected.slice(0, Math.floor(selected.length * 0.8));
15
+
16
+ while (JSON.stringify(payload).length > MAX_PAYLOAD_BYTES && selected.length > MIN_SESSIONS) {
17
+ selected = selected.slice(0, Math.floor(selected.length * 0.9));
17
18
  payload = buildPayload(selected);
18
19
  }
19
20
 
@@ -39,6 +40,7 @@ function buildPayload(sessions) {
39
40
  total_sessions: sessions.length,
40
41
  claude_code_sessions: sessions.filter(s => s.source === 'claude_code').length,
41
42
  codex_sessions: sessions.filter(s => s.source === 'codex').length,
43
+ opencode_sessions: sessions.filter(s => s.source === 'opencode').length,
42
44
  total_messages: messages.length,
43
45
  date_range: {
44
46
  earliest: messages.reduce((min, m) => m.timestamp && m.timestamp < min ? m.timestamp : min, messages[0]?.timestamp || ''),