@gethmy/mcp 2.1.3 → 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/dist/lib/http.js CHANGED
@@ -17,6 +17,7 @@ const app = new Hono();
17
17
  app.use("/*", cors({
18
18
  origin: [
19
19
  "https://gethmy.com",
20
+ "https://app.gethmy.com",
20
21
  "http://localhost:8080",
21
22
  "http://localhost:3000",
22
23
  ],
@@ -301,6 +301,7 @@ export function generatePrompt(options) {
301
301
  roleFraming.focus.forEach((f) => {
302
302
  sections.push(`- ${f}`);
303
303
  });
304
+ sections.push(`- **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).`);
304
305
  // Output suggestions
305
306
  sections.push(`\n## Suggested Outputs`);
306
307
  roleFraming.outputSuggestions.forEach((s) => {
@@ -9,7 +9,7 @@
9
9
  * Claude.ai → POST https://mcp.gethmy.com/mcp (Bearer: hmy_xxx)
10
10
  *
11
11
  * Env vars:
12
- * HARMONY_API_URL - Harmony API base URL (default: https://gethmy.com/api)
12
+ * HARMONY_API_URL - Harmony API base URL (default: https://app.gethmy.com/api)
13
13
  * PORT - Listen port (default: 3002)
14
14
  */
15
15
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -22,7 +22,7 @@ import { registerHandlers } from "./server.js";
22
22
  // ---------------------------------------------------------------------------
23
23
  // Config from env
24
24
  // ---------------------------------------------------------------------------
25
- const HARMONY_API_URL = process.env.HARMONY_API_URL || "https://gethmy.com/api";
25
+ const HARMONY_API_URL = process.env.HARMONY_API_URL || "https://app.gethmy.com/api";
26
26
  const PORT = parseInt(process.env.PORT || "3002", 10);
27
27
  async function validateApiKey(apiKey) {
28
28
  try {
@@ -12,6 +12,118 @@ import { assembleContext, cacheManifest, computeRelevanceScore, getCachedManifes
12
12
  import { autoExpandGraph } from "./graph-expansion.js";
13
13
  import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
14
14
  import { generatePrompt, } from "./prompt-builder.js";
15
+ const memorySessions = new Map();
16
+ function initMemorySession(cardId, agentIdentifier, agentName) {
17
+ memorySessions.set(cardId, {
18
+ cardId,
19
+ agentIdentifier,
20
+ agentName,
21
+ memoryReadCount: 0,
22
+ pendingActions: [],
23
+ allActions: [],
24
+ dirty: false,
25
+ });
26
+ }
27
+ function getMemorySession(cardId) {
28
+ return memorySessions.get(cardId);
29
+ }
30
+ function appendMemoryAction(cardId, action) {
31
+ const session = memorySessions.get(cardId);
32
+ if (!session)
33
+ return;
34
+ const truncated = action.length > 512 ? action.slice(0, 509) + "..." : action;
35
+ const entry = { action: truncated, ts: new Date().toISOString() };
36
+ session.pendingActions.push(entry);
37
+ session.dirty = true;
38
+ }
39
+ function incrementMemoryReads(cardId) {
40
+ const session = memorySessions.get(cardId);
41
+ if (!session)
42
+ return;
43
+ session.memoryReadCount++;
44
+ session.dirty = true;
45
+ }
46
+ /**
47
+ * Flush pending memory actions to the backend via updateAgentProgress.
48
+ * Fire-and-forget: errors are logged but never thrown.
49
+ */
50
+ async function flushMemoryActions(client, cardId) {
51
+ const session = memorySessions.get(cardId);
52
+ if (!session || !session.dirty)
53
+ return;
54
+ try {
55
+ // Batch reads into a single summary line
56
+ if (session.memoryReadCount > 0) {
57
+ session.allActions.push({
58
+ action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
59
+ ts: new Date().toISOString(),
60
+ });
61
+ session.memoryReadCount = 0;
62
+ }
63
+ // Move pending writes to allActions
64
+ if (session.pendingActions.length > 0) {
65
+ session.allActions.push(...session.pendingActions);
66
+ session.pendingActions = [];
67
+ }
68
+ // Trim to max 10 (drop oldest)
69
+ if (session.allActions.length > 10) {
70
+ session.allActions = session.allActions.slice(-10);
71
+ }
72
+ await client.updateAgentProgress(cardId, {
73
+ agentIdentifier: session.agentIdentifier,
74
+ agentName: session.agentName,
75
+ recentActions: session.allActions,
76
+ });
77
+ session.dirty = false;
78
+ }
79
+ catch (err) {
80
+ // Fire-and-forget: log but don't propagate
81
+ console.error("[memory-session] flush failed:", err);
82
+ }
83
+ }
84
+ /**
85
+ * Merge memory actions into an existing recentActions array from a progress update.
86
+ * Returns the merged array (caller's actions first, then memory actions appended).
87
+ */
88
+ function mergeMemoryActionsInto(cardId, callerActions) {
89
+ const session = memorySessions.get(cardId);
90
+ if (!session)
91
+ return callerActions;
92
+ // Flush reads into allActions
93
+ if (session.memoryReadCount > 0) {
94
+ session.allActions.push({
95
+ action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
96
+ ts: new Date().toISOString(),
97
+ });
98
+ session.memoryReadCount = 0;
99
+ }
100
+ // Move pending to allActions
101
+ if (session.pendingActions.length > 0) {
102
+ session.allActions.push(...session.pendingActions);
103
+ session.pendingActions = [];
104
+ }
105
+ // Merge: caller actions + memory actions, trim to 10
106
+ const merged = [...callerActions, ...session.allActions];
107
+ const trimmed = merged.length > 10 ? merged.slice(-10) : merged;
108
+ // Update allActions to the merged state
109
+ session.allActions = trimmed;
110
+ session.dirty = false;
111
+ return trimmed;
112
+ }
113
+ /**
114
+ * Get the first (and typically only) active memory session.
115
+ * Memory tools don't carry a cardId, so we find the active session.
116
+ */
117
+ function getActiveMemorySession() {
118
+ // Return the first active session (there should be at most one)
119
+ for (const session of memorySessions.values()) {
120
+ return session;
121
+ }
122
+ return undefined;
123
+ }
124
+ function cleanupMemorySession(cardId) {
125
+ memorySessions.delete(cardId);
126
+ }
15
127
  // Tool definitions
16
128
  const TOOLS = {
17
129
  // Card operations
@@ -1597,7 +1709,7 @@ async function handleToolCall(name, args, deps) {
1597
1709
  const unauthenticatedTools = ["harmony_signup", "harmony_onboard"];
1598
1710
  if (!unauthenticatedTools.includes(name) && !deps.isConfigured()) {
1599
1711
  throw new Error('Not configured. Run "npx @gethmy/mcp setup" to set your API key.\n' +
1600
- "You can generate an API key at https://gethmy.com → Settings → API Keys.\n" +
1712
+ "You can generate an API key at https://app.gethmy.com → Settings → API Keys.\n" +
1601
1713
  'Or use "harmony_onboard" to create an account and configure automatically.');
1602
1714
  }
1603
1715
  const client = deps.isConfigured()
@@ -1944,7 +2056,9 @@ async function handleToolCall(name, args, deps) {
1944
2056
  estimatedMinutesRemaining: args.estimatedMinutesRemaining,
1945
2057
  });
1946
2058
  // Mark as explicit so auto-session won't interfere
1947
- markExplicit(cardId);
2059
+ markExplicit(cardId, { agentIdentifier, agentName });
2060
+ // Initialize memory session tracking for action visibility
2061
+ initMemorySession(cardId, agentIdentifier, agentName);
1948
2062
  // Prefetch relevant context (non-blocking, best-effort)
1949
2063
  let prefetchedMemoryIds = [];
1950
2064
  try {
@@ -2011,6 +2125,16 @@ async function handleToolCall(name, args, deps) {
2011
2125
  const progressPercent = args.progressPercent !== undefined
2012
2126
  ? z.number().min(0).max(100).parse(args.progressPercent)
2013
2127
  : undefined;
2128
+ // Merge any pending memory actions into the progress update
2129
+ const callerRecentActions = args.recentActions;
2130
+ const memSession = getMemorySession(cardId);
2131
+ let mergedRecentActions;
2132
+ if (memSession?.dirty) {
2133
+ mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions || []);
2134
+ }
2135
+ else if (callerRecentActions) {
2136
+ mergedRecentActions = callerRecentActions;
2137
+ }
2014
2138
  const result = await client.updateAgentProgress(cardId, {
2015
2139
  agentIdentifier,
2016
2140
  agentName,
@@ -2019,6 +2143,7 @@ async function handleToolCall(name, args, deps) {
2019
2143
  currentTask: args.currentTask,
2020
2144
  blockers: args.blockers,
2021
2145
  estimatedMinutesRemaining: args.estimatedMinutesRemaining,
2146
+ ...(mergedRecentActions && { recentActions: mergedRecentActions }),
2022
2147
  });
2023
2148
  // Mid-session learning extraction (fire-and-forget)
2024
2149
  let midSessionLearnings = 0;
@@ -2049,6 +2174,9 @@ async function handleToolCall(name, args, deps) {
2049
2174
  const endProgressPercent = args.progressPercent !== undefined
2050
2175
  ? z.number().min(0).max(100).parse(args.progressPercent)
2051
2176
  : undefined;
2177
+ // Final flush of any pending memory actions before ending the session
2178
+ await flushMemoryActions(client, cardId);
2179
+ cleanupMemorySession(cardId);
2052
2180
  const result = await client.endAgentSession(cardId, {
2053
2181
  status: sessionStatus,
2054
2182
  progressPercent: endProgressPercent,
@@ -2270,6 +2398,8 @@ async function handleToolCall(name, args, deps) {
2270
2398
  }
2271
2399
  const entityType = args.type || "context";
2272
2400
  const entityTags = args.tags || [];
2401
+ // Use session's agent identifier if available, otherwise null
2402
+ const activeMemSession = getActiveMemorySession();
2273
2403
  const result = await client.createMemoryEntity({
2274
2404
  workspace_id: workspaceId,
2275
2405
  project_id: args.projectId || deps.getActiveProjectId() || undefined,
@@ -2283,7 +2413,7 @@ async function handleToolCall(name, args, deps) {
2283
2413
  ? z.number().min(0).max(1).parse(args.confidence)
2284
2414
  : undefined,
2285
2415
  tags: entityTags.length > 0 ? entityTags : undefined,
2286
- agent_identifier: "claude-code",
2416
+ agent_identifier: activeMemSession?.agentIdentifier || undefined,
2287
2417
  });
2288
2418
  // Fire-and-forget graph expansion: link new entity to semantically similar ones
2289
2419
  const newEntityIdForGraph = result.entity?.id;
@@ -2303,6 +2433,11 @@ async function handleToolCall(name, args, deps) {
2303
2433
  // Don't block creation if contradiction detection fails
2304
2434
  }
2305
2435
  }
2436
+ // Track memory write action and flush (fire-and-forget)
2437
+ if (activeMemSession) {
2438
+ appendMemoryAction(activeMemSession.cardId, `Stored memory: ${title}`);
2439
+ flushMemoryActions(client, activeMemSession.cardId).catch(() => { });
2440
+ }
2306
2441
  return {
2307
2442
  success: true,
2308
2443
  ...result,
@@ -2373,6 +2508,11 @@ async function handleToolCall(name, args, deps) {
2373
2508
  }
2374
2509
  })).catch(() => { });
2375
2510
  }
2511
+ // Track memory read (batched on flush)
2512
+ const recallMemSession = getActiveMemorySession();
2513
+ if (recallMemSession) {
2514
+ incrementMemoryReads(recallMemSession.cardId);
2515
+ }
2376
2516
  return markdown || "No memories found.";
2377
2517
  }
2378
2518
  case "harmony_update_memory": {
@@ -2393,11 +2533,35 @@ async function handleToolCall(name, args, deps) {
2393
2533
  if (args.metadata !== undefined)
2394
2534
  updates.metadata = args.metadata;
2395
2535
  const result = await client.updateMemoryEntity(entityId, updates);
2536
+ // Track memory write action and flush (fire-and-forget)
2537
+ const updateMemSession = getActiveMemorySession();
2538
+ if (updateMemSession) {
2539
+ const updateTitle = updates.title || entityId.slice(0, 8);
2540
+ appendMemoryAction(updateMemSession.cardId, `Updated memory: ${updateTitle}`);
2541
+ flushMemoryActions(client, updateMemSession.cardId).catch(() => { });
2542
+ }
2396
2543
  return { success: true, ...result };
2397
2544
  }
2398
2545
  case "harmony_forget": {
2399
2546
  const entityId = z.string().uuid().parse(args.entityId);
2547
+ // Fetch title before deletion for action tracking
2548
+ let forgetTitle = null;
2549
+ const forgetMemSession = getActiveMemorySession();
2550
+ if (forgetMemSession) {
2551
+ try {
2552
+ const { entity } = await client.getMemoryEntity(entityId);
2553
+ forgetTitle = entity?.title || null;
2554
+ }
2555
+ catch {
2556
+ // Non-fatal: use truncated ID if fetch fails
2557
+ }
2558
+ }
2400
2559
  await client.deleteMemoryEntity(entityId);
2560
+ // Track memory write action and flush (fire-and-forget)
2561
+ if (forgetMemSession) {
2562
+ appendMemoryAction(forgetMemSession.cardId, `Removed memory: ${forgetTitle || entityId.slice(0, 8)}`);
2563
+ flushMemoryActions(client, forgetMemSession.cardId).catch(() => { });
2564
+ }
2401
2565
  return { success: true };
2402
2566
  }
2403
2567
  case "harmony_relate": {
@@ -2423,6 +2587,12 @@ async function handleToolCall(name, args, deps) {
2423
2587
  ? z.number().min(0).max(1).parse(args.confidence)
2424
2588
  : undefined,
2425
2589
  });
2590
+ // Track memory write action and flush (fire-and-forget)
2591
+ const relateMemSession = getActiveMemorySession();
2592
+ if (relateMemSession) {
2593
+ appendMemoryAction(relateMemSession.cardId, `Linked memories: ${relationType}`);
2594
+ flushMemoryActions(client, relateMemSession.cardId).catch(() => { });
2595
+ }
2426
2596
  return { success: true, ...result };
2427
2597
  }
2428
2598
  case "harmony_memory_search": {
@@ -2438,6 +2608,11 @@ async function handleToolCall(name, args, deps) {
2438
2608
  type: args.type,
2439
2609
  limit: args.limit,
2440
2610
  });
2611
+ // Track memory read (batched on flush)
2612
+ const searchMemSession = getActiveMemorySession();
2613
+ if (searchMemSession) {
2614
+ incrementMemoryReads(searchMemSession.cardId);
2615
+ }
2441
2616
  return markdown || "No memories found.";
2442
2617
  }
2443
2618
  // Vault index
@@ -2537,7 +2712,7 @@ async function handleToolCall(name, args, deps) {
2537
2712
  tasks: args.tasks,
2538
2713
  });
2539
2714
  // Build URL for viewing the plan
2540
- const planUrl = `https://gethmy.com/plans/${result.plan.id}`;
2715
+ const planUrl = `https://app.gethmy.com/plans/${result.plan.id}`;
2541
2716
  return {
2542
2717
  success: true,
2543
2718
  planId: result.plan.id,
@@ -246,7 +246,7 @@ Once you have the plan ID, call \`harmony_get_plan\` to fetch the full plan with
246
246
  ## [Plan Title]
247
247
  **Status:** draft/active/archived | **Phase:** plan/execute/verify/done
248
248
  **Tasks:** N total (X pending, Y in_progress, Z completed)
249
- **URL:** https://gethmy.com/plans/{id}
249
+ **URL:** https://app.gethmy.com/plans/{id}
250
250
  \\\`\\\`\\\`
251
251
 
252
252
  If plan is in execute phase and tasks already have linked cards, note which tasks have cards and which don't.
@@ -280,7 +280,7 @@ Only offer **(A) Single card**, **(C) Analyze only**, or **(D) Skip**.
280
280
  #### Option A — Single card
281
281
  1. Call \`harmony_create_card\` with:
282
282
  - \`title\`: Plan title
283
- - \`description\`: Brief 2-3 sentence summary of the plan + \`\\n\\n[View plan](https://gethmy.com/plans/{planId})\`
283
+ - \`description\`: Brief 2-3 sentence summary of the plan + \`\\n\\n[View plan](https://app.gethmy.com/plans/{planId})\`
284
284
  - \`priority\`: based on plan task priorities (use highest)
285
285
  2. Call \`harmony_update_plan\` to set \`status: "active"\`, \`workflowPhase: "execute"\`
286
286
 
@@ -402,7 +402,7 @@ Call \`harmony_create_plan\` with:
402
402
  ### 2B.5 — User Approval
403
403
 
404
404
  Show the user:
405
- - Plan URL: \`https://gethmy.com/plans/{id}\`
405
+ - Plan URL: \`https://app.gethmy.com/plans/{id}\`
406
406
  - Number of tasks created
407
407
  - Brief summary
408
408
 
@@ -11,7 +11,7 @@ import { getWriteSummary, writeFilesWithProgress } from "./writer.js";
11
11
  // Central skills directory for global installation
12
12
  const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
13
13
  // API base URL
14
- const API_URL = "https://gethmy.com/api";
14
+ const API_URL = "https://app.gethmy.com/api";
15
15
  /**
16
16
  * Register MCP server using Claude CLI
17
17
  * Returns true if successful, false if CLI unavailable or failed
@@ -422,7 +422,7 @@ export async function runSetup(options = {}) {
422
422
  if (!validation.valid) {
423
423
  spinner.stop(colors.error("API key validation failed"));
424
424
  p.log.error(validation.error || "Could not connect to Harmony API");
425
- p.log.info("Get an API key at: https://gethmy.com/user/keys");
425
+ p.log.info("Get an API key at: https://app.gethmy.com/user/keys");
426
426
  process.exit(1);
427
427
  }
428
428
  if (!userEmail) {
@@ -765,6 +765,6 @@ export async function runSetup(options = {}) {
765
765
  }
766
766
  console.log("");
767
767
  console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
768
- console.log(` ${colors.dim("Need help? Visit https://gethmy.com/docs/mcp")}`);
768
+ console.log(` ${colors.dim("Need help? Visit https://app.gethmy.com/docs/mcp")}`);
769
769
  console.log("");
770
770
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -118,11 +118,18 @@ export async function trackActivity(
118
118
 
119
119
  /**
120
120
  * Mark a session as explicitly started (won't be auto-ended by card switching or inactivity).
121
+ * Optionally accepts the real agent identifier/name to store in the session.
121
122
  */
122
- export function markExplicit(cardId: string): void {
123
+ export function markExplicit(
124
+ cardId: string,
125
+ options?: { agentIdentifier?: string; agentName?: string },
126
+ ): void {
123
127
  const existing = activeSessions.get(cardId);
124
128
  if (existing) {
125
129
  existing.isExplicit = true;
130
+ if (options?.agentIdentifier)
131
+ existing.agentIdentifier = options.agentIdentifier;
132
+ if (options?.agentName) existing.agentName = options.agentName;
126
133
  } else {
127
134
  // Track the explicit session even if we didn't auto-start it
128
135
  activeSessions.set(cardId, {
@@ -130,8 +137,8 @@ export function markExplicit(cardId: string): void {
130
137
  startedAt: Date.now(),
131
138
  lastActivityAt: Date.now(),
132
139
  isExplicit: true,
133
- agentIdentifier: "explicit",
134
- agentName: "Explicit Agent",
140
+ agentIdentifier: options?.agentIdentifier ?? "explicit",
141
+ agentName: options?.agentName ?? "Explicit Agent",
135
142
  });
136
143
  }
137
144
  }
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,7 @@ 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 = process.env.HARMONY_API_URL || "https://app.gethmy.com/api";
29
29
  const PORT = parseInt(process.env.PORT || "3002", 10);
30
30
 
31
31
  // ---------------------------------------------------------------------------