flint-agent 1.14.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 (171) hide show
  1. package/.env.example +108 -0
  2. package/CHANGELOG.md +55 -0
  3. package/FEATURES.md +298 -0
  4. package/LICENSE +21 -0
  5. package/README.md +435 -0
  6. package/bin/flint.js +47 -0
  7. package/config/classifier-prompt.md +218 -0
  8. package/config/models-curated.json +4 -0
  9. package/config/providers.json +74 -0
  10. package/package.json +92 -0
  11. package/patches/ink+6.8.0.patch +78 -0
  12. package/profiles/desktop.md +65 -0
  13. package/profiles/generic.md +20 -0
  14. package/profiles/marketer.md +20 -0
  15. package/profiles/profiles.json +34 -0
  16. package/profiles/ux-reviewer.md +25 -0
  17. package/src/agent/agent.js +1743 -0
  18. package/src/agent/auto.js +346 -0
  19. package/src/agent/backoff.js +143 -0
  20. package/src/agent/compression.js +310 -0
  21. package/src/agent/content-resolver.js +180 -0
  22. package/src/agent/flow-controller.js +309 -0
  23. package/src/agent/intent-manifest.js +231 -0
  24. package/src/agent/intent-timeout.js +46 -0
  25. package/src/agent/intent.js +633 -0
  26. package/src/agent/knowledge.js +114 -0
  27. package/src/agent/learning.js +180 -0
  28. package/src/agent/modes.js +187 -0
  29. package/src/agent/outcome-ask.js +91 -0
  30. package/src/agent/project-context.js +76 -0
  31. package/src/agent/prompt-budget.js +117 -0
  32. package/src/agent/reflection-extractor.js +140 -0
  33. package/src/agent/steering.js +86 -0
  34. package/src/agent/supervisor.js +430 -0
  35. package/src/agent/swap.js +443 -0
  36. package/src/agent/system-prompt.js +446 -0
  37. package/src/agent/time-stamp.js +48 -0
  38. package/src/agent/tool-guard.js +201 -0
  39. package/src/agent/toolcall-text.js +162 -0
  40. package/src/agent/usage.js +297 -0
  41. package/src/agent/vision.js +94 -0
  42. package/src/agent/watchdog.js +139 -0
  43. package/src/agent/workspace-changes.js +177 -0
  44. package/src/api/address.js +14 -0
  45. package/src/api/client.js +280 -0
  46. package/src/api/server.js +535 -0
  47. package/src/api/stream-pipe.js +113 -0
  48. package/src/app-state.js +39 -0
  49. package/src/bootstrap.js +501 -0
  50. package/src/bus/drain-loop.js +497 -0
  51. package/src/bus/index.js +270 -0
  52. package/src/bus/plugins.js +65 -0
  53. package/src/child-idle.js +14 -0
  54. package/src/cli.js +118 -0
  55. package/src/commands/commands.js +1297 -0
  56. package/src/commands/registry.js +132 -0
  57. package/src/components/App.js +491 -0
  58. package/src/components/CarefulMenu.js +145 -0
  59. package/src/components/HistoryWriter.js +86 -0
  60. package/src/components/LineInput.js +69 -0
  61. package/src/components/LiveZone.js +294 -0
  62. package/src/components/OverlayMenu.js +179 -0
  63. package/src/components/SystemPanel.js +156 -0
  64. package/src/components/Table.js +54 -0
  65. package/src/config.js +249 -0
  66. package/src/free-models.js +230 -0
  67. package/src/index.js +1111 -0
  68. package/src/input-handler.js +13 -0
  69. package/src/input-text.js +123 -0
  70. package/src/launcher.js +129 -0
  71. package/src/logging/api-log.js +95 -0
  72. package/src/logging/chat-log-follower.js +113 -0
  73. package/src/logging/chat-log.js +15 -0
  74. package/src/logging/log-collector.js +182 -0
  75. package/src/logging/logger.js +112 -0
  76. package/src/logging/tool-log.js +20 -0
  77. package/src/mcp-client.js +314 -0
  78. package/src/memory/conversation-digest.js +113 -0
  79. package/src/memory/extract-facts.js +98 -0
  80. package/src/memory/facts.js +181 -0
  81. package/src/memory/inbox.js +63 -0
  82. package/src/memory/markdown.js +38 -0
  83. package/src/memory/patterns.js +185 -0
  84. package/src/memory/project.js +66 -0
  85. package/src/memory/reflections.js +74 -0
  86. package/src/memory/retrieval.js +84 -0
  87. package/src/memory/rules.js +105 -0
  88. package/src/memory/session-facts.js +125 -0
  89. package/src/memory/skills.js +191 -0
  90. package/src/memory/sqlite-store.js +653 -0
  91. package/src/memory/store.js +208 -0
  92. package/src/memory/tools.js +196 -0
  93. package/src/memory/user-model.js +86 -0
  94. package/src/message-handler.js +775 -0
  95. package/src/model-check.js +218 -0
  96. package/src/plugins/loader.js +120 -0
  97. package/src/plugins/manager.js +88 -0
  98. package/src/production-env.js +22 -0
  99. package/src/profiles.js +42 -0
  100. package/src/providers/adapters/anthropic.js +270 -0
  101. package/src/providers/adapters/openai.js +120 -0
  102. package/src/providers/keys-dpapi.js +41 -0
  103. package/src/providers/keys-fallback.js +31 -0
  104. package/src/providers/keys.js +132 -0
  105. package/src/providers/models.js +154 -0
  106. package/src/providers/registry.js +56 -0
  107. package/src/providers/state.js +56 -0
  108. package/src/registry.js +96 -0
  109. package/src/restart.js +29 -0
  110. package/src/sandbox/backend.js +130 -0
  111. package/src/security/api-auth.js +132 -0
  112. package/src/security/audit.js +98 -0
  113. package/src/security/child-policy.js +41 -0
  114. package/src/security/command-guard.js +173 -0
  115. package/src/security/content-fence.js +250 -0
  116. package/src/security/content-validator.js +132 -0
  117. package/src/security/index.js +143 -0
  118. package/src/security/network-guard.js +126 -0
  119. package/src/security/pairing.js +180 -0
  120. package/src/security/path-guard.js +140 -0
  121. package/src/security/persona-guard.js +67 -0
  122. package/src/security/policies.js +452 -0
  123. package/src/security/safety-constants.js +34 -0
  124. package/src/security/watchdog.js +107 -0
  125. package/src/sessions.js +130 -0
  126. package/src/spend.js +97 -0
  127. package/src/startup-watchdog.js +59 -0
  128. package/src/stdio/args.js +71 -0
  129. package/src/stdio/guard.js +59 -0
  130. package/src/stdio/protocol.js +167 -0
  131. package/src/stdio/run.js +106 -0
  132. package/src/stdio/session.js +180 -0
  133. package/src/store/agent-slice.js +306 -0
  134. package/src/store/dataset-slice.js +73 -0
  135. package/src/store/index.js +22 -0
  136. package/src/store/process-slice.js +135 -0
  137. package/src/store/session-slice.js +191 -0
  138. package/src/store/ui-slice.js +119 -0
  139. package/src/tasks/db.js +184 -0
  140. package/src/tasks/queries.js +589 -0
  141. package/src/tools/agent-tools.js +473 -0
  142. package/src/tools/checkpoint.js +152 -0
  143. package/src/tools/command-approvals.js +180 -0
  144. package/src/tools/dataset.js +50 -0
  145. package/src/tools/filesystem.js +682 -0
  146. package/src/tools/inbox-tools.js +48 -0
  147. package/src/tools/mesh.js +135 -0
  148. package/src/tools/own-env.js +136 -0
  149. package/src/tools/permissions.js +681 -0
  150. package/src/tools/plugin-tools.js +123 -0
  151. package/src/tools/process-tools.js +595 -0
  152. package/src/tools/registry.js +307 -0
  153. package/src/tools/swap-tools.js +72 -0
  154. package/src/tools/system.js +662 -0
  155. package/src/tools/tasks.js +532 -0
  156. package/src/tools/tool-search.js +171 -0
  157. package/src/ui/header.js +140 -0
  158. package/src/ui/input-cursor.js +23 -0
  159. package/src/ui/last-line.js +25 -0
  160. package/src/ui/line-edit.js +135 -0
  161. package/src/ui/output.js +399 -0
  162. package/src/ui/paste-tokens.js +131 -0
  163. package/src/ui/prompt-attention.js +134 -0
  164. package/src/ui/render-options.js +13 -0
  165. package/src/ui/replay.js +94 -0
  166. package/src/ui/splash.js +49 -0
  167. package/src/ui/status-level.js +36 -0
  168. package/src/ui/tool-ledger.js +203 -0
  169. package/src/ui/window-title.js +150 -0
  170. package/src/update.js +205 -0
  171. package/system.md +63 -0
@@ -0,0 +1,181 @@
1
+ // Facts memory — Layer 4 of memory architecture.
2
+ //
3
+ // User/project facts explicitly stated or extracted from conversation.
4
+ // "We use yarn not npm", "project runs on port 8080", "DB is PostgreSQL".
5
+ // Injected into system prompt so agent applies them without being reminded.
6
+ //
7
+ // Internal task reference removed.
8
+
9
+ import { insertFact, getAllFacts, deleteFact, indexEntry, searchFts, setFactPinned } from "./sqlite-store.js";
10
+
11
+ const MAX_FACTS = 100;
12
+
13
+ /**
14
+ * Add a fact.
15
+ * @param {string} content — the fact text
16
+ * @param {string} category — general|project|preference|environment
17
+ * @param {string} source — "user" or "agent"
18
+ * @param {number} confidence — 0.0 to 1.0
19
+ * @returns {string} fact id
20
+ */
21
+ export function addFact(content, category = "general", source = "agent", confidence = 1.0, project = null) {
22
+ if (!content || !content.trim()) return null;
23
+
24
+ // Dedupe — don't add if very similar fact exists (any project)
25
+ const existing = searchFts(content.slice(0, 50), { layer: "facts", limit: 3 });
26
+ for (const e of existing) {
27
+ if (e.text && textSimilarity(e.text, content) > 0.8) {
28
+ return null; // too similar, skip
29
+ }
30
+ }
31
+
32
+ // Eviction: never evict pinned facts. If cap reached and no unpinned
33
+ // fact exists to evict, silently reject the new fact (prevents the cap
34
+ // from being exceeded when all slots are curated).
35
+ const all = getAllFacts();
36
+ if (all.length >= MAX_FACTS) {
37
+ // getAllFacts returns ORDER BY created_at DESC, so last element is oldest
38
+ const oldestUnpinned = [...all].reverse().find(f => !f.pinned);
39
+ if (!oldestUnpinned) return null; // all slots pinned — refuse new fact
40
+ deleteFact(oldestUnpinned.id);
41
+ }
42
+
43
+ const now = new Date();
44
+ // A random tail: two facts added in the same millisecond got the same id,
45
+ // and the second overwrote the first without a word.
46
+ const id = `fact-${now.getTime().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
47
+ insertFact({ id, category, content: content.trim(), source, confidence, project, pinned: 0 });
48
+ indexEntry("facts", id, content.trim());
49
+ return id;
50
+ }
51
+
52
+ /**
53
+ * Remove a fact by id.
54
+ */
55
+ export function removeFact(id) {
56
+ deleteFact(id);
57
+ }
58
+
59
+ /**
60
+ * Pin a fact — it will never be LRU-evicted. Use for durable knowledge
61
+ * (user preferences, project metadata) that must survive heavy write
62
+ * sessions (benchmarks, bulk agent work).
63
+ */
64
+ export function pinFact(id) {
65
+ setFactPinned(id, true);
66
+ }
67
+
68
+ /**
69
+ * Unpin a fact — it becomes eligible for LRU eviction again.
70
+ */
71
+ export function unpinFact(id) {
72
+ setFactPinned(id, false);
73
+ }
74
+
75
+ /**
76
+ * Get facts, optionally filtered by category and/or project scope.
77
+ * @param {string|null} category — filter by category, null = all
78
+ * @param {string|null|undefined} project — project scope:
79
+ * undefined/null = all facts (no scope filter)
80
+ * "" (empty string) = global only (project IS NULL)
81
+ * "name" = project "name" + global facts (union)
82
+ */
83
+ export function getFacts(category = null, project = null) {
84
+ const all = project === null ? getAllFacts() : getAllFacts({ project });
85
+ if (category) return all.filter(f => f.category === category);
86
+ return all;
87
+ }
88
+
89
+ /**
90
+ * Format facts as system-prompt-ready block. Groups by category.
91
+ * Uses the given project scope: global facts always included; project-scoped
92
+ * only when currentProject matches. If currentProject is null, only globals.
93
+ *
94
+ * @param {number} maxTokens — approximate token budget
95
+ * @param {string|null} currentProject — project name or null
96
+ */
97
+ export function formatForPrompt(maxTokens = 500, currentProject = null) {
98
+ const all = currentProject
99
+ ? getAllFacts({ project: currentProject }) // scoped + global union
100
+ : getAllFacts({ project: "" }); // global only
101
+ if (!all.length) return "";
102
+
103
+ const groups = {};
104
+ for (const f of all) {
105
+ const cat = f.category || "general";
106
+ if (!groups[cat]) groups[cat] = [];
107
+ groups[cat].push(f);
108
+ }
109
+
110
+ const scopeLabel = currentProject ? `(scope: ${currentProject} + global)` : "(scope: global)";
111
+ const lines = [`Known facts about user and project ${scopeLabel}:`];
112
+ let tokens = 0;
113
+ for (const [cat, facts] of Object.entries(groups)) {
114
+ lines.push(` [${cat}]`);
115
+ for (const f of facts) {
116
+ const marker = f.project ? `§[${f.project}]` : "§";
117
+ const line = ` ${marker} ${f.content}`;
118
+ tokens += line.split(/\s+/).length;
119
+ if (tokens > maxTokens) break;
120
+ lines.push(line);
121
+ }
122
+ if (tokens > maxTokens) break;
123
+ }
124
+ return lines.join("\n");
125
+ }
126
+
127
+ /**
128
+ * Extract facts from a conversation turn.
129
+ * Heuristic: look for patterns like "we use X", "our project is Y",
130
+ * "I prefer Z", "the server runs on W".
131
+ * @param {string} userMessage
132
+ * @returns {Array<{content, category, confidence}>}
133
+ */
134
+ /**
135
+ * Extract facts from a user message. Uses the LLM-based extractor from
136
+ * extract-facts.js (async, semantic). No regex, no hardcoded patterns —
137
+ * the LLM decides what's a durable fact.
138
+ *
139
+ * Gated: only calls LLM for messages >= 20 chars that look like disclosures
140
+ * (contain a pronoun-like signal). Short chit-chat ("ok", "yes", "done")
141
+ * never triggers a call.
142
+ *
143
+ * @param {string} userMessage
144
+ * @returns {Promise<Array<{content, category, confidence}>>}
145
+ */
146
+ export async function extractFacts(userMessage) {
147
+ if (!userMessage || userMessage.length < 20) return [];
148
+ // Cheap language-neutral pre-filter — skip if message is clearly a question
149
+ // or a bare command. Real disclosures are declarative statements.
150
+ // Previously this used RU+EN keyword regex which was biased against
151
+ // Ukrainian / German / Chinese users. Replaced 2026-04-19.
152
+ const trimmed = userMessage.trim();
153
+ // Skip pure questions (ends with ?)
154
+ if (/\?\s*$/.test(trimmed)) return [];
155
+ // Skip very short declarations (covered by length check above, but also
156
+ // short imperative commands like "read X" or "open Y")
157
+ const wordCount = trimmed.split(/\s+/).length;
158
+ if (wordCount < 5) return [];
159
+
160
+ try {
161
+ const { extractFacts: extractViaLlm } = await import("./extract-facts.js");
162
+ // extract-facts.js expects messages; wrap the single user message
163
+ const out = await extractViaLlm([{ role: "user", content: userMessage }]);
164
+ if (!Array.isArray(out)) return [];
165
+ return out.map(f => ({ content: f.content, category: f.category || "general", confidence: 0.8 }));
166
+ } catch {
167
+ return [];
168
+ }
169
+ }
170
+
171
+ // Simple word-overlap similarity
172
+ function textSimilarity(a, b) {
173
+ const wa = new Set(a.toLowerCase().split(/\s+/));
174
+ const wb = new Set(b.toLowerCase().split(/\s+/));
175
+ let inter = 0;
176
+ for (const w of wa) if (wb.has(w)) inter++;
177
+ const union = wa.size + wb.size - inter;
178
+ return union === 0 ? 0 : inter / union;
179
+ }
180
+
181
+ export const __internal = { MAX_FACTS };
@@ -0,0 +1,63 @@
1
+ // Notification inbox — queues reminders, messages, events
2
+ // Agent sees count in system prompt, reads with check_inbox tool
3
+ // Does NOT interrupt current task focus
4
+
5
+ let _items = []; // [{ id, type, content, from, ts }]
6
+ let _nextId = 1;
7
+
8
+ /**
9
+ * Add a notification to the inbox.
10
+ * @param {"reminder"|"message"|"event"} type
11
+ * @param {string} content
12
+ * @param {string} [from] - source (e.g. "schedule", "api", "cron")
13
+ */
14
+ export function pushInbox(type, content, from = "") {
15
+ _items.push({
16
+ id: _nextId++,
17
+ type,
18
+ content,
19
+ from,
20
+ ts: new Date().toISOString(),
21
+ read: false,
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Get count of unread notifications.
27
+ */
28
+ export function unreadCount() {
29
+ return _items.filter((i) => !i.read).length;
30
+ }
31
+
32
+ /**
33
+ * Get all unread notifications and mark them as read.
34
+ */
35
+ export function readInbox() {
36
+ const unread = _items.filter((i) => !i.read);
37
+ for (const item of unread) {
38
+ item.read = true;
39
+ }
40
+ return unread;
41
+ }
42
+
43
+ /**
44
+ * Get all items (read + unread), last N.
45
+ */
46
+ export function allInbox(limit = 50) {
47
+ return _items.slice(-limit);
48
+ }
49
+
50
+ /**
51
+ * Format inbox summary for system prompt injection.
52
+ * Returns null if no unread notifications.
53
+ */
54
+ export function inboxPromptHint() {
55
+ const count = unreadCount();
56
+ if (count === 0) return null;
57
+ const types = {};
58
+ for (const item of _items.filter((i) => !i.read)) {
59
+ types[item.type] = (types[item.type] || 0) + 1;
60
+ }
61
+ const parts = Object.entries(types).map(([t, n]) => `${n} ${t}${n > 1 ? "s" : ""}`);
62
+ return `You have ${count} unread notification${count > 1 ? "s" : ""} (${parts.join(", ")}). Use check_inbox tool to read them when you have a moment. Do NOT interrupt your current task.`;
63
+ }
@@ -0,0 +1,38 @@
1
+ import { writeFileSync, existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { config } from "../config.js";
4
+ import { loadAll, getMemoryStats } from "./store.js";
5
+
6
+ const MEMORY_MD = path.join(config.projectRoot, "MEMORY.md");
7
+
8
+ export function updateMemoryMd() {
9
+ const memories = loadAll();
10
+ const stats = getMemoryStats();
11
+ const grouped = {};
12
+ for (const m of memories) {
13
+ if (!grouped[m.category]) grouped[m.category] = [];
14
+ grouped[m.category].push(m);
15
+ }
16
+
17
+ let md = `# Agent Memory (${stats.total} entries)\n\n`;
18
+
19
+ for (const [cat, items] of Object.entries(grouped)) {
20
+ md += `## ${cat} (${items.length})\n`;
21
+ for (const m of items) {
22
+ const date = m.created_at?.slice(0, 10) || "unknown";
23
+ const stars = "*".repeat(Math.min(m.importance, 3));
24
+ md += `- [#${m.id}] ${date}: ${m.content} ${stars}\n`;
25
+ }
26
+ md += "\n";
27
+ }
28
+
29
+ md += `Last updated: ${new Date().toISOString()}\n`;
30
+ writeFileSync(MEMORY_MD, md, "utf-8");
31
+ return md;
32
+ }
33
+
34
+ export function readMemoryMdHead(lines = 50) {
35
+ if (!existsSync(MEMORY_MD)) return "";
36
+ const content = readFileSync(MEMORY_MD, "utf-8");
37
+ return content.split("\n").slice(0, lines).join("\n");
38
+ }
@@ -0,0 +1,185 @@
1
+ // Patterns memory — Layer 2 of memory architecture.
2
+ //
3
+ // Records per-turn: what user request (tokenized) → which tool the agent
4
+ // chose first. Compiles into a tool-preference table injected at session
5
+ // start. Purpose: triplet-consistency — three paraphrases of the same
6
+ // request should converge on the same tool.
7
+
8
+ import {
9
+ insertPattern, getAllPatterns, migrateFromJsonl,
10
+ findSimilarPattern, incrementPatternSuccess, replacePatternTool, getTopPatterns,
11
+ } from "./sqlite-store.js";
12
+
13
+ // Run migration on first import (idempotent)
14
+ try { migrateFromJsonl(); } catch {}
15
+
16
+ const STOP_WORDS = new Set([
17
+ // English
18
+ "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
19
+ "have", "has", "had", "do", "does", "did", "will", "would", "should", "could",
20
+ "can", "may", "might", "i", "you", "he", "she", "it", "we", "they",
21
+ "my", "your", "his", "her", "its", "our", "their", "this", "that", "these", "those",
22
+ "to", "of", "in", "on", "at", "for", "with", "by", "from", "as", "and", "or", "but",
23
+ "not", "no", "if", "so", "then", "than", "when", "where", "what", "why", "how",
24
+ "please", "me", "some",
25
+ ]);
26
+
27
+ /**
28
+ * Tokenize a request into lowercased, stop-word-filtered tokens.
29
+ * Keeps letters and digits of any script, 3+ chars only.
30
+ */
31
+ export function tokenize(text) {
32
+ if (!text || typeof text !== "string") return [];
33
+ // Split on anything that is not a letter or digit, in any script
34
+ const raw = text.toLowerCase().split(/[^\p{L}\p{N}_]+/u);
35
+ const out = [];
36
+ const seen = new Set();
37
+ for (const t of raw) {
38
+ if (!t || t.length < 3) continue;
39
+ if (STOP_WORDS.has(t)) continue;
40
+ if (seen.has(t)) continue;
41
+ seen.add(t);
42
+ out.push(t);
43
+ }
44
+ return out;
45
+ }
46
+
47
+ /**
48
+ * Record a pattern with versioning support.
49
+ * - If similar tokens + same tool exists: increment success_count (reinforcement)
50
+ * - If similar tokens + different tool: replace tool (correction/versioning)
51
+ * - Otherwise: insert new pattern
52
+ * @param {{ request:string, first_tool:string, all_tools?:string[], ok?:boolean, session_id?:string }} p
53
+ * @returns {string|null} id or null if skipped
54
+ */
55
+ export function recordPattern(p) {
56
+ if (!p || !p.request || !p.first_tool) return null;
57
+ const tokens = tokenize(p.request);
58
+ if (tokens.length === 0) return null;
59
+
60
+ // Check for existing similar pattern (Jaccard >= 0.5 on tokens)
61
+ const existing = findSimilarPattern(tokens, p.first_tool);
62
+ if (existing) {
63
+ // Same tool: reinforce
64
+ incrementPatternSuccess(existing.id);
65
+ return existing.id;
66
+ }
67
+
68
+ // Check if there's a pattern with same tokens but DIFFERENT tool (correction)
69
+ const allPatterns = getAllPatterns();
70
+ for (const ep of allPatterns) {
71
+ if (ep.first_tool === p.first_tool) continue;
72
+ const intersection = tokens.filter(t => (ep.tokens || []).includes(t)).length;
73
+ const union = new Set([...tokens, ...(ep.tokens || [])]).size;
74
+ if (union > 0 && intersection / union >= 0.5) {
75
+ // Correction: replace tool, increment version + fail_count
76
+ replacePatternTool(ep.id, p.first_tool, Array.isArray(p.all_tools) ? p.all_tools.slice(0, 20) : [p.first_tool]);
77
+ return ep.id;
78
+ }
79
+ }
80
+
81
+ // New pattern
82
+ const now = new Date();
83
+ const record = {
84
+ // Random tail for the same reason as facts.js: one millisecond, one id.
85
+ id: `p-${now.getTime().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
86
+ ts: now.toISOString(),
87
+ session_id: p.session_id || "",
88
+ request: String(p.request).slice(0, 500),
89
+ tokens,
90
+ first_tool: p.first_tool,
91
+ all_tools: Array.isArray(p.all_tools) ? p.all_tools.slice(0, 20) : [p.first_tool],
92
+ ok: p.ok !== false,
93
+ success_count: 1,
94
+ fail_count: 0,
95
+ version: 1,
96
+ };
97
+ insertPattern(record);
98
+ return record.id;
99
+ }
100
+
101
+ export function loadAll() {
102
+ return getAllPatterns();
103
+ }
104
+
105
+ /**
106
+ * For each tool, find the tokens most predictive of that tool choice.
107
+ * Returns a map: tool_name → [tokens sorted by descriminating power].
108
+ *
109
+ * Method: for each (tool, token) pair, compute conditional probability
110
+ * P(tool | token) = count(tool ∧ token) / count(token)
111
+ * Only tokens seen ≥ MIN_TOKEN_USES times are kept. Only pairs with
112
+ * P ≥ MIN_CONFIDENCE are kept.
113
+ */
114
+ export function compilePreferences(opts = {}) {
115
+ const MIN_TOKEN_USES = opts.minTokenUses || 3;
116
+ const MIN_CONFIDENCE = opts.minConfidence || 0.6;
117
+ const MAX_TOKENS_PER_TOOL = opts.maxTokensPerTool || 6;
118
+ const records = loadAll();
119
+ // Check total weight (success_count sum), not just record count
120
+ const totalWeight = records.reduce((s, r) => s + (r.success_count ?? 1), 0);
121
+ if (totalWeight < MIN_TOKEN_USES) return {};
122
+
123
+ // Count per token: total + per-tool (weighted by success_count)
124
+ const tokenTotal = new Map();
125
+ const tokenByTool = new Map(); // token → Map(tool → count)
126
+ for (const r of records) {
127
+ if (!r.ok) continue;
128
+ // Filter out patterns with poor success rate
129
+ const sr = (r.success_count ?? 1) / Math.max((r.success_count ?? 1) + (r.fail_count ?? 0), 1);
130
+ if (sr < 0.4) continue; // skip patterns that fail more than 60% of the time
131
+ const weight = r.success_count ?? 1; // use success_count as weight
132
+ for (const tok of r.tokens || []) {
133
+ tokenTotal.set(tok, (tokenTotal.get(tok) || 0) + weight);
134
+ if (!tokenByTool.has(tok)) tokenByTool.set(tok, new Map());
135
+ const tm = tokenByTool.get(tok);
136
+ tm.set(r.first_tool, (tm.get(r.first_tool) || 0) + weight);
137
+ }
138
+ }
139
+
140
+ // For each tool, collect tokens where P(tool | token) >= threshold
141
+ const prefs = {}; // tool → [{token, confidence, support}]
142
+ for (const [token, total] of tokenTotal) {
143
+ if (total < MIN_TOKEN_USES) continue;
144
+ const tm = tokenByTool.get(token);
145
+ for (const [tool, count] of tm) {
146
+ const confidence = count / total;
147
+ if (confidence < MIN_CONFIDENCE) continue;
148
+ if (!prefs[tool]) prefs[tool] = [];
149
+ prefs[tool].push({ token, confidence, support: count });
150
+ }
151
+ }
152
+
153
+ // Sort and cap per tool
154
+ for (const tool of Object.keys(prefs)) {
155
+ prefs[tool].sort((a, b) => {
156
+ // Primary: confidence desc; secondary: support desc
157
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
158
+ return b.support - a.support;
159
+ });
160
+ prefs[tool] = prefs[tool].slice(0, MAX_TOKENS_PER_TOOL);
161
+ }
162
+
163
+ return prefs;
164
+ }
165
+
166
+ /**
167
+ * Format compiled preferences as a compact prompt-ready block.
168
+ * @param {Record<string, Array<{token,confidence,support}>>} prefs
169
+ * @returns {string}
170
+ */
171
+ export function formatForPrompt(prefs) {
172
+ const tools = Object.keys(prefs);
173
+ if (!tools.length) return "";
174
+ const lines = ["Tool preferences learned from past successful sessions:"];
175
+ for (const tool of tools) {
176
+ const items = prefs[tool];
177
+ if (!items?.length) continue;
178
+ const tokens = items.map(i => `${i.token} (${Math.round(i.confidence * 100)}%, n=${i.support})`).join(", ");
179
+ lines.push(` ${tool}: ${tokens}`);
180
+ }
181
+ lines.push("Prefer these tools when the user's request contains the listed keywords.");
182
+ return lines.join("\n");
183
+ }
184
+
185
+ export const __internal = { STOP_WORDS };
@@ -0,0 +1,66 @@
1
+ // Project scope detection.
2
+ //
3
+ // Facts are split into global (about the user) and project-scoped (about a
4
+ // specific project). This file resolves "what project are we currently in?"
5
+ // so the rest of the memory layer can filter appropriately.
6
+ //
7
+ // Priority:
8
+ // 1. Explicit override via setCurrentProject(name) — from /project set
9
+ // 2. MEMORY.md in current workspace contains `project: <name>` frontmatter
10
+ // 3. CWD leaf name (last path segment) matches a known project from registry
11
+ // 4. Fallback: null (no project scope; everything treated as global)
12
+
13
+ import { readFileSync, existsSync } from "node:fs";
14
+ import { join, basename } from "node:path";
15
+
16
+ let _explicitProject = null;
17
+
18
+ /**
19
+ * Explicit override — set by /project set NAME. Takes priority over detection.
20
+ */
21
+ export function setCurrentProject(name) {
22
+ _explicitProject = name ? String(name).trim().toLowerCase() : null;
23
+ }
24
+
25
+ /**
26
+ * Clear the explicit override and fall back to detection.
27
+ */
28
+ export function clearCurrentProject() {
29
+ _explicitProject = null;
30
+ }
31
+
32
+ /**
33
+ * Detect the current project name.
34
+ *
35
+ * @param {string} [cwd=process.cwd()] — working directory
36
+ * @returns {string|null} lowercase project name, or null if no project
37
+ */
38
+ export function getCurrentProject(cwd = process.cwd()) {
39
+ if (_explicitProject) return _explicitProject;
40
+
41
+ // 2. MEMORY.md with `project:` frontmatter
42
+ try {
43
+ const memoryPath = join(cwd, "MEMORY.md");
44
+ if (existsSync(memoryPath)) {
45
+ const content = readFileSync(memoryPath, "utf-8").slice(0, 1000);
46
+ const match = content.match(/^project:\s*([a-z0-9-_]+)/im);
47
+ if (match) return match[1].toLowerCase();
48
+ }
49
+ } catch {}
50
+
51
+ // 3. CWD leaf — only if it matches a plausible project pattern.
52
+ // Using leaf makes this automatic for users working in repo dirs like
53
+ // `flint-agent`, `my-app`, `screenbox`. Normalised: lowercased, strip trailing
54
+ // suffixes like "-agent" that are tooling-specific.
55
+ try {
56
+ const leaf = basename(cwd).toLowerCase();
57
+ // Ignore generic names that could collide: "src", "tmp", "home", "desktop"
58
+ const ignored = new Set(["src", "tmp", "temp", "home", "desktop", "documents", "downloads", "node_modules", ""]);
59
+ if (leaf && !ignored.has(leaf) && /^[a-z0-9][a-z0-9-_]{1,40}$/.test(leaf)) {
60
+ // Strip common suffixes
61
+ return leaf.replace(/-agent$|-app$|-site$/, "");
62
+ }
63
+ } catch {}
64
+
65
+ return null;
66
+ }
@@ -0,0 +1,74 @@
1
+ // Reflections memory — auto-extracted learnings at session end.
2
+ //
3
+ // Layer 1 of memory architecture.
4
+ // Backend: SQLite via sqlite-store.js (migrated from JSONL 2026-04-16).
5
+ // At session end, model is prompted with 3 questions (did / wrong / better),
6
+ // response is parsed and stored. At next session start, last N are prepended
7
+ // to system prompt as trusted-context so model doesn't repeat past mistakes.
8
+
9
+ import { insertReflection, getRecentReflections, getAllReflections, migrateFromJsonl } from "./sqlite-store.js";
10
+
11
+ // Run migration on first import (idempotent — skips if already migrated)
12
+ try { migrateFromJsonl(); } catch {}
13
+
14
+ /**
15
+ * Append a reflection.
16
+ * @param {{ id?:string, ts?:string, session_id?:string, did:string, wrong:string[], better:string[], tags?:string[] }} reflection
17
+ * @returns {string|null} id
18
+ */
19
+ export function appendReflection(reflection) {
20
+ const now = new Date();
21
+ const record = {
22
+ id: reflection.id || `r-${now.toISOString().slice(0, 10)}-${now.getTime().toString(36).slice(-5)}${Math.random().toString(36).slice(2, 5)}`,
23
+ ts: reflection.ts || now.toISOString(),
24
+ session_id: reflection.session_id || "",
25
+ did: String(reflection.did || "").trim(),
26
+ wrong: Array.isArray(reflection.wrong) ? reflection.wrong.filter(Boolean) : [],
27
+ better: Array.isArray(reflection.better) ? reflection.better.filter(Boolean) : [],
28
+ tags: Array.isArray(reflection.tags) ? reflection.tags.filter(Boolean) : [],
29
+ };
30
+ if (!record.did && record.wrong.length === 0 && record.better.length === 0) {
31
+ return null;
32
+ }
33
+ return insertReflection(record);
34
+ }
35
+
36
+ /**
37
+ * Load last N reflections (most recent first).
38
+ */
39
+ export function loadRecent(n = 5) {
40
+ return getRecentReflections(n);
41
+ }
42
+
43
+ /**
44
+ * Load all reflections. Used by the pattern analyzer (Layer 2).
45
+ */
46
+ export function loadAll() {
47
+ return getAllReflections();
48
+ }
49
+
50
+ /**
51
+ * Format reflections as a system-prompt-ready text block.
52
+ * Short, scannable, focused on 'better' guidance.
53
+ * @param {object[]} reflections
54
+ * @returns {string}
55
+ */
56
+ export function formatForPrompt(reflections) {
57
+ if (!reflections.length) return "";
58
+ const lines = ["Past lessons from recent sessions (avoid repeating these mistakes):"];
59
+ for (const r of reflections) {
60
+ const date = r.ts ? r.ts.slice(0, 10) : "";
61
+ if (r.wrong.length) {
62
+ lines.push(`\n[${date}] I did wrong:`);
63
+ for (const w of r.wrong.slice(0, 3)) lines.push(` - ${w}`);
64
+ }
65
+ if (r.better.length) {
66
+ lines.push(`[${date}] Next time:`);
67
+ for (const b of r.better.slice(0, 3)) lines.push(` - ${b}`);
68
+ }
69
+ }
70
+ return lines.join("\n");
71
+ }
72
+
73
+ // Exposed for tests (legacy references removed — now in sqlite-store)
74
+ export const __internal = {};
@@ -0,0 +1,84 @@
1
+ // Retrieval memory — Layer 4 of memory architecture.
2
+ //
3
+ // Unlike Layers 1-3 (frozen at session start in system prompt), Layer 4
4
+ // injects PER-TURN context: on each user message, look up the most
5
+ // similar past requests and surface "similar past requests used tool X"
6
+ // as a just-in-time hint.
7
+ //
8
+ // Backing store: patterns.jsonl (already populated by Layer 2).
9
+ // Similarity: Jaccard token overlap (no embeddings, no extra deps).
10
+
11
+ import { loadAll as loadAllPatterns } from "./patterns.js";
12
+ import { tokenize } from "./patterns.js";
13
+
14
+ function jaccard(aSet, bSet) {
15
+ let inter = 0;
16
+ for (const x of aSet) if (bSet.has(x)) inter++;
17
+ const union = aSet.size + bSet.size - inter;
18
+ return union === 0 ? 0 : inter / union;
19
+ }
20
+
21
+ /**
22
+ * Find the top-K most-similar past requests to the given request.
23
+ * @param {string} request
24
+ * @param {object} opts
25
+ * @param {number} opts.topK — how many matches to return (default 5)
26
+ * @param {number} opts.minSimilarity — Jaccard threshold (default 0.25)
27
+ * @returns {Array<{record:object, similarity:number}>}
28
+ */
29
+ export function findSimilarRequests(request, opts = {}) {
30
+ const topK = opts.topK || 5;
31
+ const minSim = opts.minSimilarity ?? 0.25;
32
+ if (!request || typeof request !== "string") return [];
33
+ const queryTokens = new Set(tokenize(request));
34
+ if (queryTokens.size === 0) return [];
35
+
36
+ const records = loadAllPatterns();
37
+ const scored = [];
38
+ for (const r of records) {
39
+ if (!r.tokens || !Array.isArray(r.tokens)) continue;
40
+ const s = jaccard(queryTokens, new Set(r.tokens));
41
+ if (s < minSim) continue;
42
+ scored.push({ record: r, similarity: s });
43
+ }
44
+ scored.sort((a, b) => b.similarity - a.similarity);
45
+ return scored.slice(0, topK);
46
+ }
47
+
48
+ /**
49
+ * Aggregate matches into "N past requests similar to this one used tool X
50
+ * in M of N cases". Returns an injectable hint string or empty.
51
+ *
52
+ * @param {Array<{record,similarity}>} matches
53
+ * @returns {string}
54
+ */
55
+ export function formatRetrievalHint(matches) {
56
+ if (!matches?.length) return "";
57
+
58
+ // Count first_tool occurrences
59
+ const toolCounts = new Map();
60
+ for (const m of matches) {
61
+ const t = m.record.first_tool;
62
+ if (!t) continue;
63
+ toolCounts.set(t, (toolCounts.get(t) || 0) + 1);
64
+ }
65
+ if (toolCounts.size === 0) return "";
66
+
67
+ const sorted = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);
68
+ const top = sorted[0];
69
+ const total = matches.length;
70
+ const [topTool, topCount] = top;
71
+
72
+ // Only emit a hint if the dominant tool is majority (>= 50%) and we
73
+ // have at least 2 supporting cases. Otherwise, ambiguous history.
74
+ if (topCount < 2 || topCount / total < 0.5) return "";
75
+
76
+ const topSimilarity = Math.round(matches[0].similarity * 100);
77
+ const parts = [`Similar past requests (top match ${topSimilarity}% token overlap): ${topCount}/${total} used ${topTool}.`];
78
+ if (sorted.length > 1) {
79
+ const others = sorted.slice(1).map(([t, c]) => `${t}(${c})`).join(", ");
80
+ parts.push(`Others: ${others}.`);
81
+ }
82
+ parts.push(`Prefer ${topTool} unless the current request differs materially.`);
83
+ return parts.join(" ");
84
+ }