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,208 @@
1
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, chmodSync } from "node:fs";
2
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3
+ import path from "node:path";
4
+ import { config } from "../config.js";
5
+
6
+ const MEMORY_DIR = path.join(config.projectRoot, "memory");
7
+ const MEMORIES_FILE = path.join(MEMORY_DIR, "memories.jsonl");
8
+ const HMAC_FILE = path.join(MEMORY_DIR, "memories.hmac");
9
+ const KEY_FILE = path.join(MEMORY_DIR, ".hmac-key");
10
+
11
+ const MAX_CONTENT_LENGTH = 10000;
12
+ const MAX_IMPORTANCE = 3;
13
+ const MIN_IMPORTANCE = 1;
14
+
15
+ // ── HMAC Integrity ──────────────────────────────────────────
16
+
17
+ function getHmacKey() {
18
+ // Key from env takes priority (for CI/deployment)
19
+ if (process.env.AGENT_MEMORY_HMAC_KEY) {
20
+ return process.env.AGENT_MEMORY_HMAC_KEY;
21
+ }
22
+ // Otherwise generate and persist a key per project
23
+ ensureDir();
24
+ if (existsSync(KEY_FILE)) {
25
+ return readFileSync(KEY_FILE, "utf-8").trim();
26
+ }
27
+ const key = randomBytes(32).toString("hex");
28
+ writeFileSync(KEY_FILE, key, { encoding: "utf-8", mode: 0o600 });
29
+ return key;
30
+ }
31
+
32
+ function computeHmac(data) {
33
+ return createHmac("sha256", getHmacKey()).update(data, "utf-8").digest("hex");
34
+ }
35
+
36
+ function verifyHmac(data) {
37
+ if (!existsSync(HMAC_FILE)) {
38
+ // No HMAC file: only trust if data file is also empty/missing (true first run)
39
+ // If data exists without HMAC → possible tampering (attacker deleted .hmac)
40
+ if (existsSync(MEMORIES_FILE) && data.trim().length > 0) {
41
+ console.error("[SECURITY] Memory data exists without HMAC file — possible tampering (HMAC deleted)");
42
+ // Auto-heal: create HMAC for existing data (migration from pre-HMAC version)
43
+ // This is a one-time migration — after this, HMAC is required
44
+ saveHmac(data);
45
+ return true;
46
+ }
47
+ return true; // genuinely empty first run
48
+ }
49
+ const stored = readFileSync(HMAC_FILE, "utf-8").trim();
50
+ const computed = computeHmac(data);
51
+ // Timing-safe comparison to prevent timing attacks
52
+ try {
53
+ const storedBuf = Buffer.from(stored, "hex");
54
+ const computedBuf = Buffer.from(computed, "hex");
55
+ if (storedBuf.length !== computedBuf.length || !timingSafeEqual(storedBuf, computedBuf)) {
56
+ console.error("[SECURITY] Memory file integrity check FAILED — possible tampering detected");
57
+ return false;
58
+ }
59
+ } catch {
60
+ console.error("[SECURITY] Memory HMAC comparison error — rejecting data");
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+
66
+ function saveHmac(data) {
67
+ writeFileSync(HMAC_FILE, computeHmac(data), "utf-8");
68
+ }
69
+
70
+ // ── Core ─────────────────────────────────────────────────────
71
+
72
+ // PERF-01: In-process cache — load file once, invalidate on write
73
+ let _cache = null;
74
+
75
+ /** Invalidate in-memory cache (call after external file modification) */
76
+ export function invalidateCache() { _cache = null; }
77
+
78
+ function ensureDir() {
79
+ if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
80
+ }
81
+
82
+ export function loadAll() {
83
+ if (_cache !== null) return _cache;
84
+ ensureDir();
85
+ if (!existsSync(MEMORIES_FILE)) { _cache = []; return _cache; }
86
+ const raw = readFileSync(MEMORIES_FILE, "utf-8");
87
+
88
+ // Verify integrity before parsing
89
+ if (!verifyHmac(raw)) {
90
+ // Tampering detected — return empty to prevent poisoned memory from loading
91
+ _cache = [];
92
+ return _cache;
93
+ }
94
+
95
+ const lines = raw.split("\n").filter(Boolean);
96
+ _cache = lines.map((line) => {
97
+ try { return JSON.parse(line); } catch { return null; }
98
+ }).filter(Boolean);
99
+ return _cache;
100
+ }
101
+
102
+ function atomicWrite(data) {
103
+ ensureDir();
104
+ const tmp = MEMORIES_FILE + ".tmp";
105
+ writeFileSync(tmp, data, "utf-8");
106
+ renameSync(tmp, MEMORIES_FILE);
107
+ // Update HMAC after successful write
108
+ saveHmac(data);
109
+ }
110
+
111
+ function saveAll(memories) {
112
+ const data = memories.map((m) => JSON.stringify(m)).join("\n") + (memories.length ? "\n" : "");
113
+ atomicWrite(data);
114
+ _cache = memories; // update cache after write
115
+ }
116
+
117
+ function nextId(memories) {
118
+ if (!memories.length) return 1;
119
+ let max = 0;
120
+ for (const m of memories) {
121
+ if (m.id > max) max = m.id;
122
+ }
123
+ return max + 1;
124
+ }
125
+
126
+ function clampImportance(val) {
127
+ const n = typeof val === "number" ? val : MIN_IMPORTANCE;
128
+ return Math.max(MIN_IMPORTANCE, Math.min(MAX_IMPORTANCE, Math.round(n)));
129
+ }
130
+
131
+ export function insertMemory({ content, category = "general", importance = 1, sessionId = null }) {
132
+ if (!content || typeof content !== "string") {
133
+ throw new Error("Memory content must be a non-empty string");
134
+ }
135
+ const trimmed = content.slice(0, MAX_CONTENT_LENGTH);
136
+ const memories = loadAll();
137
+ const entry = {
138
+ id: nextId(memories),
139
+ content: trimmed,
140
+ category: String(category || "general"),
141
+ importance: clampImportance(importance),
142
+ session_id: sessionId,
143
+ created_at: new Date().toISOString(),
144
+ };
145
+ memories.push(entry);
146
+ saveAll(memories);
147
+ return entry;
148
+ }
149
+
150
+ export function searchMemories(query, limit = 10) {
151
+ if (!query || typeof query !== "string") return [];
152
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
153
+ if (!terms.length) return [];
154
+
155
+ const memories = loadAll();
156
+ const scored = [];
157
+
158
+ for (const m of memories) {
159
+ const text = `${m.content} ${m.category}`.toLowerCase();
160
+ let score = 0;
161
+ for (const term of terms) {
162
+ if (text.includes(term)) score++;
163
+ }
164
+ if (score === 0) continue;
165
+ score += (m.importance - 1) * 0.5;
166
+ scored.push({ ...m, score });
167
+ }
168
+
169
+ scored.sort((a, b) => b.score - a.score);
170
+ return scored.slice(0, limit);
171
+ }
172
+
173
+ export function getMemory(id) {
174
+ const memories = loadAll();
175
+ return memories.find((m) => m.id === id) || null;
176
+ }
177
+
178
+ export function listRecentMemories(limit = 10) {
179
+ const memories = loadAll();
180
+ return memories.slice(-limit).reverse();
181
+ }
182
+
183
+ export function deleteMemory(id) {
184
+ const memories = loadAll();
185
+ const idx = memories.findIndex((m) => m.id === id);
186
+ if (idx === -1) return false;
187
+ memories.splice(idx, 1);
188
+ saveAll(memories);
189
+ return true;
190
+ }
191
+
192
+ export function getMemoryStats() {
193
+ const memories = loadAll();
194
+ const byCategory = {};
195
+ for (const m of memories) {
196
+ byCategory[m.category] = (byCategory[m.category] || 0) + 1;
197
+ }
198
+ return {
199
+ total: memories.length,
200
+ byCategory,
201
+ oldest: memories[0]?.created_at || null,
202
+ newest: memories[memories.length - 1]?.created_at || null,
203
+ };
204
+ }
205
+
206
+ export function clearAllMemories() {
207
+ saveAll([]);
208
+ }
@@ -0,0 +1,196 @@
1
+ import { insertMemory, searchMemories, getMemory, listRecentMemories, deleteMemory } from "./store.js";
2
+ import { updateMemoryMd } from "./markdown.js";
3
+ import { createSkill, updateSkill, removeSkill, listSkillSummaries } from "./skills.js";
4
+ import { searchFts, getSkill as getSkillById } from "./sqlite-store.js";
5
+
6
+ export const tools = [
7
+ {
8
+ type: "function",
9
+ function: {
10
+ name: "memory_write",
11
+ description: "Save a fact, decision, or observation to persistent memory. Use this when you learn something important that should be remembered across sessions.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ content: { type: "string", description: "The information to remember" },
16
+ category: { type: "string", description: "Category: facts, decisions, preferences, bugs, patterns", default: "general" },
17
+ importance: { type: "integer", description: "1-3, where 3 is critical", default: 1 },
18
+ },
19
+ required: ["content"],
20
+ },
21
+ },
22
+ },
23
+ {
24
+ type: "function",
25
+ function: {
26
+ name: "memory_search",
27
+ description: "Search persistent memory for relevant information. Use this to recall previously saved knowledge.",
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ query: { type: "string", description: "Search query" },
32
+ limit: { type: "integer", description: "Max results", default: 10 },
33
+ },
34
+ required: ["query"],
35
+ },
36
+ },
37
+ },
38
+ {
39
+ type: "function",
40
+ function: {
41
+ name: "memory_get",
42
+ description: "Get a specific memory by ID or list recent memories.",
43
+ parameters: {
44
+ type: "object",
45
+ properties: {
46
+ id: { type: "integer", description: "Memory ID to retrieve" },
47
+ last: { type: "integer", description: "Get last N memories instead" },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ {
53
+ type: "function",
54
+ function: {
55
+ name: "memory_delete",
56
+ description: "Delete a memory by ID.",
57
+ parameters: {
58
+ type: "object",
59
+ properties: {
60
+ id: { type: "integer", description: "Memory ID to delete" },
61
+ },
62
+ required: ["id"],
63
+ },
64
+ },
65
+ },
66
+ {
67
+ type: "function",
68
+ function: {
69
+ name: "skill_add",
70
+ description: "Create a reusable skill — a named procedure the agent remembers permanently. Use when you learn a multi-step workflow the user might need again (e.g. 'how we deploy', 'how to run tests for this project').",
71
+ parameters: {
72
+ type: "object",
73
+ properties: {
74
+ name: { type: "string", description: "Short name for the skill, e.g. 'Deploy to staging'" },
75
+ content: { type: "string", description: "Full procedure in markdown. Include steps, commands, and caveats." },
76
+ tags: { type: "string", description: "Comma-separated tags, e.g. 'deploy, aws, staging'" },
77
+ },
78
+ required: ["name", "content"],
79
+ },
80
+ },
81
+ },
82
+ {
83
+ type: "function",
84
+ function: {
85
+ name: "skill_update",
86
+ description: "Update the content of an existing skill.",
87
+ parameters: {
88
+ type: "object",
89
+ properties: {
90
+ id: { type: "string", description: "Skill ID (from skill_list)" },
91
+ content: { type: "string", description: "New content replacing the old" },
92
+ },
93
+ required: ["id", "content"],
94
+ },
95
+ },
96
+ },
97
+ {
98
+ type: "function",
99
+ function: {
100
+ name: "skill_remove",
101
+ description: "Delete a skill permanently.",
102
+ parameters: {
103
+ type: "object",
104
+ properties: {
105
+ id: { type: "string", description: "Skill ID to delete" },
106
+ },
107
+ required: ["id"],
108
+ },
109
+ },
110
+ },
111
+ {
112
+ type: "function",
113
+ function: {
114
+ name: "memory_expand",
115
+ description: "Retrieve full content of a skill or memory entry by ID. Use when the system prompt shows a skill index (§ heading) and you need the full procedure. Alias: load_skill.",
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ id: { type: "string", description: "Entry ID (skill-xxx or memory ID)" },
120
+ layer: { type: "string", description: "Layer: skills, reflections, patterns, facts, user_model", default: "skills" },
121
+ },
122
+ required: ["id"],
123
+ },
124
+ },
125
+ },
126
+ ];
127
+
128
+ export const handlers = {
129
+ memory_write(args) {
130
+ const entry = insertMemory({
131
+ content: args.content,
132
+ category: args.category ?? "general",
133
+ importance: args.importance ?? 1,
134
+ });
135
+ updateMemoryMd();
136
+ return `Saved memory #${entry.id} [${entry.category}]: ${entry.content}`;
137
+ },
138
+
139
+ memory_search(args) {
140
+ const results = searchMemories(args.query, args.limit ?? 10);
141
+ if (!results.length) return "No memories found matching your query.";
142
+ return results.map((m) => {
143
+ const date = m.created_at?.slice(0, 10) || "";
144
+ return `[#${m.id}] (${m.category}) ${date}: ${m.content} [score: ${m.score.toFixed(1)}]`;
145
+ }).join("\n");
146
+ },
147
+
148
+ memory_get(args) {
149
+ if (args.id != null) {
150
+ const m = getMemory(args.id);
151
+ if (!m) return `Memory #${args.id} not found.`;
152
+ return `[#${m.id}] (${m.category}) ${m.created_at}: ${m.content} [importance: ${m.importance}]`;
153
+ }
154
+ const recent = listRecentMemories(args.last ?? 5);
155
+ if (!recent.length) return "No memories stored yet.";
156
+ return recent.map((m) => `[#${m.id}] (${m.category}) ${m.created_at?.slice(0, 10)}: ${m.content}`).join("\n");
157
+ },
158
+
159
+ memory_delete(args) {
160
+ const ok = deleteMemory(args.id);
161
+ if (ok) {
162
+ updateMemoryMd();
163
+ return `Deleted memory #${args.id}.`;
164
+ }
165
+ return `Memory #${args.id} not found.`;
166
+ },
167
+
168
+ skill_add(args) {
169
+ const tags = args.tags ? args.tags.split(",").map(t => t.trim()).filter(Boolean) : [];
170
+ const id = createSkill(args.name, args.content, tags);
171
+ return `Created skill "${args.name}" (${id}). Stored as markdown in ~/.flint/memory/skills/ and indexed for search.`;
172
+ },
173
+
174
+ skill_update(args) {
175
+ const ok = updateSkill(args.id, args.content);
176
+ return ok ? `Updated skill ${args.id}.` : `Skill ${args.id} not found.`;
177
+ },
178
+
179
+ skill_remove(args) {
180
+ const ok = removeSkill(args.id);
181
+ return ok ? `Removed skill ${args.id} (file deleted).` : `Skill ${args.id} not found.`;
182
+ },
183
+
184
+ memory_expand(args) {
185
+ const layer = args.layer || "skills";
186
+ if (layer === "skills") {
187
+ const skill = getSkillById(args.id);
188
+ if (!skill) return `Skill ${args.id} not found.`;
189
+ return `# ${skill.name}\n\n${skill.content}`;
190
+ }
191
+ // For other layers — FTS search by id
192
+ const results = searchFts(args.id, { limit: 1 });
193
+ if (results.length) return results[0].text;
194
+ return `Entry ${args.id} not found in layer ${layer}.`;
195
+ },
196
+ };
@@ -0,0 +1,86 @@
1
+ // User Model — Layer 5 of memory architecture (dialectic).
2
+ //
3
+ // Builds a profile of the user over time by observing conversations.
4
+ // Traits: communication style, expertise level, preferences, recurring topics.
5
+ // Not explicit "user said X" (that's facts L4). This is "agent OBSERVED X".
6
+ //
7
+ // Inspired by Honcho (Plastic Labs) but implemented in own SQLite, no dependency.
8
+ // Internal task reference removed.
9
+
10
+ import { upsertTrait, getAllTraits, indexEntry } from "./sqlite-store.js";
11
+
12
+ /**
13
+ * Observe a user message and update traits.
14
+ * Called on each user turn. Lightweight language-neutral observations only.
15
+ *
16
+ * - Communication style (terse / verbose) from word count
17
+ * - Language (ru / en / mixed) from unicode block presence
18
+ *
19
+ * Expertise level and role hint used to be guessed from hardcoded RU/EN keyword
20
+ * regex (git rebase|API|CSS|...). Removed 2026-04-19 — biased to English,
21
+ * misses Ukrainian/German/Chinese users, and is the LLM's job anyway. Those
22
+ * traits are inferred by the LLM-based fact extractor at session end
23
+ * (see src/memory/extract-facts.js, category "preference" / "person").
24
+ *
25
+ * @param {string} userMessage
26
+ * @param {object} context — { toolsUsed, responseLanguage, sessionLength }
27
+ */
28
+ export function observeUser(userMessage, context = {}) {
29
+ if (!userMessage || userMessage.length < 5) return;
30
+
31
+ // Communication style — word count is language-neutral
32
+ const words = userMessage.split(/\s+/).length;
33
+ if (words <= 5) {
34
+ updateTrait("communication_style", "terse", `message: ${words} words`, 0.6);
35
+ } else if (words > 50) {
36
+ updateTrait("communication_style", "verbose", `message: ${words} words`, 0.6);
37
+ }
38
+
39
+ // Language — unicode block observation (ru / en / mixed).
40
+ // This is observation of the user's script, not interpretation of intent.
41
+ const hasCyrillic = /\p{Script=Cyrillic}/u.test(userMessage);
42
+ const hasLatin = /\p{Script=Latin}/u.test(userMessage);
43
+ const hasCjk = /\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Hangul}/u.test(userMessage);
44
+ const hasArabic = /\p{Script=Arabic}/u.test(userMessage);
45
+ let lang = null;
46
+ if (hasCjk) lang = hasLatin ? "cjk+latin" : "cjk";
47
+ else if (hasArabic) lang = hasLatin ? "arabic+latin" : "arabic";
48
+ else if (hasCyrillic && !hasLatin) lang = "cyrillic";
49
+ else if (hasLatin && !hasCyrillic) lang = "latin";
50
+ else if (hasCyrillic && hasLatin) lang = "cyrillic+latin";
51
+ if (lang) updateTrait("language", lang, "script observation", 0.7);
52
+ }
53
+
54
+ function updateTrait(trait, value, evidence, confidence) {
55
+ upsertTrait(trait, value, evidence, confidence);
56
+ indexEntry("user_model", `trait-${trait}`, `${trait}: ${value}`);
57
+ }
58
+
59
+ /**
60
+ * Get current user profile as structured object.
61
+ * @returns {Record<string, {value, confidence, evidence}>}
62
+ */
63
+ export function getProfile() {
64
+ const traits = getAllTraits();
65
+ const profile = {};
66
+ for (const t of traits) {
67
+ profile[t.trait] = { value: t.value, confidence: t.confidence, evidence: t.evidence };
68
+ }
69
+ return profile;
70
+ }
71
+
72
+ /**
73
+ * Format user model as system-prompt-ready block.
74
+ * Only includes traits with confidence >= threshold.
75
+ * @param {number} minConfidence
76
+ * @returns {string}
77
+ */
78
+ export function formatForPrompt(minConfidence = 0.5) {
79
+ const traits = getAllTraits().filter(t => t.confidence >= minConfidence);
80
+ if (!traits.length) return "";
81
+ const lines = ["User profile (observed, adapt your behavior):"];
82
+ for (const t of traits) {
83
+ lines.push(` § ${t.trait}: ${t.value} (${Math.round(t.confidence * 100)}%)`);
84
+ }
85
+ return lines.join("\n");
86
+ }