@gethmy/mcp 2.1.3 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2279 -2017
- package/dist/index.js +24656 -24394
- package/dist/lib/__tests__/auto-session.test.js +33 -33
- package/dist/lib/api-client.js +116 -0
- package/dist/lib/auto-session.js +49 -8
- package/dist/lib/cli.js +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/http.js +1 -0
- package/dist/lib/prompt-builder.js +1 -0
- package/dist/lib/remote.js +2 -2
- package/dist/lib/server.js +200 -100
- package/dist/lib/skills.js +3 -3
- package/dist/lib/tui/setup.js +3 -3
- package/package.json +1 -1
- package/src/__tests__/auto-session.test.ts +33 -33
- package/src/api-client.ts +205 -0
- package/src/auto-session.ts +64 -7
- package/src/cli.ts +1 -1
- package/src/config.ts +1 -1
- package/src/http.ts +1 -0
- package/src/prompt-builder.ts +3 -0
- package/src/remote.ts +3 -2
- package/src/server.ts +267 -121
- package/src/skills.ts +3 -3
- package/src/tui/setup.ts +3 -3
package/src/api-client.ts
CHANGED
|
@@ -952,6 +952,211 @@ export class HarmonyApiClient {
|
|
|
952
952
|
}> {
|
|
953
953
|
return this.request("POST", "/api-keys", { name });
|
|
954
954
|
}
|
|
955
|
+
|
|
956
|
+
// ============ PROMPT GENERATION ============
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Generate a prompt for a card with full memory context assembly.
|
|
960
|
+
*
|
|
961
|
+
* This is the shared entry point for prompt generation — used by the MCP
|
|
962
|
+
* server tool handler and the agent daemon. It fetches the card, assembles
|
|
963
|
+
* relevant memories, and produces a role-framed prompt.
|
|
964
|
+
*/
|
|
965
|
+
async generateCardPrompt(options: {
|
|
966
|
+
cardId: string;
|
|
967
|
+
workspaceId: string;
|
|
968
|
+
projectId?: string;
|
|
969
|
+
variant?: "analysis" | "draft" | "execute";
|
|
970
|
+
customConstraints?: string;
|
|
971
|
+
contextOptions?: Partial<{
|
|
972
|
+
includeSubtasks: boolean;
|
|
973
|
+
includeLinks: boolean;
|
|
974
|
+
includeDescription: boolean;
|
|
975
|
+
}>;
|
|
976
|
+
}): Promise<{
|
|
977
|
+
prompt: string;
|
|
978
|
+
variant: string;
|
|
979
|
+
category: string;
|
|
980
|
+
role: string;
|
|
981
|
+
contextSummary: {
|
|
982
|
+
hasDescription: boolean;
|
|
983
|
+
labelCount: number;
|
|
984
|
+
subtaskCount: number;
|
|
985
|
+
completedSubtasks: number;
|
|
986
|
+
linkedCardCount: number;
|
|
987
|
+
memoryCount: number;
|
|
988
|
+
};
|
|
989
|
+
tokenEstimate: number;
|
|
990
|
+
assemblyId?: string;
|
|
991
|
+
cardId: string;
|
|
992
|
+
shortId: number;
|
|
993
|
+
title: string;
|
|
994
|
+
}> {
|
|
995
|
+
const { assembleContext, cacheManifest, generatePrompt } =
|
|
996
|
+
await loadPromptModules();
|
|
997
|
+
|
|
998
|
+
// Fetch card data
|
|
999
|
+
const cardResult = await this.getCard(options.cardId);
|
|
1000
|
+
const cardData = cardResult.card as CardPromptData;
|
|
1001
|
+
|
|
1002
|
+
// Try to get column info
|
|
1003
|
+
let columnData: { name: string } | null = null;
|
|
1004
|
+
const projectIdForBoard = options.projectId || cardData.project_id;
|
|
1005
|
+
if (projectIdForBoard) {
|
|
1006
|
+
try {
|
|
1007
|
+
const board = await this.getBoard(projectIdForBoard, { summary: true });
|
|
1008
|
+
const column = (
|
|
1009
|
+
board.columns as Array<{ id: string; name: string }>
|
|
1010
|
+
).find((col) => col.id === cardData.column_id);
|
|
1011
|
+
if (column) {
|
|
1012
|
+
columnData = { name: column.name };
|
|
1013
|
+
}
|
|
1014
|
+
} catch {
|
|
1015
|
+
// Column info not available, continue without it
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const variant = options.variant || "execute";
|
|
1020
|
+
|
|
1021
|
+
// Assemble memory context
|
|
1022
|
+
let assembledContextStr: string | undefined;
|
|
1023
|
+
let assemblyId: string | undefined;
|
|
1024
|
+
let memories: MemoryItem[] | undefined;
|
|
1025
|
+
|
|
1026
|
+
try {
|
|
1027
|
+
if (options.workspaceId && cardData.title) {
|
|
1028
|
+
const cardLabels = (cardData.labels || []).map((l) => l.name);
|
|
1029
|
+
const taskContext = [cardData.title, cardData.description || ""]
|
|
1030
|
+
.filter(Boolean)
|
|
1031
|
+
.join(" ");
|
|
1032
|
+
|
|
1033
|
+
const assembled = await assembleContext({
|
|
1034
|
+
workspaceId: options.workspaceId,
|
|
1035
|
+
projectId: options.projectId,
|
|
1036
|
+
taskContext,
|
|
1037
|
+
cardLabels,
|
|
1038
|
+
cardId: cardData.id,
|
|
1039
|
+
client: this,
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
if (assembled.context) {
|
|
1043
|
+
assembledContextStr = assembled.context;
|
|
1044
|
+
assemblyId = assembled.manifest.assemblyId;
|
|
1045
|
+
cacheManifest(assembled.manifest);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
// Context assembly failed, try legacy fallback
|
|
1050
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1051
|
+
console.debug(`[generateCardPrompt] Context assembly failed: ${msg}`);
|
|
1052
|
+
try {
|
|
1053
|
+
if (options.workspaceId && cardData.title) {
|
|
1054
|
+
const memoryResult = await this.searchMemoryEntities(
|
|
1055
|
+
options.workspaceId,
|
|
1056
|
+
cardData.title,
|
|
1057
|
+
{
|
|
1058
|
+
project_id: options.projectId,
|
|
1059
|
+
limit: 5,
|
|
1060
|
+
},
|
|
1061
|
+
);
|
|
1062
|
+
if (memoryResult.entities?.length > 0) {
|
|
1063
|
+
memories = (memoryResult.entities as MemoryItem[]).map((e) => ({
|
|
1064
|
+
id: e.id,
|
|
1065
|
+
type: e.type,
|
|
1066
|
+
title: e.title,
|
|
1067
|
+
content: e.content,
|
|
1068
|
+
confidence: e.confidence,
|
|
1069
|
+
tags: e.tags || [],
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
} catch (fallbackErr) {
|
|
1074
|
+
const fallbackMsg =
|
|
1075
|
+
fallbackErr instanceof Error
|
|
1076
|
+
? fallbackErr.message
|
|
1077
|
+
: String(fallbackErr);
|
|
1078
|
+
console.debug(
|
|
1079
|
+
`[generateCardPrompt] Memory fallback also failed: ${fallbackMsg}`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const result = generatePrompt({
|
|
1085
|
+
card: cardData,
|
|
1086
|
+
column: columnData,
|
|
1087
|
+
variant,
|
|
1088
|
+
contextOptions: options.contextOptions,
|
|
1089
|
+
customConstraints: options.customConstraints,
|
|
1090
|
+
memories,
|
|
1091
|
+
assembledContext: assembledContextStr,
|
|
1092
|
+
assemblyId,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
...result,
|
|
1097
|
+
cardId: cardData.id,
|
|
1098
|
+
shortId: cardData.short_id,
|
|
1099
|
+
title: cardData.title,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Shared types for generateCardPrompt to avoid inline assertions
|
|
1105
|
+
interface CardPromptData {
|
|
1106
|
+
id: string;
|
|
1107
|
+
short_id: number;
|
|
1108
|
+
title: string;
|
|
1109
|
+
description?: string | null;
|
|
1110
|
+
priority: string;
|
|
1111
|
+
due_date?: string | null;
|
|
1112
|
+
done: boolean;
|
|
1113
|
+
labels?: Array<{ name: string; color: string }>;
|
|
1114
|
+
subtasks?: Array<{ title: string; completed: boolean }>;
|
|
1115
|
+
links?: Array<{
|
|
1116
|
+
target_card: { short_id: number; title: string };
|
|
1117
|
+
display_type: string;
|
|
1118
|
+
direction: "outgoing" | "incoming";
|
|
1119
|
+
}>;
|
|
1120
|
+
assignee?: { full_name?: string; email: string } | null;
|
|
1121
|
+
column_id?: string;
|
|
1122
|
+
project_id?: string;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
interface MemoryItem {
|
|
1126
|
+
id: string;
|
|
1127
|
+
type: string;
|
|
1128
|
+
title: string;
|
|
1129
|
+
content: string;
|
|
1130
|
+
confidence: number;
|
|
1131
|
+
tags: string[];
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Cached dynamic imports for context-assembly and prompt-builder
|
|
1135
|
+
let _promptModules: {
|
|
1136
|
+
assembleContext: Awaited<
|
|
1137
|
+
typeof import("./context-assembly.js")
|
|
1138
|
+
>["assembleContext"];
|
|
1139
|
+
cacheManifest: Awaited<
|
|
1140
|
+
typeof import("./context-assembly.js")
|
|
1141
|
+
>["cacheManifest"];
|
|
1142
|
+
generatePrompt: Awaited<
|
|
1143
|
+
typeof import("./prompt-builder.js")
|
|
1144
|
+
>["generatePrompt"];
|
|
1145
|
+
} | null = null;
|
|
1146
|
+
|
|
1147
|
+
async function loadPromptModules() {
|
|
1148
|
+
if (!_promptModules) {
|
|
1149
|
+
const [ca, pb] = await Promise.all([
|
|
1150
|
+
import("./context-assembly.js"),
|
|
1151
|
+
import("./prompt-builder.js"),
|
|
1152
|
+
]);
|
|
1153
|
+
_promptModules = {
|
|
1154
|
+
assembleContext: ca.assembleContext,
|
|
1155
|
+
cacheManifest: ca.cacheManifest,
|
|
1156
|
+
generatePrompt: pb.generatePrompt,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return _promptModules;
|
|
955
1160
|
}
|
|
956
1161
|
|
|
957
1162
|
// Singleton instance
|
package/src/auto-session.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Automatically detects agent session boundaries by monitoring tool calls.
|
|
5
5
|
* Sessions auto-start when card-mutating tools are called, and auto-end
|
|
6
6
|
* after 10 minutes of inactivity or when a different card is worked on.
|
|
7
|
+
*
|
|
8
|
+
* Agent identity is resolved from the MCP client's `initialize` handshake
|
|
9
|
+
* (clientInfo.name), so "Claude Code", "Cursor", "Windsurf", etc. are
|
|
10
|
+
* detected automatically — no hardcoded fallback needed.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import type { HarmonyApiClient } from "./api-client.js";
|
|
@@ -24,6 +28,43 @@ export type EndSessionCallback = (
|
|
|
24
28
|
status: "completed" | "paused",
|
|
25
29
|
) => Promise<void>;
|
|
26
30
|
|
|
31
|
+
/** MCP client identity from the `initialize` handshake */
|
|
32
|
+
export interface ClientInfo {
|
|
33
|
+
name: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Well-known MCP client names → human-friendly display names */
|
|
38
|
+
const CLIENT_DISPLAY_NAMES: Record<string, string> = {
|
|
39
|
+
"claude-code": "Claude Code",
|
|
40
|
+
"claude-desktop": "Claude Desktop",
|
|
41
|
+
cursor: "Cursor",
|
|
42
|
+
windsurf: "Windsurf",
|
|
43
|
+
cline: "Cline",
|
|
44
|
+
continue: "Continue",
|
|
45
|
+
"codex-cli": "OpenAI Codex",
|
|
46
|
+
zed: "Zed",
|
|
47
|
+
"gemini-cli": "Gemini CLI",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Derive a slug-style identifier from a client name */
|
|
51
|
+
function toIdentifier(name: string): string {
|
|
52
|
+
return name.toLowerCase().replace(/\s+/g, "-");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve agent identity from MCP client info */
|
|
56
|
+
export function resolveAgentIdentity(info: ClientInfo | null): {
|
|
57
|
+
agentIdentifier: string;
|
|
58
|
+
agentName: string;
|
|
59
|
+
} {
|
|
60
|
+
if (!info?.name) {
|
|
61
|
+
return { agentIdentifier: "unknown", agentName: "Unknown Agent" };
|
|
62
|
+
}
|
|
63
|
+
const key = toIdentifier(info.name);
|
|
64
|
+
const displayName = CLIENT_DISPLAY_NAMES[key] ?? info.name; // use raw name if not in map
|
|
65
|
+
return { agentIdentifier: key, agentName: displayName };
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
/** Tools that trigger auto-start of a session */
|
|
28
69
|
export const AUTO_START_TRIGGERS = new Set([
|
|
29
70
|
"harmony_generate_prompt",
|
|
@@ -42,18 +83,22 @@ const activeSessions = new Map<string, TrackedSession>();
|
|
|
42
83
|
let inactivityTimer: ReturnType<typeof setInterval> | null = null;
|
|
43
84
|
let endCallback: EndSessionCallback | null = null;
|
|
44
85
|
let clientGetter: (() => HarmonyApiClient) | null = null;
|
|
86
|
+
let clientInfoGetter: (() => ClientInfo | null) | null = null;
|
|
45
87
|
|
|
46
88
|
/**
|
|
47
89
|
* Initialize auto-session tracking.
|
|
48
90
|
* @param callback Called when an auto-session ends (runs the learning pipeline)
|
|
49
91
|
* @param getClient Function to get the current API client
|
|
92
|
+
* @param getClientInfo Function to get MCP client identity from the initialize handshake
|
|
50
93
|
*/
|
|
51
94
|
export function initAutoSession(
|
|
52
95
|
callback: EndSessionCallback,
|
|
53
96
|
getClient: () => HarmonyApiClient,
|
|
97
|
+
getClientInfo?: () => ClientInfo | null,
|
|
54
98
|
): void {
|
|
55
99
|
endCallback = callback;
|
|
56
100
|
clientGetter = getClient;
|
|
101
|
+
clientInfoGetter = getClientInfo ?? null;
|
|
57
102
|
|
|
58
103
|
if (inactivityTimer) clearInterval(inactivityTimer);
|
|
59
104
|
inactivityTimer = setInterval(checkInactivity, CHECK_INTERVAL_MS);
|
|
@@ -95,11 +140,15 @@ export async function trackActivity(
|
|
|
95
140
|
await autoEndSession(client, otherCardId, "completed");
|
|
96
141
|
}
|
|
97
142
|
|
|
143
|
+
// Resolve agent identity from MCP client info
|
|
144
|
+
const info = clientInfoGetter?.() ?? null;
|
|
145
|
+
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
146
|
+
|
|
98
147
|
// Start a new auto-session
|
|
99
148
|
try {
|
|
100
149
|
await client.startAgentSession(cardId, {
|
|
101
|
-
agentIdentifier
|
|
102
|
-
agentName
|
|
150
|
+
agentIdentifier,
|
|
151
|
+
agentName,
|
|
103
152
|
status: "working",
|
|
104
153
|
});
|
|
105
154
|
} catch {
|
|
@@ -111,18 +160,25 @@ export async function trackActivity(
|
|
|
111
160
|
startedAt: now,
|
|
112
161
|
lastActivityAt: now,
|
|
113
162
|
isExplicit: false,
|
|
114
|
-
agentIdentifier
|
|
115
|
-
agentName
|
|
163
|
+
agentIdentifier,
|
|
164
|
+
agentName,
|
|
116
165
|
});
|
|
117
166
|
}
|
|
118
167
|
|
|
119
168
|
/**
|
|
120
169
|
* Mark a session as explicitly started (won't be auto-ended by card switching or inactivity).
|
|
170
|
+
* Optionally accepts the real agent identifier/name to store in the session.
|
|
121
171
|
*/
|
|
122
|
-
export function markExplicit(
|
|
172
|
+
export function markExplicit(
|
|
173
|
+
cardId: string,
|
|
174
|
+
options?: { agentIdentifier?: string; agentName?: string },
|
|
175
|
+
): void {
|
|
123
176
|
const existing = activeSessions.get(cardId);
|
|
124
177
|
if (existing) {
|
|
125
178
|
existing.isExplicit = true;
|
|
179
|
+
if (options?.agentIdentifier)
|
|
180
|
+
existing.agentIdentifier = options.agentIdentifier;
|
|
181
|
+
if (options?.agentName) existing.agentName = options.agentName;
|
|
126
182
|
} else {
|
|
127
183
|
// Track the explicit session even if we didn't auto-start it
|
|
128
184
|
activeSessions.set(cardId, {
|
|
@@ -130,8 +186,8 @@ export function markExplicit(cardId: string): void {
|
|
|
130
186
|
startedAt: Date.now(),
|
|
131
187
|
lastActivityAt: Date.now(),
|
|
132
188
|
isExplicit: true,
|
|
133
|
-
agentIdentifier: "explicit",
|
|
134
|
-
agentName: "Explicit Agent",
|
|
189
|
+
agentIdentifier: options?.agentIdentifier ?? "explicit",
|
|
190
|
+
agentName: options?.agentName ?? "Explicit Agent",
|
|
135
191
|
});
|
|
136
192
|
}
|
|
137
193
|
}
|
|
@@ -169,6 +225,7 @@ export function destroyAutoSession(): void {
|
|
|
169
225
|
activeSessions.clear();
|
|
170
226
|
endCallback = null;
|
|
171
227
|
clientGetter = null;
|
|
228
|
+
clientInfoGetter = null;
|
|
172
229
|
}
|
|
173
230
|
|
|
174
231
|
/**
|
package/src/cli.ts
CHANGED
|
@@ -113,7 +113,7 @@ program
|
|
|
113
113
|
} else {
|
|
114
114
|
console.log("Status: Not configured\n");
|
|
115
115
|
console.log("Run: npx @gethmy/mcp setup");
|
|
116
|
-
console.log("Get an API key at: https://gethmy.com/user/keys");
|
|
116
|
+
console.log("Get an API key at: https://app.gethmy.com/user/keys");
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
package/src/config.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface LocalConfig {
|
|
|
20
20
|
projectId: string | null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const DEFAULT_API_URL = "https://gethmy.com/api";
|
|
23
|
+
const DEFAULT_API_URL = "https://app.gethmy.com/api";
|
|
24
24
|
const LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
25
25
|
|
|
26
26
|
export function getConfigDir(): string {
|
package/src/http.ts
CHANGED
package/src/prompt-builder.ts
CHANGED
|
@@ -438,6 +438,9 @@ export function generatePrompt(
|
|
|
438
438
|
roleFraming.focus.forEach((f) => {
|
|
439
439
|
sections.push(`- ${f}`);
|
|
440
440
|
});
|
|
441
|
+
sections.push(
|
|
442
|
+
`- **Memory:** When you discover important domain knowledge, architectural decisions, or infrastructure details, store them via \`harmony_remember\`. Focus on durable knowledge that future agents would benefit from — not ephemeral task details (those are auto-extracted from your session).`,
|
|
443
|
+
);
|
|
441
444
|
|
|
442
445
|
// Output suggestions
|
|
443
446
|
sections.push(`\n## Suggested Outputs`);
|
package/src/remote.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Claude.ai → POST https://mcp.gethmy.com/mcp (Bearer: hmy_xxx)
|
|
11
11
|
*
|
|
12
12
|
* Env vars:
|
|
13
|
-
* HARMONY_API_URL - Harmony API base URL (default: https://gethmy.com/api)
|
|
13
|
+
* HARMONY_API_URL - Harmony API base URL (default: https://app.gethmy.com/api)
|
|
14
14
|
* PORT - Listen port (default: 3002)
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -25,7 +25,8 @@ import { registerHandlers, type ToolDeps } from "./server.js";
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
// Config from env
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
|
-
const HARMONY_API_URL =
|
|
28
|
+
const HARMONY_API_URL =
|
|
29
|
+
process.env.HARMONY_API_URL || "https://app.gethmy.com/api";
|
|
29
30
|
const PORT = parseInt(process.env.PORT || "3002", 10);
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|