truememory-mirror 1.0.12 → 1.0.14

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));
@@ -255,12 +272,13 @@ async function main() {
255
272
 
256
273
  const smoothTimer = setInterval(() => {
257
274
  if (done) return;
275
+ const ceiling = Math.min(serverPct + 5, 99);
258
276
  if (displayPct < serverPct) {
259
- displayPct += Math.max(1, (serverPct - displayPct) * 0.3);
260
- } else if (displayPct < 95) {
261
- displayPct += 0.7;
277
+ displayPct += Math.max(0.3, (serverPct - displayPct) * 0.12);
278
+ } else if (displayPct < ceiling) {
279
+ displayPct += 0.15;
262
280
  }
263
- displayPct = Math.min(displayPct, 99);
281
+ displayPct = Math.min(displayPct, ceiling);
264
282
  renderProgress();
265
283
  }, 500);
266
284
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truememory-mirror",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Behavioral prediction engine for how you think, decide, and react",
5
5
  "bin": {
6
6
  "truememory-mirror": "./bin/mirror.js"
package/src/api-client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const POLL_INTERVAL_MS = 3000;
2
+ const MAX_POLL_RETRIES = 3;
2
3
 
3
4
  export async function submitForExtraction(payload, apiUrl) {
4
5
  const body = JSON.stringify(payload);
@@ -17,12 +18,26 @@ export async function submitForExtraction(payload, apiUrl) {
17
18
  }
18
19
 
19
20
  export async function pollStatus(profileId, apiUrl, onUpdate) {
21
+ let retries = 0;
20
22
  while (true) {
21
- const response = await fetch(`${apiUrl}/api/profile/${profileId}/status`);
23
+ let response;
24
+ try {
25
+ response = await fetch(`${apiUrl}/api/profile/${profileId}/status`);
26
+ } catch {
27
+ retries++;
28
+ if (retries > MAX_POLL_RETRIES) throw new Error('Lost connection to server');
29
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS * 2));
30
+ continue;
31
+ }
32
+
22
33
  if (!response.ok) {
23
- throw new Error(`Status check failed (${response.status})`);
34
+ retries++;
35
+ if (retries > MAX_POLL_RETRIES) throw new Error(`Status check failed (${response.status})`);
36
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS * 2));
37
+ continue;
24
38
  }
25
39
 
40
+ retries = 0;
26
41
  const status = await response.json();
27
42
  if (onUpdate) onUpdate(status);
28
43
 
@@ -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 || ''),