@memoryrelay/plugin-memoryrelay-ai 0.15.7 → 0.16.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 (42) hide show
  1. package/index.ts +493 -4868
  2. package/openclaw.plugin.json +41 -3
  3. package/package.json +1 -1
  4. package/src/client/memoryrelay-client.ts +816 -0
  5. package/src/context/namespace-router.ts +19 -0
  6. package/src/context/request-context.ts +39 -0
  7. package/src/context/session-resolver.ts +93 -0
  8. package/src/filters/content-patterns.ts +32 -0
  9. package/src/filters/noise-patterns.ts +33 -0
  10. package/src/filters/non-interactive.ts +30 -0
  11. package/src/hooks/activity.ts +51 -0
  12. package/src/hooks/agent-end.ts +48 -0
  13. package/src/hooks/before-agent-start.ts +109 -0
  14. package/src/hooks/before-prompt-build.ts +46 -0
  15. package/src/hooks/compaction.ts +51 -0
  16. package/src/hooks/privacy.ts +44 -0
  17. package/src/hooks/session-lifecycle.ts +47 -0
  18. package/src/hooks/subagent.ts +62 -0
  19. package/src/pipelines/capture/content-strip.ts +14 -0
  20. package/src/pipelines/capture/dedup.ts +17 -0
  21. package/src/pipelines/capture/index.ts +13 -0
  22. package/src/pipelines/capture/message-filter.ts +16 -0
  23. package/src/pipelines/capture/store.ts +33 -0
  24. package/src/pipelines/capture/trigger-gate.ts +21 -0
  25. package/src/pipelines/capture/truncate.ts +16 -0
  26. package/src/pipelines/recall/format.ts +30 -0
  27. package/src/pipelines/recall/index.ts +12 -0
  28. package/src/pipelines/recall/rank.ts +40 -0
  29. package/src/pipelines/recall/scope-resolver.ts +20 -0
  30. package/src/pipelines/recall/search.ts +43 -0
  31. package/src/pipelines/recall/trigger-gate.ts +17 -0
  32. package/src/pipelines/runner.ts +25 -0
  33. package/src/pipelines/types.ts +157 -0
  34. package/src/tools/agent-tools.ts +127 -0
  35. package/src/tools/decision-tools.ts +309 -0
  36. package/src/tools/entity-tools.ts +215 -0
  37. package/src/tools/health-tools.ts +42 -0
  38. package/src/tools/memory-tools.ts +690 -0
  39. package/src/tools/pattern-tools.ts +250 -0
  40. package/src/tools/project-tools.ts +444 -0
  41. package/src/tools/session-tools.ts +195 -0
  42. package/src/tools/v2-tools.ts +228 -0
@@ -0,0 +1,816 @@
1
+ /**
2
+ * MemoryRelay API Client
3
+ *
4
+ * Extracted from index.ts — provides typed HTTP access to the MemoryRelay API
5
+ * with timeout, retry, and debug/status instrumentation.
6
+ */
7
+
8
+ import type { DebugLogger } from "../debug-logger.js";
9
+ import type { StatusReporter } from "../status-reporter.js";
10
+ import type { Memory, MemoryRelayClient as IMemoryRelayClient } from "../pipelines/types.js";
11
+
12
+ // ============================================================================
13
+ // Constants
14
+ // ============================================================================
15
+
16
+ export const DEFAULT_API_URL = "https://api.memoryrelay.net";
17
+ export const REQUEST_TIMEOUT_MS = 30000; // 30 seconds
18
+ export const MAX_RETRIES = 3;
19
+ export const INITIAL_RETRY_DELAY_MS = 1000; // 1 second
20
+ export const VALID_HEALTH_STATUSES = ["ok", "healthy", "up"];
21
+
22
+ // ============================================================================
23
+ // Types
24
+ // ============================================================================
25
+
26
+ // Re-export Memory from canonical source
27
+ export type { Memory } from "../pipelines/types.js";
28
+
29
+ export interface SearchResult {
30
+ memory: Memory;
31
+ score: number;
32
+ }
33
+
34
+ export interface Stats {
35
+ total_memories: number;
36
+ last_updated?: string;
37
+ }
38
+
39
+ // ============================================================================
40
+ // Utility Functions
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Sleep for specified milliseconds
45
+ */
46
+ function sleep(ms: number): Promise<void> {
47
+ return new Promise((resolve) => setTimeout(resolve, ms));
48
+ }
49
+
50
+ /**
51
+ * Check if error is retryable (network/timeout errors)
52
+ */
53
+ function isRetryableError(error: unknown): boolean {
54
+ const errStr = String(error).toLowerCase();
55
+ return (
56
+ errStr.includes("timeout") ||
57
+ errStr.includes("econnrefused") ||
58
+ errStr.includes("enotfound") ||
59
+ errStr.includes("network") ||
60
+ errStr.includes("fetch failed") ||
61
+ errStr.includes("502") ||
62
+ errStr.includes("503") ||
63
+ errStr.includes("504")
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Fetch with timeout
69
+ */
70
+ export async function fetchWithTimeout(
71
+ url: string,
72
+ options: RequestInit,
73
+ timeoutMs: number,
74
+ ): Promise<Response> {
75
+ const controller = new AbortController();
76
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
77
+
78
+ try {
79
+ const response = await fetch(url, {
80
+ ...options,
81
+ signal: controller.signal,
82
+ });
83
+ clearTimeout(timeout);
84
+ return response;
85
+ } catch (err) {
86
+ clearTimeout(timeout);
87
+ if ((err as Error).name === "AbortError") {
88
+ throw new Error("Request timeout");
89
+ }
90
+ throw err;
91
+ }
92
+ }
93
+
94
+ // ============================================================================
95
+ // MemoryRelay API Client (Full Suite)
96
+ // ============================================================================
97
+
98
+ export class MemoryRelayClient implements IMemoryRelayClient {
99
+ private debugLogger?: DebugLogger;
100
+ private statusReporter?: StatusReporter;
101
+
102
+ constructor(
103
+ private readonly apiKey: string,
104
+ private readonly agentId: string,
105
+ private readonly apiUrl: string = DEFAULT_API_URL,
106
+ debugLogger?: DebugLogger,
107
+ statusReporter?: StatusReporter,
108
+ ) {
109
+ this.debugLogger = debugLogger;
110
+ this.statusReporter = statusReporter;
111
+ }
112
+
113
+ /**
114
+ * Extract tool name from API path
115
+ */
116
+ private extractToolName(path: string): string {
117
+ // /v1/memories -> memory
118
+ // /v1/memories/batch -> memory_batch
119
+ // /v1/sessions/123/end -> session_end
120
+ const parts = path.split("/").filter(Boolean);
121
+ if (parts.length < 2) return "unknown";
122
+
123
+ let toolName = parts[1].replace(/s$/, ""); // Remove trailing 's'
124
+
125
+ // Check for specific endpoints
126
+ if (path.includes("/batch")) toolName += "_batch";
127
+ if (path.includes("/recall")) toolName += "_recall";
128
+ if (path.includes("/context")) toolName += "_context";
129
+ if (path.includes("/end")) toolName += "_end";
130
+ if (path.includes("/health")) return "memory_health";
131
+
132
+ return toolName;
133
+ }
134
+
135
+ /**
136
+ * Make HTTP request with retry logic and timeout
137
+ */
138
+ private async request<T>(
139
+ method: string,
140
+ path: string,
141
+ body?: unknown,
142
+ retryCount = 0,
143
+ ): Promise<T> {
144
+ const url = `${this.apiUrl}${path}`;
145
+ const startTime = Date.now();
146
+ const toolName = this.extractToolName(path);
147
+
148
+ try {
149
+ const response = await fetchWithTimeout(
150
+ url,
151
+ {
152
+ method,
153
+ headers: {
154
+ "Content-Type": "application/json",
155
+ Authorization: `Bearer ${this.apiKey}`,
156
+ "User-Agent": "openclaw-memory-memoryrelay/0.16.1",
157
+ },
158
+ body: body ? JSON.stringify(body) : undefined,
159
+ },
160
+ REQUEST_TIMEOUT_MS,
161
+ );
162
+
163
+ const duration = Date.now() - startTime;
164
+
165
+ if (!response.ok) {
166
+ const errorData = await response.json().catch(() => ({}));
167
+ const errorMsg = errorData.detail || errorData.message || "";
168
+ const error = new Error(
169
+ `MemoryRelay API error: ${response.status} ${response.statusText}` +
170
+ (errorMsg ? ` - ${errorMsg}` : ""),
171
+ );
172
+
173
+ // Log error
174
+ if (this.debugLogger) {
175
+ this.debugLogger.log({
176
+ timestamp: new Date().toISOString(),
177
+ tool: toolName,
178
+ method,
179
+ path,
180
+ duration,
181
+ status: "error",
182
+ responseStatus: response.status,
183
+ error: error.message,
184
+ retries: retryCount,
185
+ requestBody: this.debugLogger && body ? body : undefined,
186
+ });
187
+ }
188
+
189
+ // Track failure
190
+ if (this.statusReporter) {
191
+ this.statusReporter.recordFailure(toolName, `${response.status} ${errorMsg || response.statusText}`);
192
+ }
193
+
194
+ // Retry on 5xx errors
195
+ if (response.status >= 500 && retryCount < MAX_RETRIES) {
196
+ const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
197
+ await sleep(delay);
198
+ return this.request<T>(method, path, body, retryCount + 1);
199
+ }
200
+
201
+ throw error;
202
+ }
203
+
204
+ const result = await response.json();
205
+
206
+ // Log success
207
+ if (this.debugLogger) {
208
+ this.debugLogger.log({
209
+ timestamp: new Date().toISOString(),
210
+ tool: toolName,
211
+ method,
212
+ path,
213
+ duration,
214
+ status: "success",
215
+ responseStatus: response.status,
216
+ retries: retryCount,
217
+ requestBody: this.debugLogger && body ? body : undefined,
218
+ responseBody: this.debugLogger && result ? result : undefined,
219
+ });
220
+ }
221
+
222
+ // Track success
223
+ if (this.statusReporter) {
224
+ this.statusReporter.recordSuccess(toolName);
225
+ }
226
+
227
+ return result;
228
+ } catch (err) {
229
+ const duration = Date.now() - startTime;
230
+
231
+ // Log error
232
+ if (this.debugLogger) {
233
+ this.debugLogger.log({
234
+ timestamp: new Date().toISOString(),
235
+ tool: toolName,
236
+ method,
237
+ path,
238
+ duration,
239
+ status: "error",
240
+ error: String(err),
241
+ retries: retryCount,
242
+ requestBody: this.debugLogger && body ? body : undefined,
243
+ });
244
+ }
245
+
246
+ // Track failure
247
+ if (this.statusReporter) {
248
+ this.statusReporter.recordFailure(toolName, String(err));
249
+ }
250
+
251
+ // Retry on network errors
252
+ if (isRetryableError(err) && retryCount < MAX_RETRIES) {
253
+ const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
254
+ await sleep(delay);
255
+ return this.request<T>(method, path, body, retryCount + 1);
256
+ }
257
+
258
+ throw err;
259
+ }
260
+ }
261
+
262
+ // --------------------------------------------------------------------------
263
+ // Memory operations
264
+ // --------------------------------------------------------------------------
265
+
266
+ async store(
267
+ content: string,
268
+ metadata?: Record<string, string>,
269
+ options?: {
270
+ deduplicate?: boolean;
271
+ dedup_threshold?: number;
272
+ project?: string;
273
+ importance?: number;
274
+ tier?: string;
275
+ scope?: string;
276
+ },
277
+ ): Promise<Memory> {
278
+ // Extract session_id from metadata if present and move to top-level
279
+ const { session_id, ...cleanMetadata } = metadata || {};
280
+
281
+ const payload: any = {
282
+ content,
283
+ agent_id: this.agentId,
284
+ ...options,
285
+ };
286
+
287
+ // Only include metadata if there's something left after extracting session_id
288
+ if (Object.keys(cleanMetadata).length > 0) {
289
+ payload.metadata = cleanMetadata;
290
+ }
291
+
292
+ // Add session_id as top-level parameter if provided
293
+ if (session_id) {
294
+ payload.session_id = session_id;
295
+ }
296
+
297
+ return this.request<Memory>("POST", "/v1/memories", payload);
298
+ }
299
+
300
+ async search(
301
+ query: string,
302
+ limit: number = 5,
303
+ threshold: number = 0.3,
304
+ opts?: {
305
+ include_confidential?: boolean;
306
+ include_archived?: boolean;
307
+ compress?: boolean;
308
+ max_context_tokens?: number;
309
+ project?: string;
310
+ tier?: string;
311
+ min_importance?: number;
312
+ scope?: string;
313
+ session_id?: string;
314
+ namespace?: string;
315
+ },
316
+ ): Promise<SearchResult[]> {
317
+ const params = new URLSearchParams({
318
+ q: query,
319
+ limit: String(limit),
320
+ threshold: String(threshold),
321
+ });
322
+ if (opts?.scope) params.set("scope", opts.scope);
323
+ if (opts?.session_id) params.set("session_id", opts.session_id);
324
+ if (opts?.namespace) params.set("namespace", opts.namespace);
325
+
326
+ // Build POST body from remaining options (existing search contract)
327
+ const { scope, session_id, namespace, ...searchOptions } = opts || {};
328
+
329
+ const response = await this.request<{ data: SearchResult[] }>(
330
+ "POST",
331
+ `/v1/memories/search?${params.toString()}`,
332
+ {
333
+ query,
334
+ limit,
335
+ threshold,
336
+ agent_id: this.agentId,
337
+ ...searchOptions,
338
+ },
339
+ );
340
+ return response.data || [];
341
+ }
342
+
343
+ async list(limit: number = 20, offset: number = 0, opts?: { scope?: string }): Promise<Memory[]> {
344
+ const cappedLimit = Math.min(limit, 100);
345
+ let path = `/v1/memories?limit=${cappedLimit}&offset=${offset}&agent_id=${encodeURIComponent(this.agentId)}`;
346
+ if (opts?.scope) path += `&scope=${encodeURIComponent(opts.scope)}`;
347
+ const response = await this.request<{ data: Memory[] }>(
348
+ "GET",
349
+ path,
350
+ );
351
+ return response.data || [];
352
+ }
353
+
354
+ async get(id: string): Promise<Memory> {
355
+ return this.request<Memory>("GET", `/v1/memories/${id}`);
356
+ }
357
+
358
+ async update(id: string, content: string, metadata?: Record<string, string>): Promise<Memory> {
359
+ return this.request<Memory>("PUT", `/v1/memories/${id}`, {
360
+ content,
361
+ metadata,
362
+ });
363
+ }
364
+
365
+ async delete(id: string): Promise<void> {
366
+ await this.request<void>("DELETE", `/v1/memories/${id}`);
367
+ }
368
+
369
+ async batchStore(
370
+ memories: Array<{ content: string; metadata?: Record<string, string> }>,
371
+ ): Promise<any> {
372
+ return this.request("POST", "/v1/memories/batch", {
373
+ memories,
374
+ agent_id: this.agentId,
375
+ });
376
+ }
377
+
378
+ async buildContext(
379
+ query: string,
380
+ limit?: number,
381
+ threshold?: number,
382
+ maxTokens?: number,
383
+ project?: string,
384
+ ): Promise<any> {
385
+ return this.request("POST", "/v1/memories/context", {
386
+ query,
387
+ limit,
388
+ threshold,
389
+ max_tokens: maxTokens,
390
+ agent_id: this.agentId,
391
+ project,
392
+ });
393
+ }
394
+
395
+ async promote(memoryId: string, importance: number, tier?: string): Promise<any> {
396
+ return this.request("PUT", `/v1/memories/${memoryId}/importance`, {
397
+ importance,
398
+ tier,
399
+ });
400
+ }
401
+
402
+ // --------------------------------------------------------------------------
403
+ // V2 Async API Methods (v0.15.0)
404
+ // --------------------------------------------------------------------------
405
+
406
+ async storeAsync(
407
+ content: string,
408
+ metadata?: Record<string, string>,
409
+ project?: string,
410
+ importance?: number,
411
+ tier?: string,
412
+ webhook_url?: string,
413
+ ): Promise<{ id: string; status: string; job_id: string; estimated_completion_seconds: number }> {
414
+ if (!content || content.length === 0 || content.length > 50000) {
415
+ throw new Error("Content must be between 1 and 50,000 characters");
416
+ }
417
+ const body: Record<string, unknown> = {
418
+ content,
419
+ agent_id: this.agentId,
420
+ };
421
+ if (metadata) body.metadata = metadata;
422
+ if (project) body.project = project;
423
+ if (importance != null) body.importance = importance;
424
+ if (tier) body.tier = tier;
425
+ if (webhook_url) body.webhook_url = webhook_url;
426
+ return this.request("POST", "/v2/memories", body);
427
+ }
428
+
429
+ async getMemoryStatus(memoryId: string): Promise<{
430
+ id: string;
431
+ status: "pending" | "processing" | "ready" | "failed";
432
+ created_at: string;
433
+ updated_at: string;
434
+ error?: string;
435
+ }> {
436
+ return this.request("GET", `/v2/memories/${memoryId}/status`);
437
+ }
438
+
439
+ async buildContextV2(
440
+ query: string,
441
+ options?: {
442
+ maxMemories?: number;
443
+ maxTokens?: number;
444
+ aiEnhanced?: boolean;
445
+ searchMode?: "semantic" | "hybrid" | "keyword";
446
+ excludeMemoryIds?: string[];
447
+ },
448
+ ): Promise<any> {
449
+ const body: Record<string, unknown> = {
450
+ query,
451
+ agent_id: this.agentId,
452
+ };
453
+ if (options?.maxMemories != null) body.max_memories = options.maxMemories;
454
+ if (options?.maxTokens != null) body.max_tokens = options.maxTokens;
455
+ if (options?.aiEnhanced != null) body.ai_enhanced = options.aiEnhanced;
456
+ if (options?.searchMode) body.search_mode = options.searchMode;
457
+ if (options?.excludeMemoryIds) body.exclude_memory_ids = options.excludeMemoryIds;
458
+ return this.request("POST", "/v2/context", body);
459
+ }
460
+
461
+ // --------------------------------------------------------------------------
462
+ // Entity operations
463
+ // --------------------------------------------------------------------------
464
+
465
+ async createEntity(
466
+ name: string,
467
+ type: string,
468
+ metadata?: Record<string, string>,
469
+ ): Promise<any> {
470
+ return this.request("POST", "/v1/entities", {
471
+ name,
472
+ type,
473
+ metadata,
474
+ agent_id: this.agentId,
475
+ });
476
+ }
477
+
478
+ async linkEntity(
479
+ entityId: string,
480
+ memoryId: string,
481
+ relationship?: string,
482
+ ): Promise<any> {
483
+ return this.request("POST", `/v1/entities/links`, {
484
+ entity_id: entityId,
485
+ memory_id: memoryId,
486
+ relationship,
487
+ });
488
+ }
489
+
490
+ async listEntities(limit: number = 20, offset: number = 0): Promise<any> {
491
+ return this.request("GET", `/v1/entities?limit=${limit}&offset=${offset}`);
492
+ }
493
+
494
+ async entityGraph(
495
+ entityId: string,
496
+ depth: number = 2,
497
+ maxNeighbors: number = 10,
498
+ ): Promise<any> {
499
+ return this.request(
500
+ "GET",
501
+ `/v1/entities/${entityId}/neighborhood?depth=${depth}&max_neighbors=${maxNeighbors}`,
502
+ );
503
+ }
504
+
505
+ // --------------------------------------------------------------------------
506
+ // Agent operations
507
+ // --------------------------------------------------------------------------
508
+
509
+ async listAgents(limit: number = 20): Promise<any> {
510
+ return this.request("GET", `/v1/agents?limit=${limit}`);
511
+ }
512
+
513
+ async createAgent(name: string, description?: string): Promise<any> {
514
+ return this.request("POST", "/v1/agents", { name, description });
515
+ }
516
+
517
+ async getAgent(id: string): Promise<any> {
518
+ return this.request("GET", `/v1/agents/${id}`);
519
+ }
520
+
521
+ // --------------------------------------------------------------------------
522
+ // Session operations
523
+ // --------------------------------------------------------------------------
524
+
525
+ async startSession(
526
+ title?: string,
527
+ project?: string,
528
+ metadata?: Record<string, string>,
529
+ ): Promise<any> {
530
+ return this.request("POST", "/v1/sessions", {
531
+ title,
532
+ project,
533
+ metadata,
534
+ agent_id: this.agentId,
535
+ });
536
+ }
537
+
538
+ async getOrCreateSession(
539
+ external_id: string,
540
+ agent_id?: string,
541
+ title?: string,
542
+ project?: string,
543
+ metadata?: Record<string, string>,
544
+ ): Promise<any> {
545
+ return this.request("POST", "/v1/sessions/get-or-create", {
546
+ external_id,
547
+ agent_id: agent_id || this.agentId,
548
+ title,
549
+ project,
550
+ metadata,
551
+ });
552
+ }
553
+
554
+ async endSession(id: string, summary?: string): Promise<any> {
555
+ return this.request("PUT", `/v1/sessions/${id}/end`, { summary });
556
+ }
557
+
558
+ async getSession(id: string): Promise<any> {
559
+ return this.request("GET", `/v1/sessions/${id}`);
560
+ }
561
+
562
+ async listSessions(
563
+ limit: number = 20,
564
+ project?: string,
565
+ status?: string,
566
+ ): Promise<any> {
567
+ let path = `/v1/sessions?limit=${limit}`;
568
+ if (project) path += `&project=${encodeURIComponent(project)}`;
569
+ if (status) path += `&status=${encodeURIComponent(status)}`;
570
+ return this.request("GET", path);
571
+ }
572
+
573
+ // --------------------------------------------------------------------------
574
+ // Decision operations
575
+ // --------------------------------------------------------------------------
576
+
577
+ async recordDecision(
578
+ title: string,
579
+ rationale: string,
580
+ alternatives?: string,
581
+ project?: string,
582
+ tags?: string[],
583
+ status?: string,
584
+ metadata?: Record<string, string>,
585
+ ): Promise<any> {
586
+ return this.request("POST", "/v1/decisions", {
587
+ title,
588
+ rationale,
589
+ alternatives,
590
+ project_slug: project,
591
+ tags,
592
+ status,
593
+ metadata,
594
+ agent_id: this.agentId,
595
+ });
596
+ }
597
+
598
+ async listDecisions(
599
+ limit: number = 20,
600
+ project?: string,
601
+ status?: string,
602
+ tags?: string,
603
+ ): Promise<any> {
604
+ let path = `/v1/decisions?limit=${limit}`;
605
+ if (project) path += `&project=${encodeURIComponent(project)}`;
606
+ if (status) path += `&status=${encodeURIComponent(status)}`;
607
+ if (tags) path += `&tags=${encodeURIComponent(tags)}`;
608
+ return this.request("GET", path);
609
+ }
610
+
611
+ async supersedeDecision(
612
+ id: string,
613
+ title: string,
614
+ rationale: string,
615
+ alternatives?: string,
616
+ tags?: string[],
617
+ ): Promise<any> {
618
+ return this.request("POST", `/v1/decisions/${id}/supersede`, {
619
+ title,
620
+ rationale,
621
+ alternatives,
622
+ tags,
623
+ });
624
+ }
625
+
626
+ async checkDecisions(
627
+ query: string,
628
+ project?: string,
629
+ limit?: number,
630
+ threshold?: number,
631
+ includeSuperseded?: boolean,
632
+ ): Promise<any> {
633
+ const params = new URLSearchParams();
634
+ params.set("query", query);
635
+ if (project) params.set("project", project);
636
+ if (limit !== undefined) params.set("limit", String(limit));
637
+ if (threshold !== undefined) params.set("threshold", String(threshold));
638
+ if (includeSuperseded) params.set("include_superseded", "true");
639
+ return this.request("GET", `/v1/decisions/check?${params.toString()}`);
640
+ }
641
+
642
+ // --------------------------------------------------------------------------
643
+ // Pattern operations
644
+ // --------------------------------------------------------------------------
645
+
646
+ async createPattern(
647
+ title: string,
648
+ description: string,
649
+ category?: string,
650
+ exampleCode?: string,
651
+ scope?: string,
652
+ tags?: string[],
653
+ sourceProject?: string,
654
+ ): Promise<any> {
655
+ return this.request("POST", "/v1/patterns", {
656
+ title,
657
+ description,
658
+ category,
659
+ example_code: exampleCode,
660
+ scope,
661
+ tags,
662
+ source_project: sourceProject,
663
+ });
664
+ }
665
+
666
+ async searchPatterns(
667
+ query: string,
668
+ category?: string,
669
+ project?: string,
670
+ limit?: number,
671
+ threshold?: number,
672
+ ): Promise<any> {
673
+ const params = new URLSearchParams();
674
+ params.set("query", query);
675
+ if (category) params.set("category", category);
676
+ if (project) params.set("project", project);
677
+ if (limit !== undefined) params.set("limit", String(limit));
678
+ if (threshold !== undefined) params.set("threshold", String(threshold));
679
+ return this.request("GET", `/v1/patterns/search?${params.toString()}`);
680
+ }
681
+
682
+ async adoptPattern(id: string, project: string): Promise<any> {
683
+ return this.request("POST", `/v1/patterns/${id}/adopt`, { project });
684
+ }
685
+
686
+ async suggestPatterns(project: string, limit?: number): Promise<any> {
687
+ let path = `/v1/patterns/suggest?project=${encodeURIComponent(project)}`;
688
+ if (limit) path += `&limit=${limit}`;
689
+ return this.request("GET", path);
690
+ }
691
+
692
+ // --------------------------------------------------------------------------
693
+ // Project operations
694
+ // --------------------------------------------------------------------------
695
+
696
+ async registerProject(
697
+ slug: string,
698
+ name: string,
699
+ description?: string,
700
+ stack?: Record<string, unknown>,
701
+ repoUrl?: string,
702
+ ): Promise<any> {
703
+ return this.request("POST", "/v1/projects", {
704
+ slug,
705
+ name,
706
+ description,
707
+ stack,
708
+ repo_url: repoUrl,
709
+ });
710
+ }
711
+
712
+ async listProjects(limit: number = 20): Promise<any> {
713
+ return this.request("GET", `/v1/projects?limit=${limit}`);
714
+ }
715
+
716
+ async getProject(slug: string): Promise<any> {
717
+ return this.request("GET", `/v1/projects/${encodeURIComponent(slug)}`);
718
+ }
719
+
720
+ async addProjectRelationship(
721
+ from: string,
722
+ to: string,
723
+ type: string,
724
+ metadata?: Record<string, unknown>,
725
+ ): Promise<any> {
726
+ return this.request("POST", `/v1/projects/${encodeURIComponent(from)}/relationships`, {
727
+ target_project: to,
728
+ relationship_type: type,
729
+ metadata,
730
+ });
731
+ }
732
+
733
+ async getProjectDependencies(project: string): Promise<any> {
734
+ return this.request(
735
+ "GET",
736
+ `/v1/projects/${encodeURIComponent(project)}/dependencies`,
737
+ );
738
+ }
739
+
740
+ async getProjectDependents(project: string): Promise<any> {
741
+ return this.request(
742
+ "GET",
743
+ `/v1/projects/${encodeURIComponent(project)}/dependents`,
744
+ );
745
+ }
746
+
747
+ async getProjectRelated(project: string): Promise<any> {
748
+ return this.request(
749
+ "GET",
750
+ `/v1/projects/${encodeURIComponent(project)}/related`,
751
+ );
752
+ }
753
+
754
+ async projectImpact(project: string, changeDescription: string): Promise<any> {
755
+ return this.request(
756
+ "POST",
757
+ `/v1/projects/impact-analysis`,
758
+ { project, change_description: changeDescription },
759
+ );
760
+ }
761
+
762
+ async getSharedPatterns(projectA: string, projectB: string): Promise<any> {
763
+ const params = new URLSearchParams();
764
+ params.set("a", projectA);
765
+ params.set("b", projectB);
766
+ return this.request(
767
+ "GET",
768
+ `/v1/projects/shared-patterns?${params.toString()}`,
769
+ );
770
+ }
771
+
772
+ async getProjectContext(project: string): Promise<any> {
773
+ return this.request(
774
+ "GET",
775
+ `/v1/projects/${encodeURIComponent(project)}/context`,
776
+ );
777
+ }
778
+
779
+ // --------------------------------------------------------------------------
780
+ // Health & stats
781
+ // --------------------------------------------------------------------------
782
+
783
+ async health(): Promise<{ status: string }> {
784
+ return this.request<{ status: string }>("GET", "/v1/health");
785
+ }
786
+
787
+ async stats(): Promise<Stats> {
788
+ const response = await this.request<{ data: Stats }>(
789
+ "GET",
790
+ `/v1/agents/${encodeURIComponent(this.agentId)}/stats`,
791
+ );
792
+ return {
793
+ total_memories: response.data?.total_memories ?? 0,
794
+ last_updated: response.data?.last_updated,
795
+ };
796
+ }
797
+
798
+ /**
799
+ * Export all memories as JSON
800
+ */
801
+ async export(): Promise<Memory[]> {
802
+ const allMemories: Memory[] = [];
803
+ let offset = 0;
804
+ const limit = 100;
805
+
806
+ while (true) {
807
+ const batch = await this.list(limit, offset);
808
+ if (batch.length === 0) break;
809
+ allMemories.push(...batch);
810
+ offset += limit;
811
+ if (batch.length < limit) break;
812
+ }
813
+
814
+ return allMemories;
815
+ }
816
+ }