@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flarecode/import-memory",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "One-command helper for discovering local coding-agent memory sources before importing them into FlareCode.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
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.1.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
- 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");
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 = [];