@gethmy/mcp 2.13.4 → 2.15.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/README.md +5 -2
- package/dist/cli.js +333 -276
- package/dist/index.js +333 -276
- package/dist/lib/api-client.js +170 -140
- package/dist/lib/oauth-refresh.js +350 -0
- package/package.json +7 -3
- package/src/api-client.ts +77 -12
- package/src/auto-session.ts +91 -1
- package/src/server.ts +403 -326
- package/src/skills.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.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"
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"./src/config.js": {
|
|
17
17
|
"types": "./src/config.ts",
|
|
18
18
|
"default": "./dist/lib/config.js"
|
|
19
|
+
},
|
|
20
|
+
"./src/oauth-refresh.js": {
|
|
21
|
+
"types": "./src/oauth-refresh.ts",
|
|
22
|
+
"default": "./dist/lib/oauth-refresh.js"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"bin": {
|
|
@@ -55,12 +59,12 @@
|
|
|
55
59
|
"bun": ">=1.0.0"
|
|
56
60
|
},
|
|
57
61
|
"scripts": {
|
|
58
|
-
"build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod && bun build src/api-client.ts src/config.ts --outdir dist/lib --root src --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod",
|
|
62
|
+
"build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod && bun build src/api-client.ts src/config.ts src/oauth-refresh.ts --outdir dist/lib --root src --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod",
|
|
59
63
|
"build:bun": "bun build src/index.ts src/http.ts src/remote.ts src/cli.ts --outdir dist --target bun",
|
|
60
64
|
"serve:remote": "bun src/remote.ts",
|
|
61
65
|
"dev": "bun --watch src/index.ts",
|
|
62
66
|
"test": "bun run test:unit && bun run test:integration",
|
|
63
|
-
"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",
|
|
64
68
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
65
69
|
"typecheck": "tsc --noEmit",
|
|
66
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(
|
|
@@ -719,6 +783,17 @@ export class HarmonyApiClient {
|
|
|
719
783
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
720
784
|
}
|
|
721
785
|
|
|
786
|
+
async addExternalLink(
|
|
787
|
+
cardId: string,
|
|
788
|
+
url: string,
|
|
789
|
+
title?: string,
|
|
790
|
+
): Promise<{ link: CardExternalLinkRow }> {
|
|
791
|
+
return this.request("POST", `/cards/${cardId}/external-links`, {
|
|
792
|
+
url,
|
|
793
|
+
title,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
722
797
|
// ============ ARTIFACTS (hosted HTML documents) ============
|
|
723
798
|
|
|
724
799
|
async uploadArtifact(data: {
|
|
@@ -842,6 +917,7 @@ export class HarmonyApiClient {
|
|
|
842
917
|
commentType?: string;
|
|
843
918
|
supersedesId?: string;
|
|
844
919
|
confirmsId?: string;
|
|
920
|
+
replyToId?: string;
|
|
845
921
|
agentSessionId?: string;
|
|
846
922
|
},
|
|
847
923
|
): Promise<{ comment: unknown }> {
|
|
@@ -851,6 +927,7 @@ export class HarmonyApiClient {
|
|
|
851
927
|
commentType: opts?.commentType,
|
|
852
928
|
supersedesId: opts?.supersedesId,
|
|
853
929
|
confirmsId: opts?.confirmsId,
|
|
930
|
+
replyToId: opts?.replyToId,
|
|
854
931
|
agentSessionId: opts?.agentSessionId,
|
|
855
932
|
});
|
|
856
933
|
}
|
|
@@ -1871,7 +1948,6 @@ export class HarmonyApiClient {
|
|
|
1871
1948
|
name: string;
|
|
1872
1949
|
description?: string;
|
|
1873
1950
|
steps?: unknown;
|
|
1874
|
-
stepsVersion?: number;
|
|
1875
1951
|
triggerType?: string;
|
|
1876
1952
|
}): Promise<{ playbook: unknown }> {
|
|
1877
1953
|
return this.request("POST", "/playbooks", data);
|
|
@@ -1889,17 +1965,6 @@ export class HarmonyApiClient {
|
|
|
1889
1965
|
): Promise<{ playbook: unknown }> {
|
|
1890
1966
|
return this.request("PATCH", `/playbooks/${playbookId}`, updates);
|
|
1891
1967
|
}
|
|
1892
|
-
|
|
1893
|
-
async runPlaybook(playbookId: string): Promise<{ run: unknown }> {
|
|
1894
|
-
return this.request("POST", `/playbooks/${playbookId}/run`);
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
async savePlaybookFromCard(data: {
|
|
1898
|
-
cardId: string;
|
|
1899
|
-
name?: string;
|
|
1900
|
-
}): Promise<{ playbook: unknown }> {
|
|
1901
|
-
return this.request("POST", "/playbooks/from-card", data);
|
|
1902
|
-
}
|
|
1903
1968
|
}
|
|
1904
1969
|
|
|
1905
1970
|
// Shared types for generateCardPrompt to avoid inline assertions
|
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(
|