@gethmy/mcp 2.2.0 → 2.2.2

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.
@@ -29,16 +29,16 @@ describe("auto-start", () => {
29
29
  await trackActivity(CARD_A, { autoStart: true, client: client });
30
30
  expect(client.startAgentSession).toHaveBeenCalledTimes(1);
31
31
  expect(client.startAgentSession).toHaveBeenCalledWith(CARD_A, {
32
- agentIdentifier: "auto",
33
- agentName: "Auto-detected Agent",
32
+ agentIdentifier: "unknown",
33
+ agentName: "Unknown Agent",
34
34
  status: "working",
35
35
  });
36
36
  expect(getActiveSessions().size).toBe(1);
37
37
  const session = getActiveSessions().get(CARD_A);
38
38
  expect(session).toBeDefined();
39
39
  expect(session.isExplicit).toBe(false);
40
- expect(session.agentIdentifier).toBe("auto");
41
- expect(session.agentName).toBe("Auto-detected Agent");
40
+ expect(session.agentIdentifier).toBe("unknown");
41
+ expect(session.agentName).toBe("Unknown Agent");
42
42
  });
43
43
  test("does NOT trigger on autoStart=false", async () => {
44
44
  const client = makeMockClient();
@@ -68,7 +68,7 @@ describe("auto-start", () => {
68
68
  await trackActivity(CARD_A, { autoStart: true, client: client });
69
69
  // Should still be tracked despite API error
70
70
  expect(getActiveSessions().size).toBe(1);
71
- expect(getActiveSessions().get(CARD_A).agentIdentifier).toBe("auto");
71
+ expect(getActiveSessions().get(CARD_A).agentIdentifier).toBe("unknown");
72
72
  });
73
73
  test("uses clientGetter when no client in options", async () => {
74
74
  const client = makeMockClient();
@@ -222,8 +222,8 @@ describe("inactivity timeout", () => {
222
222
  startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
223
223
  lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
224
224
  isExplicit: false,
225
- agentIdentifier: "auto",
226
- agentName: "Auto-detected Agent",
225
+ agentIdentifier: "unknown",
226
+ agentName: "Unknown Agent",
227
227
  });
228
228
  checkInactivity();
229
229
  // autoEndSession is fire-and-forget in checkInactivity, wait for it
@@ -241,8 +241,8 @@ describe("inactivity timeout", () => {
241
241
  startedAt: Date.now(),
242
242
  lastActivityAt: Date.now(), // just now — well within timeout
243
243
  isExplicit: false,
244
- agentIdentifier: "auto",
245
- agentName: "Auto-detected Agent",
244
+ agentIdentifier: "unknown",
245
+ agentName: "Unknown Agent",
246
246
  });
247
247
  checkInactivity();
248
248
  expect(client.endAgentSession).not.toHaveBeenCalled();
@@ -273,8 +273,8 @@ describe("inactivity timeout", () => {
273
273
  startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
274
274
  lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
275
275
  isExplicit: false,
276
- agentIdentifier: "auto",
277
- agentName: "Auto-detected Agent",
276
+ agentIdentifier: "unknown",
277
+ agentName: "Unknown Agent",
278
278
  });
279
279
  // CARD_B — still active
280
280
  getActiveSessions().set(CARD_B, {
@@ -282,8 +282,8 @@ describe("inactivity timeout", () => {
282
282
  startedAt: Date.now() - 1000,
283
283
  lastActivityAt: Date.now() - 1000,
284
284
  isExplicit: false,
285
- agentIdentifier: "auto",
286
- agentName: "Auto-detected Agent",
285
+ agentIdentifier: "unknown",
286
+ agentName: "Unknown Agent",
287
287
  });
288
288
  // CARD_C — timed out but explicit
289
289
  getActiveSessions().set(CARD_C, {
@@ -312,8 +312,8 @@ describe("inactivity timeout", () => {
312
312
  startedAt: 0,
313
313
  lastActivityAt: 0,
314
314
  isExplicit: false,
315
- agentIdentifier: "auto",
316
- agentName: "Auto-detected Agent",
315
+ agentIdentifier: "unknown",
316
+ agentName: "Unknown Agent",
317
317
  });
318
318
  // Should not throw
319
319
  checkInactivity();
@@ -330,8 +330,8 @@ describe("inactivity timeout", () => {
330
330
  startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
331
331
  lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
332
332
  isExplicit: false,
333
- agentIdentifier: "auto",
334
- agentName: "Auto-detected Agent",
333
+ agentIdentifier: "unknown",
334
+ agentName: "Unknown Agent",
335
335
  });
336
336
  // Refresh activity
337
337
  await trackActivity(CARD_A, { autoStart: false });
@@ -351,8 +351,8 @@ describe("markExplicit", () => {
351
351
  expect(getActiveSessions().get(CARD_A).isExplicit).toBe(false);
352
352
  markExplicit(CARD_A);
353
353
  expect(getActiveSessions().get(CARD_A).isExplicit).toBe(true);
354
- // agentIdentifier should remain "auto" since it was auto-started
355
- expect(getActiveSessions().get(CARD_A).agentIdentifier).toBe("auto");
354
+ // agentIdentifier should remain "unknown" since no clientInfo was provided
355
+ expect(getActiveSessions().get(CARD_A).agentIdentifier).toBe("unknown");
356
356
  });
357
357
  test("creates new tracking entry for unknown card", () => {
358
358
  initAutoSession(mock(async () => { }), () => makeMockClient());
@@ -406,8 +406,8 @@ describe("shutdown", () => {
406
406
  startedAt: Date.now(),
407
407
  lastActivityAt: Date.now(),
408
408
  isExplicit: false,
409
- agentIdentifier: "auto",
410
- agentName: "Auto-detected Agent",
409
+ agentIdentifier: "unknown",
410
+ agentName: "Unknown Agent",
411
411
  });
412
412
  await shutdownAllSessions();
413
413
  expect(client.endAgentSession).toHaveBeenCalledWith(CARD_A, {
@@ -425,8 +425,8 @@ describe("shutdown", () => {
425
425
  startedAt: Date.now(),
426
426
  lastActivityAt: Date.now(),
427
427
  isExplicit: false,
428
- agentIdentifier: "auto",
429
- agentName: "Auto-detected Agent",
428
+ agentIdentifier: "unknown",
429
+ agentName: "Unknown Agent",
430
430
  });
431
431
  getActiveSessions().set(CARD_B, {
432
432
  cardId: CARD_B,
@@ -449,8 +449,8 @@ describe("shutdown", () => {
449
449
  startedAt: Date.now(),
450
450
  lastActivityAt: Date.now(),
451
451
  isExplicit: false,
452
- agentIdentifier: "auto",
453
- agentName: "Auto-detected Agent",
452
+ agentIdentifier: "unknown",
453
+ agentName: "Unknown Agent",
454
454
  });
455
455
  // Should not throw
456
456
  await shutdownAllSessions();
@@ -473,16 +473,16 @@ describe("shutdown", () => {
473
473
  startedAt: Date.now(),
474
474
  lastActivityAt: Date.now(),
475
475
  isExplicit: false,
476
- agentIdentifier: "auto",
477
- agentName: "Auto-detected Agent",
476
+ agentIdentifier: "unknown",
477
+ agentName: "Unknown Agent",
478
478
  });
479
479
  getActiveSessions().set(CARD_B, {
480
480
  cardId: CARD_B,
481
481
  startedAt: Date.now(),
482
482
  lastActivityAt: Date.now(),
483
483
  isExplicit: false,
484
- agentIdentifier: "auto",
485
- agentName: "Auto-detected Agent",
484
+ agentIdentifier: "unknown",
485
+ agentName: "Unknown Agent",
486
486
  });
487
487
  await shutdownAllSessions();
488
488
  // Both should be removed from tracking despite first API error
@@ -595,8 +595,8 @@ describe("edge cases", () => {
595
595
  startedAt: 0,
596
596
  lastActivityAt: 0,
597
597
  isExplicit: false,
598
- agentIdentifier: "auto",
599
- agentName: "Auto-detected Agent",
598
+ agentIdentifier: "unknown",
599
+ agentName: "Unknown Agent",
600
600
  });
601
601
  checkInactivity();
602
602
  await new Promise((r) => setTimeout(r, 50));
@@ -615,8 +615,8 @@ describe("edge cases", () => {
615
615
  startedAt: 0,
616
616
  lastActivityAt: 0,
617
617
  isExplicit: false,
618
- agentIdentifier: "auto",
619
- agentName: "Auto-detected Agent",
618
+ agentIdentifier: "unknown",
619
+ agentName: "Unknown Agent",
620
620
  });
621
621
  checkInactivity();
622
622
  await new Promise((r) => setTimeout(r, 50));
@@ -536,6 +536,122 @@ export class HarmonyApiClient {
536
536
  async generateApiKey(name) {
537
537
  return this.request("POST", "/api-keys", { name });
538
538
  }
539
+ // ============ PROMPT GENERATION ============
540
+ /**
541
+ * Generate a prompt for a card with full memory context assembly.
542
+ *
543
+ * This is the shared entry point for prompt generation — used by the MCP
544
+ * server tool handler and the agent daemon. It fetches the card, assembles
545
+ * relevant memories, and produces a role-framed prompt.
546
+ */
547
+ async generateCardPrompt(options) {
548
+ const { assembleContext, cacheManifest, generatePrompt } = await loadPromptModules();
549
+ // Fetch card data
550
+ const cardResult = await this.getCard(options.cardId);
551
+ const cardData = cardResult.card;
552
+ // Try to get column info
553
+ let columnData = null;
554
+ const projectIdForBoard = options.projectId || cardData.project_id;
555
+ if (projectIdForBoard) {
556
+ try {
557
+ const board = await this.getBoard(projectIdForBoard, { summary: true });
558
+ const column = board.columns.find((col) => col.id === cardData.column_id);
559
+ if (column) {
560
+ columnData = { name: column.name };
561
+ }
562
+ }
563
+ catch {
564
+ // Column info not available, continue without it
565
+ }
566
+ }
567
+ const variant = options.variant || "execute";
568
+ // Assemble memory context
569
+ let assembledContextStr;
570
+ let assemblyId;
571
+ let memories;
572
+ try {
573
+ if (options.workspaceId && cardData.title) {
574
+ const cardLabels = (cardData.labels || []).map((l) => l.name);
575
+ const taskContext = [cardData.title, cardData.description || ""]
576
+ .filter(Boolean)
577
+ .join(" ");
578
+ const assembled = await assembleContext({
579
+ workspaceId: options.workspaceId,
580
+ projectId: options.projectId,
581
+ taskContext,
582
+ cardLabels,
583
+ cardId: cardData.id,
584
+ client: this,
585
+ });
586
+ if (assembled.context) {
587
+ assembledContextStr = assembled.context;
588
+ assemblyId = assembled.manifest.assemblyId;
589
+ cacheManifest(assembled.manifest);
590
+ }
591
+ }
592
+ }
593
+ catch (err) {
594
+ // Context assembly failed, try legacy fallback
595
+ const msg = err instanceof Error ? err.message : String(err);
596
+ console.debug(`[generateCardPrompt] Context assembly failed: ${msg}`);
597
+ try {
598
+ if (options.workspaceId && cardData.title) {
599
+ const memoryResult = await this.searchMemoryEntities(options.workspaceId, cardData.title, {
600
+ project_id: options.projectId,
601
+ limit: 5,
602
+ });
603
+ if (memoryResult.entities?.length > 0) {
604
+ memories = memoryResult.entities.map((e) => ({
605
+ id: e.id,
606
+ type: e.type,
607
+ title: e.title,
608
+ content: e.content,
609
+ confidence: e.confidence,
610
+ tags: e.tags || [],
611
+ }));
612
+ }
613
+ }
614
+ }
615
+ catch (fallbackErr) {
616
+ const fallbackMsg = fallbackErr instanceof Error
617
+ ? fallbackErr.message
618
+ : String(fallbackErr);
619
+ console.debug(`[generateCardPrompt] Memory fallback also failed: ${fallbackMsg}`);
620
+ }
621
+ }
622
+ const result = generatePrompt({
623
+ card: cardData,
624
+ column: columnData,
625
+ variant,
626
+ contextOptions: options.contextOptions,
627
+ customConstraints: options.customConstraints,
628
+ memories,
629
+ assembledContext: assembledContextStr,
630
+ assemblyId,
631
+ });
632
+ return {
633
+ ...result,
634
+ cardId: cardData.id,
635
+ shortId: cardData.short_id,
636
+ title: cardData.title,
637
+ };
638
+ }
639
+ }
640
+ // Cached dynamic imports for context-assembly and prompt-builder
641
+ let _promptModules = null;
642
+ async function loadPromptModules() {
643
+ if (!_promptModules) {
644
+ const [ca, pb] = await Promise.all([
645
+ import("./context-assembly.js"),
646
+ import("./prompt-builder.js"),
647
+ ]);
648
+ _promptModules = {
649
+ assembleContext: ca.assembleContext,
650
+ cacheManifest: ca.cacheManifest,
651
+ generatePrompt: pb.generatePrompt,
652
+ };
653
+ }
654
+ return _promptModules;
539
655
  }
540
656
  // Singleton instance
541
657
  let client = null;
@@ -4,7 +4,36 @@
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", "Codex", etc. are
10
+ * detected automatically — no hardcoded fallback needed.
7
11
  */
12
+ /** Well-known MCP client names → human-friendly display names */
13
+ const CLIENT_DISPLAY_NAMES = {
14
+ "claude-code": "Claude Code",
15
+ "claude-desktop": "Claude Desktop",
16
+ cursor: "Cursor",
17
+ windsurf: "Windsurf",
18
+ cline: "Cline",
19
+ continue: "Continue",
20
+ "codex-cli": "OpenAI Codex",
21
+ zed: "Zed",
22
+ "gemini-cli": "Gemini CLI",
23
+ };
24
+ /** Derive a slug-style identifier from a client name */
25
+ function toIdentifier(name) {
26
+ return name.toLowerCase().replace(/\s+/g, "-");
27
+ }
28
+ /** Resolve agent identity from MCP client info */
29
+ export function resolveAgentIdentity(info) {
30
+ if (!info?.name) {
31
+ return { agentIdentifier: "unknown", agentName: "Unknown Agent" };
32
+ }
33
+ const key = toIdentifier(info.name);
34
+ const displayName = CLIENT_DISPLAY_NAMES[key] ?? info.name; // use raw name if not in map
35
+ return { agentIdentifier: key, agentName: displayName };
36
+ }
8
37
  /** Tools that trigger auto-start of a session */
9
38
  export const AUTO_START_TRIGGERS = new Set([
10
39
  "harmony_generate_prompt",
@@ -21,14 +50,17 @@ const activeSessions = new Map();
21
50
  let inactivityTimer = null;
22
51
  let endCallback = null;
23
52
  let clientGetter = null;
53
+ let clientInfoGetter = null;
24
54
  /**
25
55
  * Initialize auto-session tracking.
26
56
  * @param callback Called when an auto-session ends (runs the learning pipeline)
27
57
  * @param getClient Function to get the current API client
58
+ * @param getClientInfo Function to get MCP client identity from the initialize handshake
28
59
  */
29
- export function initAutoSession(callback, getClient) {
60
+ export function initAutoSession(callback, getClient, getClientInfo) {
30
61
  endCallback = callback;
31
62
  clientGetter = getClient;
63
+ clientInfoGetter = getClientInfo ?? null;
32
64
  if (inactivityTimer)
33
65
  clearInterval(inactivityTimer);
34
66
  inactivityTimer = setInterval(checkInactivity, CHECK_INTERVAL_MS);
@@ -60,11 +92,14 @@ export async function trackActivity(cardId, options) {
60
92
  for (const otherCardId of toEnd) {
61
93
  await autoEndSession(client, otherCardId, "completed");
62
94
  }
95
+ // Resolve agent identity from MCP client info
96
+ const info = clientInfoGetter?.() ?? null;
97
+ const { agentIdentifier, agentName } = resolveAgentIdentity(info);
63
98
  // Start a new auto-session
64
99
  try {
65
100
  await client.startAgentSession(cardId, {
66
- agentIdentifier: "auto",
67
- agentName: "Auto-detected Agent",
101
+ agentIdentifier,
102
+ agentName,
68
103
  status: "working",
69
104
  });
70
105
  }
@@ -76,8 +111,8 @@ export async function trackActivity(cardId, options) {
76
111
  startedAt: now,
77
112
  lastActivityAt: now,
78
113
  isExplicit: false,
79
- agentIdentifier: "auto",
80
- agentName: "Auto-detected Agent",
114
+ agentIdentifier,
115
+ agentName,
81
116
  });
82
117
  }
83
118
  /**
@@ -134,6 +169,7 @@ export function destroyAutoSession() {
134
169
  activeSessions.clear();
135
170
  endCallback = null;
136
171
  clientGetter = null;
172
+ clientInfoGetter = null;
137
173
  }
138
174
  /**
139
175
  * Get a snapshot of active sessions (for testing/debugging).
package/dist/lib/cli.js CHANGED
@@ -15,6 +15,11 @@ program
15
15
  .command("serve")
16
16
  .description("Start the MCP server (stdio transport)")
17
17
  .action(async () => {
18
+ if (!isConfigured()) {
19
+ console.error("No API key configured.");
20
+ console.error("Run: npx @gethmy/mcp setup");
21
+ process.exit(1);
22
+ }
18
23
  await refreshSkills();
19
24
  const server = new HarmonyMCPServer();
20
25
  await server.run();
@@ -109,6 +114,8 @@ program
109
114
  .option("-p, --project <id>", "Set project context")
110
115
  .option("--skip-context", "Skip workspace/project selection")
111
116
  .option("--skip-docs", "Skip project docs scaffold/verification")
117
+ .option("--new", "Create a new account (skip the choice prompt)")
118
+ .option("-n, --name <name>", "Full name (for account creation)")
112
119
  .action(async (options) => {
113
120
  await runSetup({
114
121
  force: options.force,
@@ -124,6 +131,8 @@ program
124
131
  projectId: options.project,
125
132
  skipContext: options.skipContext,
126
133
  skipDocs: options.skipDocs,
134
+ newAccount: options.new,
135
+ name: options.name,
127
136
  });
128
137
  });
129
138
  program.parse();
@@ -0,0 +1,36 @@
1
+ import { requestWithBearer, signupUser } from "./api-client.js";
2
+ import { getApiUrl } from "./config.js";
3
+ export async function onboardNewUser(params) {
4
+ const { email, password, fullName, workspaceName = `${fullName}'s Workspace`, projectName = "My First Board", template = "kanban", keyName = "mcp-agent", apiUrl = getApiUrl(), } = params;
5
+ // 1. Signup
6
+ const signupResult = await signupUser(apiUrl, {
7
+ email,
8
+ password,
9
+ full_name: fullName,
10
+ });
11
+ const token = signupResult.session.access_token;
12
+ // 2. Create workspace
13
+ const workspaceResult = await requestWithBearer(apiUrl, token, "POST", "/workspaces", {
14
+ name: workspaceName,
15
+ });
16
+ // 3. Create project
17
+ const projectResult = await requestWithBearer(apiUrl, token, "POST", "/projects", {
18
+ workspaceId: workspaceResult.workspace.id,
19
+ name: projectName,
20
+ template,
21
+ });
22
+ // 4. Generate API key
23
+ const keyResult = await requestWithBearer(apiUrl, token, "POST", "/api-keys", {
24
+ name: keyName,
25
+ });
26
+ return {
27
+ user: signupResult.user,
28
+ workspace: workspaceResult.workspace,
29
+ project: projectResult.project,
30
+ columns: projectResult.columns,
31
+ apiKey: {
32
+ rawKey: keyResult.rawKey,
33
+ prefix: keyResult.apiKey.prefix,
34
+ },
35
+ };
36
+ }