conare 0.3.1 → 0.3.3
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/dist/index.js +968 -753
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,584 @@ var __export = (target, all) => {
|
|
|
29
29
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
30
30
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
31
31
|
|
|
32
|
+
// src/ingest/shared.ts
|
|
33
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { join as join2 } from "node:path";
|
|
36
|
+
import { homedir as homedir2 } from "node:os";
|
|
37
|
+
function isNarration(text) {
|
|
38
|
+
const stripped = text.trim();
|
|
39
|
+
if (stripped.length < MIN_SUBSTANTIVE)
|
|
40
|
+
return true;
|
|
41
|
+
if (stripped.length <= 300 && NARRATION_RE.test(stripped))
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function cleanText(raw) {
|
|
46
|
+
let text = raw;
|
|
47
|
+
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
|
|
48
|
+
text = text.replace(/<attached-context[\s\S]*?<\/attached-context>/g, "");
|
|
49
|
+
text = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, "");
|
|
50
|
+
text = text.replace(/^\s*Base directory for this skill:[\s\S]*/g, "");
|
|
51
|
+
text = text.replace(/\nBase directory for this skill:[\s\S]*/g, "");
|
|
52
|
+
text = text.replace(/<!-- vibe-rules Integration -->[\s\S]*?<!-- \/vibe-rules Integration -->/g, "");
|
|
53
|
+
text = text.replace(/<good-behaviour>[\s\S]*?<\/good-behaviour>/g, "");
|
|
54
|
+
text = text.replace(/<frontend-design>[\s\S]*?<\/frontend-design>/g, "");
|
|
55
|
+
text = text.replace(/<architecture>[\s\S]*?<\/architecture>/g, "");
|
|
56
|
+
text = text.replace(/<task-notification>[\s\S]*?<\/task-notification>/g, "");
|
|
57
|
+
text = text.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, "");
|
|
58
|
+
text = text.replace(/<system_instruction>[\s\S]*?<\/system_instruction>/g, "");
|
|
59
|
+
return text.trim();
|
|
60
|
+
}
|
|
61
|
+
function createContentHash(content) {
|
|
62
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
63
|
+
}
|
|
64
|
+
function getIngested() {
|
|
65
|
+
try {
|
|
66
|
+
if (existsSync2(MANIFEST_PATH)) {
|
|
67
|
+
return JSON.parse(readFileSync2(MANIFEST_PATH, "utf-8"));
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
function markIngested(source, sessionIds) {
|
|
73
|
+
const manifest = getIngested();
|
|
74
|
+
const existing = new Set(manifest[source] || []);
|
|
75
|
+
for (const id of sessionIds)
|
|
76
|
+
existing.add(id);
|
|
77
|
+
manifest[source] = [...existing];
|
|
78
|
+
const dir = join2(homedir2(), ".conare");
|
|
79
|
+
if (!existsSync2(dir))
|
|
80
|
+
mkdirSync(dir, { recursive: true });
|
|
81
|
+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
82
|
+
}
|
|
83
|
+
function isIngested(source, sessionId) {
|
|
84
|
+
const manifest = getIngested();
|
|
85
|
+
return (manifest[source] || []).includes(sessionId);
|
|
86
|
+
}
|
|
87
|
+
function clearIngested(source) {
|
|
88
|
+
const manifest = getIngested();
|
|
89
|
+
if (source) {
|
|
90
|
+
delete manifest[source];
|
|
91
|
+
} else {
|
|
92
|
+
for (const key of Object.keys(manifest))
|
|
93
|
+
delete manifest[key];
|
|
94
|
+
}
|
|
95
|
+
const dir = join2(homedir2(), ".conare");
|
|
96
|
+
if (!existsSync2(dir))
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
99
|
+
}
|
|
100
|
+
var MANIFEST_PATH, MIN_SUBSTANTIVE = 200, NARRATION_RE;
|
|
101
|
+
var init_shared = __esm(() => {
|
|
102
|
+
MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
|
|
103
|
+
NARRATION_RE = /^[\s\n]*(Let me |Now let me |Now I['\u2019]|Now add |Now fix |Now replace |Now integrate |Now update |Now pass |Now clean |Now build|Update the |Builds clean|Deployed\.|Wait, I |Let['\u2019]s test |Good —|Great\.|Perfect\.|Alright|OK,? let me|I[''\u2019]ll |Starting |I need to |Need |I found |I read |I[''\u2019]ve (loaded|confirmed|verified)|Context loaded|Next (I[''\u2019]|step)|Deps confirm|Diff check|Still missing|I[''\u2019]ll (do|check|inspect|trace|run|grab|pull|read|verify))/;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/ingest/codebase.ts
|
|
107
|
+
var exports_codebase = {};
|
|
108
|
+
__export(exports_codebase, {
|
|
109
|
+
indexCodebase: () => indexCodebase,
|
|
110
|
+
getRemoteFilePath: () => getRemoteFilePath,
|
|
111
|
+
getChangedFiles: () => getChangedFiles,
|
|
112
|
+
detectProjectName: () => detectProjectName
|
|
113
|
+
});
|
|
114
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
115
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync2, existsSync as existsSync5 } from "node:fs";
|
|
116
|
+
import { join as join6, relative, extname, resolve, basename as basename3 } from "node:path";
|
|
117
|
+
import { execSync } from "node:child_process";
|
|
118
|
+
function parseGitignore(rootPath) {
|
|
119
|
+
const patterns = new Set;
|
|
120
|
+
const gitignorePath = join6(rootPath, ".gitignore");
|
|
121
|
+
if (!existsSync5(gitignorePath))
|
|
122
|
+
return patterns;
|
|
123
|
+
try {
|
|
124
|
+
const content = readFileSync6(gitignorePath, "utf-8");
|
|
125
|
+
for (const line of content.split(`
|
|
126
|
+
`)) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
129
|
+
continue;
|
|
130
|
+
patterns.add(trimmed.replace(/\/$/, ""));
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
return patterns;
|
|
134
|
+
}
|
|
135
|
+
function shouldIgnore(name, ignorePatterns) {
|
|
136
|
+
if (DEFAULT_IGNORE.has(name))
|
|
137
|
+
return true;
|
|
138
|
+
if (IGNORE_FILES.has(name))
|
|
139
|
+
return true;
|
|
140
|
+
if (name.startsWith(".env"))
|
|
141
|
+
return true;
|
|
142
|
+
if (ignorePatterns.has(name))
|
|
143
|
+
return true;
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function langFromExt(ext) {
|
|
147
|
+
return EXT_TO_LANG[ext] || ext.slice(1);
|
|
148
|
+
}
|
|
149
|
+
function formatFile(relPath, content, ext) {
|
|
150
|
+
const lang = langFromExt(ext);
|
|
151
|
+
return `# File: ${relPath}
|
|
152
|
+
|
|
153
|
+
\`\`\`${lang}
|
|
154
|
+
${content}
|
|
155
|
+
\`\`\``;
|
|
156
|
+
}
|
|
157
|
+
function detectProjectName(rootPath) {
|
|
158
|
+
const readers = [
|
|
159
|
+
() => {
|
|
160
|
+
const pkg = JSON.parse(readFileSync6(join6(rootPath, "package.json"), "utf-8"));
|
|
161
|
+
return typeof pkg.name === "string" ? pkg.name : null;
|
|
162
|
+
},
|
|
163
|
+
() => {
|
|
164
|
+
const content = readFileSync6(join6(rootPath, "Cargo.toml"), "utf-8");
|
|
165
|
+
return content.match(/^name\s*=\s*"([^"]+)"/m)?.[1] ?? null;
|
|
166
|
+
},
|
|
167
|
+
() => {
|
|
168
|
+
const content = readFileSync6(join6(rootPath, "pyproject.toml"), "utf-8");
|
|
169
|
+
return content.match(/^name\s*=\s*"([^"]+)"/m)?.[1] ?? null;
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
for (const reader of readers) {
|
|
173
|
+
try {
|
|
174
|
+
const name = reader();
|
|
175
|
+
if (name)
|
|
176
|
+
return name;
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
return basename3(resolve(rootPath));
|
|
180
|
+
}
|
|
181
|
+
function getChangedFiles(rootPath) {
|
|
182
|
+
try {
|
|
183
|
+
const committed = execSync("git diff --name-only HEAD 2>/dev/null || true", {
|
|
184
|
+
cwd: rootPath,
|
|
185
|
+
encoding: "utf-8",
|
|
186
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
187
|
+
}).trim();
|
|
188
|
+
const staged = execSync("git diff --name-only --cached 2>/dev/null || true", {
|
|
189
|
+
cwd: rootPath,
|
|
190
|
+
encoding: "utf-8",
|
|
191
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
192
|
+
}).trim();
|
|
193
|
+
const all = new Set;
|
|
194
|
+
for (const line of [...committed.split(`
|
|
195
|
+
`), ...staged.split(`
|
|
196
|
+
`)]) {
|
|
197
|
+
const f = line.trim();
|
|
198
|
+
if (f.length > 0)
|
|
199
|
+
all.add(f);
|
|
200
|
+
}
|
|
201
|
+
return [...all];
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function getRemoteFilePath(memory) {
|
|
207
|
+
const meta = memory.metadata;
|
|
208
|
+
if (!meta || typeof meta !== "object")
|
|
209
|
+
return null;
|
|
210
|
+
const fp = meta.filePath;
|
|
211
|
+
return typeof fp === "string" ? fp : null;
|
|
212
|
+
}
|
|
213
|
+
function indexCodebase(rootPath, options = {}) {
|
|
214
|
+
const absRoot = resolve(rootPath);
|
|
215
|
+
const repoHash = createHash2("sha256").update(absRoot).digest("hex").slice(0, 12);
|
|
216
|
+
const gitignorePatterns = parseGitignore(absRoot);
|
|
217
|
+
const memories = [];
|
|
218
|
+
const localFilePaths = [];
|
|
219
|
+
let fileCount = 0;
|
|
220
|
+
let skipped = 0;
|
|
221
|
+
const project = options.project || detectProjectName(absRoot);
|
|
222
|
+
const changedFiles = options.changedOnly ? getChangedFiles(absRoot) : null;
|
|
223
|
+
const changedSet = changedFiles ? new Set(changedFiles) : null;
|
|
224
|
+
if (options.changedOnly && changedSet !== null && changedSet.size === 0) {
|
|
225
|
+
return { memories, fileCount: 0, skipped: 0, localFilePaths: [], project };
|
|
226
|
+
}
|
|
227
|
+
function walk(dir) {
|
|
228
|
+
let entries;
|
|
229
|
+
try {
|
|
230
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
231
|
+
} catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (shouldIgnore(entry.name, gitignorePatterns))
|
|
236
|
+
continue;
|
|
237
|
+
const fullPath = join6(dir, entry.name);
|
|
238
|
+
if (entry.isDirectory()) {
|
|
239
|
+
walk(fullPath);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!entry.isFile())
|
|
243
|
+
continue;
|
|
244
|
+
const ext = extname(entry.name).toLowerCase();
|
|
245
|
+
if (!CODE_EXTENSIONS.has(ext) && !SPECIAL_FILES.has(entry.name))
|
|
246
|
+
continue;
|
|
247
|
+
const relPath = relative(absRoot, fullPath);
|
|
248
|
+
localFilePaths.push(relPath);
|
|
249
|
+
if (changedSet && !changedSet.has(relPath)) {
|
|
250
|
+
skipped++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
let stat;
|
|
254
|
+
try {
|
|
255
|
+
stat = statSync2(fullPath);
|
|
256
|
+
} catch {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (stat.size > MAX_FILE_SIZE || stat.size === 0) {
|
|
260
|
+
skipped++;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
let raw;
|
|
264
|
+
try {
|
|
265
|
+
raw = readFileSync6(fullPath, "utf-8");
|
|
266
|
+
} catch {
|
|
267
|
+
skipped++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (raw.includes("\x00")) {
|
|
271
|
+
skipped++;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const contentHash = createContentHash(raw);
|
|
275
|
+
const dedupKey = `codebase:${repoHash}:${relPath}`;
|
|
276
|
+
const fingerprint = `${dedupKey}:${contentHash}`;
|
|
277
|
+
if (isIngested("codebase", fingerprint)) {
|
|
278
|
+
skipped++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const content = formatFile(relPath, raw, ext);
|
|
282
|
+
const metadata = {
|
|
283
|
+
dedupKey,
|
|
284
|
+
contentHash,
|
|
285
|
+
source: "codebase-index",
|
|
286
|
+
repoHash,
|
|
287
|
+
filePath: relPath,
|
|
288
|
+
fileHash: `${relPath}:${contentHash}`,
|
|
289
|
+
language: langFromExt(ext),
|
|
290
|
+
project
|
|
291
|
+
};
|
|
292
|
+
memories.push({ content, containerTag: "codebase", metadata });
|
|
293
|
+
fileCount++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
walk(absRoot);
|
|
297
|
+
return { memories, fileCount, skipped, localFilePaths, project };
|
|
298
|
+
}
|
|
299
|
+
var DEFAULT_IGNORE, IGNORE_FILES, CODE_EXTENSIONS, SPECIAL_FILES, MAX_FILE_SIZE = 1e5, EXT_TO_LANG;
|
|
300
|
+
var init_codebase = __esm(() => {
|
|
301
|
+
init_shared();
|
|
302
|
+
DEFAULT_IGNORE = new Set([
|
|
303
|
+
"node_modules",
|
|
304
|
+
".git",
|
|
305
|
+
".next",
|
|
306
|
+
".nuxt",
|
|
307
|
+
".output",
|
|
308
|
+
"dist",
|
|
309
|
+
"build",
|
|
310
|
+
".cache",
|
|
311
|
+
"__pycache__",
|
|
312
|
+
".venv",
|
|
313
|
+
"venv",
|
|
314
|
+
"vendor",
|
|
315
|
+
"target",
|
|
316
|
+
".turbo",
|
|
317
|
+
".DS_Store",
|
|
318
|
+
".claude",
|
|
319
|
+
".conare"
|
|
320
|
+
]);
|
|
321
|
+
IGNORE_FILES = new Set([
|
|
322
|
+
"package-lock.json",
|
|
323
|
+
"bun.lockb",
|
|
324
|
+
"yarn.lock",
|
|
325
|
+
"pnpm-lock.yaml"
|
|
326
|
+
]);
|
|
327
|
+
CODE_EXTENSIONS = new Set([
|
|
328
|
+
".ts",
|
|
329
|
+
".tsx",
|
|
330
|
+
".js",
|
|
331
|
+
".jsx",
|
|
332
|
+
".mjs",
|
|
333
|
+
".cjs",
|
|
334
|
+
".vue",
|
|
335
|
+
".svelte",
|
|
336
|
+
".astro",
|
|
337
|
+
".py",
|
|
338
|
+
".rb",
|
|
339
|
+
".go",
|
|
340
|
+
".rs",
|
|
341
|
+
".java",
|
|
342
|
+
".kt",
|
|
343
|
+
".swift",
|
|
344
|
+
".c",
|
|
345
|
+
".cpp",
|
|
346
|
+
".h",
|
|
347
|
+
".css",
|
|
348
|
+
".scss",
|
|
349
|
+
".less",
|
|
350
|
+
".html",
|
|
351
|
+
".json",
|
|
352
|
+
".yaml",
|
|
353
|
+
".yml",
|
|
354
|
+
".toml",
|
|
355
|
+
".md",
|
|
356
|
+
".mdx",
|
|
357
|
+
".txt",
|
|
358
|
+
".sql",
|
|
359
|
+
".graphql",
|
|
360
|
+
".prisma",
|
|
361
|
+
".sh",
|
|
362
|
+
".bash",
|
|
363
|
+
".zsh",
|
|
364
|
+
".lua",
|
|
365
|
+
".zig",
|
|
366
|
+
".ex",
|
|
367
|
+
".exs"
|
|
368
|
+
]);
|
|
369
|
+
SPECIAL_FILES = new Set([
|
|
370
|
+
"Dockerfile",
|
|
371
|
+
"Makefile",
|
|
372
|
+
"Procfile",
|
|
373
|
+
"Justfile",
|
|
374
|
+
".gitignore",
|
|
375
|
+
".dockerignore",
|
|
376
|
+
"CLAUDE.md",
|
|
377
|
+
"AGENTS.md",
|
|
378
|
+
"README.md",
|
|
379
|
+
"ARCHITECTURE.md",
|
|
380
|
+
"wrangler.toml",
|
|
381
|
+
"wrangler.json"
|
|
382
|
+
]);
|
|
383
|
+
EXT_TO_LANG = {
|
|
384
|
+
".ts": "typescript",
|
|
385
|
+
".tsx": "tsx",
|
|
386
|
+
".js": "javascript",
|
|
387
|
+
".jsx": "jsx",
|
|
388
|
+
".mjs": "javascript",
|
|
389
|
+
".cjs": "javascript",
|
|
390
|
+
".vue": "vue",
|
|
391
|
+
".svelte": "svelte",
|
|
392
|
+
".astro": "astro",
|
|
393
|
+
".py": "python",
|
|
394
|
+
".rb": "ruby",
|
|
395
|
+
".go": "go",
|
|
396
|
+
".rs": "rust",
|
|
397
|
+
".java": "java",
|
|
398
|
+
".kt": "kotlin",
|
|
399
|
+
".swift": "swift",
|
|
400
|
+
".c": "c",
|
|
401
|
+
".cpp": "cpp",
|
|
402
|
+
".h": "c",
|
|
403
|
+
".css": "css",
|
|
404
|
+
".scss": "scss",
|
|
405
|
+
".less": "less",
|
|
406
|
+
".html": "html",
|
|
407
|
+
".json": "json",
|
|
408
|
+
".yaml": "yaml",
|
|
409
|
+
".yml": "yaml",
|
|
410
|
+
".toml": "toml",
|
|
411
|
+
".md": "markdown",
|
|
412
|
+
".mdx": "mdx",
|
|
413
|
+
".sql": "sql",
|
|
414
|
+
".graphql": "graphql",
|
|
415
|
+
".prisma": "prisma",
|
|
416
|
+
".sh": "bash",
|
|
417
|
+
".bash": "bash",
|
|
418
|
+
".zsh": "zsh",
|
|
419
|
+
".lua": "lua",
|
|
420
|
+
".zig": "zig",
|
|
421
|
+
".ex": "elixir",
|
|
422
|
+
".exs": "elixir"
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// src/api.ts
|
|
427
|
+
var exports_api = {};
|
|
428
|
+
__export(exports_api, {
|
|
429
|
+
validateKey: () => validateKey,
|
|
430
|
+
uploadBulk: () => uploadBulk,
|
|
431
|
+
listRemoteMemories: () => listRemoteMemories,
|
|
432
|
+
getRemoteMemoryCount: () => getRemoteMemoryCount,
|
|
433
|
+
deleteMemory: () => deleteMemory,
|
|
434
|
+
deleteMemories: () => deleteMemories,
|
|
435
|
+
createUploadBatches: () => createUploadBatches,
|
|
436
|
+
ApiError: () => ApiError
|
|
437
|
+
});
|
|
438
|
+
async function apiRequest(path, apiKey, init) {
|
|
439
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
440
|
+
...init,
|
|
441
|
+
headers: {
|
|
442
|
+
Authorization: `Bearer ${apiKey}`,
|
|
443
|
+
"Content-Type": "application/json",
|
|
444
|
+
...init?.headers || {}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
if (!res.ok) {
|
|
448
|
+
const body = await res.text().catch(() => "");
|
|
449
|
+
throw new ApiError(`${path} failed: HTTP ${res.status}${body ? ` — ${body.slice(0, 200)}` : ""}`, res.status, path);
|
|
450
|
+
}
|
|
451
|
+
return res.json();
|
|
452
|
+
}
|
|
453
|
+
async function listRemoteMemories(apiKey, containerTag) {
|
|
454
|
+
const all = [];
|
|
455
|
+
let offset = 0;
|
|
456
|
+
while (true) {
|
|
457
|
+
const data = await apiRequest(`/api/memories?containerTag=${encodeURIComponent(containerTag)}&limit=${PAGE_SIZE}&offset=${offset}`, apiKey);
|
|
458
|
+
const page = data.memories || [];
|
|
459
|
+
all.push(...page);
|
|
460
|
+
if (page.length < PAGE_SIZE)
|
|
461
|
+
break;
|
|
462
|
+
offset += PAGE_SIZE;
|
|
463
|
+
}
|
|
464
|
+
return all;
|
|
465
|
+
}
|
|
466
|
+
async function deleteMemory(apiKey, memoryId) {
|
|
467
|
+
try {
|
|
468
|
+
await apiRequest(`/api/memories/${memoryId}`, apiKey, { method: "DELETE" });
|
|
469
|
+
return true;
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (err instanceof ApiError && err.statusCode === 404)
|
|
472
|
+
return false;
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function deleteMemories(apiKey, memoryIds, onProgress) {
|
|
477
|
+
let deleted = 0;
|
|
478
|
+
const total = memoryIds.length;
|
|
479
|
+
for (let i = 0;i < total; i += DELETE_CONCURRENCY) {
|
|
480
|
+
const batch = memoryIds.slice(i, i + DELETE_CONCURRENCY);
|
|
481
|
+
const results = await Promise.all(batch.map((id) => deleteMemory(apiKey, id)));
|
|
482
|
+
deleted += results.filter(Boolean).length;
|
|
483
|
+
onProgress?.(deleted, total);
|
|
484
|
+
}
|
|
485
|
+
return deleted;
|
|
486
|
+
}
|
|
487
|
+
function createUploadBatches(memories, maxItems = 10, maxChars = 500000) {
|
|
488
|
+
const batches = [];
|
|
489
|
+
let current = [];
|
|
490
|
+
let currentChars = 0;
|
|
491
|
+
let startIndex = 0;
|
|
492
|
+
for (let i = 0;i < memories.length; i++) {
|
|
493
|
+
const item = memories[i];
|
|
494
|
+
const itemChars = item.content.length;
|
|
495
|
+
const exceedsBatch = current.length > 0 && (current.length >= maxItems || currentChars + itemChars > maxChars);
|
|
496
|
+
if (exceedsBatch) {
|
|
497
|
+
batches.push({ startIndex, items: current });
|
|
498
|
+
current = [];
|
|
499
|
+
currentChars = 0;
|
|
500
|
+
startIndex = i;
|
|
501
|
+
}
|
|
502
|
+
current.push(item);
|
|
503
|
+
currentChars += itemChars;
|
|
504
|
+
}
|
|
505
|
+
if (current.length > 0) {
|
|
506
|
+
batches.push({ startIndex, items: current });
|
|
507
|
+
}
|
|
508
|
+
return batches;
|
|
509
|
+
}
|
|
510
|
+
async function validateKey(apiKey) {
|
|
511
|
+
try {
|
|
512
|
+
const data = await apiRequest("/api/auth/me", apiKey);
|
|
513
|
+
return { valid: true, email: data.user.email, name: data.user.name };
|
|
514
|
+
} catch {
|
|
515
|
+
return { valid: false };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async function getRemoteMemoryCount(apiKey) {
|
|
519
|
+
try {
|
|
520
|
+
const data = await apiRequest("/api/containers", apiKey);
|
|
521
|
+
if (!Array.isArray(data.containers))
|
|
522
|
+
return 0;
|
|
523
|
+
return data.containers.reduce((sum, c) => sum + (c.count || 0), 0);
|
|
524
|
+
} catch {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function uploadItems(apiKey, items, asyncEmbed = false) {
|
|
529
|
+
let retries = 4;
|
|
530
|
+
while (retries > 0) {
|
|
531
|
+
try {
|
|
532
|
+
const data = await apiRequest("/api/memories/bulk", apiKey, {
|
|
533
|
+
method: "POST",
|
|
534
|
+
body: JSON.stringify(asyncEmbed ? { items, async: true } : items)
|
|
535
|
+
});
|
|
536
|
+
if (!Array.isArray(data.results) || data.results.length !== items.length) {
|
|
537
|
+
throw new Error("Unexpected bulk upload response");
|
|
538
|
+
}
|
|
539
|
+
return data.results;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (error instanceof ApiError && error.statusCode === 429) {
|
|
542
|
+
retries--;
|
|
543
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
retries--;
|
|
547
|
+
if (retries === 0) {
|
|
548
|
+
return items.map(() => ({
|
|
549
|
+
success: false,
|
|
550
|
+
error: error instanceof Error ? error.message : String(error)
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return items.map(() => ({ success: false, error: "Upload failed" }));
|
|
557
|
+
}
|
|
558
|
+
async function uploadBulk(apiKey, memories, onProgress) {
|
|
559
|
+
let success = 0;
|
|
560
|
+
let failed = 0;
|
|
561
|
+
const total = memories.length;
|
|
562
|
+
const results = [];
|
|
563
|
+
const batches = createUploadBatches(memories);
|
|
564
|
+
for (const batch of batches) {
|
|
565
|
+
let batchResults = await uploadItems(apiKey, batch.items, true);
|
|
566
|
+
if (batch.items.length > 1 && batchResults.some((result) => !result.success)) {
|
|
567
|
+
const retriedResults = [];
|
|
568
|
+
for (let i = 0;i < batch.items.length; i++) {
|
|
569
|
+
const result = batchResults[i];
|
|
570
|
+
if (result.success) {
|
|
571
|
+
retriedResults.push(result);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const [singleResult] = await uploadItems(apiKey, [batch.items[i]], true);
|
|
575
|
+
retriedResults.push(singleResult);
|
|
576
|
+
}
|
|
577
|
+
batchResults = retriedResults;
|
|
578
|
+
}
|
|
579
|
+
for (let i = 0;i < batchResults.length; i++) {
|
|
580
|
+
const result = batchResults[i];
|
|
581
|
+
results.push({
|
|
582
|
+
index: batch.startIndex + i,
|
|
583
|
+
success: result.success,
|
|
584
|
+
deduped: result.deduped,
|
|
585
|
+
error: result.error
|
|
586
|
+
});
|
|
587
|
+
if (result.success)
|
|
588
|
+
success++;
|
|
589
|
+
else
|
|
590
|
+
failed++;
|
|
591
|
+
}
|
|
592
|
+
onProgress?.(success + failed, total, failed);
|
|
593
|
+
}
|
|
594
|
+
return { success, failed, results };
|
|
595
|
+
}
|
|
596
|
+
var API_URL = "https://mcp.conare.ai", ApiError, PAGE_SIZE = 200, DELETE_CONCURRENCY = 5;
|
|
597
|
+
var init_api = __esm(() => {
|
|
598
|
+
ApiError = class ApiError extends Error {
|
|
599
|
+
statusCode;
|
|
600
|
+
endpoint;
|
|
601
|
+
constructor(message, statusCode, endpoint) {
|
|
602
|
+
super(message);
|
|
603
|
+
this.statusCode = statusCode;
|
|
604
|
+
this.endpoint = endpoint;
|
|
605
|
+
this.name = "ApiError";
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
|
|
32
610
|
// node_modules/sisteransi/src/index.js
|
|
33
611
|
var require_src = __commonJS((exports, module) => {
|
|
34
612
|
var ESC = "\x1B";
|
|
@@ -794,7 +1372,8 @@ __export(exports_interactive, {
|
|
|
794
1372
|
selectChatSources: () => selectChatSources,
|
|
795
1373
|
promptApiKey: () => promptApiKey,
|
|
796
1374
|
finishSetup: () => finishSetup,
|
|
797
|
-
confirmIndexCodebase: () => confirmIndexCodebase
|
|
1375
|
+
confirmIndexCodebase: () => confirmIndexCodebase,
|
|
1376
|
+
confirmBackgroundSync: () => confirmBackgroundSync
|
|
798
1377
|
});
|
|
799
1378
|
function formatDetectedCount(count) {
|
|
800
1379
|
if (count === undefined)
|
|
@@ -866,6 +1445,12 @@ async function confirmIndexCodebase() {
|
|
|
866
1445
|
initialValue: false
|
|
867
1446
|
}));
|
|
868
1447
|
}
|
|
1448
|
+
async function confirmBackgroundSync() {
|
|
1449
|
+
return ensureValue(await ye({
|
|
1450
|
+
message: "Auto-ingest new chats every 10 minutes?",
|
|
1451
|
+
initialValue: true
|
|
1452
|
+
}));
|
|
1453
|
+
}
|
|
869
1454
|
var init_interactive = __esm(() => {
|
|
870
1455
|
init_dist2();
|
|
871
1456
|
});
|
|
@@ -875,10 +1460,10 @@ import { existsSync as existsSync9 } from "node:fs";
|
|
|
875
1460
|
import { join as join10 } from "node:path";
|
|
876
1461
|
|
|
877
1462
|
// src/detect.ts
|
|
878
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
879
|
-
import { spawnSync } from "node:child_process";
|
|
1463
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
880
1464
|
import { join } from "node:path";
|
|
881
1465
|
import { homedir, platform } from "node:os";
|
|
1466
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
882
1467
|
function countJsonlFiles(dir) {
|
|
883
1468
|
let count = 0;
|
|
884
1469
|
try {
|
|
@@ -892,50 +1477,41 @@ function countJsonlFiles(dir) {
|
|
|
892
1477
|
} catch {}
|
|
893
1478
|
return count;
|
|
894
1479
|
}
|
|
895
|
-
function countCursorSessions(dbPath) {
|
|
1480
|
+
async function countCursorSessions(dbPath) {
|
|
896
1481
|
try {
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
)
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
)
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
HAVING SUM(CASE WHEN type = 1 THEN 1 ELSE 0 END) > 0
|
|
926
|
-
AND SUM(CASE WHEN type = 2 THEN 1 ELSE 0 END) > 0
|
|
927
|
-
);
|
|
928
|
-
`.trim()
|
|
929
|
-
], { encoding: "utf-8" });
|
|
930
|
-
if (result.status !== 0)
|
|
931
|
-
return 0;
|
|
932
|
-
const count = Number.parseInt(result.stdout.trim(), 10);
|
|
933
|
-
return Number.isFinite(count) ? count : 0;
|
|
1482
|
+
const require2 = createRequire2(import.meta.url);
|
|
1483
|
+
const initSqlJs = require2("sql.js");
|
|
1484
|
+
const SQL = await initSqlJs();
|
|
1485
|
+
const buffer = readFileSync(dbPath);
|
|
1486
|
+
const db = new SQL.Database(buffer);
|
|
1487
|
+
try {
|
|
1488
|
+
const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
|
|
1489
|
+
if (results.length === 0)
|
|
1490
|
+
return 0;
|
|
1491
|
+
let count = 0;
|
|
1492
|
+
for (const [, value] of results[0].values) {
|
|
1493
|
+
try {
|
|
1494
|
+
const parsed = JSON.parse(value);
|
|
1495
|
+
const headers = parsed.fullConversationHeadersOnly;
|
|
1496
|
+
if (!Array.isArray(headers) || headers.length < 2)
|
|
1497
|
+
continue;
|
|
1498
|
+
const hasUser = headers.some((h) => h.type === 1);
|
|
1499
|
+
const hasAssistant = headers.some((h) => h.type === 2);
|
|
1500
|
+
if (hasUser && hasAssistant)
|
|
1501
|
+
count++;
|
|
1502
|
+
} catch {
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return count;
|
|
1507
|
+
} finally {
|
|
1508
|
+
db.close();
|
|
1509
|
+
}
|
|
934
1510
|
} catch {
|
|
935
1511
|
return 0;
|
|
936
1512
|
}
|
|
937
1513
|
}
|
|
938
|
-
function detect() {
|
|
1514
|
+
async function detect() {
|
|
939
1515
|
const home = homedir();
|
|
940
1516
|
const tools = [];
|
|
941
1517
|
const claudeDir = join(home, ".claude", "projects");
|
|
@@ -956,135 +1532,74 @@ function detect() {
|
|
|
956
1532
|
let sessionCount = 0;
|
|
957
1533
|
if (existsSync(codexHistory)) {
|
|
958
1534
|
try {
|
|
959
|
-
const { readFileSync } = __require("node:fs");
|
|
960
|
-
const lines =
|
|
1535
|
+
const { readFileSync: readFileSync2 } = __require("node:fs");
|
|
1536
|
+
const lines = readFileSync2(codexHistory, "utf-8").split(`
|
|
961
1537
|
`).filter(Boolean);
|
|
962
1538
|
const sessions = new Set(lines.map((l) => {
|
|
963
1539
|
try {
|
|
964
1540
|
return JSON.parse(l).session_id;
|
|
965
1541
|
} catch {
|
|
966
1542
|
return null;
|
|
967
|
-
}
|
|
968
|
-
}));
|
|
969
|
-
sessions.delete(null);
|
|
970
|
-
sessionCount = sessions.size;
|
|
971
|
-
} catch {}
|
|
972
|
-
}
|
|
973
|
-
tools.push({
|
|
974
|
-
name: "Codex",
|
|
975
|
-
available: true,
|
|
976
|
-
path: existsSync(codexHistory) ? codexHistory : codexSessions,
|
|
977
|
-
sessionCount
|
|
978
|
-
});
|
|
979
|
-
} else {
|
|
980
|
-
tools.push({ name: "Codex", available: false, path: codexHistory, sessionCount: 0 });
|
|
981
|
-
}
|
|
982
|
-
const os = platform();
|
|
983
|
-
let cursorDbPath;
|
|
984
|
-
if (os === "darwin") {
|
|
985
|
-
cursorDbPath = join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
986
|
-
} else if (os === "win32") {
|
|
987
|
-
cursorDbPath = join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb");
|
|
988
|
-
} else {
|
|
989
|
-
cursorDbPath = join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
990
|
-
}
|
|
991
|
-
tools.push({
|
|
992
|
-
name: "Cursor",
|
|
993
|
-
available: existsSync(cursorDbPath),
|
|
994
|
-
path: cursorDbPath,
|
|
995
|
-
sessionCount: existsSync(cursorDbPath) ? countCursorSessions(cursorDbPath) : 0
|
|
996
|
-
});
|
|
997
|
-
const openclawDir = join(home, ".openclaw");
|
|
998
|
-
tools.push({
|
|
999
|
-
name: "OpenClaw",
|
|
1000
|
-
available: existsSync(openclawDir),
|
|
1001
|
-
path: openclawDir,
|
|
1002
|
-
sessionCount: 0
|
|
1003
|
-
});
|
|
1004
|
-
return tools;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// src/ingest/claude.ts
|
|
1008
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
1009
|
-
import { join as join3, basename } from "node:path";
|
|
1010
|
-
import { homedir as homedir3 } from "node:os";
|
|
1011
|
-
|
|
1012
|
-
// src/ingest/shared.ts
|
|
1013
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
1014
|
-
import { createHash } from "node:crypto";
|
|
1015
|
-
import { join as join2 } from "node:path";
|
|
1016
|
-
import { homedir as homedir2 } from "node:os";
|
|
1017
|
-
var MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
|
|
1018
|
-
var MIN_SUBSTANTIVE = 200;
|
|
1019
|
-
var NARRATION_RE = /^[\s\n]*(Let me |Now let me |Now I['\u2019]|Now add |Now fix |Now replace |Now integrate |Now update |Now pass |Now clean |Now build|Update the |Builds clean|Deployed\.|Wait, I |Let['\u2019]s test |Good —|Great\.|Perfect\.|Alright|OK,? let me|I[''\u2019]ll |Starting |I need to |Need |I found |I read |I[''\u2019]ve (loaded|confirmed|verified)|Context loaded|Next (I[''\u2019]|step)|Deps confirm|Diff check|Still missing|I[''\u2019]ll (do|check|inspect|trace|run|grab|pull|read|verify))/;
|
|
1020
|
-
function isNarration(text) {
|
|
1021
|
-
const stripped = text.trim();
|
|
1022
|
-
if (stripped.length < MIN_SUBSTANTIVE)
|
|
1023
|
-
return true;
|
|
1024
|
-
if (stripped.length <= 300 && NARRATION_RE.test(stripped))
|
|
1025
|
-
return true;
|
|
1026
|
-
return false;
|
|
1027
|
-
}
|
|
1028
|
-
function cleanText(raw) {
|
|
1029
|
-
let text = raw;
|
|
1030
|
-
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
|
|
1031
|
-
text = text.replace(/<attached-context[\s\S]*?<\/attached-context>/g, "");
|
|
1032
|
-
text = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, "");
|
|
1033
|
-
text = text.replace(/^\s*Base directory for this skill:[\s\S]*/g, "");
|
|
1034
|
-
text = text.replace(/\nBase directory for this skill:[\s\S]*/g, "");
|
|
1035
|
-
text = text.replace(/<!-- vibe-rules Integration -->[\s\S]*?<!-- \/vibe-rules Integration -->/g, "");
|
|
1036
|
-
text = text.replace(/<good-behaviour>[\s\S]*?<\/good-behaviour>/g, "");
|
|
1037
|
-
text = text.replace(/<frontend-design>[\s\S]*?<\/frontend-design>/g, "");
|
|
1038
|
-
text = text.replace(/<architecture>[\s\S]*?<\/architecture>/g, "");
|
|
1039
|
-
return text.trim();
|
|
1040
|
-
}
|
|
1041
|
-
function createContentHash(content) {
|
|
1042
|
-
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
1043
|
-
}
|
|
1044
|
-
function getIngested() {
|
|
1045
|
-
try {
|
|
1046
|
-
if (existsSync2(MANIFEST_PATH)) {
|
|
1047
|
-
return JSON.parse(readFileSync(MANIFEST_PATH, "utf-8"));
|
|
1048
|
-
}
|
|
1049
|
-
} catch {}
|
|
1050
|
-
return {};
|
|
1051
|
-
}
|
|
1052
|
-
function markIngested(source, sessionIds) {
|
|
1053
|
-
const manifest = getIngested();
|
|
1054
|
-
const existing = new Set(manifest[source] || []);
|
|
1055
|
-
for (const id of sessionIds)
|
|
1056
|
-
existing.add(id);
|
|
1057
|
-
manifest[source] = [...existing];
|
|
1058
|
-
const dir = join2(homedir2(), ".conare");
|
|
1059
|
-
if (!existsSync2(dir))
|
|
1060
|
-
mkdirSync(dir, { recursive: true });
|
|
1061
|
-
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
1062
|
-
}
|
|
1063
|
-
function isIngested(source, sessionId) {
|
|
1064
|
-
const manifest = getIngested();
|
|
1065
|
-
return (manifest[source] || []).includes(sessionId);
|
|
1066
|
-
}
|
|
1067
|
-
function clearIngested(source) {
|
|
1068
|
-
const manifest = getIngested();
|
|
1069
|
-
if (source) {
|
|
1070
|
-
delete manifest[source];
|
|
1543
|
+
}
|
|
1544
|
+
}));
|
|
1545
|
+
sessions.delete(null);
|
|
1546
|
+
sessionCount = sessions.size;
|
|
1547
|
+
} catch {}
|
|
1548
|
+
}
|
|
1549
|
+
tools.push({
|
|
1550
|
+
name: "Codex",
|
|
1551
|
+
available: true,
|
|
1552
|
+
path: existsSync(codexHistory) ? codexHistory : codexSessions,
|
|
1553
|
+
sessionCount
|
|
1554
|
+
});
|
|
1071
1555
|
} else {
|
|
1072
|
-
|
|
1073
|
-
delete manifest[key];
|
|
1556
|
+
tools.push({ name: "Codex", available: false, path: codexHistory, sessionCount: 0 });
|
|
1074
1557
|
}
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1558
|
+
const os = platform();
|
|
1559
|
+
let cursorDbPath;
|
|
1560
|
+
if (os === "darwin") {
|
|
1561
|
+
cursorDbPath = join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
1562
|
+
} else if (os === "win32") {
|
|
1563
|
+
cursorDbPath = join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb");
|
|
1564
|
+
} else {
|
|
1565
|
+
cursorDbPath = join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
1566
|
+
}
|
|
1567
|
+
tools.push({
|
|
1568
|
+
name: "Cursor",
|
|
1569
|
+
available: existsSync(cursorDbPath),
|
|
1570
|
+
path: cursorDbPath,
|
|
1571
|
+
sessionCount: existsSync(cursorDbPath) ? await countCursorSessions(cursorDbPath) : 0
|
|
1572
|
+
});
|
|
1573
|
+
const openclawDir = join(home, ".openclaw");
|
|
1574
|
+
tools.push({
|
|
1575
|
+
name: "OpenClaw",
|
|
1576
|
+
available: existsSync(openclawDir),
|
|
1577
|
+
path: openclawDir,
|
|
1578
|
+
sessionCount: 0
|
|
1579
|
+
});
|
|
1580
|
+
return tools;
|
|
1079
1581
|
}
|
|
1080
1582
|
|
|
1081
1583
|
// src/ingest/claude.ts
|
|
1584
|
+
init_shared();
|
|
1585
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
|
|
1586
|
+
import { join as join3, basename } from "node:path";
|
|
1587
|
+
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
1082
1588
|
var MAX_CONTENT = 48000;
|
|
1083
1589
|
var MIN_TURN_LEN = 50;
|
|
1084
1590
|
function resolveProjectName(dirName) {
|
|
1085
1591
|
const segments = dirName.replace(/^-/, "").split("-");
|
|
1086
|
-
|
|
1087
|
-
let
|
|
1592
|
+
const isWindows = platform2() === "win32";
|
|
1593
|
+
let resolved;
|
|
1594
|
+
let startIdx;
|
|
1595
|
+
if (isWindows && segments.length > 0 && /^[A-Za-z]$/.test(segments[0])) {
|
|
1596
|
+
resolved = segments[0].toUpperCase() + ":\\";
|
|
1597
|
+
startIdx = 1;
|
|
1598
|
+
} else {
|
|
1599
|
+
resolved = "/";
|
|
1600
|
+
startIdx = 0;
|
|
1601
|
+
}
|
|
1602
|
+
let i = startIdx;
|
|
1088
1603
|
while (i < segments.length) {
|
|
1089
1604
|
let found = false;
|
|
1090
1605
|
for (let end = segments.length;end > i; end--) {
|
|
@@ -1103,10 +1618,11 @@ function resolveProjectName(dirName) {
|
|
|
1103
1618
|
}
|
|
1104
1619
|
}
|
|
1105
1620
|
const home = homedir3();
|
|
1106
|
-
|
|
1107
|
-
|
|
1621
|
+
const sep = isWindows ? "\\" : "/";
|
|
1622
|
+
if (resolved.startsWith(home + sep)) {
|
|
1623
|
+
return resolved.slice(home.length + 1).replace(/\\/g, "/");
|
|
1108
1624
|
}
|
|
1109
|
-
return resolved;
|
|
1625
|
+
return resolved.replace(/\\/g, "/");
|
|
1110
1626
|
}
|
|
1111
1627
|
function extractText(content) {
|
|
1112
1628
|
if (typeof content === "string")
|
|
@@ -1182,7 +1698,7 @@ function ingestClaude() {
|
|
|
1182
1698
|
}
|
|
1183
1699
|
for (const file of files) {
|
|
1184
1700
|
const sessionId = basename(file, ".jsonl");
|
|
1185
|
-
const raw =
|
|
1701
|
+
const raw = readFileSync3(join3(projPath, file), "utf-8");
|
|
1186
1702
|
const { turns, date } = parseSession(raw.split(`
|
|
1187
1703
|
`));
|
|
1188
1704
|
if (turns.length === 0) {
|
|
@@ -1191,8 +1707,7 @@ function ingestClaude() {
|
|
|
1191
1707
|
}
|
|
1192
1708
|
const header = `# Chat: ${project}${date ? ` | ${date}` : ""}`;
|
|
1193
1709
|
const body = turns.map((t) => {
|
|
1194
|
-
|
|
1195
|
-
return `## Q: ${q}
|
|
1710
|
+
return `## Q: ${t.user}
|
|
1196
1711
|
|
|
1197
1712
|
${t.assistant}`;
|
|
1198
1713
|
}).join(`
|
|
@@ -1233,103 +1748,32 @@ ${body}`;
|
|
|
1233
1748
|
}
|
|
1234
1749
|
|
|
1235
1750
|
// src/ingest/codex.ts
|
|
1236
|
-
|
|
1751
|
+
init_shared();
|
|
1752
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
1237
1753
|
import { join as join4, basename as basename2 } from "node:path";
|
|
1238
1754
|
import { homedir as homedir4 } from "node:os";
|
|
1239
1755
|
var MAX_CONTENT2 = 48000;
|
|
1240
1756
|
function isCodexBoilerplate(text) {
|
|
1241
1757
|
return text.startsWith("# AGENTS.md instructions for") || text.startsWith("<INSTRUCTIONS>") || text.startsWith("<user_instructions>") || text.startsWith("<user_action>");
|
|
1242
1758
|
}
|
|
1243
|
-
function
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (cwdMatch)
|
|
1247
|
-
cwd = cwdMatch[1];
|
|
1248
|
-
const cleaned = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, "").trim();
|
|
1249
|
-
return { text: cleaned, cwd };
|
|
1759
|
+
function extractCwd(text) {
|
|
1760
|
+
const match = text.match(/<cwd>([^<]+)<\/cwd>/);
|
|
1761
|
+
return match ? match[1] : null;
|
|
1250
1762
|
}
|
|
1251
1763
|
function projectFromCwd(cwd) {
|
|
1252
|
-
|
|
1764
|
+
const home = homedir4();
|
|
1765
|
+
const normalized = cwd.replace(/\\/g, "/");
|
|
1766
|
+
const normalizedHome = home.replace(/\\/g, "/");
|
|
1767
|
+
if (normalized.startsWith(normalizedHome + "/")) {
|
|
1768
|
+
return normalized.slice(normalizedHome.length + 1);
|
|
1769
|
+
}
|
|
1770
|
+
return normalized.replace(/^\/Users\/[^/]+\//, "").replace(/^\/home\/[^/]+\//, "").replace(/^[A-Za-z]:\/Users\/[^/]+\//, "");
|
|
1253
1771
|
}
|
|
1254
1772
|
function ingestCodex() {
|
|
1255
1773
|
const memories = [];
|
|
1256
1774
|
const sessionIds = [];
|
|
1257
1775
|
let filtered = 0;
|
|
1258
1776
|
let deduped = 0;
|
|
1259
|
-
const historyPath = join4(homedir4(), ".codex", "history.jsonl");
|
|
1260
|
-
if (existsSync4(historyPath)) {
|
|
1261
|
-
try {
|
|
1262
|
-
const lines = readFileSync3(historyPath, "utf-8").split(`
|
|
1263
|
-
`).filter(Boolean);
|
|
1264
|
-
const sessions = new Map;
|
|
1265
|
-
for (const line of lines) {
|
|
1266
|
-
try {
|
|
1267
|
-
const obj = JSON.parse(line);
|
|
1268
|
-
if (!obj.session_id || !obj.text)
|
|
1269
|
-
continue;
|
|
1270
|
-
if (!sessions.has(obj.session_id))
|
|
1271
|
-
sessions.set(obj.session_id, []);
|
|
1272
|
-
sessions.get(obj.session_id).push({ ts: obj.ts, text: obj.text });
|
|
1273
|
-
} catch {
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
for (const [sessionId, entries] of sessions) {
|
|
1278
|
-
entries.sort((a, b) => a.ts - b.ts);
|
|
1279
|
-
const date = new Date(entries[0].ts * 1000).toISOString().slice(0, 10);
|
|
1280
|
-
let project = null;
|
|
1281
|
-
const cleanEntries = [];
|
|
1282
|
-
for (const e of entries) {
|
|
1283
|
-
let text = cleanText(e.text);
|
|
1284
|
-
if (isCodexBoilerplate(text))
|
|
1285
|
-
continue;
|
|
1286
|
-
const env = stripEnvironmentContext(text);
|
|
1287
|
-
text = env.text;
|
|
1288
|
-
if (!project && env.cwd)
|
|
1289
|
-
project = projectFromCwd(env.cwd);
|
|
1290
|
-
if (text.length === 0)
|
|
1291
|
-
continue;
|
|
1292
|
-
cleanEntries.push(text.length > 300 ? text.slice(0, 300) + "..." : text);
|
|
1293
|
-
}
|
|
1294
|
-
const body = cleanEntries.filter(Boolean).join(`
|
|
1295
|
-
|
|
1296
|
-
---
|
|
1297
|
-
|
|
1298
|
-
`);
|
|
1299
|
-
let content = `# Codex Chat | ${date}
|
|
1300
|
-
|
|
1301
|
-
${body}`;
|
|
1302
|
-
if (content.length > MAX_CONTENT2)
|
|
1303
|
-
content = content.slice(0, MAX_CONTENT2) + `
|
|
1304
|
-
|
|
1305
|
-
[truncated]`;
|
|
1306
|
-
if (content.length < 100) {
|
|
1307
|
-
filtered++;
|
|
1308
|
-
continue;
|
|
1309
|
-
}
|
|
1310
|
-
const contentHash = createContentHash(content);
|
|
1311
|
-
const dedupKey = `codex:${sessionId}`;
|
|
1312
|
-
const fingerprint = `${dedupKey}:${contentHash}`;
|
|
1313
|
-
if (isIngested("codex", fingerprint)) {
|
|
1314
|
-
deduped++;
|
|
1315
|
-
continue;
|
|
1316
|
-
}
|
|
1317
|
-
memories.push({
|
|
1318
|
-
content,
|
|
1319
|
-
containerTag: "codex-chats",
|
|
1320
|
-
metadata: {
|
|
1321
|
-
dedupKey,
|
|
1322
|
-
contentHash,
|
|
1323
|
-
source: "codex",
|
|
1324
|
-
sessionId,
|
|
1325
|
-
date,
|
|
1326
|
-
...project ? { project } : {}
|
|
1327
|
-
}
|
|
1328
|
-
});
|
|
1329
|
-
sessionIds.push(sessionId);
|
|
1330
|
-
}
|
|
1331
|
-
} catch {}
|
|
1332
|
-
}
|
|
1333
1777
|
const sessionsDir = join4(homedir4(), ".codex", "sessions");
|
|
1334
1778
|
if (existsSync4(sessionsDir)) {
|
|
1335
1779
|
try {
|
|
@@ -1348,10 +1792,8 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
|
|
|
1348
1792
|
walkCodexSessions(join4(dir, entry.name), memories, sessionIds, stats);
|
|
1349
1793
|
} else if (entry.name.endsWith(".jsonl")) {
|
|
1350
1794
|
const sessionId = basename2(entry.name, ".jsonl");
|
|
1351
|
-
if (sessionIds.includes(sessionId))
|
|
1352
|
-
continue;
|
|
1353
1795
|
try {
|
|
1354
|
-
const lines =
|
|
1796
|
+
const lines = readFileSync4(join4(dir, entry.name), "utf-8").split(`
|
|
1355
1797
|
`).filter(Boolean);
|
|
1356
1798
|
let date = null;
|
|
1357
1799
|
let project = null;
|
|
@@ -1362,19 +1804,29 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
|
|
|
1362
1804
|
try {
|
|
1363
1805
|
const obj = JSON.parse(line);
|
|
1364
1806
|
if (!date && obj.timestamp)
|
|
1365
|
-
date = obj.timestamp.slice(0, 10);
|
|
1807
|
+
date = typeof obj.timestamp === "string" ? obj.timestamp.slice(0, 10) : null;
|
|
1366
1808
|
if (obj.type === "session_meta" && obj.payload?.cwd) {
|
|
1367
1809
|
project = projectFromCwd(obj.payload.cwd);
|
|
1368
1810
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1811
|
+
let role;
|
|
1812
|
+
let msgContent;
|
|
1813
|
+
if (obj.type === "response_item" && obj.payload?.type === "message") {
|
|
1814
|
+
role = obj.payload.role;
|
|
1815
|
+
msgContent = Array.isArray(obj.payload.content) ? obj.payload.content : undefined;
|
|
1816
|
+
} else if (obj.type === "message" && obj.role) {
|
|
1817
|
+
role = obj.role;
|
|
1818
|
+
msgContent = Array.isArray(obj.content) ? obj.content : undefined;
|
|
1819
|
+
}
|
|
1820
|
+
if (!role || !msgContent)
|
|
1374
1821
|
continue;
|
|
1375
1822
|
if (role === "user") {
|
|
1376
1823
|
for (const block of msgContent) {
|
|
1377
1824
|
if (block.type === "input_text" && block.text) {
|
|
1825
|
+
if (!project) {
|
|
1826
|
+
const cwd = extractCwd(block.text);
|
|
1827
|
+
if (cwd)
|
|
1828
|
+
project = projectFromCwd(cwd);
|
|
1829
|
+
}
|
|
1378
1830
|
const text = cleanText(block.text);
|
|
1379
1831
|
if (isCodexBoilerplate(text))
|
|
1380
1832
|
continue;
|
|
@@ -1409,11 +1861,10 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
|
|
|
1409
1861
|
continue;
|
|
1410
1862
|
}
|
|
1411
1863
|
const body = rounds.map((r) => {
|
|
1412
|
-
const q = r.user.length > 300 ? r.user.slice(0, 300) + "..." : r.user;
|
|
1413
1864
|
const assistant = r.assistantParts.join(`
|
|
1414
1865
|
|
|
1415
1866
|
`);
|
|
1416
|
-
return `## Q: ${
|
|
1867
|
+
return `## Q: ${r.user}
|
|
1417
1868
|
|
|
1418
1869
|
${assistant}`;
|
|
1419
1870
|
}).join(`
|
|
@@ -1455,9 +1906,10 @@ ${body}`;
|
|
|
1455
1906
|
}
|
|
1456
1907
|
|
|
1457
1908
|
// src/ingest/cursor.ts
|
|
1458
|
-
|
|
1909
|
+
init_shared();
|
|
1910
|
+
import { readFileSync as readFileSync5, statSync } from "node:fs";
|
|
1459
1911
|
import { join as join5 } from "node:path";
|
|
1460
|
-
import { createRequire as
|
|
1912
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
1461
1913
|
var MAX_CONTENT3 = 48000;
|
|
1462
1914
|
var MAX_DB_SIZE = 2 * 1024 * 1024 * 1024;
|
|
1463
1915
|
var WARN_DB_SIZE = 500 * 1024 * 1024;
|
|
@@ -1465,10 +1917,10 @@ var MIN_TURN_LEN2 = 50;
|
|
|
1465
1917
|
function loadSqlJs(wasmDir) {
|
|
1466
1918
|
try {
|
|
1467
1919
|
if (wasmDir) {
|
|
1468
|
-
const require2 =
|
|
1920
|
+
const require2 = createRequire3(join5(wasmDir, "sql.js", "package.json"));
|
|
1469
1921
|
return require2("sql.js");
|
|
1470
1922
|
} else {
|
|
1471
|
-
const require2 =
|
|
1923
|
+
const require2 = createRequire3(import.meta.url);
|
|
1472
1924
|
return require2("sql.js");
|
|
1473
1925
|
}
|
|
1474
1926
|
} catch {
|
|
@@ -1481,7 +1933,7 @@ function openDb(initSqlJs, dbPath, wasmDir) {
|
|
|
1481
1933
|
locateOpts.locateFile = (file) => join5(wasmDir, "sql.js", "dist", file);
|
|
1482
1934
|
}
|
|
1483
1935
|
return initSqlJs(locateOpts).then((SQL) => {
|
|
1484
|
-
const buffer =
|
|
1936
|
+
const buffer = readFileSync5(dbPath);
|
|
1485
1937
|
return new SQL.Database(buffer);
|
|
1486
1938
|
});
|
|
1487
1939
|
}
|
|
@@ -1528,477 +1980,107 @@ async function ingestCursor(dbPath, wasmDir) {
|
|
|
1528
1980
|
}
|
|
1529
1981
|
if (fileSize > WARN_DB_SIZE) {
|
|
1530
1982
|
console.log(` Warning: large database (${(fileSize / 1024 / 1024).toFixed(0)}MB), loading into memory...`);
|
|
1531
|
-
}
|
|
1532
|
-
const initSqlJs = loadSqlJs(wasmDir);
|
|
1533
|
-
if (!initSqlJs) {
|
|
1534
|
-
console.log(" Skipping Cursor: sql.js not available");
|
|
1535
|
-
return { memories, sessionIds, skipped: 0, filtered, deduped };
|
|
1536
|
-
}
|
|
1537
|
-
let db;
|
|
1538
|
-
try {
|
|
1539
|
-
db = await openDb(initSqlJs, dbPath, wasmDir);
|
|
1540
|
-
} catch (e) {
|
|
1541
|
-
console.log(` Skipping Cursor: cannot open database (${e.message})`);
|
|
1542
|
-
return { memories, sessionIds, skipped: 0, filtered, deduped };
|
|
1543
|
-
}
|
|
1544
|
-
try {
|
|
1545
|
-
let rows = [];
|
|
1546
|
-
try {
|
|
1547
|
-
const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
|
|
1548
|
-
if (results.length > 0)
|
|
1549
|
-
rows = results[0].values;
|
|
1550
|
-
} catch {}
|
|
1551
|
-
for (const [key, value] of rows) {
|
|
1552
|
-
const composerId = key.replace("composerData:", "");
|
|
1553
|
-
let parsed;
|
|
1554
|
-
try {
|
|
1555
|
-
parsed = JSON.parse(value);
|
|
1556
|
-
if (!parsed || typeof parsed !== "object") {
|
|
1557
|
-
filtered++;
|
|
1558
|
-
continue;
|
|
1559
|
-
}
|
|
1560
|
-
} catch {
|
|
1561
|
-
filtered++;
|
|
1562
|
-
continue;
|
|
1563
|
-
}
|
|
1564
|
-
const bubbleHeaders = parsed.fullConversationHeadersOnly;
|
|
1565
|
-
if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
|
|
1566
|
-
filtered++;
|
|
1567
|
-
continue;
|
|
1568
|
-
}
|
|
1569
|
-
const turns = extractTurns(db, composerId, bubbleHeaders);
|
|
1570
|
-
if (turns.length === 0) {
|
|
1571
|
-
filtered++;
|
|
1572
|
-
continue;
|
|
1573
|
-
}
|
|
1574
|
-
const sessionName = parsed.name || "Cursor Chat";
|
|
1575
|
-
const date = parsed.createdAt ? new Date(parsed.createdAt).toISOString().slice(0, 10) : "unknown";
|
|
1576
|
-
const header = `# ${sessionName} | ${date}`;
|
|
1577
|
-
const body = turns.map((t) => {
|
|
1578
|
-
const q = t.user.length > 300 ? t.user.slice(0, 300) + "..." : t.user;
|
|
1579
|
-
return `## Q: ${q}
|
|
1580
|
-
|
|
1581
|
-
${t.assistant}`;
|
|
1582
|
-
}).join(`
|
|
1583
|
-
|
|
1584
|
-
---
|
|
1585
|
-
|
|
1586
|
-
`);
|
|
1587
|
-
let content = `${header}
|
|
1588
|
-
|
|
1589
|
-
${body}`;
|
|
1590
|
-
if (content.length > MAX_CONTENT3)
|
|
1591
|
-
content = content.slice(0, MAX_CONTENT3) + `
|
|
1592
|
-
|
|
1593
|
-
[truncated]`;
|
|
1594
|
-
const contentHash = createContentHash(content);
|
|
1595
|
-
const dedupKey = `cursor:${composerId}`;
|
|
1596
|
-
const fingerprint = `${dedupKey}:${contentHash}`;
|
|
1597
|
-
if (isIngested("cursor", fingerprint)) {
|
|
1598
|
-
deduped++;
|
|
1599
|
-
continue;
|
|
1600
|
-
}
|
|
1601
|
-
memories.push({
|
|
1602
|
-
content,
|
|
1603
|
-
containerTag: "cursor-chats",
|
|
1604
|
-
metadata: {
|
|
1605
|
-
dedupKey,
|
|
1606
|
-
contentHash,
|
|
1607
|
-
source: "cursor",
|
|
1608
|
-
sessionId: composerId,
|
|
1609
|
-
name: sessionName,
|
|
1610
|
-
date
|
|
1611
|
-
}
|
|
1612
|
-
});
|
|
1613
|
-
sessionIds.push(composerId);
|
|
1614
|
-
}
|
|
1615
|
-
} catch (e) {
|
|
1616
|
-
console.log(` Cursor ingestion error: ${e.message}`);
|
|
1617
|
-
} finally {
|
|
1618
|
-
db.close();
|
|
1619
|
-
}
|
|
1620
|
-
return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// src/ingest/codebase.ts
|
|
1624
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1625
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2, existsSync as existsSync5 } from "node:fs";
|
|
1626
|
-
import { join as join6, relative, extname, resolve } from "node:path";
|
|
1627
|
-
var DEFAULT_IGNORE = new Set([
|
|
1628
|
-
"node_modules",
|
|
1629
|
-
".git",
|
|
1630
|
-
".next",
|
|
1631
|
-
".nuxt",
|
|
1632
|
-
".output",
|
|
1633
|
-
"dist",
|
|
1634
|
-
"build",
|
|
1635
|
-
".cache",
|
|
1636
|
-
"__pycache__",
|
|
1637
|
-
".venv",
|
|
1638
|
-
"venv",
|
|
1639
|
-
"vendor",
|
|
1640
|
-
"target",
|
|
1641
|
-
".turbo",
|
|
1642
|
-
".DS_Store",
|
|
1643
|
-
".claude",
|
|
1644
|
-
".conare"
|
|
1645
|
-
]);
|
|
1646
|
-
var IGNORE_FILES = new Set([
|
|
1647
|
-
"package-lock.json",
|
|
1648
|
-
"bun.lockb",
|
|
1649
|
-
"yarn.lock",
|
|
1650
|
-
"pnpm-lock.yaml"
|
|
1651
|
-
]);
|
|
1652
|
-
var CODE_EXTENSIONS = new Set([
|
|
1653
|
-
".ts",
|
|
1654
|
-
".tsx",
|
|
1655
|
-
".js",
|
|
1656
|
-
".jsx",
|
|
1657
|
-
".mjs",
|
|
1658
|
-
".cjs",
|
|
1659
|
-
".vue",
|
|
1660
|
-
".svelte",
|
|
1661
|
-
".astro",
|
|
1662
|
-
".py",
|
|
1663
|
-
".rb",
|
|
1664
|
-
".go",
|
|
1665
|
-
".rs",
|
|
1666
|
-
".java",
|
|
1667
|
-
".kt",
|
|
1668
|
-
".swift",
|
|
1669
|
-
".c",
|
|
1670
|
-
".cpp",
|
|
1671
|
-
".h",
|
|
1672
|
-
".css",
|
|
1673
|
-
".scss",
|
|
1674
|
-
".less",
|
|
1675
|
-
".html",
|
|
1676
|
-
".json",
|
|
1677
|
-
".yaml",
|
|
1678
|
-
".yml",
|
|
1679
|
-
".toml",
|
|
1680
|
-
".md",
|
|
1681
|
-
".mdx",
|
|
1682
|
-
".txt",
|
|
1683
|
-
".sql",
|
|
1684
|
-
".graphql",
|
|
1685
|
-
".prisma",
|
|
1686
|
-
".sh",
|
|
1687
|
-
".bash",
|
|
1688
|
-
".zsh",
|
|
1689
|
-
".lua",
|
|
1690
|
-
".zig",
|
|
1691
|
-
".ex",
|
|
1692
|
-
".exs"
|
|
1693
|
-
]);
|
|
1694
|
-
var SPECIAL_FILES = new Set([
|
|
1695
|
-
"Dockerfile",
|
|
1696
|
-
"Makefile",
|
|
1697
|
-
"Procfile",
|
|
1698
|
-
"Justfile",
|
|
1699
|
-
".gitignore",
|
|
1700
|
-
".dockerignore",
|
|
1701
|
-
"CLAUDE.md",
|
|
1702
|
-
"AGENTS.md",
|
|
1703
|
-
"README.md",
|
|
1704
|
-
"ARCHITECTURE.md",
|
|
1705
|
-
"wrangler.toml",
|
|
1706
|
-
"wrangler.json"
|
|
1707
|
-
]);
|
|
1708
|
-
var MAX_FILE_SIZE = 1e5;
|
|
1709
|
-
function parseGitignore(rootPath) {
|
|
1710
|
-
const patterns = new Set;
|
|
1711
|
-
const gitignorePath = join6(rootPath, ".gitignore");
|
|
1712
|
-
if (!existsSync5(gitignorePath))
|
|
1713
|
-
return patterns;
|
|
1714
|
-
try {
|
|
1715
|
-
const content = readFileSync5(gitignorePath, "utf-8");
|
|
1716
|
-
for (const line of content.split(`
|
|
1717
|
-
`)) {
|
|
1718
|
-
const trimmed = line.trim();
|
|
1719
|
-
if (!trimmed || trimmed.startsWith("#"))
|
|
1720
|
-
continue;
|
|
1721
|
-
patterns.add(trimmed.replace(/\/$/, ""));
|
|
1722
|
-
}
|
|
1723
|
-
} catch {}
|
|
1724
|
-
return patterns;
|
|
1725
|
-
}
|
|
1726
|
-
function shouldIgnore(name, ignorePatterns) {
|
|
1727
|
-
if (DEFAULT_IGNORE.has(name))
|
|
1728
|
-
return true;
|
|
1729
|
-
if (IGNORE_FILES.has(name))
|
|
1730
|
-
return true;
|
|
1731
|
-
if (name.startsWith(".env"))
|
|
1732
|
-
return true;
|
|
1733
|
-
if (ignorePatterns.has(name))
|
|
1734
|
-
return true;
|
|
1735
|
-
return false;
|
|
1736
|
-
}
|
|
1737
|
-
function langFromExt(ext) {
|
|
1738
|
-
const map = {
|
|
1739
|
-
".ts": "typescript",
|
|
1740
|
-
".tsx": "tsx",
|
|
1741
|
-
".js": "javascript",
|
|
1742
|
-
".jsx": "jsx",
|
|
1743
|
-
".mjs": "javascript",
|
|
1744
|
-
".cjs": "javascript",
|
|
1745
|
-
".vue": "vue",
|
|
1746
|
-
".svelte": "svelte",
|
|
1747
|
-
".astro": "astro",
|
|
1748
|
-
".py": "python",
|
|
1749
|
-
".rb": "ruby",
|
|
1750
|
-
".go": "go",
|
|
1751
|
-
".rs": "rust",
|
|
1752
|
-
".java": "java",
|
|
1753
|
-
".kt": "kotlin",
|
|
1754
|
-
".swift": "swift",
|
|
1755
|
-
".c": "c",
|
|
1756
|
-
".cpp": "cpp",
|
|
1757
|
-
".h": "c",
|
|
1758
|
-
".css": "css",
|
|
1759
|
-
".scss": "scss",
|
|
1760
|
-
".less": "less",
|
|
1761
|
-
".html": "html",
|
|
1762
|
-
".json": "json",
|
|
1763
|
-
".yaml": "yaml",
|
|
1764
|
-
".yml": "yaml",
|
|
1765
|
-
".toml": "toml",
|
|
1766
|
-
".md": "markdown",
|
|
1767
|
-
".mdx": "mdx",
|
|
1768
|
-
".sql": "sql",
|
|
1769
|
-
".graphql": "graphql",
|
|
1770
|
-
".prisma": "prisma",
|
|
1771
|
-
".sh": "bash",
|
|
1772
|
-
".bash": "bash",
|
|
1773
|
-
".zsh": "zsh",
|
|
1774
|
-
".lua": "lua",
|
|
1775
|
-
".zig": "zig",
|
|
1776
|
-
".ex": "elixir",
|
|
1777
|
-
".exs": "elixir"
|
|
1778
|
-
};
|
|
1779
|
-
return map[ext] || ext.slice(1);
|
|
1780
|
-
}
|
|
1781
|
-
function formatFile(relPath, content, ext) {
|
|
1782
|
-
const lang = langFromExt(ext);
|
|
1783
|
-
return `# File: ${relPath}
|
|
1784
|
-
|
|
1785
|
-
\`\`\`${lang}
|
|
1786
|
-
${content}
|
|
1787
|
-
\`\`\``;
|
|
1788
|
-
}
|
|
1789
|
-
function indexCodebase(rootPath) {
|
|
1790
|
-
const absRoot = resolve(rootPath);
|
|
1791
|
-
const repoHash = createHash2("sha256").update(absRoot).digest("hex").slice(0, 12);
|
|
1792
|
-
const gitignorePatterns = parseGitignore(absRoot);
|
|
1793
|
-
const memories = [];
|
|
1794
|
-
let fileCount = 0;
|
|
1795
|
-
let skipped = 0;
|
|
1796
|
-
function walk(dir) {
|
|
1797
|
-
let entries;
|
|
1983
|
+
}
|
|
1984
|
+
const initSqlJs = loadSqlJs(wasmDir);
|
|
1985
|
+
if (!initSqlJs) {
|
|
1986
|
+
console.log(" Skipping Cursor: sql.js not available");
|
|
1987
|
+
return { memories, sessionIds, skipped: 0, filtered, deduped };
|
|
1988
|
+
}
|
|
1989
|
+
let db;
|
|
1990
|
+
try {
|
|
1991
|
+
db = await openDb(initSqlJs, dbPath, wasmDir);
|
|
1992
|
+
} catch (e) {
|
|
1993
|
+
console.log(` Skipping Cursor: cannot open database (${e.message})`);
|
|
1994
|
+
return { memories, sessionIds, skipped: 0, filtered, deduped };
|
|
1995
|
+
}
|
|
1996
|
+
try {
|
|
1997
|
+
let rows = [];
|
|
1798
1998
|
try {
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
}
|
|
1803
|
-
for (const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
const fullPath = join6(dir, entry.name);
|
|
1807
|
-
if (entry.isDirectory()) {
|
|
1808
|
-
walk(fullPath);
|
|
1809
|
-
continue;
|
|
1810
|
-
}
|
|
1811
|
-
if (!entry.isFile())
|
|
1812
|
-
continue;
|
|
1813
|
-
const ext = extname(entry.name).toLowerCase();
|
|
1814
|
-
if (!CODE_EXTENSIONS.has(ext) && !SPECIAL_FILES.has(entry.name))
|
|
1815
|
-
continue;
|
|
1816
|
-
let stat;
|
|
1999
|
+
const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
|
|
2000
|
+
if (results.length > 0)
|
|
2001
|
+
rows = results[0].values;
|
|
2002
|
+
} catch {}
|
|
2003
|
+
for (const [key, value] of rows) {
|
|
2004
|
+
const composerId = key.replace("composerData:", "");
|
|
2005
|
+
let parsed;
|
|
1817
2006
|
try {
|
|
1818
|
-
|
|
2007
|
+
parsed = JSON.parse(value);
|
|
2008
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2009
|
+
filtered++;
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
1819
2012
|
} catch {
|
|
2013
|
+
filtered++;
|
|
1820
2014
|
continue;
|
|
1821
2015
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1826
|
-
let raw;
|
|
1827
|
-
try {
|
|
1828
|
-
raw = readFileSync5(fullPath, "utf-8");
|
|
1829
|
-
} catch {
|
|
1830
|
-
skipped++;
|
|
2016
|
+
const bubbleHeaders = parsed.fullConversationHeadersOnly;
|
|
2017
|
+
if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
|
|
2018
|
+
filtered++;
|
|
1831
2019
|
continue;
|
|
1832
2020
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
2021
|
+
const turns = extractTurns(db, composerId, bubbleHeaders);
|
|
2022
|
+
if (turns.length === 0) {
|
|
2023
|
+
filtered++;
|
|
1835
2024
|
continue;
|
|
1836
2025
|
}
|
|
1837
|
-
const
|
|
1838
|
-
const
|
|
1839
|
-
const
|
|
2026
|
+
const sessionName = parsed.name || "Cursor Chat";
|
|
2027
|
+
const date = parsed.createdAt ? new Date(parsed.createdAt).toISOString().slice(0, 10) : "unknown";
|
|
2028
|
+
const header = `# ${sessionName} | ${date}`;
|
|
2029
|
+
const body = turns.map((t) => {
|
|
2030
|
+
return `## Q: ${t.user}
|
|
2031
|
+
|
|
2032
|
+
${t.assistant}`;
|
|
2033
|
+
}).join(`
|
|
2034
|
+
|
|
2035
|
+
---
|
|
2036
|
+
|
|
2037
|
+
`);
|
|
2038
|
+
let content = `${header}
|
|
2039
|
+
|
|
2040
|
+
${body}`;
|
|
2041
|
+
if (content.length > MAX_CONTENT3)
|
|
2042
|
+
content = content.slice(0, MAX_CONTENT3) + `
|
|
2043
|
+
|
|
2044
|
+
[truncated]`;
|
|
2045
|
+
const contentHash = createContentHash(content);
|
|
2046
|
+
const dedupKey = `cursor:${composerId}`;
|
|
1840
2047
|
const fingerprint = `${dedupKey}:${contentHash}`;
|
|
1841
|
-
if (isIngested("
|
|
1842
|
-
|
|
2048
|
+
if (isIngested("cursor", fingerprint)) {
|
|
2049
|
+
deduped++;
|
|
1843
2050
|
continue;
|
|
1844
2051
|
}
|
|
1845
|
-
const content = formatFile(relPath, raw, ext);
|
|
1846
2052
|
memories.push({
|
|
1847
2053
|
content,
|
|
1848
|
-
containerTag: "
|
|
2054
|
+
containerTag: "cursor-chats",
|
|
1849
2055
|
metadata: {
|
|
1850
2056
|
dedupKey,
|
|
1851
2057
|
contentHash,
|
|
1852
|
-
source: "
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
language: langFromExt(ext)
|
|
2058
|
+
source: "cursor",
|
|
2059
|
+
sessionId: composerId,
|
|
2060
|
+
name: sessionName,
|
|
2061
|
+
date
|
|
1857
2062
|
}
|
|
1858
2063
|
});
|
|
1859
|
-
|
|
2064
|
+
sessionIds.push(composerId);
|
|
1860
2065
|
}
|
|
2066
|
+
} catch (e) {
|
|
2067
|
+
console.log(` Cursor ingestion error: ${e.message}`);
|
|
2068
|
+
} finally {
|
|
2069
|
+
db.close();
|
|
1861
2070
|
}
|
|
1862
|
-
|
|
1863
|
-
return { memories, fileCount, skipped };
|
|
2071
|
+
return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
|
|
1864
2072
|
}
|
|
1865
2073
|
|
|
1866
|
-
// src/
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
let current = [];
|
|
1871
|
-
let currentChars = 0;
|
|
1872
|
-
let startIndex = 0;
|
|
1873
|
-
for (let i = 0;i < memories.length; i++) {
|
|
1874
|
-
const item = memories[i];
|
|
1875
|
-
const itemChars = item.content.length;
|
|
1876
|
-
const exceedsBatch = current.length > 0 && (current.length >= maxItems || currentChars + itemChars > maxChars);
|
|
1877
|
-
if (exceedsBatch) {
|
|
1878
|
-
batches.push({ startIndex, items: current });
|
|
1879
|
-
current = [];
|
|
1880
|
-
currentChars = 0;
|
|
1881
|
-
startIndex = i;
|
|
1882
|
-
}
|
|
1883
|
-
current.push(item);
|
|
1884
|
-
currentChars += itemChars;
|
|
1885
|
-
}
|
|
1886
|
-
if (current.length > 0) {
|
|
1887
|
-
batches.push({ startIndex, items: current });
|
|
1888
|
-
}
|
|
1889
|
-
return batches;
|
|
1890
|
-
}
|
|
1891
|
-
async function validateKey(apiKey) {
|
|
1892
|
-
try {
|
|
1893
|
-
const res = await fetch(`${API_URL}/api/auth/me`, {
|
|
1894
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1895
|
-
});
|
|
1896
|
-
if (!res.ok)
|
|
1897
|
-
return { valid: false };
|
|
1898
|
-
const data = await res.json();
|
|
1899
|
-
return { valid: true, email: data.user.email, name: data.user.name };
|
|
1900
|
-
} catch {
|
|
1901
|
-
return { valid: false };
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
async function getRemoteMemoryCount(apiKey) {
|
|
1905
|
-
try {
|
|
1906
|
-
const res = await fetch(`${API_URL}/api/containers`, {
|
|
1907
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1908
|
-
});
|
|
1909
|
-
if (!res.ok)
|
|
1910
|
-
return null;
|
|
1911
|
-
const data = await res.json();
|
|
1912
|
-
if (!Array.isArray(data.containers))
|
|
1913
|
-
return 0;
|
|
1914
|
-
return data.containers.reduce((sum, container) => sum + (container.count || 0), 0);
|
|
1915
|
-
} catch {
|
|
1916
|
-
return null;
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
async function uploadItems(apiKey, items, asyncEmbed = false) {
|
|
1920
|
-
let retries = 4;
|
|
1921
|
-
while (retries > 0) {
|
|
1922
|
-
try {
|
|
1923
|
-
const res = await fetch(`${API_URL}/api/memories/bulk`, {
|
|
1924
|
-
method: "POST",
|
|
1925
|
-
headers: {
|
|
1926
|
-
"Content-Type": "application/json",
|
|
1927
|
-
Authorization: `Bearer ${apiKey}`
|
|
1928
|
-
},
|
|
1929
|
-
body: JSON.stringify(asyncEmbed ? { items, async: true } : items)
|
|
1930
|
-
});
|
|
1931
|
-
if (res.status === 429) {
|
|
1932
|
-
retries--;
|
|
1933
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
1934
|
-
continue;
|
|
1935
|
-
}
|
|
1936
|
-
if (!res.ok) {
|
|
1937
|
-
const body = await res.text().catch(() => "");
|
|
1938
|
-
throw new Error(`HTTP ${res.status}: ${body.slice(0, 120)}`);
|
|
1939
|
-
}
|
|
1940
|
-
const data = await res.json();
|
|
1941
|
-
if (!Array.isArray(data.results) || data.results.length !== items.length) {
|
|
1942
|
-
throw new Error("Unexpected bulk upload response");
|
|
1943
|
-
}
|
|
1944
|
-
return data.results;
|
|
1945
|
-
} catch (error) {
|
|
1946
|
-
retries--;
|
|
1947
|
-
if (retries === 0) {
|
|
1948
|
-
return items.map(() => ({
|
|
1949
|
-
success: false,
|
|
1950
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1951
|
-
}));
|
|
1952
|
-
}
|
|
1953
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
return items.map(() => ({ success: false, error: "Upload failed" }));
|
|
1957
|
-
}
|
|
1958
|
-
async function uploadBulk(apiKey, memories, onProgress) {
|
|
1959
|
-
let success = 0;
|
|
1960
|
-
let failed = 0;
|
|
1961
|
-
const total = memories.length;
|
|
1962
|
-
const results = [];
|
|
1963
|
-
const batches = createUploadBatches(memories);
|
|
1964
|
-
for (const batch of batches) {
|
|
1965
|
-
let batchResults = await uploadItems(apiKey, batch.items, true);
|
|
1966
|
-
if (batch.items.length > 1 && batchResults.some((result) => !result.success)) {
|
|
1967
|
-
const retriedResults = [];
|
|
1968
|
-
for (let i = 0;i < batch.items.length; i++) {
|
|
1969
|
-
const result = batchResults[i];
|
|
1970
|
-
if (result.success) {
|
|
1971
|
-
retriedResults.push(result);
|
|
1972
|
-
continue;
|
|
1973
|
-
}
|
|
1974
|
-
const [singleResult] = await uploadItems(apiKey, [batch.items[i]], true);
|
|
1975
|
-
retriedResults.push(singleResult);
|
|
1976
|
-
}
|
|
1977
|
-
batchResults = retriedResults;
|
|
1978
|
-
}
|
|
1979
|
-
for (let i = 0;i < batchResults.length; i++) {
|
|
1980
|
-
const result = batchResults[i];
|
|
1981
|
-
results.push({
|
|
1982
|
-
index: batch.startIndex + i,
|
|
1983
|
-
success: result.success,
|
|
1984
|
-
deduped: result.deduped,
|
|
1985
|
-
error: result.error
|
|
1986
|
-
});
|
|
1987
|
-
if (result.success)
|
|
1988
|
-
success++;
|
|
1989
|
-
else
|
|
1990
|
-
failed++;
|
|
1991
|
-
}
|
|
1992
|
-
onProgress?.(success + failed, total, failed);
|
|
1993
|
-
}
|
|
1994
|
-
return { success, failed, results };
|
|
1995
|
-
}
|
|
2074
|
+
// src/index.ts
|
|
2075
|
+
init_codebase();
|
|
2076
|
+
init_shared();
|
|
2077
|
+
init_api();
|
|
1996
2078
|
|
|
1997
2079
|
// src/configure.ts
|
|
1998
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as
|
|
2080
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1999
2081
|
import { dirname, join as join7 } from "node:path";
|
|
2000
|
-
import { homedir as homedir5 } from "node:os";
|
|
2001
|
-
import { spawnSync
|
|
2082
|
+
import { homedir as homedir5, platform as platform4 } from "node:os";
|
|
2083
|
+
import { spawnSync } from "node:child_process";
|
|
2002
2084
|
var CONARE_URL = "https://mcp.conare.ai";
|
|
2003
2085
|
var SERVER_NAME = "conare-memory";
|
|
2004
2086
|
var MCP_TARGETS = [
|
|
@@ -2009,7 +2091,7 @@ var MCP_TARGETS = [
|
|
|
2009
2091
|
];
|
|
2010
2092
|
function readJsonFile(path) {
|
|
2011
2093
|
try {
|
|
2012
|
-
return JSON.parse(
|
|
2094
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
2013
2095
|
} catch {
|
|
2014
2096
|
return {};
|
|
2015
2097
|
}
|
|
@@ -2039,8 +2121,9 @@ function upsertMcpServer(path, apiKey) {
|
|
|
2039
2121
|
function configureClaude(apiKey) {
|
|
2040
2122
|
const claudeConfigPath = join7(homedir5(), ".claude.json");
|
|
2041
2123
|
const claudeMcpPath = join7(homedir5(), ".claude", "mcp.json");
|
|
2042
|
-
if (
|
|
2043
|
-
stdio: "ignore"
|
|
2124
|
+
if (spawnSync("claude", ["mcp", "add-json", SERVER_NAME, "--scope", "user", JSON.stringify(getServerConfig(apiKey))], {
|
|
2125
|
+
stdio: "ignore",
|
|
2126
|
+
shell: platform4() === "win32"
|
|
2044
2127
|
}).status === 0) {
|
|
2045
2128
|
return "Claude Code configured via `claude mcp add-json`";
|
|
2046
2129
|
}
|
|
@@ -2072,7 +2155,7 @@ function configureMcp(apiKey, targets = ["claude", "cursor", "codex"]) {
|
|
|
2072
2155
|
}
|
|
2073
2156
|
|
|
2074
2157
|
// src/config.ts
|
|
2075
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as
|
|
2158
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2076
2159
|
import { join as join8 } from "node:path";
|
|
2077
2160
|
import { homedir as homedir6 } from "node:os";
|
|
2078
2161
|
var CONFIG_DIR = join8(homedir6(), ".conare");
|
|
@@ -2081,7 +2164,7 @@ function readConfig() {
|
|
|
2081
2164
|
try {
|
|
2082
2165
|
if (!existsSync7(CONFIG_PATH))
|
|
2083
2166
|
return {};
|
|
2084
|
-
return JSON.parse(
|
|
2167
|
+
return JSON.parse(readFileSync8(CONFIG_PATH, "utf-8"));
|
|
2085
2168
|
} catch {
|
|
2086
2169
|
return {};
|
|
2087
2170
|
}
|
|
@@ -2096,10 +2179,10 @@ function getSavedApiKey() {
|
|
|
2096
2179
|
}
|
|
2097
2180
|
|
|
2098
2181
|
// src/sync.ts
|
|
2099
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as
|
|
2182
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync9, chmodSync, cpSync, rmSync, symlinkSync, readlinkSync, appendFileSync } from "node:fs";
|
|
2100
2183
|
import { join as join9, dirname as dirname2 } from "node:path";
|
|
2101
|
-
import { homedir as homedir7, platform as
|
|
2102
|
-
import { execSync } from "node:child_process";
|
|
2184
|
+
import { homedir as homedir7, platform as platform5 } from "node:os";
|
|
2185
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
2103
2186
|
var CONARE_DIR = join9(homedir7(), ".conare");
|
|
2104
2187
|
var BIN_DIR = join9(CONARE_DIR, "bin");
|
|
2105
2188
|
var CONFIG_PATH2 = join9(CONARE_DIR, "config.json");
|
|
@@ -2108,6 +2191,43 @@ var PLIST_PATH = join9(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.pl
|
|
|
2108
2191
|
var SYSTEMD_DIR = join9(homedir7(), ".config", "systemd", "user");
|
|
2109
2192
|
var SYSTEMD_SERVICE = join9(SYSTEMD_DIR, "conare-sync.service");
|
|
2110
2193
|
var SYSTEMD_TIMER = join9(SYSTEMD_DIR, "conare-sync.timer");
|
|
2194
|
+
var TASK_NAME = "ConareMemorySync";
|
|
2195
|
+
var RUN_VBS = `Set WshShell = CreateObject("WScript.Shell")
|
|
2196
|
+
WshShell.Run """" & CreateObject("WScript.Shell").ExpandEnvironmentStrings("%USERPROFILE%") & "\\.conare\\bin\\run.cmd" & """", 0, True
|
|
2197
|
+
`;
|
|
2198
|
+
var RUN_CMD = `@echo off
|
|
2199
|
+
REM Conare Memory — background sync wrapper (Windows)
|
|
2200
|
+
setlocal
|
|
2201
|
+
|
|
2202
|
+
set "CONARE_DIR=%USERPROFILE%\\.conare"
|
|
2203
|
+
set "LOG=%CONARE_DIR%\\ingest.log"
|
|
2204
|
+
set "LOCKFILE=%CONARE_DIR%\\sync.lock.d"
|
|
2205
|
+
|
|
2206
|
+
REM File lock: prevent concurrent runs
|
|
2207
|
+
if exist "%LOCKFILE%" (
|
|
2208
|
+
echo %DATE% %TIME% SKIP: another instance running >> "%LOG%"
|
|
2209
|
+
exit /b 0
|
|
2210
|
+
)
|
|
2211
|
+
mkdir "%LOCKFILE%" 2>nul
|
|
2212
|
+
if errorlevel 1 (
|
|
2213
|
+
echo %DATE% %TIME% SKIP: lock failed >> "%LOG%"
|
|
2214
|
+
exit /b 0
|
|
2215
|
+
)
|
|
2216
|
+
|
|
2217
|
+
REM Resolve node
|
|
2218
|
+
where node >nul 2>nul
|
|
2219
|
+
if errorlevel 1 (
|
|
2220
|
+
echo %DATE% %TIME% ERROR: node not found >> "%LOG%"
|
|
2221
|
+
rmdir "%LOCKFILE%" 2>nul
|
|
2222
|
+
exit /b 1
|
|
2223
|
+
)
|
|
2224
|
+
|
|
2225
|
+
echo %DATE% %TIME% START sync >> "%LOG%"
|
|
2226
|
+
node "%CONARE_DIR%\\bin\\conare-ingest.mjs" --config-file "%CONARE_DIR%\\config.json" --ingest-only --quiet 2>> "%LOG%"
|
|
2227
|
+
echo %DATE% %TIME% DONE sync (exit %ERRORLEVEL%) >> "%LOG%"
|
|
2228
|
+
|
|
2229
|
+
rmdir "%LOCKFILE%" 2>nul
|
|
2230
|
+
`;
|
|
2111
2231
|
var RUN_SH = `#!/bin/bash
|
|
2112
2232
|
# Conare Memory — background sync wrapper
|
|
2113
2233
|
# Resolves node at runtime to survive nvm/fnm upgrades
|
|
@@ -2218,7 +2338,7 @@ WantedBy=timers.target
|
|
|
2218
2338
|
}
|
|
2219
2339
|
function hasSystemd() {
|
|
2220
2340
|
try {
|
|
2221
|
-
|
|
2341
|
+
execSync2("systemctl --user status 2>/dev/null", { stdio: "ignore" });
|
|
2222
2342
|
return true;
|
|
2223
2343
|
} catch {
|
|
2224
2344
|
return false;
|
|
@@ -2226,7 +2346,7 @@ function hasSystemd() {
|
|
|
2226
2346
|
}
|
|
2227
2347
|
function uid() {
|
|
2228
2348
|
try {
|
|
2229
|
-
return
|
|
2349
|
+
return execSync2("id -u", { encoding: "utf-8" }).trim();
|
|
2230
2350
|
} catch {
|
|
2231
2351
|
return "501";
|
|
2232
2352
|
}
|
|
@@ -2238,7 +2358,7 @@ function persistBinary(apiKey) {
|
|
|
2238
2358
|
throw new Error("Could not locate CLI bundle. Run from an installed conare package.");
|
|
2239
2359
|
}
|
|
2240
2360
|
const dest = join9(BIN_DIR, "conare-ingest.mjs");
|
|
2241
|
-
const content =
|
|
2361
|
+
const content = readFileSync9(cliEntry, "utf-8");
|
|
2242
2362
|
writeFileSync4(dest, content);
|
|
2243
2363
|
const sqlJsDir = findSqlJs();
|
|
2244
2364
|
if (sqlJsDir) {
|
|
@@ -2250,7 +2370,13 @@ function persistBinary(apiKey) {
|
|
|
2250
2370
|
}
|
|
2251
2371
|
const runShPath = join9(BIN_DIR, "run.sh");
|
|
2252
2372
|
writeFileSync4(runShPath, RUN_SH);
|
|
2253
|
-
|
|
2373
|
+
try {
|
|
2374
|
+
chmodSync(runShPath, 493);
|
|
2375
|
+
} catch {}
|
|
2376
|
+
const runCmdPath = join9(BIN_DIR, "run.cmd");
|
|
2377
|
+
writeFileSync4(runCmdPath, RUN_CMD);
|
|
2378
|
+
const runVbsPath = join9(BIN_DIR, "run.vbs");
|
|
2379
|
+
writeFileSync4(runVbsPath, RUN_VBS);
|
|
2254
2380
|
writeFileSync4(CONFIG_PATH2, JSON.stringify({ apiKey }, null, 2) + `
|
|
2255
2381
|
`);
|
|
2256
2382
|
}
|
|
@@ -2286,13 +2412,13 @@ function setupMacOS(intervalMinutes) {
|
|
|
2286
2412
|
writeFileSync4(PLIST_PATH, makePlist(intervalMinutes));
|
|
2287
2413
|
const id = uid();
|
|
2288
2414
|
try {
|
|
2289
|
-
|
|
2415
|
+
execSync2(`launchctl bootout gui/${id} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
|
|
2290
2416
|
} catch {}
|
|
2291
2417
|
try {
|
|
2292
|
-
|
|
2418
|
+
execSync2(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
|
|
2293
2419
|
} catch {
|
|
2294
2420
|
try {
|
|
2295
|
-
|
|
2421
|
+
execSync2(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
|
|
2296
2422
|
} catch {
|
|
2297
2423
|
throw new Error("Failed to load launchd agent. Try manually: launchctl load " + PLIST_PATH);
|
|
2298
2424
|
}
|
|
@@ -2302,27 +2428,51 @@ function setupLinuxSystemd(intervalMinutes) {
|
|
|
2302
2428
|
mkdirSync4(SYSTEMD_DIR, { recursive: true });
|
|
2303
2429
|
writeFileSync4(SYSTEMD_SERVICE, SYSTEMD_SERVICE_CONTENT);
|
|
2304
2430
|
writeFileSync4(SYSTEMD_TIMER, makeSystemdTimer(intervalMinutes));
|
|
2305
|
-
|
|
2306
|
-
|
|
2431
|
+
execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
2432
|
+
execSync2("systemctl --user enable --now conare-sync.timer", { stdio: "ignore" });
|
|
2307
2433
|
}
|
|
2308
2434
|
function setupLinuxCron(intervalMinutes) {
|
|
2309
2435
|
const cronCmd = `${homedir7()}/.conare/bin/run.sh`;
|
|
2310
2436
|
const cronLine = `*/${intervalMinutes} * * * * ${cronCmd}`;
|
|
2311
2437
|
try {
|
|
2312
|
-
const existing =
|
|
2438
|
+
const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
2313
2439
|
const filtered = existing.split(`
|
|
2314
2440
|
`).filter((l) => !l.includes("conare")).join(`
|
|
2315
2441
|
`);
|
|
2316
2442
|
const newCrontab = (filtered.trim() ? filtered.trim() + `
|
|
2317
2443
|
` : "") + cronLine + `
|
|
2318
2444
|
`;
|
|
2319
|
-
|
|
2445
|
+
execSync2("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
|
|
2320
2446
|
} catch {
|
|
2321
|
-
|
|
2447
|
+
execSync2("crontab -", { input: cronLine + `
|
|
2322
2448
|
`, stdio: ["pipe", "ignore", "ignore"] });
|
|
2323
2449
|
}
|
|
2324
2450
|
}
|
|
2325
2451
|
function installGlobalCommand() {
|
|
2452
|
+
const isWindows = platform5() === "win32";
|
|
2453
|
+
if (isWindows) {
|
|
2454
|
+
const wrapper2 = join9(BIN_DIR, "conare.cmd");
|
|
2455
|
+
const content2 = `@echo off\r
|
|
2456
|
+
where node >nul 2>nul\r
|
|
2457
|
+
if errorlevel 1 (\r
|
|
2458
|
+
echo Error: node not found. Install Node.js first. >&2\r
|
|
2459
|
+
exit /b 1\r
|
|
2460
|
+
)\r
|
|
2461
|
+
node "%USERPROFILE%\\.conare\\bin\\conare-ingest.mjs" %*\r
|
|
2462
|
+
`;
|
|
2463
|
+
writeFileSync4(wrapper2, content2);
|
|
2464
|
+
const pathDirs = (process.env.PATH || "").split(";");
|
|
2465
|
+
if (pathDirs.some((d) => d.toLowerCase() === BIN_DIR.toLowerCase())) {
|
|
2466
|
+
return "Global command: conare (via .conare\\bin in PATH)";
|
|
2467
|
+
}
|
|
2468
|
+
const binDirWin = BIN_DIR.replace(/\//g, "\\");
|
|
2469
|
+
try {
|
|
2470
|
+
execSync2(`powershell -NoProfile -Command "$p = [Environment]::GetEnvironmentVariable('PATH','User'); if ($p -and $p.ToLower().Contains('${binDirWin.toLowerCase().replace(/\\/g, "\\\\")}')) { exit 0 }; [Environment]::SetEnvironmentVariable('PATH', $(if($p){$p + ';'} else {''}) + '${binDirWin.replace(/\\/g, "\\\\")}', 'User')"`, { stdio: "ignore" });
|
|
2471
|
+
return `Global command: conare (added .conare\\bin to user PATH — restart terminal)`;
|
|
2472
|
+
} catch {
|
|
2473
|
+
return `Global command: add ${binDirWin} to your PATH manually`;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2326
2476
|
const wrapper = join9(BIN_DIR, "conare");
|
|
2327
2477
|
const content = `#!/bin/bash
|
|
2328
2478
|
# Conare global command — runs the persisted CLI bundle
|
|
@@ -2362,7 +2512,7 @@ exec "$NODE" "$CONARE_DIR/bin/conare-ingest.mjs" "$@"
|
|
|
2362
2512
|
const shellProfile = getShellProfile();
|
|
2363
2513
|
if (shellProfile) {
|
|
2364
2514
|
try {
|
|
2365
|
-
const profileContent = existsSync8(shellProfile) ?
|
|
2515
|
+
const profileContent = existsSync8(shellProfile) ? readFileSync9(shellProfile, "utf-8") : "";
|
|
2366
2516
|
const exportLine = `export PATH="$HOME/.conare/bin:$PATH"`;
|
|
2367
2517
|
if (!profileContent.includes(".conare/bin")) {
|
|
2368
2518
|
appendFileSync(shellProfile, `
|
|
@@ -2386,7 +2536,7 @@ function getShellProfile() {
|
|
|
2386
2536
|
return join9(home, ".zshrc");
|
|
2387
2537
|
if (shell.includes("bash")) {
|
|
2388
2538
|
const profile = join9(home, ".bash_profile");
|
|
2389
|
-
if (
|
|
2539
|
+
if (platform5() === "darwin" && existsSync8(profile))
|
|
2390
2540
|
return profile;
|
|
2391
2541
|
return join9(home, ".bashrc");
|
|
2392
2542
|
}
|
|
@@ -2396,9 +2546,16 @@ function getShellProfile() {
|
|
|
2396
2546
|
return join9(home, ".bashrc");
|
|
2397
2547
|
return null;
|
|
2398
2548
|
}
|
|
2549
|
+
function setupWindows(intervalMinutes) {
|
|
2550
|
+
const runVbs = join9(BIN_DIR, "run.vbs").replace(/\//g, "\\");
|
|
2551
|
+
try {
|
|
2552
|
+
execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
|
|
2553
|
+
} catch {}
|
|
2554
|
+
execSync2(`schtasks /Create /TN "${TASK_NAME}" /TR "wscript.exe \\"${runVbs}\\"" /SC MINUTE /MO ${intervalMinutes} /F`, { stdio: "ignore" });
|
|
2555
|
+
}
|
|
2399
2556
|
function installSync(apiKey, intervalMinutes = 10) {
|
|
2400
2557
|
const messages = [];
|
|
2401
|
-
const os =
|
|
2558
|
+
const os = platform5();
|
|
2402
2559
|
persistBinary(apiKey);
|
|
2403
2560
|
messages.push("Persisted CLI to ~/.conare/bin/");
|
|
2404
2561
|
messages.push("Saved config to ~/.conare/config.json");
|
|
@@ -2409,6 +2566,9 @@ function installSync(apiKey, intervalMinutes = 10) {
|
|
|
2409
2566
|
setupMacOS(intervalMinutes);
|
|
2410
2567
|
messages.push(`Installed launchd agent (every ${intervalMinutes} min)`);
|
|
2411
2568
|
messages.push(`Plist: ${PLIST_PATH}`);
|
|
2569
|
+
} else if (os === "win32") {
|
|
2570
|
+
setupWindows(intervalMinutes);
|
|
2571
|
+
messages.push(`Installed Windows Task Scheduler (every ${intervalMinutes} min)`);
|
|
2412
2572
|
} else if (os === "linux") {
|
|
2413
2573
|
if (hasSystemd()) {
|
|
2414
2574
|
setupLinuxSystemd(intervalMinutes);
|
|
@@ -2424,35 +2584,40 @@ function installSync(apiKey, intervalMinutes = 10) {
|
|
|
2424
2584
|
}
|
|
2425
2585
|
function uninstallSync() {
|
|
2426
2586
|
const messages = [];
|
|
2427
|
-
const os =
|
|
2587
|
+
const os = platform5();
|
|
2428
2588
|
if (os === "darwin") {
|
|
2429
2589
|
try {
|
|
2430
|
-
|
|
2590
|
+
execSync2(`launchctl bootout gui/${uid()} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
|
|
2431
2591
|
} catch {}
|
|
2432
2592
|
if (existsSync8(PLIST_PATH)) {
|
|
2433
2593
|
unlinkSync(PLIST_PATH);
|
|
2434
2594
|
messages.push("Removed launchd agent");
|
|
2435
2595
|
}
|
|
2596
|
+
} else if (os === "win32") {
|
|
2597
|
+
try {
|
|
2598
|
+
execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
|
|
2599
|
+
messages.push("Removed Windows scheduled task");
|
|
2600
|
+
} catch {}
|
|
2436
2601
|
} else if (os === "linux") {
|
|
2437
2602
|
if (hasSystemd()) {
|
|
2438
2603
|
try {
|
|
2439
|
-
|
|
2604
|
+
execSync2("systemctl --user disable --now conare-sync.timer 2>/dev/null", { stdio: "ignore" });
|
|
2440
2605
|
} catch {}
|
|
2441
2606
|
if (existsSync8(SYSTEMD_SERVICE))
|
|
2442
2607
|
unlinkSync(SYSTEMD_SERVICE);
|
|
2443
2608
|
if (existsSync8(SYSTEMD_TIMER))
|
|
2444
2609
|
unlinkSync(SYSTEMD_TIMER);
|
|
2445
2610
|
try {
|
|
2446
|
-
|
|
2611
|
+
execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
2447
2612
|
} catch {}
|
|
2448
2613
|
messages.push("Removed systemd timer");
|
|
2449
2614
|
} else {
|
|
2450
2615
|
try {
|
|
2451
|
-
const existing =
|
|
2616
|
+
const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
2452
2617
|
const filtered = existing.split(`
|
|
2453
2618
|
`).filter((l) => !l.includes("conare")).join(`
|
|
2454
2619
|
`);
|
|
2455
|
-
|
|
2620
|
+
execSync2("crontab -", { input: filtered.trim() + `
|
|
2456
2621
|
`, stdio: ["pipe", "ignore", "ignore"] });
|
|
2457
2622
|
messages.push("Removed cron job");
|
|
2458
2623
|
} catch {}
|
|
@@ -2547,6 +2712,8 @@ function parseArgs() {
|
|
|
2547
2712
|
let source;
|
|
2548
2713
|
let wasmDir;
|
|
2549
2714
|
let indexPath;
|
|
2715
|
+
let indexProject;
|
|
2716
|
+
let indexChanged = false;
|
|
2550
2717
|
for (let i = 0;i < args.length; i++) {
|
|
2551
2718
|
if (args[i] === "--key" && args[i + 1]) {
|
|
2552
2719
|
key = args[++i];
|
|
@@ -2570,6 +2737,10 @@ function parseArgs() {
|
|
|
2570
2737
|
wasmDir = args[++i];
|
|
2571
2738
|
} else if (args[i] === "--index") {
|
|
2572
2739
|
indexPath = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : ".";
|
|
2740
|
+
} else if (args[i] === "--project" && args[i + 1]) {
|
|
2741
|
+
indexProject = args[++i];
|
|
2742
|
+
} else if (args[i] === "--changed") {
|
|
2743
|
+
indexChanged = true;
|
|
2573
2744
|
} else if (args[i] === "--ingest-only") {
|
|
2574
2745
|
ingestOnly = true;
|
|
2575
2746
|
} else if (args[i] === "--config-only") {
|
|
@@ -2590,6 +2761,8 @@ Options:
|
|
|
2590
2761
|
--key <key> Your Conare API key (required, starts with cmem_)
|
|
2591
2762
|
--config-file <path> Read API key from JSON config file (e.g. ~/.conare/config.json)
|
|
2592
2763
|
--index [path] Index codebase at path (default: current directory)
|
|
2764
|
+
--project <name> Project name for codebase indexing (auto-detected if omitted)
|
|
2765
|
+
--changed Only index files changed in the last git commit
|
|
2593
2766
|
--dry-run Preview what would be uploaded
|
|
2594
2767
|
--force Re-ingest all / re-index all (bypass dedup)
|
|
2595
2768
|
--quiet Suppress all stdout output (for background timer runs)
|
|
@@ -2615,7 +2788,7 @@ Get your API key at https://mcp.conare.ai
|
|
|
2615
2788
|
console.error("Error: --ingest-only and --config-only cannot be used together");
|
|
2616
2789
|
process.exit(1);
|
|
2617
2790
|
}
|
|
2618
|
-
return { key, configFile, dryRun, force, ingestOnly, configOnly, interactive, quiet, installSync: installSyncFlag, uninstallSync: uninstallSyncFlag, syncInterval, source, wasmDir, indexPath };
|
|
2791
|
+
return { key, configFile, dryRun, force, ingestOnly, configOnly, interactive, quiet, installSync: installSyncFlag, uninstallSync: uninstallSyncFlag, syncInterval, source, wasmDir, indexPath, indexProject, indexChanged };
|
|
2619
2792
|
}
|
|
2620
2793
|
async function runInstall() {
|
|
2621
2794
|
const args = process.argv.slice(3);
|
|
@@ -2658,8 +2831,8 @@ async function main() {
|
|
|
2658
2831
|
let configFileKey;
|
|
2659
2832
|
if (opts.configFile) {
|
|
2660
2833
|
try {
|
|
2661
|
-
const { readFileSync:
|
|
2662
|
-
const raw = JSON.parse(
|
|
2834
|
+
const { readFileSync: readFileSync10 } = await import("node:fs");
|
|
2835
|
+
const raw = JSON.parse(readFileSync10(opts.configFile, "utf-8"));
|
|
2663
2836
|
configFileKey = raw.apiKey || raw.key;
|
|
2664
2837
|
if (!configFileKey) {
|
|
2665
2838
|
console.error(`Error: no apiKey/key found in ${opts.configFile}`);
|
|
@@ -2691,7 +2864,7 @@ async function main() {
|
|
|
2691
2864
|
}));
|
|
2692
2865
|
let interactiveMode = false;
|
|
2693
2866
|
if (shouldRunInteractive) {
|
|
2694
|
-
const detectedTools = detect();
|
|
2867
|
+
const detectedTools = await detect();
|
|
2695
2868
|
interactiveTargets = MCP_TARGETS.map((target) => {
|
|
2696
2869
|
const detected = detectedTools.find((tool) => target.id === "claude" && tool.name === "Claude Code" || target.id === "cursor" && tool.name === "Cursor" || target.id === "codex" && tool.name === "Codex" || target.id === "openclaw" && tool.name === "OpenClaw");
|
|
2697
2870
|
return {
|
|
@@ -2779,15 +2952,52 @@ async function main() {
|
|
|
2779
2952
|
} catch {}
|
|
2780
2953
|
}
|
|
2781
2954
|
const idxSpinner = Y2();
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2955
|
+
const scanLabel = opts.indexChanged ? "Scanning changed files..." : "Scanning codebase...";
|
|
2956
|
+
idxSpinner.start(scanLabel);
|
|
2957
|
+
const { memories, fileCount, skipped, localFilePaths, project } = indexCodebase(absPath, {
|
|
2958
|
+
changedOnly: opts.indexChanged,
|
|
2959
|
+
project: opts.indexProject
|
|
2960
|
+
});
|
|
2961
|
+
idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped (project: ${project})`);
|
|
2962
|
+
if (!opts.dryRun && !opts.indexChanged) {
|
|
2963
|
+
const cleanupSpinner = Y2();
|
|
2964
|
+
cleanupSpinner.start("Checking for stale/deleted files...");
|
|
2965
|
+
try {
|
|
2966
|
+
const { listRemoteMemories: listRemoteMemories2, deleteMemories: deleteMemories2 } = await Promise.resolve().then(() => (init_api(), exports_api));
|
|
2967
|
+
const { getRemoteFilePath: getRemoteFilePath2 } = await Promise.resolve().then(() => (init_codebase(), exports_codebase));
|
|
2968
|
+
const remoteMemories = await listRemoteMemories2(apiKey, "codebase");
|
|
2969
|
+
const localPathSet = new Set(localFilePaths);
|
|
2970
|
+
const uploadingPaths = new Set(memories.map((m2) => getRemoteFilePath2(m2)).filter(Boolean));
|
|
2971
|
+
const toDelete = [];
|
|
2972
|
+
for (const remote of remoteMemories) {
|
|
2973
|
+
const remotePath = getRemoteFilePath2(remote);
|
|
2974
|
+
if (!remotePath)
|
|
2975
|
+
continue;
|
|
2976
|
+
if (!localPathSet.has(remotePath) || uploadingPaths.has(remotePath)) {
|
|
2977
|
+
toDelete.push(remote.id);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
if (toDelete.length > 0) {
|
|
2981
|
+
cleanupSpinner.stop(`Found ${toDelete.length} stale/orphaned files`);
|
|
2982
|
+
const deleted = await deleteMemories2(apiKey, toDelete, (done, total) => {
|
|
2983
|
+
write(`\r Cleaning up ${done}/${total}...`);
|
|
2984
|
+
});
|
|
2985
|
+
write(`\r Cleaned up ${deleted}/${toDelete.length} old memories
|
|
2986
|
+
`);
|
|
2987
|
+
} else {
|
|
2988
|
+
cleanupSpinner.stop("No stale files found");
|
|
2989
|
+
}
|
|
2990
|
+
} catch (err) {
|
|
2991
|
+
cleanupSpinner.stop(`Cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2992
|
+
log(" Continuing with upload (stale files may remain)...");
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2785
2995
|
if (memories.length === 0) {
|
|
2786
2996
|
log(`
|
|
2787
2997
|
Nothing new to index.`);
|
|
2788
2998
|
} else if (opts.dryRun) {
|
|
2789
2999
|
log(`
|
|
2790
|
-
[DRY RUN] Would upload ${memories.length} files`);
|
|
3000
|
+
[DRY RUN] Would upload ${memories.length} files (project: ${project})`);
|
|
2791
3001
|
for (const m2 of memories.slice(0, 10)) {
|
|
2792
3002
|
const meta = m2.metadata;
|
|
2793
3003
|
log(` ${meta.language.padEnd(12)} ${meta.filePath}`);
|
|
@@ -2828,7 +3038,7 @@ Nothing new to index.`);
|
|
|
2828
3038
|
}
|
|
2829
3039
|
log();
|
|
2830
3040
|
}
|
|
2831
|
-
const tools = detect();
|
|
3041
|
+
const tools = await detect();
|
|
2832
3042
|
if (!interactiveMode) {
|
|
2833
3043
|
log("Detected AI tools:");
|
|
2834
3044
|
for (const t of tools) {
|
|
@@ -2961,8 +3171,10 @@ Nothing new to index.`);
|
|
|
2961
3171
|
const absPath = resolve2(postIngestIndexPath);
|
|
2962
3172
|
const idxSpinner = Y2();
|
|
2963
3173
|
idxSpinner.start("Indexing codebase...");
|
|
2964
|
-
const { memories, fileCount, skipped } = indexCodebase(absPath
|
|
2965
|
-
|
|
3174
|
+
const { memories, fileCount, skipped, project } = indexCodebase(absPath, {
|
|
3175
|
+
project: opts.indexProject
|
|
3176
|
+
});
|
|
3177
|
+
idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped (project: ${project})`);
|
|
2966
3178
|
if (memories.length === 0) {
|
|
2967
3179
|
log(`
|
|
2968
3180
|
Nothing new to index.`);
|
|
@@ -2983,13 +3195,16 @@ Nothing new to index.`);
|
|
|
2983
3195
|
log("");
|
|
2984
3196
|
}
|
|
2985
3197
|
if (!opts.dryRun && !opts.quiet) {
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3198
|
+
const shouldSync = interactiveMode ? await confirmBackgroundSync() : true;
|
|
3199
|
+
if (shouldSync) {
|
|
3200
|
+
const syncSpinner = Y2();
|
|
3201
|
+
syncSpinner.start("Setting up background sync...");
|
|
3202
|
+
try {
|
|
3203
|
+
installSync(apiKey, opts.syncInterval);
|
|
3204
|
+
syncSpinner.stop(`Background sync active (every ${opts.syncInterval}min)`);
|
|
3205
|
+
} catch (e2) {
|
|
3206
|
+
syncSpinner.stop(`Could not set up background sync: ${e2.message}`);
|
|
3207
|
+
}
|
|
2993
3208
|
}
|
|
2994
3209
|
}
|
|
2995
3210
|
log("");
|