@flarecode/import-memory 0.1.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 +20 -0
- package/package.json +24 -0
- package/src/index.js +112 -0
- package/src/scanner.js +429 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @flarecode/import-memory
|
|
2
|
+
|
|
3
|
+
One-command helper for finding local AI coding-agent history before importing it into FlareCode.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx --yes @flarecode/import-memory@latest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The helper scans common local locations for Claude Code, Codex, Cursor, VS Code, and repo-local instruction files. Raw transcript contents are not uploaded by this package. The shell wrapper at `https://flarecode.sh/import.sh` only detects a package runner and delegates to this npm package.
|
|
10
|
+
|
|
11
|
+
Useful flags:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx --yes @flarecode/import-memory@latest --dry-run
|
|
15
|
+
npx --yes @flarecode/import-memory@latest --json
|
|
16
|
+
npx --yes @flarecode/import-memory@latest --source claude,codex,cursor
|
|
17
|
+
npx --yes @flarecode/import-memory@latest --repo ~/code/my-app
|
|
18
|
+
npx --yes @flarecode/import-memory@latest --since 90d
|
|
19
|
+
npx --yes @flarecode/import-memory@latest --no-transcripts
|
|
20
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flarecode/import-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-command helper for discovering local coding-agent memory sources before importing them into FlareCode.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"bin": {
|
|
8
|
+
"flarecode-import-memory": "./src/index.js",
|
|
9
|
+
"import-memory": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/scanner.js"
|
|
13
|
+
},
|
|
14
|
+
"files": ["src", "README.md"],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test",
|
|
20
|
+
"typecheck": "node --check src/index.js && node --check src/scanner.js && node --check tests/scanner.test.js",
|
|
21
|
+
"lint": "biome check ./src ./tests package.json README.md",
|
|
22
|
+
"clean": "rm -rf dist node_modules"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { formatScanSummary, parseSince, parseSourceFilter, scanSources } from "./scanner.js";
|
|
4
|
+
|
|
5
|
+
const VERSION = "0.1.0";
|
|
6
|
+
|
|
7
|
+
async function main(argv) {
|
|
8
|
+
const args = parseArgs(argv);
|
|
9
|
+
if (args.help) {
|
|
10
|
+
process.stdout.write(helpText());
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (args.version) {
|
|
14
|
+
process.stdout.write(`${VERSION}\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sinceMs = parseSince(args.since);
|
|
19
|
+
const sources = parseSourceFilter(args.source);
|
|
20
|
+
|
|
21
|
+
if (!args.json) {
|
|
22
|
+
process.stdout.write("FlareCode memory import helper\n");
|
|
23
|
+
process.stdout.write("Scanning local coding-agent history and settings.\n");
|
|
24
|
+
process.stdout.write("Raw transcript contents stay on this machine in this version.\n\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await scanSources({
|
|
28
|
+
sources,
|
|
29
|
+
sinceMs,
|
|
30
|
+
repoPath: args.repo,
|
|
31
|
+
includeTranscripts: !args.noTranscripts,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (args.json) {
|
|
35
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process.stdout.write(`${formatScanSummary(result)}\n`);
|
|
40
|
+
process.stdout.write("\n");
|
|
41
|
+
|
|
42
|
+
if (args.dryRun) {
|
|
43
|
+
process.stdout.write("Dry run complete. Nothing was uploaded.\n");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.stdout.write("Next step: upload/linking is intentionally not in the shell wrapper.\n");
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
"When the FlareCode import API is enabled, this same npm package will prompt you to link your account and upload reviewed summaries.\n",
|
|
50
|
+
);
|
|
51
|
+
process.stdout.write("For now, use --dry-run or --json to inspect what the helper can discover.\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(argv) {
|
|
55
|
+
const out = {
|
|
56
|
+
dryRun: false,
|
|
57
|
+
json: false,
|
|
58
|
+
help: false,
|
|
59
|
+
version: false,
|
|
60
|
+
noTranscripts: false,
|
|
61
|
+
source: null,
|
|
62
|
+
since: null,
|
|
63
|
+
repo: null,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
67
|
+
const arg = argv[i];
|
|
68
|
+
if (arg === "--dry-run") out.dryRun = true;
|
|
69
|
+
else if (arg === "--json") out.json = true;
|
|
70
|
+
else if (arg === "--help" || arg === "-h") out.help = true;
|
|
71
|
+
else if (arg === "--version" || arg === "-v") out.version = true;
|
|
72
|
+
else if (arg === "--no-transcripts") out.noTranscripts = true;
|
|
73
|
+
else if (arg === "--source") out.source = requireValue(argv, ++i, "--source");
|
|
74
|
+
else if (arg.startsWith("--source=")) out.source = arg.slice("--source=".length);
|
|
75
|
+
else if (arg === "--since") out.since = requireValue(argv, ++i, "--since");
|
|
76
|
+
else if (arg.startsWith("--since=")) out.since = arg.slice("--since=".length);
|
|
77
|
+
else if (arg === "--repo") out.repo = requireValue(argv, ++i, "--repo");
|
|
78
|
+
else if (arg.startsWith("--repo=")) out.repo = arg.slice("--repo=".length);
|
|
79
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function requireValue(argv, index, flag) {
|
|
86
|
+
const value = argv[index];
|
|
87
|
+
if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function helpText() {
|
|
92
|
+
return `FlareCode memory import helper
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
npx --yes @flarecode/import-memory@latest [options]
|
|
96
|
+
|
|
97
|
+
Options:
|
|
98
|
+
--dry-run Scan and print a summary without preparing upload.
|
|
99
|
+
--json Print machine-readable scan output.
|
|
100
|
+
--source <list> Comma-separated: claude,codex,cursor,vscode,repo.
|
|
101
|
+
--since <duration> Only include files modified within 30d, 12w, 6m, or 1y.
|
|
102
|
+
--repo <path> Include repo-local rules from a specific repo.
|
|
103
|
+
--no-transcripts Import rules/settings only.
|
|
104
|
+
--version, -v Print version.
|
|
105
|
+
--help, -h Print this help.
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
110
|
+
process.stderr.write(`flarecode-import-memory: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
});
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { access, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 6;
|
|
6
|
+
const DEFAULT_MAX_FILES_PER_SOURCE = 5000;
|
|
7
|
+
|
|
8
|
+
const SOURCE_IDS = new Set(["claude", "codex", "cursor", "vscode", "repo"]);
|
|
9
|
+
|
|
10
|
+
const TRANSCRIPT_EXTENSIONS = new Set([".jsonl", ".json", ".ndjson"]);
|
|
11
|
+
|
|
12
|
+
export function parseSourceFilter(value) {
|
|
13
|
+
if (!value) return [...SOURCE_IDS];
|
|
14
|
+
const parsed = value
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((item) => item.trim().toLowerCase())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
const invalid = parsed.filter((item) => !SOURCE_IDS.has(item));
|
|
19
|
+
if (invalid.length > 0) {
|
|
20
|
+
throw new Error(`Unknown source: ${invalid.join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
return [...new Set(parsed)];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseSince(value) {
|
|
26
|
+
if (!value) return null;
|
|
27
|
+
const match = /^(\d+)([dwmy])?$/.exec(value.trim().toLowerCase());
|
|
28
|
+
if (!match) throw new Error("Invalid --since value. Use forms like 30d, 12w, 6m, or 1y.");
|
|
29
|
+
const amount = Number(match[1]);
|
|
30
|
+
const unit = match[2] ?? "d";
|
|
31
|
+
const days = unit === "d" ? amount : unit === "w" ? amount * 7 : unit === "m" ? amount * 30 : amount * 365;
|
|
32
|
+
return Date.now() - days * 24 * 60 * 60 * 1000;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function scanSources(options = {}) {
|
|
36
|
+
const sourceFilter = new Set(options.sources ?? [...SOURCE_IDS]);
|
|
37
|
+
const home = options.home ?? homedir();
|
|
38
|
+
const cwd = options.cwd ?? process.cwd();
|
|
39
|
+
const repoPath = options.repoPath ? expandHome(options.repoPath, home) : await findRepoRoot(cwd);
|
|
40
|
+
const sinceMs = options.sinceMs ?? null;
|
|
41
|
+
const includeTranscripts = options.includeTranscripts ?? true;
|
|
42
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
43
|
+
const maxFilesPerSource = options.maxFilesPerSource ?? DEFAULT_MAX_FILES_PER_SOURCE;
|
|
44
|
+
|
|
45
|
+
const sources = [];
|
|
46
|
+
|
|
47
|
+
if (sourceFilter.has("claude")) {
|
|
48
|
+
sources.push(
|
|
49
|
+
await scanSource({
|
|
50
|
+
id: "claude",
|
|
51
|
+
label: "Claude Code",
|
|
52
|
+
roots: claudeRoots(home),
|
|
53
|
+
sinceMs,
|
|
54
|
+
includeTranscripts,
|
|
55
|
+
maxDepth,
|
|
56
|
+
maxFiles: maxFilesPerSource,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (sourceFilter.has("codex")) {
|
|
62
|
+
sources.push(
|
|
63
|
+
await scanSource({
|
|
64
|
+
id: "codex",
|
|
65
|
+
label: "Codex CLI",
|
|
66
|
+
roots: codexRoots(home),
|
|
67
|
+
sinceMs,
|
|
68
|
+
includeTranscripts,
|
|
69
|
+
maxDepth,
|
|
70
|
+
maxFiles: maxFilesPerSource,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (sourceFilter.has("cursor")) {
|
|
76
|
+
sources.push(
|
|
77
|
+
await scanSource({
|
|
78
|
+
id: "cursor",
|
|
79
|
+
label: "Cursor",
|
|
80
|
+
roots: cursorRoots(home),
|
|
81
|
+
sinceMs,
|
|
82
|
+
includeTranscripts,
|
|
83
|
+
maxDepth,
|
|
84
|
+
maxFiles: maxFilesPerSource,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (sourceFilter.has("vscode")) {
|
|
90
|
+
sources.push(
|
|
91
|
+
await scanSource({
|
|
92
|
+
id: "vscode",
|
|
93
|
+
label: "VS Code",
|
|
94
|
+
roots: vscodeRoots(home),
|
|
95
|
+
sinceMs,
|
|
96
|
+
includeTranscripts,
|
|
97
|
+
maxDepth,
|
|
98
|
+
maxFiles: maxFilesPerSource,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (sourceFilter.has("repo")) {
|
|
104
|
+
sources.push(
|
|
105
|
+
await scanRepoSource({
|
|
106
|
+
repoPath,
|
|
107
|
+
sinceMs,
|
|
108
|
+
maxDepth: 3,
|
|
109
|
+
maxFiles: maxFilesPerSource,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const foundSources = sources.filter((source) => source.exists);
|
|
115
|
+
const totals = sources.reduce(
|
|
116
|
+
(acc, source) => ({
|
|
117
|
+
files: acc.files + source.files.length,
|
|
118
|
+
transcriptFiles: acc.transcriptFiles + source.files.filter((file) => file.kind === "transcript").length,
|
|
119
|
+
settingsFiles: acc.settingsFiles + source.files.filter((file) => file.kind === "settings").length,
|
|
120
|
+
ruleFiles: acc.ruleFiles + source.files.filter((file) => file.kind === "rule").length,
|
|
121
|
+
bytes: acc.bytes + source.files.reduce((sum, file) => sum + file.bytes, 0),
|
|
122
|
+
}),
|
|
123
|
+
{ files: 0, transcriptFiles: 0, settingsFiles: 0, ruleFiles: 0, bytes: 0 },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
scannedAt: new Date().toISOString(),
|
|
128
|
+
platform: platform(),
|
|
129
|
+
home,
|
|
130
|
+
repoPath,
|
|
131
|
+
sources,
|
|
132
|
+
foundSources: foundSources.length,
|
|
133
|
+
totals,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function formatScanSummary(result) {
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push("FlareCode import helper");
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push(`Scanned sources: ${result.sources.length}`);
|
|
142
|
+
lines.push(`Found sources: ${result.foundSources}`);
|
|
143
|
+
lines.push(
|
|
144
|
+
`Candidate files: ${result.totals.files} (${result.totals.transcriptFiles} transcripts, ${result.totals.settingsFiles} settings, ${result.totals.ruleFiles} rules)`,
|
|
145
|
+
);
|
|
146
|
+
lines.push(`Approx bytes: ${formatBytes(result.totals.bytes)}`);
|
|
147
|
+
if (result.repoPath) lines.push(`Repo context: ${result.repoPath}`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
|
|
150
|
+
for (const source of result.sources) {
|
|
151
|
+
const status = source.exists ? `${source.files.length} files` : "not found";
|
|
152
|
+
lines.push(`${source.label}: ${status}`);
|
|
153
|
+
for (const root of source.roots) {
|
|
154
|
+
lines.push(` - ${root.exists ? "found" : "missing"} ${root.path}`);
|
|
155
|
+
}
|
|
156
|
+
if (source.files.length > 0) {
|
|
157
|
+
for (const file of source.files.slice(0, 5)) {
|
|
158
|
+
lines.push(` ${file.kind}: ${file.path}`);
|
|
159
|
+
}
|
|
160
|
+
if (source.files.length > 5) lines.push(` ... ${source.files.length - 5} more`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push("No raw transcript contents were uploaded.");
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function scanSource(input) {
|
|
170
|
+
const roots = [];
|
|
171
|
+
const files = [];
|
|
172
|
+
let truncated = false;
|
|
173
|
+
|
|
174
|
+
for (const root of input.roots) {
|
|
175
|
+
const expanded = expandHome(root.path);
|
|
176
|
+
const rootStat = await safeStat(expanded);
|
|
177
|
+
const rootInfo = {
|
|
178
|
+
path: expanded,
|
|
179
|
+
role: root.role,
|
|
180
|
+
exists: Boolean(rootStat),
|
|
181
|
+
isDirectory: Boolean(rootStat?.isDirectory()),
|
|
182
|
+
};
|
|
183
|
+
roots.push(rootInfo);
|
|
184
|
+
if (!rootStat) continue;
|
|
185
|
+
if (rootStat.isDirectory()) {
|
|
186
|
+
const walked = await walkCandidateFiles(expanded, {
|
|
187
|
+
sourceId: input.id,
|
|
188
|
+
rootRole: root.role,
|
|
189
|
+
sinceMs: input.sinceMs,
|
|
190
|
+
includeTranscripts: input.includeTranscripts,
|
|
191
|
+
maxDepth: input.maxDepth,
|
|
192
|
+
maxFiles: input.maxFiles - files.length,
|
|
193
|
+
});
|
|
194
|
+
files.push(...walked.files);
|
|
195
|
+
truncated = truncated || walked.truncated;
|
|
196
|
+
} else if (rootStat.isFile()) {
|
|
197
|
+
const candidate = classifyCandidate(expanded, input.id, root.role);
|
|
198
|
+
if (candidate && passesFilters(candidate, rootStat, input.sinceMs, input.includeTranscripts)) {
|
|
199
|
+
files.push(toFileSummary(expanded, candidate.kind, rootStat));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (files.length >= input.maxFiles) {
|
|
203
|
+
truncated = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
id: input.id,
|
|
210
|
+
label: input.label,
|
|
211
|
+
exists: roots.some((root) => root.exists),
|
|
212
|
+
roots,
|
|
213
|
+
files,
|
|
214
|
+
truncated,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function scanRepoSource(input) {
|
|
219
|
+
if (!input.repoPath) {
|
|
220
|
+
return {
|
|
221
|
+
id: "repo",
|
|
222
|
+
label: "Repo rules",
|
|
223
|
+
exists: false,
|
|
224
|
+
roots: [],
|
|
225
|
+
files: [],
|
|
226
|
+
truncated: false,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return scanSource({
|
|
230
|
+
id: "repo",
|
|
231
|
+
label: "Repo rules",
|
|
232
|
+
roots: repoRuleRoots(input.repoPath),
|
|
233
|
+
sinceMs: input.sinceMs,
|
|
234
|
+
includeTranscripts: false,
|
|
235
|
+
maxDepth: input.maxDepth,
|
|
236
|
+
maxFiles: input.maxFiles,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function walkCandidateFiles(root, input, depth = 0) {
|
|
241
|
+
const files = [];
|
|
242
|
+
let truncated = false;
|
|
243
|
+
if (input.maxFiles <= 0) return { files, truncated: true };
|
|
244
|
+
|
|
245
|
+
let entries;
|
|
246
|
+
try {
|
|
247
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
248
|
+
} catch {
|
|
249
|
+
return { files, truncated: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (files.length >= input.maxFiles) {
|
|
254
|
+
truncated = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (shouldSkipEntry(entry.name)) continue;
|
|
258
|
+
const fullPath = path.join(root, entry.name);
|
|
259
|
+
if (entry.isDirectory()) {
|
|
260
|
+
if (depth >= input.maxDepth) continue;
|
|
261
|
+
const walked = await walkCandidateFiles(fullPath, input, depth + 1);
|
|
262
|
+
files.push(...walked.files.slice(0, input.maxFiles - files.length));
|
|
263
|
+
truncated = truncated || walked.truncated;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!entry.isFile()) continue;
|
|
267
|
+
const candidate = classifyCandidate(fullPath, input.sourceId, input.rootRole);
|
|
268
|
+
if (!candidate) continue;
|
|
269
|
+
const fileStat = await safeStat(fullPath);
|
|
270
|
+
if (!fileStat || !passesFilters(candidate, fileStat, input.sinceMs, input.includeTranscripts)) continue;
|
|
271
|
+
files.push(toFileSummary(fullPath, candidate.kind, fileStat));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { files, truncated };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function classifyCandidate(filePath, sourceId, rootRole) {
|
|
278
|
+
const base = path.basename(filePath).toLowerCase();
|
|
279
|
+
const ext = path.extname(base);
|
|
280
|
+
const normalized = filePath.split(path.sep).join("/");
|
|
281
|
+
|
|
282
|
+
if (sourceId === "repo") {
|
|
283
|
+
if (base === "agents.md" || base === "claude.md" || base === ".cursorrules") return { kind: "rule" };
|
|
284
|
+
if (normalized.endsWith("/.github/copilot-instructions.md")) return { kind: "rule" };
|
|
285
|
+
if (normalized.includes("/.cursor/rules/")) return { kind: "rule" };
|
|
286
|
+
if (normalized.endsWith("/.vscode/settings.json")) return { kind: "settings" };
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (base === "settings.json" || base === "keybindings.json" || base === "config.toml" || base === "config.json") {
|
|
291
|
+
return { kind: "settings" };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (base === "claude.json" || base === ".claude.json") return { kind: "settings" };
|
|
295
|
+
if (base === "agents.md" || base === "claude.md" || base === ".cursorrules") return { kind: "rule" };
|
|
296
|
+
if (rootRole === "transcripts" && TRANSCRIPT_EXTENSIONS.has(ext)) return { kind: "transcript" };
|
|
297
|
+
if (rootRole === "workspace-storage" && (TRANSCRIPT_EXTENSIONS.has(ext) || base.endsWith(".vscdb"))) {
|
|
298
|
+
return { kind: "transcript" };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function passesFilters(candidate, fileStat, sinceMs, includeTranscripts) {
|
|
305
|
+
if (!includeTranscripts && candidate.kind === "transcript") return false;
|
|
306
|
+
if (sinceMs && fileStat.mtimeMs < sinceMs) return false;
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function toFileSummary(filePath, kind, fileStat) {
|
|
311
|
+
return {
|
|
312
|
+
path: filePath,
|
|
313
|
+
kind,
|
|
314
|
+
bytes: fileStat.size,
|
|
315
|
+
modifiedAt: fileStat.mtime.toISOString(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function claudeRoots(home) {
|
|
320
|
+
return [
|
|
321
|
+
{ path: path.join(home, ".claude", "projects"), role: "transcripts" },
|
|
322
|
+
{ path: path.join(home, ".claude", "settings.json"), role: "settings" },
|
|
323
|
+
{ path: path.join(home, ".claude.json"), role: "settings" },
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function codexRoots(home) {
|
|
328
|
+
return [
|
|
329
|
+
{ path: path.join(home, ".codex", "sessions"), role: "transcripts" },
|
|
330
|
+
{ path: path.join(home, ".codex", "config.toml"), role: "settings" },
|
|
331
|
+
{ path: path.join(home, ".codex", "config.json"), role: "settings" },
|
|
332
|
+
{ path: path.join(home, ".codex", "AGENTS.md"), role: "rules" },
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cursorRoots(home) {
|
|
337
|
+
const base = appSupportPath(home, "Cursor");
|
|
338
|
+
return [
|
|
339
|
+
{ path: path.join(base, "User", "workspaceStorage"), role: "workspace-storage" },
|
|
340
|
+
{ path: path.join(base, "User", "globalStorage"), role: "workspace-storage" },
|
|
341
|
+
{ path: path.join(base, "User", "settings.json"), role: "settings" },
|
|
342
|
+
{ path: path.join(base, "User", "keybindings.json"), role: "settings" },
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function vscodeRoots(home) {
|
|
347
|
+
const base = appSupportPath(home, "Code");
|
|
348
|
+
return [
|
|
349
|
+
{ path: path.join(base, "User", "workspaceStorage"), role: "workspace-storage" },
|
|
350
|
+
{ path: path.join(base, "User", "globalStorage"), role: "workspace-storage" },
|
|
351
|
+
{ path: path.join(base, "User", "settings.json"), role: "settings" },
|
|
352
|
+
{ path: path.join(base, "User", "keybindings.json"), role: "settings" },
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function repoRuleRoots(repoPath) {
|
|
357
|
+
return [
|
|
358
|
+
{ path: path.join(repoPath, "AGENTS.md"), role: "rules" },
|
|
359
|
+
{ path: path.join(repoPath, "CLAUDE.md"), role: "rules" },
|
|
360
|
+
{ path: path.join(repoPath, ".cursorrules"), role: "rules" },
|
|
361
|
+
{ path: path.join(repoPath, ".cursor", "rules"), role: "rules" },
|
|
362
|
+
{ path: path.join(repoPath, ".github", "copilot-instructions.md"), role: "rules" },
|
|
363
|
+
{ path: path.join(repoPath, ".vscode", "settings.json"), role: "settings" },
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function appSupportPath(home, appName) {
|
|
368
|
+
if (platform() === "darwin") return path.join(home, "Library", "Application Support", appName);
|
|
369
|
+
if (platform() === "win32") {
|
|
370
|
+
const roaming = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
371
|
+
return path.join(roaming, appName);
|
|
372
|
+
}
|
|
373
|
+
return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), appName);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function findRepoRoot(start) {
|
|
377
|
+
let current = path.resolve(start);
|
|
378
|
+
while (true) {
|
|
379
|
+
if (await exists(path.join(current, ".git"))) return current;
|
|
380
|
+
if (await hasRepoRuleFile(current)) return current;
|
|
381
|
+
const parent = path.dirname(current);
|
|
382
|
+
if (parent === current) return null;
|
|
383
|
+
current = parent;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function hasRepoRuleFile(dir) {
|
|
388
|
+
const names = ["AGENTS.md", "CLAUDE.md", ".cursorrules"];
|
|
389
|
+
for (const name of names) {
|
|
390
|
+
if (await exists(path.join(dir, name))) return true;
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function expandHome(value, home = homedir()) {
|
|
396
|
+
if (value === "~") return home;
|
|
397
|
+
if (value.startsWith("~/")) return path.join(home, value.slice(2));
|
|
398
|
+
return path.resolve(value);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function exists(filePath) {
|
|
402
|
+
try {
|
|
403
|
+
await access(filePath);
|
|
404
|
+
return true;
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function safeStat(filePath) {
|
|
411
|
+
try {
|
|
412
|
+
return await stat(filePath);
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function shouldSkipEntry(name) {
|
|
419
|
+
return name === "node_modules" || name === ".git" || name === "vendor" || name === "dist" || name === "build";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function formatBytes(bytes) {
|
|
423
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
424
|
+
const kb = bytes / 1024;
|
|
425
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
426
|
+
const mb = kb / 1024;
|
|
427
|
+
if (mb < 1024) return `${mb.toFixed(1)} MB`;
|
|
428
|
+
return `${(mb / 1024).toFixed(1)} GB`;
|
|
429
|
+
}
|