chainlesschain 0.37.12 → 0.40.1

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 (48) hide show
  1. package/package.json +3 -2
  2. package/src/commands/agent.js +7 -1
  3. package/src/commands/ask.js +24 -9
  4. package/src/commands/chat.js +7 -1
  5. package/src/commands/cli-anything.js +266 -0
  6. package/src/commands/compliance.js +216 -0
  7. package/src/commands/dao.js +312 -0
  8. package/src/commands/dlp.js +278 -0
  9. package/src/commands/evomap.js +558 -0
  10. package/src/commands/hardening.js +230 -0
  11. package/src/commands/matrix.js +168 -0
  12. package/src/commands/nostr.js +185 -0
  13. package/src/commands/pqc.js +162 -0
  14. package/src/commands/scim.js +218 -0
  15. package/src/commands/serve.js +109 -0
  16. package/src/commands/siem.js +156 -0
  17. package/src/commands/social.js +480 -0
  18. package/src/commands/terraform.js +148 -0
  19. package/src/constants.js +1 -0
  20. package/src/index.js +60 -0
  21. package/src/lib/autonomous-agent.js +487 -0
  22. package/src/lib/cli-anything-bridge.js +379 -0
  23. package/src/lib/cli-context-engineering.js +472 -0
  24. package/src/lib/compliance-manager.js +290 -0
  25. package/src/lib/content-recommender.js +205 -0
  26. package/src/lib/dao-governance.js +296 -0
  27. package/src/lib/dlp-engine.js +304 -0
  28. package/src/lib/evomap-client.js +135 -0
  29. package/src/lib/evomap-federation.js +240 -0
  30. package/src/lib/evomap-governance.js +250 -0
  31. package/src/lib/evomap-manager.js +227 -0
  32. package/src/lib/git-integration.js +1 -1
  33. package/src/lib/hardening-manager.js +275 -0
  34. package/src/lib/llm-providers.js +14 -1
  35. package/src/lib/matrix-bridge.js +196 -0
  36. package/src/lib/nostr-bridge.js +195 -0
  37. package/src/lib/permanent-memory.js +370 -0
  38. package/src/lib/plan-mode.js +211 -0
  39. package/src/lib/pqc-manager.js +196 -0
  40. package/src/lib/scim-manager.js +212 -0
  41. package/src/lib/session-manager.js +38 -0
  42. package/src/lib/siem-exporter.js +137 -0
  43. package/src/lib/social-manager.js +283 -0
  44. package/src/lib/task-model-selector.js +232 -0
  45. package/src/lib/terraform-manager.js +201 -0
  46. package/src/lib/ws-server.js +474 -0
  47. package/src/repl/agent-repl.js +796 -41
  48. package/src/repl/chat-repl.js +14 -6
@@ -0,0 +1,472 @@
1
+ /**
2
+ * CLI Context Engineering — lightweight adapter for agent-repl.
3
+ *
4
+ * Integrates 5 context injectors (Instinct / Memory / BM25 Notes / Task Reminder / Permanent Memory)
5
+ * with KV-Cache-friendly system prompt cleaning, importance-based compaction,
6
+ * resumable compaction summaries, and stable prefix caching.
7
+ *
8
+ * Graceful degradation: works without DB (static prompt fallback).
9
+ */
10
+
11
+ import { generateInstinctPrompt } from "./instinct-manager.js";
12
+ import { recallMemory } from "./hierarchical-memory.js";
13
+ import { BM25Search } from "./bm25-search.js";
14
+ import { createHash } from "crypto";
15
+
16
+ // Exported for test injection
17
+ export const _deps = {
18
+ generateInstinctPrompt,
19
+ recallMemory,
20
+ BM25Search,
21
+ createHash,
22
+ };
23
+
24
+ // ─── System prompt cleaning regexes (match desktop KV-Cache optimization) ───
25
+ const CLEAN_PATTERNS = [
26
+ {
27
+ pattern: /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[.\dZ+-]*/g,
28
+ replacement: "[DATE]",
29
+ },
30
+ {
31
+ pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
32
+ replacement: "[UUID]",
33
+ },
34
+ { pattern: /session[-_]?[0-9a-f]{6,}/gi, replacement: "[SESSION]" },
35
+ { pattern: /\b\d{10,13}\b/g, replacement: "[TIMESTAMP]" },
36
+ ];
37
+
38
+ export class CLIContextEngineering {
39
+ /**
40
+ * @param {object} options
41
+ * @param {object|null} options.db - Database instance (null for graceful degradation)
42
+ * @param {object|null} options.permanentMemory - CLIPermanentMemory instance (optional)
43
+ */
44
+ constructor({ db, permanentMemory } = {}) {
45
+ this.db = db || null;
46
+ this.permanentMemory = permanentMemory || null;
47
+ this.errorHistory = [];
48
+ this.taskContext = null;
49
+ this._bm25 = null;
50
+ this._notesIndexed = false;
51
+ // Resumable compaction: summaries of discarded message pairs
52
+ this._compactionSummaries = [];
53
+ // Stable prefix cache: { hash, cleanedPrefix }
54
+ this._prefixCache = null;
55
+ }
56
+
57
+ /**
58
+ * Build optimized messages for LLM consumption.
59
+ * Returns a new array (does not modify input).
60
+ *
61
+ * @param {Array} rawMessages - Original messages array
62
+ * @param {object} options
63
+ * @param {string} [options.userQuery] - Latest user query for relevance matching
64
+ * @returns {Array} Optimized messages
65
+ */
66
+ buildOptimizedMessages(rawMessages, { userQuery } = {}) {
67
+ const result = [];
68
+
69
+ // 1. System prompt — clean dynamic content for KV-Cache stability
70
+ let historyStart = 0;
71
+ if (rawMessages.length > 0 && rawMessages[0].role === "system") {
72
+ result.push({
73
+ role: "system",
74
+ content: this._cleanSystemPrompt(rawMessages[0].content),
75
+ });
76
+ historyStart = 1;
77
+ }
78
+
79
+ // 2. Instinct injection
80
+ if (this.db) {
81
+ try {
82
+ const instinctPrompt = _deps.generateInstinctPrompt(this.db);
83
+ if (instinctPrompt) {
84
+ result.push({
85
+ role: "system",
86
+ content: `## Learned Preferences\n${instinctPrompt}`,
87
+ });
88
+ }
89
+ } catch (_err) {
90
+ // Instinct injection failed — skip silently
91
+ }
92
+ }
93
+
94
+ // 3. Memory injection
95
+ if (this.db && userQuery) {
96
+ try {
97
+ const memories = _deps.recallMemory(this.db, userQuery, { limit: 5 });
98
+ if (memories && memories.length > 0) {
99
+ const lines = memories.map(
100
+ (m) =>
101
+ `- [${m.layer}] ${m.content} (retention: ${(m.retention * 100).toFixed(0)}%)`,
102
+ );
103
+ result.push({
104
+ role: "system",
105
+ content: `## Relevant Memories\n${lines.join("\n")}`,
106
+ });
107
+ }
108
+ } catch (_err) {
109
+ // Memory injection failed — skip silently
110
+ }
111
+ }
112
+
113
+ // 4. Notes injection (BM25 search)
114
+ if (this.db && userQuery) {
115
+ try {
116
+ this._ensureNotesIndex();
117
+ if (this._bm25 && this._bm25.totalDocs > 0) {
118
+ const hits = this._bm25.search(userQuery, {
119
+ topK: 3,
120
+ threshold: 0.5,
121
+ });
122
+ if (hits.length > 0) {
123
+ const lines = hits.map(
124
+ (h) =>
125
+ `- **${h.doc.title || "Untitled"}**: ${(h.doc.content || "").substring(0, 200)}`,
126
+ );
127
+ result.push({
128
+ role: "system",
129
+ content: `## Relevant Notes\n${lines.join("\n")}`,
130
+ });
131
+ }
132
+ }
133
+ } catch (_err) {
134
+ // Notes injection failed — skip silently
135
+ }
136
+ }
137
+
138
+ // 5. Permanent memory injection
139
+ if (this.permanentMemory && userQuery) {
140
+ try {
141
+ const pmResults = this.permanentMemory.getRelevantContext(userQuery, 3);
142
+ if (pmResults && pmResults.length > 0) {
143
+ const lines = pmResults.map(
144
+ (r) => `- [${r.source || "memory"}] ${r.content}`,
145
+ );
146
+ result.push({
147
+ role: "system",
148
+ content: `## Permanent Memory\n${lines.join("\n")}`,
149
+ });
150
+ }
151
+ } catch (_err) {
152
+ // Permanent memory injection failed — skip silently
153
+ }
154
+ }
155
+
156
+ // 5b. Compaction summaries (resumable context from discarded messages)
157
+ if (this._compactionSummaries.length > 0) {
158
+ result.push({
159
+ role: "system",
160
+ content: `## Compacted Context Summary\n${this._compactionSummaries.join("\n")}`,
161
+ });
162
+ }
163
+
164
+ // 6. Conversation history — clean metadata
165
+ for (let i = historyStart; i < rawMessages.length; i++) {
166
+ const msg = rawMessages[i];
167
+ const cleaned = { role: msg.role };
168
+ if (msg.content !== undefined) cleaned.content = msg.content;
169
+ if (msg.tool_calls) cleaned.tool_calls = msg.tool_calls;
170
+ if (msg.name) cleaned.name = msg.name;
171
+ if (msg.tool_call_id) cleaned.tool_call_id = msg.tool_call_id;
172
+ result.push(cleaned);
173
+ }
174
+
175
+ // 7. Error context
176
+ if (this.errorHistory.length > 0) {
177
+ const recent = this.errorHistory.slice(-5);
178
+ const lines = recent.map(
179
+ (e) =>
180
+ `- [${e.step}] ${e.message}${e.resolution ? ` → Fixed: ${e.resolution}` : ""}`,
181
+ );
182
+ result.push({
183
+ role: "system",
184
+ content: `## Recent Errors\n${lines.join("\n")}\nAvoid repeating these mistakes.`,
185
+ });
186
+ }
187
+
188
+ // 8. Task reminder
189
+ if (this.taskContext) {
190
+ const tc = this.taskContext;
191
+ const stepLines = tc.steps
192
+ ? tc.steps.map((s, i) => {
193
+ const status =
194
+ i < tc.currentStep
195
+ ? "done"
196
+ : i === tc.currentStep
197
+ ? "current"
198
+ : "pending";
199
+ return ` ${status === "done" ? "✓" : status === "current" ? "→" : "○"} ${s}`;
200
+ })
201
+ : [];
202
+ result.push({
203
+ role: "system",
204
+ content: [
205
+ "## Current Task Status",
206
+ `**Objective**: ${tc.objective}`,
207
+ ...(stepLines.length > 0 ? ["**Progress**:", ...stepLines] : []),
208
+ "Stay focused on this objective.",
209
+ ].join("\n"),
210
+ });
211
+ }
212
+
213
+ return result;
214
+ }
215
+
216
+ /**
217
+ * Record an error for context injection.
218
+ */
219
+ recordError({ step, message, resolution }) {
220
+ this.errorHistory.push({ step, message, resolution, time: Date.now() });
221
+ if (this.errorHistory.length > 10) {
222
+ this.errorHistory.shift();
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Set current task objective and steps.
228
+ */
229
+ setTask(objective, steps = []) {
230
+ this.taskContext = {
231
+ objective,
232
+ steps,
233
+ currentStep: 0,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Update task progress.
239
+ */
240
+ updateTaskProgress(step, _status) {
241
+ if (!this.taskContext) return;
242
+ if (typeof step === "number") {
243
+ this.taskContext.currentStep = step;
244
+ } else {
245
+ // Find step by name
246
+ const idx = this.taskContext.steps.indexOf(step);
247
+ if (idx >= 0) this.taskContext.currentStep = idx;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Clear current task.
253
+ */
254
+ clearTask() {
255
+ this.taskContext = null;
256
+ }
257
+
258
+ /**
259
+ * Importance-based smart compaction.
260
+ * Keeps system prompt + top-scoring message pairs.
261
+ *
262
+ * @param {Array} messages - Full messages array
263
+ * @param {object} options
264
+ * @param {number} [options.keepPairs=6] - Number of user+assistant pairs to keep
265
+ * @returns {Array} Compacted messages
266
+ */
267
+ smartCompact(messages, { keepPairs = 6 } = {}) {
268
+ if (messages.length <= 3) return [...messages];
269
+
270
+ // Always keep messages[0] (system prompt)
271
+ const systemMsg = messages[0];
272
+ const rest = messages.slice(1);
273
+
274
+ // Group into user+assistant pairs (+ tool messages attached to assistant)
275
+ const pairs = [];
276
+ let currentPair = [];
277
+
278
+ for (const msg of rest) {
279
+ if (msg.role === "user" && currentPair.length > 0) {
280
+ pairs.push(currentPair);
281
+ currentPair = [];
282
+ }
283
+ currentPair.push(msg);
284
+ }
285
+ if (currentPair.length > 0) {
286
+ pairs.push(currentPair);
287
+ }
288
+
289
+ if (pairs.length <= keepPairs) return [...messages];
290
+
291
+ // Score each pair
292
+ const scored = pairs.map((pair, idx) => {
293
+ let score = 0;
294
+
295
+ // Recency bonus (higher index = more recent)
296
+ score += (idx / pairs.length) * 5;
297
+
298
+ // Tool calls bonus
299
+ if (pair.some((m) => m.tool_calls || m.role === "tool")) {
300
+ score += 2;
301
+ }
302
+
303
+ // Task relevance bonus
304
+ if (this.taskContext) {
305
+ const objective = this.taskContext.objective.toLowerCase();
306
+ if (
307
+ pair.some(
308
+ (m) => m.content && m.content.toLowerCase().includes(objective),
309
+ )
310
+ ) {
311
+ score += 3;
312
+ }
313
+ }
314
+
315
+ // Error context bonus
316
+ if (pair.some((m) => m.content && m.content.includes("Error"))) {
317
+ score += 1;
318
+ }
319
+
320
+ return { pair, score };
321
+ });
322
+
323
+ // Sort by score descending, keep top pairs
324
+ scored.sort((a, b) => b.score - a.score);
325
+ const kept = scored.slice(0, keepPairs);
326
+ const discarded = scored.slice(keepPairs);
327
+
328
+ // Generate one-line summaries for discarded pairs (resumable compaction)
329
+ for (const { pair } of discarded) {
330
+ const userMsg = pair.find((m) => m.role === "user");
331
+ const assistantMsg = pair.find((m) => m.role === "assistant");
332
+ if (userMsg && userMsg.content) {
333
+ const topic = userMsg.content.substring(0, 80).replace(/\n/g, " ");
334
+ const hadTools = pair.some((m) => m.tool_calls || m.role === "tool");
335
+ const summary = hadTools
336
+ ? `- Q: "${topic}" → [used tools] ${(assistantMsg?.content || "").substring(0, 60).replace(/\n/g, " ")}`
337
+ : `- Q: "${topic}" → ${(assistantMsg?.content || "").substring(0, 80).replace(/\n/g, " ")}`;
338
+ this._compactionSummaries.push(summary);
339
+ }
340
+ }
341
+ // Cap summaries at 20
342
+ if (this._compactionSummaries.length > 20) {
343
+ this._compactionSummaries = this._compactionSummaries.slice(-20);
344
+ }
345
+
346
+ // Restore chronological order
347
+ kept.sort((a, b) => pairs.indexOf(a.pair) - pairs.indexOf(b.pair));
348
+
349
+ const result = [systemMsg];
350
+ for (const { pair } of kept) {
351
+ result.push(...pair);
352
+ }
353
+
354
+ return result;
355
+ }
356
+
357
+ /**
358
+ * Get engine statistics.
359
+ */
360
+ getStats() {
361
+ return {
362
+ hasDb: !!this.db,
363
+ hasPermanentMemory: !!this.permanentMemory,
364
+ errorCount: this.errorHistory.length,
365
+ hasTask: !!this.taskContext,
366
+ notesIndexed: this._bm25 ? this._bm25.totalDocs : 0,
367
+ compactionSummaries: this._compactionSummaries.length,
368
+ prefixCached: !!this._prefixCache,
369
+ };
370
+ }
371
+
372
+ /**
373
+ * Clear compaction summaries.
374
+ */
375
+ clearCompactionSummaries() {
376
+ this._compactionSummaries = [];
377
+ }
378
+
379
+ /**
380
+ * Force reindex notes from DB.
381
+ */
382
+ reindexNotes() {
383
+ this._notesIndexed = false;
384
+ this._bm25 = null;
385
+ this._ensureNotesIndex();
386
+ }
387
+
388
+ // ─── Internal ───────────────────────────────────────────────────
389
+
390
+ _cleanSystemPrompt(content) {
391
+ // Use stable prefix cache if available
392
+ const prefix = this._computeStablePrefix(content);
393
+ if (prefix) {
394
+ // Only clean the dynamic suffix
395
+ const suffix = content.slice(prefix.originalLength);
396
+ let cleanedSuffix = suffix;
397
+ for (const { pattern, replacement } of CLEAN_PATTERNS) {
398
+ cleanedSuffix = cleanedSuffix.replace(pattern, replacement);
399
+ }
400
+ return prefix.cleaned + cleanedSuffix;
401
+ }
402
+
403
+ let cleaned = content;
404
+ for (const { pattern, replacement } of CLEAN_PATTERNS) {
405
+ cleaned = cleaned.replace(pattern, replacement);
406
+ }
407
+ return cleaned;
408
+ }
409
+
410
+ /**
411
+ * Compute stable prefix — the portion of system prompt that doesn't change.
412
+ * Caches the cleaned prefix so subsequent calls only re-clean the dynamic tail.
413
+ */
414
+ _computeStablePrefix(content) {
415
+ if (!content || content.length < 100) return null;
416
+
417
+ // Find the stable portion (before first dynamic pattern match)
418
+ let firstMatchIdx = content.length;
419
+ for (const { pattern } of CLEAN_PATTERNS) {
420
+ // Use non-global copy to get .index
421
+ const nonGlobal = new RegExp(
422
+ pattern.source,
423
+ pattern.flags.replace("g", ""),
424
+ );
425
+ const match = nonGlobal.exec(content);
426
+ if (match && match.index < firstMatchIdx) {
427
+ firstMatchIdx = match.index;
428
+ }
429
+ }
430
+
431
+ // If no dynamic content found, or prefix too short, skip caching
432
+ if (firstMatchIdx === content.length || firstMatchIdx < 50) return null;
433
+
434
+ const rawPrefix = content.slice(0, firstMatchIdx);
435
+ const hash = _deps
436
+ .createHash("sha256")
437
+ .update(rawPrefix)
438
+ .digest("hex")
439
+ .slice(0, 16);
440
+
441
+ if (this._prefixCache && this._prefixCache.hash === hash) {
442
+ return this._prefixCache;
443
+ }
444
+
445
+ // Clean and cache the prefix (should be stable, no dynamic patterns)
446
+ let cleaned = rawPrefix;
447
+ for (const { pattern, replacement } of CLEAN_PATTERNS) {
448
+ cleaned = cleaned.replace(pattern, replacement);
449
+ }
450
+
451
+ this._prefixCache = { hash, cleaned, originalLength: firstMatchIdx };
452
+ return this._prefixCache;
453
+ }
454
+
455
+ _ensureNotesIndex() {
456
+ if (this._notesIndexed || !this.db) return;
457
+ this._notesIndexed = true;
458
+
459
+ try {
460
+ const notes = this.db
461
+ .prepare("SELECT id, title, content FROM notes LIMIT 500")
462
+ .all();
463
+
464
+ if (notes && notes.length > 0) {
465
+ this._bm25 = new _deps.BM25Search();
466
+ this._bm25.indexDocuments(notes);
467
+ }
468
+ } catch (_err) {
469
+ // Notes table may not exist — skip
470
+ }
471
+ }
472
+ }