@gethmy/mcp 2.1.2 → 2.2.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/src/server.ts CHANGED
@@ -85,6 +85,155 @@ export interface ToolDeps {
85
85
  resetClient: () => void;
86
86
  }
87
87
 
88
+ // --- Memory Session Tracking ---
89
+ // Tracks memory operations per card session for visibility in AgentCardFooter
90
+
91
+ interface MemorySessionState {
92
+ cardId: string;
93
+ agentIdentifier: string;
94
+ agentName: string;
95
+ memoryReadCount: number;
96
+ pendingActions: { action: string; ts: string }[];
97
+ allActions: { action: string; ts: string }[];
98
+ dirty: boolean;
99
+ }
100
+
101
+ const memorySessions = new Map<string, MemorySessionState>();
102
+
103
+ function initMemorySession(
104
+ cardId: string,
105
+ agentIdentifier: string,
106
+ agentName: string,
107
+ ): void {
108
+ memorySessions.set(cardId, {
109
+ cardId,
110
+ agentIdentifier,
111
+ agentName,
112
+ memoryReadCount: 0,
113
+ pendingActions: [],
114
+ allActions: [],
115
+ dirty: false,
116
+ });
117
+ }
118
+
119
+ function getMemorySession(cardId: string): MemorySessionState | undefined {
120
+ return memorySessions.get(cardId);
121
+ }
122
+
123
+ function appendMemoryAction(cardId: string, action: string): void {
124
+ const session = memorySessions.get(cardId);
125
+ if (!session) return;
126
+ const truncated = action.length > 512 ? action.slice(0, 509) + "..." : action;
127
+ const entry = { action: truncated, ts: new Date().toISOString() };
128
+ session.pendingActions.push(entry);
129
+ session.dirty = true;
130
+ }
131
+
132
+ function incrementMemoryReads(cardId: string): void {
133
+ const session = memorySessions.get(cardId);
134
+ if (!session) return;
135
+ session.memoryReadCount++;
136
+ session.dirty = true;
137
+ }
138
+
139
+ /**
140
+ * Flush pending memory actions to the backend via updateAgentProgress.
141
+ * Fire-and-forget: errors are logged but never thrown.
142
+ */
143
+ async function flushMemoryActions(
144
+ client: HarmonyApiClient,
145
+ cardId: string,
146
+ ): Promise<void> {
147
+ const session = memorySessions.get(cardId);
148
+ if (!session || !session.dirty) return;
149
+
150
+ try {
151
+ // Batch reads into a single summary line
152
+ if (session.memoryReadCount > 0) {
153
+ session.allActions.push({
154
+ action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
155
+ ts: new Date().toISOString(),
156
+ });
157
+ session.memoryReadCount = 0;
158
+ }
159
+
160
+ // Move pending writes to allActions
161
+ if (session.pendingActions.length > 0) {
162
+ session.allActions.push(...session.pendingActions);
163
+ session.pendingActions = [];
164
+ }
165
+
166
+ // Trim to max 10 (drop oldest)
167
+ if (session.allActions.length > 10) {
168
+ session.allActions = session.allActions.slice(-10);
169
+ }
170
+
171
+ await client.updateAgentProgress(cardId, {
172
+ agentIdentifier: session.agentIdentifier,
173
+ agentName: session.agentName,
174
+ recentActions: session.allActions,
175
+ });
176
+
177
+ session.dirty = false;
178
+ } catch (err) {
179
+ // Fire-and-forget: log but don't propagate
180
+ console.error("[memory-session] flush failed:", err);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Merge memory actions into an existing recentActions array from a progress update.
186
+ * Returns the merged array (caller's actions first, then memory actions appended).
187
+ */
188
+ function mergeMemoryActionsInto(
189
+ cardId: string,
190
+ callerActions: { action: string; ts: string }[],
191
+ ): { action: string; ts: string }[] {
192
+ const session = memorySessions.get(cardId);
193
+ if (!session) return callerActions;
194
+
195
+ // Flush reads into allActions
196
+ if (session.memoryReadCount > 0) {
197
+ session.allActions.push({
198
+ action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
199
+ ts: new Date().toISOString(),
200
+ });
201
+ session.memoryReadCount = 0;
202
+ }
203
+
204
+ // Move pending to allActions
205
+ if (session.pendingActions.length > 0) {
206
+ session.allActions.push(...session.pendingActions);
207
+ session.pendingActions = [];
208
+ }
209
+
210
+ // Merge: caller actions + memory actions, trim to 10
211
+ const merged = [...callerActions, ...session.allActions];
212
+ const trimmed = merged.length > 10 ? merged.slice(-10) : merged;
213
+
214
+ // Update allActions to the merged state
215
+ session.allActions = trimmed;
216
+ session.dirty = false;
217
+
218
+ return trimmed;
219
+ }
220
+
221
+ /**
222
+ * Get the first (and typically only) active memory session.
223
+ * Memory tools don't carry a cardId, so we find the active session.
224
+ */
225
+ function getActiveMemorySession(): MemorySessionState | undefined {
226
+ // Return the first active session (there should be at most one)
227
+ for (const session of memorySessions.values()) {
228
+ return session;
229
+ }
230
+ return undefined;
231
+ }
232
+
233
+ function cleanupMemorySession(cardId: string): void {
234
+ memorySessions.delete(cardId);
235
+ }
236
+
88
237
  // Tool definitions
89
238
  const TOOLS = {
90
239
  // Card operations
@@ -1838,7 +1987,7 @@ async function handleToolCall(
1838
1987
  if (!unauthenticatedTools.includes(name) && !deps.isConfigured()) {
1839
1988
  throw new Error(
1840
1989
  'Not configured. Run "npx @gethmy/mcp setup" to set your API key.\n' +
1841
- "You can generate an API key at https://gethmy.com → Settings → API Keys.\n" +
1990
+ "You can generate an API key at https://app.gethmy.com → Settings → API Keys.\n" +
1842
1991
  'Or use "harmony_onboard" to create an account and configure automatically.',
1843
1992
  );
1844
1993
  }
@@ -2267,7 +2416,10 @@ async function handleToolCall(
2267
2416
  });
2268
2417
 
2269
2418
  // Mark as explicit so auto-session won't interfere
2270
- markExplicit(cardId);
2419
+ markExplicit(cardId, { agentIdentifier, agentName });
2420
+
2421
+ // Initialize memory session tracking for action visibility
2422
+ initMemorySession(cardId, agentIdentifier, agentName);
2271
2423
 
2272
2424
  // Prefetch relevant context (non-blocking, best-effort)
2273
2425
  let prefetchedMemoryIds: string[] = [];
@@ -2348,6 +2500,21 @@ async function handleToolCall(
2348
2500
  args.progressPercent !== undefined
2349
2501
  ? z.number().min(0).max(100).parse(args.progressPercent)
2350
2502
  : undefined;
2503
+ // Merge any pending memory actions into the progress update
2504
+ const callerRecentActions = args.recentActions as
2505
+ | { action: string; ts: string }[]
2506
+ | undefined;
2507
+ const memSession = getMemorySession(cardId);
2508
+ let mergedRecentActions: { action: string; ts: string }[] | undefined;
2509
+ if (memSession?.dirty) {
2510
+ mergedRecentActions = mergeMemoryActionsInto(
2511
+ cardId,
2512
+ callerRecentActions || [],
2513
+ );
2514
+ } else if (callerRecentActions) {
2515
+ mergedRecentActions = callerRecentActions;
2516
+ }
2517
+
2351
2518
  const result = await client.updateAgentProgress(cardId, {
2352
2519
  agentIdentifier,
2353
2520
  agentName,
@@ -2363,6 +2530,7 @@ async function handleToolCall(
2363
2530
  estimatedMinutesRemaining: args.estimatedMinutesRemaining as
2364
2531
  | number
2365
2532
  | undefined,
2533
+ ...(mergedRecentActions && { recentActions: mergedRecentActions }),
2366
2534
  });
2367
2535
 
2368
2536
  // Mid-session learning extraction (fire-and-forget)
@@ -2405,6 +2573,10 @@ async function handleToolCall(
2405
2573
  ? z.number().min(0).max(100).parse(args.progressPercent)
2406
2574
  : undefined;
2407
2575
 
2576
+ // Final flush of any pending memory actions before ending the session
2577
+ await flushMemoryActions(client, cardId);
2578
+ cleanupMemorySession(cardId);
2579
+
2408
2580
  const result = await client.endAgentSession(cardId, {
2409
2581
  status: sessionStatus,
2410
2582
  progressPercent: endProgressPercent,
@@ -2689,6 +2861,8 @@ async function handleToolCall(
2689
2861
  }
2690
2862
  const entityType = (args.type as string) || "context";
2691
2863
  const entityTags = (args.tags as string[]) || [];
2864
+ // Use session's agent identifier if available, otherwise null
2865
+ const activeMemSession = getActiveMemorySession();
2692
2866
  const result = await client.createMemoryEntity({
2693
2867
  workspace_id: workspaceId,
2694
2868
  project_id:
@@ -2704,7 +2878,7 @@ async function handleToolCall(
2704
2878
  ? z.number().min(0).max(1).parse(args.confidence)
2705
2879
  : undefined,
2706
2880
  tags: entityTags.length > 0 ? entityTags : undefined,
2707
- agent_identifier: "claude-code",
2881
+ agent_identifier: activeMemSession?.agentIdentifier || undefined,
2708
2882
  });
2709
2883
 
2710
2884
  // Fire-and-forget graph expansion: link new entity to semantically similar ones
@@ -2748,6 +2922,12 @@ async function handleToolCall(
2748
2922
  }
2749
2923
  }
2750
2924
 
2925
+ // Track memory write action and flush (fire-and-forget)
2926
+ if (activeMemSession) {
2927
+ appendMemoryAction(activeMemSession.cardId, `Stored memory: ${title}`);
2928
+ flushMemoryActions(client, activeMemSession.cardId).catch(() => {});
2929
+ }
2930
+
2751
2931
  return {
2752
2932
  success: true,
2753
2933
  ...result,
@@ -2830,6 +3010,12 @@ async function handleToolCall(
2830
3010
  ).catch(() => {});
2831
3011
  }
2832
3012
 
3013
+ // Track memory read (batched on flush)
3014
+ const recallMemSession = getActiveMemorySession();
3015
+ if (recallMemSession) {
3016
+ incrementMemoryReads(recallMemSession.cardId);
3017
+ }
3018
+
2833
3019
  return markdown || "No memories found.";
2834
3020
  }
2835
3021
 
@@ -2847,12 +3033,48 @@ async function handleToolCall(
2847
3033
  updates.confidence = z.number().min(0).max(1).parse(args.confidence);
2848
3034
  if (args.metadata !== undefined) updates.metadata = args.metadata;
2849
3035
  const result = await client.updateMemoryEntity(entityId, updates);
3036
+
3037
+ // Track memory write action and flush (fire-and-forget)
3038
+ const updateMemSession = getActiveMemorySession();
3039
+ if (updateMemSession) {
3040
+ const updateTitle =
3041
+ (updates.title as string) || entityId.slice(0, 8);
3042
+ appendMemoryAction(
3043
+ updateMemSession.cardId,
3044
+ `Updated memory: ${updateTitle}`,
3045
+ );
3046
+ flushMemoryActions(client, updateMemSession.cardId).catch(() => {});
3047
+ }
3048
+
2850
3049
  return { success: true, ...result };
2851
3050
  }
2852
3051
 
2853
3052
  case "harmony_forget": {
2854
3053
  const entityId = z.string().uuid().parse(args.entityId);
3054
+
3055
+ // Fetch title before deletion for action tracking
3056
+ let forgetTitle: string | null = null;
3057
+ const forgetMemSession = getActiveMemorySession();
3058
+ if (forgetMemSession) {
3059
+ try {
3060
+ const { entity } = await client.getMemoryEntity(entityId);
3061
+ forgetTitle = (entity as { title?: string })?.title || null;
3062
+ } catch {
3063
+ // Non-fatal: use truncated ID if fetch fails
3064
+ }
3065
+ }
3066
+
2855
3067
  await client.deleteMemoryEntity(entityId);
3068
+
3069
+ // Track memory write action and flush (fire-and-forget)
3070
+ if (forgetMemSession) {
3071
+ appendMemoryAction(
3072
+ forgetMemSession.cardId,
3073
+ `Removed memory: ${forgetTitle || entityId.slice(0, 8)}`,
3074
+ );
3075
+ flushMemoryActions(client, forgetMemSession.cardId).catch(() => {});
3076
+ }
3077
+
2856
3078
  return { success: true };
2857
3079
  }
2858
3080
 
@@ -2880,6 +3102,17 @@ async function handleToolCall(
2880
3102
  ? z.number().min(0).max(1).parse(args.confidence)
2881
3103
  : undefined,
2882
3104
  });
3105
+
3106
+ // Track memory write action and flush (fire-and-forget)
3107
+ const relateMemSession = getActiveMemorySession();
3108
+ if (relateMemSession) {
3109
+ appendMemoryAction(
3110
+ relateMemSession.cardId,
3111
+ `Linked memories: ${relationType}`,
3112
+ );
3113
+ flushMemoryActions(client, relateMemSession.cardId).catch(() => {});
3114
+ }
3115
+
2883
3116
  return { success: true, ...result };
2884
3117
  }
2885
3118
 
@@ -2904,6 +3137,13 @@ async function handleToolCall(
2904
3137
  limit: args.limit as number | undefined,
2905
3138
  },
2906
3139
  );
3140
+
3141
+ // Track memory read (batched on flush)
3142
+ const searchMemSession = getActiveMemorySession();
3143
+ if (searchMemSession) {
3144
+ incrementMemoryReads(searchMemSession.cardId);
3145
+ }
3146
+
2907
3147
  return markdown || "No memories found.";
2908
3148
  }
2909
3149
 
@@ -3039,7 +3279,7 @@ async function handleToolCall(
3039
3279
  });
3040
3280
 
3041
3281
  // Build URL for viewing the plan
3042
- const planUrl = `https://gethmy.com/plans/${(result.plan as { id: string }).id}`;
3282
+ const planUrl = `https://app.gethmy.com/plans/${(result.plan as { id: string }).id}`;
3043
3283
 
3044
3284
  return {
3045
3285
  success: true,
package/src/skills.ts CHANGED
@@ -251,7 +251,7 @@ Once you have the plan ID, call \`harmony_get_plan\` to fetch the full plan with
251
251
  ## [Plan Title]
252
252
  **Status:** draft/active/archived | **Phase:** plan/execute/verify/done
253
253
  **Tasks:** N total (X pending, Y in_progress, Z completed)
254
- **URL:** https://gethmy.com/plans/{id}
254
+ **URL:** https://app.gethmy.com/plans/{id}
255
255
  \\\`\\\`\\\`
256
256
 
257
257
  If plan is in execute phase and tasks already have linked cards, note which tasks have cards and which don't.
@@ -285,7 +285,7 @@ Only offer **(A) Single card**, **(C) Analyze only**, or **(D) Skip**.
285
285
  #### Option A — Single card
286
286
  1. Call \`harmony_create_card\` with:
287
287
  - \`title\`: Plan title
288
- - \`description\`: Brief 2-3 sentence summary of the plan + \`\\n\\n[View plan](https://gethmy.com/plans/{planId})\`
288
+ - \`description\`: Brief 2-3 sentence summary of the plan + \`\\n\\n[View plan](https://app.gethmy.com/plans/{planId})\`
289
289
  - \`priority\`: based on plan task priorities (use highest)
290
290
  2. Call \`harmony_update_plan\` to set \`status: "active"\`, \`workflowPhase: "execute"\`
291
291
 
@@ -407,7 +407,7 @@ Call \`harmony_create_plan\` with:
407
407
  ### 2B.5 — User Approval
408
408
 
409
409
  Show the user:
410
- - Plan URL: \`https://gethmy.com/plans/{id}\`
410
+ - Plan URL: \`https://app.gethmy.com/plans/{id}\`
411
411
  - Number of tasks created
412
412
  - Brief summary
413
413
 
package/src/tui/setup.ts CHANGED
@@ -41,7 +41,7 @@ export interface SetupOptions {
41
41
  const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
42
42
 
43
43
  // API base URL
44
- const API_URL = "https://gethmy.com/api";
44
+ const API_URL = "https://app.gethmy.com/api";
45
45
 
46
46
  /**
47
47
  * Register MCP server using Claude CLI
@@ -536,7 +536,7 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
536
536
  if (!validation.valid) {
537
537
  spinner.stop(colors.error("API key validation failed"));
538
538
  p.log.error(validation.error || "Could not connect to Harmony API");
539
- p.log.info("Get an API key at: https://gethmy.com/user/keys");
539
+ p.log.info("Get an API key at: https://app.gethmy.com/user/keys");
540
540
  process.exit(1);
541
541
  }
542
542
 
@@ -797,7 +797,9 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
797
797
  }
798
798
 
799
799
  if (allFiles.length > 0) {
800
- const summary = getWriteSummary(allFiles, { force: options.force });
800
+ const summary = getWriteSummary(allFiles, {
801
+ force: options.force || needsSkills,
802
+ });
801
803
 
802
804
  if (summary.toCreate.length > 0) {
803
805
  console.log("");
@@ -850,8 +852,12 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
850
852
  console.log("");
851
853
 
852
854
  // Step 7: Write files
855
+ // Force-write when user chose to reinstall skills — skill files are
856
+ // package-generated content that should always match the installed version.
853
857
  if (allFiles.length > 0) {
854
- await writeFilesWithProgress(allFiles, { force: options.force });
858
+ await writeFilesWithProgress(allFiles, {
859
+ force: options.force || needsSkills,
860
+ });
855
861
  }
856
862
 
857
863
  // Step 8: Create symlinks
@@ -968,7 +974,7 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
968
974
  console.log("");
969
975
  console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
970
976
  console.log(
971
- ` ${colors.dim("Need help? Visit https://gethmy.com/docs/mcp")}`,
977
+ ` ${colors.dim("Need help? Visit https://app.gethmy.com/docs/mcp")}`,
972
978
  );
973
979
  console.log("");
974
980
  }