@gethmy/mcp 2.14.0 → 2.16.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/cli.js +98 -19
- package/dist/index.js +98 -19
- package/dist/lib/api-client.js +31 -0
- package/package.json +2 -2
- package/src/api-client.ts +83 -0
- package/src/auto-session.ts +91 -1
- package/src/server.ts +67 -29
package/dist/cli.js
CHANGED
|
@@ -1793,12 +1793,43 @@ class HarmonyApiClient {
|
|
|
1793
1793
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1794
1794
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
1795
1795
|
}
|
|
1796
|
+
async getFullBoard(projectId, options) {
|
|
1797
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
1798
|
+
const cards = [];
|
|
1799
|
+
let offset = 0;
|
|
1800
|
+
let last = null;
|
|
1801
|
+
for (;; ) {
|
|
1802
|
+
const page = await this.getBoard(projectId, {
|
|
1803
|
+
...boardOpts,
|
|
1804
|
+
limit: pageSize,
|
|
1805
|
+
offset
|
|
1806
|
+
});
|
|
1807
|
+
last = page;
|
|
1808
|
+
const pageCards = page.cards ?? [];
|
|
1809
|
+
cards.push(...pageCards);
|
|
1810
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
1811
|
+
if (!hasMore || pageCards.length === 0)
|
|
1812
|
+
break;
|
|
1813
|
+
offset += pageSize;
|
|
1814
|
+
}
|
|
1815
|
+
return {
|
|
1816
|
+
project: last?.project,
|
|
1817
|
+
columns: last?.columns ?? [],
|
|
1818
|
+
cards,
|
|
1819
|
+
labels: last?.labels ?? [],
|
|
1820
|
+
totalCards: last?.pagination?.totalCards ?? cards.length
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1796
1823
|
async createCard(projectId, data) {
|
|
1797
1824
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
1798
1825
|
}
|
|
1799
1826
|
async updateCard(cardId, updates) {
|
|
1800
1827
|
return this.request("PATCH", `/cards/${cardId}`, updates);
|
|
1801
1828
|
}
|
|
1829
|
+
async claimCard(cardId, agentId) {
|
|
1830
|
+
const res = await this.request("PATCH", `/cards/${cardId}`, { assignedAgentId: agentId, ifAssignedAgentNull: true });
|
|
1831
|
+
return { claimed: res.claimed !== false };
|
|
1832
|
+
}
|
|
1802
1833
|
async moveCard(cardId, columnId, position) {
|
|
1803
1834
|
return this.request("POST", `/cards/${cardId}/move`, {
|
|
1804
1835
|
columnId,
|
|
@@ -2460,7 +2491,7 @@ function initAutoSession(callback, getClient2, getClientInfo, scopeId = DEFAULT_
|
|
|
2460
2491
|
scope.clientGetter = getClient2;
|
|
2461
2492
|
scope.clientInfoGetter = getClientInfo ?? null;
|
|
2462
2493
|
if (!inactivityTimer) {
|
|
2463
|
-
inactivityTimer = setInterval(
|
|
2494
|
+
inactivityTimer = setInterval(sweepTick, CHECK_INTERVAL_MS);
|
|
2464
2495
|
}
|
|
2465
2496
|
}
|
|
2466
2497
|
async function trackActivity(cardId, options) {
|
|
@@ -2502,7 +2533,8 @@ async function trackActivity(cardId, options) {
|
|
|
2502
2533
|
lastActivityAt: now,
|
|
2503
2534
|
isExplicit: false,
|
|
2504
2535
|
agentIdentifier,
|
|
2505
|
-
agentName
|
|
2536
|
+
agentName,
|
|
2537
|
+
status: "working"
|
|
2506
2538
|
});
|
|
2507
2539
|
}
|
|
2508
2540
|
function markExplicit(cardId, options) {
|
|
@@ -2521,7 +2553,8 @@ function markExplicit(cardId, options) {
|
|
|
2521
2553
|
lastActivityAt: Date.now(),
|
|
2522
2554
|
isExplicit: true,
|
|
2523
2555
|
agentIdentifier: options?.agentIdentifier ?? "explicit",
|
|
2524
|
-
agentName: options?.agentName ?? "Explicit Agent"
|
|
2556
|
+
agentName: options?.agentName ?? "Explicit Agent",
|
|
2557
|
+
status: "working"
|
|
2525
2558
|
});
|
|
2526
2559
|
}
|
|
2527
2560
|
}
|
|
@@ -2567,6 +2600,31 @@ function checkInactivity() {
|
|
|
2567
2600
|
}
|
|
2568
2601
|
}
|
|
2569
2602
|
}
|
|
2603
|
+
function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
|
|
2604
|
+
const session = scopes.get(scopeId)?.sessions.get(cardId);
|
|
2605
|
+
if (session)
|
|
2606
|
+
session.status = status;
|
|
2607
|
+
}
|
|
2608
|
+
function heartbeatActiveSessions() {
|
|
2609
|
+
for (const scope of scopes.values()) {
|
|
2610
|
+
const client3 = scope.clientGetter?.();
|
|
2611
|
+
if (!client3)
|
|
2612
|
+
continue;
|
|
2613
|
+
for (const session of scope.sessions.values()) {
|
|
2614
|
+
if ((session.status ?? "working") !== "working")
|
|
2615
|
+
continue;
|
|
2616
|
+
client3.updateAgentProgress(session.cardId, {
|
|
2617
|
+
agentIdentifier: session.agentIdentifier,
|
|
2618
|
+
agentName: session.agentName,
|
|
2619
|
+
status: "working"
|
|
2620
|
+
}).catch(() => {});
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function sweepTick() {
|
|
2625
|
+
checkInactivity();
|
|
2626
|
+
heartbeatActiveSessions();
|
|
2627
|
+
}
|
|
2570
2628
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2571
2629
|
if (!scope.sessions.delete(cardId))
|
|
2572
2630
|
return;
|
|
@@ -6203,10 +6261,14 @@ async function handleToolCall(name, args, deps) {
|
|
|
6203
6261
|
mergedRecentActions = callerRecentActions;
|
|
6204
6262
|
}
|
|
6205
6263
|
const runActivity = (callerActions || []).map((a) => a.description).filter((d) => typeof d === "string" && d.length > 0);
|
|
6264
|
+
const reportedStatus = args.status;
|
|
6265
|
+
if (reportedStatus) {
|
|
6266
|
+
noteSessionStatus(cardId, reportedStatus, deps.getScopeId?.());
|
|
6267
|
+
}
|
|
6206
6268
|
const result = await client3.updateAgentProgress(cardId, {
|
|
6207
6269
|
agentIdentifier,
|
|
6208
6270
|
agentName,
|
|
6209
|
-
status:
|
|
6271
|
+
status: reportedStatus,
|
|
6210
6272
|
progressPercent,
|
|
6211
6273
|
currentTask: args.currentTask,
|
|
6212
6274
|
blockers: args.blockers,
|
|
@@ -6277,11 +6339,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
6277
6339
|
}
|
|
6278
6340
|
case "harmony_generate_prompt": {
|
|
6279
6341
|
let cardId;
|
|
6342
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
6280
6343
|
if (args.cardId) {
|
|
6281
6344
|
cardId = z.string().uuid().parse(args.cardId);
|
|
6282
6345
|
} else if (args.shortId !== undefined) {
|
|
6283
6346
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
6284
|
-
const projectId = args.projectId || deps.getActiveProjectId();
|
|
6285
6347
|
if (!projectId) {
|
|
6286
6348
|
throw new Error("Project ID required when using shortId. Use harmony_set_project_context or provide projectId.");
|
|
6287
6349
|
}
|
|
@@ -6290,23 +6352,40 @@ async function handleToolCall(name, args, deps) {
|
|
|
6290
6352
|
} else {
|
|
6291
6353
|
throw new Error("Either cardId or shortId must be provided");
|
|
6292
6354
|
}
|
|
6293
|
-
|
|
6294
|
-
|
|
6355
|
+
const variant = args.variant || undefined;
|
|
6356
|
+
const customConstraints = typeof args.customConstraints === "string" ? args.customConstraints : undefined;
|
|
6357
|
+
const contextOptions = {
|
|
6358
|
+
includeSubtasks: args.includeSubtasks !== false,
|
|
6359
|
+
includeLinks: args.includeLinks !== false,
|
|
6360
|
+
includeDescription: args.includeDescription !== false
|
|
6361
|
+
};
|
|
6295
6362
|
try {
|
|
6296
|
-
const
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6363
|
+
const result = await client3.generateCardPrompt({
|
|
6364
|
+
cardId,
|
|
6365
|
+
workspaceId: deps.getActiveWorkspaceId() || "",
|
|
6366
|
+
projectId,
|
|
6367
|
+
variant,
|
|
6368
|
+
customConstraints,
|
|
6369
|
+
contextOptions
|
|
6370
|
+
});
|
|
6371
|
+
return { success: true, ...result };
|
|
6372
|
+
} catch (err) {
|
|
6373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6374
|
+
console.debug(`[harmony_generate_prompt] falling back: ${msg}`);
|
|
6375
|
+
let cardTitle = "";
|
|
6376
|
+
let cardDescription = "";
|
|
6377
|
+
try {
|
|
6378
|
+
const { card } = await client3.getCard(cardId);
|
|
6379
|
+
const typedCard = card;
|
|
6380
|
+
cardTitle = typedCard.title || "";
|
|
6381
|
+
cardDescription = typedCard.description || "";
|
|
6382
|
+
} catch {}
|
|
6383
|
+
const taskBlock = [cardTitle, cardDescription].filter(Boolean).join(`
|
|
6302
6384
|
|
|
6303
6385
|
`);
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
prompt,
|
|
6308
|
-
cardId
|
|
6309
|
-
};
|
|
6386
|
+
const prompt = `Here is the task: ${taskBlock || "(no description available)"}`;
|
|
6387
|
+
return { success: true, prompt, cardId };
|
|
6388
|
+
}
|
|
6310
6389
|
}
|
|
6311
6390
|
case "harmony_remember": {
|
|
6312
6391
|
const title = z.string().min(1).max(300).parse(args.title);
|
package/dist/index.js
CHANGED
|
@@ -1788,12 +1788,43 @@ class HarmonyApiClient {
|
|
|
1788
1788
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1789
1789
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
1790
1790
|
}
|
|
1791
|
+
async getFullBoard(projectId, options) {
|
|
1792
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
1793
|
+
const cards = [];
|
|
1794
|
+
let offset = 0;
|
|
1795
|
+
let last = null;
|
|
1796
|
+
for (;; ) {
|
|
1797
|
+
const page = await this.getBoard(projectId, {
|
|
1798
|
+
...boardOpts,
|
|
1799
|
+
limit: pageSize,
|
|
1800
|
+
offset
|
|
1801
|
+
});
|
|
1802
|
+
last = page;
|
|
1803
|
+
const pageCards = page.cards ?? [];
|
|
1804
|
+
cards.push(...pageCards);
|
|
1805
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
1806
|
+
if (!hasMore || pageCards.length === 0)
|
|
1807
|
+
break;
|
|
1808
|
+
offset += pageSize;
|
|
1809
|
+
}
|
|
1810
|
+
return {
|
|
1811
|
+
project: last?.project,
|
|
1812
|
+
columns: last?.columns ?? [],
|
|
1813
|
+
cards,
|
|
1814
|
+
labels: last?.labels ?? [],
|
|
1815
|
+
totalCards: last?.pagination?.totalCards ?? cards.length
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1791
1818
|
async createCard(projectId, data) {
|
|
1792
1819
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
1793
1820
|
}
|
|
1794
1821
|
async updateCard(cardId, updates) {
|
|
1795
1822
|
return this.request("PATCH", `/cards/${cardId}`, updates);
|
|
1796
1823
|
}
|
|
1824
|
+
async claimCard(cardId, agentId) {
|
|
1825
|
+
const res = await this.request("PATCH", `/cards/${cardId}`, { assignedAgentId: agentId, ifAssignedAgentNull: true });
|
|
1826
|
+
return { claimed: res.claimed !== false };
|
|
1827
|
+
}
|
|
1797
1828
|
async moveCard(cardId, columnId, position) {
|
|
1798
1829
|
return this.request("POST", `/cards/${cardId}/move`, {
|
|
1799
1830
|
columnId,
|
|
@@ -2455,7 +2486,7 @@ function initAutoSession(callback, getClient2, getClientInfo, scopeId = DEFAULT_
|
|
|
2455
2486
|
scope.clientGetter = getClient2;
|
|
2456
2487
|
scope.clientInfoGetter = getClientInfo ?? null;
|
|
2457
2488
|
if (!inactivityTimer) {
|
|
2458
|
-
inactivityTimer = setInterval(
|
|
2489
|
+
inactivityTimer = setInterval(sweepTick, CHECK_INTERVAL_MS);
|
|
2459
2490
|
}
|
|
2460
2491
|
}
|
|
2461
2492
|
async function trackActivity(cardId, options) {
|
|
@@ -2497,7 +2528,8 @@ async function trackActivity(cardId, options) {
|
|
|
2497
2528
|
lastActivityAt: now,
|
|
2498
2529
|
isExplicit: false,
|
|
2499
2530
|
agentIdentifier,
|
|
2500
|
-
agentName
|
|
2531
|
+
agentName,
|
|
2532
|
+
status: "working"
|
|
2501
2533
|
});
|
|
2502
2534
|
}
|
|
2503
2535
|
function markExplicit(cardId, options) {
|
|
@@ -2516,7 +2548,8 @@ function markExplicit(cardId, options) {
|
|
|
2516
2548
|
lastActivityAt: Date.now(),
|
|
2517
2549
|
isExplicit: true,
|
|
2518
2550
|
agentIdentifier: options?.agentIdentifier ?? "explicit",
|
|
2519
|
-
agentName: options?.agentName ?? "Explicit Agent"
|
|
2551
|
+
agentName: options?.agentName ?? "Explicit Agent",
|
|
2552
|
+
status: "working"
|
|
2520
2553
|
});
|
|
2521
2554
|
}
|
|
2522
2555
|
}
|
|
@@ -2562,6 +2595,31 @@ function checkInactivity() {
|
|
|
2562
2595
|
}
|
|
2563
2596
|
}
|
|
2564
2597
|
}
|
|
2598
|
+
function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
|
|
2599
|
+
const session = scopes.get(scopeId)?.sessions.get(cardId);
|
|
2600
|
+
if (session)
|
|
2601
|
+
session.status = status;
|
|
2602
|
+
}
|
|
2603
|
+
function heartbeatActiveSessions() {
|
|
2604
|
+
for (const scope of scopes.values()) {
|
|
2605
|
+
const client3 = scope.clientGetter?.();
|
|
2606
|
+
if (!client3)
|
|
2607
|
+
continue;
|
|
2608
|
+
for (const session of scope.sessions.values()) {
|
|
2609
|
+
if ((session.status ?? "working") !== "working")
|
|
2610
|
+
continue;
|
|
2611
|
+
client3.updateAgentProgress(session.cardId, {
|
|
2612
|
+
agentIdentifier: session.agentIdentifier,
|
|
2613
|
+
agentName: session.agentName,
|
|
2614
|
+
status: "working"
|
|
2615
|
+
}).catch(() => {});
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
function sweepTick() {
|
|
2620
|
+
checkInactivity();
|
|
2621
|
+
heartbeatActiveSessions();
|
|
2622
|
+
}
|
|
2565
2623
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2566
2624
|
if (!scope.sessions.delete(cardId))
|
|
2567
2625
|
return;
|
|
@@ -6198,10 +6256,14 @@ async function handleToolCall(name, args, deps) {
|
|
|
6198
6256
|
mergedRecentActions = callerRecentActions;
|
|
6199
6257
|
}
|
|
6200
6258
|
const runActivity = (callerActions || []).map((a) => a.description).filter((d) => typeof d === "string" && d.length > 0);
|
|
6259
|
+
const reportedStatus = args.status;
|
|
6260
|
+
if (reportedStatus) {
|
|
6261
|
+
noteSessionStatus(cardId, reportedStatus, deps.getScopeId?.());
|
|
6262
|
+
}
|
|
6201
6263
|
const result = await client3.updateAgentProgress(cardId, {
|
|
6202
6264
|
agentIdentifier,
|
|
6203
6265
|
agentName,
|
|
6204
|
-
status:
|
|
6266
|
+
status: reportedStatus,
|
|
6205
6267
|
progressPercent,
|
|
6206
6268
|
currentTask: args.currentTask,
|
|
6207
6269
|
blockers: args.blockers,
|
|
@@ -6272,11 +6334,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
6272
6334
|
}
|
|
6273
6335
|
case "harmony_generate_prompt": {
|
|
6274
6336
|
let cardId;
|
|
6337
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
6275
6338
|
if (args.cardId) {
|
|
6276
6339
|
cardId = z.string().uuid().parse(args.cardId);
|
|
6277
6340
|
} else if (args.shortId !== undefined) {
|
|
6278
6341
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
6279
|
-
const projectId = args.projectId || deps.getActiveProjectId();
|
|
6280
6342
|
if (!projectId) {
|
|
6281
6343
|
throw new Error("Project ID required when using shortId. Use harmony_set_project_context or provide projectId.");
|
|
6282
6344
|
}
|
|
@@ -6285,23 +6347,40 @@ async function handleToolCall(name, args, deps) {
|
|
|
6285
6347
|
} else {
|
|
6286
6348
|
throw new Error("Either cardId or shortId must be provided");
|
|
6287
6349
|
}
|
|
6288
|
-
|
|
6289
|
-
|
|
6350
|
+
const variant = args.variant || undefined;
|
|
6351
|
+
const customConstraints = typeof args.customConstraints === "string" ? args.customConstraints : undefined;
|
|
6352
|
+
const contextOptions = {
|
|
6353
|
+
includeSubtasks: args.includeSubtasks !== false,
|
|
6354
|
+
includeLinks: args.includeLinks !== false,
|
|
6355
|
+
includeDescription: args.includeDescription !== false
|
|
6356
|
+
};
|
|
6290
6357
|
try {
|
|
6291
|
-
const
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6358
|
+
const result = await client3.generateCardPrompt({
|
|
6359
|
+
cardId,
|
|
6360
|
+
workspaceId: deps.getActiveWorkspaceId() || "",
|
|
6361
|
+
projectId,
|
|
6362
|
+
variant,
|
|
6363
|
+
customConstraints,
|
|
6364
|
+
contextOptions
|
|
6365
|
+
});
|
|
6366
|
+
return { success: true, ...result };
|
|
6367
|
+
} catch (err) {
|
|
6368
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6369
|
+
console.debug(`[harmony_generate_prompt] falling back: ${msg}`);
|
|
6370
|
+
let cardTitle = "";
|
|
6371
|
+
let cardDescription = "";
|
|
6372
|
+
try {
|
|
6373
|
+
const { card } = await client3.getCard(cardId);
|
|
6374
|
+
const typedCard = card;
|
|
6375
|
+
cardTitle = typedCard.title || "";
|
|
6376
|
+
cardDescription = typedCard.description || "";
|
|
6377
|
+
} catch {}
|
|
6378
|
+
const taskBlock = [cardTitle, cardDescription].filter(Boolean).join(`
|
|
6297
6379
|
|
|
6298
6380
|
`);
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
prompt,
|
|
6303
|
-
cardId
|
|
6304
|
-
};
|
|
6381
|
+
const prompt = `Here is the task: ${taskBlock || "(no description available)"}`;
|
|
6382
|
+
return { success: true, prompt, cardId };
|
|
6383
|
+
}
|
|
6305
6384
|
}
|
|
6306
6385
|
case "harmony_remember": {
|
|
6307
6386
|
const title = z.string().min(1).max(300).parse(args.title);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1240,12 +1240,43 @@ class HarmonyApiClient {
|
|
|
1240
1240
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1241
1241
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
1242
1242
|
}
|
|
1243
|
+
async getFullBoard(projectId, options) {
|
|
1244
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
1245
|
+
const cards = [];
|
|
1246
|
+
let offset = 0;
|
|
1247
|
+
let last = null;
|
|
1248
|
+
for (;; ) {
|
|
1249
|
+
const page = await this.getBoard(projectId, {
|
|
1250
|
+
...boardOpts,
|
|
1251
|
+
limit: pageSize,
|
|
1252
|
+
offset
|
|
1253
|
+
});
|
|
1254
|
+
last = page;
|
|
1255
|
+
const pageCards = page.cards ?? [];
|
|
1256
|
+
cards.push(...pageCards);
|
|
1257
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
1258
|
+
if (!hasMore || pageCards.length === 0)
|
|
1259
|
+
break;
|
|
1260
|
+
offset += pageSize;
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
project: last?.project,
|
|
1264
|
+
columns: last?.columns ?? [],
|
|
1265
|
+
cards,
|
|
1266
|
+
labels: last?.labels ?? [],
|
|
1267
|
+
totalCards: last?.pagination?.totalCards ?? cards.length
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1243
1270
|
async createCard(projectId, data) {
|
|
1244
1271
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
1245
1272
|
}
|
|
1246
1273
|
async updateCard(cardId, updates) {
|
|
1247
1274
|
return this.request("PATCH", `/cards/${cardId}`, updates);
|
|
1248
1275
|
}
|
|
1276
|
+
async claimCard(cardId, agentId) {
|
|
1277
|
+
const res = await this.request("PATCH", `/cards/${cardId}`, { assignedAgentId: agentId, ifAssignedAgentNull: true });
|
|
1278
|
+
return { claimed: res.claimed !== false };
|
|
1279
|
+
}
|
|
1249
1280
|
async moveCard(cardId, columnId, position) {
|
|
1250
1281
|
return this.request("POST", `/cards/${cardId}/move`, {
|
|
1251
1282
|
columnId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.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"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"serve:remote": "bun src/remote.ts",
|
|
65
65
|
"dev": "bun --watch src/index.ts",
|
|
66
66
|
"test": "bun run test:unit && bun run test:integration",
|
|
67
|
-
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts",
|
|
67
|
+
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts src/__tests__/auto-session.test.ts",
|
|
68
68
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
69
69
|
"typecheck": "tsc --noEmit",
|
|
70
70
|
"prepublishOnly": "bun run typecheck && bun run build"
|
package/src/api-client.ts
CHANGED
|
@@ -527,6 +527,70 @@ export class HarmonyApiClient {
|
|
|
527
527
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Fetch the FULL board — every non-archived card — by paginating past the
|
|
532
|
+
* server's default 50-card window (`getBoard` caps at `limit=50` to keep MCP
|
|
533
|
+
* responses under the token budget).
|
|
534
|
+
*
|
|
535
|
+
* Daemon board scans must see the whole board: with the 50-card cap the
|
|
536
|
+
* entire `Review` column fell outside the first-50-by-position window once a
|
|
537
|
+
* project crossed 50 cards, silently starving the review pipeline (#628 — a
|
|
538
|
+
* 272-card project had 0 reviews for ~2 days). This has no LLM-context/token
|
|
539
|
+
* concern daemon-side, so it walks `offset` until `pagination.hasMore` is
|
|
540
|
+
* false and concatenates every page's cards.
|
|
541
|
+
*
|
|
542
|
+
* Summary mode carries no `cards` array, so callers that only need per-column
|
|
543
|
+
* counts should keep using `getBoard({ summary: true })`. This is the
|
|
544
|
+
* card-list path.
|
|
545
|
+
*/
|
|
546
|
+
async getFullBoard(
|
|
547
|
+
projectId: string,
|
|
548
|
+
options?: {
|
|
549
|
+
columnId?: string;
|
|
550
|
+
includeArchived?: boolean;
|
|
551
|
+
labelName?: string;
|
|
552
|
+
/** Page size for the offset walk. Defaults to 200. */
|
|
553
|
+
pageSize?: number;
|
|
554
|
+
},
|
|
555
|
+
): Promise<{
|
|
556
|
+
project: unknown;
|
|
557
|
+
columns: unknown[];
|
|
558
|
+
cards: unknown[];
|
|
559
|
+
labels: unknown[];
|
|
560
|
+
totalCards: number;
|
|
561
|
+
}> {
|
|
562
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
563
|
+
const cards: unknown[] = [];
|
|
564
|
+
let offset = 0;
|
|
565
|
+
let last: Awaited<ReturnType<HarmonyApiClient["getBoard"]>> | null = null;
|
|
566
|
+
|
|
567
|
+
// Walk pages until the server reports no more. Guard on `hasMore` (not
|
|
568
|
+
// `cards.length`) so a label-filtered final page shorter than pageSize
|
|
569
|
+
// terminates cleanly; the empty-page break is a belt-and-braces backstop
|
|
570
|
+
// against a server that omits `pagination`.
|
|
571
|
+
for (;;) {
|
|
572
|
+
const page = await this.getBoard(projectId, {
|
|
573
|
+
...boardOpts,
|
|
574
|
+
limit: pageSize,
|
|
575
|
+
offset,
|
|
576
|
+
});
|
|
577
|
+
last = page;
|
|
578
|
+
const pageCards = page.cards ?? [];
|
|
579
|
+
cards.push(...pageCards);
|
|
580
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
581
|
+
if (!hasMore || pageCards.length === 0) break;
|
|
582
|
+
offset += pageSize;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
project: last?.project,
|
|
587
|
+
columns: last?.columns ?? [],
|
|
588
|
+
cards,
|
|
589
|
+
labels: last?.labels ?? [],
|
|
590
|
+
totalCards: last?.pagination?.totalCards ?? cards.length,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
530
594
|
// ============ CARD OPERATIONS ============
|
|
531
595
|
|
|
532
596
|
async createCard(
|
|
@@ -561,6 +625,25 @@ export class HarmonyApiClient {
|
|
|
561
625
|
return this.request("PATCH", `/cards/${cardId}`, updates);
|
|
562
626
|
}
|
|
563
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Compare-and-set claim of a card for a virtual agent: sets assigned_agent_id
|
|
630
|
+
* only if it is currently NULL (server-side guard). Returns `{ claimed: false }`
|
|
631
|
+
* when another daemon already owns it. Used by the agent-agnostic review pickup.
|
|
632
|
+
*/
|
|
633
|
+
async claimCard(
|
|
634
|
+
cardId: string,
|
|
635
|
+
agentId: string,
|
|
636
|
+
): Promise<{ claimed: boolean }> {
|
|
637
|
+
const res = await this.request<{ card?: unknown; claimed?: boolean }>(
|
|
638
|
+
"PATCH",
|
|
639
|
+
`/cards/${cardId}`,
|
|
640
|
+
{ assignedAgentId: agentId, ifAssignedAgentNull: true },
|
|
641
|
+
);
|
|
642
|
+
// Success body is `{ card }` (no `claimed` key) → won. Lost-race body is
|
|
643
|
+
// `{ claimed: false }`.
|
|
644
|
+
return { claimed: res.claimed !== false };
|
|
645
|
+
}
|
|
646
|
+
|
|
564
647
|
async moveCard(
|
|
565
648
|
cardId: string,
|
|
566
649
|
columnId: string,
|
package/src/auto-session.ts
CHANGED
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
|
|
26
26
|
import type { HarmonyApiClient } from "./api-client.js";
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Status reported for a tracked session. Drives the heartbeat decision: only
|
|
30
|
+
* `working` sessions are heartbeated by the sweep. `paused`/`blocked`/`waiting`
|
|
31
|
+
* are intentional non-heartbeat states — the agent has deliberately stopped
|
|
32
|
+
* making progress (parked for a human, awaiting input, …), so bumping their
|
|
33
|
+
* `updated_at` would both keep a paused row looking active and mask a genuinely
|
|
34
|
+
* stalled run.
|
|
35
|
+
*/
|
|
36
|
+
export type SessionStatus = "working" | "blocked" | "waiting" | "paused";
|
|
37
|
+
|
|
28
38
|
export interface TrackedSession {
|
|
29
39
|
cardId: string;
|
|
30
40
|
startedAt: number;
|
|
@@ -33,6 +43,11 @@ export interface TrackedSession {
|
|
|
33
43
|
isExplicit: boolean;
|
|
34
44
|
agentIdentifier: string;
|
|
35
45
|
agentName: string;
|
|
46
|
+
/**
|
|
47
|
+
* Last status reported for the session (default `working`). Only `working`
|
|
48
|
+
* sessions are heartbeated by the sweep; see `heartbeatActiveSessions`.
|
|
49
|
+
*/
|
|
50
|
+
status?: SessionStatus;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
export type EndSessionCallback = (
|
|
@@ -173,7 +188,7 @@ export function initAutoSession(
|
|
|
173
188
|
// the remote transport, connections arrive faster than the 60s interval and a
|
|
174
189
|
// restart-each-time would keep deferring the sweep indefinitely (starvation).
|
|
175
190
|
if (!inactivityTimer) {
|
|
176
|
-
inactivityTimer = setInterval(
|
|
191
|
+
inactivityTimer = setInterval(sweepTick, CHECK_INTERVAL_MS);
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -256,6 +271,7 @@ export async function trackActivity(
|
|
|
256
271
|
isExplicit: false,
|
|
257
272
|
agentIdentifier,
|
|
258
273
|
agentName,
|
|
274
|
+
status: "working",
|
|
259
275
|
});
|
|
260
276
|
}
|
|
261
277
|
|
|
@@ -283,6 +299,7 @@ export function markExplicit(
|
|
|
283
299
|
isExplicit: true,
|
|
284
300
|
agentIdentifier: options?.agentIdentifier ?? "explicit",
|
|
285
301
|
agentName: options?.agentName ?? "Explicit Agent",
|
|
302
|
+
status: "working",
|
|
286
303
|
});
|
|
287
304
|
}
|
|
288
305
|
}
|
|
@@ -376,6 +393,79 @@ export function checkInactivity(): void {
|
|
|
376
393
|
}
|
|
377
394
|
}
|
|
378
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Record the latest status reported for a tracked session so the sweep knows
|
|
398
|
+
* whether to keep heartbeating it. Wired into `harmony_update_agent_progress`
|
|
399
|
+
* (server.ts): when an agent reports `paused`/`blocked`/`waiting` the heartbeat
|
|
400
|
+
* stops for that session (those are intentional non-heartbeat states); a later
|
|
401
|
+
* `working` report resumes it. No-op when the card isn't tracked in the scope.
|
|
402
|
+
*/
|
|
403
|
+
export function noteSessionStatus(
|
|
404
|
+
cardId: string,
|
|
405
|
+
status: SessionStatus,
|
|
406
|
+
scopeId: string = DEFAULT_SCOPE,
|
|
407
|
+
): void {
|
|
408
|
+
const session = scopes.get(scopeId)?.sessions.get(cardId);
|
|
409
|
+
if (session) session.status = status;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Heartbeat every tracked `working` session by writing a status-only
|
|
414
|
+
* `updateAgentProgress`. The backend's `card_agent_context` update trigger
|
|
415
|
+
* bumps `updated_at` — the field both the kanban card ribbon
|
|
416
|
+
* (`isAgentContextLive`) and the header "Connected Agents" panel key off to
|
|
417
|
+
* decide a session is still live.
|
|
418
|
+
*
|
|
419
|
+
* WHY (card #608): an interactive MCP client (Claude Code, Cursor, /hmy) opens a
|
|
420
|
+
* `card_agent_context` row but, unlike the agent daemon's progress-tracker (60s
|
|
421
|
+
* heartbeat), only writes progress at milestones — often many minutes apart. On
|
|
422
|
+
* a long run `updated_at` goes stale and the card drops its live indicator
|
|
423
|
+
* mid-flight. This sweep gives interactive sessions the same ~60s heartbeat.
|
|
424
|
+
*
|
|
425
|
+
* Only `working` sessions are heartbeated; `paused`/`blocked`/`waiting` are
|
|
426
|
+
* intentional non-heartbeat states (see `SessionStatus`). The payload is
|
|
427
|
+
* status-only (no currentTask/progressPercent) so the heartbeat never clobbers
|
|
428
|
+
* the real progress the agent last reported — the trigger still bumps
|
|
429
|
+
* `updated_at`. Covers both auto-started and explicit sessions (both live in
|
|
430
|
+
* `scope.sessions`).
|
|
431
|
+
*
|
|
432
|
+
* No double-heartbeat for the daemon: `packages/harmony-agent` has its own
|
|
433
|
+
* progress-tracker heartbeat and never routes through mcp-server auto-sessions.
|
|
434
|
+
*/
|
|
435
|
+
export function heartbeatActiveSessions(): void {
|
|
436
|
+
for (const scope of scopes.values()) {
|
|
437
|
+
const client = scope.clientGetter?.();
|
|
438
|
+
if (!client) continue;
|
|
439
|
+
for (const session of scope.sessions.values()) {
|
|
440
|
+
// Default-undefined status is treated as `working` (sessions created
|
|
441
|
+
// before a status was reported).
|
|
442
|
+
if ((session.status ?? "working") !== "working") continue;
|
|
443
|
+
client
|
|
444
|
+
.updateAgentProgress(session.cardId, {
|
|
445
|
+
agentIdentifier: session.agentIdentifier,
|
|
446
|
+
agentName: session.agentName,
|
|
447
|
+
status: "working",
|
|
448
|
+
})
|
|
449
|
+
.catch(() => {
|
|
450
|
+
// Best-effort: a transient failure just defers the bump to the next
|
|
451
|
+
// sweep. updateAgentProgress is fire-and-forget here.
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* One tick of the shared 60s sweep: prune idle auto-sessions
|
|
459
|
+
* (`checkInactivity`) first, then heartbeat the surviving `working` sessions
|
|
460
|
+
* (`heartbeatActiveSessions`). Order matters — prune before heartbeat so we
|
|
461
|
+
* never bump a session we're about to end. Exported for tests; in production
|
|
462
|
+
* the single process-wide timer calls it every CHECK_INTERVAL_MS.
|
|
463
|
+
*/
|
|
464
|
+
export function sweepTick(): void {
|
|
465
|
+
checkInactivity();
|
|
466
|
+
heartbeatActiveSessions();
|
|
467
|
+
}
|
|
468
|
+
|
|
379
469
|
// --- Internal ---
|
|
380
470
|
|
|
381
471
|
async function autoEndSession(
|
package/src/server.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
destroyAutoSession,
|
|
23
23
|
initAutoSession,
|
|
24
24
|
markExplicit,
|
|
25
|
+
noteSessionStatus,
|
|
25
26
|
shutdownAllSessions,
|
|
26
27
|
trackActivity,
|
|
27
28
|
untrack,
|
|
@@ -3684,15 +3685,24 @@ async function handleToolCall(
|
|
|
3684
3685
|
.map((a) => a.description)
|
|
3685
3686
|
.filter((d): d is string => typeof d === "string" && d.length > 0);
|
|
3686
3687
|
|
|
3688
|
+
const reportedStatus = args.status as
|
|
3689
|
+
| "working"
|
|
3690
|
+
| "blocked"
|
|
3691
|
+
| "waiting"
|
|
3692
|
+
| "paused"
|
|
3693
|
+
| undefined;
|
|
3694
|
+
|
|
3695
|
+
// Keep the auto-session heartbeat in step with the agent's own status
|
|
3696
|
+
// reports: `paused`/`blocked`/`waiting` stop the 60s heartbeat (intentional
|
|
3697
|
+
// non-heartbeat states), a later `working` resumes it (card #608).
|
|
3698
|
+
if (reportedStatus) {
|
|
3699
|
+
noteSessionStatus(cardId, reportedStatus, deps.getScopeId?.());
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3687
3702
|
const result = await client.updateAgentProgress(cardId, {
|
|
3688
3703
|
agentIdentifier,
|
|
3689
3704
|
agentName,
|
|
3690
|
-
status:
|
|
3691
|
-
| "working"
|
|
3692
|
-
| "blocked"
|
|
3693
|
-
| "waiting"
|
|
3694
|
-
| "paused"
|
|
3695
|
-
| undefined,
|
|
3705
|
+
status: reportedStatus,
|
|
3696
3706
|
progressPercent,
|
|
3697
3707
|
currentTask: args.currentTask as string | undefined,
|
|
3698
3708
|
blockers: args.blockers as string[] | undefined,
|
|
@@ -3814,17 +3824,16 @@ async function handleToolCall(
|
|
|
3814
3824
|
}
|
|
3815
3825
|
|
|
3816
3826
|
// Prompt generation
|
|
3817
|
-
// TODO Phase 1: rebuild full context assembly per docs/superpowers/plans/2026-05-07-memory-architecture-v2.md §10
|
|
3818
3827
|
case "harmony_generate_prompt": {
|
|
3819
3828
|
// Resolve card ID — either directly or via short ID
|
|
3820
3829
|
let cardId: string;
|
|
3830
|
+
const projectId =
|
|
3831
|
+
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
3821
3832
|
|
|
3822
3833
|
if (args.cardId) {
|
|
3823
3834
|
cardId = z.string().uuid().parse(args.cardId);
|
|
3824
3835
|
} else if (args.shortId !== undefined) {
|
|
3825
3836
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
3826
|
-
const projectId =
|
|
3827
|
-
(args.projectId as string) || deps.getActiveProjectId();
|
|
3828
3837
|
if (!projectId) {
|
|
3829
3838
|
throw new Error(
|
|
3830
3839
|
"Project ID required when using shortId. Use harmony_set_project_context or provide projectId.",
|
|
@@ -3836,29 +3845,58 @@ async function handleToolCall(
|
|
|
3836
3845
|
throw new Error("Either cardId or shortId must be provided");
|
|
3837
3846
|
}
|
|
3838
3847
|
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3848
|
+
const variant =
|
|
3849
|
+
(args.variant as "analysis" | "draft" | "execute") || undefined;
|
|
3850
|
+
const customConstraints =
|
|
3851
|
+
typeof args.customConstraints === "string"
|
|
3852
|
+
? args.customConstraints
|
|
3853
|
+
: undefined;
|
|
3854
|
+
|
|
3855
|
+
// Context toggles default ON — a card's reference fields (linked cards,
|
|
3856
|
+
// external URLs, attachments) are hydrated inside generateCardPrompt so
|
|
3857
|
+
// the prompt carries the links the agent needs to fetch related content.
|
|
3858
|
+
const contextOptions = {
|
|
3859
|
+
includeSubtasks: args.includeSubtasks !== false,
|
|
3860
|
+
includeLinks: args.includeLinks !== false,
|
|
3861
|
+
includeDescription: args.includeDescription !== false,
|
|
3862
|
+
};
|
|
3863
|
+
|
|
3843
3864
|
try {
|
|
3844
|
-
const
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3865
|
+
const result = await client.generateCardPrompt({
|
|
3866
|
+
cardId,
|
|
3867
|
+
workspaceId: deps.getActiveWorkspaceId() || "",
|
|
3868
|
+
projectId,
|
|
3869
|
+
variant,
|
|
3870
|
+
customConstraints,
|
|
3871
|
+
contextOptions,
|
|
3872
|
+
});
|
|
3873
|
+
return { success: true, ...result };
|
|
3874
|
+
} catch (err) {
|
|
3875
|
+
// Graceful fallback: if full context assembly fails (e.g. an offline
|
|
3876
|
+
// sub-fetch), still return a usable title/description prompt so the
|
|
3877
|
+
// Harmony command never dead-ends. The reference-link hydration is
|
|
3878
|
+
// best-effort inside generateCardPrompt and won't reach here.
|
|
3879
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3880
|
+
console.debug(`[harmony_generate_prompt] falling back: ${msg}`);
|
|
3881
|
+
|
|
3882
|
+
let cardTitle = "";
|
|
3883
|
+
let cardDescription = "";
|
|
3884
|
+
try {
|
|
3885
|
+
const { card } = await client.getCard(cardId);
|
|
3886
|
+
const typedCard = card as { title?: string; description?: string };
|
|
3887
|
+
cardTitle = typedCard.title || "";
|
|
3888
|
+
cardDescription = typedCard.description || "";
|
|
3889
|
+
} catch {
|
|
3890
|
+
// Card fetch failed; return an empty task description.
|
|
3891
|
+
}
|
|
3851
3892
|
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3893
|
+
const taskBlock = [cardTitle, cardDescription]
|
|
3894
|
+
.filter(Boolean)
|
|
3895
|
+
.join("\n\n");
|
|
3896
|
+
const prompt = `Here is the task: ${taskBlock || "(no description available)"}`;
|
|
3856
3897
|
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
prompt,
|
|
3860
|
-
cardId,
|
|
3861
|
-
};
|
|
3898
|
+
return { success: true, prompt, cardId };
|
|
3899
|
+
}
|
|
3862
3900
|
}
|
|
3863
3901
|
|
|
3864
3902
|
// Memory / Knowledge Graph operations
|