@gethmy/mcp 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +201 -36
  2. package/dist/cli.js +20938 -20249
  3. package/dist/http.js +1957 -0
  4. package/dist/index.js +17833 -17888
  5. package/dist/lib/__tests__/active-learning.test.js +386 -0
  6. package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
  7. package/dist/lib/__tests__/auto-session.test.js +661 -0
  8. package/dist/lib/__tests__/context-assembly.test.js +362 -0
  9. package/dist/lib/__tests__/graph-expansion.test.js +150 -0
  10. package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
  11. package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
  12. package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
  13. package/dist/lib/__tests__/pattern-detection.test.js +295 -0
  14. package/dist/lib/__tests__/prompt-builder.test.js +418 -0
  15. package/dist/lib/active-learning.js +878 -0
  16. package/dist/lib/api-client.js +548 -0
  17. package/dist/lib/auto-session.js +173 -0
  18. package/dist/lib/cli.js +127 -0
  19. package/dist/lib/config.js +205 -0
  20. package/dist/lib/consolidation.js +243 -0
  21. package/dist/lib/context-assembly.js +606 -0
  22. package/dist/lib/graph-expansion.js +163 -0
  23. package/dist/lib/http.js +174 -0
  24. package/dist/lib/index.js +7 -0
  25. package/dist/lib/lifecycle-maintenance.js +88 -0
  26. package/dist/lib/prompt-builder.js +483 -0
  27. package/dist/lib/remote.js +166 -0
  28. package/dist/lib/server.js +3132 -0
  29. package/dist/lib/tui/agents.js +116 -0
  30. package/dist/lib/tui/docs.js +558 -0
  31. package/dist/lib/tui/setup.js +1068 -0
  32. package/dist/lib/tui/theme.js +95 -0
  33. package/dist/lib/tui/writer.js +200 -0
  34. package/dist/remote.js +34534 -0
  35. package/dist/server.js +31967 -0
  36. package/package.json +20 -7
  37. package/src/__tests__/active-learning.test.ts +483 -0
  38. package/src/__tests__/agent-performance-profiles.test.ts +468 -0
  39. package/src/__tests__/auto-session.test.ts +912 -0
  40. package/src/__tests__/context-assembly.test.ts +506 -0
  41. package/src/__tests__/graph-expansion.test.ts +285 -0
  42. package/src/__tests__/integration-memory-crud.test.ts +948 -0
  43. package/src/__tests__/integration-memory-system.test.ts +321 -0
  44. package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
  45. package/src/__tests__/pattern-detection.test.ts +438 -0
  46. package/src/__tests__/prompt-builder.test.ts +505 -0
  47. package/src/active-learning.ts +1227 -0
  48. package/src/api-client.ts +963 -0
  49. package/src/auto-session.ts +218 -0
  50. package/src/cli.ts +166 -0
  51. package/src/config.ts +285 -0
  52. package/src/consolidation.ts +314 -0
  53. package/src/context-assembly.ts +842 -0
  54. package/src/graph-expansion.ts +234 -0
  55. package/src/http.ts +265 -0
  56. package/src/index.ts +8 -0
  57. package/src/lifecycle-maintenance.ts +120 -0
  58. package/src/prompt-builder.ts +681 -0
  59. package/src/remote.ts +227 -0
  60. package/src/server.ts +3858 -0
  61. package/src/tui/agents.ts +154 -0
  62. package/src/tui/docs.ts +650 -0
  63. package/src/tui/setup.ts +1281 -0
  64. package/src/tui/theme.ts +114 -0
  65. package/src/tui/writer.ts +260 -0
@@ -0,0 +1,963 @@
1
+ import { getApiKey, getApiUrl } from "./config.js";
2
+
3
+ export interface ApiResponse<T = unknown> {
4
+ success?: boolean;
5
+ error?: string;
6
+ [key: string]: T | boolean | string | undefined;
7
+ }
8
+
9
+ // Retry configuration
10
+ const RETRY_CONFIG = {
11
+ maxRetries: 3,
12
+ baseDelayMs: 1000,
13
+ maxDelayMs: 10000,
14
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504],
15
+ };
16
+
17
+ // Check if error is retryable
18
+ function isRetryableError(error: unknown, status?: number): boolean {
19
+ if (status && RETRY_CONFIG.retryableStatusCodes.includes(status)) {
20
+ return true;
21
+ }
22
+ if (error instanceof TypeError) return true; // Network errors
23
+ const msg = error instanceof Error ? error.message : String(error);
24
+ return (
25
+ msg.includes("ECONNRESET") ||
26
+ msg.includes("ETIMEDOUT") ||
27
+ msg.includes("fetch failed")
28
+ );
29
+ }
30
+
31
+ // Exponential backoff with jitter
32
+ function getRetryDelay(attempt: number): number {
33
+ const delay = Math.min(
34
+ RETRY_CONFIG.baseDelayMs * 2 ** attempt,
35
+ RETRY_CONFIG.maxDelayMs,
36
+ );
37
+ return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
38
+ }
39
+
40
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
41
+
42
+ // Semaphore for concurrency control
43
+ class Semaphore {
44
+ private permits: number;
45
+ private queue: Array<() => void> = [];
46
+
47
+ constructor(permits: number) {
48
+ this.permits = permits;
49
+ }
50
+
51
+ async acquire(): Promise<void> {
52
+ if (this.permits > 0) {
53
+ this.permits--;
54
+ return;
55
+ }
56
+ return new Promise((resolve) => this.queue.push(resolve));
57
+ }
58
+
59
+ release(): void {
60
+ const next = this.queue.shift();
61
+ if (next) next();
62
+ else this.permits++;
63
+ }
64
+ }
65
+
66
+ // Limit concurrent requests to 3
67
+ const requestSemaphore = new Semaphore(3);
68
+
69
+ // ============ PRE-AUTH FREE FUNCTIONS ============
70
+ // These bypass the singleton + semaphore for unauthenticated/JWT-based requests
71
+
72
+ export async function signupUser(
73
+ apiUrl: string,
74
+ data: { email: string; password: string; full_name: string },
75
+ ): Promise<{
76
+ user: { id: string; email: string; full_name: string };
77
+ session: { access_token: string; refresh_token: string; expires_in: number };
78
+ }> {
79
+ const url = `${apiUrl}/v1/auth/signup`;
80
+ const response = await fetch(url, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify(data),
84
+ });
85
+ const result = await response.json();
86
+ if (!response.ok) {
87
+ throw new Error(result.error || `Signup failed: ${response.status}`);
88
+ }
89
+ return result;
90
+ }
91
+
92
+ export async function requestWithBearer<T = unknown>(
93
+ apiUrl: string,
94
+ bearerToken: string,
95
+ method: string,
96
+ path: string,
97
+ body?: unknown,
98
+ ): Promise<T> {
99
+ const url = `${apiUrl}/v1${path}`;
100
+ const response = await fetch(url, {
101
+ method,
102
+ headers: {
103
+ "Content-Type": "application/json",
104
+ Authorization: `Bearer ${bearerToken}`,
105
+ },
106
+ body: body ? JSON.stringify(body) : undefined,
107
+ });
108
+ const result = await response.json();
109
+ if (!response.ok) {
110
+ throw new Error(result.error || `API error: ${response.status}`);
111
+ }
112
+ return result as T;
113
+ }
114
+
115
+ export class HarmonyApiClient {
116
+ private apiKey: string;
117
+ private apiUrl: string;
118
+
119
+ constructor(options?: { apiKey?: string; apiUrl?: string }) {
120
+ this.apiKey = options?.apiKey ?? getApiKey();
121
+ this.apiUrl = options?.apiUrl ?? getApiUrl();
122
+ }
123
+
124
+ getApiUrl(): string {
125
+ return this.apiUrl;
126
+ }
127
+
128
+ private async request<T>(
129
+ method: string,
130
+ path: string,
131
+ body?: unknown,
132
+ options?: { accept?: string; contentType?: string; rawBody?: string },
133
+ ): Promise<T> {
134
+ await requestSemaphore.acquire();
135
+ try {
136
+ return await this.requestWithRetry<T>(method, path, body, options);
137
+ } finally {
138
+ requestSemaphore.release();
139
+ }
140
+ }
141
+
142
+ private async requestRaw(
143
+ method: string,
144
+ path: string,
145
+ body?: unknown,
146
+ options?: { accept?: string; contentType?: string; rawBody?: string },
147
+ ): Promise<string> {
148
+ await requestSemaphore.acquire();
149
+ try {
150
+ return await this.requestRawWithRetry(method, path, body, options);
151
+ } finally {
152
+ requestSemaphore.release();
153
+ }
154
+ }
155
+
156
+ private async requestWithRetry<T>(
157
+ method: string,
158
+ path: string,
159
+ body?: unknown,
160
+ options?: { accept?: string; contentType?: string; rawBody?: string },
161
+ ): Promise<T> {
162
+ const url = `${this.apiUrl}/v1${path}`;
163
+ let lastError: Error | null = null;
164
+ const contentType = options?.contentType || "application/json";
165
+ const accept = options?.accept || "application/json";
166
+
167
+ for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
168
+ try {
169
+ const response = await fetch(url, {
170
+ method,
171
+ headers: {
172
+ "Content-Type": contentType,
173
+ Accept: accept,
174
+ "X-API-Key": this.apiKey,
175
+ },
176
+ body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined),
177
+ });
178
+
179
+ const data = await response.json();
180
+
181
+ if (!response.ok) {
182
+ const errorMsg = data.error || `API error: ${response.status}`;
183
+ if (!isRetryableError(null, response.status)) {
184
+ throw new Error(errorMsg);
185
+ }
186
+ lastError = new Error(errorMsg);
187
+ if (attempt < RETRY_CONFIG.maxRetries) {
188
+ await sleep(getRetryDelay(attempt));
189
+ continue;
190
+ }
191
+ throw lastError;
192
+ }
193
+
194
+ return data as T;
195
+ } catch (error) {
196
+ lastError = error instanceof Error ? error : new Error(String(error));
197
+ if (!isRetryableError(error)) throw lastError;
198
+ if (attempt < RETRY_CONFIG.maxRetries) {
199
+ await sleep(getRetryDelay(attempt));
200
+ }
201
+ }
202
+ }
203
+
204
+ throw lastError || new Error("Request failed after retries");
205
+ }
206
+
207
+ private async requestRawWithRetry(
208
+ method: string,
209
+ path: string,
210
+ body?: unknown,
211
+ options?: { accept?: string; contentType?: string; rawBody?: string },
212
+ ): Promise<string> {
213
+ const url = `${this.apiUrl}/v1${path}`;
214
+ let lastError: Error | null = null;
215
+ const contentType = options?.contentType || "application/json";
216
+ const accept = options?.accept || "text/markdown";
217
+
218
+ for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
219
+ try {
220
+ const response = await fetch(url, {
221
+ method,
222
+ headers: {
223
+ "Content-Type": contentType,
224
+ Accept: accept,
225
+ "X-API-Key": this.apiKey,
226
+ },
227
+ body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined),
228
+ });
229
+
230
+ if (!response.ok) {
231
+ const text = await response.text();
232
+ let errorMsg: string;
233
+ try {
234
+ errorMsg =
235
+ JSON.parse(text).error || `API error: ${response.status}`;
236
+ } catch {
237
+ errorMsg = text || `API error: ${response.status}`;
238
+ }
239
+ if (!isRetryableError(null, response.status)) {
240
+ throw new Error(errorMsg);
241
+ }
242
+ lastError = new Error(errorMsg);
243
+ if (attempt < RETRY_CONFIG.maxRetries) {
244
+ await sleep(getRetryDelay(attempt));
245
+ continue;
246
+ }
247
+ throw lastError;
248
+ }
249
+
250
+ return await response.text();
251
+ } catch (error) {
252
+ lastError = error instanceof Error ? error : new Error(String(error));
253
+ if (!isRetryableError(error)) throw lastError;
254
+ if (attempt < RETRY_CONFIG.maxRetries) {
255
+ await sleep(getRetryDelay(attempt));
256
+ }
257
+ }
258
+ }
259
+
260
+ throw lastError || new Error("Request failed after retries");
261
+ }
262
+
263
+ // ============ WORKSPACE OPERATIONS ============
264
+
265
+ async listWorkspaces(): Promise<{ workspaces: unknown[] }> {
266
+ return this.request("GET", "/workspaces");
267
+ }
268
+
269
+ async getWorkspaceMembers(
270
+ workspaceId: string,
271
+ ): Promise<{ members: unknown[] }> {
272
+ return this.request("GET", `/workspaces/${workspaceId}/members`);
273
+ }
274
+
275
+ // ============ PROJECT OPERATIONS ============
276
+
277
+ async listProjects(workspaceId: string): Promise<{ projects: unknown[] }> {
278
+ return this.request("GET", `/workspaces/${workspaceId}/projects`);
279
+ }
280
+
281
+ async getBoard(
282
+ projectId: string,
283
+ options?: {
284
+ limit?: number;
285
+ offset?: number;
286
+ columnId?: string;
287
+ summary?: boolean;
288
+ includeArchived?: boolean;
289
+ },
290
+ ): Promise<{
291
+ project: unknown;
292
+ columns: unknown[];
293
+ cards?: unknown[];
294
+ labels: unknown[];
295
+ totalCards?: number;
296
+ pagination?: {
297
+ limit: number;
298
+ offset: number;
299
+ totalCards: number;
300
+ hasMore: boolean;
301
+ };
302
+ summary?: boolean;
303
+ }> {
304
+ const params = new URLSearchParams();
305
+ if (options?.limit !== undefined)
306
+ params.set("limit", String(options.limit));
307
+ if (options?.offset !== undefined)
308
+ params.set("offset", String(options.offset));
309
+ if (options?.columnId) params.set("column_id", options.columnId);
310
+ if (options?.summary) params.set("summary", "true");
311
+ if (options?.includeArchived) params.set("include_archived", "true");
312
+
313
+ const query = params.toString() ? `?${params.toString()}` : "";
314
+ return this.request("GET", `/board/${projectId}${query}`);
315
+ }
316
+
317
+ // ============ CARD OPERATIONS ============
318
+
319
+ async createCard(
320
+ projectId: string,
321
+ data: {
322
+ title: string;
323
+ columnId?: string;
324
+ description?: string;
325
+ priority?: string;
326
+ assigneeId?: string;
327
+ },
328
+ ): Promise<{ card: unknown }> {
329
+ return this.request("POST", "/cards", { projectId, ...data });
330
+ }
331
+
332
+ async updateCard(
333
+ cardId: string,
334
+ updates: {
335
+ title?: string;
336
+ description?: string;
337
+ priority?: string;
338
+ assigneeId?: string | null;
339
+ dueDate?: string | null;
340
+ done?: boolean;
341
+ archivedAt?: string | null;
342
+ },
343
+ ): Promise<{ card: unknown }> {
344
+ return this.request("PATCH", `/cards/${cardId}`, updates);
345
+ }
346
+
347
+ async moveCard(
348
+ cardId: string,
349
+ columnId: string,
350
+ position?: number,
351
+ ): Promise<{ card: unknown }> {
352
+ return this.request("POST", `/cards/${cardId}/move`, {
353
+ columnId,
354
+ position,
355
+ });
356
+ }
357
+
358
+ async archiveCard(cardId: string): Promise<{ card: unknown }> {
359
+ return this.updateCard(cardId, { archivedAt: new Date().toISOString() });
360
+ }
361
+
362
+ async unarchiveCard(cardId: string): Promise<{ card: unknown }> {
363
+ return this.updateCard(cardId, { archivedAt: null });
364
+ }
365
+
366
+ async deleteCard(cardId: string): Promise<{ success: boolean }> {
367
+ return this.request("DELETE", `/cards/${cardId}`);
368
+ }
369
+
370
+ async getCard(cardId: string): Promise<{ card: unknown }> {
371
+ return this.request("GET", `/cards/${cardId}`);
372
+ }
373
+
374
+ async getCardByShortId(
375
+ projectId: string,
376
+ shortId: number,
377
+ ): Promise<{ card: unknown }> {
378
+ return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
379
+ }
380
+
381
+ async searchCards(
382
+ query: string,
383
+ options?: { projectId?: string },
384
+ ): Promise<{ cards: unknown[] }> {
385
+ const params = new URLSearchParams({ q: query });
386
+ if (options?.projectId) {
387
+ params.set("project_id", options.projectId);
388
+ }
389
+ return this.request("GET", `/search?${params.toString()}`);
390
+ }
391
+
392
+ async addLabelToCard(
393
+ cardId: string,
394
+ labelId: string,
395
+ ): Promise<{ success: boolean }> {
396
+ return this.request("POST", `/cards/${cardId}/labels`, { labelId });
397
+ }
398
+
399
+ async removeLabelFromCard(
400
+ cardId: string,
401
+ labelId: string,
402
+ ): Promise<{ success: boolean }> {
403
+ return this.request("DELETE", `/cards/${cardId}/labels/${labelId}`);
404
+ }
405
+
406
+ // ============ CARD LINK OPERATIONS ============
407
+
408
+ async addLinkToCard(
409
+ sourceCardId: string,
410
+ targetCardId: string,
411
+ linkType: "relates_to" | "blocks" | "duplicates" | "is_part_of",
412
+ ): Promise<{ link: unknown }> {
413
+ return this.request("POST", `/cards/${sourceCardId}/links`, {
414
+ targetCardId,
415
+ linkType,
416
+ });
417
+ }
418
+
419
+ async removeLinkFromCard(linkId: string): Promise<{ success: boolean }> {
420
+ return this.request("DELETE", `/card-links/${linkId}`);
421
+ }
422
+
423
+ async getCardLinks(cardId: string): Promise<{ links: unknown[] }> {
424
+ return this.request("GET", `/cards/${cardId}/links`);
425
+ }
426
+
427
+ // ============ COLUMN OPERATIONS ============
428
+
429
+ async createColumn(
430
+ projectId: string,
431
+ name: string,
432
+ ): Promise<{ column: unknown }> {
433
+ return this.request("POST", "/columns", { projectId, name });
434
+ }
435
+
436
+ async updateColumn(
437
+ columnId: string,
438
+ name: string,
439
+ ): Promise<{ column: unknown }> {
440
+ return this.request("PATCH", `/columns/${columnId}`, { name });
441
+ }
442
+
443
+ async deleteColumn(columnId: string): Promise<{ success: boolean }> {
444
+ return this.request("DELETE", `/columns/${columnId}`);
445
+ }
446
+
447
+ // ============ LABEL OPERATIONS ============
448
+
449
+ async createLabel(
450
+ projectId: string,
451
+ data: { name: string; color: string },
452
+ ): Promise<{ label: unknown }> {
453
+ return this.request("POST", "/labels", { projectId, ...data });
454
+ }
455
+
456
+ // ============ SUBTASK OPERATIONS ============
457
+
458
+ async createSubtask(
459
+ cardId: string,
460
+ title: string,
461
+ ): Promise<{ subtask: unknown }> {
462
+ return this.request("POST", "/subtasks", { cardId, title });
463
+ }
464
+
465
+ async toggleSubtask(subtaskId: string): Promise<{ subtask: unknown }> {
466
+ return this.request("POST", `/subtasks/${subtaskId}/toggle`);
467
+ }
468
+
469
+ async deleteSubtask(subtaskId: string): Promise<{ success: boolean }> {
470
+ return this.request("DELETE", `/subtasks/${subtaskId}`);
471
+ }
472
+
473
+ // ============ AGENT CONTEXT OPERATIONS ============
474
+
475
+ async startAgentSession(
476
+ cardId: string,
477
+ data: {
478
+ agentIdentifier: string;
479
+ agentName: string;
480
+ status?: "working" | "blocked" | "paused" | "completed";
481
+ progressPercent?: number;
482
+ currentTask?: string;
483
+ blockers?: string[];
484
+ estimatedMinutesRemaining?: number;
485
+ },
486
+ ): Promise<{ session: unknown; created: boolean }> {
487
+ return this.request("POST", `/cards/${cardId}/agent-context`, data);
488
+ }
489
+
490
+ async updateAgentProgress(
491
+ cardId: string,
492
+ data: {
493
+ agentIdentifier: string;
494
+ agentName: string;
495
+ status?: "working" | "blocked" | "paused" | "completed";
496
+ progressPercent?: number;
497
+ currentTask?: string;
498
+ blockers?: string[];
499
+ estimatedMinutesRemaining?: number;
500
+ },
501
+ ): Promise<{ session: unknown; created: boolean }> {
502
+ return this.request("POST", `/cards/${cardId}/agent-context`, data);
503
+ }
504
+
505
+ async endAgentSession(
506
+ cardId: string,
507
+ data?: {
508
+ status?: "completed" | "paused";
509
+ progressPercent?: number;
510
+ },
511
+ ): Promise<{ session: unknown }> {
512
+ return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
513
+ }
514
+
515
+ async getAgentSession(
516
+ cardId: string,
517
+ options?: { includeEnded?: boolean },
518
+ ): Promise<{ session?: unknown; sessions?: unknown[] }> {
519
+ const params = new URLSearchParams();
520
+ if (options?.includeEnded) params.set("include_ended", "true");
521
+ const query = params.toString() ? `?${params.toString()}` : "";
522
+ return this.request("GET", `/cards/${cardId}/agent-context${query}`);
523
+ }
524
+
525
+ // ============ AGENT PERFORMANCE PROFILES ============
526
+
527
+ async getAgentProfile(
528
+ workspaceId: string,
529
+ agentIdentifier: string,
530
+ ): Promise<{ profile: unknown }> {
531
+ const params = new URLSearchParams({
532
+ workspace_id: workspaceId,
533
+ agent_identifier: agentIdentifier,
534
+ });
535
+ return this.request("GET", `/agent-profiles?${params.toString()}`);
536
+ }
537
+
538
+ async listAgentProfiles(
539
+ workspaceId: string,
540
+ ): Promise<{ profiles: unknown[] }> {
541
+ const params = new URLSearchParams({ workspace_id: workspaceId });
542
+ return this.request("GET", `/agent-profiles?${params.toString()}`);
543
+ }
544
+
545
+ async refreshAgentProfiles(
546
+ workspaceId: string,
547
+ ): Promise<{ refreshed: boolean }> {
548
+ return this.request("POST", "/agent-profiles/refresh", {
549
+ workspace_id: workspaceId,
550
+ });
551
+ }
552
+
553
+ // ============ MEMORY OPERATIONS ============
554
+
555
+ async createMemoryEntity(data: {
556
+ workspace_id: string;
557
+ project_id?: string;
558
+ type: string;
559
+ scope?: string;
560
+ memory_tier?: string;
561
+ title: string;
562
+ content: string;
563
+ metadata?: Record<string, unknown>;
564
+ confidence?: number;
565
+ tags?: string[];
566
+ agent_identifier?: string;
567
+ }): Promise<{ entity: unknown; warnings?: string[] }> {
568
+ return this.request("POST", "/memory/entities", data);
569
+ }
570
+
571
+ async listMemoryEntities(options: {
572
+ workspace_id: string;
573
+ project_id?: string;
574
+ type?: string;
575
+ scope?: string;
576
+ tags?: string[];
577
+ agent_identifier?: string;
578
+ min_confidence?: number;
579
+ q?: string;
580
+ limit?: number;
581
+ offset?: number;
582
+ }): Promise<{ entities: unknown[]; count: number }> {
583
+ const params = new URLSearchParams();
584
+ params.set("workspace_id", options.workspace_id);
585
+ if (options.project_id) params.set("project_id", options.project_id);
586
+ if (options.type) params.set("type", options.type);
587
+ if (options.scope) params.set("scope", options.scope);
588
+ if (options.tags?.length) params.set("tags", options.tags.join(","));
589
+ if (options.agent_identifier)
590
+ params.set("agent_identifier", options.agent_identifier);
591
+ if (options.min_confidence !== undefined)
592
+ params.set("min_confidence", String(options.min_confidence));
593
+ if (options.q) params.set("q", options.q);
594
+ if (options.limit !== undefined) params.set("limit", String(options.limit));
595
+ if (options.offset !== undefined)
596
+ params.set("offset", String(options.offset));
597
+ return this.request("GET", `/memory/entities?${params.toString()}`);
598
+ }
599
+
600
+ async getMemoryEntity(entityId: string): Promise<{ entity: unknown }> {
601
+ return this.request("GET", `/memory/entities/${entityId}`);
602
+ }
603
+
604
+ async updateMemoryEntity(
605
+ entityId: string,
606
+ updates: {
607
+ title?: string;
608
+ content?: string;
609
+ metadata?: Record<string, unknown>;
610
+ confidence?: number;
611
+ tags?: string[];
612
+ scope?: string;
613
+ type?: string;
614
+ memory_tier?: string;
615
+ },
616
+ ): Promise<{ entity: unknown; warnings?: string[] }> {
617
+ return this.request("PUT", `/memory/entities/${entityId}`, updates);
618
+ }
619
+
620
+ async deleteMemoryEntity(entityId: string): Promise<{ success: boolean }> {
621
+ return this.request("DELETE", `/memory/entities/${entityId}`);
622
+ }
623
+
624
+ async touchMemoryEntity(entityId: string): Promise<{ success: boolean }> {
625
+ return this.request("POST", `/memory/entities/${entityId}/touch`);
626
+ }
627
+
628
+ async batchTouchMemoryEntities(
629
+ entityIds: string[],
630
+ ): Promise<{ success: boolean; count: number }> {
631
+ return this.request("POST", "/memory/entities/batch-touch", {
632
+ entity_ids: entityIds,
633
+ });
634
+ }
635
+
636
+ async createMemoryRelation(data: {
637
+ source_id: string;
638
+ target_id: string;
639
+ relation_type: string;
640
+ confidence?: number;
641
+ }): Promise<{ relation: unknown }> {
642
+ return this.request("POST", "/memory/relations", data);
643
+ }
644
+
645
+ async deleteMemoryRelation(
646
+ relationId: string,
647
+ ): Promise<{ success: boolean }> {
648
+ return this.request("DELETE", `/memory/relations/${relationId}`);
649
+ }
650
+
651
+ async getRelatedEntities(
652
+ entityId: string,
653
+ ): Promise<{ outgoing: unknown[]; incoming: unknown[] }> {
654
+ return this.request("GET", `/memory/entities/${entityId}/related`);
655
+ }
656
+
657
+ async searchMemoryEntities(
658
+ workspaceId: string,
659
+ query: string,
660
+ options?: {
661
+ project_id?: string;
662
+ type?: string;
663
+ limit?: number;
664
+ },
665
+ ): Promise<{ entities: unknown[]; count: number }> {
666
+ const params = new URLSearchParams();
667
+ params.set("workspace_id", workspaceId);
668
+ params.set("q", query);
669
+ if (options?.project_id) params.set("project_id", options.project_id);
670
+ if (options?.type) params.set("type", options.type);
671
+ if (options?.limit !== undefined)
672
+ params.set("limit", String(options.limit));
673
+ return this.request("GET", `/memory/search?${params.toString()}`);
674
+ }
675
+
676
+ // ============ VAULT INDEX ============
677
+
678
+ async getVaultIndex(options: {
679
+ workspace_id: string;
680
+ project_id?: string;
681
+ type?: string;
682
+ limit?: number;
683
+ }): Promise<{ entities: unknown[]; count: number }> {
684
+ const params = new URLSearchParams();
685
+ params.set("workspace_id", options.workspace_id);
686
+ if (options.project_id) params.set("project_id", options.project_id);
687
+ if (options.type) params.set("type", options.type);
688
+ if (options.limit !== undefined) params.set("limit", String(options.limit));
689
+ return this.request("GET", `/memory/index?${params.toString()}`);
690
+ }
691
+
692
+ async getVaultIndexMarkdown(options: {
693
+ workspace_id: string;
694
+ project_id?: string;
695
+ type?: string;
696
+ limit?: number;
697
+ }): Promise<string> {
698
+ const params = new URLSearchParams();
699
+ params.set("workspace_id", options.workspace_id);
700
+ if (options.project_id) params.set("project_id", options.project_id);
701
+ if (options.type) params.set("type", options.type);
702
+ if (options.limit !== undefined) params.set("limit", String(options.limit));
703
+ return this.requestRaw(
704
+ "GET",
705
+ `/memory/index?${params.toString()}`,
706
+ undefined,
707
+ {
708
+ accept: "text/markdown",
709
+ },
710
+ );
711
+ }
712
+
713
+ // ============ ENTITY STATS (materialized view) ============
714
+
715
+ async getMemoryStats(options: {
716
+ workspace_id: string;
717
+ project_id?: string;
718
+ }): Promise<{ stats: unknown[] }> {
719
+ const params = new URLSearchParams();
720
+ params.set("workspace_id", options.workspace_id);
721
+ if (options.project_id) params.set("project_id", options.project_id);
722
+ return this.request("GET", `/memory/stats?${params.toString()}`);
723
+ }
724
+
725
+ async refreshMemoryStats(workspaceId: string): Promise<{ success: boolean }> {
726
+ return this.request("POST", "/memory/stats", { workspace_id: workspaceId });
727
+ }
728
+
729
+ // ============ WIKI-LINK RESOLUTION ============
730
+
731
+ async resolveLinks(options: {
732
+ workspace_id: string;
733
+ project_id?: string;
734
+ }): Promise<{ resolved: number; unresolved: number }> {
735
+ return this.request("POST", "/memory/resolve-links", options);
736
+ }
737
+
738
+ // ============ MARKDOWN MEMORY OPERATIONS ============
739
+
740
+ async listMemoryEntitiesMarkdown(options: {
741
+ workspace_id: string;
742
+ project_id?: string;
743
+ type?: string;
744
+ scope?: string;
745
+ tags?: string[];
746
+ agent_identifier?: string;
747
+ min_confidence?: number;
748
+ q?: string;
749
+ limit?: number;
750
+ offset?: number;
751
+ }): Promise<string> {
752
+ const params = new URLSearchParams();
753
+ params.set("workspace_id", options.workspace_id);
754
+ if (options.project_id) params.set("project_id", options.project_id);
755
+ if (options.type) params.set("type", options.type);
756
+ if (options.scope) params.set("scope", options.scope);
757
+ if (options.tags?.length) params.set("tags", options.tags.join(","));
758
+ if (options.agent_identifier)
759
+ params.set("agent_identifier", options.agent_identifier);
760
+ if (options.min_confidence !== undefined)
761
+ params.set("min_confidence", String(options.min_confidence));
762
+ if (options.q) params.set("q", options.q);
763
+ if (options.limit !== undefined) params.set("limit", String(options.limit));
764
+ if (options.offset !== undefined)
765
+ params.set("offset", String(options.offset));
766
+ return this.requestRaw(
767
+ "GET",
768
+ `/memory/entities?${params.toString()}`,
769
+ undefined,
770
+ {
771
+ accept: "text/markdown",
772
+ },
773
+ );
774
+ }
775
+
776
+ async getMemoryEntityMarkdown(entityId: string): Promise<string> {
777
+ return this.requestRaw("GET", `/memory/entities/${entityId}`, undefined, {
778
+ accept: "text/markdown",
779
+ });
780
+ }
781
+
782
+ async searchMemoryEntitiesMarkdown(
783
+ workspaceId: string,
784
+ query: string,
785
+ options?: {
786
+ project_id?: string;
787
+ type?: string;
788
+ limit?: number;
789
+ },
790
+ ): Promise<string> {
791
+ const params = new URLSearchParams();
792
+ params.set("workspace_id", workspaceId);
793
+ params.set("q", query);
794
+ if (options?.project_id) params.set("project_id", options.project_id);
795
+ if (options?.type) params.set("type", options.type);
796
+ if (options?.limit !== undefined)
797
+ params.set("limit", String(options.limit));
798
+ return this.requestRaw(
799
+ "GET",
800
+ `/memory/search?${params.toString()}`,
801
+ undefined,
802
+ {
803
+ accept: "text/markdown",
804
+ },
805
+ );
806
+ }
807
+
808
+ // ============ EMBEDDING OPERATIONS ============
809
+
810
+ async backfillEmbeddings(
811
+ workspaceId: string,
812
+ batchSize?: number,
813
+ ): Promise<{
814
+ processed: number;
815
+ remaining: number;
816
+ errors: Array<{ entity_id: string; error: string }>;
817
+ }> {
818
+ return this.request("POST", "/memory/backfill-embeddings", {
819
+ workspace_id: workspaceId,
820
+ batch_size: batchSize || 50,
821
+ });
822
+ }
823
+
824
+ // ============ NLU OPERATIONS ============
825
+
826
+ async processNLU(data: {
827
+ command: string;
828
+ projectId?: string;
829
+ workspaceId?: string;
830
+ language?: string;
831
+ execute?: boolean;
832
+ }): Promise<unknown> {
833
+ return this.request("POST", "/nlu", data);
834
+ }
835
+
836
+ // ============ PLAN OPERATIONS ============
837
+
838
+ async createPlan(
839
+ projectId: string,
840
+ data: {
841
+ title: string;
842
+ content?: string;
843
+ source?: "user" | "agent" | "imported";
844
+ tasks?: Array<{
845
+ content: string;
846
+ priority?: "high" | "medium" | "low";
847
+ status?: "pending" | "in_progress" | "completed";
848
+ }>;
849
+ },
850
+ ): Promise<{ plan: unknown; tasks?: unknown[] }> {
851
+ return this.request("POST", "/plans", { projectId, ...data });
852
+ }
853
+
854
+ async listPlans(
855
+ projectId: string,
856
+ options?: {
857
+ search?: string;
858
+ status?: string;
859
+ },
860
+ ): Promise<{ plans: unknown[] }> {
861
+ const params = new URLSearchParams({ projectId });
862
+ if (options?.search) params.set("search", options.search);
863
+ if (options?.status) params.set("status", options.status);
864
+ return this.request("GET", `/plans?${params.toString()}`);
865
+ }
866
+
867
+ async getPlan(planId: string): Promise<{
868
+ plan: unknown;
869
+ tasks: unknown[];
870
+ }> {
871
+ return this.request("GET", `/plans/${planId}`);
872
+ }
873
+
874
+ async getPlanByCardId(cardId: string): Promise<{
875
+ plan: unknown;
876
+ tasks: unknown[];
877
+ } | null> {
878
+ return this.request("GET", `/cards/${cardId}/plan`);
879
+ }
880
+
881
+ async updatePlan(
882
+ planId: string,
883
+ updates: {
884
+ title?: string;
885
+ content?: string;
886
+ status?: "draft" | "active" | "archived";
887
+ },
888
+ ): Promise<{ plan: unknown }> {
889
+ return this.request("PATCH", `/plans/${planId}`, updates);
890
+ }
891
+
892
+ async updatePlanTask(
893
+ planId: string,
894
+ taskId: string,
895
+ updates: {
896
+ cardId?: string;
897
+ status?: "pending" | "in_progress" | "completed";
898
+ },
899
+ ): Promise<{ task: unknown }> {
900
+ return this.request("PATCH", `/plans/${planId}/tasks/${taskId}`, updates);
901
+ }
902
+
903
+ // ============ ONBOARDING OPERATIONS ============
904
+
905
+ async createWorkspace(data: { name: string; description?: string }): Promise<{
906
+ workspace: { id: string; name: string; slug: string; created_at: string };
907
+ }> {
908
+ return this.request("POST", "/workspaces", data);
909
+ }
910
+
911
+ async createProject(data: {
912
+ workspaceId: string;
913
+ name: string;
914
+ description?: string;
915
+ color?: string;
916
+ template?: string;
917
+ }): Promise<{
918
+ project: { id: string; name: string; slug: string };
919
+ columns: unknown[];
920
+ }> {
921
+ return this.request("POST", "/projects", data);
922
+ }
923
+
924
+ async sendInvitations(data: {
925
+ workspaceId: string;
926
+ emails: string[];
927
+ role?: string;
928
+ sendEmail?: boolean;
929
+ }): Promise<{
930
+ results: {
931
+ email: string;
932
+ success: boolean;
933
+ invitationId?: string;
934
+ error?: string;
935
+ }[];
936
+ totalSent: number;
937
+ totalFailed: number;
938
+ }> {
939
+ return this.request("POST", "/invitations", data);
940
+ }
941
+
942
+ async generateApiKey(name: string): Promise<{
943
+ apiKey: { id: string; name: string; prefix: string; created_at: string };
944
+ rawKey: string;
945
+ warning: string;
946
+ }> {
947
+ return this.request("POST", "/api-keys", { name });
948
+ }
949
+ }
950
+
951
+ // Singleton instance
952
+ let client: HarmonyApiClient | null = null;
953
+
954
+ export function getClient(): HarmonyApiClient {
955
+ if (!client) {
956
+ client = new HarmonyApiClient();
957
+ }
958
+ return client;
959
+ }
960
+
961
+ export function resetClient(): void {
962
+ client = null;
963
+ }