fraude-code 0.1.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.
Files changed (127) hide show
  1. package/README.md +68 -0
  2. package/dist/index.js +179297 -0
  3. package/package.json +88 -0
  4. package/src/agent/agent.ts +475 -0
  5. package/src/agent/contextManager.ts +141 -0
  6. package/src/agent/index.ts +14 -0
  7. package/src/agent/pendingChanges.ts +270 -0
  8. package/src/agent/prompts/AskPrompt.txt +10 -0
  9. package/src/agent/prompts/FastPrompt.txt +40 -0
  10. package/src/agent/prompts/PlannerPrompt.txt +51 -0
  11. package/src/agent/prompts/ReviewerPrompt.txt +57 -0
  12. package/src/agent/prompts/WorkerPrompt.txt +33 -0
  13. package/src/agent/subagents/askAgent.ts +37 -0
  14. package/src/agent/subagents/extractionAgent.ts +123 -0
  15. package/src/agent/subagents/fastAgent.ts +45 -0
  16. package/src/agent/subagents/managerAgent.ts +36 -0
  17. package/src/agent/subagents/relationAgent.ts +76 -0
  18. package/src/agent/subagents/researchSubAgent.ts +79 -0
  19. package/src/agent/subagents/reviewerSubAgent.ts +42 -0
  20. package/src/agent/subagents/workerSubAgent.ts +42 -0
  21. package/src/agent/tools/bashTool.ts +94 -0
  22. package/src/agent/tools/descriptions/bash.txt +47 -0
  23. package/src/agent/tools/descriptions/edit.txt +7 -0
  24. package/src/agent/tools/descriptions/glob.txt +4 -0
  25. package/src/agent/tools/descriptions/grep.txt +8 -0
  26. package/src/agent/tools/descriptions/lsp.txt +20 -0
  27. package/src/agent/tools/descriptions/plan.txt +3 -0
  28. package/src/agent/tools/descriptions/read.txt +9 -0
  29. package/src/agent/tools/descriptions/todo.txt +12 -0
  30. package/src/agent/tools/descriptions/write.txt +8 -0
  31. package/src/agent/tools/editTool.ts +44 -0
  32. package/src/agent/tools/globTool.ts +59 -0
  33. package/src/agent/tools/grepTool.ts +343 -0
  34. package/src/agent/tools/lspTool.ts +429 -0
  35. package/src/agent/tools/planTool.ts +118 -0
  36. package/src/agent/tools/readTool.ts +78 -0
  37. package/src/agent/tools/rememberTool.ts +91 -0
  38. package/src/agent/tools/testRunnerTool.ts +77 -0
  39. package/src/agent/tools/testTool.ts +44 -0
  40. package/src/agent/tools/todoTool.ts +224 -0
  41. package/src/agent/tools/writeTool.ts +33 -0
  42. package/src/commands/COMMANDS.ts +38 -0
  43. package/src/commands/cerebras/auth.ts +27 -0
  44. package/src/commands/cerebras/index.ts +31 -0
  45. package/src/commands/forget.ts +29 -0
  46. package/src/commands/google/auth.ts +24 -0
  47. package/src/commands/google/index.ts +31 -0
  48. package/src/commands/groq/add_model.ts +60 -0
  49. package/src/commands/groq/auth.ts +24 -0
  50. package/src/commands/groq/index.ts +33 -0
  51. package/src/commands/index.ts +65 -0
  52. package/src/commands/knowledge.ts +92 -0
  53. package/src/commands/log.ts +32 -0
  54. package/src/commands/mistral/auth.ts +27 -0
  55. package/src/commands/mistral/index.ts +31 -0
  56. package/src/commands/model/index.ts +145 -0
  57. package/src/commands/models/index.ts +16 -0
  58. package/src/commands/ollama/index.ts +29 -0
  59. package/src/commands/openrouter/add_model.ts +64 -0
  60. package/src/commands/openrouter/auth.ts +24 -0
  61. package/src/commands/openrouter/index.ts +33 -0
  62. package/src/commands/remember.ts +48 -0
  63. package/src/commands/serve.ts +31 -0
  64. package/src/commands/session/index.ts +21 -0
  65. package/src/commands/usage.ts +15 -0
  66. package/src/commands/visualize.ts +773 -0
  67. package/src/components/App.tsx +55 -0
  68. package/src/components/IntroComponent.tsx +70 -0
  69. package/src/components/LoaderComponent.tsx +68 -0
  70. package/src/components/OutputRenderer.tsx +88 -0
  71. package/src/components/SettingsRenderer.tsx +23 -0
  72. package/src/components/input/CommandSuggestions.tsx +41 -0
  73. package/src/components/input/FileSuggestions.tsx +61 -0
  74. package/src/components/input/InputBox.tsx +371 -0
  75. package/src/components/output/CheckpointView.tsx +13 -0
  76. package/src/components/output/CommandView.tsx +13 -0
  77. package/src/components/output/CommentView.tsx +12 -0
  78. package/src/components/output/ConfirmationView.tsx +179 -0
  79. package/src/components/output/ContextUsage.tsx +62 -0
  80. package/src/components/output/DiffView.tsx +202 -0
  81. package/src/components/output/ErrorView.tsx +14 -0
  82. package/src/components/output/InteractiveServerView.tsx +69 -0
  83. package/src/components/output/KnowledgeView.tsx +220 -0
  84. package/src/components/output/MarkdownView.tsx +15 -0
  85. package/src/components/output/ModelSelectView.tsx +71 -0
  86. package/src/components/output/ReasoningView.tsx +21 -0
  87. package/src/components/output/ToolCallView.tsx +45 -0
  88. package/src/components/settings/ModelList.tsx +250 -0
  89. package/src/components/settings/TokenUsage.tsx +274 -0
  90. package/src/config/schema.ts +19 -0
  91. package/src/config/settings.ts +229 -0
  92. package/src/index.tsx +100 -0
  93. package/src/parsers/tree-sitter-python.wasm +0 -0
  94. package/src/providers/providers.ts +71 -0
  95. package/src/services/PluginLoader.ts +123 -0
  96. package/src/services/cerebras.ts +69 -0
  97. package/src/services/embeddingService.ts +229 -0
  98. package/src/services/google.ts +65 -0
  99. package/src/services/graphSerializer.ts +248 -0
  100. package/src/services/groq.ts +23 -0
  101. package/src/services/knowledgeOrchestrator.ts +286 -0
  102. package/src/services/mistral.ts +79 -0
  103. package/src/services/ollama.ts +109 -0
  104. package/src/services/openrouter.ts +23 -0
  105. package/src/services/symbolExtractor.ts +277 -0
  106. package/src/store/useFraudeStore.ts +123 -0
  107. package/src/store/useSettingsStore.ts +38 -0
  108. package/src/theme.ts +26 -0
  109. package/src/types/Agent.ts +147 -0
  110. package/src/types/CommandDefinition.ts +8 -0
  111. package/src/types/Model.ts +94 -0
  112. package/src/types/OutputItem.ts +24 -0
  113. package/src/types/PluginContext.ts +55 -0
  114. package/src/types/TokenUsage.ts +5 -0
  115. package/src/types/assets.d.ts +4 -0
  116. package/src/utils/agentCognition.ts +1152 -0
  117. package/src/utils/fileSuggestions.ts +111 -0
  118. package/src/utils/index.ts +17 -0
  119. package/src/utils/initFraude.ts +8 -0
  120. package/src/utils/logger.ts +24 -0
  121. package/src/utils/lspClient.ts +1415 -0
  122. package/src/utils/paths.ts +24 -0
  123. package/src/utils/queryHandler.ts +227 -0
  124. package/src/utils/router.ts +278 -0
  125. package/src/utils/streamHandler.ts +132 -0
  126. package/src/utils/treeSitterQueries.ts +125 -0
  127. package/tsconfig.json +33 -0
@@ -0,0 +1,1152 @@
1
+ import { Database, Connection, QueryResult, PreparedStatement } from "kuzu";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import type { ModelMessage } from "ai";
5
+ import ignore from "ignore";
6
+ import EmbeddingService from "@/services/embeddingService";
7
+ import log from "./logger";
8
+
9
+ // ============================================================================
10
+ // Types
11
+ // ============================================================================
12
+
13
+ export interface Fact {
14
+ id: string;
15
+ type:
16
+ | "fact"
17
+ | "decision"
18
+ | "concept"
19
+ | "reference"
20
+ | "file"
21
+ | "module"
22
+ | "function"
23
+ | "class"
24
+ | "interface"
25
+ | "variable"
26
+ | "symbol";
27
+ content: string;
28
+ data?: {
29
+ file?: string;
30
+ function?: string;
31
+ symbol?: string;
32
+ [key: string]: unknown;
33
+ };
34
+ timestamp: number;
35
+ sessionId: string;
36
+ confidence: number;
37
+ validated?: number;
38
+ }
39
+
40
+ export interface ValidationResult {
41
+ valid: boolean;
42
+ reason?: "file_missing" | "code_changed" | "stale" | "conflict";
43
+ suggestion?: "delete" | "update" | "keep";
44
+ }
45
+
46
+ // ============================================================================
47
+ // AgentCognition - Consolidated Knowledge Management
48
+ // ============================================================================
49
+
50
+ class AgentCognition {
51
+ private static instance: AgentCognition;
52
+ private db: Database | null = null;
53
+ private conn: Connection | null = null;
54
+ private dbPath: string;
55
+ private initPromise: Promise<void> | null = null;
56
+ private sessionId: string;
57
+ private embeddings: EmbeddingService;
58
+
59
+ // Staleness threshold: 7 days
60
+ private static STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
61
+
62
+ private constructor() {
63
+ this.dbPath = path.join(process.cwd(), ".fraude", "kuzu");
64
+ this.sessionId = crypto.randomUUID();
65
+ this.embeddings = EmbeddingService.getInstance();
66
+ }
67
+
68
+ static getInstance(): AgentCognition {
69
+ if (!AgentCognition.instance) {
70
+ AgentCognition.instance = new AgentCognition();
71
+ }
72
+ return AgentCognition.instance;
73
+ }
74
+
75
+ // ============================================================================
76
+ // Initialization
77
+ // ============================================================================
78
+
79
+ async init(): Promise<void> {
80
+ if (this.initPromise) return this.initPromise;
81
+
82
+ this.initPromise = (async () => {
83
+ this.ensureDirectory();
84
+ this.db = new Database(this.dbPath);
85
+ this.conn = new Connection(this.db);
86
+ await this.initSchema();
87
+ })();
88
+
89
+ return this.initPromise;
90
+ }
91
+
92
+ private ensureDirectory(): void {
93
+ const dir = path.dirname(this.dbPath);
94
+ if (!fs.existsSync(dir)) {
95
+ fs.mkdirSync(dir, { recursive: true });
96
+ }
97
+ }
98
+
99
+ private async initSchema(): Promise<void> {
100
+ if (!this.conn) throw new Error("Connection not initialized");
101
+
102
+ try {
103
+ // Create Fact node table
104
+ await this.conn.query(`
105
+ CREATE NODE TABLE IF NOT EXISTS Fact (
106
+ id STRING,
107
+ type STRING,
108
+ content STRING,
109
+ data STRING,
110
+ timestamp INT64,
111
+ sessionId STRING,
112
+ confidence DOUBLE,
113
+ validated INT64,
114
+ PRIMARY KEY (id)
115
+ )
116
+ `);
117
+
118
+ // Create relationship table
119
+ await this.conn.query(`
120
+ CREATE REL TABLE IF NOT EXISTS RELATED_TO (
121
+ FROM Fact TO Fact,
122
+ relation STRING,
123
+ weight DOUBLE
124
+ )
125
+ `);
126
+ } catch (e: unknown) {
127
+ // Tables may already exist, ignore errors
128
+ }
129
+ }
130
+
131
+ // ============================================================================
132
+ // Graph Operations
133
+ // ============================================================================
134
+
135
+ private async execute(
136
+ cypher: string,
137
+ params: Record<string, any> = {},
138
+ ): Promise<QueryResult> {
139
+ await this.init();
140
+ if (!this.conn) throw new Error("Connection not initialized");
141
+
142
+ const stmt: PreparedStatement = await this.conn.prepare(cypher);
143
+ if (!stmt.isSuccess()) {
144
+ throw new Error(`Failed to prepare statement: ${stmt.getErrorMessage()}`);
145
+ }
146
+ const result = await this.conn.execute(stmt, params);
147
+ log("DB EXECUTE RESULT:", result, "IsArray:", Array.isArray(result));
148
+ if (Array.isArray(result)) {
149
+ if (result.length === 0) {
150
+ // Fallback for empty array result
151
+ return {
152
+ getAll: async () => [],
153
+ getNumTuples: () => 0,
154
+ } as unknown as QueryResult;
155
+ }
156
+ return result[0] as QueryResult;
157
+ }
158
+ return result as QueryResult;
159
+ }
160
+
161
+ async addFact(
162
+ fact: Omit<Fact, "id" | "timestamp" | "sessionId">,
163
+ ): Promise<string> {
164
+ // Deduplication: Check if a fact with same type+content already exists
165
+ const existing = await this.findDuplicate(
166
+ fact.type,
167
+ fact.content,
168
+ fact.data,
169
+ );
170
+ if (existing) {
171
+ // Update timestamp to keep it fresh, but don't create duplicate
172
+ await this.execute(
173
+ `
174
+ MATCH (f:Fact {id: $id})
175
+ SET f.validated = $validated,
176
+ f.confidence = CASE WHEN $confidence > f.confidence THEN $confidence ELSE f.confidence END,
177
+ f.content = $content,
178
+ f.data = $data
179
+ RETURN f
180
+ `,
181
+ {
182
+ id: existing.id,
183
+ validated: Date.now(),
184
+ confidence: fact.confidence,
185
+ content: fact.content,
186
+ data: JSON.stringify(fact.data || {}),
187
+ },
188
+ );
189
+ // Ensure symbol linking even on duplicate (heals missing links)
190
+ if (
191
+ fact.data?.symbol &&
192
+ fact.data?.file &&
193
+ typeof fact.data.file === "string" &&
194
+ typeof fact.data.symbol === "string"
195
+ ) {
196
+ try {
197
+ const symbolNode = await this.findSymbolNode(
198
+ fact.data.file,
199
+ fact.data.symbol,
200
+ );
201
+ if (symbolNode && symbolNode.id !== existing.id) {
202
+ await this.addRelation(existing.id, symbolNode.id, "ABOUT", 1.0);
203
+ }
204
+ } catch (e) {
205
+ /* ignore linking errors on dup update */
206
+ }
207
+ }
208
+ return existing.id;
209
+ }
210
+
211
+ const id = crypto.randomUUID();
212
+ const timestamp = Date.now();
213
+ const dataStr = JSON.stringify(fact.data || {});
214
+
215
+ await this.execute(
216
+ `
217
+ CREATE (f:Fact {
218
+ id: $id,
219
+ type: $type,
220
+ content: $content,
221
+ data: $data,
222
+ timestamp: $timestamp,
223
+ sessionId: $sessionId,
224
+ confidence: $confidence,
225
+ validated: $validated
226
+ })
227
+ RETURN f
228
+ `,
229
+ {
230
+ id,
231
+ type: fact.type,
232
+ content: fact.content,
233
+ data: dataStr,
234
+ timestamp,
235
+ sessionId: this.sessionId,
236
+ confidence: fact.confidence,
237
+ validated: timestamp,
238
+ },
239
+ );
240
+
241
+ // Also store in vector DB for semantic search
242
+ try {
243
+ await this.embeddings.store({
244
+ id,
245
+ type: fact.type,
246
+ content: fact.content,
247
+ data: dataStr,
248
+ timestamp,
249
+ sessionId: this.sessionId,
250
+ confidence: fact.confidence,
251
+ });
252
+
253
+ // Link to canonical symbol node if this fact contains symbol data
254
+ if (
255
+ fact.data?.symbol &&
256
+ fact.data?.file &&
257
+ typeof fact.data.file === "string" &&
258
+ typeof fact.data.symbol === "string"
259
+ ) {
260
+ const symbolNode = await this.findSymbolNode(
261
+ fact.data.file,
262
+ fact.data.symbol,
263
+ );
264
+ if (symbolNode && symbolNode.id !== id) {
265
+ await this.addRelation(id, symbolNode.id, "ABOUT", 1.0);
266
+ }
267
+ } else if (fact.data?.file && typeof fact.data.file === "string") {
268
+ // Fallback: Link to file node if no symbol but file is present
269
+ const fileNode = await this.findFileNode(fact.data.file);
270
+ if (fileNode && fileNode.id !== id) {
271
+ await this.addRelation(id, fileNode.id, "ABOUT", 1.0);
272
+ }
273
+ } else if (fact.data?.file && typeof fact.data.file === "string") {
274
+ // Fallback: Link to file node if no symbol but file is present
275
+ // This catches generic summaries or file-level facts
276
+ const fileNode = await this.findFileNode(fact.data.file);
277
+ if (fileNode && fileNode.id !== id) {
278
+ await this.addRelation(id, fileNode.id, "ABOUT", 1.0);
279
+ }
280
+ }
281
+
282
+ // Discover relations to existing facts
283
+ await this.discoverRelations(id, fact.content);
284
+ } catch (e) {
285
+ // Embedding storage is optional, don't fail if it errors
286
+ }
287
+
288
+ return id;
289
+ }
290
+
291
+ private async findDuplicate(
292
+ type: Fact["type"],
293
+ content: string,
294
+ data?: Fact["data"],
295
+ ): Promise<Fact | null> {
296
+ // 1. Structural Identity (Stable IDs for code symbols)
297
+ if (data?.file && data?.symbol) {
298
+ const relFile = path.isAbsolute(data.file)
299
+ ? path.relative(process.cwd(), data.file)
300
+ : data.file;
301
+
302
+ try {
303
+ const fileFrag = `"file":"${relFile}"`;
304
+ const symbolFrag = `"symbol":"${data.symbol}"`;
305
+ const result = await this.execute(
306
+ `
307
+ MATCH (f:Fact {type: $type})
308
+ WHERE f.data CONTAINS $fileFrag AND f.data CONTAINS $symbolFrag
309
+ RETURN f
310
+ LIMIT 10
311
+ `,
312
+ { type, fileFrag, symbolFrag },
313
+ );
314
+ const rows = await result.getAll();
315
+ for (const row of rows) {
316
+ const parsed = this.parseFact(row.f as Record<string, unknown>);
317
+ if (
318
+ parsed.data?.file === relFile &&
319
+ parsed.data?.symbol === data.symbol
320
+ ) {
321
+ return parsed;
322
+ }
323
+ }
324
+ } catch (e) {}
325
+ }
326
+
327
+ // 2. Exact string match (Fast path)
328
+ try {
329
+ const result = await this.execute(
330
+ `
331
+ MATCH (f:Fact {type: $type})
332
+ WHERE f.content = $content
333
+ RETURN f
334
+ LIMIT 1
335
+ `,
336
+ { type, content },
337
+ );
338
+ const rows = await result.getAll();
339
+ if (rows.length > 0) {
340
+ return this.parseFact((rows[0] as any).f as Record<string, unknown>);
341
+ }
342
+ } catch (e) {}
343
+
344
+ // 3. Semantic similarity check (Vector DB)
345
+ // Use a high threshold (0.92) to establish "essentially the same meaning"
346
+ try {
347
+ const semanticResults = await this.embeddings.search(content, 3, 0.92);
348
+ const match = semanticResults.find((r) => {
349
+ return r.type === type;
350
+ });
351
+
352
+ if (match) {
353
+ // Fetch full fact from Graph DB to get all fields (e.g. validated status)
354
+ const result = await this.execute(
355
+ `
356
+ MATCH (f:Fact {id: $id})
357
+ RETURN f
358
+ `,
359
+ { id: match.id },
360
+ );
361
+ const rows = await result.getAll();
362
+ if (rows.length > 0) {
363
+ return this.parseFact((rows[0] as any).f as Record<string, unknown>);
364
+ }
365
+ }
366
+ } catch (e) {}
367
+
368
+ return null;
369
+ }
370
+
371
+ async addRelation(
372
+ sourceId: string,
373
+ targetId: string,
374
+ relation: string,
375
+ weight: number = 1.0,
376
+ ): Promise<void> {
377
+ await this.execute(
378
+ `
379
+ MATCH (a:Fact {id: $sourceId})
380
+ MATCH (b:Fact {id: $targetId})
381
+ MERGE (a)-[r:RELATED_TO {relation: $relation}]->(b)
382
+ SET r.weight = $weight
383
+ RETURN r
384
+ `,
385
+ { sourceId, targetId, relation, weight },
386
+ );
387
+ }
388
+
389
+ /**
390
+ * Discover and create relations between a new fact and existing facts.
391
+ * Uses vector similarity for discovery and LLM for relation typing.
392
+ */
393
+ private async discoverRelations(
394
+ newFactId: string,
395
+ content: string,
396
+ ): Promise<void> {
397
+ // Skip for very short content
398
+ if (content.length < 30) return;
399
+
400
+ try {
401
+ // Find semantically similar existing facts
402
+ const similar = await this.embeddings.search(content, 5, 0.6);
403
+
404
+ for (const match of similar) {
405
+ // Skip self-references
406
+ if (match.id === newFactId) continue;
407
+
408
+ // High similarity: use LLM to classify relation type
409
+ if (match.score >= 0.85) {
410
+ try {
411
+ const { classifyRelation } =
412
+ await import("@/agent/subagents/relationAgent");
413
+ const relationType = await classifyRelation(content, match.content);
414
+ await this.addRelation(
415
+ newFactId,
416
+ match.id,
417
+ relationType,
418
+ match.score,
419
+ );
420
+ } catch (e) {
421
+ // Fallback if import fails
422
+ await this.addRelation(
423
+ newFactId,
424
+ match.id,
425
+ "RELATED_TO",
426
+ match.score,
427
+ );
428
+ }
429
+ } else if (match.score >= 0.6) {
430
+ // Medium similarity: use generic RELATED_TO
431
+ await this.addRelation(
432
+ newFactId,
433
+ match.id,
434
+ "RELATED_TO",
435
+ match.score,
436
+ );
437
+ }
438
+ }
439
+ } catch (e) {
440
+ // Relation discovery is non-critical, log and continue
441
+ log("Relation discovery failed: " + e);
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Batch insert facts and relations for atomic knowledge updates.
447
+ * Efficiently handles indexing large amounts of symbol data.
448
+ */
449
+ async addFactsWithRelations(
450
+ facts: Omit<Fact, "id" | "timestamp" | "sessionId">[],
451
+ relations: { sourceIdx: number; targetIdx: number; type: string }[],
452
+ ): Promise<string[]> {
453
+ // 1. Insert all facts first, collecting IDs
454
+ const factIds: string[] = [];
455
+ for (const fact of facts) {
456
+ // Use standard addFact to ensure deduplication and vector embedding
457
+ // Note: This calls discoverRelations() for each fact, which is good for cross-linking
458
+ const id = await this.addFact(fact);
459
+ factIds.push(id);
460
+ }
461
+
462
+ // 2. Create provided relations using collected IDs
463
+ for (const rel of relations) {
464
+ const sourceId = factIds[rel.sourceIdx];
465
+ const targetId = factIds[rel.targetIdx];
466
+
467
+ // Safety check: ensure both ends exist
468
+ if (sourceId && targetId) {
469
+ await this.addRelation(sourceId, targetId, rel.type);
470
+ }
471
+ }
472
+
473
+ return factIds;
474
+ }
475
+
476
+ /**
477
+ * Index a source file to extract symbols and structural relations.
478
+ * Maps code intelligence (DEFINES, CALLS) into the knowledge graph.
479
+ */
480
+ async indexFile(filePath: string): Promise<void> {
481
+ try {
482
+ const stats = fs.statSync(filePath);
483
+ const mtime = stats.mtimeMs;
484
+ const relPath = path.relative(process.cwd(), filePath);
485
+
486
+ // Check if file has changed since last indexing
487
+ const existingFileFact = await this.findDuplicate(
488
+ "file",
489
+ `File: ${relPath}`,
490
+ { file: relPath },
491
+ );
492
+ if (existingFileFact && existingFileFact.data?.mtime === mtime) {
493
+ log(`[AgentCognition] Skipping unchanged file: ${relPath}`);
494
+ return;
495
+ }
496
+
497
+ const { SymbolExtractor } = await import("@/services/symbolExtractor");
498
+ const extractor = new SymbolExtractor();
499
+ const { facts, relations } = await extractor.analyze(filePath);
500
+
501
+ if (facts.length > 0) {
502
+ // Update file fact with mtime
503
+ const fileFactIdx = facts.findIndex((f) => f.type === "file");
504
+ if (fileFactIdx !== -1) {
505
+ const fileFact = facts[fileFactIdx];
506
+ if (fileFact) {
507
+ fileFact.data = { ...(fileFact.data || {}), mtime };
508
+ }
509
+ }
510
+
511
+ log(
512
+ `Indexing ${filePath}: ${facts.length} symbols, ${relations.length} relations`,
513
+ );
514
+ const indexedIds = await this.addFactsWithRelations(facts, relations);
515
+
516
+ // Purge symbols that are no longer in the file
517
+ const currentSymbolNames = facts
518
+ .filter((f) => f.type !== "file" && f.type !== "module")
519
+ .map((f) => (f.data as any).symbol)
520
+ .filter(Boolean);
521
+
522
+ await this.purgeOrphanedSymbols(filePath, currentSymbolNames);
523
+ }
524
+ } catch (e) {
525
+ log(`Failed to index file ${filePath}: ${e}`);
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Remove symbol-related facts for a file that are no longer present.
531
+ * Uses DETACH DELETE to ensure no orphaned relations remain.
532
+ */
533
+ private async purgeOrphanedSymbols(
534
+ filePath: string,
535
+ currentSymbols: string[],
536
+ ): Promise<void> {
537
+ const codeTypes =
538
+ "['function', 'class', 'interface', 'variable', 'symbol']";
539
+ const relFile = path.relative(process.cwd(), filePath);
540
+ try {
541
+ const result = await this.execute(
542
+ `
543
+ MATCH (f:Fact)
544
+ WHERE f.type IN ${codeTypes} AND f.data CONTAINS $file
545
+ RETURN f
546
+ `,
547
+ { file: `"${relFile}"` },
548
+ );
549
+ const rows = await result.getAll();
550
+
551
+ for (const row of rows) {
552
+ const fact = this.parseFact(row.f as Record<string, unknown>);
553
+ if (
554
+ fact.data?.file === relFile &&
555
+ fact.data?.symbol &&
556
+ !currentSymbols.includes(fact.data.symbol as string)
557
+ ) {
558
+ log(
559
+ `[AgentCognition] Purging orphaned symbol: ${fact.data.symbol} from ${relFile}`,
560
+ );
561
+ await this.execute(`MATCH (f:Fact {id: $id}) DETACH DELETE f`, {
562
+ id: fact.id,
563
+ });
564
+ }
565
+ }
566
+ } catch (e) {
567
+ log(`Failed to purge orphaned symbols for ${relFile}: ${e}`);
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Remove all symbol-related facts for a file (called before re-indexing).
573
+ * Also cleans up relations to prevent orphaned edges.
574
+ */
575
+ /**
576
+ * Remove all symbol-related facts for a file (called when cleanup is forced).
577
+ * Also cleans up relations to prevent orphaned edges.
578
+ */
579
+ private async clearFileSymbols(filePath: string): Promise<void> {
580
+ const codeTypes =
581
+ "['file', 'module', 'function', 'class', 'interface', 'variable', 'symbol']";
582
+ const relFile = path.relative(process.cwd(), filePath);
583
+ try {
584
+ // Use DETACH DELETE for cleaner relation removal
585
+ await this.execute(
586
+ `
587
+ MATCH (f:Fact)
588
+ WHERE f.type IN ${codeTypes} AND f.data CONTAINS $file
589
+ DETACH DELETE f
590
+ `,
591
+ { file: `"${relFile}"` },
592
+ );
593
+ } catch (e) {
594
+ // Non-critical
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Index all supported files in a directory (background batch indexing).
600
+ * Runs non-blocking and logs progress.
601
+ */
602
+ async indexDirectory(
603
+ dirPath: string,
604
+ extensions = [".ts", ".tsx", ".js", ".jsx", ".py"],
605
+ ): Promise<void> {
606
+ // Load .gitignore patterns
607
+ const ig = ignore();
608
+ const gitignorePath = path.join(dirPath, ".gitignore");
609
+ if (fs.existsSync(gitignorePath)) {
610
+ const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
611
+ ig.add(gitignoreContent);
612
+ }
613
+ // Always ignore .fraude data folder and common build artifacts
614
+ ig.add([".fraude", "node_modules", "dist", "build", ".git"]);
615
+
616
+ const glob = new Bun.Glob(`**/*{${extensions.join(",")}}`);
617
+ const files: string[] = [];
618
+
619
+ for await (const file of glob.scan({
620
+ cwd: dirPath,
621
+ absolute: true,
622
+ onlyFiles: true,
623
+ dot: false,
624
+ })) {
625
+ // Get relative path for gitignore matching
626
+ const relativePath = path.relative(dirPath, file);
627
+ if (ig.ignores(relativePath)) {
628
+ continue;
629
+ }
630
+ files.push(file);
631
+ }
632
+
633
+ log(`Background indexing: ${files.length} files in ${dirPath}`);
634
+
635
+ // Process files with a small delay between each to avoid blocking
636
+ for (const file of files) {
637
+ try {
638
+ await this.indexFile(file);
639
+ } catch (e) {
640
+ // Continue on individual file failures
641
+ }
642
+ // Yield to event loop occasionally
643
+ await new Promise((r) => setTimeout(r, 10));
644
+ }
645
+
646
+ log(`Background indexing complete: ${files.length} files processed`);
647
+ }
648
+
649
+ /**
650
+ * Find a canonical symbol node in the graph.
651
+ * Used to link extracted semantic facts to concrete code symbols.
652
+ */
653
+ async findSymbolNode(file: string, symbol: string): Promise<Fact | null> {
654
+ const relFile = path.relative(process.cwd(), file);
655
+ try {
656
+ // Precise search using both symbol name and file path
657
+ // This avoids the "floating node" issue where common symbols (e.g. "init")
658
+ // were missed due to LIMIT clauses on broad queries
659
+ const symbolFrag = `"symbol":"${symbol}"`;
660
+ const fileFrag = `"file":"${relFile}"`;
661
+
662
+ const result = await this.execute(
663
+ `
664
+ MATCH (f:Fact)
665
+ WHERE f.data CONTAINS $symbolFrag AND f.data CONTAINS $fileFrag
666
+ RETURN f
667
+ LIMIT 1
668
+ `,
669
+ { symbolFrag, fileFrag },
670
+ );
671
+
672
+ const rows = await result.getAll();
673
+
674
+ if (rows.length > 0) {
675
+ return this.parseFact((rows[0] as any).f as Record<string, unknown>);
676
+ }
677
+ } catch (e) {
678
+ log(`findSymbolNode error: ${e}`);
679
+ }
680
+ return null;
681
+ }
682
+
683
+ /**
684
+ * Find a file node by path, handling normalization.
685
+ */
686
+ async findFileNode(filePath: string): Promise<Fact | null> {
687
+ const relFile = path.isAbsolute(filePath)
688
+ ? path.relative(process.cwd(), filePath)
689
+ : filePath;
690
+
691
+ try {
692
+ const fileFrag = `"file":"${relFile}"`;
693
+ const result = await this.execute(
694
+ `
695
+ MATCH (f:Fact {type: 'file'})
696
+ WHERE f.data CONTAINS $fileFrag
697
+ RETURN f
698
+ LIMIT 1
699
+ `,
700
+ { fileFrag },
701
+ );
702
+
703
+ const rows = await result.getAll();
704
+ if (rows.length > 0) {
705
+ return this.parseFact((rows[0] as any).f as Record<string, unknown>);
706
+ }
707
+ } catch (e) {
708
+ log(`findFileNode error: ${e}`);
709
+ }
710
+ return null;
711
+ }
712
+
713
+ async query(
714
+ cypher: string,
715
+ params: Record<string, unknown> = {},
716
+ ): Promise<unknown[]> {
717
+ const result = await this.execute(cypher, params);
718
+ return result.getAll();
719
+ }
720
+
721
+ // ============================================================================
722
+ // Retrieval
723
+ // ============================================================================
724
+
725
+ async findByType(type: Fact["type"]): Promise<Fact[]> {
726
+ const result = await this.execute(`MATCH (f:Fact {type: $type}) RETURN f`, {
727
+ type,
728
+ });
729
+ const rows = await result.getAll();
730
+ return rows.map((row: Record<string, unknown>) =>
731
+ this.parseFact(row.f as Record<string, unknown>),
732
+ );
733
+ }
734
+
735
+ async findRelated(factId: string, depth: number = 1): Promise<Fact[]> {
736
+ const result = await this.execute(
737
+ `
738
+ MATCH (start:Fact {id: $factId})-[r*1..${depth}]->(related:Fact)
739
+ WHERE NOT start.id = related.id
740
+ RETURN DISTINCT related
741
+ `,
742
+ { factId },
743
+ );
744
+ const rows = await result.getAll();
745
+ return rows.map((row: Record<string, unknown>) =>
746
+ this.parseFact(row.related as Record<string, unknown>),
747
+ );
748
+ }
749
+
750
+ async searchByContent(query: string, limit: number = 10): Promise<Fact[]> {
751
+ // Simple contains search - could be enhanced with embeddings later
752
+ const result = await this.execute(
753
+ `
754
+ MATCH (f:Fact)
755
+ WHERE f.content CONTAINS $query
756
+ RETURN f
757
+ LIMIT $limit
758
+ `,
759
+ { query, limit },
760
+ );
761
+ const rows = await result.getAll();
762
+ return rows.map((row: Record<string, unknown>) =>
763
+ this.parseFact(row.f as Record<string, unknown>),
764
+ );
765
+ }
766
+
767
+ // async getPrimingContext(): Promise<string> {
768
+ // // Get key project knowledge: recent summaries and high-confidence decisions
769
+ // const summaries = await this.findByType("summary");
770
+ // const decisions = await this.findByType("decision");
771
+
772
+ // const validSummaries = await this.filterValid(summaries.slice(0, 3));
773
+ // const validDecisions = await this.filterValid(decisions.slice(0, 5));
774
+
775
+ // const parts: string[] = [];
776
+
777
+ // if (validSummaries.length > 0) {
778
+ // parts.push("<previous_session_context>");
779
+ // validSummaries.forEach((s) => parts.push(`- ${s.content}`));
780
+ // parts.push("</previous_session_context>");
781
+ // }
782
+
783
+ // if (validDecisions.length > 0) {
784
+ // parts.push("<project_decisions>");
785
+ // validDecisions.forEach((d) => parts.push(`- ${d.content}`));
786
+ // parts.push("</project_decisions>");
787
+ // }
788
+
789
+ // return parts.join("\n");
790
+ // }
791
+
792
+ async retrieveRelevant(query: string, limit: number = 5): Promise<Fact[]> {
793
+ // Hybrid search: combine vector similarity + graph relationships
794
+ const scored = new Map<string, { fact: Fact; score: number }>();
795
+
796
+ // Phase 1: Vector search for semantic similarity
797
+ try {
798
+ const semanticResults = await this.embeddings.search(
799
+ query,
800
+ limit * 2,
801
+ 0.3,
802
+ );
803
+ for (const r of semanticResults) {
804
+ const fact: Fact = {
805
+ id: r.id,
806
+ type: r.type as Fact["type"],
807
+ content: r.content,
808
+ data: r.data ? JSON.parse(r.data) : undefined,
809
+ timestamp: r.timestamp,
810
+ sessionId: r.sessionId,
811
+ confidence: r.confidence,
812
+ };
813
+ // Vector score: similarity (0-1) weighted heavily
814
+ scored.set(r.id, { fact, score: r.score * 0.7 });
815
+ }
816
+ } catch (e) {
817
+ // Continue with graph-only if embeddings fail
818
+ }
819
+
820
+ // Phase 2: Graph expansion - find related facts for top vector hits
821
+ const topVectorIds = [...scored.entries()]
822
+ .sort((a, b) => b[1].score - a[1].score)
823
+ .slice(0, 3)
824
+ .map(([id]) => id);
825
+
826
+ for (const factId of topVectorIds) {
827
+ try {
828
+ const related = await this.findRelated(factId, 1);
829
+ for (const rel of related) {
830
+ const existing = scored.get(rel.id);
831
+ if (existing) {
832
+ // Boost score for facts found via both vector AND graph
833
+ existing.score += 0.2;
834
+ } else {
835
+ // Add graph-discovered facts with lower base score
836
+ scored.set(rel.id, { fact: rel, score: 0.3 });
837
+ }
838
+ }
839
+ } catch (e) {
840
+ // Continue if graph query fails
841
+ }
842
+ }
843
+
844
+ // Phase 3: Text fallback if no results yet
845
+ if (scored.size === 0) {
846
+ const textResults = await this.searchByContent(query, limit * 2);
847
+ for (const fact of textResults) {
848
+ scored.set(fact.id, { fact, score: 0.4 });
849
+ }
850
+ }
851
+
852
+ // Phase 4: Rank, validate, and return
853
+ const ranked = [...scored.values()]
854
+ .sort((a, b) => b.score - a.score)
855
+ .slice(0, limit * 2)
856
+ .map((s) => s.fact);
857
+
858
+ return this.filterValid(ranked.slice(0, limit));
859
+ }
860
+
861
+ private parseFact(raw: Record<string, unknown>): Fact {
862
+ return {
863
+ id: raw.id as string,
864
+ type: raw.type as Fact["type"],
865
+ content: raw.content as string,
866
+ data: raw.data ? JSON.parse(raw.data as string) : undefined,
867
+ timestamp: raw.timestamp as number,
868
+ sessionId: raw.sessionId as string,
869
+ confidence: raw.confidence as number,
870
+ validated: raw.validated as number,
871
+ };
872
+ }
873
+
874
+ // ============================================================================
875
+ // Validation
876
+ // ============================================================================
877
+
878
+ async validateFact(fact: Fact): Promise<ValidationResult> {
879
+ // 1. File reference check
880
+ if (fact.data?.file) {
881
+ // Normalize to absolute for existence check
882
+ const filePath = path.isAbsolute(fact.data.file)
883
+ ? fact.data.file
884
+ : path.resolve(process.cwd(), fact.data.file);
885
+
886
+ if (fact.type === "file" && !fs.existsSync(filePath)) {
887
+ // Strict check for FactType.FILE
888
+ return { valid: false, reason: "file_missing", suggestion: "delete" };
889
+ } else if (!fs.existsSync(filePath) && !fact.data.isExternal) {
890
+ // Loose check for other types (might be external libs)
891
+ return { valid: false, reason: "file_missing", suggestion: "delete" };
892
+ }
893
+ }
894
+
895
+ // 2. Code presence check (function/symbol)
896
+ if (fact.data?.function || fact.data?.symbol) {
897
+ const exists = await this.checkCodeExists(fact.data);
898
+ if (!exists) {
899
+ return { valid: false, reason: "code_changed", suggestion: "delete" };
900
+ }
901
+ }
902
+
903
+ // 3. Staleness check
904
+ const age = Date.now() - fact.timestamp;
905
+ if (age > AgentCognition.STALE_THRESHOLD_MS) {
906
+ // If it references files, check if they changed
907
+ if (fact.data?.file) {
908
+ const filePath = path.isAbsolute(fact.data.file)
909
+ ? fact.data.file
910
+ : path.resolve(process.cwd(), fact.data.file);
911
+
912
+ if (fs.existsSync(filePath)) {
913
+ const stat = fs.statSync(filePath);
914
+ if (stat.mtimeMs > fact.timestamp) {
915
+ return { valid: false, reason: "stale", suggestion: "update" };
916
+ }
917
+ }
918
+ }
919
+ // Generic staleness for old facts without file refs
920
+ if (age > AgentCognition.STALE_THRESHOLD_MS * 4) {
921
+ return { valid: false, reason: "stale", suggestion: "update" };
922
+ }
923
+ }
924
+
925
+ return { valid: true };
926
+ }
927
+
928
+ private async checkCodeExists(data: Fact["data"]): Promise<boolean> {
929
+ if (!data?.file) return true;
930
+
931
+ const filePath = path.isAbsolute(data.file)
932
+ ? data.file
933
+ : path.resolve(process.cwd(), data.file);
934
+
935
+ if (!fs.existsSync(filePath)) return false;
936
+
937
+ // Check if function/symbol exists in file
938
+ const target = data.function || data.symbol;
939
+ if (!target) return true;
940
+
941
+ try {
942
+ const content = fs.readFileSync(filePath, "utf-8");
943
+ // Simple check: does the symbol name appear in the file?
944
+ return content.includes(target);
945
+ } catch {
946
+ return false;
947
+ }
948
+ }
949
+
950
+ async filterValid(facts: Fact[]): Promise<Fact[]> {
951
+ const results: Fact[] = [];
952
+ for (const fact of facts) {
953
+ const validation = await this.validateFact(fact);
954
+ if (validation.valid) {
955
+ results.push(fact);
956
+ } else if (validation.suggestion === "delete") {
957
+ await this.deleteFact(fact.id);
958
+ }
959
+ }
960
+ return results;
961
+ }
962
+
963
+ async deleteFact(id: string): Promise<void> {
964
+ await this.execute(`MATCH (f:Fact {id: $id}) DELETE f`, { id });
965
+ try {
966
+ await this.embeddings.delete(id);
967
+ } catch (e) {
968
+ // Vector deletion is best-effort, don't fail if it errors
969
+ }
970
+ }
971
+
972
+ async pruneStale(): Promise<number> {
973
+ const allFacts = await this.query(`MATCH (f:Fact) RETURN f`);
974
+ let pruned = 0;
975
+
976
+ for (const row of allFacts) {
977
+ const fact = this.parseFact(
978
+ (row as Record<string, unknown>).f as Record<string, unknown>,
979
+ );
980
+ const validation = await this.validateFact(fact);
981
+ if (!validation.valid && validation.suggestion === "delete") {
982
+ await this.deleteFact(fact.id);
983
+ pruned++;
984
+ }
985
+ }
986
+
987
+ return pruned;
988
+ }
989
+
990
+ // ============================================================================
991
+ // Session Management
992
+ // ============================================================================
993
+
994
+ async summarizeSession(
995
+ messages: { role: string; content: string }[],
996
+ ): Promise<string> {
997
+ // Simple extraction: look for key patterns in messages
998
+ // Could be enhanced with LLM-based summarization
999
+ const userMessages = messages
1000
+ .filter((m) => m.role === "user" || m.role === "assistant")
1001
+ .map((m) => m.content)
1002
+ .filter((c) => c.length > 0);
1003
+
1004
+ if (userMessages.length === 0) return "";
1005
+
1006
+ // Create a simple summary from first and last user messages
1007
+ const first = userMessages[0] ?? "";
1008
+ const last = userMessages[userMessages.length - 1] ?? "";
1009
+
1010
+ return `"${first.slice(0, 100)}"${userMessages.length > 1 ? ` → "${last.slice(0, 100)}"` : ""}`;
1011
+ }
1012
+
1013
+ async extractFromSession(
1014
+ messages: { role: string; content: string }[],
1015
+ ): Promise<Fact[]> {
1016
+ log("Extracting facts from session");
1017
+ // Extract facts from session using LLM-based analysis
1018
+ const recentMessages = messages.slice(-15);
1019
+ const conversationText = recentMessages
1020
+ .map((m) => `${m.role.toUpperCase()}:\n${m.content}`)
1021
+ .join("\n---\n");
1022
+
1023
+ const assistantMessages = messages
1024
+ .filter((m) => m.role === "assistant" || m.role === "user")
1025
+ .map((m) => m.content);
1026
+
1027
+ if (conversationText.length < 50 && assistantMessages.length === 0)
1028
+ return [];
1029
+
1030
+ // Try LLM-based extraction first
1031
+ try {
1032
+ if (conversationText.length >= 50) {
1033
+ const extracted = await this.extractWithLLM(conversationText);
1034
+ if (extracted.length > 0) return extracted;
1035
+ }
1036
+ } catch (e) {}
1037
+
1038
+ // Fallback: regex-based extraction
1039
+ return this.extractWithPatterns(assistantMessages);
1040
+ }
1041
+
1042
+ private async extractWithLLM(content: string): Promise<Fact[]> {
1043
+ const { extractFacts } = await import("@/agent/subagents/extractionAgent");
1044
+
1045
+ const extracted = await extractFacts(content, this.sessionId);
1046
+ log("Extracted facts: " + JSON.stringify(extracted, null, 2));
1047
+
1048
+ return extracted.map((f) => ({
1049
+ ...f,
1050
+ id: crypto.randomUUID(),
1051
+ timestamp: Date.now(),
1052
+ })) as Fact[];
1053
+ }
1054
+
1055
+ private extractWithPatterns(assistantMessages: string[]): Fact[] {
1056
+ const facts: Fact[] = [];
1057
+
1058
+ for (const content of assistantMessages) {
1059
+ const decisionPatterns = [
1060
+ /(?:decided|choosing|using|implementing)\s+([^.]+)/gi,
1061
+ /(?:will use|should use|recommend)\s+([^.]+)/gi,
1062
+ ];
1063
+
1064
+ for (const pattern of decisionPatterns) {
1065
+ let match;
1066
+ while ((match = pattern.exec(content)) !== null) {
1067
+ const captured = match[1];
1068
+ if (captured && captured.length > 10 && captured.length < 200) {
1069
+ facts.push({
1070
+ id: crypto.randomUUID(),
1071
+ type: "decision",
1072
+ content: captured.trim(),
1073
+ timestamp: Date.now(),
1074
+ sessionId: this.sessionId,
1075
+ confidence: 0.7,
1076
+ validated: Date.now(),
1077
+ });
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ // Look for fact patterns
1083
+ const factPatterns = [
1084
+ /(?:the project|this codebase|the app)\s+([^.]+)/gi,
1085
+ /(?:note that|remember that|important:)\s+([^.]+)/gi,
1086
+ ];
1087
+
1088
+ for (const pattern of factPatterns) {
1089
+ let match;
1090
+ while ((match = pattern.exec(content)) !== null) {
1091
+ const captured = match[1];
1092
+ if (captured && captured.length > 10 && captured.length < 200) {
1093
+ facts.push({
1094
+ id: crypto.randomUUID(),
1095
+ type: "fact",
1096
+ content: captured.trim(),
1097
+ timestamp: Date.now(),
1098
+ sessionId: this.sessionId,
1099
+ confidence: 0.6,
1100
+ validated: Date.now(),
1101
+ });
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ return facts;
1108
+ }
1109
+
1110
+ getSessionId(): string {
1111
+ return this.sessionId;
1112
+ }
1113
+
1114
+ // ============================================================================
1115
+ // Cleanup
1116
+ // ============================================================================
1117
+
1118
+ async reset(): Promise<void> {
1119
+ await this.init();
1120
+
1121
+ // Clear Graph DB - drop relation tables first (they depend on Fact)
1122
+ if (this.conn) {
1123
+ try {
1124
+ await this.conn.query("DROP TABLE RELATED_TO");
1125
+ } catch (e) {}
1126
+
1127
+ try {
1128
+ await this.conn.query("DROP TABLE Fact");
1129
+ } catch (e) {}
1130
+ }
1131
+
1132
+ // Clear Vector DB
1133
+ await this.embeddings.clear();
1134
+
1135
+ // Re-initialize schema
1136
+ await this.initSchema();
1137
+ }
1138
+
1139
+ async close(): Promise<void> {
1140
+ if (this.conn) {
1141
+ await this.conn.close();
1142
+ this.conn = null;
1143
+ }
1144
+ if (this.db) {
1145
+ await this.db.close();
1146
+ this.db = null;
1147
+ }
1148
+ this.initPromise = null;
1149
+ }
1150
+ }
1151
+
1152
+ export default AgentCognition;