creditdaddy 0.8.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.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 本机 Qoder 检测 — 安装探测 + 凭据扫描。
3
+ *
4
+ * Qoder IDE 是 VS Code 系(Electron),凭据散落在 userData 目录;
5
+ * 另扫 ~/.qoder(CLI)。扫描纯本地进行,候选 token 只保留在内存,
6
+ * 面板按 candidateId 导入,token 不在日志中输出。
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import fsp from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import os from 'node:os';
13
+ import crypto from 'node:crypto';
14
+
15
+ // 候选内存表:candidateId → { token, file?, provider?, user?, refreshToken?, expiresAt?, at }(10 分钟过期)
16
+ const candidates = new Map();
17
+ const CAND_TTL_MS = 10 * 60 * 1000;
18
+
19
+ const TOKEN_RE = /\b(?:dt|pt)-[A-Za-z0-9_-]{16,}\b/g;
20
+
21
+ /** 纯函数:提取文本中的 Qoder token(去重)——供测试 */
22
+ export function extractTokens(text) {
23
+ const found = new Set();
24
+ let m;
25
+ TOKEN_RE.lastIndex = 0;
26
+ while ((m = TOKEN_RE.exec(String(text))) !== null) found.add(m[0]);
27
+ return [...found];
28
+ }
29
+
30
+ function mask(token) {
31
+ return token.slice(0, 5) + '...' + token.slice(-4);
32
+ }
33
+
34
+ function isWindows() { return process.platform === 'win32'; }
35
+ function isMac() { return process.platform === 'darwin'; }
36
+
37
+ /** 安装探测:IDE 可执行文件 + CLI */
38
+ export function detectInstalls() {
39
+ const home = os.homedir();
40
+ const appdata = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
41
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
42
+ const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
43
+
44
+ const idePaths = isMac()
45
+ ? ['/Applications/Qoder.app']
46
+ : isWindows()
47
+ ? [
48
+ path.join(localAppData, 'Programs', 'Qoder', 'Qoder.exe'),
49
+ path.join(programFiles, 'Qoder', 'Qoder.exe'),
50
+ ]
51
+ : ['/usr/bin/qoder', '/opt/Qoder/qoder', '/usr/share/qoder/qoder'];
52
+
53
+ const ideDataDirs = isMac()
54
+ ? [path.join(home, 'Library', 'Application Support', 'Qoder')]
55
+ : isWindows()
56
+ ? [path.join(appdata, 'Qoder')]
57
+ : [path.join(home, '.config', 'Qoder')];
58
+
59
+ const exists = (p) => { try { return fs.existsSync(p); } catch { return false; } };
60
+
61
+ return {
62
+ platform: process.platform,
63
+ ide: idePaths.map((p) => ({ path: p, exists: exists(p) })),
64
+ ideDataDirs: ideDataDirs.map((p) => ({ path: p, exists: exists(p) })),
65
+ cliDir: { path: path.join(home, '.qoder'), exists: exists(path.join(home, '.qoder')) },
66
+ installed: idePaths.some((p) => exists(p)),
67
+ };
68
+ }
69
+
70
+ // 扫描时跳过的目录(缓存/噪音)
71
+ const SKIP_DIRS = new Set([
72
+ 'node_modules', '.git', 'Cache', 'CachedData', 'CacheStorage', 'Code Cache',
73
+ 'GPUCache', 'logs', 'Crashes', 'crashpad', 'blob_storage', 'Service Worker',
74
+ 'History', 'DawnGraphiteCache', 'DawnWebGPUCache', 'SharedDictionary',
75
+ 'index', 'tmp', 'temp', 'workspaceStorage',
76
+ ]);
77
+
78
+ const MAX_FILE_BYTES = 1.5 * 1024 * 1024;
79
+ const MAX_FILES = 4000;
80
+ const MAX_MS = 6000;
81
+
82
+ /** 扫描目录集合,产出候选 token(含少量上下文以辅助辨认) */
83
+ export async function scanLocalTokens(dirs) {
84
+ const deadline = Date.now() + MAX_MS;
85
+ const found = new Map(); // token → { file, count }
86
+ let scanned = 0;
87
+
88
+ async function walk(dir, depth) {
89
+ if (depth > 5 || scanned >= MAX_FILES || Date.now() > deadline) return;
90
+ let entries;
91
+ try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return; }
92
+ for (const e of entries) {
93
+ if (scanned >= MAX_FILES || Date.now() > deadline) return;
94
+ const p = path.join(dir, e.name);
95
+ if (e.isDirectory()) {
96
+ if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) await walk(p, depth + 1);
97
+ continue;
98
+ }
99
+ let st;
100
+ try { st = await fsp.stat(p); } catch { continue; }
101
+ if (!st.isFile() || st.size === 0 || st.size > MAX_FILE_BYTES) continue;
102
+ scanned++;
103
+ let text;
104
+ try { text = await fsp.readFile(p, 'utf8'); } catch { continue; }
105
+ for (const token of extractTokens(text)) {
106
+ const prev = found.get(token);
107
+ if (prev) prev.count++;
108
+ else found.set(token, { file: p, count: 1 });
109
+ }
110
+ }
111
+ }
112
+
113
+ for (const d of dirs) {
114
+ try { if (fs.existsSync(d) && fs.statSync(d).isDirectory()) await walk(d, 0); } catch {}
115
+ }
116
+
117
+ const list = [];
118
+ for (const [token, meta] of found) {
119
+ const id = putCandidate({ token, file: meta.file });
120
+ list.push({
121
+ id,
122
+ tokenMasked: mask(token),
123
+ kind: token.startsWith('pt-') ? 'PAT' : 'device',
124
+ file: meta.file,
125
+ hits: meta.count,
126
+ });
127
+ }
128
+ list.sort((a, b) => b.hits - a.hits);
129
+ return { scanned, candidates: list.slice(0, 50) };
130
+ }
131
+
132
+ /** 放入候选表(带 TTL),返回 candidateId。c 至少含 token,可带 provider / user / refreshToken / expiresAt */
133
+ export function putCandidate(c) {
134
+ const now = Date.now();
135
+ for (const [k, v] of candidates) if (v.at < now - CAND_TTL_MS) candidates.delete(k);
136
+ const id = crypto.createHash('sha256').update(c.token).digest('hex').slice(0, 16);
137
+ candidates.set(id, { ...c, at: now });
138
+ return id;
139
+ }
140
+
141
+ /** 按 candidateId 取回完整候选(导入用),过期返回 null */
142
+ export function takeCandidate(id) {
143
+ const c = candidates.get(id);
144
+ if (!c) return null;
145
+ if (c.at < Date.now() - CAND_TTL_MS) { candidates.delete(id); return null; }
146
+ return c;
147
+ }
package/src/logger.js ADDED
@@ -0,0 +1,27 @@
1
+ /** 轻量日志:环形缓冲(供面板查看)+ 控制台输出 */
2
+
3
+ const RING_SIZE = 300;
4
+ const ring = [];
5
+
6
+ function ts() {
7
+ return new Date().toISOString().replace('T', ' ').slice(0, 19);
8
+ }
9
+
10
+ export function log(level, tag, msg) {
11
+ const line = { at: ts(), level, tag, msg: String(msg) };
12
+ ring.push(line);
13
+ if (ring.length > RING_SIZE) ring.shift();
14
+ const prefix = level === 'error' ? '✗' : level === 'warn' ? '!' : '·';
15
+ console.error(`[${line.at}] ${prefix} [${tag}] ${line.msg}`);
16
+ }
17
+
18
+ export const logger = {
19
+ info: (tag, msg) => log('info', tag, msg),
20
+ warn: (tag, msg) => log('warn', tag, msg),
21
+ error: (tag, msg) => log('error', tag, msg),
22
+ debug: (tag, msg) => log('debug', tag, msg),
23
+ };
24
+
25
+ export function getLogs(limit = 100) {
26
+ return ring.slice(-limit);
27
+ }