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 +29 -10
- package/package.json +1 -1
- package/src/parsers/opencode.js +60 -0
- package/src/sanitizer.js +1 -1
- package/src/scanner.js +24 -2
- package/src/unified.js +8 -6
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:
|
|
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) ||
|
|
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:
|
|
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
|
|
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(
|
|
260
|
-
} else
|
|
261
|
-
|
|
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
|
@@ -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 =
|
|
1
|
+
const MAX_PAYLOAD_BYTES = 10 * 1024 * 1024; // 10MB
|
|
2
|
+
const MIN_SESSIONS = 500;
|
|
2
3
|
|
|
3
|
-
export function unifyMessages(claudeSessions, codexSessions, limit =
|
|
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
|
-
|
|
16
|
-
|
|
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 || ''),
|