@shahmilsaari/memory-core 1.0.22 → 1.0.26

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,301 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/db.ts
4
+ import pg from "pg";
5
+ import { createHash } from "crypto";
6
+
7
+ // src/config.ts
8
+ import { config } from "dotenv";
9
+ import { join } from "path";
10
+ var localEnv = join(process.cwd(), ".memory-core.env");
11
+ config({ path: localEnv });
12
+ var Config = {
13
+ get databaseUrl() {
14
+ return process.env.DATABASE_URL ?? "";
15
+ },
16
+ get ollamaUrl() {
17
+ return process.env.OLLAMA_URL ?? "http://localhost:11434";
18
+ },
19
+ get ollamaModel() {
20
+ return process.env.OLLAMA_MODEL ?? "nomic-embed-text";
21
+ },
22
+ get chatModel() {
23
+ return process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
24
+ },
25
+ get chatProvider() {
26
+ return process.env.CHAT_PROVIDER ?? "ollama";
27
+ },
28
+ get chatApiKey() {
29
+ return process.env.CHAT_API_KEY ?? "";
30
+ }
31
+ };
32
+
33
+ // src/db.ts
34
+ var { Pool } = pg;
35
+ var pool = null;
36
+ var migrationsRun = false;
37
+ function readPositiveIntEnv(name, fallback) {
38
+ const raw = Number(process.env[name]);
39
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
40
+ }
41
+ function hashMemoryContent(content) {
42
+ return createHash("md5").update(content.trim()).digest("hex");
43
+ }
44
+ function getPool() {
45
+ if (!pool) {
46
+ if (!Config.databaseUrl) {
47
+ throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
48
+ }
49
+ const timeoutMs = readPositiveIntEnv("DATABASE_TIMEOUT_MS", 5e3);
50
+ pool = new Pool({
51
+ connectionString: Config.databaseUrl,
52
+ connectionTimeoutMillis: timeoutMs,
53
+ query_timeout: timeoutMs,
54
+ statement_timeout: timeoutMs
55
+ });
56
+ }
57
+ return pool;
58
+ }
59
+ async function runMigrations() {
60
+ if (migrationsRun) return;
61
+ const client = await getPool().connect();
62
+ try {
63
+ await client.query("BEGIN");
64
+ await client.query(`ALTER TABLE memories ALTER COLUMN scope SET DEFAULT 'project'`);
65
+ await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`);
66
+ await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`);
67
+ await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS context JSONB NOT NULL DEFAULT '{}'::jsonb`);
68
+ await client.query(
69
+ `UPDATE memories
70
+ SET content_hash = md5(trim(content))
71
+ WHERE content_hash IS NULL`
72
+ );
73
+ await client.query(`CREATE INDEX IF NOT EXISTS memories_content_hash_idx ON memories (content_hash)`);
74
+ await client.query("COMMIT");
75
+ migrationsRun = true;
76
+ } catch (err) {
77
+ await client.query("ROLLBACK");
78
+ throw err;
79
+ } finally {
80
+ client.release();
81
+ }
82
+ }
83
+ async function saveMemory(memory) {
84
+ await runMigrations();
85
+ const { type, scope, architecture, projectName, title, content, reason, context, tags, embedding } = memory;
86
+ const contentHash = hashMemoryContent(content);
87
+ await getPool().query(
88
+ `INSERT INTO memories (type, scope, architecture, project_name, title, content, reason, context, tags, embedding, content_hash)
89
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)`,
90
+ [
91
+ type,
92
+ scope,
93
+ architecture ?? null,
94
+ projectName ?? null,
95
+ title ?? null,
96
+ content,
97
+ reason ?? null,
98
+ JSON.stringify(context ?? {}),
99
+ tags ?? [],
100
+ `[${embedding.join(",")}]`,
101
+ contentHash
102
+ ]
103
+ );
104
+ }
105
+ async function upsertMemory(memory) {
106
+ await runMigrations();
107
+ const contentHash = hashMemoryContent(memory.content);
108
+ const existing = await getPool().query(
109
+ `SELECT id FROM memories
110
+ WHERE content_hash = $1
111
+ AND COALESCE(architecture, '') = COALESCE($2, '')
112
+ AND scope = $3
113
+ AND type = $4
114
+ LIMIT 1`,
115
+ [contentHash, memory.architecture ?? null, memory.scope, memory.type]
116
+ );
117
+ if (existing.rowCount) return "skipped";
118
+ await saveMemory(memory);
119
+ return "inserted";
120
+ }
121
+ async function listMemories(filters = {}) {
122
+ await runMigrations();
123
+ const where = [];
124
+ const params = [];
125
+ if (filters.type) {
126
+ params.push(filters.type);
127
+ where.push(`type = $${params.length}`);
128
+ }
129
+ if (filters.scope) {
130
+ params.push(filters.scope);
131
+ where.push(`scope = $${params.length}`);
132
+ }
133
+ if (filters.architecture) {
134
+ if (Array.isArray(filters.architecture)) {
135
+ params.push(filters.architecture);
136
+ where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = ANY($${params.length}))` : `architecture = ANY($${params.length})`);
137
+ } else {
138
+ params.push(filters.architecture);
139
+ where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = $${params.length})` : `architecture = $${params.length}`);
140
+ }
141
+ }
142
+ if (filters.projectName) {
143
+ params.push(filters.projectName);
144
+ where.push(filters.includeGlobal ? `(project_name IS NULL OR project_name = $${params.length})` : `project_name = $${params.length}`);
145
+ }
146
+ if (filters.tags?.length) {
147
+ params.push(filters.tags);
148
+ where.push(`tags && $${params.length}::text[]`);
149
+ }
150
+ const limit = filters.limit ?? 200;
151
+ params.push(limit);
152
+ const result = await getPool().query(
153
+ `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
154
+ FROM memories
155
+ ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
156
+ ORDER BY id ASC
157
+ LIMIT $${params.length}`,
158
+ params
159
+ );
160
+ return result.rows;
161
+ }
162
+ async function getMemory(id) {
163
+ await runMigrations();
164
+ const result = await getPool().query(
165
+ `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
166
+ FROM memories
167
+ WHERE id = $1`,
168
+ [id]
169
+ );
170
+ return result.rows[0] ?? null;
171
+ }
172
+ async function deleteMemory(id) {
173
+ await runMigrations();
174
+ const result = await getPool().query(`DELETE FROM memories WHERE id = $1`, [id]);
175
+ return (result.rowCount ?? 0) > 0;
176
+ }
177
+ async function deleteMemories(filters) {
178
+ await runMigrations();
179
+ const where = [];
180
+ const params = [];
181
+ if (filters.type) {
182
+ params.push(filters.type);
183
+ where.push(`type = $${params.length}`);
184
+ }
185
+ if (filters.scope) {
186
+ params.push(filters.scope);
187
+ where.push(`scope = $${params.length}`);
188
+ }
189
+ if (filters.architecture) {
190
+ if (Array.isArray(filters.architecture)) {
191
+ params.push(filters.architecture);
192
+ where.push(`architecture = ANY($${params.length})`);
193
+ } else {
194
+ params.push(filters.architecture);
195
+ where.push(`architecture = $${params.length}`);
196
+ }
197
+ }
198
+ if (filters.tag) {
199
+ params.push(filters.tag);
200
+ where.push(`$${params.length} = ANY(tags)`);
201
+ }
202
+ if (where.length === 0) {
203
+ throw new Error("Refusing to bulk-delete without filters");
204
+ }
205
+ const result = await getPool().query(
206
+ `DELETE FROM memories WHERE ${where.join(" AND ")}`,
207
+ params
208
+ );
209
+ return result.rowCount ?? 0;
210
+ }
211
+ async function updateMemory(id, patch) {
212
+ await runMigrations();
213
+ const current = await getMemory(id);
214
+ if (!current) return null;
215
+ const content = patch.content ?? current.content;
216
+ const contentHash = hashMemoryContent(content);
217
+ const embedding = patch.embedding ? `[${patch.embedding.join(",")}]` : null;
218
+ const result = await getPool().query(
219
+ `UPDATE memories
220
+ SET type = $2,
221
+ scope = $3,
222
+ title = $4,
223
+ content = $5,
224
+ reason = $6,
225
+ context = $7::jsonb,
226
+ tags = $8,
227
+ content_hash = $9,
228
+ embedding = COALESCE($10::vector, embedding)
229
+ WHERE id = $1
230
+ RETURNING id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash`,
231
+ [
232
+ id,
233
+ patch.type ?? current.type,
234
+ patch.scope ?? current.scope,
235
+ patch.title ?? current.title ?? null,
236
+ content,
237
+ patch.reason ?? current.reason ?? null,
238
+ JSON.stringify(patch.context ?? current.context ?? {}),
239
+ patch.tags ?? current.tags ?? [],
240
+ contentHash,
241
+ embedding
242
+ ]
243
+ );
244
+ return result.rows[0] ?? null;
245
+ }
246
+ async function searchMemories(embedding, architectures, limit = 10) {
247
+ await runMigrations();
248
+ const vector = `[${embedding.join(",")}]`;
249
+ const params = [vector];
250
+ let whereClause = "";
251
+ const selectedArchitectures = architectures ? (Array.isArray(architectures) ? architectures : [architectures]).filter(Boolean) : [];
252
+ if (selectedArchitectures.length > 0) {
253
+ whereClause = `WHERE (
254
+ architecture = ANY($2)
255
+ OR architecture IS NULL
256
+ OR architecture = 'global'
257
+ )`;
258
+ params.push(selectedArchitectures);
259
+ }
260
+ const client = await getPool().connect();
261
+ try {
262
+ await client.query("BEGIN");
263
+ await client.query("SET LOCAL ivfflat.probes = 10");
264
+ const result = await client.query(
265
+ `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags,
266
+ 1 - (embedding <=> $1) AS similarity
267
+ FROM memories
268
+ ${whereClause}
269
+ ORDER BY embedding <=> $1
270
+ LIMIT $${params.length + 1}`,
271
+ [...params, limit]
272
+ );
273
+ await client.query("COMMIT");
274
+ return result.rows;
275
+ } finally {
276
+ client.release();
277
+ }
278
+ }
279
+ async function closePool() {
280
+ if (pool) {
281
+ await pool.end();
282
+ pool = null;
283
+ migrationsRun = false;
284
+ }
285
+ }
286
+
287
+ export {
288
+ Config,
289
+ hashMemoryContent,
290
+ getPool,
291
+ runMigrations,
292
+ saveMemory,
293
+ upsertMemory,
294
+ listMemories,
295
+ getMemory,
296
+ deleteMemory,
297
+ deleteMemories,
298
+ updateMemory,
299
+ searchMemories,
300
+ closePool
301
+ };
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/chat.ts
4
+ function getChatConfig() {
5
+ const provider = process.env.CHAT_PROVIDER ?? "ollama";
6
+ const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
7
+ return {
8
+ provider,
9
+ model,
10
+ ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
11
+ apiKey: process.env.CHAT_API_KEY ?? "",
12
+ baseUrl: process.env.CHAT_BASE_URL ?? ""
13
+ };
14
+ }
15
+ function getDefaultTimeoutMs() {
16
+ const raw = Number(process.env.CHAT_TIMEOUT_MS ?? 6e4);
17
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 6e4;
18
+ }
19
+ function timeoutSignal(timeoutMs) {
20
+ return AbortSignal.timeout(timeoutMs ?? getDefaultTimeoutMs());
21
+ }
22
+ function normalizeChatError(err, timeoutMs) {
23
+ const ms = timeoutMs ?? getDefaultTimeoutMs();
24
+ if (err instanceof Error && err.name === "AbortError") {
25
+ return new Error(`TIMEOUT:${ms}`);
26
+ }
27
+ if (err instanceof Error) {
28
+ return err;
29
+ }
30
+ return new Error(String(err));
31
+ }
32
+ async function callOllama(cfg, messages, options = {}) {
33
+ const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ signal: timeoutSignal(options.timeoutMs),
37
+ body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
38
+ });
39
+ if (!res.ok) {
40
+ const body = await res.text();
41
+ if (body.includes("not found") || body.includes("model")) {
42
+ throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
43
+ }
44
+ throw new Error(body);
45
+ }
46
+ const data = await res.json();
47
+ const inputTokens = data.prompt_eval_count ?? 0;
48
+ const outputTokens = data.eval_count ?? 0;
49
+ return {
50
+ content: data.message.content.trim(),
51
+ usage: inputTokens || outputTokens ? { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens } : void 0
52
+ };
53
+ }
54
+ async function callOpenAICompat(cfg, messages, options = {}) {
55
+ const base = (cfg.baseUrl ?? "").replace(/\/$/, "") || "https://api.openai.com/v1";
56
+ const res = await fetch(`${base}/chat/completions`, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ "Authorization": `Bearer ${cfg.apiKey}`
61
+ },
62
+ signal: timeoutSignal(options.timeoutMs),
63
+ body: JSON.stringify({
64
+ model: cfg.model,
65
+ messages,
66
+ response_format: { type: "json_object" }
67
+ })
68
+ });
69
+ if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
70
+ const data = await res.json();
71
+ const u = data.usage;
72
+ return {
73
+ content: data.choices[0].message.content.trim(),
74
+ usage: u ? { inputTokens: u.prompt_tokens, outputTokens: u.completion_tokens, totalTokens: u.total_tokens } : void 0
75
+ };
76
+ }
77
+ async function callAnthropic(cfg, messages, options = {}) {
78
+ const system = messages.find((m) => m.role === "system")?.content ?? "";
79
+ const userMessages = messages.filter((m) => m.role !== "system");
80
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
81
+ method: "POST",
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ "x-api-key": cfg.apiKey,
85
+ "anthropic-version": "2023-06-01"
86
+ },
87
+ signal: timeoutSignal(options.timeoutMs),
88
+ body: JSON.stringify({
89
+ model: cfg.model,
90
+ max_tokens: 4096,
91
+ system,
92
+ messages: userMessages
93
+ })
94
+ });
95
+ if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
96
+ const data = await res.json();
97
+ const u = data.usage;
98
+ return {
99
+ content: data.content[0].text.trim(),
100
+ usage: u ? { inputTokens: u.input_tokens, outputTokens: u.output_tokens, totalTokens: u.input_tokens + u.output_tokens } : void 0
101
+ };
102
+ }
103
+ async function callMiniMax(cfg, messages, options = {}) {
104
+ const res = await fetch("https://api.minimax.io/v1/chat/completions", {
105
+ method: "POST",
106
+ headers: {
107
+ "Content-Type": "application/json",
108
+ "Authorization": `Bearer ${cfg.apiKey}`
109
+ },
110
+ signal: timeoutSignal(options.timeoutMs),
111
+ body: JSON.stringify({
112
+ model: cfg.model,
113
+ messages,
114
+ response_format: { type: "json_object" }
115
+ })
116
+ });
117
+ if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
118
+ const data = await res.json();
119
+ const u = data.usage;
120
+ return {
121
+ content: data.choices[0].message.content.trim(),
122
+ usage: u ? { inputTokens: u.prompt_tokens, outputTokens: u.completion_tokens, totalTokens: u.total_tokens } : void 0
123
+ };
124
+ }
125
+ async function callChatModel(messages, options = {}) {
126
+ const cfg = getChatConfig();
127
+ try {
128
+ switch (cfg.provider) {
129
+ case "openai":
130
+ case "openai-compatible":
131
+ return await callOpenAICompat(cfg, messages, options);
132
+ case "anthropic":
133
+ return await callAnthropic(cfg, messages, options);
134
+ case "minimax":
135
+ return await callMiniMax(cfg, messages, options);
136
+ default:
137
+ return await callOllama(cfg, messages, options);
138
+ }
139
+ } catch (err) {
140
+ throw normalizeChatError(err, options.timeoutMs);
141
+ }
142
+ }
143
+ function getChatProviderLabel() {
144
+ const cfg = getChatConfig();
145
+ if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
146
+ if (cfg.provider === "openai-compatible") {
147
+ const host = cfg.baseUrl ? new URL(cfg.baseUrl).hostname : "custom";
148
+ return `openai-compat/${host} (${cfg.model})`;
149
+ }
150
+ return `${cfg.provider} (${cfg.model})`;
151
+ }
152
+
153
+ export {
154
+ callChatModel,
155
+ getChatProviderLabel
156
+ };
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/automation/approval-queue.ts
4
+ import { existsSync, readFileSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ var QUEUE_FILE = "approval-queue.json";
7
+ var ApprovalQueue = class {
8
+ constructor(configDir) {
9
+ this.configDir = configDir;
10
+ this.queuePath = join(configDir, QUEUE_FILE);
11
+ }
12
+ configDir;
13
+ queuePath;
14
+ load() {
15
+ if (!existsSync(this.queuePath)) return [];
16
+ return JSON.parse(readFileSync(this.queuePath, "utf-8"));
17
+ }
18
+ save(items) {
19
+ writeFileSync(this.queuePath, JSON.stringify(items, null, 2));
20
+ }
21
+ add(rule, source, confidence) {
22
+ const items = this.load();
23
+ const item = {
24
+ id: rule.id,
25
+ rule,
26
+ source,
27
+ confidence,
28
+ status: "pending",
29
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
30
+ };
31
+ items.push(item);
32
+ this.save(items);
33
+ return item;
34
+ }
35
+ approve(id) {
36
+ const items = this.load();
37
+ const item = items.find((i) => i.id === id);
38
+ if (!item) return false;
39
+ item.status = "approved";
40
+ this.save(items);
41
+ return true;
42
+ }
43
+ reject(id) {
44
+ const items = this.load();
45
+ const item = items.find((i) => i.id === id);
46
+ if (!item) return false;
47
+ item.status = "rejected";
48
+ this.save(items);
49
+ return true;
50
+ }
51
+ pending() {
52
+ return this.load().filter((i) => i.status === "pending");
53
+ }
54
+ persistApproved(rulesPath) {
55
+ const items = this.load();
56
+ const approved = items.filter((i) => i.status === "approved");
57
+ if (approved.length === 0) return 0;
58
+ const existing = existsSync(rulesPath) ? JSON.parse(readFileSync(rulesPath, "utf-8")).rules : [];
59
+ const merged = [...existing, ...approved.map((i) => i.rule)];
60
+ writeFileSync(rulesPath, JSON.stringify({ rules: merged }, null, 2));
61
+ const remaining = items.filter((i) => i.status !== "approved");
62
+ this.save(remaining);
63
+ return approved.length;
64
+ }
65
+ };
66
+
67
+ export {
68
+ ApprovalQueue
69
+ };
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/infrastructure/ast/import-analysis.ts
4
+ import { builtinModules } from "module";
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { dirname, extname, isAbsolute, join, normalize, resolve } from "path";
7
+ var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
8
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((entry) => `node:${entry}`)]);
9
+ function countNewlines(value) {
10
+ let count = 0;
11
+ for (const char of value) {
12
+ if (char === "\n") count += 1;
13
+ }
14
+ return count;
15
+ }
16
+ function parseImports(source) {
17
+ const imports = [];
18
+ const patterns = [
19
+ { kind: "import", regex: /(^|\n)\s*import\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)?['"]([^'"\n]+)['"]/g },
20
+ { kind: "export-from", regex: /(^|\n)\s*export\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)['"]([^'"\n]+)['"]/g },
21
+ { kind: "require", regex: /(^|\n)[^\n]*?\brequire\(\s*['"]([^'"\n]+)['"]\s*\)/g },
22
+ { kind: "dynamic-import", regex: /(^|\n)[^\n]*?\bimport\(\s*['"]([^'"\n]+)['"]\s*\)/g }
23
+ ];
24
+ for (const { kind, regex } of patterns) {
25
+ for (const match of source.matchAll(regex)) {
26
+ const prefix = source.slice(0, match.index ?? 0);
27
+ imports.push({
28
+ kind,
29
+ specifier: match[2],
30
+ line: countNewlines(prefix) + 1
31
+ });
32
+ }
33
+ }
34
+ return imports;
35
+ }
36
+ function tryResolveFilePath(candidate) {
37
+ if (existsSync(candidate)) return normalize(candidate);
38
+ if (!extname(candidate)) {
39
+ for (const ext of SOURCE_EXTENSIONS) {
40
+ const withExt = `${candidate}${ext}`;
41
+ if (existsSync(withExt)) return normalize(withExt);
42
+ }
43
+ for (const ext of SOURCE_EXTENSIONS) {
44
+ const indexFile = join(candidate, `index${ext}`);
45
+ if (existsSync(indexFile)) return normalize(indexFile);
46
+ }
47
+ }
48
+ return void 0;
49
+ }
50
+ function looksLikeExternal(specifier) {
51
+ return !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("@/");
52
+ }
53
+ function resolveImportPath(fromFile, specifier, cwd = process.cwd()) {
54
+ if (specifier.startsWith(".")) {
55
+ return tryResolveFilePath(resolve(dirname(fromFile), specifier));
56
+ }
57
+ if (specifier.startsWith("/")) {
58
+ return tryResolveFilePath(resolve(cwd, `.${specifier}`));
59
+ }
60
+ if (specifier.startsWith("@/")) {
61
+ return tryResolveFilePath(resolve(cwd, "src", specifier.slice(2)));
62
+ }
63
+ if (isAbsolute(specifier)) {
64
+ return tryResolveFilePath(specifier);
65
+ }
66
+ return void 0;
67
+ }
68
+ function collectResolvedImports(filePath, cwd = process.cwd()) {
69
+ if (!existsSync(filePath)) return [];
70
+ const source = readFileSync(filePath, "utf-8");
71
+ const imports = parseImports(source);
72
+ return imports.map((entry) => {
73
+ if (looksLikeExternal(entry.specifier) || NODE_BUILTINS.has(entry.specifier)) {
74
+ return { ...entry, isExternal: true };
75
+ }
76
+ return {
77
+ ...entry,
78
+ isExternal: false,
79
+ resolvedPath: resolveImportPath(filePath, entry.specifier, cwd)
80
+ };
81
+ });
82
+ }
83
+ function asPosix(value) {
84
+ return value.replace(/\\/g, "/");
85
+ }
86
+ function moduleNameFromPath(absPath, cwd = process.cwd()) {
87
+ const rel = asPosix(normalize(absPath)).replace(asPosix(normalize(cwd)) + "/", "");
88
+ const match = rel.match(/^src\/modules\/([^/]+)\//);
89
+ return match?.[1];
90
+ }
91
+ function buildModuleDependencyEdges(files, cwd = process.cwd()) {
92
+ const edges = [];
93
+ for (const file of files) {
94
+ const absoluteFile = resolve(cwd, file);
95
+ const fromModule = moduleNameFromPath(absoluteFile, cwd);
96
+ if (!fromModule) continue;
97
+ const imports = collectResolvedImports(absoluteFile, cwd);
98
+ for (const imp of imports) {
99
+ if (!imp.resolvedPath) continue;
100
+ const toModule = moduleNameFromPath(imp.resolvedPath, cwd);
101
+ if (!toModule || toModule === fromModule) continue;
102
+ edges.push({
103
+ fromModule,
104
+ toModule,
105
+ file,
106
+ line: imp.line
107
+ });
108
+ }
109
+ }
110
+ return edges;
111
+ }
112
+ function parseChangedFilesFromDiff(diff) {
113
+ const files = /* @__PURE__ */ new Set();
114
+ for (const line of diff.split("\n")) {
115
+ if (!line.startsWith("+++ b/")) continue;
116
+ const file = line.slice("+++ b/".length).trim();
117
+ if (!file || file === "/dev/null") continue;
118
+ files.add(file);
119
+ }
120
+ return [...files];
121
+ }
122
+ function detectModuleCycles(edges) {
123
+ const graph = /* @__PURE__ */ new Map();
124
+ for (const edge of edges) {
125
+ const next = graph.get(edge.fromModule) ?? /* @__PURE__ */ new Set();
126
+ next.add(edge.toModule);
127
+ graph.set(edge.fromModule, next);
128
+ }
129
+ const visited = /* @__PURE__ */ new Set();
130
+ const stack = /* @__PURE__ */ new Set();
131
+ const cycles = /* @__PURE__ */ new Set();
132
+ const visit = (node, path) => {
133
+ visited.add(node);
134
+ stack.add(node);
135
+ const next = graph.get(node) ?? /* @__PURE__ */ new Set();
136
+ for (const target of next) {
137
+ if (!visited.has(target)) {
138
+ visit(target, [...path, target]);
139
+ continue;
140
+ }
141
+ if (stack.has(target)) {
142
+ const start = path.indexOf(target);
143
+ const cycle = start >= 0 ? path.slice(start).concat(target) : [node, target, node];
144
+ cycles.add(cycle.join("->"));
145
+ }
146
+ }
147
+ stack.delete(node);
148
+ };
149
+ for (const node of graph.keys()) {
150
+ if (!visited.has(node)) {
151
+ visit(node, [node]);
152
+ }
153
+ }
154
+ return [...cycles].map((value) => value.split("->"));
155
+ }
156
+ function isExternalFrameworkSpecifier(specifier) {
157
+ const frameworkPrefixes = [
158
+ "express",
159
+ "fastify",
160
+ "@nestjs/",
161
+ "react",
162
+ "vue",
163
+ "svelte",
164
+ "@angular/",
165
+ "next/",
166
+ "nuxt",
167
+ "typeorm",
168
+ "@prisma/",
169
+ "mongoose",
170
+ "sequelize",
171
+ "axios"
172
+ ];
173
+ return frameworkPrefixes.some((prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`));
174
+ }
175
+
176
+ export {
177
+ parseImports,
178
+ collectResolvedImports,
179
+ buildModuleDependencyEdges,
180
+ parseChangedFilesFromDiff,
181
+ detectModuleCycles,
182
+ isExternalFrameworkSpecifier
183
+ };