chainlesschain 0.37.9 → 0.37.11

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 (84) hide show
  1. package/README.md +309 -19
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +1 -1
  4. package/src/commands/a2a.js +374 -0
  5. package/src/commands/audit.js +286 -0
  6. package/src/commands/auth.js +387 -0
  7. package/src/commands/bi.js +240 -0
  8. package/src/commands/browse.js +184 -0
  9. package/src/commands/cowork.js +317 -0
  10. package/src/commands/did.js +376 -0
  11. package/src/commands/economy.js +375 -0
  12. package/src/commands/encrypt.js +233 -0
  13. package/src/commands/evolution.js +398 -0
  14. package/src/commands/export.js +125 -0
  15. package/src/commands/git.js +215 -0
  16. package/src/commands/hmemory.js +273 -0
  17. package/src/commands/hook.js +260 -0
  18. package/src/commands/import.js +259 -0
  19. package/src/commands/init.js +184 -0
  20. package/src/commands/instinct.js +202 -0
  21. package/src/commands/llm.js +155 -4
  22. package/src/commands/lowcode.js +320 -0
  23. package/src/commands/mcp.js +302 -0
  24. package/src/commands/memory.js +282 -0
  25. package/src/commands/note.js +187 -0
  26. package/src/commands/org.js +505 -0
  27. package/src/commands/p2p.js +274 -0
  28. package/src/commands/plugin.js +451 -0
  29. package/src/commands/sandbox.js +366 -0
  30. package/src/commands/search.js +237 -0
  31. package/src/commands/session.js +238 -0
  32. package/src/commands/skill.js +254 -201
  33. package/src/commands/sync.js +249 -0
  34. package/src/commands/tokens.js +214 -0
  35. package/src/commands/wallet.js +416 -0
  36. package/src/commands/workflow.js +359 -0
  37. package/src/commands/zkp.js +277 -0
  38. package/src/index.js +93 -1
  39. package/src/lib/a2a-protocol.js +371 -0
  40. package/src/lib/agent-coordinator.js +273 -0
  41. package/src/lib/agent-economy.js +369 -0
  42. package/src/lib/app-builder.js +377 -0
  43. package/src/lib/audit-logger.js +364 -0
  44. package/src/lib/bi-engine.js +299 -0
  45. package/src/lib/bm25-search.js +322 -0
  46. package/src/lib/browser-automation.js +216 -0
  47. package/src/lib/cowork/ab-comparator-cli.js +180 -0
  48. package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
  49. package/src/lib/cowork/debate-review-cli.js +144 -0
  50. package/src/lib/cowork/decision-kb-cli.js +153 -0
  51. package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
  52. package/src/lib/cowork-adapter.js +106 -0
  53. package/src/lib/crypto-manager.js +246 -0
  54. package/src/lib/did-manager.js +270 -0
  55. package/src/lib/ensure-utf8.js +59 -0
  56. package/src/lib/evolution-system.js +508 -0
  57. package/src/lib/git-integration.js +220 -0
  58. package/src/lib/hierarchical-memory.js +471 -0
  59. package/src/lib/hook-manager.js +387 -0
  60. package/src/lib/instinct-manager.js +190 -0
  61. package/src/lib/knowledge-exporter.js +302 -0
  62. package/src/lib/knowledge-importer.js +293 -0
  63. package/src/lib/llm-providers.js +325 -0
  64. package/src/lib/mcp-client.js +413 -0
  65. package/src/lib/memory-manager.js +211 -0
  66. package/src/lib/note-versioning.js +244 -0
  67. package/src/lib/org-manager.js +424 -0
  68. package/src/lib/p2p-manager.js +317 -0
  69. package/src/lib/pdf-parser.js +96 -0
  70. package/src/lib/permission-engine.js +374 -0
  71. package/src/lib/plan-mode.js +333 -0
  72. package/src/lib/plugin-manager.js +430 -0
  73. package/src/lib/project-detector.js +53 -0
  74. package/src/lib/response-cache.js +156 -0
  75. package/src/lib/sandbox-v2.js +503 -0
  76. package/src/lib/service-container.js +183 -0
  77. package/src/lib/session-manager.js +189 -0
  78. package/src/lib/skill-loader.js +274 -0
  79. package/src/lib/sync-manager.js +347 -0
  80. package/src/lib/token-tracker.js +200 -0
  81. package/src/lib/wallet-manager.js +348 -0
  82. package/src/lib/workflow-engine.js +503 -0
  83. package/src/lib/zkp-engine.js +241 -0
  84. package/src/repl/agent-repl.js +259 -124
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Hook Manager — Lifecycle hook registration, execution, and statistics for CLI.
3
+ * Manages hooks that trigger on system events (IPC, tools, sessions, git, etc.).
4
+ */
5
+
6
+ import crypto from "crypto";
7
+
8
+ /**
9
+ * Hook priority levels — lower values run first.
10
+ */
11
+ export const HookPriority = {
12
+ SYSTEM: 0,
13
+ HIGH: 100,
14
+ NORMAL: 500,
15
+ LOW: 900,
16
+ MONITOR: 1000,
17
+ };
18
+
19
+ /**
20
+ * Hook execution types.
21
+ */
22
+ export const HookType = {
23
+ SYNC: "sync",
24
+ ASYNC: "async",
25
+ COMMAND: "command",
26
+ SCRIPT: "script",
27
+ };
28
+
29
+ /**
30
+ * All supported hook event names.
31
+ */
32
+ export const HookEvents = {
33
+ PreIPCCall: "PreIPCCall",
34
+ PostIPCCall: "PostIPCCall",
35
+ IPCError: "IPCError",
36
+ PreToolUse: "PreToolUse",
37
+ PostToolUse: "PostToolUse",
38
+ ToolError: "ToolError",
39
+ SessionStart: "SessionStart",
40
+ SessionEnd: "SessionEnd",
41
+ PreCompact: "PreCompact",
42
+ PostCompact: "PostCompact",
43
+ UserPromptSubmit: "UserPromptSubmit",
44
+ AssistantResponse: "AssistantResponse",
45
+ AgentStart: "AgentStart",
46
+ AgentStop: "AgentStop",
47
+ TaskAssigned: "TaskAssigned",
48
+ TaskCompleted: "TaskCompleted",
49
+ PreFileAccess: "PreFileAccess",
50
+ PostFileAccess: "PostFileAccess",
51
+ FileModified: "FileModified",
52
+ MemorySave: "MemorySave",
53
+ MemoryLoad: "MemoryLoad",
54
+ AuditLog: "AuditLog",
55
+ ComplianceCheck: "ComplianceCheck",
56
+ DataSubjectRequest: "DataSubjectRequest",
57
+ PreGitCommit: "PreGitCommit",
58
+ PostGitCommit: "PostGitCommit",
59
+ PreGitPush: "PreGitPush",
60
+ CIFailure: "CIFailure",
61
+ };
62
+
63
+ /**
64
+ * Ensure hooks table exists in the database.
65
+ */
66
+ export function ensureHookTables(db) {
67
+ db.exec(`
68
+ CREATE TABLE IF NOT EXISTS hooks (
69
+ id TEXT PRIMARY KEY,
70
+ event TEXT NOT NULL,
71
+ name TEXT NOT NULL,
72
+ type TEXT NOT NULL DEFAULT 'sync',
73
+ priority INTEGER NOT NULL DEFAULT 500,
74
+ handler TEXT,
75
+ matcher TEXT,
76
+ timeout INTEGER DEFAULT 5000,
77
+ enabled INTEGER DEFAULT 1,
78
+ description TEXT,
79
+ execution_count INTEGER DEFAULT 0,
80
+ error_count INTEGER DEFAULT 0,
81
+ total_execution_time REAL DEFAULT 0,
82
+ created_at TEXT DEFAULT (datetime('now')),
83
+ updated_at TEXT DEFAULT (datetime('now'))
84
+ )
85
+ `);
86
+ }
87
+
88
+ /**
89
+ * Register a new hook.
90
+ */
91
+ export function registerHook(db, hookConfig) {
92
+ ensureHookTables(db);
93
+
94
+ const {
95
+ event,
96
+ name,
97
+ type = HookType.SYNC,
98
+ priority = HookPriority.NORMAL,
99
+ handler,
100
+ matcher,
101
+ timeout = 5000,
102
+ enabled = true,
103
+ description,
104
+ } = hookConfig;
105
+
106
+ if (!event) {
107
+ throw new Error("Hook event is required");
108
+ }
109
+ if (!name) {
110
+ throw new Error("Hook name is required");
111
+ }
112
+
113
+ // Validate event name
114
+ if (!Object.values(HookEvents).includes(event)) {
115
+ throw new Error(
116
+ `Invalid hook event: ${event}. Use one of: ${Object.values(HookEvents).join(", ")}`,
117
+ );
118
+ }
119
+
120
+ // Validate type
121
+ if (!Object.values(HookType).includes(type)) {
122
+ throw new Error(
123
+ `Invalid hook type: ${type}. Use one of: ${Object.values(HookType).join(", ")}`,
124
+ );
125
+ }
126
+
127
+ const id = `hook-${crypto.randomBytes(8).toString("hex")}`;
128
+
129
+ db.prepare(
130
+ `INSERT INTO hooks (id, event, name, type, priority, handler, matcher, timeout, enabled, description)
131
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
132
+ ).run(
133
+ id,
134
+ event,
135
+ name,
136
+ type,
137
+ priority,
138
+ handler || null,
139
+ matcher || null,
140
+ timeout,
141
+ enabled ? 1 : 0,
142
+ description || null,
143
+ );
144
+
145
+ return {
146
+ id,
147
+ event,
148
+ name,
149
+ type,
150
+ priority,
151
+ handler,
152
+ matcher,
153
+ timeout,
154
+ enabled: !!enabled,
155
+ description,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Unregister (remove) a hook by ID.
161
+ */
162
+ export function unregisterHook(db, hookId) {
163
+ ensureHookTables(db);
164
+ const result = db.prepare("DELETE FROM hooks WHERE id = ?").run(hookId);
165
+ return result.changes > 0;
166
+ }
167
+
168
+ /**
169
+ * List hooks with optional filters.
170
+ */
171
+ export function listHooks(db, options = {}) {
172
+ ensureHookTables(db);
173
+ const { event, enabledOnly = false } = options;
174
+
175
+ if (event && enabledOnly) {
176
+ return db
177
+ .prepare(
178
+ "SELECT * FROM hooks WHERE event = ? AND enabled = 1 ORDER BY priority ASC",
179
+ )
180
+ .all(event);
181
+ }
182
+ if (event) {
183
+ return db
184
+ .prepare("SELECT * FROM hooks WHERE event = ? ORDER BY priority ASC")
185
+ .all(event);
186
+ }
187
+ if (enabledOnly) {
188
+ return db
189
+ .prepare("SELECT * FROM hooks WHERE enabled = 1 ORDER BY priority ASC")
190
+ .all();
191
+ }
192
+ return db.prepare("SELECT * FROM hooks ORDER BY priority ASC").all();
193
+ }
194
+
195
+ /**
196
+ * Get a single hook by ID.
197
+ */
198
+ export function getHook(db, hookId) {
199
+ ensureHookTables(db);
200
+ return db.prepare("SELECT * FROM hooks WHERE id = ?").get(hookId);
201
+ }
202
+
203
+ /**
204
+ * Compile a matcher pattern into a test function.
205
+ * Supports:
206
+ * - null/undefined → matches everything
207
+ * - Pipe-separated patterns: "Edit|Write" matches "Edit" or "Write"
208
+ * - Wildcards: "*" matches any chars, "?" matches one char
209
+ * - Regex strings starting with "/": "/^Pre/" matches "PreIPCCall"
210
+ */
211
+ export function compileMatcher(pattern) {
212
+ if (!pattern) {
213
+ return () => true;
214
+ }
215
+
216
+ // Regex pattern (starts and ends with /)
217
+ if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
218
+ const lastSlash = pattern.lastIndexOf("/");
219
+ const regexBody = pattern.slice(1, lastSlash);
220
+ const flags = pattern.slice(lastSlash + 1);
221
+ try {
222
+ const re = new RegExp(regexBody, flags);
223
+ return (value) => re.test(value);
224
+ } catch (_err) {
225
+ // Invalid regex — fall through to wildcard matching
226
+ }
227
+ }
228
+
229
+ // Pipe-separated patterns (e.g. "Edit|Write")
230
+ if (pattern.includes("|")) {
231
+ const parts = pattern.split("|").map((p) => p.trim());
232
+ const matchers = parts.map((p) => compileMatcher(p));
233
+ return (value) => matchers.some((m) => m(value));
234
+ }
235
+
236
+ // Wildcard pattern (* and ?)
237
+ const escaped = pattern
238
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
239
+ .replace(/\*/g, ".*")
240
+ .replace(/\?/g, ".");
241
+ const re = new RegExp(`^${escaped}$`);
242
+ return (value) => re.test(value);
243
+ }
244
+
245
+ /**
246
+ * Execute a single hook with context.
247
+ * Returns { success, result, error, executionTime }.
248
+ */
249
+ export async function executeHook(hook, context = {}) {
250
+ const start = Date.now();
251
+
252
+ try {
253
+ const type = hook.type || HookType.SYNC;
254
+
255
+ if (type === HookType.COMMAND || type === HookType.SCRIPT) {
256
+ // Command/script hooks execute a shell command
257
+ const { execSync } = await import("child_process");
258
+ const cmd = hook.handler || "";
259
+ if (!cmd) {
260
+ return {
261
+ success: false,
262
+ result: null,
263
+ error: "No handler command specified",
264
+ executionTime: 0,
265
+ };
266
+ }
267
+ const env = {
268
+ ...process.env,
269
+ HOOK_EVENT: hook.event,
270
+ HOOK_CONTEXT: JSON.stringify(context),
271
+ };
272
+ const output = execSync(cmd, {
273
+ encoding: "utf-8",
274
+ timeout: hook.timeout || 5000,
275
+ env,
276
+ });
277
+ const executionTime = Date.now() - start;
278
+ return {
279
+ success: true,
280
+ result: output.trim(),
281
+ error: null,
282
+ executionTime,
283
+ };
284
+ }
285
+
286
+ // For sync/async hooks with a handler function string
287
+ if (hook.handlerFn && typeof hook.handlerFn === "function") {
288
+ const result = await Promise.resolve(hook.handlerFn(context));
289
+ const executionTime = Date.now() - start;
290
+ return { success: true, result, error: null, executionTime };
291
+ }
292
+
293
+ // No executable handler
294
+ const executionTime = Date.now() - start;
295
+ return { success: true, result: null, error: null, executionTime };
296
+ } catch (err) {
297
+ const executionTime = Date.now() - start;
298
+ return { success: false, result: null, error: err.message, executionTime };
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Execute all hooks for a given event, in priority order.
304
+ * Returns array of { hookId, hookName, success, result, error, executionTime }.
305
+ */
306
+ export async function executeHooks(db, eventName, context = {}) {
307
+ const hooks = listHooks(db, { event: eventName, enabledOnly: true });
308
+ const results = [];
309
+
310
+ for (const hook of hooks) {
311
+ // Check matcher against context
312
+ if (hook.matcher) {
313
+ const matchFn = compileMatcher(hook.matcher);
314
+ const target =
315
+ context.target ||
316
+ context.channel ||
317
+ context.tool ||
318
+ context.file ||
319
+ eventName;
320
+ if (!matchFn(target)) {
321
+ continue;
322
+ }
323
+ }
324
+
325
+ const outcome = await executeHook(hook, context);
326
+ results.push({
327
+ hookId: hook.id,
328
+ hookName: hook.name,
329
+ ...outcome,
330
+ });
331
+
332
+ // Update stats
333
+ updateHookStats(db, hook.id, {
334
+ executionTime: outcome.executionTime,
335
+ success: outcome.success,
336
+ });
337
+ }
338
+
339
+ return results;
340
+ }
341
+
342
+ /**
343
+ * Get hook execution statistics.
344
+ */
345
+ export function getHookStats(db) {
346
+ ensureHookTables(db);
347
+ const hooks = db
348
+ .prepare(
349
+ "SELECT id, event, name, execution_count, error_count, total_execution_time FROM hooks ORDER BY execution_count DESC",
350
+ )
351
+ .all();
352
+
353
+ return hooks.map((h) => ({
354
+ id: h.id,
355
+ event: h.event,
356
+ name: h.name,
357
+ executionCount: h.execution_count || 0,
358
+ errorCount: h.error_count || 0,
359
+ avgExecutionTime:
360
+ h.execution_count > 0
361
+ ? Math.round((h.total_execution_time / h.execution_count) * 100) / 100
362
+ : 0,
363
+ totalExecutionTime: h.total_execution_time || 0,
364
+ }));
365
+ }
366
+
367
+ /**
368
+ * Update hook statistics after execution.
369
+ */
370
+ export function updateHookStats(
371
+ db,
372
+ hookId,
373
+ { executionTime = 0, success = true } = {},
374
+ ) {
375
+ ensureHookTables(db);
376
+
377
+ const hook = getHook(db, hookId);
378
+ if (!hook) return;
379
+
380
+ const newCount = (hook.execution_count || 0) + 1;
381
+ const newErrorCount = (hook.error_count || 0) + (success ? 0 : 1);
382
+ const newTotalTime = (hook.total_execution_time || 0) + executionTime;
383
+
384
+ db.prepare(
385
+ "UPDATE hooks SET execution_count = ?, error_count = ?, total_execution_time = ?, updated_at = datetime('now') WHERE id = ?",
386
+ ).run(newCount, newErrorCount, newTotalTime, hookId);
387
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Instinct Manager — learns user preferences from agent interactions.
3
+ * Tracks patterns like preferred tools, coding style, response format, etc.
4
+ */
5
+
6
+ /**
7
+ * Ensure instincts table exists.
8
+ */
9
+ export function ensureInstinctsTable(db) {
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS instincts (
12
+ id TEXT PRIMARY KEY,
13
+ category TEXT NOT NULL,
14
+ pattern TEXT NOT NULL,
15
+ confidence REAL DEFAULT 0.5,
16
+ occurrences INTEGER DEFAULT 1,
17
+ last_seen TEXT DEFAULT (datetime('now')),
18
+ created_at TEXT DEFAULT (datetime('now'))
19
+ )
20
+ `);
21
+ }
22
+
23
+ function generateId() {
24
+ const hex = () =>
25
+ Math.floor(Math.random() * 0x10000)
26
+ .toString(16)
27
+ .padStart(4, "0");
28
+ return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`;
29
+ }
30
+
31
+ /**
32
+ * Instinct categories.
33
+ */
34
+ export const INSTINCT_CATEGORIES = {
35
+ TOOL_PREFERENCE: "tool_preference",
36
+ CODING_STYLE: "coding_style",
37
+ RESPONSE_FORMAT: "response_format",
38
+ LANGUAGE: "language",
39
+ WORKFLOW: "workflow",
40
+ BEHAVIOR: "behavior",
41
+ };
42
+
43
+ /**
44
+ * Record an instinct observation.
45
+ * If an instinct with the same category+pattern exists, increment its confidence and occurrences.
46
+ */
47
+ export function recordInstinct(db, category, pattern) {
48
+ ensureInstinctsTable(db);
49
+
50
+ // Check if exists
51
+ const existing = db
52
+ .prepare("SELECT * FROM instincts WHERE category = ? AND pattern = ?")
53
+ .get(category, pattern);
54
+
55
+ if (existing) {
56
+ // Boost confidence (asymptotic approach to 1.0)
57
+ const newConfidence = Math.min(
58
+ 0.99,
59
+ existing.confidence + (1 - existing.confidence) * 0.1,
60
+ );
61
+ db.prepare(
62
+ "UPDATE instincts SET confidence = ?, occurrences = occurrences + 1, last_seen = datetime('now') WHERE id = ?",
63
+ ).run(newConfidence, existing.id);
64
+
65
+ return {
66
+ id: existing.id,
67
+ category,
68
+ pattern,
69
+ confidence: newConfidence,
70
+ occurrences: (existing.occurrences || 1) + 1,
71
+ isNew: false,
72
+ };
73
+ }
74
+
75
+ // Create new instinct
76
+ const id = generateId();
77
+ db.prepare(
78
+ "INSERT INTO instincts (id, category, pattern, confidence, occurrences) VALUES (?, ?, ?, ?, ?)",
79
+ ).run(id, category, pattern, 0.5, 1);
80
+
81
+ return {
82
+ id,
83
+ category,
84
+ pattern,
85
+ confidence: 0.5,
86
+ occurrences: 1,
87
+ isNew: true,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Get all instincts, optionally filtered by category.
93
+ */
94
+ export function getInstincts(db, options = {}) {
95
+ ensureInstinctsTable(db);
96
+
97
+ let sql = "SELECT * FROM instincts";
98
+ const params = [];
99
+
100
+ if (options.category) {
101
+ sql += " WHERE category = ?";
102
+ params.push(options.category);
103
+ }
104
+
105
+ sql += " ORDER BY confidence DESC";
106
+
107
+ if (options.limit) {
108
+ sql += " LIMIT ?";
109
+ params.push(options.limit);
110
+ }
111
+
112
+ return db.prepare(sql).all(...params);
113
+ }
114
+
115
+ /**
116
+ * Get top instincts (confidence >= threshold).
117
+ */
118
+ export function getStrongInstincts(db, threshold = 0.7) {
119
+ ensureInstinctsTable(db);
120
+ return db
121
+ .prepare(
122
+ "SELECT * FROM instincts WHERE confidence >= ? ORDER BY confidence DESC",
123
+ )
124
+ .all(threshold);
125
+ }
126
+
127
+ /**
128
+ * Delete an instinct by ID or prefix.
129
+ */
130
+ export function deleteInstinct(db, id) {
131
+ ensureInstinctsTable(db);
132
+ const result = db
133
+ .prepare("DELETE FROM instincts WHERE id LIKE ?")
134
+ .run(`${id}%`);
135
+ return result.changes > 0;
136
+ }
137
+
138
+ /**
139
+ * Reset all instincts (clear the table).
140
+ */
141
+ export function resetInstincts(db) {
142
+ ensureInstinctsTable(db);
143
+ const result = db.prepare("DELETE FROM instincts WHERE 1=1").run();
144
+ return result.changes;
145
+ }
146
+
147
+ /**
148
+ * Decay instincts that haven't been seen recently.
149
+ * Reduces confidence of old instincts over time.
150
+ */
151
+ export function decayInstincts(db, daysThreshold = 30) {
152
+ ensureInstinctsTable(db);
153
+ // Simple decay: multiply confidence by 0.9 for old instincts
154
+ const rows = db.prepare("SELECT * FROM instincts").all();
155
+ let decayed = 0;
156
+
157
+ const cutoff = new Date();
158
+ cutoff.setDate(cutoff.getDate() - daysThreshold);
159
+ const cutoffStr = cutoff.toISOString().replace("T", " ").slice(0, 19);
160
+
161
+ for (const row of rows) {
162
+ if (row.last_seen && row.last_seen < cutoffStr) {
163
+ const newConfidence = Math.max(0.1, (row.confidence || 0.5) * 0.9);
164
+ db.prepare("UPDATE instincts SET confidence = ? WHERE id = ?").run(
165
+ newConfidence,
166
+ row.id,
167
+ );
168
+ decayed++;
169
+ }
170
+ }
171
+
172
+ return decayed;
173
+ }
174
+
175
+ /**
176
+ * Generate a system prompt fragment from strong instincts.
177
+ */
178
+ export function generateInstinctPrompt(db) {
179
+ const strong = getStrongInstincts(db, 0.6);
180
+ if (strong.length === 0) return "";
181
+
182
+ const lines = ["Based on learned preferences:"];
183
+ for (const inst of strong) {
184
+ lines.push(
185
+ `- [${inst.category}] ${inst.pattern} (confidence: ${(inst.confidence * 100).toFixed(0)}%)`,
186
+ );
187
+ }
188
+
189
+ return lines.join("\n");
190
+ }