@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.
- package/README.md +201 -36
- package/dist/cli.js +20938 -20249
- package/dist/http.js +1957 -0
- package/dist/index.js +17833 -17888
- package/dist/lib/__tests__/active-learning.test.js +386 -0
- package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
- package/dist/lib/__tests__/auto-session.test.js +661 -0
- package/dist/lib/__tests__/context-assembly.test.js +362 -0
- package/dist/lib/__tests__/graph-expansion.test.js +150 -0
- package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
- package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
- package/dist/lib/__tests__/pattern-detection.test.js +295 -0
- package/dist/lib/__tests__/prompt-builder.test.js +418 -0
- package/dist/lib/active-learning.js +878 -0
- package/dist/lib/api-client.js +548 -0
- package/dist/lib/auto-session.js +173 -0
- package/dist/lib/cli.js +127 -0
- package/dist/lib/config.js +205 -0
- package/dist/lib/consolidation.js +243 -0
- package/dist/lib/context-assembly.js +606 -0
- package/dist/lib/graph-expansion.js +163 -0
- package/dist/lib/http.js +174 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/lifecycle-maintenance.js +88 -0
- package/dist/lib/prompt-builder.js +483 -0
- package/dist/lib/remote.js +166 -0
- package/dist/lib/server.js +3132 -0
- package/dist/lib/tui/agents.js +116 -0
- package/dist/lib/tui/docs.js +558 -0
- package/dist/lib/tui/setup.js +1068 -0
- package/dist/lib/tui/theme.js +95 -0
- package/dist/lib/tui/writer.js +200 -0
- package/dist/remote.js +34534 -0
- package/dist/server.js +31967 -0
- package/package.json +20 -7
- package/src/__tests__/active-learning.test.ts +483 -0
- package/src/__tests__/agent-performance-profiles.test.ts +468 -0
- package/src/__tests__/auto-session.test.ts +912 -0
- package/src/__tests__/context-assembly.test.ts +506 -0
- package/src/__tests__/graph-expansion.test.ts +285 -0
- package/src/__tests__/integration-memory-crud.test.ts +948 -0
- package/src/__tests__/integration-memory-system.test.ts +321 -0
- package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
- package/src/__tests__/pattern-detection.test.ts +438 -0
- package/src/__tests__/prompt-builder.test.ts +505 -0
- package/src/active-learning.ts +1227 -0
- package/src/api-client.ts +963 -0
- package/src/auto-session.ts +218 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +285 -0
- package/src/consolidation.ts +314 -0
- package/src/context-assembly.ts +842 -0
- package/src/graph-expansion.ts +234 -0
- package/src/http.ts +265 -0
- package/src/index.ts +8 -0
- package/src/lifecycle-maintenance.ts +120 -0
- package/src/prompt-builder.ts +681 -0
- package/src/remote.ts +227 -0
- package/src/server.ts +3858 -0
- package/src/tui/agents.ts +154 -0
- package/src/tui/docs.ts +650 -0
- package/src/tui/setup.ts +1281 -0
- package/src/tui/theme.ts +114 -0
- package/src/tui/writer.ts +260 -0
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
import type { HarmonyApiClient } from "./api-client.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Automatically expand the knowledge graph by linking a newly-created entity
|
|
12
|
+
* to semantically similar existing entities via `relates_to` relations.
|
|
13
|
+
*
|
|
14
|
+
* Non-fatal: errors are caught and silently ignored so callers are never blocked.
|
|
15
|
+
*/
|
|
16
|
+
export async function autoExpandGraph(
|
|
17
|
+
client: HarmonyApiClient,
|
|
18
|
+
entityId: string,
|
|
19
|
+
title: string,
|
|
20
|
+
content: string,
|
|
21
|
+
_tags: string[],
|
|
22
|
+
workspaceId: string,
|
|
23
|
+
projectId?: string,
|
|
24
|
+
maxRelations: number = 5,
|
|
25
|
+
): Promise<{ relationsCreated: number }> {
|
|
26
|
+
try {
|
|
27
|
+
// Build a search query from the title + first 200 chars of content
|
|
28
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
29
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
30
|
+
|
|
31
|
+
let candidates: Array<{ id: string }> = [];
|
|
32
|
+
|
|
33
|
+
// First attempt
|
|
34
|
+
const { entities } = await client.searchMemoryEntities(workspaceId, query, {
|
|
35
|
+
project_id: projectId,
|
|
36
|
+
limit: 20,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
candidates = (entities as Array<{ id: string }>)
|
|
40
|
+
.filter((e) => e.id !== entityId)
|
|
41
|
+
.slice(0, maxRelations);
|
|
42
|
+
|
|
43
|
+
// Retry once after 2s if no candidates found (handles embedding generation race)
|
|
44
|
+
if (candidates.length === 0) {
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
46
|
+
const retry = await client.searchMemoryEntities(workspaceId, query, {
|
|
47
|
+
project_id: projectId,
|
|
48
|
+
limit: 20,
|
|
49
|
+
});
|
|
50
|
+
candidates = (retry.entities as Array<{ id: string }>)
|
|
51
|
+
.filter((e) => e.id !== entityId)
|
|
52
|
+
.slice(0, maxRelations);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let relationsCreated = 0;
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
try {
|
|
58
|
+
await client.createMemoryRelation({
|
|
59
|
+
source_id: entityId,
|
|
60
|
+
target_id: candidate.id,
|
|
61
|
+
relation_type: "relates_to",
|
|
62
|
+
confidence: 0.6,
|
|
63
|
+
});
|
|
64
|
+
relationsCreated++;
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
// Silently skip 409 Conflict (relation already exists) and other errors
|
|
67
|
+
const status = (err as { status?: number })?.status;
|
|
68
|
+
if (status !== 409) {
|
|
69
|
+
// Non-409 errors are still non-fatal; just skip
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { relationsCreated };
|
|
75
|
+
} catch {
|
|
76
|
+
// Never block callers due to graph expansion failures
|
|
77
|
+
return { relationsCreated: 0 };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SimilarEntity {
|
|
82
|
+
id: string;
|
|
83
|
+
type: string;
|
|
84
|
+
title: string;
|
|
85
|
+
content: string;
|
|
86
|
+
confidence: number;
|
|
87
|
+
rrf_score?: number;
|
|
88
|
+
tags?: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Find entities semantically similar to the given title+content using
|
|
93
|
+
* hybrid FTS+vector search. Returns entities sorted by relevance.
|
|
94
|
+
*/
|
|
95
|
+
export async function findSimilarEntities(
|
|
96
|
+
client: HarmonyApiClient,
|
|
97
|
+
title: string,
|
|
98
|
+
content: string,
|
|
99
|
+
workspaceId: string,
|
|
100
|
+
options?: {
|
|
101
|
+
projectId?: string;
|
|
102
|
+
limit?: number;
|
|
103
|
+
minRrfScore?: number;
|
|
104
|
+
excludeIds?: string[];
|
|
105
|
+
type?: string;
|
|
106
|
+
},
|
|
107
|
+
): Promise<SimilarEntity[]> {
|
|
108
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
109
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const { entities } = await client.searchMemoryEntities(workspaceId, query, {
|
|
113
|
+
project_id: options?.projectId,
|
|
114
|
+
limit: options?.limit ?? 20,
|
|
115
|
+
type: options?.type,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const minScore = options?.minRrfScore ?? 0;
|
|
119
|
+
const excludeSet = new Set(options?.excludeIds || []);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
entities as Array<{
|
|
123
|
+
id: string;
|
|
124
|
+
type: string;
|
|
125
|
+
title: string;
|
|
126
|
+
content: string;
|
|
127
|
+
confidence: number;
|
|
128
|
+
rrf_score?: number;
|
|
129
|
+
tags?: string[];
|
|
130
|
+
}>
|
|
131
|
+
).filter((e) => {
|
|
132
|
+
if (excludeSet.has(e.id)) return false;
|
|
133
|
+
if (minScore > 0 && (e.rrf_score ?? 0) < minScore) return false;
|
|
134
|
+
return true;
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Causal lookup table: maps an entity type to the target types it should
|
|
143
|
+
* be linked to, along with the relation type and direction.
|
|
144
|
+
*
|
|
145
|
+
* "forward" means source=newEntity → target=match.
|
|
146
|
+
* "reverse" means source=match → target=newEntity.
|
|
147
|
+
*/
|
|
148
|
+
const CAUSAL_LOOKUP: Array<{
|
|
149
|
+
sourceType: string;
|
|
150
|
+
targetType: string;
|
|
151
|
+
relation: string;
|
|
152
|
+
direction: "forward" | "reverse";
|
|
153
|
+
}> = [
|
|
154
|
+
{
|
|
155
|
+
sourceType: "error",
|
|
156
|
+
targetType: "solution",
|
|
157
|
+
relation: "resolved_by",
|
|
158
|
+
direction: "forward",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
sourceType: "solution",
|
|
162
|
+
targetType: "error",
|
|
163
|
+
relation: "resolved_by",
|
|
164
|
+
direction: "reverse",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
sourceType: "lesson",
|
|
168
|
+
targetType: "error",
|
|
169
|
+
relation: "learned_from",
|
|
170
|
+
direction: "forward",
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Link a newly created entity to causally-related entities of *other* types.
|
|
176
|
+
*
|
|
177
|
+
* For example, a new `error` entity gets linked to similar `solution` entities
|
|
178
|
+
* via `resolved_by`, enabling agents to discover "what solved similar problems."
|
|
179
|
+
*
|
|
180
|
+
* Non-fatal: all errors are caught silently.
|
|
181
|
+
*/
|
|
182
|
+
export async function linkCrossTypeNeighbors(
|
|
183
|
+
client: HarmonyApiClient,
|
|
184
|
+
entityId: string,
|
|
185
|
+
entityType: string,
|
|
186
|
+
title: string,
|
|
187
|
+
content: string,
|
|
188
|
+
workspaceId: string,
|
|
189
|
+
projectId?: string,
|
|
190
|
+
): Promise<{ relationsCreated: number }> {
|
|
191
|
+
const rules = CAUSAL_LOOKUP.filter((r) => r.sourceType === entityType);
|
|
192
|
+
if (rules.length === 0) return { relationsCreated: 0 };
|
|
193
|
+
|
|
194
|
+
let relationsCreated = 0;
|
|
195
|
+
|
|
196
|
+
for (const rule of rules) {
|
|
197
|
+
try {
|
|
198
|
+
const matches = await findSimilarEntities(
|
|
199
|
+
client,
|
|
200
|
+
title,
|
|
201
|
+
content,
|
|
202
|
+
workspaceId,
|
|
203
|
+
{
|
|
204
|
+
projectId,
|
|
205
|
+
limit: 10,
|
|
206
|
+
minRrfScore: 0.04,
|
|
207
|
+
excludeIds: [entityId],
|
|
208
|
+
type: rule.targetType,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Cap at 3 matches per target type
|
|
213
|
+
for (const match of matches.slice(0, 3)) {
|
|
214
|
+
const sourceId = rule.direction === "forward" ? entityId : match.id;
|
|
215
|
+
const targetId = rule.direction === "forward" ? match.id : entityId;
|
|
216
|
+
try {
|
|
217
|
+
await client.createMemoryRelation({
|
|
218
|
+
source_id: sourceId,
|
|
219
|
+
target_id: targetId,
|
|
220
|
+
relation_type: rule.relation,
|
|
221
|
+
confidence: 0.65,
|
|
222
|
+
});
|
|
223
|
+
relationsCreated++;
|
|
224
|
+
} catch {
|
|
225
|
+
// Skip duplicate/failed relations silently
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// Non-fatal: search failure for one rule shouldn't block others
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { relationsCreated };
|
|
234
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP REST Adapter for Harmony MCP
|
|
5
|
+
*
|
|
6
|
+
* This provides a REST API that forwards requests to the Harmony API.
|
|
7
|
+
* It accepts Bearer tokens (JWT) or X-API-Key headers for authentication.
|
|
8
|
+
*
|
|
9
|
+
* This adapter is useful for non-MCP clients that want to interact with
|
|
10
|
+
* Harmony via HTTP instead of the MCP stdio protocol.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { serve } from "bun";
|
|
14
|
+
import { Hono } from "hono";
|
|
15
|
+
import { cors } from "hono/cors";
|
|
16
|
+
import { loadConfig } from "./config.js";
|
|
17
|
+
|
|
18
|
+
const app = new Hono();
|
|
19
|
+
|
|
20
|
+
// CORS configuration
|
|
21
|
+
app.use(
|
|
22
|
+
"/*",
|
|
23
|
+
cors({
|
|
24
|
+
origin: [
|
|
25
|
+
"https://gethmy.com",
|
|
26
|
+
"http://localhost:8080",
|
|
27
|
+
"http://localhost:3000",
|
|
28
|
+
],
|
|
29
|
+
allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
|
30
|
+
allowHeaders: ["Content-Type", "Authorization", "X-API-Key"],
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Get API URL from config or use default
|
|
35
|
+
function getApiUrl(): string {
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
return config.apiUrl;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Forward request to Harmony API
|
|
41
|
+
async function forwardToApi(
|
|
42
|
+
method: string,
|
|
43
|
+
path: string,
|
|
44
|
+
authHeader: string | undefined,
|
|
45
|
+
apiKey: string | undefined,
|
|
46
|
+
body?: unknown,
|
|
47
|
+
): Promise<{ data: unknown; status: number }> {
|
|
48
|
+
const apiUrl = getApiUrl();
|
|
49
|
+
|
|
50
|
+
const headers: Record<string, string> = {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Use API key if provided, otherwise forward the Authorization header
|
|
55
|
+
if (apiKey) {
|
|
56
|
+
headers["X-API-Key"] = apiKey;
|
|
57
|
+
} else if (authHeader) {
|
|
58
|
+
headers.Authorization = authHeader;
|
|
59
|
+
} else {
|
|
60
|
+
throw new Error("Unauthorized: Missing API key or Authorization header");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
64
|
+
method,
|
|
65
|
+
headers,
|
|
66
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
return { data, status: response.status };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Health check
|
|
74
|
+
app.get("/health", (c) =>
|
|
75
|
+
c.json({ status: "ok", service: "harmony-mcp-http" }),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// List available endpoints
|
|
79
|
+
app.get("/", (c) =>
|
|
80
|
+
c.json({
|
|
81
|
+
service: "Harmony MCP HTTP Adapter",
|
|
82
|
+
endpoints: {
|
|
83
|
+
workspaces: {
|
|
84
|
+
list: "GET /workspaces",
|
|
85
|
+
members: "GET /workspaces/:id/members",
|
|
86
|
+
projects: "GET /workspaces/:id/projects",
|
|
87
|
+
},
|
|
88
|
+
board: {
|
|
89
|
+
get: "GET /board/:projectId",
|
|
90
|
+
},
|
|
91
|
+
cards: {
|
|
92
|
+
create: "POST /cards",
|
|
93
|
+
get: "GET /cards/:id",
|
|
94
|
+
getByShortId: "GET /projects/:projectId/cards/:shortId",
|
|
95
|
+
update: "PATCH /cards/:id",
|
|
96
|
+
delete: "DELETE /cards/:id",
|
|
97
|
+
move: "POST /cards/:id/move",
|
|
98
|
+
search: "GET /search?q=query",
|
|
99
|
+
},
|
|
100
|
+
columns: {
|
|
101
|
+
create: "POST /columns",
|
|
102
|
+
update: "PATCH /columns/:id",
|
|
103
|
+
delete: "DELETE /columns/:id",
|
|
104
|
+
},
|
|
105
|
+
labels: {
|
|
106
|
+
create: "POST /labels",
|
|
107
|
+
addToCard: "POST /cards/:id/labels",
|
|
108
|
+
removeFromCard: "DELETE /cards/:id/labels/:labelId",
|
|
109
|
+
},
|
|
110
|
+
subtasks: {
|
|
111
|
+
create: "POST /subtasks",
|
|
112
|
+
toggle: "POST /subtasks/:id/toggle",
|
|
113
|
+
delete: "DELETE /subtasks/:id",
|
|
114
|
+
},
|
|
115
|
+
nlu: {
|
|
116
|
+
process: "POST /nlu",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
authentication: "Include X-API-Key header or Authorization: Bearer <token>",
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Generic proxy handler
|
|
124
|
+
async function handleRequest(
|
|
125
|
+
c: {
|
|
126
|
+
req: {
|
|
127
|
+
method: string;
|
|
128
|
+
url: string;
|
|
129
|
+
header: (name: string) => string | undefined;
|
|
130
|
+
};
|
|
131
|
+
json: () => Promise<unknown>;
|
|
132
|
+
},
|
|
133
|
+
method: string,
|
|
134
|
+
path: string,
|
|
135
|
+
) {
|
|
136
|
+
try {
|
|
137
|
+
const authHeader = c.req.header("Authorization");
|
|
138
|
+
const apiKey = c.req.header("X-API-Key");
|
|
139
|
+
|
|
140
|
+
let body: unknown;
|
|
141
|
+
if (["POST", "PATCH", "PUT"].includes(method)) {
|
|
142
|
+
try {
|
|
143
|
+
body = await c.json();
|
|
144
|
+
} catch {
|
|
145
|
+
// No body or invalid JSON
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { data, status } = await forwardToApi(
|
|
150
|
+
method,
|
|
151
|
+
path,
|
|
152
|
+
authHeader,
|
|
153
|
+
apiKey,
|
|
154
|
+
body,
|
|
155
|
+
);
|
|
156
|
+
return new Response(JSON.stringify(data), {
|
|
157
|
+
status,
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
const status = message.includes("Unauthorized") ? 401 : 500;
|
|
163
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
164
|
+
status,
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Workspaces
|
|
171
|
+
app.get("/workspaces", (c) => handleRequest(c, "GET", "/workspaces"));
|
|
172
|
+
app.get("/workspaces/:id/members", (c) =>
|
|
173
|
+
handleRequest(c, "GET", `/workspaces/${c.req.param("id")}/members`),
|
|
174
|
+
);
|
|
175
|
+
app.get("/workspaces/:id/projects", (c) =>
|
|
176
|
+
handleRequest(c, "GET", `/workspaces/${c.req.param("id")}/projects`),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Projects
|
|
180
|
+
app.get("/projects", (c) => {
|
|
181
|
+
const workspaceId = new URL(c.req.url).searchParams.get("workspace_id");
|
|
182
|
+
return handleRequest(c, "GET", `/projects?workspace_id=${workspaceId}`);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Get card by short ID: GET /projects/:projectId/cards/:shortId
|
|
186
|
+
app.get("/projects/:projectId/cards/:shortId", (c) =>
|
|
187
|
+
handleRequest(
|
|
188
|
+
c,
|
|
189
|
+
"GET",
|
|
190
|
+
`/projects/${c.req.param("projectId")}/cards/${c.req.param("shortId")}`,
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Board
|
|
195
|
+
app.get("/board/:projectId", (c) =>
|
|
196
|
+
handleRequest(c, "GET", `/board/${c.req.param("projectId")}`),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Cards
|
|
200
|
+
app.get("/cards/:id", (c) =>
|
|
201
|
+
handleRequest(c, "GET", `/cards/${c.req.param("id")}`),
|
|
202
|
+
);
|
|
203
|
+
app.post("/cards", (c) => handleRequest(c, "POST", "/cards"));
|
|
204
|
+
app.patch("/cards/:id", (c) =>
|
|
205
|
+
handleRequest(c, "PATCH", `/cards/${c.req.param("id")}`),
|
|
206
|
+
);
|
|
207
|
+
app.delete("/cards/:id", (c) =>
|
|
208
|
+
handleRequest(c, "DELETE", `/cards/${c.req.param("id")}`),
|
|
209
|
+
);
|
|
210
|
+
app.post("/cards/:id/move", (c) =>
|
|
211
|
+
handleRequest(c, "POST", `/cards/${c.req.param("id")}/move`),
|
|
212
|
+
);
|
|
213
|
+
app.post("/cards/:id/labels", (c) =>
|
|
214
|
+
handleRequest(c, "POST", `/cards/${c.req.param("id")}/labels`),
|
|
215
|
+
);
|
|
216
|
+
app.delete("/cards/:id/labels/:labelId", (c) =>
|
|
217
|
+
handleRequest(
|
|
218
|
+
c,
|
|
219
|
+
"DELETE",
|
|
220
|
+
`/cards/${c.req.param("id")}/labels/${c.req.param("labelId")}`,
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Search
|
|
225
|
+
app.get("/search", (c) => {
|
|
226
|
+
const params = new URL(c.req.url).searchParams;
|
|
227
|
+
return handleRequest(c, "GET", `/search?${params.toString()}`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Columns
|
|
231
|
+
app.post("/columns", (c) => handleRequest(c, "POST", "/columns"));
|
|
232
|
+
app.patch("/columns/:id", (c) =>
|
|
233
|
+
handleRequest(c, "PATCH", `/columns/${c.req.param("id")}`),
|
|
234
|
+
);
|
|
235
|
+
app.delete("/columns/:id", (c) =>
|
|
236
|
+
handleRequest(c, "DELETE", `/columns/${c.req.param("id")}`),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Labels
|
|
240
|
+
app.post("/labels", (c) => handleRequest(c, "POST", "/labels"));
|
|
241
|
+
|
|
242
|
+
// Subtasks
|
|
243
|
+
app.post("/subtasks", (c) => handleRequest(c, "POST", "/subtasks"));
|
|
244
|
+
app.post("/subtasks/:id/toggle", (c) =>
|
|
245
|
+
handleRequest(c, "POST", `/subtasks/${c.req.param("id")}/toggle`),
|
|
246
|
+
);
|
|
247
|
+
app.delete("/subtasks/:id", (c) =>
|
|
248
|
+
handleRequest(c, "DELETE", `/subtasks/${c.req.param("id")}`),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// NLU
|
|
252
|
+
app.post("/nlu", (c) => handleRequest(c, "POST", "/nlu"));
|
|
253
|
+
app.post("/nlu/process", (c) => handleRequest(c, "POST", "/nlu"));
|
|
254
|
+
|
|
255
|
+
// Start server
|
|
256
|
+
const port = parseInt(process.env.PORT || "3001", 10);
|
|
257
|
+
|
|
258
|
+
console.log(`Starting Harmony MCP HTTP adapter on port ${port}...`);
|
|
259
|
+
|
|
260
|
+
serve({
|
|
261
|
+
fetch: app.fetch,
|
|
262
|
+
port,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
console.log(`Harmony MCP HTTP adapter running at http://localhost:${port}`);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
|
|
12
|
+
import { evaluateLifecycle } from "@harmony/memory";
|
|
13
|
+
import type { HarmonyApiClient } from "./api-client.js";
|
|
14
|
+
|
|
15
|
+
interface MemoryEntity {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
memory_tier: "draft" | "episode" | "reference";
|
|
19
|
+
confidence: number;
|
|
20
|
+
access_count: number;
|
|
21
|
+
last_accessed_at: string | null;
|
|
22
|
+
created_at: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MaintenanceResult {
|
|
26
|
+
archived: number;
|
|
27
|
+
pruned: number;
|
|
28
|
+
promoted: number;
|
|
29
|
+
reviewed: number;
|
|
30
|
+
errors: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run lifecycle maintenance for a workspace.
|
|
35
|
+
* Called automatically at session end alongside consolidation.
|
|
36
|
+
*/
|
|
37
|
+
export async function runLifecycleMaintenance(
|
|
38
|
+
client: HarmonyApiClient,
|
|
39
|
+
workspaceId: string,
|
|
40
|
+
projectId?: string,
|
|
41
|
+
): Promise<MaintenanceResult> {
|
|
42
|
+
const result: MaintenanceResult = {
|
|
43
|
+
archived: 0,
|
|
44
|
+
pruned: 0,
|
|
45
|
+
promoted: 0,
|
|
46
|
+
reviewed: 0,
|
|
47
|
+
errors: 0,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let entities: MemoryEntity[];
|
|
51
|
+
try {
|
|
52
|
+
const listResult = await client.listMemoryEntities({
|
|
53
|
+
workspace_id: workspaceId,
|
|
54
|
+
project_id: projectId,
|
|
55
|
+
limit: 200,
|
|
56
|
+
});
|
|
57
|
+
entities = (listResult.entities || []) as MemoryEntity[];
|
|
58
|
+
} catch {
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (entities.length === 0) return result;
|
|
63
|
+
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const STALE_DRAFT_MAX_AGE_DAYS = 30;
|
|
66
|
+
|
|
67
|
+
for (const entity of entities) {
|
|
68
|
+
try {
|
|
69
|
+
const lifecycle = evaluateLifecycle(entity);
|
|
70
|
+
|
|
71
|
+
// 1. Archive low-confidence entities (confidence < 0.3)
|
|
72
|
+
if (lifecycle.shouldArchive) {
|
|
73
|
+
await client.deleteMemoryEntity(entity.id);
|
|
74
|
+
result.archived++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Prune stale drafts (>30 days old with low decay score)
|
|
79
|
+
if (entity.memory_tier === "draft") {
|
|
80
|
+
const ageDays =
|
|
81
|
+
(now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
82
|
+
if (ageDays > STALE_DRAFT_MAX_AGE_DAYS && lifecycle.decay.score < 0.3) {
|
|
83
|
+
await client.deleteMemoryEntity(entity.id);
|
|
84
|
+
result.pruned++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 3. Auto-promote eligible entities
|
|
90
|
+
if (lifecycle.promotion.eligible && lifecycle.promotion.targetTier) {
|
|
91
|
+
await client.updateMemoryEntity(entity.id, {
|
|
92
|
+
memory_tier: lifecycle.promotion.targetTier,
|
|
93
|
+
metadata: {
|
|
94
|
+
promoted_at: new Date().toISOString(),
|
|
95
|
+
promotion_reason: lifecycle.promotion.reason,
|
|
96
|
+
promoted_from: entity.memory_tier,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
result.promoted++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. Flag stale entities for review (>90 days, <3 accesses)
|
|
104
|
+
if (lifecycle.shouldFlagForReview) {
|
|
105
|
+
await client.updateMemoryEntity(entity.id, {
|
|
106
|
+
metadata: {
|
|
107
|
+
needs_review: true,
|
|
108
|
+
review_reason: lifecycle.reviewReason,
|
|
109
|
+
flagged_at: new Date().toISOString(),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
result.reviewed++;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
result.errors++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|