@flarecode/import-memory 0.1.0 → 0.2.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/package.json +1 -1
- package/src/index.js +79 -8
- package/src/scanner.js +86 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,8 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { exec } from "node:child_process";
|
|
2
3
|
import process from "node:process";
|
|
3
|
-
import { formatScanSummary, parseSince, parseSourceFilter, scanSources } from "./scanner.js";
|
|
4
|
+
import { formatScanSummary, parseSince, parseSourceFilter, readRulesContent, scanSources } from "./scanner.js";
|
|
4
5
|
|
|
5
|
-
const VERSION = "0.
|
|
6
|
+
const VERSION = "0.2.0";
|
|
7
|
+
const API_BASE = "https://api.flarecode.sh";
|
|
8
|
+
const APP_BASE = "https://app.flarecode.sh";
|
|
9
|
+
|
|
10
|
+
async function startDeviceAuth() {
|
|
11
|
+
const res = await fetch(`${API_BASE}/cli/device/start`, { method: "POST" });
|
|
12
|
+
if (!res.ok) throw new Error(`Device auth failed: ${res.status}`);
|
|
13
|
+
return res.json();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function pollDeviceToken(deviceCode, timeoutMs = 10 * 60 * 1000) {
|
|
17
|
+
const deadline = Date.now() + timeoutMs;
|
|
18
|
+
while (Date.now() < deadline) {
|
|
19
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
20
|
+
const res = await fetch(`${API_BASE}/cli/device/poll?device_code=${deviceCode}`);
|
|
21
|
+
if (!res.ok) throw new Error(`Poll failed: ${res.status}`);
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
if (data.status === "confirmed") return data.token;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("Timed out waiting for browser confirmation (10 min).");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function openBrowser(url) {
|
|
29
|
+
const cmd =
|
|
30
|
+
process.platform === "darwin"
|
|
31
|
+
? `open "${url}"`
|
|
32
|
+
: process.platform === "win32"
|
|
33
|
+
? `start "" "${url}"`
|
|
34
|
+
: `xdg-open "${url}"`;
|
|
35
|
+
exec(cmd, () => {});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function uploadImport(token, scanResult, rulesContent) {
|
|
39
|
+
const createRes = await fetch(`${API_BASE}/memory-imports`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
42
|
+
body: JSON.stringify({ platform: scanResult.platform }),
|
|
43
|
+
});
|
|
44
|
+
if (!createRes.ok) throw new Error(`Create import failed: ${createRes.status}`);
|
|
45
|
+
const { id: importId } = await createRes.json();
|
|
46
|
+
|
|
47
|
+
const chunksRes = await fetch(`${API_BASE}/memory-imports/${importId}/chunks`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
50
|
+
body: JSON.stringify({ sourcesJson: JSON.stringify(rulesContent) }),
|
|
51
|
+
});
|
|
52
|
+
if (!chunksRes.ok) throw new Error(`Upload chunks failed: ${chunksRes.status}`);
|
|
53
|
+
|
|
54
|
+
const completeRes = await fetch(`${API_BASE}/memory-imports/${importId}/complete`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
57
|
+
});
|
|
58
|
+
if (!completeRes.ok) throw new Error(`Complete failed: ${completeRes.status}`);
|
|
59
|
+
|
|
60
|
+
return importId;
|
|
61
|
+
}
|
|
6
62
|
|
|
7
63
|
async function main(argv) {
|
|
8
64
|
const args = parseArgs(argv);
|
|
@@ -39,21 +95,34 @@ async function main(argv) {
|
|
|
39
95
|
process.stdout.write(`${formatScanSummary(result)}\n`);
|
|
40
96
|
process.stdout.write("\n");
|
|
41
97
|
|
|
42
|
-
if (args.dryRun) {
|
|
98
|
+
if (args.dryRun || args.skipUpload) {
|
|
43
99
|
process.stdout.write("Dry run complete. Nothing was uploaded.\n");
|
|
44
100
|
return;
|
|
45
101
|
}
|
|
46
102
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
103
|
+
try {
|
|
104
|
+
process.stdout.write("Starting secure browser auth...\n");
|
|
105
|
+
const { deviceCode, confirmUrl } = await startDeviceAuth();
|
|
106
|
+
process.stdout.write(`Opening: ${confirmUrl}\n`);
|
|
107
|
+
process.stdout.write(`If your browser doesn't open automatically, visit: ${confirmUrl}\n`);
|
|
108
|
+
openBrowser(confirmUrl);
|
|
109
|
+
process.stdout.write("Waiting for you to confirm in the browser...\n");
|
|
110
|
+
const token = await pollDeviceToken(deviceCode);
|
|
111
|
+
process.stdout.write("Authorized! Uploading scan summary...\n");
|
|
112
|
+
const rulesContent = await readRulesContent(result);
|
|
113
|
+
const importId = await uploadImport(token, result, rulesContent);
|
|
114
|
+
process.stdout.write(`Done. Review and approve your memory at: ${APP_BASE}/?memory=true\n`);
|
|
115
|
+
void importId;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
process.stderr.write(`Upload failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
}
|
|
52
120
|
}
|
|
53
121
|
|
|
54
122
|
function parseArgs(argv) {
|
|
55
123
|
const out = {
|
|
56
124
|
dryRun: false,
|
|
125
|
+
skipUpload: false,
|
|
57
126
|
json: false,
|
|
58
127
|
help: false,
|
|
59
128
|
version: false,
|
|
@@ -66,6 +135,7 @@ function parseArgs(argv) {
|
|
|
66
135
|
for (let i = 0; i < argv.length; i += 1) {
|
|
67
136
|
const arg = argv[i];
|
|
68
137
|
if (arg === "--dry-run") out.dryRun = true;
|
|
138
|
+
else if (arg === "--skip-upload") out.skipUpload = true;
|
|
69
139
|
else if (arg === "--json") out.json = true;
|
|
70
140
|
else if (arg === "--help" || arg === "-h") out.help = true;
|
|
71
141
|
else if (arg === "--version" || arg === "-v") out.version = true;
|
|
@@ -96,6 +166,7 @@ Usage:
|
|
|
96
166
|
|
|
97
167
|
Options:
|
|
98
168
|
--dry-run Scan and print a summary without preparing upload.
|
|
169
|
+
--skip-upload Scan and print a summary without uploading.
|
|
99
170
|
--json Print machine-readable scan output.
|
|
100
171
|
--source <list> Comma-separated: claude,codex,cursor,vscode,repo.
|
|
101
172
|
--since <duration> Only include files modified within 30d, 12w, 6m, or 1y.
|
package/src/scanner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, readdir, stat } from "node:fs/promises";
|
|
1
|
+
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir, platform } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
@@ -166,6 +166,91 @@ export function formatScanSummary(result) {
|
|
|
166
166
|
return lines.join("\n");
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export async function readRulesContent(scanResult) {
|
|
170
|
+
const out = { rulesContent: [], settingsSummary: [] };
|
|
171
|
+
const REDACT_PATTERNS = [
|
|
172
|
+
/sk-[a-zA-Z0-9]{20,}/g,
|
|
173
|
+
/npm_[a-zA-Z0-9]{20,}/g,
|
|
174
|
+
/ghp_[a-zA-Z0-9]{20,}/g,
|
|
175
|
+
/AIza[0-9A-Za-z-_]{35}/g,
|
|
176
|
+
/Bearer\s+[a-zA-Z0-9._-]{20,}/gi,
|
|
177
|
+
/token["\s:=]+["']?[a-zA-Z0-9._-]{20,}/gi,
|
|
178
|
+
/secret["\s:=]+["']?[a-zA-Z0-9._-]{20,}/gi,
|
|
179
|
+
/password["\s:=]+["']?\S+/gi,
|
|
180
|
+
/apikey["\s:=]+["']?\S+/gi,
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const redact = (text) => {
|
|
184
|
+
let redacted = text;
|
|
185
|
+
for (const pat of REDACT_PATTERNS) redacted = redacted.replace(pat, "[REDACTED]");
|
|
186
|
+
return redacted;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const source of scanResult.sources) {
|
|
190
|
+
for (const file of source.files) {
|
|
191
|
+
if (file.kind !== "rule" && file.kind !== "settings") continue;
|
|
192
|
+
if (file.bytes > 100_000) continue;
|
|
193
|
+
try {
|
|
194
|
+
const raw = await readFile(file.path, "utf8");
|
|
195
|
+
const content = redact(raw).slice(0, 8000);
|
|
196
|
+
if (file.kind === "rule") {
|
|
197
|
+
out.rulesContent.push({ source: source.id, path: file.path, content });
|
|
198
|
+
} else {
|
|
199
|
+
const summary = extractSettingsSummary(raw, source.id);
|
|
200
|
+
if (summary) out.settingsSummary.push({ source: source.id, content: summary });
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Unreadable files are skipped silently
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function extractSettingsSummary(raw, sourceId) {
|
|
211
|
+
if (sourceId === "claude") {
|
|
212
|
+
try {
|
|
213
|
+
const data = JSON.parse(raw);
|
|
214
|
+
const lines = [];
|
|
215
|
+
if (data.defaultModel) lines.push(`Default model: ${data.defaultModel}`);
|
|
216
|
+
if (data.theme) lines.push(`Theme: ${data.theme}`);
|
|
217
|
+
if (Array.isArray(data.enabledModels)) lines.push(`Enabled models: ${data.enabledModels.join(", ")}`);
|
|
218
|
+
return lines.length ? lines.join("\n") : null;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (sourceId === "codex") {
|
|
224
|
+
const lines = raw.split("\n").filter((l) => {
|
|
225
|
+
const lower = l.toLowerCase();
|
|
226
|
+
return !lower.includes("token") && !lower.includes("key") && !lower.includes("secret") && l.trim().length > 0;
|
|
227
|
+
});
|
|
228
|
+
return lines.slice(0, 20).join("\n") || null;
|
|
229
|
+
}
|
|
230
|
+
if (sourceId === "cursor" || sourceId === "vscode") {
|
|
231
|
+
try {
|
|
232
|
+
const data = JSON.parse(raw);
|
|
233
|
+
const interested = [
|
|
234
|
+
"editor.fontSize",
|
|
235
|
+
"editor.tabSize",
|
|
236
|
+
"editor.formatOnSave",
|
|
237
|
+
"files.autoSave",
|
|
238
|
+
"workbench.colorTheme",
|
|
239
|
+
"editor.wordWrap",
|
|
240
|
+
"editor.defaultFormatter",
|
|
241
|
+
];
|
|
242
|
+
const lines = [];
|
|
243
|
+
for (const key of interested) {
|
|
244
|
+
if (data[key] !== undefined) lines.push(`${key}: ${data[key]}`);
|
|
245
|
+
}
|
|
246
|
+
return lines.length ? lines.join("\n") : null;
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
169
254
|
async function scanSource(input) {
|
|
170
255
|
const roots = [];
|
|
171
256
|
const files = [];
|