@gethmy/mcp 2.9.4 → 2.9.6
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 +15 -15
- package/dist/cli.js +674 -275
- package/dist/index.js +530 -205
- package/dist/lib/api-client.js +352 -182
- package/dist/lib/config.js +37 -26
- package/package.json +3 -2
- package/src/api-client.ts +50 -4
- package/src/cli.ts +13 -2
- package/src/config.ts +53 -25
- package/src/graph-expansion.ts +7 -4
- package/src/http.ts +3 -14
- package/src/memory-floor.ts +2 -1
- package/src/oauth-login.ts +288 -0
- package/src/oauth-refresh.ts +209 -0
- package/src/server.ts +6 -6
- package/src/skills.ts +4 -2
- package/src/tui/setup.ts +106 -16
package/dist/lib/config.js
CHANGED
|
@@ -18,8 +18,6 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
-
var DEFAULT_API_URL = "https://app.gethmy.com/api";
|
|
22
|
-
var LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
23
21
|
function getConfigDir() {
|
|
24
22
|
return join(homedir(), ".harmony-mcp");
|
|
25
23
|
}
|
|
@@ -29,17 +27,24 @@ function getConfigPath() {
|
|
|
29
27
|
function getLocalConfigPath(cwd) {
|
|
30
28
|
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
31
29
|
}
|
|
30
|
+
function emptyConfig() {
|
|
31
|
+
return {
|
|
32
|
+
apiKey: null,
|
|
33
|
+
apiUrl: DEFAULT_API_URL,
|
|
34
|
+
activeWorkspaceId: null,
|
|
35
|
+
activeProjectId: null,
|
|
36
|
+
userEmail: null,
|
|
37
|
+
memoryDir: null,
|
|
38
|
+
oauthAccessToken: null,
|
|
39
|
+
oauthRefreshToken: null,
|
|
40
|
+
oauthExpiresAt: null,
|
|
41
|
+
oauthClientId: null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
32
44
|
function loadConfig() {
|
|
33
45
|
const configPath = getConfigPath();
|
|
34
46
|
if (!existsSync(configPath)) {
|
|
35
|
-
return
|
|
36
|
-
apiKey: null,
|
|
37
|
-
apiUrl: DEFAULT_API_URL,
|
|
38
|
-
activeWorkspaceId: null,
|
|
39
|
-
activeProjectId: null,
|
|
40
|
-
userEmail: null,
|
|
41
|
-
memoryDir: null
|
|
42
|
-
};
|
|
47
|
+
return emptyConfig();
|
|
43
48
|
}
|
|
44
49
|
try {
|
|
45
50
|
const data = readFileSync(configPath, "utf-8");
|
|
@@ -50,17 +55,14 @@ function loadConfig() {
|
|
|
50
55
|
activeWorkspaceId: config.activeWorkspaceId || null,
|
|
51
56
|
activeProjectId: config.activeProjectId || null,
|
|
52
57
|
userEmail: config.userEmail || null,
|
|
53
|
-
memoryDir: config.memoryDir || null
|
|
58
|
+
memoryDir: config.memoryDir || null,
|
|
59
|
+
oauthAccessToken: config.oauthAccessToken || null,
|
|
60
|
+
oauthRefreshToken: config.oauthRefreshToken || null,
|
|
61
|
+
oauthExpiresAt: typeof config.oauthExpiresAt === "number" ? config.oauthExpiresAt : null,
|
|
62
|
+
oauthClientId: config.oauthClientId || null
|
|
54
63
|
};
|
|
55
64
|
} catch {
|
|
56
|
-
return
|
|
57
|
-
apiKey: null,
|
|
58
|
-
apiUrl: DEFAULT_API_URL,
|
|
59
|
-
activeWorkspaceId: null,
|
|
60
|
-
activeProjectId: null,
|
|
61
|
-
userEmail: null,
|
|
62
|
-
memoryDir: null
|
|
63
|
-
};
|
|
65
|
+
return emptyConfig();
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
function saveConfig(config) {
|
|
@@ -108,13 +110,17 @@ function saveLocalConfig(config, cwd) {
|
|
|
108
110
|
function hasLocalConfig(cwd) {
|
|
109
111
|
return existsSync(getLocalConfigPath(cwd));
|
|
110
112
|
}
|
|
111
|
-
function
|
|
113
|
+
function getActiveCredential() {
|
|
112
114
|
const config = loadConfig();
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
if (config.oauthAccessToken)
|
|
116
|
+
return config.oauthAccessToken;
|
|
117
|
+
if (config.apiKey)
|
|
118
|
+
return config.apiKey;
|
|
119
|
+
throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to connect Harmony.
|
|
120
|
+
` + "Setup authorizes in your browser — no API key handling required.");
|
|
121
|
+
}
|
|
122
|
+
function getApiKey() {
|
|
123
|
+
return getActiveCredential();
|
|
118
124
|
}
|
|
119
125
|
function getApiUrl() {
|
|
120
126
|
const config = loadConfig();
|
|
@@ -157,7 +163,7 @@ function getActiveProjectId(cwd) {
|
|
|
157
163
|
}
|
|
158
164
|
function isConfigured() {
|
|
159
165
|
const config = loadConfig();
|
|
160
|
-
return !!config.apiKey;
|
|
166
|
+
return !!(config.apiKey || config.oauthAccessToken);
|
|
161
167
|
}
|
|
162
168
|
function areSkillsInstalled(cwd) {
|
|
163
169
|
const home = homedir();
|
|
@@ -201,6 +207,10 @@ function getMemoryDir() {
|
|
|
201
207
|
return config.memoryDir;
|
|
202
208
|
return join(homedir(), ".harmony", "memory");
|
|
203
209
|
}
|
|
210
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
211
|
+
var init_config = () => {};
|
|
212
|
+
init_config();
|
|
213
|
+
|
|
204
214
|
export {
|
|
205
215
|
setUserEmail,
|
|
206
216
|
setActiveWorkspace,
|
|
@@ -221,5 +231,6 @@ export {
|
|
|
221
231
|
getApiKey,
|
|
222
232
|
getActiveWorkspaceId,
|
|
223
233
|
getActiveProjectId,
|
|
234
|
+
getActiveCredential,
|
|
224
235
|
areSkillsInstalled
|
|
225
236
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.6",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts",
|
|
64
64
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
65
65
|
"typecheck": "tsc --noEmit",
|
|
66
|
-
"prepublishOnly": "bun run build"
|
|
66
|
+
"prepublishOnly": "bun run typecheck && bun run build"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@clack/prompts": "^0.11.0",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@harmony/memory": "workspace:*",
|
|
78
|
+
"@types/bun": "^1.3.13",
|
|
78
79
|
"@types/node": "^25.5.0",
|
|
79
80
|
"typescript": "^6.0.1"
|
|
80
81
|
}
|
package/src/api-client.ts
CHANGED
|
@@ -155,15 +155,21 @@ export class HarmonyApiClient {
|
|
|
155
155
|
private apiKey: string;
|
|
156
156
|
private apiUrl: string;
|
|
157
157
|
private onUnauthorized?: () => void;
|
|
158
|
+
private refreshCredential?: () => Promise<string | null>;
|
|
158
159
|
|
|
159
160
|
constructor(options?: {
|
|
160
161
|
apiKey?: string;
|
|
161
162
|
apiUrl?: string;
|
|
162
163
|
onUnauthorized?: () => void;
|
|
164
|
+
// Called once on a 401 to obtain a fresh credential (OAuth refresh in stdio
|
|
165
|
+
// mode). Returns the new credential to retry with, or null to give up and
|
|
166
|
+
// surface HarmonyUnauthorizedError.
|
|
167
|
+
refreshCredential?: () => Promise<string | null>;
|
|
163
168
|
}) {
|
|
164
169
|
this.apiKey = options?.apiKey ?? getApiKey();
|
|
165
170
|
this.apiUrl = options?.apiUrl ?? getApiUrl();
|
|
166
171
|
this.onUnauthorized = options?.onUnauthorized;
|
|
172
|
+
this.refreshCredential = options?.refreshCredential;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
getApiUrl(): string {
|
|
@@ -217,6 +223,7 @@ export class HarmonyApiClient {
|
|
|
217
223
|
): Promise<T> {
|
|
218
224
|
const url = `${this.apiUrl}/v1${path}`;
|
|
219
225
|
let lastError: Error | null = null;
|
|
226
|
+
let refreshed = false;
|
|
220
227
|
const contentType = options?.contentType || "application/json";
|
|
221
228
|
const accept = options?.accept || "application/json";
|
|
222
229
|
|
|
@@ -253,10 +260,26 @@ export class HarmonyApiClient {
|
|
|
253
260
|
? null
|
|
254
261
|
: `API error: ${response.status} (non-JSON response)`) ||
|
|
255
262
|
`API error: ${response.status}`;
|
|
256
|
-
// 401:
|
|
257
|
-
//
|
|
258
|
-
//
|
|
263
|
+
// 401: credential rejected. In stdio mode we own an OAuth token, so
|
|
264
|
+
// try a one-shot refresh and retry with the new credential. If there
|
|
265
|
+
// is no refresher (remote mode) or it fails, surface a typed error so
|
|
266
|
+
// the transport layer can issue an HTTP 401 challenge / re-auth.
|
|
259
267
|
if (response.status === 401) {
|
|
268
|
+
if (this.refreshCredential && !refreshed) {
|
|
269
|
+
refreshed = true;
|
|
270
|
+
const fresh = await this.refreshCredential();
|
|
271
|
+
if (fresh) {
|
|
272
|
+
this.apiKey = fresh;
|
|
273
|
+
// A credential refresh is a one-shot, not a network retry, so
|
|
274
|
+
// give back the attempt slot: a 401 on the final iteration
|
|
275
|
+
// (after transient errors burned the budget) must still
|
|
276
|
+
// re-issue with the fresh token rather than throw the stale
|
|
277
|
+
// error. The `refreshed` guard caps this at one extra pass — a
|
|
278
|
+
// second 401 falls through to the unauthorized throw.
|
|
279
|
+
attempt--;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
260
283
|
this.onUnauthorized?.();
|
|
261
284
|
throw new HarmonyUnauthorizedError(errorMsg);
|
|
262
285
|
}
|
|
@@ -298,6 +321,7 @@ export class HarmonyApiClient {
|
|
|
298
321
|
): Promise<string> {
|
|
299
322
|
const url = `${this.apiUrl}/v1${path}`;
|
|
300
323
|
let lastError: Error | null = null;
|
|
324
|
+
let refreshed = false;
|
|
301
325
|
const contentType = options?.contentType || "application/json";
|
|
302
326
|
const accept = options?.accept || "text/markdown";
|
|
303
327
|
|
|
@@ -323,6 +347,21 @@ export class HarmonyApiClient {
|
|
|
323
347
|
errorMsg = text || `API error: ${response.status}`;
|
|
324
348
|
}
|
|
325
349
|
if (response.status === 401) {
|
|
350
|
+
if (this.refreshCredential && !refreshed) {
|
|
351
|
+
refreshed = true;
|
|
352
|
+
const fresh = await this.refreshCredential();
|
|
353
|
+
if (fresh) {
|
|
354
|
+
this.apiKey = fresh;
|
|
355
|
+
// A credential refresh is a one-shot, not a network retry, so
|
|
356
|
+
// give back the attempt slot: a 401 on the final iteration
|
|
357
|
+
// (after transient errors burned the budget) must still
|
|
358
|
+
// re-issue with the fresh token rather than throw the stale
|
|
359
|
+
// error. The `refreshed` guard caps this at one extra pass — a
|
|
360
|
+
// second 401 falls through to the unauthorized throw.
|
|
361
|
+
attempt--;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
326
365
|
this.onUnauthorized?.();
|
|
327
366
|
throw new HarmonyUnauthorizedError(errorMsg);
|
|
328
367
|
}
|
|
@@ -1662,7 +1701,14 @@ let client: HarmonyApiClient | null = null;
|
|
|
1662
1701
|
|
|
1663
1702
|
export function getClient(): HarmonyApiClient {
|
|
1664
1703
|
if (!client) {
|
|
1665
|
-
|
|
1704
|
+
// stdio mode owns its OAuth token, so wire the refresh-on-401 path. Lazy
|
|
1705
|
+
// import avoids a module cycle (oauth-refresh → config, api-client → config).
|
|
1706
|
+
client = new HarmonyApiClient({
|
|
1707
|
+
refreshCredential: async () => {
|
|
1708
|
+
const { refreshOAuthToken } = await import("./oauth-refresh.js");
|
|
1709
|
+
return refreshOAuthToken();
|
|
1710
|
+
},
|
|
1711
|
+
});
|
|
1666
1712
|
}
|
|
1667
1713
|
return client;
|
|
1668
1714
|
}
|
package/src/cli.ts
CHANGED
|
@@ -59,7 +59,11 @@ program
|
|
|
59
59
|
console.log("Status: Configured\n");
|
|
60
60
|
|
|
61
61
|
console.log("API:");
|
|
62
|
-
|
|
62
|
+
if (globalConfig.oauthAccessToken) {
|
|
63
|
+
console.log(" Credential: Browser sign-in (OAuth, workspace-scoped)");
|
|
64
|
+
} else {
|
|
65
|
+
console.log(` Key: ${globalConfig.apiKey?.slice(0, 8)}...`);
|
|
66
|
+
}
|
|
63
67
|
console.log(` URL: ${globalConfig.apiUrl}`);
|
|
64
68
|
console.log(
|
|
65
69
|
` Email: ${globalConfig.userEmail ? maskEmail(globalConfig.userEmail) : "(not set)"}`,
|
|
@@ -131,6 +135,10 @@ program
|
|
|
131
135
|
activeWorkspaceId: null,
|
|
132
136
|
activeProjectId: null,
|
|
133
137
|
userEmail: null,
|
|
138
|
+
oauthAccessToken: null,
|
|
139
|
+
oauthRefreshToken: null,
|
|
140
|
+
oauthExpiresAt: null,
|
|
141
|
+
oauthClientId: null,
|
|
134
142
|
});
|
|
135
143
|
console.log("Configuration reset successfully");
|
|
136
144
|
console.log("\nTo reconfigure, run: npx @gethmy/mcp setup");
|
|
@@ -144,7 +152,10 @@ program
|
|
|
144
152
|
"Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)",
|
|
145
153
|
)
|
|
146
154
|
.option("-f, --force", "Overwrite existing configuration files")
|
|
147
|
-
.option(
|
|
155
|
+
.option(
|
|
156
|
+
"-k, --api-key <key>",
|
|
157
|
+
"DEPRECATED (insecure: key leaks via argv/shell history). For unattended CI only — interactive setup uses browser sign-in.",
|
|
158
|
+
)
|
|
148
159
|
.option("-e, --email <email>", "Your email for auto-assignment")
|
|
149
160
|
.option(
|
|
150
161
|
"-a, --agents <agents...>",
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,16 @@ export interface HarmonyConfig {
|
|
|
9
9
|
activeProjectId: string | null;
|
|
10
10
|
userEmail: string | null;
|
|
11
11
|
memoryDir: string | null;
|
|
12
|
+
// OAuth 2.1 credentials (card #364). Preferred over `apiKey` when present —
|
|
13
|
+
// workspace-scoped + expiring, refreshed via the rotating refresh token.
|
|
14
|
+
// Acquired by the loopback+PKCE browser flow in `setup` (see oauth-login.ts);
|
|
15
|
+
// `apiKey` stays as the legacy/CI credential.
|
|
16
|
+
oauthAccessToken: string | null;
|
|
17
|
+
oauthRefreshToken: string | null;
|
|
18
|
+
// Epoch milliseconds at which the access token expires.
|
|
19
|
+
oauthExpiresAt: number | null;
|
|
20
|
+
// The public client_id the tokens were issued to — required to refresh them.
|
|
21
|
+
oauthClientId: string | null;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
/**
|
|
@@ -35,18 +45,26 @@ export function getLocalConfigPath(cwd?: string): string {
|
|
|
35
45
|
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
function emptyConfig(): HarmonyConfig {
|
|
49
|
+
return {
|
|
50
|
+
apiKey: null,
|
|
51
|
+
apiUrl: DEFAULT_API_URL,
|
|
52
|
+
activeWorkspaceId: null,
|
|
53
|
+
activeProjectId: null,
|
|
54
|
+
userEmail: null,
|
|
55
|
+
memoryDir: null,
|
|
56
|
+
oauthAccessToken: null,
|
|
57
|
+
oauthRefreshToken: null,
|
|
58
|
+
oauthExpiresAt: null,
|
|
59
|
+
oauthClientId: null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
export function loadConfig(): HarmonyConfig {
|
|
39
64
|
const configPath = getConfigPath();
|
|
40
65
|
|
|
41
66
|
if (!existsSync(configPath)) {
|
|
42
|
-
return
|
|
43
|
-
apiKey: null,
|
|
44
|
-
apiUrl: DEFAULT_API_URL,
|
|
45
|
-
activeWorkspaceId: null,
|
|
46
|
-
activeProjectId: null,
|
|
47
|
-
userEmail: null,
|
|
48
|
-
memoryDir: null,
|
|
49
|
-
};
|
|
67
|
+
return emptyConfig();
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
try {
|
|
@@ -59,16 +77,16 @@ export function loadConfig(): HarmonyConfig {
|
|
|
59
77
|
activeProjectId: config.activeProjectId || null,
|
|
60
78
|
userEmail: config.userEmail || null,
|
|
61
79
|
memoryDir: config.memoryDir || null,
|
|
80
|
+
oauthAccessToken: config.oauthAccessToken || null,
|
|
81
|
+
oauthRefreshToken: config.oauthRefreshToken || null,
|
|
82
|
+
oauthExpiresAt:
|
|
83
|
+
typeof config.oauthExpiresAt === "number"
|
|
84
|
+
? config.oauthExpiresAt
|
|
85
|
+
: null,
|
|
86
|
+
oauthClientId: config.oauthClientId || null,
|
|
62
87
|
};
|
|
63
88
|
} catch {
|
|
64
|
-
return
|
|
65
|
-
apiKey: null,
|
|
66
|
-
apiUrl: DEFAULT_API_URL,
|
|
67
|
-
activeWorkspaceId: null,
|
|
68
|
-
activeProjectId: null,
|
|
69
|
-
userEmail: null,
|
|
70
|
-
memoryDir: null,
|
|
71
|
-
};
|
|
89
|
+
return emptyConfig();
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
92
|
|
|
@@ -131,15 +149,25 @@ export function hasLocalConfig(cwd?: string): boolean {
|
|
|
131
149
|
return existsSync(getLocalConfigPath(cwd));
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
|
|
152
|
+
/**
|
|
153
|
+
* The credential the runtime should send as `X-API-Key`. harmony-api accepts
|
|
154
|
+
* both an OAuth access token (`hmy_at_`) and a legacy key (`hmy_`) on that
|
|
155
|
+
* header, so callers don't branch. Prefers a fresh OAuth access token, then a
|
|
156
|
+
* (possibly stale) OAuth access token the caller can refresh on 401, then the
|
|
157
|
+
* legacy `apiKey`.
|
|
158
|
+
*/
|
|
159
|
+
export function getActiveCredential(): string {
|
|
135
160
|
const config = loadConfig();
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
161
|
+
if (config.oauthAccessToken) return config.oauthAccessToken;
|
|
162
|
+
if (config.apiKey) return config.apiKey;
|
|
163
|
+
throw new Error(
|
|
164
|
+
'Not configured. Run "npx @gethmy/mcp setup" to connect Harmony.\n' +
|
|
165
|
+
"Setup authorizes in your browser — no API key handling required.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getApiKey(): string {
|
|
170
|
+
return getActiveCredential();
|
|
143
171
|
}
|
|
144
172
|
|
|
145
173
|
export function getApiUrl(): string {
|
|
@@ -203,7 +231,7 @@ export function getActiveProjectId(cwd?: string): string | null {
|
|
|
203
231
|
|
|
204
232
|
export function isConfigured(): boolean {
|
|
205
233
|
const config = loadConfig();
|
|
206
|
-
return !!config.apiKey;
|
|
234
|
+
return !!(config.apiKey || config.oauthAccessToken);
|
|
207
235
|
}
|
|
208
236
|
|
|
209
237
|
/**
|
package/src/graph-expansion.ts
CHANGED
|
@@ -36,8 +36,9 @@ export async function autoExpandGraph(
|
|
|
36
36
|
limit: 20,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Filter: exclude self, only high-enough confidence results
|
|
40
|
+
candidates = (entities as Array<{ id: string; confidence?: number }>)
|
|
41
|
+
.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4)
|
|
41
42
|
.slice(0, maxRelations);
|
|
42
43
|
|
|
43
44
|
// Retry once after 2s if no candidates found (handles embedding generation race)
|
|
@@ -47,8 +48,10 @@ export async function autoExpandGraph(
|
|
|
47
48
|
project_id: projectId,
|
|
48
49
|
limit: 20,
|
|
49
50
|
});
|
|
50
|
-
candidates = (
|
|
51
|
-
.
|
|
51
|
+
candidates = (
|
|
52
|
+
retry.entities as Array<{ id: string; confidence?: number }>
|
|
53
|
+
)
|
|
54
|
+
.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4)
|
|
52
55
|
.slice(0, maxRelations);
|
|
53
56
|
}
|
|
54
57
|
|
package/src/http.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { serve } from "bun";
|
|
14
|
-
import { Hono } from "hono";
|
|
14
|
+
import { type Context, Hono } from "hono";
|
|
15
15
|
import { cors } from "hono/cors";
|
|
16
16
|
import { loadConfig } from "./config.js";
|
|
17
17
|
|
|
@@ -122,18 +122,7 @@ app.get("/", (c) =>
|
|
|
122
122
|
);
|
|
123
123
|
|
|
124
124
|
// Generic proxy handler
|
|
125
|
-
async function handleRequest(
|
|
126
|
-
c: {
|
|
127
|
-
req: {
|
|
128
|
-
method: string;
|
|
129
|
-
url: string;
|
|
130
|
-
header: (name: string) => string | undefined;
|
|
131
|
-
};
|
|
132
|
-
json: () => Promise<unknown>;
|
|
133
|
-
},
|
|
134
|
-
method: string,
|
|
135
|
-
path: string,
|
|
136
|
-
) {
|
|
125
|
+
async function handleRequest(c: Context, method: string, path: string) {
|
|
137
126
|
try {
|
|
138
127
|
const authHeader = c.req.header("Authorization");
|
|
139
128
|
const apiKey = c.req.header("X-API-Key");
|
|
@@ -141,7 +130,7 @@ async function handleRequest(
|
|
|
141
130
|
let body: unknown;
|
|
142
131
|
if (["POST", "PATCH", "PUT"].includes(method)) {
|
|
143
132
|
try {
|
|
144
|
-
body = await c.json();
|
|
133
|
+
body = await c.req.json();
|
|
145
134
|
} catch {
|
|
146
135
|
// No body or invalid JSON
|
|
147
136
|
}
|
package/src/memory-floor.ts
CHANGED
|
@@ -55,6 +55,7 @@ export type FloorRuleId =
|
|
|
55
55
|
| "frequency-meta"
|
|
56
56
|
| "self-referential"
|
|
57
57
|
| "bare-type-prefix"
|
|
58
|
+
| "impl-detail-lesson"
|
|
58
59
|
| "specificity-floor"
|
|
59
60
|
| "length-floor"
|
|
60
61
|
| "operational-data-ban";
|
|
@@ -218,7 +219,7 @@ export function validateMemoryQuality(
|
|
|
218
219
|
rule: "impl-detail-lesson",
|
|
219
220
|
message:
|
|
220
221
|
"Lesson body reads as an implementation TODO/issue, not a learning. Convert to a positive takeaway or move to a code comment.",
|
|
221
|
-
}
|
|
222
|
+
};
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// Rule 4: Specificity floor (skipped for doc-source imports — they may
|