@duckmind/deepquark-darwin-arm64 0.9.83 → 0.9.90

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 (70) hide show
  1. package/.deepquark/skills/bundled/knowledge-graph/SKILL.md +385 -0
  2. package/.deepquark/skills/bundled/knowledge-graph/STANDARDS.md +461 -0
  3. package/.deepquark/skills/bundled/knowledge-graph/lib/cli.ts +588 -0
  4. package/.deepquark/skills/bundled/knowledge-graph/lib/config.ts +630 -0
  5. package/.deepquark/skills/bundled/knowledge-graph/lib/connection-profile.ts +629 -0
  6. package/.deepquark/skills/bundled/knowledge-graph/lib/container.ts +756 -0
  7. package/.deepquark/skills/bundled/knowledge-graph/lib/mcp-client.ts +1310 -0
  8. package/.deepquark/skills/bundled/knowledge-graph/lib/output-formatter.ts +997 -0
  9. package/.deepquark/skills/bundled/knowledge-graph/lib/token-metrics.ts +335 -0
  10. package/.deepquark/skills/bundled/knowledge-graph/lib/transformation-log.ts +137 -0
  11. package/.deepquark/skills/bundled/knowledge-graph/lib/wrapper-config.ts +113 -0
  12. package/.deepquark/skills/bundled/knowledge-graph/server/.env.example +129 -0
  13. package/.deepquark/skills/bundled/knowledge-graph/server/compare-embeddings.ts +175 -0
  14. package/.deepquark/skills/bundled/knowledge-graph/server/config-falkordb.yaml +108 -0
  15. package/.deepquark/skills/bundled/knowledge-graph/server/config-neo4j.yaml +111 -0
  16. package/.deepquark/skills/bundled/knowledge-graph/server/diagnose.ts +483 -0
  17. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb-dev.yml +146 -0
  18. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb.yml +151 -0
  19. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev-local.yml +161 -0
  20. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev.yml +161 -0
  21. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j.yml +169 -0
  22. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-production.yml +128 -0
  23. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-test.yml +10 -0
  24. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose.yml +84 -0
  25. package/.deepquark/skills/bundled/knowledge-graph/server/entrypoint.sh +40 -0
  26. package/.deepquark/skills/bundled/knowledge-graph/server/install.ts +2054 -0
  27. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-falkordb.yml +78 -0
  28. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-neo4j.yml +88 -0
  29. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose.yml +83 -0
  30. package/.deepquark/skills/bundled/knowledge-graph/server/test-all-llms-mcp.ts +387 -0
  31. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-models.ts +201 -0
  32. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-providers.ts +641 -0
  33. package/.deepquark/skills/bundled/knowledge-graph/server/test-graphiti-model.ts +217 -0
  34. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-correct.ts +141 -0
  35. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-llms-mcp.ts +386 -0
  36. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-models.ts +173 -0
  37. package/.deepquark/skills/bundled/knowledge-graph/server/test-llama-extraction.ts +188 -0
  38. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-final.ts +240 -0
  39. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-live.ts +187 -0
  40. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-session.ts +127 -0
  41. package/.deepquark/skills/bundled/knowledge-graph/server/test-model-combinations.ts +316 -0
  42. package/.deepquark/skills/bundled/knowledge-graph/server/test-ollama-models.ts +228 -0
  43. package/.deepquark/skills/bundled/knowledge-graph/server/test-openrouter-models.ts +460 -0
  44. package/.deepquark/skills/bundled/knowledge-graph/server/test-real-life-mcp.ts +311 -0
  45. package/.deepquark/skills/bundled/knowledge-graph/server/test-search-debug.ts +199 -0
  46. package/.deepquark/skills/bundled/knowledge-graph/tools/Install.md +104 -0
  47. package/.deepquark/skills/bundled/knowledge-graph/tools/README.md +120 -0
  48. package/.deepquark/skills/bundled/knowledge-graph/tools/knowledge-cli.ts +996 -0
  49. package/.deepquark/skills/bundled/knowledge-graph/tools/server-cli.ts +531 -0
  50. package/.deepquark/skills/bundled/knowledge-graph/workflows/BulkImport.md +514 -0
  51. package/.deepquark/skills/bundled/knowledge-graph/workflows/CaptureEpisode.md +242 -0
  52. package/.deepquark/skills/bundled/knowledge-graph/workflows/ClearGraph.md +392 -0
  53. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetRecent.md +352 -0
  54. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetStatus.md +373 -0
  55. package/.deepquark/skills/bundled/knowledge-graph/workflows/HealthReport.md +212 -0
  56. package/.deepquark/skills/bundled/knowledge-graph/workflows/InvestigateEntity.md +142 -0
  57. package/.deepquark/skills/bundled/knowledge-graph/workflows/OntologyManagement.md +201 -0
  58. package/.deepquark/skills/bundled/knowledge-graph/workflows/RunMaintenance.md +302 -0
  59. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchByDate.md +255 -0
  60. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchFacts.md +382 -0
  61. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchKnowledge.md +374 -0
  62. package/.deepquark/skills/bundled/knowledge-graph/workflows/StixImport.md +212 -0
  63. package/bin/deepquark +0 -0
  64. package/package.json +1 -1
  65. package/.deepquark/skills/bundled/ge-payroll/SKILL.md +0 -153
  66. package/.deepquark/skills/bundled/ge-payroll/evals/evals.json +0 -23
  67. package/.deepquark/skills/bundled/ge-payroll/references/pain-points-improvements.md +0 -106
  68. package/.deepquark/skills/bundled/ge-payroll/references/process-detail.md +0 -217
  69. package/.deepquark/skills/bundled/ge-payroll/references/raci-stakeholders.md +0 -85
  70. package/.deepquark/skills/bundled/ge-payroll/references/timeline-mandays.md +0 -64
@@ -0,0 +1,997 @@
1
+ /**
2
+ * Output Formatter for token-efficient MCP responses
3
+ * @module output-formatter
4
+ */
5
+
6
+ import { logTransformationFailure, logTransformationSuccess } from './transformation-log';
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ export interface FormatOptions {
13
+ maxLines?: number;
14
+ maxLineLength?: number;
15
+ collectMetrics?: boolean;
16
+ timeoutMs?: number;
17
+ query?: string;
18
+ }
19
+
20
+ export interface FormatResult {
21
+ output: string;
22
+ usedFallback: boolean;
23
+ error?: string;
24
+ metrics?: {
25
+ rawBytes: number;
26
+ compactBytes: number;
27
+ savingsPercent: number;
28
+ processingTimeMs: number;
29
+ };
30
+ }
31
+
32
+ export const DEFAULT_OPTIONS: FormatOptions = {
33
+ maxLines: 20,
34
+ maxLineLength: 120,
35
+ collectMetrics: false,
36
+ timeoutMs: 100,
37
+ };
38
+
39
+ export type OperationFormatter = (data: unknown, options: FormatOptions) => string;
40
+
41
+ // ============================================================================
42
+ // Formatter Registry
43
+ // ============================================================================
44
+
45
+ const formatterRegistry = new Map<string, OperationFormatter>();
46
+
47
+ export function registerFormatter(operation: string, formatter: OperationFormatter): void {
48
+ formatterRegistry.set(operation, formatter);
49
+ }
50
+
51
+ // ============================================================================
52
+ // Utility Functions (T007)
53
+ // ============================================================================
54
+
55
+ /**
56
+ * Convert ISO timestamp to human-readable relative time.
57
+ * Examples: "2h ago", "1d ago", "1mo ago", "1y ago"
58
+ */
59
+ export function relativeTime(isoString: string): string {
60
+ const date = new Date(isoString);
61
+ const now = new Date();
62
+ const diffMs = now.getTime() - date.getTime();
63
+
64
+ const seconds = Math.floor(diffMs / 1000);
65
+ const minutes = Math.floor(seconds / 60);
66
+ const hours = Math.floor(minutes / 60);
67
+ const days = Math.floor(hours / 24);
68
+ const months = Math.floor(days / 30);
69
+ const years = Math.floor(days / 365);
70
+
71
+ if (years > 0) return `${years}y ago`;
72
+ if (months > 0) return `${months}mo ago`;
73
+ if (days > 0) return `${days}d ago`;
74
+ if (hours > 0) return `${hours}h ago`;
75
+ if (minutes > 0) return `${minutes}m ago`;
76
+ return 'just now';
77
+ }
78
+
79
+ /**
80
+ * Truncate UUID to last 8 characters with ellipsis prefix.
81
+ * Example: "550e8400-e29b-41d4-a716-446655440000" -> "...55440000"
82
+ */
83
+ export function truncateUuid(uuid: string): string {
84
+ if (!uuid || uuid.length < 8) return uuid || '';
85
+ return `...${uuid.slice(-8)}`;
86
+ }
87
+
88
+ /**
89
+ * Truncate text at word boundary with ellipsis.
90
+ * The result including ellipsis will not exceed maxLength.
91
+ * Example: "This is a long text that needs truncation" (maxLength=25) -> "This is a long text..."
92
+ */
93
+ export function truncateText(text: string, maxLength: number): string {
94
+ if (!text || text.length <= maxLength) return text || '';
95
+
96
+ // Account for ellipsis (3 chars) in max length
97
+ const effectiveMax = maxLength - 3;
98
+ if (effectiveMax <= 0) {
99
+ return `${text.slice(0, maxLength)}...`;
100
+ }
101
+
102
+ // Find last space before effective max
103
+ const truncated = text.slice(0, effectiveMax);
104
+ const lastSpace = truncated.lastIndexOf(' ');
105
+
106
+ // If no space found or space is at the beginning, just cut at effectiveMax
107
+ if (lastSpace <= 0) {
108
+ return `${truncated.trim()}...`;
109
+ }
110
+
111
+ return `${truncated.slice(0, lastSpace).trim()}...`;
112
+ }
113
+
114
+ // ============================================================================
115
+ // MCP Response Type Guards
116
+ // ============================================================================
117
+
118
+ interface SearchNodesResponse {
119
+ nodes: Array<{
120
+ uuid?: string;
121
+ name: string;
122
+ labels?: string[]; // Actual MCP response uses labels array
123
+ entity_type?: string; // Legacy field
124
+ summary?: string;
125
+ created_at?: string;
126
+ group_id?: string;
127
+ attributes?: Record<string, unknown>; // For weighted scores, importance, etc.
128
+ }>;
129
+ }
130
+
131
+ interface SearchFactsResponse {
132
+ facts: Array<{
133
+ uuid?: string;
134
+ name: string; // Relation type (e.g., "ABOUT", "RELATES_TO")
135
+ fact: string; // Human-readable fact description
136
+ source_node_uuid?: string;
137
+ target_node_uuid?: string;
138
+ created_at?: string;
139
+ valid_at?: string;
140
+ // Legacy fields (older response format)
141
+ source?: { name: string };
142
+ target?: { name: string };
143
+ relation?: string;
144
+ confidence?: number;
145
+ }>;
146
+ }
147
+
148
+ interface GetEpisodesResponse {
149
+ episodes: Array<{
150
+ uuid?: string;
151
+ name: string;
152
+ content?: string;
153
+ created_at: string;
154
+ source_description?: string;
155
+ }>;
156
+ }
157
+
158
+ interface AddMemoryResponse {
159
+ message: string;
160
+ // Legacy fields (may not be present in newer server versions)
161
+ uuid?: string;
162
+ name?: string;
163
+ entities_extracted?: number;
164
+ facts_extracted?: number;
165
+ }
166
+
167
+ interface GetStatusResponse {
168
+ status: string;
169
+ message: string;
170
+ // Legacy fields (may not be present in newer server versions)
171
+ entity_count?: number;
172
+ episode_count?: number;
173
+ last_updated?: string;
174
+ }
175
+
176
+ interface DeleteResponse {
177
+ success: boolean;
178
+ uuid?: string;
179
+ message?: string;
180
+ }
181
+
182
+ interface ClearGraphResponse {
183
+ success: boolean;
184
+ deleted_entities?: number;
185
+ deleted_episodes?: number;
186
+ }
187
+
188
+ // Feature 009: Memory decay response types
189
+ interface HealthMetricsResponse {
190
+ states: Record<string, number>;
191
+ aggregates: {
192
+ total: number;
193
+ average_decay: number;
194
+ average_importance: number;
195
+ average_stability: number;
196
+ };
197
+ age_distribution: Record<string, number>;
198
+ maintenance: {
199
+ last_run: string | null;
200
+ duration_seconds: number;
201
+ processed: number;
202
+ transitions: number;
203
+ };
204
+ generated_at: string;
205
+ }
206
+
207
+ interface MaintenanceResultResponse {
208
+ success: boolean;
209
+ memories_processed: number;
210
+ nodes_classified: {
211
+ found: number;
212
+ classified: number;
213
+ failed: number;
214
+ using_llm: boolean;
215
+ };
216
+ decay_scores_updated: number;
217
+ state_transitions: {
218
+ active_to_dormant: number;
219
+ dormant_to_archived: number;
220
+ archived_to_expired: number;
221
+ expired_to_soft_deleted: number;
222
+ };
223
+ soft_deleted_purged: number;
224
+ duration_seconds: number;
225
+ completed_at: string | null;
226
+ error: string | null;
227
+ }
228
+
229
+ interface ClassificationResponse {
230
+ importance: number;
231
+ stability: number;
232
+ is_permanent: boolean;
233
+ classification_source: string;
234
+ }
235
+
236
+ interface RecoveryResponse {
237
+ message: string;
238
+ uuid: string;
239
+ name: string;
240
+ new_state: string;
241
+ }
242
+
243
+ // Feature 020: Investigative search response types
244
+ interface InvestigateEntityResponse {
245
+ entity: {
246
+ uuid: string;
247
+ name: string;
248
+ labels: string[];
249
+ summary?: string;
250
+ created_at?: string;
251
+ group_id?: string;
252
+ };
253
+ connections: Array<{
254
+ relationship: string;
255
+ direction: string;
256
+ hop_distance: number;
257
+ target_entity: {
258
+ uuid: string;
259
+ name: string;
260
+ labels: string[];
261
+ summary?: string;
262
+ created_at?: string;
263
+ group_id?: string;
264
+ };
265
+ fact?: string;
266
+ confidence?: number;
267
+ }>;
268
+ metadata: {
269
+ depth_explored: number;
270
+ total_connections_explored: number;
271
+ connections_returned: number;
272
+ cycles_detected: number;
273
+ cycles_pruned: number;
274
+ entities_skipped: number;
275
+ query_duration_ms: number;
276
+ max_connections_exceeded?: boolean;
277
+ };
278
+ warning?: string;
279
+ }
280
+
281
+ function isSearchNodesResponse(data: unknown): data is SearchNodesResponse {
282
+ return (
283
+ typeof data === 'object' &&
284
+ data !== null &&
285
+ 'nodes' in data &&
286
+ Array.isArray((data as SearchNodesResponse).nodes)
287
+ );
288
+ }
289
+
290
+ function isSearchFactsResponse(data: unknown): data is SearchFactsResponse {
291
+ if (typeof data !== 'object' || data === null) return false;
292
+ if (!('facts' in data)) return false;
293
+ const facts = (data as SearchFactsResponse).facts;
294
+ if (!Array.isArray(facts)) return false;
295
+ // Validate at least first fact has expected fields (name+fact or source+target+relation)
296
+ if (facts.length > 0) {
297
+ const first = facts[0];
298
+ const hasNewFormat = 'name' in first && 'fact' in first;
299
+ const hasLegacyFormat = 'source' in first && 'target' in first && 'relation' in first;
300
+ return hasNewFormat || hasLegacyFormat;
301
+ }
302
+ return true; // Empty array is valid
303
+ }
304
+
305
+ function isGetEpisodesResponse(data: unknown): data is GetEpisodesResponse {
306
+ return (
307
+ typeof data === 'object' &&
308
+ data !== null &&
309
+ 'episodes' in data &&
310
+ Array.isArray((data as GetEpisodesResponse).episodes)
311
+ );
312
+ }
313
+
314
+ function isAddMemoryResponse(data: unknown): data is AddMemoryResponse {
315
+ return (
316
+ typeof data === 'object' &&
317
+ data !== null &&
318
+ 'message' in data &&
319
+ typeof (data as AddMemoryResponse).message === 'string'
320
+ );
321
+ }
322
+
323
+ function isGetStatusResponse(data: unknown): data is GetStatusResponse {
324
+ return typeof data === 'object' && data !== null && 'status' in data && 'message' in data;
325
+ }
326
+
327
+ function isDeleteResponse(data: unknown): data is DeleteResponse {
328
+ return (
329
+ typeof data === 'object' &&
330
+ data !== null &&
331
+ 'success' in data &&
332
+ typeof (data as DeleteResponse).success === 'boolean'
333
+ );
334
+ }
335
+
336
+ function isClearGraphResponse(data: unknown): data is ClearGraphResponse {
337
+ return (
338
+ typeof data === 'object' &&
339
+ data !== null &&
340
+ 'success' in data &&
341
+ typeof (data as ClearGraphResponse).success === 'boolean'
342
+ );
343
+ }
344
+
345
+ function isHealthMetricsResponse(data: unknown): data is HealthMetricsResponse {
346
+ return (
347
+ typeof data === 'object' &&
348
+ data !== null &&
349
+ 'states' in data &&
350
+ 'aggregates' in data &&
351
+ 'age_distribution' in data
352
+ );
353
+ }
354
+
355
+ function isMaintenanceResultResponse(data: unknown): data is MaintenanceResultResponse {
356
+ return (
357
+ typeof data === 'object' &&
358
+ data !== null &&
359
+ 'success' in data &&
360
+ 'memories_processed' in data &&
361
+ 'state_transitions' in data
362
+ );
363
+ }
364
+
365
+ function isClassificationResponse(data: unknown): data is ClassificationResponse {
366
+ return (
367
+ typeof data === 'object' &&
368
+ data !== null &&
369
+ 'importance' in data &&
370
+ 'stability' in data &&
371
+ 'is_permanent' in data
372
+ );
373
+ }
374
+
375
+ function isRecoveryResponse(data: unknown): data is RecoveryResponse {
376
+ return (
377
+ typeof data === 'object' &&
378
+ data !== null &&
379
+ 'message' in data &&
380
+ 'uuid' in data &&
381
+ 'name' in data &&
382
+ 'new_state' in data
383
+ );
384
+ }
385
+
386
+ function isInvestigateEntityResponse(data: unknown): data is InvestigateEntityResponse {
387
+ if (typeof data !== 'object' || data === null) return false;
388
+ const d = data as Record<string, unknown>;
389
+ return (
390
+ 'entity' in d &&
391
+ typeof d.entity === 'object' &&
392
+ d.entity !== null &&
393
+ 'connections' in d &&
394
+ Array.isArray(d.connections) &&
395
+ 'metadata' in d &&
396
+ typeof d.metadata === 'object' &&
397
+ d.metadata !== null
398
+ );
399
+ }
400
+
401
+ // ============================================================================
402
+ // Individual Formatters (T014-T020)
403
+ // ============================================================================
404
+
405
+ /**
406
+ * Format search_nodes / search_memory_nodes response
407
+ * Output: Found N entities for "query":
408
+ * 1. Name [Type] - Summary (truncated to 120 chars)
409
+ * When weighted scores present: 📊 Score: 0.XX (S:0.XX R:0.XX I:0.XX)
410
+ */
411
+ export function formatSearchNodes(data: unknown, options: FormatOptions): string {
412
+ if (!isSearchNodesResponse(data)) {
413
+ throw new Error('Invalid data format for search_nodes');
414
+ }
415
+
416
+ const { nodes } = data;
417
+ const query = options.query || 'query';
418
+ const maxLines = options.maxLines ?? DEFAULT_OPTIONS.maxLines ?? 20;
419
+
420
+ if (nodes.length === 0) {
421
+ return `No entities found for "${query}"`;
422
+ }
423
+
424
+ const lines: string[] = [`Found ${nodes.length} entities for "${query}":`];
425
+
426
+ // DEBUG: Log first node structure to understand what attributes are present
427
+ if (nodes.length > 0) {
428
+ const firstNode = nodes[0];
429
+ console.error('[DEBUG] First node keys:', Object.keys(firstNode));
430
+ console.error('[DEBUG] First node sample:', JSON.stringify(firstNode).substring(0, 500));
431
+ }
432
+
433
+ const displayNodes = nodes.slice(0, maxLines);
434
+ displayNodes.forEach((node, index) => {
435
+ const summary = truncateText(node.summary || '', 120);
436
+ // Use labels array (new format) or entity_type (legacy)
437
+ const entityType = node.labels?.[0] || node.entity_type || 'Entity';
438
+
439
+ let line = `${index + 1}. ${node.name} [${entityType}] - ${summary}`;
440
+
441
+ // Display weighted scores if present (Feature 009: Memory Decay Scoring)
442
+ if (node.attributes?.weighted_score !== undefined) {
443
+ const score = node.attributes.weighted_score as number;
444
+ const breakdown = node.attributes.score_breakdown as { semantic: number; recency: number; importance: number } | undefined;
445
+
446
+ if (breakdown) {
447
+ line += `\n 📊 Score: ${score.toFixed(2)} (S:${breakdown.semantic.toFixed(2)} R:${breakdown.recency.toFixed(2)} I:${breakdown.importance.toFixed(2)})`;
448
+ } else {
449
+ line += `\n 📊 Score: ${score.toFixed(2)}`;
450
+ }
451
+
452
+ // Show lifecycle state if present
453
+ if (node.attributes.lifecycle_state) {
454
+ const state = node.attributes.lifecycle_state as string;
455
+ line += ` [${state}]`;
456
+ }
457
+
458
+ // Show importance/stability if present
459
+ if (node.attributes.importance !== undefined || node.attributes.stability !== undefined) {
460
+ const imp = node.attributes.importance as number | undefined;
461
+ const stab = node.attributes.stability as number | undefined;
462
+ const parts = [];
463
+ if (imp !== undefined) parts.push(`Imp:${imp}`);
464
+ if (stab !== undefined) parts.push(`Stab:${stab}`);
465
+ if (parts.length > 0) line += ` (${parts.join(' ')})`;
466
+ }
467
+ }
468
+
469
+ lines.push(line);
470
+ });
471
+
472
+ if (nodes.length > maxLines) {
473
+ lines.push(`... and ${nodes.length - maxLines} more`);
474
+ }
475
+
476
+ return lines.join('\n');
477
+ }
478
+
479
+ /**
480
+ * Format search_facts / search_memory_facts response
481
+ * Output: Found N facts for "query":
482
+ * 1. [RELATION] Fact description (truncated)
483
+ */
484
+ export function formatSearchFacts(data: unknown, options: FormatOptions): string {
485
+ if (!isSearchFactsResponse(data)) {
486
+ throw new Error('Invalid data format for search_facts');
487
+ }
488
+
489
+ const { facts } = data;
490
+ const query = options.query || 'query';
491
+ const maxLines = options.maxLines ?? DEFAULT_OPTIONS.maxLines ?? 20;
492
+
493
+ if (facts.length === 0) {
494
+ return `No facts found for "${query}"`;
495
+ }
496
+
497
+ const lines: string[] = [`Found ${facts.length} facts for "${query}":`];
498
+
499
+ const displayFacts = facts.slice(0, maxLines);
500
+ displayFacts.forEach((fact, index) => {
501
+ // New format: name is relation type, fact is description
502
+ if (fact.name && fact.fact) {
503
+ const relation = fact.name.toUpperCase();
504
+ // Use full fact text - no truncation to preserve complete knowledge
505
+ lines.push(`${index + 1}. [${relation}] ${fact.fact}`);
506
+ }
507
+ // Legacy format: source/target/relation objects
508
+ else if (fact.source && fact.target && fact.relation) {
509
+ const relation = fact.relation.toLowerCase().replace(/_/g, '-');
510
+ let line = `${index + 1}. ${fact.source.name} --${relation}--> ${fact.target.name}`;
511
+ if (fact.confidence !== undefined && fact.confidence > 0) {
512
+ line += ` (${(fact.confidence * 100).toFixed(0)}%)`;
513
+ }
514
+ lines.push(line);
515
+ }
516
+ });
517
+
518
+ if (facts.length > maxLines) {
519
+ lines.push(`... and ${facts.length - maxLines} more`);
520
+ }
521
+
522
+ return lines.join('\n');
523
+ }
524
+
525
+ /**
526
+ * Format get_episodes response
527
+ * Output: Recent episodes (N):
528
+ * - [2h ago] Name - Content preview...
529
+ */
530
+ export function formatGetEpisodes(data: unknown, options: FormatOptions): string {
531
+ if (!isGetEpisodesResponse(data)) {
532
+ throw new Error('Invalid data format for get_episodes');
533
+ }
534
+
535
+ const { episodes } = data;
536
+ const maxLines = options.maxLines ?? DEFAULT_OPTIONS.maxLines ?? 20;
537
+
538
+ if (episodes.length === 0) {
539
+ return 'No episodes found';
540
+ }
541
+
542
+ const lines: string[] = [`Recent episodes (${episodes.length}):`];
543
+
544
+ const displayEpisodes = episodes.slice(0, maxLines);
545
+ displayEpisodes.forEach((episode) => {
546
+ const time = relativeTime(episode.created_at);
547
+ // Use full content - no truncation to preserve complete knowledge
548
+ const content = episode.content || '';
549
+ lines.push(`- [${time}] ${episode.name} - ${content}`);
550
+ });
551
+
552
+ if (episodes.length > maxLines) {
553
+ lines.push(`... and ${episodes.length - maxLines} more`);
554
+ }
555
+
556
+ return lines.join('\n');
557
+ }
558
+
559
+ /**
560
+ * Format add_memory response
561
+ * Output: ✓ Episode queued: "Episode 'Name' queued for processing in group 'main'"
562
+ * Legacy: ✓ Episode added: "Name" (id: ...uuid8)
563
+ * Extracted: N entities, M facts
564
+ */
565
+ export function formatAddMemory(data: unknown, _options: FormatOptions): string {
566
+ if (!isAddMemoryResponse(data)) {
567
+ throw new Error('Invalid data format for add_memory');
568
+ }
569
+
570
+ // New format: server returns a message string
571
+ const lines: string[] = [`✓ ${data.message}`];
572
+
573
+ // Legacy format: include uuid and extraction stats if available
574
+ if (data.uuid) {
575
+ const uuid = truncateUuid(data.uuid);
576
+ const name = data.name || 'Unnamed episode';
577
+ lines[0] = `✓ Episode added: "${name}" (id: ${uuid})`;
578
+ }
579
+
580
+ if (data.entities_extracted !== undefined || data.facts_extracted !== undefined) {
581
+ const entities = data.entities_extracted ?? 0;
582
+ const facts = data.facts_extracted ?? 0;
583
+ lines.push(` Extracted: ${entities} entities, ${facts} facts`);
584
+ }
585
+
586
+ return lines.join('\n');
587
+ }
588
+
589
+ /**
590
+ * Format get_status response
591
+ * Output: Knowledge Graph Status: OK
592
+ * Connected to neo4j database
593
+ */
594
+ export function formatGetStatus(data: unknown, _options: FormatOptions): string {
595
+ if (!isGetStatusResponse(data)) {
596
+ throw new Error('Invalid data format for get_status');
597
+ }
598
+
599
+ const status = data.status.toUpperCase();
600
+ const lines: string[] = [`Knowledge Graph Status: ${status}`];
601
+ lines.push(data.message);
602
+
603
+ // Include legacy stats if available
604
+ if (data.entity_count !== undefined && data.episode_count !== undefined) {
605
+ let statsLine = `Entities: ${data.entity_count} | Episodes: ${data.episode_count}`;
606
+ if (data.last_updated) {
607
+ statsLine += ` | Last update: ${relativeTime(data.last_updated)}`;
608
+ }
609
+ lines.push(statsLine);
610
+ }
611
+
612
+ return lines.join('\n');
613
+ }
614
+
615
+ /**
616
+ * Format delete_episode / delete_entity_edge response
617
+ * Output: ✓ Deleted: ...uuid8
618
+ * or: ✗ Delete failed: message
619
+ */
620
+ export function formatDelete(data: unknown, _options: FormatOptions): string {
621
+ if (!isDeleteResponse(data)) {
622
+ throw new Error('Invalid data format for delete');
623
+ }
624
+
625
+ if (data.success) {
626
+ const uuid = data.uuid ? truncateUuid(data.uuid) : 'unknown';
627
+ return `✓ Deleted: ${uuid}`;
628
+ }
629
+ const message = data.message || 'Unknown error';
630
+ return `✗ Delete failed: ${message}`;
631
+ }
632
+
633
+ /**
634
+ * Format clear_graph response
635
+ * Output: ✓ Knowledge graph cleared
636
+ * Removed: N entities, M episodes
637
+ */
638
+ export function formatClearGraph(data: unknown, _options: FormatOptions): string {
639
+ if (!isClearGraphResponse(data)) {
640
+ throw new Error('Invalid data format for clear_graph');
641
+ }
642
+
643
+ if (!data.success) {
644
+ return '✗ Clear graph failed';
645
+ }
646
+
647
+ const lines: string[] = ['✓ Knowledge graph cleared'];
648
+
649
+ if (data.deleted_entities !== undefined || data.deleted_episodes !== undefined) {
650
+ const entities = data.deleted_entities ?? 0;
651
+ const episodes = data.deleted_episodes ?? 0;
652
+ lines.push(` Removed: ${entities} entities, ${episodes} episodes`);
653
+ }
654
+
655
+ return lines.join('\n');
656
+ }
657
+
658
+ // ============================================================================
659
+ // Feature 009: Memory Decay Formatters
660
+ // ============================================================================
661
+
662
+ /**
663
+ * Format get_knowledge_health response
664
+ * Output: Memory Lifecycle Health:
665
+ * Lifecycle States: Active, Dormant, Archived, Expired, Soft Deleted, Permanent
666
+ * Aggregates: Total, Avg Decay, Avg Importance, Avg Stability
667
+ * Age Distribution: < 7d, 7-30d, 30-90d, > 90d
668
+ * Last Maintenance: timestamp, duration, processed, transitions
669
+ */
670
+ export function formatHealthMetrics(data: unknown, _options: FormatOptions): string {
671
+ if (!isHealthMetricsResponse(data)) {
672
+ throw new Error('Invalid data format for get_knowledge_health');
673
+ }
674
+
675
+ const lines: string[] = ['Memory Lifecycle Health:'];
676
+
677
+ // Lifecycle states
678
+ lines.push('\nLifecycle States:');
679
+ lines.push(` Active: ${data.states.active.toLocaleString()}`);
680
+ lines.push(` Dormant: ${data.states.dormant.toLocaleString()}`);
681
+ lines.push(` Archived: ${data.states.archived.toLocaleString()}`);
682
+ lines.push(` Expired: ${data.states.expired.toLocaleString()}`);
683
+ lines.push(` Soft Deleted: ${data.states.soft_deleted.toLocaleString()}`);
684
+ lines.push(` Permanent: ${data.states.permanent.toLocaleString()}`);
685
+
686
+ // Aggregates
687
+ lines.push('\nAggregates:');
688
+ lines.push(` Total: ${data.aggregates.total.toLocaleString()}`);
689
+ lines.push(` Avg Decay: ${data.aggregates.average_decay.toFixed(3)}`);
690
+ lines.push(` Avg Importance: ${data.aggregates.average_importance.toFixed(2)}/5`);
691
+ lines.push(` Avg Stability: ${data.aggregates.average_stability.toFixed(2)}/5`);
692
+
693
+ // Age distribution
694
+ lines.push('\nAge Distribution:');
695
+ lines.push(` < 7 days: ${data.age_distribution.under_7_days.toLocaleString()}`);
696
+ lines.push(` 7-30 days: ${data.age_distribution.days_7_to_30.toLocaleString()}`);
697
+ lines.push(` 30-90 days: ${data.age_distribution.days_30_to_90.toLocaleString()}`);
698
+ lines.push(` > 90 days: ${data.age_distribution.over_90_days.toLocaleString()}`);
699
+
700
+ // Maintenance info
701
+ if (data.maintenance.last_run) {
702
+ lines.push('\nLast Maintenance:');
703
+ lines.push(` Run: ${relativeTime(data.maintenance.last_run)}`);
704
+ lines.push(` Duration: ${data.maintenance.duration_seconds.toFixed(1)}s`);
705
+ lines.push(` Processed: ${data.maintenance.processed.toLocaleString()}`);
706
+ lines.push(` Transitions: ${data.maintenance.transitions.toLocaleString()}`);
707
+ }
708
+
709
+ return lines.join('\n');
710
+ }
711
+
712
+ /**
713
+ * Format run_decay_maintenance response
714
+ * Output: ✓ Maintenance completed successfully
715
+ * Processed: N memories
716
+ * Duration: X.Xs
717
+ * Classification: Found/Classified/Failed
718
+ * Decay scores updated: N
719
+ * State Transitions: ACTIVE→DORMANT, etc.
720
+ * Purged: N soft-deleted memories
721
+ */
722
+ export function formatMaintenanceResult(data: unknown, _options: FormatOptions): string {
723
+ if (!isMaintenanceResultResponse(data)) {
724
+ throw new Error('Invalid data format for run_decay_maintenance');
725
+ }
726
+
727
+ const lines: string[] = [];
728
+
729
+ if (data.success) {
730
+ lines.push('✓ Maintenance completed successfully');
731
+ } else {
732
+ lines.push(`✗ Maintenance failed: ${data.error || 'Unknown error'}`);
733
+ }
734
+
735
+ lines.push(` Processed: ${data.memories_processed.toLocaleString()} memories`);
736
+ lines.push(` Duration: ${data.duration_seconds.toFixed(1)}s`);
737
+
738
+ // Classification results
739
+ if (data.nodes_classified.classified > 0) {
740
+ lines.push('\nClassification:');
741
+ lines.push(` Found: ${data.nodes_classified.found}`);
742
+ lines.push(` Classified: ${data.nodes_classified.classified} (${data.nodes_classified.using_llm ? 'LLM' : 'default'})`);
743
+ if (data.nodes_classified.failed > 0) {
744
+ lines.push(` Failed: ${data.nodes_classified.failed}`);
745
+ }
746
+ }
747
+
748
+ // Decay scores
749
+ if (data.decay_scores_updated > 0) {
750
+ lines.push(`\nDecay scores updated: ${data.decay_scores_updated.toLocaleString()}`);
751
+ }
752
+
753
+ // State transitions
754
+ const st = data.state_transitions;
755
+ const total = st.active_to_dormant + st.dormant_to_archived + st.archived_to_expired + st.expired_to_soft_deleted;
756
+ if (total > 0) {
757
+ lines.push('\nState Transitions:');
758
+ lines.push(` ACTIVE → DORMANT: ${st.active_to_dormant}`);
759
+ lines.push(` DORMANT → ARCHIVED: ${st.dormant_to_archived}`);
760
+ lines.push(` ARCHIVED → EXPIRED: ${st.archived_to_expired}`);
761
+ lines.push(` EXPIRED → SOFT_DEL: ${st.expired_to_soft_deleted}`);
762
+ lines.push(` Total: ${total}`);
763
+ }
764
+
765
+ // Purged
766
+ if (data.soft_deleted_purged > 0) {
767
+ lines.push(`\nPurged: ${data.soft_deleted_purged.toLocaleString()} soft-deleted memories`);
768
+ }
769
+
770
+ if (data.completed_at) {
771
+ lines.push(`\nCompleted: ${relativeTime(data.completed_at)}`);
772
+ }
773
+
774
+ return lines.join('\n');
775
+ }
776
+
777
+ /**
778
+ * Format classify_memory response
779
+ * Output: Memory Classification:
780
+ * Importance: 3/3 ★★★☆☆
781
+ * Stability: 4/5 ★★★★☆
782
+ * Source: llm
783
+ * Status: Subject to decay / PERMANENT (exempt from decay)
784
+ */
785
+ export function formatClassification(data: unknown, _options: FormatOptions): string {
786
+ if (!isClassificationResponse(data)) {
787
+ throw new Error('Invalid data format for classify_memory');
788
+ }
789
+
790
+ const lines: string[] = ['Memory Classification:'];
791
+ lines.push(` Importance: ${data.importance}/5 ${'★'.repeat(data.importance)}${'☆'.repeat(5 - data.importance)}`);
792
+ lines.push(` Stability: ${data.stability}/5 ${'★'.repeat(data.stability)}${'☆'.repeat(5 - data.stability)}`);
793
+ lines.push(` Source: ${data.classification_source}`);
794
+
795
+ if (data.is_permanent) {
796
+ lines.push(` Status: PERMANENT (exempt from decay)`);
797
+ } else {
798
+ lines.push(` Status: Subject to decay`);
799
+ }
800
+
801
+ return lines.join('\n');
802
+ }
803
+
804
+ /**
805
+ * Format recover_soft_deleted response
806
+ * Output: ✓ Memory recovered successfully
807
+ * Name: Memory Name
808
+ * UUID: ...uuid8
809
+ * New State: ARCHIVED
810
+ */
811
+ export function formatRecovery(data: unknown, _options: FormatOptions): string {
812
+ if (!isRecoveryResponse(data)) {
813
+ throw new Error('Invalid data format for recover_soft_deleted');
814
+ }
815
+
816
+ const lines: string[] = [`✓ ${data.message}`];
817
+ lines.push(` Name: ${data.name}`);
818
+ lines.push(` UUID: ...${data.uuid.slice(-8)}`);
819
+ lines.push(` New State: ${data.new_state}`);
820
+
821
+ return lines.join('\n');
822
+ }
823
+
824
+ /**
825
+ * Format investigate_entity response
826
+ * Output: Entity: [Type] Name - Summary
827
+ * Connections:
828
+ * 1. [RELATION] Target [Type] (hop N)
829
+ * Metadata: depth=X, connections=Y, cycles=Z
830
+ */
831
+ export function formatInvestigateEntity(data: unknown, _options: FormatOptions): string {
832
+ if (!isInvestigateEntityResponse(data)) {
833
+ throw new Error('Invalid data format for investigate_entity');
834
+ }
835
+
836
+ const { entity, connections, metadata, warning } = data;
837
+ const lines: string[] = [];
838
+
839
+ // Format the primary entity
840
+ const entityType = entity.labels?.[0] || 'Entity';
841
+ const summary = entity.summary ? truncateText(entity.summary, 120) : '';
842
+ lines.push(`Entity: [${entityType}] ${entity.name}${summary ? ' - ' + summary : ''}`);
843
+
844
+ // Format connections
845
+ if (connections.length === 0) {
846
+ lines.push('No connections found.');
847
+ } else {
848
+ lines.push(`\nConnections (${connections.length}):`);
849
+ connections.forEach((conn, index) => {
850
+ const targetType = conn.target_entity.labels?.[0] || 'Entity';
851
+ const relation = conn.relationship.toUpperCase();
852
+ const directionSymbol = conn.direction === 'outbound' ? '→' : conn.direction === 'inbound' ? '←' : '↔';
853
+ const hopInfo = conn.hop_distance > 1 ? ` (hop ${conn.hop_distance})` : '';
854
+
855
+ let connLine = ` ${index + 1}. [${relation}] ${directionSymbol} ${conn.target_entity.name} [${targetType}]${hopInfo}`;
856
+
857
+ if (conn.fact) {
858
+ connLine += `\n Fact: ${truncateText(conn.fact, 100)}`;
859
+ }
860
+
861
+ if (conn.confidence !== undefined) {
862
+ connLine += ` (${(conn.confidence * 100).toFixed(0)}% confidence)`;
863
+ }
864
+
865
+ lines.push(connLine);
866
+ });
867
+ }
868
+
869
+ // Format metadata
870
+ lines.push('\nMetadata:');
871
+ lines.push(` Depth explored: ${metadata.depth_explored}`);
872
+ lines.push(` Connections: ${metadata.connections_returned}/${metadata.total_connections_explored}`);
873
+ if (metadata.cycles_detected > 0) {
874
+ lines.push(` Cycles detected: ${metadata.cycles_detected} (pruned: ${metadata.cycles_pruned})`);
875
+ }
876
+ if (metadata.entities_skipped > 0) {
877
+ lines.push(` Entities skipped: ${metadata.entities_skipped}`);
878
+ }
879
+ lines.push(` Query duration: ${metadata.query_duration_ms}ms`);
880
+ if (metadata.max_connections_exceeded) {
881
+ lines.push(` ⚠ Max connections limit exceeded`);
882
+ }
883
+
884
+ // Add warning if present
885
+ if (warning) {
886
+ lines.push(`\n⚠ Warning: ${warning}`);
887
+ }
888
+
889
+ return lines.join('\n');
890
+ }
891
+
892
+ // ============================================================================
893
+ // Main Entry Point (T021)
894
+ // ============================================================================
895
+
896
+ // Register all built-in formatters
897
+ registerFormatter('search_nodes', formatSearchNodes);
898
+ registerFormatter('search_memory_nodes', formatSearchNodes);
899
+ registerFormatter('search_facts', formatSearchFacts);
900
+ registerFormatter('search_memory_facts', formatSearchFacts);
901
+ registerFormatter('get_episodes', formatGetEpisodes);
902
+ registerFormatter('add_memory', formatAddMemory);
903
+ registerFormatter('add_episode', formatAddMemory);
904
+ registerFormatter('get_status', formatGetStatus);
905
+ registerFormatter('delete_episode', formatDelete);
906
+ registerFormatter('delete_entity_edge', formatDelete);
907
+ registerFormatter('clear_graph', formatClearGraph);
908
+ // Feature 009: Memory decay formatters
909
+ registerFormatter('get_knowledge_health', formatHealthMetrics);
910
+ registerFormatter('run_decay_maintenance', formatMaintenanceResult);
911
+ registerFormatter('classify_memory', formatClassification);
912
+ registerFormatter('recover_soft_deleted', formatRecovery);
913
+ // Feature 020: Investigative search
914
+ registerFormatter('investigate_entity', formatInvestigateEntity);
915
+
916
+ /**
917
+ * Main entry point for formatting MCP responses.
918
+ * Routes to the appropriate formatter based on operation name.
919
+ * Falls back to JSON on unknown operations or errors.
920
+ */
921
+ export function formatOutput(
922
+ operation: string,
923
+ data: unknown,
924
+ options?: FormatOptions
925
+ ): FormatResult {
926
+ const opts = { ...DEFAULT_OPTIONS, ...options };
927
+ const startTime = performance.now();
928
+
929
+ // Calculate raw size
930
+ const rawJson = JSON.stringify(data, null, 2);
931
+ const rawBytes = new TextEncoder().encode(rawJson).length;
932
+
933
+ // Try to find and execute formatter
934
+ const formatter = formatterRegistry.get(operation);
935
+
936
+ if (!formatter) {
937
+ // Unknown operation - fallback to JSON
938
+ return {
939
+ output: rawJson,
940
+ usedFallback: true,
941
+ error: `No formatter registered for operation: ${operation}`,
942
+ metrics: opts.collectMetrics
943
+ ? {
944
+ rawBytes,
945
+ compactBytes: rawBytes,
946
+ savingsPercent: 0,
947
+ processingTimeMs: performance.now() - startTime,
948
+ }
949
+ : undefined,
950
+ };
951
+ }
952
+
953
+ try {
954
+ const output = formatter(data, opts);
955
+ const compactBytes = new TextEncoder().encode(output).length;
956
+ const processingTimeMs = performance.now() - startTime;
957
+ const savingsPercent = rawBytes > 0 ? ((rawBytes - compactBytes) / rawBytes) * 100 : 0;
958
+
959
+ // Log slow transformations
960
+ if (processingTimeMs >= 50) {
961
+ logTransformationSuccess(operation, rawBytes, processingTimeMs).catch(() => {});
962
+ }
963
+
964
+ return {
965
+ output,
966
+ usedFallback: false,
967
+ metrics: opts.collectMetrics
968
+ ? {
969
+ rawBytes,
970
+ compactBytes,
971
+ savingsPercent,
972
+ processingTimeMs,
973
+ }
974
+ : undefined,
975
+ };
976
+ } catch (error) {
977
+ const processingTimeMs = performance.now() - startTime;
978
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
979
+
980
+ // Log transformation failure
981
+ logTransformationFailure(operation, rawBytes, errorMessage, processingTimeMs).catch(() => {});
982
+
983
+ return {
984
+ output: rawJson,
985
+ usedFallback: true,
986
+ error: errorMessage,
987
+ metrics: opts.collectMetrics
988
+ ? {
989
+ rawBytes,
990
+ compactBytes: rawBytes,
991
+ savingsPercent: 0,
992
+ processingTimeMs,
993
+ }
994
+ : undefined,
995
+ };
996
+ }
997
+ }