@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.
- package/README.md +5 -7
- package/dist/cli.js +2515 -2134
- package/dist/http.js +6 -4
- package/dist/index.js +8088 -7890
- package/dist/lib/__tests__/auto-session.test.js +33 -33
- package/dist/lib/api-client.js +116 -0
- package/dist/lib/auto-session.js +41 -5
- package/dist/lib/cli.js +9 -0
- package/dist/lib/onboard.js +36 -0
- package/dist/lib/server.js +150 -169
- package/dist/lib/skills.js +1 -1
- package/dist/lib/tui/setup.js +212 -59
- package/dist/remote.js +7132 -10614
- package/package.json +2 -1
- package/src/__tests__/auto-session.test.ts +33 -33
- package/src/api-client.ts +205 -0
- package/src/auto-session.ts +54 -4
- package/src/cli.ts +9 -0
- package/src/onboard.ts +93 -0
- package/src/remote.ts +2 -1
- package/src/server.ts +178 -221
- package/src/skills.ts +1 -1
- package/src/tui/setup.ts +249 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@clack/prompts": "^0.11.0",
|
|
65
|
+
"@harmony/memory": "workspace:*",
|
|
65
66
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
66
67
|
"commander": "^14.0.3",
|
|
67
68
|
"hono": "^4.11.7",
|
|
@@ -51,16 +51,16 @@ describe("auto-start", () => {
|
|
|
51
51
|
|
|
52
52
|
expect(client.startAgentSession).toHaveBeenCalledTimes(1);
|
|
53
53
|
expect(client.startAgentSession).toHaveBeenCalledWith(CARD_A, {
|
|
54
|
-
agentIdentifier: "
|
|
55
|
-
agentName: "
|
|
54
|
+
agentIdentifier: "unknown",
|
|
55
|
+
agentName: "Unknown Agent",
|
|
56
56
|
status: "working",
|
|
57
57
|
});
|
|
58
58
|
expect(getActiveSessions().size).toBe(1);
|
|
59
59
|
const session = getActiveSessions().get(CARD_A);
|
|
60
60
|
expect(session).toBeDefined();
|
|
61
61
|
expect(session!.isExplicit).toBe(false);
|
|
62
|
-
expect(session!.agentIdentifier).toBe("
|
|
63
|
-
expect(session!.agentName).toBe("
|
|
62
|
+
expect(session!.agentIdentifier).toBe("unknown");
|
|
63
|
+
expect(session!.agentName).toBe("Unknown Agent");
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
test("does NOT trigger on autoStart=false", async () => {
|
|
@@ -109,7 +109,7 @@ describe("auto-start", () => {
|
|
|
109
109
|
|
|
110
110
|
// Should still be tracked despite API error
|
|
111
111
|
expect(getActiveSessions().size).toBe(1);
|
|
112
|
-
expect(getActiveSessions().get(CARD_A)!.agentIdentifier).toBe("
|
|
112
|
+
expect(getActiveSessions().get(CARD_A)!.agentIdentifier).toBe("unknown");
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
test("uses clientGetter when no client in options", async () => {
|
|
@@ -328,8 +328,8 @@ describe("inactivity timeout", () => {
|
|
|
328
328
|
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
|
|
329
329
|
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 1000,
|
|
330
330
|
isExplicit: false,
|
|
331
|
-
agentIdentifier: "
|
|
332
|
-
agentName: "
|
|
331
|
+
agentIdentifier: "unknown",
|
|
332
|
+
agentName: "Unknown Agent",
|
|
333
333
|
});
|
|
334
334
|
|
|
335
335
|
checkInactivity();
|
|
@@ -355,8 +355,8 @@ describe("inactivity timeout", () => {
|
|
|
355
355
|
startedAt: Date.now(),
|
|
356
356
|
lastActivityAt: Date.now(), // just now — well within timeout
|
|
357
357
|
isExplicit: false,
|
|
358
|
-
agentIdentifier: "
|
|
359
|
-
agentName: "
|
|
358
|
+
agentIdentifier: "unknown",
|
|
359
|
+
agentName: "Unknown Agent",
|
|
360
360
|
});
|
|
361
361
|
|
|
362
362
|
checkInactivity();
|
|
@@ -398,8 +398,8 @@ describe("inactivity timeout", () => {
|
|
|
398
398
|
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
399
399
|
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
400
400
|
isExplicit: false,
|
|
401
|
-
agentIdentifier: "
|
|
402
|
-
agentName: "
|
|
401
|
+
agentIdentifier: "unknown",
|
|
402
|
+
agentName: "Unknown Agent",
|
|
403
403
|
});
|
|
404
404
|
|
|
405
405
|
// CARD_B — still active
|
|
@@ -408,8 +408,8 @@ describe("inactivity timeout", () => {
|
|
|
408
408
|
startedAt: Date.now() - 1000,
|
|
409
409
|
lastActivityAt: Date.now() - 1000,
|
|
410
410
|
isExplicit: false,
|
|
411
|
-
agentIdentifier: "
|
|
412
|
-
agentName: "
|
|
411
|
+
agentIdentifier: "unknown",
|
|
412
|
+
agentName: "Unknown Agent",
|
|
413
413
|
});
|
|
414
414
|
|
|
415
415
|
// CARD_C — timed out but explicit
|
|
@@ -442,8 +442,8 @@ describe("inactivity timeout", () => {
|
|
|
442
442
|
startedAt: 0,
|
|
443
443
|
lastActivityAt: 0,
|
|
444
444
|
isExplicit: false,
|
|
445
|
-
agentIdentifier: "
|
|
446
|
-
agentName: "
|
|
445
|
+
agentIdentifier: "unknown",
|
|
446
|
+
agentName: "Unknown Agent",
|
|
447
447
|
});
|
|
448
448
|
|
|
449
449
|
// Should not throw
|
|
@@ -464,8 +464,8 @@ describe("inactivity timeout", () => {
|
|
|
464
464
|
startedAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
465
465
|
lastActivityAt: Date.now() - INACTIVITY_TIMEOUT_MS - 5000,
|
|
466
466
|
isExplicit: false,
|
|
467
|
-
agentIdentifier: "
|
|
468
|
-
agentName: "
|
|
467
|
+
agentIdentifier: "unknown",
|
|
468
|
+
agentName: "Unknown Agent",
|
|
469
469
|
});
|
|
470
470
|
|
|
471
471
|
// Refresh activity
|
|
@@ -495,8 +495,8 @@ describe("markExplicit", () => {
|
|
|
495
495
|
|
|
496
496
|
markExplicit(CARD_A);
|
|
497
497
|
expect(getActiveSessions().get(CARD_A)!.isExplicit).toBe(true);
|
|
498
|
-
// agentIdentifier should remain "
|
|
499
|
-
expect(getActiveSessions().get(CARD_A)!.agentIdentifier).toBe("
|
|
498
|
+
// agentIdentifier should remain "unknown" since no clientInfo was provided
|
|
499
|
+
expect(getActiveSessions().get(CARD_A)!.agentIdentifier).toBe("unknown");
|
|
500
500
|
});
|
|
501
501
|
|
|
502
502
|
test("creates new tracking entry for unknown card", () => {
|
|
@@ -583,8 +583,8 @@ describe("shutdown", () => {
|
|
|
583
583
|
startedAt: Date.now(),
|
|
584
584
|
lastActivityAt: Date.now(),
|
|
585
585
|
isExplicit: false,
|
|
586
|
-
agentIdentifier: "
|
|
587
|
-
agentName: "
|
|
586
|
+
agentIdentifier: "unknown",
|
|
587
|
+
agentName: "Unknown Agent",
|
|
588
588
|
});
|
|
589
589
|
|
|
590
590
|
await shutdownAllSessions();
|
|
@@ -606,8 +606,8 @@ describe("shutdown", () => {
|
|
|
606
606
|
startedAt: Date.now(),
|
|
607
607
|
lastActivityAt: Date.now(),
|
|
608
608
|
isExplicit: false,
|
|
609
|
-
agentIdentifier: "
|
|
610
|
-
agentName: "
|
|
609
|
+
agentIdentifier: "unknown",
|
|
610
|
+
agentName: "Unknown Agent",
|
|
611
611
|
});
|
|
612
612
|
getActiveSessions().set(CARD_B, {
|
|
613
613
|
cardId: CARD_B,
|
|
@@ -633,8 +633,8 @@ describe("shutdown", () => {
|
|
|
633
633
|
startedAt: Date.now(),
|
|
634
634
|
lastActivityAt: Date.now(),
|
|
635
635
|
isExplicit: false,
|
|
636
|
-
agentIdentifier: "
|
|
637
|
-
agentName: "
|
|
636
|
+
agentIdentifier: "unknown",
|
|
637
|
+
agentName: "Unknown Agent",
|
|
638
638
|
});
|
|
639
639
|
|
|
640
640
|
// Should not throw
|
|
@@ -660,16 +660,16 @@ describe("shutdown", () => {
|
|
|
660
660
|
startedAt: Date.now(),
|
|
661
661
|
lastActivityAt: Date.now(),
|
|
662
662
|
isExplicit: false,
|
|
663
|
-
agentIdentifier: "
|
|
664
|
-
agentName: "
|
|
663
|
+
agentIdentifier: "unknown",
|
|
664
|
+
agentName: "Unknown Agent",
|
|
665
665
|
});
|
|
666
666
|
getActiveSessions().set(CARD_B, {
|
|
667
667
|
cardId: CARD_B,
|
|
668
668
|
startedAt: Date.now(),
|
|
669
669
|
lastActivityAt: Date.now(),
|
|
670
670
|
isExplicit: false,
|
|
671
|
-
agentIdentifier: "
|
|
672
|
-
agentName: "
|
|
671
|
+
agentIdentifier: "unknown",
|
|
672
|
+
agentName: "Unknown Agent",
|
|
673
673
|
});
|
|
674
674
|
|
|
675
675
|
await shutdownAllSessions();
|
|
@@ -826,8 +826,8 @@ describe("edge cases", () => {
|
|
|
826
826
|
startedAt: 0,
|
|
827
827
|
lastActivityAt: 0,
|
|
828
828
|
isExplicit: false,
|
|
829
|
-
agentIdentifier: "
|
|
830
|
-
agentName: "
|
|
829
|
+
agentIdentifier: "unknown",
|
|
830
|
+
agentName: "Unknown Agent",
|
|
831
831
|
});
|
|
832
832
|
|
|
833
833
|
checkInactivity();
|
|
@@ -850,8 +850,8 @@ describe("edge cases", () => {
|
|
|
850
850
|
startedAt: 0,
|
|
851
851
|
lastActivityAt: 0,
|
|
852
852
|
isExplicit: false,
|
|
853
|
-
agentIdentifier: "
|
|
854
|
-
agentName: "
|
|
853
|
+
agentIdentifier: "unknown",
|
|
854
|
+
agentName: "Unknown Agent",
|
|
855
855
|
});
|
|
856
856
|
|
|
857
857
|
checkInactivity();
|
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", "Codex", 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,8 +160,8 @@ 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
|
|
|
@@ -176,6 +225,7 @@ export function destroyAutoSession(): void {
|
|
|
176
225
|
activeSessions.clear();
|
|
177
226
|
endCallback = null;
|
|
178
227
|
clientGetter = null;
|
|
228
|
+
clientInfoGetter = null;
|
|
179
229
|
}
|
|
180
230
|
|
|
181
231
|
/**
|
package/src/cli.ts
CHANGED
|
@@ -29,6 +29,11 @@ program
|
|
|
29
29
|
.command("serve")
|
|
30
30
|
.description("Start the MCP server (stdio transport)")
|
|
31
31
|
.action(async () => {
|
|
32
|
+
if (!isConfigured()) {
|
|
33
|
+
console.error("No API key configured.");
|
|
34
|
+
console.error("Run: npx @gethmy/mcp setup");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
32
37
|
await refreshSkills();
|
|
33
38
|
const server = new HarmonyMCPServer();
|
|
34
39
|
await server.run();
|
|
@@ -147,6 +152,8 @@ program
|
|
|
147
152
|
.option("-p, --project <id>", "Set project context")
|
|
148
153
|
.option("--skip-context", "Skip workspace/project selection")
|
|
149
154
|
.option("--skip-docs", "Skip project docs scaffold/verification")
|
|
155
|
+
.option("--new", "Create a new account (skip the choice prompt)")
|
|
156
|
+
.option("-n, --name <name>", "Full name (for account creation)")
|
|
150
157
|
.action(async (options) => {
|
|
151
158
|
await runSetup({
|
|
152
159
|
force: options.force,
|
|
@@ -162,6 +169,8 @@ program
|
|
|
162
169
|
projectId: options.project,
|
|
163
170
|
skipContext: options.skipContext,
|
|
164
171
|
skipDocs: options.skipDocs,
|
|
172
|
+
newAccount: options.new,
|
|
173
|
+
name: options.name,
|
|
165
174
|
});
|
|
166
175
|
});
|
|
167
176
|
|
package/src/onboard.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { requestWithBearer, signupUser } from "./api-client.js";
|
|
2
|
+
import { getApiUrl } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export interface OnboardParams {
|
|
5
|
+
email: string;
|
|
6
|
+
password: string;
|
|
7
|
+
fullName: string;
|
|
8
|
+
workspaceName?: string;
|
|
9
|
+
projectName?: string;
|
|
10
|
+
template?: string;
|
|
11
|
+
keyName?: string;
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface OnboardResult {
|
|
16
|
+
user: { id: string; email: string; full_name: string };
|
|
17
|
+
workspace: { id: string; name: string; slug: string; created_at: string };
|
|
18
|
+
project: { id: string; name: string; slug: string };
|
|
19
|
+
columns: unknown[];
|
|
20
|
+
apiKey: {
|
|
21
|
+
rawKey: string;
|
|
22
|
+
prefix: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function onboardNewUser(
|
|
27
|
+
params: OnboardParams,
|
|
28
|
+
): Promise<OnboardResult> {
|
|
29
|
+
const {
|
|
30
|
+
email,
|
|
31
|
+
password,
|
|
32
|
+
fullName,
|
|
33
|
+
workspaceName = `${fullName}'s Workspace`,
|
|
34
|
+
projectName = "My First Board",
|
|
35
|
+
template = "kanban",
|
|
36
|
+
keyName = "mcp-agent",
|
|
37
|
+
apiUrl = getApiUrl(),
|
|
38
|
+
} = params;
|
|
39
|
+
|
|
40
|
+
// 1. Signup
|
|
41
|
+
const signupResult = await signupUser(apiUrl, {
|
|
42
|
+
email,
|
|
43
|
+
password,
|
|
44
|
+
full_name: fullName,
|
|
45
|
+
});
|
|
46
|
+
const token = signupResult.session.access_token;
|
|
47
|
+
|
|
48
|
+
// 2. Create workspace
|
|
49
|
+
const workspaceResult = await requestWithBearer<{
|
|
50
|
+
workspace: {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
slug: string;
|
|
54
|
+
created_at: string;
|
|
55
|
+
};
|
|
56
|
+
}>(apiUrl, token, "POST", "/workspaces", {
|
|
57
|
+
name: workspaceName,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// 3. Create project
|
|
61
|
+
const projectResult = await requestWithBearer<{
|
|
62
|
+
project: { id: string; name: string; slug: string };
|
|
63
|
+
columns: unknown[];
|
|
64
|
+
}>(apiUrl, token, "POST", "/projects", {
|
|
65
|
+
workspaceId: workspaceResult.workspace.id,
|
|
66
|
+
name: projectName,
|
|
67
|
+
template,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 4. Generate API key
|
|
71
|
+
const keyResult = await requestWithBearer<{
|
|
72
|
+
apiKey: {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
prefix: string;
|
|
76
|
+
created_at: string;
|
|
77
|
+
};
|
|
78
|
+
rawKey: string;
|
|
79
|
+
}>(apiUrl, token, "POST", "/api-keys", {
|
|
80
|
+
name: keyName,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
user: signupResult.user,
|
|
85
|
+
workspace: workspaceResult.workspace,
|
|
86
|
+
project: projectResult.project,
|
|
87
|
+
columns: projectResult.columns,
|
|
88
|
+
apiKey: {
|
|
89
|
+
rawKey: keyResult.rawKey,
|
|
90
|
+
prefix: keyResult.apiKey.prefix,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/remote.ts
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|