@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/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
@@ -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: "auto",
102
- agentName: "Auto-detected Agent",
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: "auto",
115
- agentName: "Auto-detected Agent",
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(cardId: string): void {
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
@@ -23,6 +23,7 @@ app.use(
23
23
  cors({
24
24
  origin: [
25
25
  "https://gethmy.com",
26
+ "https://app.gethmy.com",
26
27
  "http://localhost:8080",
27
28
  "http://localhost:3000",
28
29
  ],
@@ -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 = process.env.HARMONY_API_URL || "https://gethmy.com/api";
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
  // ---------------------------------------------------------------------------