agentel 0.2.6 → 0.3.0
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/README.md +260 -79
- package/docs/code-reference.md +130 -42
- package/docs/history-source-handling.md +685 -153
- package/docs/release.md +35 -8
- package/npm-shrinkwrap.json +478 -0
- package/package.json +20 -4
- package/scripts/postinstall.js +156 -0
- package/src/archive.js +1342 -50
- package/src/canonical-events.js +346 -35
- package/src/cli.js +8835 -843
- package/src/collector.js +42 -4
- package/src/config.js +26 -4
- package/src/diffs.js +156 -0
- package/src/doctor.js +48 -5
- package/src/importers/claude.js +51 -4
- package/src/importers/copilot.js +385 -0
- package/src/importers/cursor-recovery.js +22 -0
- package/src/importers/factory.js +396 -0
- package/src/importers/gemini.js +41 -1
- package/src/importers/grok.js +367 -0
- package/src/importers/pi.js +422 -0
- package/src/importers/providers.js +64 -5
- package/src/importers.js +6429 -747
- package/src/mcp.js +1 -0
- package/src/memory-sources.js +671 -0
- package/src/memory-store.js +0 -0
- package/src/parser-versions.js +13 -0
- package/src/pricing.js +84 -0
- package/src/search.js +641 -215
- package/src/session-store.js +405 -0
- package/src/source-watch.js +293 -0
- package/src/sources.js +60 -11
- package/src/supervisor.js +197 -9
- package/src/sync.js +6 -0
- package/src/unavailable-sources.js +358 -0
- package/src/web-export-instructions.js +6 -4
package/src/mcp.js
CHANGED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
|
+
const { listSessions, readTranscript } = require("./archive");
|
|
8
|
+
const { loadConfig } = require("./config");
|
|
9
|
+
const { canonicalRepo } = require("./repo");
|
|
10
|
+
const { recordMemoryObservation } = require("./memory-store");
|
|
11
|
+
|
|
12
|
+
// One backup pass over every provider memory store we can reach. Each source
|
|
13
|
+
// is independent: a missing directory or a locked database skips that source
|
|
14
|
+
// instead of failing the run. Returns per-source summaries for the CLI.
|
|
15
|
+
const MAX_MEMORY_FILE_BYTES = 5 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
const CHATGPT_PLATFORM_NOTE =
|
|
18
|
+
"ChatGPT memories cannot be edited here; manage them in ChatGPT under Settings > Personalization > Manage memories.";
|
|
19
|
+
const CLAUDE_WEB_PLATFORM_NOTE =
|
|
20
|
+
"Claude.ai memories cannot be edited here; manage them in your Claude.ai project or profile settings.";
|
|
21
|
+
const CURSOR_PLATFORM_NOTE =
|
|
22
|
+
"Cursor memories live in Cursor's cloud; manage them in Cursor under Settings > Rules > Memories.";
|
|
23
|
+
const DEVIN_PLATFORM_NOTE =
|
|
24
|
+
"Devin knowledge lives in Devin's cloud; manage it at app.devin.ai under Knowledge.";
|
|
25
|
+
|
|
26
|
+
function userHome(env = process.env) {
|
|
27
|
+
return env && env.HOME ? env.HOME : os.homedir();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function codexHome(env = process.env) {
|
|
31
|
+
return env.CODEX_HOME || path.join(userHome(env), ".codex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function geminiHome(env = process.env) {
|
|
35
|
+
return env.AGENTLOG_GEMINI_DIR || path.join(userHome(env), ".gemini");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function antigravityHome(env = process.env) {
|
|
39
|
+
return env.AGENTLOG_ANTIGRAVITY_DIR || path.join(geminiHome(env), "antigravity");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function antigravityCliHome(env = process.env) {
|
|
43
|
+
return env.AGENTLOG_ANTIGRAVITY_CLI_HOME_DIR || path.join(geminiHome(env), "antigravity-cli");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function antigravityIdeHome(env = process.env) {
|
|
47
|
+
return env.AGENTLOG_ANTIGRAVITY_IDE_HOME_DIR || path.join(geminiHome(env), "antigravity-ide");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function grokHome(env = process.env) {
|
|
51
|
+
return env.AGENTLOG_GROK_HOME || env.GROK_HOME || path.join(userHome(env), ".grok");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cursorGlobalStorageDir(env = process.env) {
|
|
55
|
+
return (
|
|
56
|
+
env.AGENTLOG_CURSOR_GLOBAL_STORAGE_DIR ||
|
|
57
|
+
path.join(userHome(env), "Library", "Application Support", "Cursor", "User", "globalStorage")
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function contentTypeForFile(filePath) {
|
|
62
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
63
|
+
if (ext === ".md" || ext === ".mdx") return "markdown";
|
|
64
|
+
if (ext === ".json") return "json";
|
|
65
|
+
if ([".pb", ".bin", ".sqlite", ".db", ".png", ".jpg", ".jpeg", ".gif", ".pdf"].includes(ext)) return "binary";
|
|
66
|
+
return "text";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function listFilesRecursive(dir, depthLimit = 6) {
|
|
70
|
+
const files = [];
|
|
71
|
+
const stack = [{ dir, depth: 0 }];
|
|
72
|
+
while (stack.length) {
|
|
73
|
+
const { dir: current, depth } = stack.pop();
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.name.startsWith(".")) continue;
|
|
82
|
+
const full = path.join(current, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
if (depth < depthLimit) stack.push({ dir: full, depth: depth + 1 });
|
|
85
|
+
} else if (entry.isFile() && !entry.name.endsWith(".lock")) {
|
|
86
|
+
files.push(full);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return files.sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function observeFile(filePath, base, env) {
|
|
94
|
+
let stat;
|
|
95
|
+
try {
|
|
96
|
+
stat = fs.statSync(filePath);
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (!stat.isFile() || stat.size > MAX_MEMORY_FILE_BYTES) return null;
|
|
101
|
+
const contentType = contentTypeForFile(filePath);
|
|
102
|
+
const content = fs.readFileSync(filePath);
|
|
103
|
+
if (contentType !== "binary" && !content.toString("utf8").trim()) return null;
|
|
104
|
+
return recordMemoryObservation(
|
|
105
|
+
{
|
|
106
|
+
...base,
|
|
107
|
+
origin: "file",
|
|
108
|
+
sourceKind: "fs-walk",
|
|
109
|
+
livePath: filePath,
|
|
110
|
+
itemKey: filePath,
|
|
111
|
+
name: path.basename(filePath),
|
|
112
|
+
contentType,
|
|
113
|
+
content,
|
|
114
|
+
modifiedAt: stat.mtime.toISOString()
|
|
115
|
+
},
|
|
116
|
+
env
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ~/.claude/projects/<slug>/memory encodes the project cwd with "/" and other
|
|
121
|
+
// separators flattened to "-", which is ambiguous for directory names that
|
|
122
|
+
// contain dashes. Resolve by walking the filesystem and preferring the
|
|
123
|
+
// longest dash-joined segment that actually exists at each level.
|
|
124
|
+
function decodeClaudeProjectSlug(slug) {
|
|
125
|
+
const tokens = String(slug || "").replace(/^-/, "").split("-").filter(Boolean);
|
|
126
|
+
if (!tokens.length) return "";
|
|
127
|
+
return decodeSlugWalk(path.sep, tokens, 0, { steps: 0 }) || "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function decodeSlugWalk(current, tokens, index, limit) {
|
|
131
|
+
if (limit.steps > 4000) return null;
|
|
132
|
+
if (index >= tokens.length) {
|
|
133
|
+
return fs.existsSync(current) ? current : null;
|
|
134
|
+
}
|
|
135
|
+
const joins = [];
|
|
136
|
+
let acc = "";
|
|
137
|
+
for (let next = index; next < tokens.length; next++) {
|
|
138
|
+
acc = acc ? `${acc}-${tokens[next]}` : tokens[next];
|
|
139
|
+
joins.push({ candidate: path.join(current, acc), next: next + 1 });
|
|
140
|
+
}
|
|
141
|
+
// Longest join first: "fun-potholes" should beat "fun/potholes".
|
|
142
|
+
for (const { candidate, next } of joins.reverse()) {
|
|
143
|
+
limit.steps += 1;
|
|
144
|
+
if (!fs.existsSync(candidate)) continue;
|
|
145
|
+
const result = decodeSlugWalk(candidate, tokens, next, limit);
|
|
146
|
+
if (result) return result;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Claude Code project slugs flatten every non-alphanumeric character to "-".
|
|
152
|
+
// Sessions in the archive remember both the original cwd and its canonical
|
|
153
|
+
// repo key, so a slug can be resolved even after the project directory was
|
|
154
|
+
// deleted.
|
|
155
|
+
function claudeProjectSlugIndex(env) {
|
|
156
|
+
const index = new Map();
|
|
157
|
+
let sessions = [];
|
|
158
|
+
try {
|
|
159
|
+
sessions = listSessions(env);
|
|
160
|
+
} catch {
|
|
161
|
+
sessions = [];
|
|
162
|
+
}
|
|
163
|
+
for (const session of sessions) {
|
|
164
|
+
const cwd = String(session.cwd || "");
|
|
165
|
+
if (!cwd) continue;
|
|
166
|
+
const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
167
|
+
if (!index.has(slug)) index.set(slug, { cwd, repo: String(session.repoCanonical || "") });
|
|
168
|
+
}
|
|
169
|
+
return index;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function backupClaudeCodeMemories(env, summaries) {
|
|
173
|
+
const claudeDir = path.join(userHome(env), ".claude");
|
|
174
|
+
const summary = sourceSummary("claude-code");
|
|
175
|
+
const globalTargets = [
|
|
176
|
+
path.join(claudeDir, "CLAUDE.md"),
|
|
177
|
+
...listFilesRecursive(path.join(claudeDir, "memory")),
|
|
178
|
+
...listFilesRecursive(path.join(claudeDir, "memories"))
|
|
179
|
+
];
|
|
180
|
+
for (const file of globalTargets) {
|
|
181
|
+
tally(summary, observeFile(file, { provider: "claude_code", source: "claude-code-memory", scope: "global" }, env));
|
|
182
|
+
}
|
|
183
|
+
const projectsDir = path.join(claudeDir, "projects");
|
|
184
|
+
let slugs = [];
|
|
185
|
+
try {
|
|
186
|
+
slugs = fs.readdirSync(projectsDir);
|
|
187
|
+
} catch {
|
|
188
|
+
slugs = [];
|
|
189
|
+
}
|
|
190
|
+
let slugIndex = null;
|
|
191
|
+
for (const slug of slugs) {
|
|
192
|
+
const memoryDir = path.join(projectsDir, slug, "memory");
|
|
193
|
+
const files = listFilesRecursive(memoryDir);
|
|
194
|
+
if (!files.length) continue;
|
|
195
|
+
const decoded = decodeClaudeProjectSlug(slug, env);
|
|
196
|
+
if (!slugIndex) slugIndex = claudeProjectSlugIndex(env);
|
|
197
|
+
const fromSessions = slugIndex.get(slug) || null;
|
|
198
|
+
const projectPath = decoded || fromSessions?.cwd || "";
|
|
199
|
+
const repo = decoded ? canonicalRepo(decoded).key : fromSessions?.repo || "";
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
tally(
|
|
202
|
+
summary,
|
|
203
|
+
observeFile(
|
|
204
|
+
file,
|
|
205
|
+
{
|
|
206
|
+
provider: "claude_code",
|
|
207
|
+
source: "claude-code-project-memory",
|
|
208
|
+
scope: "project",
|
|
209
|
+
repo,
|
|
210
|
+
projectPath: projectPath || slug
|
|
211
|
+
},
|
|
212
|
+
env
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
summaries.push(summary);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// In-repo agent instruction files (AGENTS.md, CLAUDE.md, rules dirs) for
|
|
221
|
+
// every project the sessions archive knows about and that still exists on
|
|
222
|
+
// disk. They are git-covered in their repos, but backing them up gives the
|
|
223
|
+
// Memories tab one place to read and edit them next to provider memories.
|
|
224
|
+
const REPO_RULES_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursorrules", ".windsurfrules"];
|
|
225
|
+
// Agents also honor scoped instruction files one level down (./src/GEMINI.md,
|
|
226
|
+
// nested CLAUDE.md); a single readdir per project keeps that sweep cheap.
|
|
227
|
+
const REPO_RULES_SUBDIR_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"];
|
|
228
|
+
const REPO_RULES_SUBDIR_LIMIT = 50;
|
|
229
|
+
const REPO_RULES_SKIP_DIRS = new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage", "tmp"]);
|
|
230
|
+
const REPO_RULES_PROJECT_LIMIT = 300;
|
|
231
|
+
|
|
232
|
+
function backupRepoRulesFiles(env, summaries) {
|
|
233
|
+
const summary = sourceSummary("repo-rules");
|
|
234
|
+
let sessions = [];
|
|
235
|
+
try {
|
|
236
|
+
sessions = listSessions(env);
|
|
237
|
+
} catch {
|
|
238
|
+
sessions = [];
|
|
239
|
+
}
|
|
240
|
+
const projects = new Map();
|
|
241
|
+
for (const session of sessions) {
|
|
242
|
+
const cwd = String(session.cwd || "");
|
|
243
|
+
if (!cwd || projects.has(cwd)) continue;
|
|
244
|
+
try {
|
|
245
|
+
if (!fs.statSync(cwd).isDirectory()) continue;
|
|
246
|
+
} catch {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
projects.set(cwd, String(session.repoCanonical || ""));
|
|
250
|
+
if (projects.size >= REPO_RULES_PROJECT_LIMIT) break;
|
|
251
|
+
}
|
|
252
|
+
for (const [cwd, repo] of projects) {
|
|
253
|
+
const base = { provider: "project", source: "repo-rules", scope: "project", repo, projectPath: cwd };
|
|
254
|
+
for (const name of REPO_RULES_FILES) {
|
|
255
|
+
tally(summary, observeFile(path.join(cwd, name), base, env));
|
|
256
|
+
}
|
|
257
|
+
for (const file of listFilesRecursive(path.join(cwd, ".cursor", "rules"), 1)) {
|
|
258
|
+
tally(summary, observeFile(file, base, env));
|
|
259
|
+
}
|
|
260
|
+
let subdirs = [];
|
|
261
|
+
try {
|
|
262
|
+
subdirs = fs.readdirSync(cwd, { withFileTypes: true });
|
|
263
|
+
} catch {
|
|
264
|
+
subdirs = [];
|
|
265
|
+
}
|
|
266
|
+
let scanned = 0;
|
|
267
|
+
for (const entry of subdirs) {
|
|
268
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || REPO_RULES_SKIP_DIRS.has(entry.name)) continue;
|
|
269
|
+
if (++scanned > REPO_RULES_SUBDIR_LIMIT) break;
|
|
270
|
+
for (const name of REPO_RULES_SUBDIR_FILES) {
|
|
271
|
+
tally(summary, observeFile(path.join(cwd, entry.name, name), base, env));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
summaries.push(summary);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function backupCodexMemories(env, summaries) {
|
|
279
|
+
const summary = sourceSummary("codex");
|
|
280
|
+
const home = codexHome(env);
|
|
281
|
+
const targets = [
|
|
282
|
+
path.join(home, "AGENTS.md"),
|
|
283
|
+
...listFilesRecursive(path.join(home, "memory")),
|
|
284
|
+
...listFilesRecursive(path.join(home, "memories"))
|
|
285
|
+
];
|
|
286
|
+
for (const file of targets) {
|
|
287
|
+
tally(summary, observeFile(file, { provider: "codex", source: "codex-memory", scope: "global" }, env));
|
|
288
|
+
}
|
|
289
|
+
summaries.push(summary);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Gemini CLI keeps private per-project memory under
|
|
293
|
+
// ~/.gemini/tmp/<project-basename>/memory/ (an index MEMORY.md plus fact
|
|
294
|
+
// files). The tmp dir only records the project's directory name, so the
|
|
295
|
+
// owning repo is resolved through the sessions archive.
|
|
296
|
+
function projectBasenameIndex(env) {
|
|
297
|
+
const index = new Map();
|
|
298
|
+
let sessions = [];
|
|
299
|
+
try {
|
|
300
|
+
sessions = listSessions(env);
|
|
301
|
+
} catch {
|
|
302
|
+
sessions = [];
|
|
303
|
+
}
|
|
304
|
+
for (const session of sessions) {
|
|
305
|
+
const cwd = String(session.cwd || "");
|
|
306
|
+
if (!cwd) continue;
|
|
307
|
+
const base = path.basename(cwd);
|
|
308
|
+
if (base && !index.has(base)) index.set(base, { cwd, repo: String(session.repoCanonical || "") });
|
|
309
|
+
}
|
|
310
|
+
return index;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function backupGeminiMemories(env, summaries) {
|
|
314
|
+
const summary = sourceSummary("gemini");
|
|
315
|
+
tally(
|
|
316
|
+
summary,
|
|
317
|
+
observeFile(path.join(geminiHome(env), "GEMINI.md"), { provider: "gemini_cli", source: "gemini-memory", scope: "global" }, env)
|
|
318
|
+
);
|
|
319
|
+
const tmpRoot = path.join(geminiHome(env), "tmp");
|
|
320
|
+
let entries = [];
|
|
321
|
+
try {
|
|
322
|
+
entries = fs.readdirSync(tmpRoot, { withFileTypes: true });
|
|
323
|
+
} catch {
|
|
324
|
+
entries = [];
|
|
325
|
+
}
|
|
326
|
+
let basenames = null;
|
|
327
|
+
for (const entry of entries) {
|
|
328
|
+
if (!entry.isDirectory()) continue;
|
|
329
|
+
const files = listFilesRecursive(path.join(tmpRoot, entry.name, "memory"));
|
|
330
|
+
if (!files.length) continue;
|
|
331
|
+
if (!basenames) basenames = projectBasenameIndex(env);
|
|
332
|
+
const match = basenames.get(entry.name) || null;
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
tally(
|
|
335
|
+
summary,
|
|
336
|
+
observeFile(
|
|
337
|
+
file,
|
|
338
|
+
{
|
|
339
|
+
provider: "gemini_cli",
|
|
340
|
+
source: "gemini-project-memory",
|
|
341
|
+
scope: "project",
|
|
342
|
+
repo: match?.repo || "",
|
|
343
|
+
projectPath: match?.cwd || entry.name
|
|
344
|
+
},
|
|
345
|
+
env
|
|
346
|
+
)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
summaries.push(summary);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Grok Build cross-session memory (--experimental-memory) keeps three styles
|
|
354
|
+
// of Markdown under ~/.grok/memory/: a global MEMORY.md, per-workspace
|
|
355
|
+
// <project-slug>-<hash8>/MEMORY.md, and dated /flush session logs under
|
|
356
|
+
// <workspace>/sessions/. The hash8 is derived from the git remote URL, so
|
|
357
|
+
// workspaces map back to projects only by slug basename. The SQLite search
|
|
358
|
+
// index is derived data and skipped.
|
|
359
|
+
function backupGrokMemories(env, summaries) {
|
|
360
|
+
const summary = sourceSummary("grok");
|
|
361
|
+
const memoryRoot = path.join(grokHome(env), "memory");
|
|
362
|
+
tally(summary, observeFile(path.join(memoryRoot, "MEMORY.md"), { provider: "grok", source: "grok-memory", scope: "global" }, env));
|
|
363
|
+
let entries = [];
|
|
364
|
+
try {
|
|
365
|
+
entries = fs.readdirSync(memoryRoot, { withFileTypes: true });
|
|
366
|
+
} catch {
|
|
367
|
+
entries = [];
|
|
368
|
+
}
|
|
369
|
+
let basenames = null;
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
if (!entry.isDirectory()) continue;
|
|
372
|
+
const files = listFilesRecursive(path.join(memoryRoot, entry.name)).filter((file) => contentTypeForFile(file) !== "binary");
|
|
373
|
+
if (!files.length) continue;
|
|
374
|
+
if (!basenames) basenames = projectBasenameIndex(env);
|
|
375
|
+
const slug = entry.name.replace(/-[0-9a-f]{8}$/i, "");
|
|
376
|
+
const match = basenames.get(slug) || null;
|
|
377
|
+
for (const file of files) {
|
|
378
|
+
tally(
|
|
379
|
+
summary,
|
|
380
|
+
observeFile(
|
|
381
|
+
file,
|
|
382
|
+
{
|
|
383
|
+
provider: "grok",
|
|
384
|
+
source: "grok-project-memory",
|
|
385
|
+
scope: "project",
|
|
386
|
+
repo: match?.repo || "",
|
|
387
|
+
projectPath: match?.cwd || slug
|
|
388
|
+
},
|
|
389
|
+
env
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
summaries.push(summary);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function backupAntigravityMemories(env, summaries) {
|
|
398
|
+
const summary = sourceSummary("antigravity");
|
|
399
|
+
const home = antigravityHome(env);
|
|
400
|
+
for (const file of listFilesRecursive(path.join(home, "knowledge"))) {
|
|
401
|
+
tally(summary, observeFile(file, { provider: "antigravity", source: "antigravity-knowledge", scope: "global" }, env));
|
|
402
|
+
}
|
|
403
|
+
for (const file of listFilesRecursive(path.join(home, "implicit"), 1)) {
|
|
404
|
+
tally(summary, observeFile(file, { provider: "antigravity", source: "antigravity-implicit", scope: "global" }, env));
|
|
405
|
+
}
|
|
406
|
+
summaries.push(summary);
|
|
407
|
+
backupAntigravitySurfaceMemories(env, summaries, "antigravity_cli", antigravityCliHome(env), "antigravity-cli");
|
|
408
|
+
backupAntigravitySurfaceMemories(env, summaries, "antigravity_ide", antigravityIdeHome(env), "antigravity-ide");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// The Antigravity CLI and IDE keep their own knowledge/implicit memory stores
|
|
412
|
+
// under ~/.gemini/antigravity-cli and ~/.gemini/antigravity-ide, separate from
|
|
413
|
+
// the 2.0 app's home.
|
|
414
|
+
function backupAntigravitySurfaceMemories(env, summaries, provider, home, sourcePrefix) {
|
|
415
|
+
const summary = sourceSummary(provider);
|
|
416
|
+
for (const file of listFilesRecursive(path.join(home, "knowledge"))) {
|
|
417
|
+
tally(summary, observeFile(file, { provider, source: `${sourcePrefix}-knowledge`, scope: "global" }, env));
|
|
418
|
+
}
|
|
419
|
+
for (const file of listFilesRecursive(path.join(home, "implicit"), 1)) {
|
|
420
|
+
tally(summary, observeFile(file, { provider, source: `${sourcePrefix}-implicit`, scope: "global" }, env));
|
|
421
|
+
}
|
|
422
|
+
summaries.push(summary);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function readSqliteValue(dbPath, key) {
|
|
426
|
+
const query = `SELECT value FROM ItemTable WHERE key='${key.replace(/'/g, "''")}' LIMIT 1`;
|
|
427
|
+
const run = (target) =>
|
|
428
|
+
spawnSync("sqlite3", [target, "-json", query], { argv0: "agentlog-sqlite", encoding: "utf8", timeout: 20000, maxBuffer: 1024 * 1024 * 64 });
|
|
429
|
+
let result = run(dbPath);
|
|
430
|
+
if ((result.error || result.status !== 0) && fs.existsSync(dbPath)) {
|
|
431
|
+
// Cursor keeps the db open with WAL; query a point-in-time copy instead.
|
|
432
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "agentlog-memory-sqlite-"));
|
|
433
|
+
try {
|
|
434
|
+
const target = path.join(tmp, path.basename(dbPath));
|
|
435
|
+
fs.copyFileSync(dbPath, target);
|
|
436
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
437
|
+
try {
|
|
438
|
+
fs.copyFileSync(`${dbPath}${suffix}`, `${target}${suffix}`);
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
result = run(target);
|
|
442
|
+
if (result.error || result.status !== 0) return null;
|
|
443
|
+
return firstSqliteValue(result.stdout);
|
|
444
|
+
} finally {
|
|
445
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (result.error || result.status !== 0) return null;
|
|
449
|
+
return firstSqliteValue(result.stdout);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function firstSqliteValue(stdout) {
|
|
453
|
+
try {
|
|
454
|
+
const rows = JSON.parse(String(stdout || "").trim() || "[]");
|
|
455
|
+
return rows.length ? String(rows[0].value ?? "") : null;
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function backupCursorMemories(env, summaries) {
|
|
462
|
+
const summary = sourceSummary("cursor");
|
|
463
|
+
const dbPath = env.AGENTLOG_CURSOR_GLOBAL_STORAGE_DB || path.join(cursorGlobalStorageDir(env), "state.vscdb");
|
|
464
|
+
if (!fs.existsSync(dbPath)) {
|
|
465
|
+
summaries.push(summary);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const pending = readSqliteValue(dbPath, "cursorPendingMemories");
|
|
469
|
+
if (pending && pending.trim() && pending.trim() !== "[]") {
|
|
470
|
+
tally(
|
|
471
|
+
summary,
|
|
472
|
+
recordMemoryObservation(
|
|
473
|
+
{
|
|
474
|
+
provider: "cursor",
|
|
475
|
+
source: "cursor-pending-memories",
|
|
476
|
+
sourceKind: "sqlite-key",
|
|
477
|
+
origin: "provider-cloud",
|
|
478
|
+
scope: "global",
|
|
479
|
+
itemKey: "cursorPendingMemories",
|
|
480
|
+
name: "Cursor pending memories",
|
|
481
|
+
platformNote: CURSOR_PLATFORM_NOTE,
|
|
482
|
+
contentType: "json",
|
|
483
|
+
content: pending
|
|
484
|
+
},
|
|
485
|
+
env
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const personalContext = readSqliteValue(dbPath, "aicontext.personalContext");
|
|
490
|
+
if (personalContext && personalContext.trim()) {
|
|
491
|
+
tally(
|
|
492
|
+
summary,
|
|
493
|
+
recordMemoryObservation(
|
|
494
|
+
{
|
|
495
|
+
provider: "cursor",
|
|
496
|
+
source: "cursor-user-rules",
|
|
497
|
+
sourceKind: "sqlite-key",
|
|
498
|
+
origin: "provider-cloud",
|
|
499
|
+
scope: "global",
|
|
500
|
+
itemKey: "aicontext.personalContext",
|
|
501
|
+
name: "Cursor user rules",
|
|
502
|
+
platformNote: CURSOR_PLATFORM_NOTE,
|
|
503
|
+
contentType: "text",
|
|
504
|
+
content: personalContext
|
|
505
|
+
},
|
|
506
|
+
env
|
|
507
|
+
)
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
summaries.push(summary);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Claude.ai memories arrive through the official export and are already
|
|
514
|
+
// archived as claude-web-memory sessions; mirror them into memory records so
|
|
515
|
+
// the Memories tab sees them without a reimport.
|
|
516
|
+
function backupClaudeWebMemories(env, summaries) {
|
|
517
|
+
const summary = sourceSummary("claude-web");
|
|
518
|
+
let sessions = [];
|
|
519
|
+
try {
|
|
520
|
+
sessions = listSessions(env).filter((session) => session.sourceType === "claude-web-memory");
|
|
521
|
+
} catch {
|
|
522
|
+
sessions = [];
|
|
523
|
+
}
|
|
524
|
+
for (const session of sessions) {
|
|
525
|
+
let content = "";
|
|
526
|
+
try {
|
|
527
|
+
const messages = readTranscript(session.transcriptPath) || [];
|
|
528
|
+
content = messages.map((message) => String(message.content || "")).filter(Boolean).join("\n\n");
|
|
529
|
+
} catch {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (!content.trim()) continue;
|
|
533
|
+
tally(
|
|
534
|
+
summary,
|
|
535
|
+
recordMemoryObservation(
|
|
536
|
+
{
|
|
537
|
+
provider: "claude_web",
|
|
538
|
+
source: "claude-web-memory",
|
|
539
|
+
sourceKind: "export-import",
|
|
540
|
+
origin: "provider-cloud",
|
|
541
|
+
scope: "global",
|
|
542
|
+
itemKey: `${session.chatAccountId || ""}:${session.title || session.sessionId}`,
|
|
543
|
+
name: session.title || "Claude memory",
|
|
544
|
+
title: session.title || "",
|
|
545
|
+
account: session.chatAccountId ? { id: session.chatAccountId } : null,
|
|
546
|
+
platformNote: CLAUDE_WEB_PLATFORM_NOTE,
|
|
547
|
+
contentType: "markdown",
|
|
548
|
+
content
|
|
549
|
+
},
|
|
550
|
+
env
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
summaries.push(summary);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function backupDevinKnowledge(env, summaries) {
|
|
558
|
+
const summary = sourceSummary("devin");
|
|
559
|
+
const cfg = loadConfig(env);
|
|
560
|
+
const apiKey = env.DEVIN_API_KEY || cfg.memory?.devinApiKey || "";
|
|
561
|
+
if (!apiKey) {
|
|
562
|
+
summary.skipped = "no API key (set DEVIN_API_KEY or memory.devinApiKey in config)";
|
|
563
|
+
summaries.push(summary);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const endpoint = env.AGENTLOG_DEVIN_API_BASE || "https://api.devin.ai";
|
|
567
|
+
let payload;
|
|
568
|
+
try {
|
|
569
|
+
const response = await fetch(`${endpoint}/v1/knowledge`, {
|
|
570
|
+
headers: { authorization: `Bearer ${apiKey}` }
|
|
571
|
+
});
|
|
572
|
+
if (!response.ok) throw new Error(`devin knowledge request failed: ${response.status}`);
|
|
573
|
+
payload = await response.json();
|
|
574
|
+
} catch (error) {
|
|
575
|
+
summary.error = error.message;
|
|
576
|
+
summaries.push(summary);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const items = Array.isArray(payload?.knowledge) ? payload.knowledge : Array.isArray(payload) ? payload : [];
|
|
580
|
+
for (const item of items) {
|
|
581
|
+
if (!item || typeof item !== "object") continue;
|
|
582
|
+
const body = String(item.body || "").trim();
|
|
583
|
+
if (!body) continue;
|
|
584
|
+
const parts = [body];
|
|
585
|
+
if (item.trigger_description) parts.push(`Trigger: ${item.trigger_description}`);
|
|
586
|
+
if (item.pinned_repo) parts.push(`Pinned repo: ${item.pinned_repo}`);
|
|
587
|
+
tally(
|
|
588
|
+
summary,
|
|
589
|
+
recordMemoryObservation(
|
|
590
|
+
{
|
|
591
|
+
provider: "devin",
|
|
592
|
+
source: "devin-knowledge",
|
|
593
|
+
sourceKind: "api-poll",
|
|
594
|
+
origin: "provider-cloud",
|
|
595
|
+
scope: "global",
|
|
596
|
+
itemKey: String(item.id || item.name || body.slice(0, 80)),
|
|
597
|
+
name: String(item.name || item.id || "Devin knowledge"),
|
|
598
|
+
platformNote: DEVIN_PLATFORM_NOTE,
|
|
599
|
+
contentType: "markdown",
|
|
600
|
+
content: parts.join("\n\n"),
|
|
601
|
+
modifiedAt: firstIsoTimestamp(item.updated_at, item.created_at)
|
|
602
|
+
},
|
|
603
|
+
env
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
summaries.push(summary);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ChatGPT has no memory export or API; users paste from Settings >
|
|
611
|
+
// Personalization > Manage memories into a file and import it manually.
|
|
612
|
+
function importChatGptMemories(filePath, env = process.env, options = {}) {
|
|
613
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
614
|
+
if (!content.trim()) throw new Error(`no memory text found in ${filePath}`);
|
|
615
|
+
const result = recordMemoryObservation(
|
|
616
|
+
{
|
|
617
|
+
provider: "chatgpt",
|
|
618
|
+
source: "chatgpt-manual-memories",
|
|
619
|
+
sourceKind: "export-import",
|
|
620
|
+
origin: "manual-import",
|
|
621
|
+
scope: "global",
|
|
622
|
+
itemKey: options.account ? `chatgpt:${options.account}` : "chatgpt:default",
|
|
623
|
+
name: options.account ? `ChatGPT memories (${options.account})` : "ChatGPT memories",
|
|
624
|
+
account: options.account ? { id: options.account } : null,
|
|
625
|
+
platformNote: CHATGPT_PLATFORM_NOTE,
|
|
626
|
+
contentType: "markdown",
|
|
627
|
+
content
|
|
628
|
+
},
|
|
629
|
+
env
|
|
630
|
+
);
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function firstIsoTimestamp(...values) {
|
|
635
|
+
for (const value of values) {
|
|
636
|
+
if (!value) continue;
|
|
637
|
+
const date = new Date(value);
|
|
638
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
639
|
+
}
|
|
640
|
+
return "";
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function sourceSummary(source) {
|
|
644
|
+
return { source, items: 0, changed: 0, skipped: "", error: "" };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function tally(summary, result) {
|
|
648
|
+
if (!result) return;
|
|
649
|
+
summary.items += 1;
|
|
650
|
+
if (result.changed) summary.changed += 1;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function collectMemoryBackup(env = process.env) {
|
|
654
|
+
const summaries = [];
|
|
655
|
+
backupRepoRulesFiles(env, summaries);
|
|
656
|
+
backupClaudeCodeMemories(env, summaries);
|
|
657
|
+
backupCodexMemories(env, summaries);
|
|
658
|
+
backupGeminiMemories(env, summaries);
|
|
659
|
+
backupGrokMemories(env, summaries);
|
|
660
|
+
backupAntigravityMemories(env, summaries);
|
|
661
|
+
backupCursorMemories(env, summaries);
|
|
662
|
+
backupClaudeWebMemories(env, summaries);
|
|
663
|
+
await backupDevinKnowledge(env, summaries);
|
|
664
|
+
return summaries;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
module.exports = {
|
|
668
|
+
collectMemoryBackup,
|
|
669
|
+
decodeClaudeProjectSlug,
|
|
670
|
+
importChatGptMemories
|
|
671
|
+
};
|
|
Binary file
|