@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,163 @@
1
+ /**
2
+ * Auto Knowledge Graph Expansion
3
+ *
4
+ * When a new entity is created, find semantically similar existing entities
5
+ * and automatically create `relates_to` relations between them.
6
+ */
7
+ /**
8
+ * Automatically expand the knowledge graph by linking a newly-created entity
9
+ * to semantically similar existing entities via `relates_to` relations.
10
+ *
11
+ * Non-fatal: errors are caught and silently ignored so callers are never blocked.
12
+ */
13
+ export async function autoExpandGraph(client, entityId, title, content, _tags, workspaceId, projectId, maxRelations = 5) {
14
+ try {
15
+ // Build a search query from the title + first 200 chars of content
16
+ const contentSnippet = content.slice(0, 200).trim();
17
+ const query = [title, contentSnippet].filter(Boolean).join(" ");
18
+ let candidates = [];
19
+ // First attempt
20
+ const { entities } = await client.searchMemoryEntities(workspaceId, query, {
21
+ project_id: projectId,
22
+ limit: 20,
23
+ });
24
+ candidates = entities
25
+ .filter((e) => e.id !== entityId)
26
+ .slice(0, maxRelations);
27
+ // Retry once after 2s if no candidates found (handles embedding generation race)
28
+ if (candidates.length === 0) {
29
+ await new Promise((resolve) => setTimeout(resolve, 2000));
30
+ const retry = await client.searchMemoryEntities(workspaceId, query, {
31
+ project_id: projectId,
32
+ limit: 20,
33
+ });
34
+ candidates = retry.entities
35
+ .filter((e) => e.id !== entityId)
36
+ .slice(0, maxRelations);
37
+ }
38
+ let relationsCreated = 0;
39
+ for (const candidate of candidates) {
40
+ try {
41
+ await client.createMemoryRelation({
42
+ source_id: entityId,
43
+ target_id: candidate.id,
44
+ relation_type: "relates_to",
45
+ confidence: 0.6,
46
+ });
47
+ relationsCreated++;
48
+ }
49
+ catch (err) {
50
+ // Silently skip 409 Conflict (relation already exists) and other errors
51
+ const status = err?.status;
52
+ if (status !== 409) {
53
+ // Non-409 errors are still non-fatal; just skip
54
+ }
55
+ }
56
+ }
57
+ return { relationsCreated };
58
+ }
59
+ catch {
60
+ // Never block callers due to graph expansion failures
61
+ return { relationsCreated: 0 };
62
+ }
63
+ }
64
+ /**
65
+ * Find entities semantically similar to the given title+content using
66
+ * hybrid FTS+vector search. Returns entities sorted by relevance.
67
+ */
68
+ export async function findSimilarEntities(client, title, content, workspaceId, options) {
69
+ const contentSnippet = content.slice(0, 200).trim();
70
+ const query = [title, contentSnippet].filter(Boolean).join(" ");
71
+ try {
72
+ const { entities } = await client.searchMemoryEntities(workspaceId, query, {
73
+ project_id: options?.projectId,
74
+ limit: options?.limit ?? 20,
75
+ type: options?.type,
76
+ });
77
+ const minScore = options?.minRrfScore ?? 0;
78
+ const excludeSet = new Set(options?.excludeIds || []);
79
+ return entities.filter((e) => {
80
+ if (excludeSet.has(e.id))
81
+ return false;
82
+ if (minScore > 0 && (e.rrf_score ?? 0) < minScore)
83
+ return false;
84
+ return true;
85
+ });
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ }
91
+ /**
92
+ * Causal lookup table: maps an entity type to the target types it should
93
+ * be linked to, along with the relation type and direction.
94
+ *
95
+ * "forward" means source=newEntity → target=match.
96
+ * "reverse" means source=match → target=newEntity.
97
+ */
98
+ const CAUSAL_LOOKUP = [
99
+ {
100
+ sourceType: "error",
101
+ targetType: "solution",
102
+ relation: "resolved_by",
103
+ direction: "forward",
104
+ },
105
+ {
106
+ sourceType: "solution",
107
+ targetType: "error",
108
+ relation: "resolved_by",
109
+ direction: "reverse",
110
+ },
111
+ {
112
+ sourceType: "lesson",
113
+ targetType: "error",
114
+ relation: "learned_from",
115
+ direction: "forward",
116
+ },
117
+ ];
118
+ /**
119
+ * Link a newly created entity to causally-related entities of *other* types.
120
+ *
121
+ * For example, a new `error` entity gets linked to similar `solution` entities
122
+ * via `resolved_by`, enabling agents to discover "what solved similar problems."
123
+ *
124
+ * Non-fatal: all errors are caught silently.
125
+ */
126
+ export async function linkCrossTypeNeighbors(client, entityId, entityType, title, content, workspaceId, projectId) {
127
+ const rules = CAUSAL_LOOKUP.filter((r) => r.sourceType === entityType);
128
+ if (rules.length === 0)
129
+ return { relationsCreated: 0 };
130
+ let relationsCreated = 0;
131
+ for (const rule of rules) {
132
+ try {
133
+ const matches = await findSimilarEntities(client, title, content, workspaceId, {
134
+ projectId,
135
+ limit: 10,
136
+ minRrfScore: 0.04,
137
+ excludeIds: [entityId],
138
+ type: rule.targetType,
139
+ });
140
+ // Cap at 3 matches per target type
141
+ for (const match of matches.slice(0, 3)) {
142
+ const sourceId = rule.direction === "forward" ? entityId : match.id;
143
+ const targetId = rule.direction === "forward" ? match.id : entityId;
144
+ try {
145
+ await client.createMemoryRelation({
146
+ source_id: sourceId,
147
+ target_id: targetId,
148
+ relation_type: rule.relation,
149
+ confidence: 0.65,
150
+ });
151
+ relationsCreated++;
152
+ }
153
+ catch {
154
+ // Skip duplicate/failed relations silently
155
+ }
156
+ }
157
+ }
158
+ catch {
159
+ // Non-fatal: search failure for one rule shouldn't block others
160
+ }
161
+ }
162
+ return { relationsCreated };
163
+ }
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HTTP REST Adapter for Harmony MCP
4
+ *
5
+ * This provides a REST API that forwards requests to the Harmony API.
6
+ * It accepts Bearer tokens (JWT) or X-API-Key headers for authentication.
7
+ *
8
+ * This adapter is useful for non-MCP clients that want to interact with
9
+ * Harmony via HTTP instead of the MCP stdio protocol.
10
+ */
11
+ import { serve } from "bun";
12
+ import { Hono } from "hono";
13
+ import { cors } from "hono/cors";
14
+ import { loadConfig } from "./config.js";
15
+ const app = new Hono();
16
+ // CORS configuration
17
+ app.use("/*", cors({
18
+ origin: [
19
+ "https://gethmy.com",
20
+ "http://localhost:8080",
21
+ "http://localhost:3000",
22
+ ],
23
+ allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
24
+ allowHeaders: ["Content-Type", "Authorization", "X-API-Key"],
25
+ }));
26
+ // Get API URL from config or use default
27
+ function getApiUrl() {
28
+ const config = loadConfig();
29
+ return config.apiUrl;
30
+ }
31
+ // Forward request to Harmony API
32
+ async function forwardToApi(method, path, authHeader, apiKey, body) {
33
+ const apiUrl = getApiUrl();
34
+ const headers = {
35
+ "Content-Type": "application/json",
36
+ };
37
+ // Use API key if provided, otherwise forward the Authorization header
38
+ if (apiKey) {
39
+ headers["X-API-Key"] = apiKey;
40
+ }
41
+ else if (authHeader) {
42
+ headers.Authorization = authHeader;
43
+ }
44
+ else {
45
+ throw new Error("Unauthorized: Missing API key or Authorization header");
46
+ }
47
+ const response = await fetch(`${apiUrl}${path}`, {
48
+ method,
49
+ headers,
50
+ body: body ? JSON.stringify(body) : undefined,
51
+ });
52
+ const data = await response.json();
53
+ return { data, status: response.status };
54
+ }
55
+ // Health check
56
+ app.get("/health", (c) => c.json({ status: "ok", service: "harmony-mcp-http" }));
57
+ // List available endpoints
58
+ app.get("/", (c) => c.json({
59
+ service: "Harmony MCP HTTP Adapter",
60
+ endpoints: {
61
+ workspaces: {
62
+ list: "GET /workspaces",
63
+ members: "GET /workspaces/:id/members",
64
+ projects: "GET /workspaces/:id/projects",
65
+ },
66
+ board: {
67
+ get: "GET /board/:projectId",
68
+ },
69
+ cards: {
70
+ create: "POST /cards",
71
+ get: "GET /cards/:id",
72
+ getByShortId: "GET /projects/:projectId/cards/:shortId",
73
+ update: "PATCH /cards/:id",
74
+ delete: "DELETE /cards/:id",
75
+ move: "POST /cards/:id/move",
76
+ search: "GET /search?q=query",
77
+ },
78
+ columns: {
79
+ create: "POST /columns",
80
+ update: "PATCH /columns/:id",
81
+ delete: "DELETE /columns/:id",
82
+ },
83
+ labels: {
84
+ create: "POST /labels",
85
+ addToCard: "POST /cards/:id/labels",
86
+ removeFromCard: "DELETE /cards/:id/labels/:labelId",
87
+ },
88
+ subtasks: {
89
+ create: "POST /subtasks",
90
+ toggle: "POST /subtasks/:id/toggle",
91
+ delete: "DELETE /subtasks/:id",
92
+ },
93
+ nlu: {
94
+ process: "POST /nlu",
95
+ },
96
+ },
97
+ authentication: "Include X-API-Key header or Authorization: Bearer <token>",
98
+ }));
99
+ // Generic proxy handler
100
+ async function handleRequest(c, method, path) {
101
+ try {
102
+ const authHeader = c.req.header("Authorization");
103
+ const apiKey = c.req.header("X-API-Key");
104
+ let body;
105
+ if (["POST", "PATCH", "PUT"].includes(method)) {
106
+ try {
107
+ body = await c.json();
108
+ }
109
+ catch {
110
+ // No body or invalid JSON
111
+ }
112
+ }
113
+ const { data, status } = await forwardToApi(method, path, authHeader, apiKey, body);
114
+ return new Response(JSON.stringify(data), {
115
+ status,
116
+ headers: { "Content-Type": "application/json" },
117
+ });
118
+ }
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ const status = message.includes("Unauthorized") ? 401 : 500;
122
+ return new Response(JSON.stringify({ error: message }), {
123
+ status,
124
+ headers: { "Content-Type": "application/json" },
125
+ });
126
+ }
127
+ }
128
+ // Workspaces
129
+ app.get("/workspaces", (c) => handleRequest(c, "GET", "/workspaces"));
130
+ app.get("/workspaces/:id/members", (c) => handleRequest(c, "GET", `/workspaces/${c.req.param("id")}/members`));
131
+ app.get("/workspaces/:id/projects", (c) => handleRequest(c, "GET", `/workspaces/${c.req.param("id")}/projects`));
132
+ // Projects
133
+ app.get("/projects", (c) => {
134
+ const workspaceId = new URL(c.req.url).searchParams.get("workspace_id");
135
+ return handleRequest(c, "GET", `/projects?workspace_id=${workspaceId}`);
136
+ });
137
+ // Get card by short ID: GET /projects/:projectId/cards/:shortId
138
+ app.get("/projects/:projectId/cards/:shortId", (c) => handleRequest(c, "GET", `/projects/${c.req.param("projectId")}/cards/${c.req.param("shortId")}`));
139
+ // Board
140
+ app.get("/board/:projectId", (c) => handleRequest(c, "GET", `/board/${c.req.param("projectId")}`));
141
+ // Cards
142
+ app.get("/cards/:id", (c) => handleRequest(c, "GET", `/cards/${c.req.param("id")}`));
143
+ app.post("/cards", (c) => handleRequest(c, "POST", "/cards"));
144
+ app.patch("/cards/:id", (c) => handleRequest(c, "PATCH", `/cards/${c.req.param("id")}`));
145
+ app.delete("/cards/:id", (c) => handleRequest(c, "DELETE", `/cards/${c.req.param("id")}`));
146
+ app.post("/cards/:id/move", (c) => handleRequest(c, "POST", `/cards/${c.req.param("id")}/move`));
147
+ app.post("/cards/:id/labels", (c) => handleRequest(c, "POST", `/cards/${c.req.param("id")}/labels`));
148
+ app.delete("/cards/:id/labels/:labelId", (c) => handleRequest(c, "DELETE", `/cards/${c.req.param("id")}/labels/${c.req.param("labelId")}`));
149
+ // Search
150
+ app.get("/search", (c) => {
151
+ const params = new URL(c.req.url).searchParams;
152
+ return handleRequest(c, "GET", `/search?${params.toString()}`);
153
+ });
154
+ // Columns
155
+ app.post("/columns", (c) => handleRequest(c, "POST", "/columns"));
156
+ app.patch("/columns/:id", (c) => handleRequest(c, "PATCH", `/columns/${c.req.param("id")}`));
157
+ app.delete("/columns/:id", (c) => handleRequest(c, "DELETE", `/columns/${c.req.param("id")}`));
158
+ // Labels
159
+ app.post("/labels", (c) => handleRequest(c, "POST", "/labels"));
160
+ // Subtasks
161
+ app.post("/subtasks", (c) => handleRequest(c, "POST", "/subtasks"));
162
+ app.post("/subtasks/:id/toggle", (c) => handleRequest(c, "POST", `/subtasks/${c.req.param("id")}/toggle`));
163
+ app.delete("/subtasks/:id", (c) => handleRequest(c, "DELETE", `/subtasks/${c.req.param("id")}`));
164
+ // NLU
165
+ app.post("/nlu", (c) => handleRequest(c, "POST", "/nlu"));
166
+ app.post("/nlu/process", (c) => handleRequest(c, "POST", "/nlu"));
167
+ // Start server
168
+ const port = parseInt(process.env.PORT || "3001", 10);
169
+ console.log(`Starting Harmony MCP HTTP adapter on port ${port}...`);
170
+ serve({
171
+ fetch: app.fetch,
172
+ port,
173
+ });
174
+ console.log(`Harmony MCP HTTP adapter running at http://localhost:${port}`);
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { HarmonyMCPServer } from "./server.js";
3
+ const server = new HarmonyMCPServer();
4
+ server.run().catch((error) => {
5
+ console.error("Failed to start MCP server:", error);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Automatic Memory Lifecycle Maintenance
3
+ *
4
+ * Runs at session end to enforce decay/archival rules:
5
+ * - Archive entities with confidence < 0.3
6
+ * - Delete stale drafts (>30 days old with low decay score)
7
+ * - Auto-promote eligible entities (draft→episode, episode→reference)
8
+ *
9
+ * All operations are non-fatal — failures are logged but never block session end.
10
+ */
11
+ import { evaluateLifecycle } from "@harmony/memory";
12
+ /**
13
+ * Run lifecycle maintenance for a workspace.
14
+ * Called automatically at session end alongside consolidation.
15
+ */
16
+ export async function runLifecycleMaintenance(client, workspaceId, projectId) {
17
+ const result = {
18
+ archived: 0,
19
+ pruned: 0,
20
+ promoted: 0,
21
+ reviewed: 0,
22
+ errors: 0,
23
+ };
24
+ let entities;
25
+ try {
26
+ const listResult = await client.listMemoryEntities({
27
+ workspace_id: workspaceId,
28
+ project_id: projectId,
29
+ limit: 200,
30
+ });
31
+ entities = (listResult.entities || []);
32
+ }
33
+ catch {
34
+ return result;
35
+ }
36
+ if (entities.length === 0)
37
+ return result;
38
+ const now = Date.now();
39
+ const STALE_DRAFT_MAX_AGE_DAYS = 30;
40
+ for (const entity of entities) {
41
+ try {
42
+ const lifecycle = evaluateLifecycle(entity);
43
+ // 1. Archive low-confidence entities (confidence < 0.3)
44
+ if (lifecycle.shouldArchive) {
45
+ await client.deleteMemoryEntity(entity.id);
46
+ result.archived++;
47
+ continue;
48
+ }
49
+ // 2. Prune stale drafts (>30 days old with low decay score)
50
+ if (entity.memory_tier === "draft") {
51
+ const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
52
+ if (ageDays > STALE_DRAFT_MAX_AGE_DAYS && lifecycle.decay.score < 0.3) {
53
+ await client.deleteMemoryEntity(entity.id);
54
+ result.pruned++;
55
+ continue;
56
+ }
57
+ }
58
+ // 3. Auto-promote eligible entities
59
+ if (lifecycle.promotion.eligible && lifecycle.promotion.targetTier) {
60
+ await client.updateMemoryEntity(entity.id, {
61
+ memory_tier: lifecycle.promotion.targetTier,
62
+ metadata: {
63
+ promoted_at: new Date().toISOString(),
64
+ promotion_reason: lifecycle.promotion.reason,
65
+ promoted_from: entity.memory_tier,
66
+ },
67
+ });
68
+ result.promoted++;
69
+ continue;
70
+ }
71
+ // 4. Flag stale entities for review (>90 days, <3 accesses)
72
+ if (lifecycle.shouldFlagForReview) {
73
+ await client.updateMemoryEntity(entity.id, {
74
+ metadata: {
75
+ needs_review: true,
76
+ review_reason: lifecycle.reviewReason,
77
+ flagged_at: new Date().toISOString(),
78
+ },
79
+ });
80
+ result.reviewed++;
81
+ }
82
+ }
83
+ catch {
84
+ result.errors++;
85
+ }
86
+ }
87
+ return result;
88
+ }