@mnemonik/scanner 1.0.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,32 @@
1
+ export interface ScanPushFile {
2
+ path: string;
3
+ hash: string;
4
+ chunks: Array<{
5
+ content: string;
6
+ startLine: number;
7
+ endLine: number;
8
+ chunkType: string;
9
+ language: string;
10
+ contentHash: string;
11
+ metadata: {
12
+ fileName: string;
13
+ extension: string;
14
+ size: number;
15
+ };
16
+ }>;
17
+ }
18
+ export interface ScanPushPayload {
19
+ projectId: string;
20
+ files: ScanPushFile[];
21
+ }
22
+ export declare class MnemonikClient {
23
+ private serverUrl;
24
+ private apiKey;
25
+ constructor(serverUrl: string, apiKey: string);
26
+ private request;
27
+ getStatus(projectId: string): Promise<Map<string, string>>;
28
+ pushFiles(projectId: string, files: ScanPushFile[]): Promise<{
29
+ success: boolean;
30
+ }>;
31
+ healthCheck(): Promise<boolean>;
32
+ }
package/dist/client.js ADDED
@@ -0,0 +1,72 @@
1
+ export class MnemonikClient {
2
+ serverUrl;
3
+ apiKey;
4
+ constructor(serverUrl, apiKey) {
5
+ this.serverUrl = serverUrl;
6
+ this.apiKey = apiKey;
7
+ }
8
+ async request(path, body, retries = 3) {
9
+ const url = `${this.serverUrl}${path}`;
10
+ for (let attempt = 0; attempt <= retries; attempt++) {
11
+ try {
12
+ const res = await fetch(url, {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ Authorization: `Bearer ${this.apiKey}`,
17
+ },
18
+ body: JSON.stringify(body),
19
+ });
20
+ if (!res.ok) {
21
+ const text = await res.text().catch(() => 'Unknown error');
22
+ if (res.status === 401 || res.status === 403) {
23
+ throw new Error(`Auth failed (${res.status}). Check your API key.`);
24
+ }
25
+ if (res.status === 503 && attempt < retries) {
26
+ const retryAfter = parseInt(res.headers.get('Retry-After') || '5', 10);
27
+ await new Promise((r) => setTimeout(r, retryAfter * 1000));
28
+ continue;
29
+ }
30
+ if (res.status >= 500 && attempt < retries) {
31
+ await new Promise((r) => setTimeout(r, 2000 * (attempt + 1)));
32
+ continue;
33
+ }
34
+ throw new Error(`${res.status} ${res.statusText}: ${text}`);
35
+ }
36
+ return res.json();
37
+ }
38
+ catch (err) {
39
+ if (attempt < retries && err.message?.includes('fetch failed')) {
40
+ await new Promise((r) => setTimeout(r, 2000 * (attempt + 1)));
41
+ continue;
42
+ }
43
+ throw err;
44
+ }
45
+ }
46
+ throw new Error(`Request to ${path} failed after ${retries} retries`);
47
+ }
48
+ async getStatus(projectId) {
49
+ const result = await this.request('/api/v1/scan/status', { projectId });
50
+ return new Map(result.files.map((f) => [f.path, f.hash]));
51
+ }
52
+ async pushFiles(projectId, files) {
53
+ const batchSize = 25;
54
+ for (let i = 0; i < files.length; i += batchSize) {
55
+ const batch = files.slice(i, i + batchSize);
56
+ await this.request('/api/v1/scan/push', { projectId, files: batch });
57
+ }
58
+ return { success: true };
59
+ }
60
+ async healthCheck() {
61
+ try {
62
+ const res = await fetch(`${this.serverUrl}/api/v1/health`, {
63
+ headers: { Authorization: `Bearer ${this.apiKey}` },
64
+ });
65
+ return res.ok;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ }
72
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAuBA,MAAM,OAAO,cAAc;IAEf;IACA;IAFV,YACU,SAAiB,EACjB,MAAc;QADd,cAAS,GAAT,SAAS,CAAQ;QACjB,WAAM,GAAN,MAAM,CAAQ;IACrB,CAAC;IAEI,KAAK,CAAC,OAAO,CAAI,IAAY,EAAE,IAAa,EAAE,OAAO,GAAG,CAAC;QAC/D,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,EAAE,CAAC;QACvC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;YACpD,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;oBAC3B,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE;wBACP,cAAc,EAAE,kBAAkB;wBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;qBACvC;oBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;iBAC3B,CAAC,CAAC;gBAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;oBACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,CAAC;oBAC3D,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;wBAC7C,MAAM,IAAI,KAAK,CAAC,gBAAgB,GAAG,CAAC,MAAM,wBAAwB,CAAC,CAAC;oBACtE,CAAC;oBACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,OAAO,GAAG,OAAO,EAAE,CAAC;wBAC5C,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;wBACvE,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC;wBAC3D,SAAS;oBACX,CAAC;oBACD,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,IAAI,OAAO,GAAG,OAAO,EAAE,CAAC;wBAC3C,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;wBAC9D,SAAS;oBACX,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC,CAAC;gBAC9D,CAAC;gBAED,OAAO,GAAG,CAAC,IAAI,EAAgB,CAAC;YAClC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,OAAO,GAAG,OAAO,IAAK,GAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;oBAC1E,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;oBAC9D,SAAS;gBACX,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,iBAAiB,OAAO,UAAU,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,SAAiB;QAC/B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAC/B,qBAAqB,EACrB,EAAE,SAAS,EAAE,CACd,CAAC;QACF,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,SAAiB,EAAE,KAAqB;QACtD,MAAM,SAAS,GAAG,EAAE,CAAC;QACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC;YAC5C,MAAM,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,WAAW;QACf,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,gBAAgB,EAAE;gBACzD,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE,EAAE;aACpD,CAAC,CAAC;YACH,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ export interface DaemonConfig {
2
+ projectId: string;
3
+ projectRoot: string;
4
+ serverUrl: string;
5
+ apiKey: string;
6
+ }
7
+ export declare class ScannerDaemon {
8
+ private config;
9
+ private client;
10
+ private scanner;
11
+ private watcher;
12
+ private pendingRetries;
13
+ private retryTimer;
14
+ constructor(config: DaemonConfig);
15
+ start(): Promise<void>;
16
+ private waitForServer;
17
+ getProjectRoot(): string;
18
+ getProjectId(): string;
19
+ stop(): Promise<void>;
20
+ private startRetryLoop;
21
+ private initialScan;
22
+ private startWatching;
23
+ private handleChanges;
24
+ private groupChunksByFile;
25
+ }
package/dist/daemon.js ADDED
@@ -0,0 +1,181 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { CodeScanner } from '@mnemonik/shared';
5
+ import { MnemonikClient } from './client.js';
6
+ import { FileWatcher } from './watcher.js';
7
+ export class ScannerDaemon {
8
+ config;
9
+ client;
10
+ scanner;
11
+ watcher = null;
12
+ pendingRetries = new Set();
13
+ retryTimer = null;
14
+ constructor(config) {
15
+ this.config = config;
16
+ this.client = new MnemonikClient(config.serverUrl, config.apiKey);
17
+ this.scanner = new CodeScanner();
18
+ }
19
+ async start() {
20
+ console.log(`[scanner] Starting daemon for project: ${this.config.projectId}`);
21
+ console.log(`[scanner] Server: ${this.config.serverUrl}`);
22
+ console.log(`[scanner] Root: ${this.config.projectRoot}`);
23
+ await this.waitForServer();
24
+ await this.initialScan();
25
+ await this.startWatching();
26
+ }
27
+ async waitForServer() {
28
+ let logged = false;
29
+ let delay = 3000;
30
+ const maxDelay = 30000;
31
+ const maxRetries = 20;
32
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
33
+ if (await this.client.healthCheck()) {
34
+ if (logged)
35
+ console.log('[scanner] Server is back');
36
+ else
37
+ console.log('[scanner] Server health check passed');
38
+ return;
39
+ }
40
+ if (!logged) {
41
+ console.log('[scanner] Server unreachable, waiting...');
42
+ logged = true;
43
+ }
44
+ const jitter = delay * (0.5 + Math.random());
45
+ await new Promise((r) => setTimeout(r, jitter));
46
+ delay = Math.min(delay * 1.5, maxDelay);
47
+ }
48
+ throw new Error(`Server unreachable after ${maxRetries} attempts`);
49
+ }
50
+ getProjectRoot() {
51
+ return this.config.projectRoot;
52
+ }
53
+ getProjectId() {
54
+ return this.config.projectId;
55
+ }
56
+ async stop() {
57
+ this.watcher?.stop();
58
+ if (this.retryTimer)
59
+ clearInterval(this.retryTimer);
60
+ console.log('[scanner] Daemon stopped');
61
+ }
62
+ startRetryLoop() {
63
+ if (this.retryTimer)
64
+ return;
65
+ console.log(`[scanner] ${this.pendingRetries.size} file(s) queued for retry`);
66
+ this.retryTimer = setInterval(async () => {
67
+ if (this.pendingRetries.size === 0) {
68
+ if (this.retryTimer)
69
+ clearInterval(this.retryTimer);
70
+ this.retryTimer = null;
71
+ return;
72
+ }
73
+ const files = [...this.pendingRetries];
74
+ this.pendingRetries.clear();
75
+ await this.handleChanges(files);
76
+ }, 10_000);
77
+ this.retryTimer.unref();
78
+ }
79
+ async initialScan() {
80
+ console.log('[scanner] Starting initial scan...');
81
+ const startTime = Date.now();
82
+ const chunks = await this.scanner.scanDirectory(this.config.projectRoot);
83
+ console.log(`[scanner] Scanned ${chunks.length} chunks locally`);
84
+ const serverHashes = await this.client.getStatus(this.config.projectId);
85
+ console.log(`[scanner] Server knows ${serverHashes.size} files`);
86
+ const files = await this.groupChunksByFile(chunks);
87
+ const filesToPush = files.filter((f) => {
88
+ const serverHash = serverHashes.get(f.path);
89
+ return !serverHash || serverHash !== f.hash;
90
+ });
91
+ if (filesToPush.length === 0) {
92
+ console.log('[scanner] All files up to date, nothing to push');
93
+ }
94
+ else {
95
+ console.log(`[scanner] Pushing ${filesToPush.length} changed files...`);
96
+ await this.client.pushFiles(this.config.projectId, filesToPush);
97
+ console.log(`[scanner] Push complete`);
98
+ }
99
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
100
+ console.log(`[scanner] Initial scan complete in ${duration}s`);
101
+ }
102
+ async startWatching() {
103
+ this.watcher = new FileWatcher(this.config.projectRoot, (changedFiles) => this.handleChanges(changedFiles), 500);
104
+ await this.watcher.start();
105
+ }
106
+ async handleChanges(changedFiles) {
107
+ try {
108
+ const absPaths = changedFiles.map((rel) => join(this.config.projectRoot, rel));
109
+ const chunks = await this.scanner.scanFiles(absPaths, this.config.projectRoot);
110
+ if (chunks.length === 0)
111
+ return;
112
+ const files = await this.groupChunksByFile(chunks);
113
+ // Push in batches, only retrying files from failed batches
114
+ const batchSize = 25;
115
+ const succeededPaths = new Set();
116
+ let hadFailure = false;
117
+ for (let i = 0; i < files.length; i += batchSize) {
118
+ const batch = files.slice(i, i + batchSize);
119
+ try {
120
+ await this.client.pushFiles(this.config.projectId, batch);
121
+ for (const f of batch)
122
+ succeededPaths.add(f.path);
123
+ }
124
+ catch {
125
+ hadFailure = true;
126
+ for (const f of batch)
127
+ this.pendingRetries.add(f.path);
128
+ }
129
+ }
130
+ if (succeededPaths.size > 0) {
131
+ console.log(`[scanner] Pushed ${succeededPaths.size} changed file(s): ${[...succeededPaths].join(', ')}`);
132
+ }
133
+ if (hadFailure) {
134
+ console.warn(`[scanner] ${this.pendingRetries.size} file(s) failed, queued for retry`);
135
+ this.startRetryLoop();
136
+ }
137
+ }
138
+ catch (err) {
139
+ console.error('[scanner] Error handling changes:', err);
140
+ }
141
+ }
142
+ async groupChunksByFile(chunks) {
143
+ const fileMap = new Map();
144
+ for (const chunk of chunks) {
145
+ const key = chunk.filePath;
146
+ if (!fileMap.has(key)) {
147
+ fileMap.set(key, {
148
+ path: key,
149
+ hash: '',
150
+ chunks: [],
151
+ });
152
+ }
153
+ const file = fileMap.get(key);
154
+ file.chunks.push({
155
+ content: chunk.content,
156
+ startLine: chunk.startLine,
157
+ endLine: chunk.endLine,
158
+ chunkType: chunk.chunkType,
159
+ language: chunk.language,
160
+ contentHash: chunk.contentHash,
161
+ metadata: chunk.metadata,
162
+ });
163
+ }
164
+ for (const file of fileMap.values()) {
165
+ try {
166
+ const absPath = join(this.config.projectRoot, file.path);
167
+ const raw = await readFile(absPath, 'utf-8');
168
+ file.hash = createHash('sha256').update(raw).digest('hex');
169
+ }
170
+ catch (err) {
171
+ console.warn(`[scanner] Cannot read ${file.path} for hashing, using chunk-based fallback`, {
172
+ error: err instanceof Error ? err.message : String(err),
173
+ });
174
+ const allContent = file.chunks.map((c) => c.content).join('\n');
175
+ file.hash = createHash('sha256').update(allContent).digest('hex');
176
+ }
177
+ }
178
+ return Array.from(fileMap.values());
179
+ }
180
+ }
181
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.js","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, unlink, mkdir } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { ScannerDaemon } from './daemon.js';
6
+ const DEFAULT_SERVER = 'https://api.mnemonik.dev';
7
+ const MNEMONIK_DIR = join(homedir(), '.mnemonik');
8
+ const DAEMONS_DIR = join(MNEMONIK_DIR, 'daemons');
9
+ const LOGS_DIR = join(MNEMONIK_DIR, 'logs');
10
+ const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
11
+ function parseCliArgs() {
12
+ const args = process.argv.slice(2);
13
+ let server;
14
+ let key;
15
+ for (let i = 0; i < args.length; i++) {
16
+ if (args[i] === '--server' && args[i + 1])
17
+ server = args[++i];
18
+ else if (args[i] === '--key' && args[i + 1])
19
+ key = args[++i];
20
+ }
21
+ return { server, key };
22
+ }
23
+ async function readProjectId() {
24
+ const configPath = join(process.cwd(), '.mnemonik.json');
25
+ try {
26
+ const raw = await readFile(configPath, 'utf-8');
27
+ const parsed = JSON.parse(raw);
28
+ if (typeof parsed.projectId === 'string' && parsed.projectId.length > 0) {
29
+ return parsed.projectId;
30
+ }
31
+ console.error('[mnemonik] .mnemonik.json missing "projectId" field.');
32
+ process.exit(1);
33
+ }
34
+ catch {
35
+ console.error('[mnemonik] No .mnemonik.json found in current directory.\n' +
36
+ ' Run session_bootstrap from your IDE to create one.');
37
+ process.exit(1);
38
+ }
39
+ }
40
+ function lockFile(projectId) {
41
+ return join(DAEMONS_DIR, `${projectId}.pid`);
42
+ }
43
+ function logFile(projectId) {
44
+ return join(LOGS_DIR, `${projectId}.log`);
45
+ }
46
+ async function rotateLogIfNeeded(path) {
47
+ try {
48
+ const { stat: fsStat, rename } = await import('fs/promises');
49
+ const s = await fsStat(path);
50
+ if (s.size > MAX_LOG_SIZE) {
51
+ await rename(path, path + '.old');
52
+ }
53
+ }
54
+ catch {
55
+ // Log file doesn't exist yet
56
+ }
57
+ }
58
+ async function acquireLock(lockPath, retried = false) {
59
+ try {
60
+ const { open: fsOpen } = await import('fs/promises');
61
+ const { constants } = await import('fs');
62
+ const fd = await fsOpen(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o644);
63
+ await fd.writeFile(String(process.pid));
64
+ await fd.close();
65
+ return true;
66
+ }
67
+ catch (err) {
68
+ if (err.code !== 'EEXIST')
69
+ return false;
70
+ const existing = await readFile(lockPath, 'utf-8').catch(() => null);
71
+ if (existing) {
72
+ const pid = parseInt(existing.trim(), 10);
73
+ try {
74
+ process.kill(pid, 0);
75
+ return false; // Process alive, lock is valid
76
+ }
77
+ catch {
78
+ // Holder is dead — remove stale lock and retry once
79
+ }
80
+ }
81
+ if (retried)
82
+ return false;
83
+ await unlink(lockPath).catch(() => { });
84
+ return acquireLock(lockPath, true);
85
+ }
86
+ }
87
+ async function releaseLock(lockPath) {
88
+ await unlink(lockPath).catch(() => { });
89
+ }
90
+ async function main() {
91
+ const cli = parseCliArgs();
92
+ const projectId = await readProjectId();
93
+ const server = cli.server || DEFAULT_SERVER;
94
+ const apiKey = cli.key;
95
+ if (!apiKey) {
96
+ console.error('[mnemonik] Missing API key.\n' +
97
+ ' Usage: mnemonik-scanner --key <api-key>\n' +
98
+ ' The API key is provided by _scannerSetup in session_bootstrap.');
99
+ process.exit(1);
100
+ }
101
+ // Ensure directories exist
102
+ await mkdir(DAEMONS_DIR, { recursive: true });
103
+ await mkdir(LOGS_DIR, { recursive: true });
104
+ const lock = lockFile(projectId);
105
+ const log = logFile(projectId);
106
+ await rotateLogIfNeeded(log);
107
+ const locked = await acquireLock(lock);
108
+ if (!locked) {
109
+ console.log(`[mnemonik] Scanner already running for project ${projectId}. Exiting.`);
110
+ process.exit(0);
111
+ }
112
+ const projectRoot = process.cwd();
113
+ const daemon = new ScannerDaemon({
114
+ projectId,
115
+ projectRoot,
116
+ serverUrl: server,
117
+ apiKey,
118
+ });
119
+ const shutdown = async () => {
120
+ console.log('\n[mnemonik] Shutting down...');
121
+ await daemon.stop();
122
+ await releaseLock(lock);
123
+ process.exit(0);
124
+ };
125
+ process.on('SIGINT', shutdown);
126
+ process.on('SIGTERM', shutdown);
127
+ try {
128
+ await daemon.start();
129
+ console.log('[mnemonik] Watching for changes.');
130
+ }
131
+ catch (err) {
132
+ console.error(`[mnemonik] ${err.message}`);
133
+ await releaseLock(lock);
134
+ process.exit(1);
135
+ }
136
+ }
137
+ main();
138
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,cAAc,GAAG,0BAA0B,CAAC;AAClD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AAClD,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;AAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;AAC5C,MAAM,YAAY,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,MAAM;AAE5C,SAAS,YAAY;IACnB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,MAA0B,CAAC;IAC/B,IAAI,GAAuB,CAAC;IAE5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAAE,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAE,CAAC;aAC1D,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAAE,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAE,CAAC;IAChE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AACzB,CAAC;AAED,KAAK,UAAU,aAAa;IAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,gBAAgB,CAAC,CAAC;IACzD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;QAC1D,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxE,OAAO,MAAM,CAAC,SAAS,CAAC;QAC1B,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;QACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,KAAK,CACX,4DAA4D;YAC1D,sDAAsD,CACzD,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,SAAiB;IACjC,OAAO,IAAI,CAAC,WAAW,EAAE,GAAG,SAAS,MAAM,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,OAAO,CAAC,SAAiB;IAChC,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,SAAS,MAAM,CAAC,CAAC;AAC5C,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,IAAY;IAC3C,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAC7D,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,CAAC,IAAI,GAAG,YAAY,EAAE,CAAC;YAC1B,MAAM,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,6BAA6B;IAC/B,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,QAAgB,EAAE,OAAO,GAAG,KAAK;IAC1D,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACrD,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,EAAE,GAAG,MAAM,MAAM,CACrB,QAAQ,EACR,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,QAAQ,EACzD,KAAK,CACN,CAAC;QACF,MAAM,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACxC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAEnE,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACrE,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YAC1C,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACrB,OAAO,KAAK,CAAC,CAAC,+BAA+B;YAC/C,CAAC;YAAC,MAAM,CAAC;gBACP,oDAAoD;YACtD,CAAC;QACH,CAAC;QACD,IAAI,OAAO;YAAE,OAAO,KAAK,CAAC;QAC1B,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvC,OAAO,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,QAAgB;IACzC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,MAAM,SAAS,GAAG,MAAM,aAAa,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,cAAc,CAAC;IAC5C,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC;IAEvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CACX,+BAA+B;YAC7B,6CAA6C;YAC7C,kEAAkE,CACrE,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,2BAA2B;IAC3B,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3C,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAE/B,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAE7B,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,kDAAkD,SAAS,YAAY,CAAC,CAAC;QACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC;QAC/B,SAAS;QACT,WAAW;QACX,SAAS,EAAE,MAAM;QACjB,MAAM;KACP,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC7C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;QACxB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAClD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,cAAe,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACtD,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;QACxB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}
@@ -0,0 +1,14 @@
1
+ export type ChangeHandler = (changedFiles: string[]) => void;
2
+ export declare class FileWatcher {
3
+ private rootPath;
4
+ private onChange;
5
+ private watchers;
6
+ private pendingFiles;
7
+ private flushTimer;
8
+ private debounceMs;
9
+ constructor(rootPath: string, onChange: ChangeHandler, debounceMs?: number);
10
+ start(): Promise<void>;
11
+ stop(): void;
12
+ private scheduleFlush;
13
+ private watchDir;
14
+ }
@@ -0,0 +1,88 @@
1
+ import { watch } from 'fs';
2
+ import { join, relative } from 'path';
3
+ import { readdir } from 'fs/promises';
4
+ const SKIP_DIRS = new Set([
5
+ 'node_modules',
6
+ '.git',
7
+ 'dist',
8
+ 'build',
9
+ '.next',
10
+ '.nuxt',
11
+ '.output',
12
+ '__pycache__',
13
+ '.venv',
14
+ 'venv',
15
+ '.tox',
16
+ 'target',
17
+ '.cache',
18
+ 'coverage',
19
+ '.turbo',
20
+ '.vercel',
21
+ '.svelte-kit',
22
+ ]);
23
+ export class FileWatcher {
24
+ rootPath;
25
+ onChange;
26
+ watchers = [];
27
+ pendingFiles = new Set();
28
+ flushTimer = null;
29
+ debounceMs;
30
+ constructor(rootPath, onChange, debounceMs = 500) {
31
+ this.rootPath = rootPath;
32
+ this.onChange = onChange;
33
+ this.debounceMs = debounceMs;
34
+ }
35
+ async start() {
36
+ await this.watchDir(this.rootPath);
37
+ console.log(`[scanner] Watching ${this.rootPath} for changes`);
38
+ }
39
+ stop() {
40
+ for (const w of this.watchers) {
41
+ w.close();
42
+ }
43
+ this.watchers = [];
44
+ if (this.flushTimer) {
45
+ clearTimeout(this.flushTimer);
46
+ this.flushTimer = null;
47
+ }
48
+ this.pendingFiles.clear();
49
+ }
50
+ scheduleFlush() {
51
+ if (this.flushTimer)
52
+ clearTimeout(this.flushTimer);
53
+ this.flushTimer = setTimeout(() => {
54
+ this.flushTimer = null;
55
+ if (this.pendingFiles.size === 0)
56
+ return;
57
+ const batch = [...this.pendingFiles];
58
+ this.pendingFiles.clear();
59
+ this.onChange(batch);
60
+ }, this.debounceMs);
61
+ }
62
+ async watchDir(dir) {
63
+ const dirName = dir.split('/').pop() ?? '';
64
+ if (SKIP_DIRS.has(dirName))
65
+ return;
66
+ try {
67
+ const watcher = watch(dir, { persistent: true }, (_event, filename) => {
68
+ if (!filename)
69
+ return;
70
+ const fullPath = join(dir, filename);
71
+ const relPath = relative(this.rootPath, fullPath);
72
+ this.pendingFiles.add(relPath);
73
+ this.scheduleFlush();
74
+ });
75
+ this.watchers.push(watcher);
76
+ const entries = await readdir(dir, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
79
+ await this.watchDir(join(dir, entry.name));
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ // Permission denied or inaccessible directory
85
+ }
86
+ }
87
+ }
88
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@mnemonik/scanner",
3
+ "version": "1.0.0",
4
+ "description": "Mnemonik scanner daemon - automatic codebase indexing",
5
+ "type": "module",
6
+ "bin": {
7
+ "mnemonik-scanner": "./dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@mnemonik/shared": "1.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.3.3",
19
+ "@types/node": "^22.0.0"
20
+ }
21
+ }
package/src/client.ts ADDED
@@ -0,0 +1,99 @@
1
+ export interface ScanPushFile {
2
+ path: string;
3
+ hash: string;
4
+ chunks: Array<{
5
+ content: string;
6
+ startLine: number;
7
+ endLine: number;
8
+ chunkType: string;
9
+ language: string;
10
+ contentHash: string;
11
+ metadata: {
12
+ fileName: string;
13
+ extension: string;
14
+ size: number;
15
+ };
16
+ }>;
17
+ }
18
+
19
+ export interface ScanPushPayload {
20
+ projectId: string;
21
+ files: ScanPushFile[];
22
+ }
23
+
24
+ export class MnemonikClient {
25
+ constructor(
26
+ private serverUrl: string,
27
+ private apiKey: string
28
+ ) {}
29
+
30
+ private async request<T>(path: string, body: unknown, retries = 3): Promise<T> {
31
+ const url = `${this.serverUrl}${path}`;
32
+ for (let attempt = 0; attempt <= retries; attempt++) {
33
+ try {
34
+ const res = await fetch(url, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ Authorization: `Bearer ${this.apiKey}`,
39
+ },
40
+ body: JSON.stringify(body),
41
+ });
42
+
43
+ if (!res.ok) {
44
+ const text = await res.text().catch(() => 'Unknown error');
45
+ if (res.status === 401 || res.status === 403) {
46
+ throw new Error(`Auth failed (${res.status}). Check your API key.`);
47
+ }
48
+ if (res.status === 503 && attempt < retries) {
49
+ const retryAfter = parseInt(res.headers.get('Retry-After') || '5', 10);
50
+ await new Promise((r) => setTimeout(r, retryAfter * 1000));
51
+ continue;
52
+ }
53
+ if (res.status >= 500 && attempt < retries) {
54
+ await new Promise((r) => setTimeout(r, 2000 * (attempt + 1)));
55
+ continue;
56
+ }
57
+ throw new Error(`${res.status} ${res.statusText}: ${text}`);
58
+ }
59
+
60
+ return res.json() as Promise<T>;
61
+ } catch (err) {
62
+ if (attempt < retries && (err as Error).message?.includes('fetch failed')) {
63
+ await new Promise((r) => setTimeout(r, 2000 * (attempt + 1)));
64
+ continue;
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+ throw new Error(`Request to ${path} failed after ${retries} retries`);
70
+ }
71
+
72
+ async getStatus(projectId: string): Promise<Map<string, string>> {
73
+ const result = await this.request<{ files: Array<{ path: string; hash: string }> }>(
74
+ '/api/v1/scan/status',
75
+ { projectId }
76
+ );
77
+ return new Map(result.files.map((f) => [f.path, f.hash]));
78
+ }
79
+
80
+ async pushFiles(projectId: string, files: ScanPushFile[]): Promise<{ success: boolean }> {
81
+ const batchSize = 25;
82
+ for (let i = 0; i < files.length; i += batchSize) {
83
+ const batch = files.slice(i, i + batchSize);
84
+ await this.request('/api/v1/scan/push', { projectId, files: batch });
85
+ }
86
+ return { success: true };
87
+ }
88
+
89
+ async healthCheck(): Promise<boolean> {
90
+ try {
91
+ const res = await fetch(`${this.serverUrl}/api/v1/health`, {
92
+ headers: { Authorization: `Bearer ${this.apiKey}` },
93
+ });
94
+ return res.ok;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ }
package/src/daemon.ts ADDED
@@ -0,0 +1,207 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { CodeScanner, type CodeChunk } from '@mnemonik/shared';
5
+ import { MnemonikClient, type ScanPushFile } from './client.js';
6
+ import { FileWatcher } from './watcher.js';
7
+
8
+ export interface DaemonConfig {
9
+ projectId: string;
10
+ projectRoot: string;
11
+ serverUrl: string;
12
+ apiKey: string;
13
+ }
14
+
15
+ export class ScannerDaemon {
16
+ private client: MnemonikClient;
17
+ private scanner: CodeScanner;
18
+ private watcher: FileWatcher | null = null;
19
+ private pendingRetries: Set<string> = new Set();
20
+ private retryTimer: ReturnType<typeof setInterval> | null = null;
21
+
22
+ constructor(private config: DaemonConfig) {
23
+ this.client = new MnemonikClient(config.serverUrl, config.apiKey);
24
+ this.scanner = new CodeScanner();
25
+ }
26
+
27
+ async start(): Promise<void> {
28
+ console.log(`[scanner] Starting daemon for project: ${this.config.projectId}`);
29
+ console.log(`[scanner] Server: ${this.config.serverUrl}`);
30
+ console.log(`[scanner] Root: ${this.config.projectRoot}`);
31
+
32
+ await this.waitForServer();
33
+ await this.initialScan();
34
+ await this.startWatching();
35
+ }
36
+
37
+ private async waitForServer(): Promise<void> {
38
+ let logged = false;
39
+ let delay = 3000;
40
+ const maxDelay = 30000;
41
+ const maxRetries = 20;
42
+
43
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
44
+ if (await this.client.healthCheck()) {
45
+ if (logged) console.log('[scanner] Server is back');
46
+ else console.log('[scanner] Server health check passed');
47
+ return;
48
+ }
49
+ if (!logged) {
50
+ console.log('[scanner] Server unreachable, waiting...');
51
+ logged = true;
52
+ }
53
+ const jitter = delay * (0.5 + Math.random());
54
+ await new Promise((r) => setTimeout(r, jitter));
55
+ delay = Math.min(delay * 1.5, maxDelay);
56
+ }
57
+
58
+ throw new Error(`Server unreachable after ${maxRetries} attempts`);
59
+ }
60
+
61
+ getProjectRoot(): string {
62
+ return this.config.projectRoot;
63
+ }
64
+
65
+ getProjectId(): string {
66
+ return this.config.projectId;
67
+ }
68
+
69
+ async stop(): Promise<void> {
70
+ this.watcher?.stop();
71
+ if (this.retryTimer) clearInterval(this.retryTimer);
72
+ console.log('[scanner] Daemon stopped');
73
+ }
74
+
75
+ private startRetryLoop(): void {
76
+ if (this.retryTimer) return;
77
+ console.log(`[scanner] ${this.pendingRetries.size} file(s) queued for retry`);
78
+ this.retryTimer = setInterval(async () => {
79
+ if (this.pendingRetries.size === 0) {
80
+ if (this.retryTimer) clearInterval(this.retryTimer);
81
+ this.retryTimer = null;
82
+ return;
83
+ }
84
+ const files = [...this.pendingRetries];
85
+ this.pendingRetries.clear();
86
+ await this.handleChanges(files);
87
+ }, 10_000);
88
+ this.retryTimer.unref();
89
+ }
90
+
91
+ private async initialScan(): Promise<void> {
92
+ console.log('[scanner] Starting initial scan...');
93
+ const startTime = Date.now();
94
+
95
+ const chunks = await this.scanner.scanDirectory(this.config.projectRoot);
96
+ console.log(`[scanner] Scanned ${chunks.length} chunks locally`);
97
+
98
+ const serverHashes = await this.client.getStatus(this.config.projectId);
99
+ console.log(`[scanner] Server knows ${serverHashes.size} files`);
100
+
101
+ const files = await this.groupChunksByFile(chunks);
102
+ const filesToPush = files.filter((f) => {
103
+ const serverHash = serverHashes.get(f.path);
104
+ return !serverHash || serverHash !== f.hash;
105
+ });
106
+
107
+ if (filesToPush.length === 0) {
108
+ console.log('[scanner] All files up to date, nothing to push');
109
+ } else {
110
+ console.log(`[scanner] Pushing ${filesToPush.length} changed files...`);
111
+ await this.client.pushFiles(this.config.projectId, filesToPush);
112
+ console.log(`[scanner] Push complete`);
113
+ }
114
+
115
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
116
+ console.log(`[scanner] Initial scan complete in ${duration}s`);
117
+ }
118
+
119
+ private async startWatching(): Promise<void> {
120
+ this.watcher = new FileWatcher(
121
+ this.config.projectRoot,
122
+ (changedFiles) => this.handleChanges(changedFiles),
123
+ 500
124
+ );
125
+ await this.watcher.start();
126
+ }
127
+
128
+ private async handleChanges(changedFiles: string[]): Promise<void> {
129
+ try {
130
+ const absPaths = changedFiles.map((rel) => join(this.config.projectRoot, rel));
131
+ const chunks = await this.scanner.scanFiles(absPaths, this.config.projectRoot);
132
+
133
+ if (chunks.length === 0) return;
134
+
135
+ const files = await this.groupChunksByFile(chunks);
136
+
137
+ // Push in batches, only retrying files from failed batches
138
+ const batchSize = 25;
139
+ const succeededPaths = new Set<string>();
140
+ let hadFailure = false;
141
+
142
+ for (let i = 0; i < files.length; i += batchSize) {
143
+ const batch = files.slice(i, i + batchSize);
144
+ try {
145
+ await this.client.pushFiles(this.config.projectId, batch);
146
+ for (const f of batch) succeededPaths.add(f.path);
147
+ } catch {
148
+ hadFailure = true;
149
+ for (const f of batch) this.pendingRetries.add(f.path);
150
+ }
151
+ }
152
+
153
+ if (succeededPaths.size > 0) {
154
+ console.log(
155
+ `[scanner] Pushed ${succeededPaths.size} changed file(s): ${[...succeededPaths].join(', ')}`
156
+ );
157
+ }
158
+ if (hadFailure) {
159
+ console.warn(`[scanner] ${this.pendingRetries.size} file(s) failed, queued for retry`);
160
+ this.startRetryLoop();
161
+ }
162
+ } catch (err) {
163
+ console.error('[scanner] Error handling changes:', err);
164
+ }
165
+ }
166
+
167
+ private async groupChunksByFile(chunks: CodeChunk[]): Promise<ScanPushFile[]> {
168
+ const fileMap = new Map<string, ScanPushFile>();
169
+
170
+ for (const chunk of chunks) {
171
+ const key = chunk.filePath;
172
+ if (!fileMap.has(key)) {
173
+ fileMap.set(key, {
174
+ path: key,
175
+ hash: '',
176
+ chunks: [],
177
+ });
178
+ }
179
+ const file = fileMap.get(key)!;
180
+ file.chunks.push({
181
+ content: chunk.content,
182
+ startLine: chunk.startLine,
183
+ endLine: chunk.endLine,
184
+ chunkType: chunk.chunkType,
185
+ language: chunk.language,
186
+ contentHash: chunk.contentHash,
187
+ metadata: chunk.metadata,
188
+ });
189
+ }
190
+
191
+ for (const file of fileMap.values()) {
192
+ try {
193
+ const absPath = join(this.config.projectRoot, file.path);
194
+ const raw = await readFile(absPath, 'utf-8');
195
+ file.hash = createHash('sha256').update(raw).digest('hex');
196
+ } catch (err) {
197
+ console.warn(`[scanner] Cannot read ${file.path} for hashing, using chunk-based fallback`, {
198
+ error: err instanceof Error ? err.message : String(err),
199
+ });
200
+ const allContent = file.chunks.map((c) => c.content).join('\n');
201
+ file.hash = createHash('sha256').update(allContent).digest('hex');
202
+ }
203
+ }
204
+
205
+ return Array.from(fileMap.values());
206
+ }
207
+ }
package/src/index.ts ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, unlink, mkdir } from 'fs/promises';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ import { ScannerDaemon } from './daemon.js';
7
+
8
+ const DEFAULT_SERVER = 'https://api.mnemonik.dev';
9
+ const MNEMONIK_DIR = join(homedir(), '.mnemonik');
10
+ const DAEMONS_DIR = join(MNEMONIK_DIR, 'daemons');
11
+ const LOGS_DIR = join(MNEMONIK_DIR, 'logs');
12
+ const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
13
+
14
+ function parseCliArgs(): { server?: string; key?: string } {
15
+ const args = process.argv.slice(2);
16
+ let server: string | undefined;
17
+ let key: string | undefined;
18
+
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i] === '--server' && args[i + 1]) server = args[++i]!;
21
+ else if (args[i] === '--key' && args[i + 1]) key = args[++i]!;
22
+ }
23
+
24
+ return { server, key };
25
+ }
26
+
27
+ async function readProjectId(): Promise<string> {
28
+ const configPath = join(process.cwd(), '.mnemonik.json');
29
+ try {
30
+ const raw = await readFile(configPath, 'utf-8');
31
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
32
+ if (typeof parsed.projectId === 'string' && parsed.projectId.length > 0) {
33
+ return parsed.projectId;
34
+ }
35
+ console.error('[mnemonik] .mnemonik.json missing "projectId" field.');
36
+ process.exit(1);
37
+ } catch {
38
+ console.error(
39
+ '[mnemonik] No .mnemonik.json found in current directory.\n' +
40
+ ' Run session_bootstrap from your IDE to create one.'
41
+ );
42
+ process.exit(1);
43
+ }
44
+ }
45
+
46
+ function lockFile(projectId: string): string {
47
+ return join(DAEMONS_DIR, `${projectId}.pid`);
48
+ }
49
+
50
+ function logFile(projectId: string): string {
51
+ return join(LOGS_DIR, `${projectId}.log`);
52
+ }
53
+
54
+ async function rotateLogIfNeeded(path: string): Promise<void> {
55
+ try {
56
+ const { stat: fsStat, rename } = await import('fs/promises');
57
+ const s = await fsStat(path);
58
+ if (s.size > MAX_LOG_SIZE) {
59
+ await rename(path, path + '.old');
60
+ }
61
+ } catch {
62
+ // Log file doesn't exist yet
63
+ }
64
+ }
65
+
66
+ async function acquireLock(lockPath: string, retried = false): Promise<boolean> {
67
+ try {
68
+ const { open: fsOpen } = await import('fs/promises');
69
+ const { constants } = await import('fs');
70
+ const fd = await fsOpen(
71
+ lockPath,
72
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
73
+ 0o644
74
+ );
75
+ await fd.writeFile(String(process.pid));
76
+ await fd.close();
77
+ return true;
78
+ } catch (err: unknown) {
79
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') return false;
80
+
81
+ const existing = await readFile(lockPath, 'utf-8').catch(() => null);
82
+ if (existing) {
83
+ const pid = parseInt(existing.trim(), 10);
84
+ try {
85
+ process.kill(pid, 0);
86
+ return false; // Process alive, lock is valid
87
+ } catch {
88
+ // Holder is dead — remove stale lock and retry once
89
+ }
90
+ }
91
+ if (retried) return false;
92
+ await unlink(lockPath).catch(() => {});
93
+ return acquireLock(lockPath, true);
94
+ }
95
+ }
96
+
97
+ async function releaseLock(lockPath: string): Promise<void> {
98
+ await unlink(lockPath).catch(() => {});
99
+ }
100
+
101
+ async function main(): Promise<void> {
102
+ const cli = parseCliArgs();
103
+ const projectId = await readProjectId();
104
+ const server = cli.server || DEFAULT_SERVER;
105
+ const apiKey = cli.key;
106
+
107
+ if (!apiKey) {
108
+ console.error(
109
+ '[mnemonik] Missing API key.\n' +
110
+ ' Usage: mnemonik-scanner --key <api-key>\n' +
111
+ ' The API key is provided by _scannerSetup in session_bootstrap.'
112
+ );
113
+ process.exit(1);
114
+ }
115
+
116
+ // Ensure directories exist
117
+ await mkdir(DAEMONS_DIR, { recursive: true });
118
+ await mkdir(LOGS_DIR, { recursive: true });
119
+
120
+ const lock = lockFile(projectId);
121
+ const log = logFile(projectId);
122
+
123
+ await rotateLogIfNeeded(log);
124
+
125
+ const locked = await acquireLock(lock);
126
+ if (!locked) {
127
+ console.log(`[mnemonik] Scanner already running for project ${projectId}. Exiting.`);
128
+ process.exit(0);
129
+ }
130
+
131
+ const projectRoot = process.cwd();
132
+ const daemon = new ScannerDaemon({
133
+ projectId,
134
+ projectRoot,
135
+ serverUrl: server,
136
+ apiKey,
137
+ });
138
+
139
+ const shutdown = async () => {
140
+ console.log('\n[mnemonik] Shutting down...');
141
+ await daemon.stop();
142
+ await releaseLock(lock);
143
+ process.exit(0);
144
+ };
145
+
146
+ process.on('SIGINT', shutdown);
147
+ process.on('SIGTERM', shutdown);
148
+
149
+ try {
150
+ await daemon.start();
151
+ console.log('[mnemonik] Watching for changes.');
152
+ } catch (err) {
153
+ console.error(`[mnemonik] ${(err as Error).message}`);
154
+ await releaseLock(lock);
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ main();
package/src/watcher.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { watch, type FSWatcher } from 'fs';
2
+ import { join, relative } from 'path';
3
+ import { readdir, stat } from 'fs/promises';
4
+
5
+ const SKIP_DIRS = new Set([
6
+ 'node_modules',
7
+ '.git',
8
+ 'dist',
9
+ 'build',
10
+ '.next',
11
+ '.nuxt',
12
+ '.output',
13
+ '__pycache__',
14
+ '.venv',
15
+ 'venv',
16
+ '.tox',
17
+ 'target',
18
+ '.cache',
19
+ 'coverage',
20
+ '.turbo',
21
+ '.vercel',
22
+ '.svelte-kit',
23
+ ]);
24
+
25
+ export type ChangeHandler = (changedFiles: string[]) => void;
26
+
27
+ export class FileWatcher {
28
+ private watchers: FSWatcher[] = [];
29
+ private pendingFiles = new Set<string>();
30
+ private flushTimer: ReturnType<typeof setTimeout> | null = null;
31
+ private debounceMs: number;
32
+
33
+ constructor(
34
+ private rootPath: string,
35
+ private onChange: ChangeHandler,
36
+ debounceMs = 500
37
+ ) {
38
+ this.debounceMs = debounceMs;
39
+ }
40
+
41
+ async start(): Promise<void> {
42
+ await this.watchDir(this.rootPath);
43
+ console.log(`[scanner] Watching ${this.rootPath} for changes`);
44
+ }
45
+
46
+ stop(): void {
47
+ for (const w of this.watchers) {
48
+ w.close();
49
+ }
50
+ this.watchers = [];
51
+ if (this.flushTimer) {
52
+ clearTimeout(this.flushTimer);
53
+ this.flushTimer = null;
54
+ }
55
+ this.pendingFiles.clear();
56
+ }
57
+
58
+ private scheduleFlush(): void {
59
+ if (this.flushTimer) clearTimeout(this.flushTimer);
60
+ this.flushTimer = setTimeout(() => {
61
+ this.flushTimer = null;
62
+ if (this.pendingFiles.size === 0) return;
63
+ const batch = [...this.pendingFiles];
64
+ this.pendingFiles.clear();
65
+ this.onChange(batch);
66
+ }, this.debounceMs);
67
+ }
68
+
69
+ private async watchDir(dir: string): Promise<void> {
70
+ const dirName = dir.split('/').pop() ?? '';
71
+ if (SKIP_DIRS.has(dirName)) return;
72
+
73
+ try {
74
+ const watcher = watch(dir, { persistent: true }, (_event, filename) => {
75
+ if (!filename) return;
76
+ const fullPath = join(dir, filename);
77
+ const relPath = relative(this.rootPath, fullPath);
78
+ this.pendingFiles.add(relPath);
79
+ this.scheduleFlush();
80
+ });
81
+
82
+ this.watchers.push(watcher);
83
+
84
+ const entries = await readdir(dir, { withFileTypes: true });
85
+ for (const entry of entries) {
86
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
87
+ await this.watchDir(join(dir, entry.name));
88
+ }
89
+ }
90
+ } catch {
91
+ // Permission denied or inaccessible directory
92
+ }
93
+ }
94
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "sourceMap": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }