@gethmy/mcp 2.20.0 → 2.21.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 +72 -15
- package/dist/index.js +53 -12
- package/dist/lib/api-client.js +8 -0
- package/package.json +2 -2
- package/src/api-client.ts +69 -5
- package/src/auto-session.ts +25 -6
- package/src/remote.ts +616 -266
- package/src/server.ts +106 -11
- package/src/tui/setup.ts +19 -3
package/src/server.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./api-client.js";
|
|
20
20
|
import {
|
|
21
21
|
AUTO_START_TRIGGERS,
|
|
22
|
+
type ClientInfo,
|
|
22
23
|
destroyAutoSession,
|
|
23
24
|
initAutoSession,
|
|
24
25
|
markExplicit,
|
|
@@ -317,6 +318,18 @@ export interface ToolDeps {
|
|
|
317
318
|
* shared auto-session entry. Omitted on stdio (single user → default scope).
|
|
318
319
|
*/
|
|
319
320
|
getScopeId?: () => string;
|
|
321
|
+
/**
|
|
322
|
+
* MCP client identity for the caller, when the transport remembered one.
|
|
323
|
+
*
|
|
324
|
+
* A stateless `tools/call` carries no handshake, so `getClientVersion()` is
|
|
325
|
+
* undefined there and this is the only thing auto-session can attribute the
|
|
326
|
+
* work to (#297). Resolved per request, which makes it per *OAuth grant* on
|
|
327
|
+
* the hosted path (#774) — the scope-level `clientInfoGetter` cannot be,
|
|
328
|
+
* because a user's second client re-runs `initAutoSession` for the same scope
|
|
329
|
+
* and overwrites it (last-initializer-wins). Omitted on stdio, which reads
|
|
330
|
+
* identity straight off the in-scope `Server`.
|
|
331
|
+
*/
|
|
332
|
+
getClientInfo?: () => ClientInfo | null;
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
// --- Memory Session Tracking ---
|
|
@@ -532,8 +545,15 @@ async function flushMemoryActions(
|
|
|
532
545
|
agentIdentifier: session.agentIdentifier,
|
|
533
546
|
agentName: session.agentName,
|
|
534
547
|
recentActions: session.allActions,
|
|
548
|
+
// A bookkeeping flush of memory actions is the last thing that should mint
|
|
549
|
+
// a session on a card a human stopped (#770).
|
|
550
|
+
implicitCreate: true,
|
|
535
551
|
});
|
|
536
552
|
|
|
553
|
+
// Cleared even when the write was refused (`{session: null, stopped: true}`
|
|
554
|
+
// resolves rather than throwing). Deliberate: the bar never lapses, so these
|
|
555
|
+
// buffered display-only actions can never be written — retaining them would
|
|
556
|
+
// just re-attempt the same refusal on every later flush.
|
|
537
557
|
session.dirty = false;
|
|
538
558
|
} catch (err) {
|
|
539
559
|
// Fire-and-forget: log but don't propagate
|
|
@@ -1005,9 +1025,12 @@ export const TOOLS = {
|
|
|
1005
1025
|
'`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' +
|
|
1006
1026
|
"exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " +
|
|
1007
1027
|
"the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " +
|
|
1008
|
-
"Returns the attachment/artifact
|
|
1009
|
-
"
|
|
1010
|
-
"
|
|
1028
|
+
"Returns the attachment/artifact. An artifact comes back with TWO URLs, and the difference " +
|
|
1029
|
+
"matters: `app_url` is the durable in-app permalink — this is the one to hand a person, it " +
|
|
1030
|
+
"never expires and requires them to log in as a workspace member — while `signed_url` only " +
|
|
1031
|
+
"renders the document in-app and dies within the hour, so never pass it on. To share with " +
|
|
1032
|
+
"someone OUTSIDE the workspace, mint a public link with harmony_share_artifact. Large files " +
|
|
1033
|
+
"on the hosted MCP server: use harmony_request_upload_url + harmony_finalize_upload instead.",
|
|
1011
1034
|
inputSchema: {
|
|
1012
1035
|
type: "object",
|
|
1013
1036
|
properties: {
|
|
@@ -1119,7 +1142,9 @@ export const TOOLS = {
|
|
|
1119
1142
|
"enforces size + content-type (magic-byte sniff, never the declared type) + an optional " +
|
|
1120
1143
|
'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' +
|
|
1121
1144
|
'(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' +
|
|
1122
|
-
"cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed
|
|
1145
|
+
"cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed " +
|
|
1146
|
+
"URL; an artifact also carries `app_url`, the durable in-app permalink — hand that one to a " +
|
|
1147
|
+
"person, never the short-lived `signed_url`.",
|
|
1123
1148
|
inputSchema: {
|
|
1124
1149
|
type: "object",
|
|
1125
1150
|
properties: {
|
|
@@ -1182,7 +1207,7 @@ export const TOOLS = {
|
|
|
1182
1207
|
},
|
|
1183
1208
|
harmony_share_artifact: {
|
|
1184
1209
|
description:
|
|
1185
|
-
"Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL.",
|
|
1210
|
+
"Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL. Only needed for a recipient OUTSIDE the workspace — for a teammate, hand over the `app_url` the upload already returned instead of exposing the document publicly.",
|
|
1186
1211
|
inputSchema: {
|
|
1187
1212
|
type: "object",
|
|
1188
1213
|
properties: {
|
|
@@ -1531,7 +1556,7 @@ export const TOOLS = {
|
|
|
1531
1556
|
},
|
|
1532
1557
|
harmony_update_agent_progress: {
|
|
1533
1558
|
description:
|
|
1534
|
-
"Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes.",
|
|
1559
|
+
"Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live.",
|
|
1535
1560
|
inputSchema: {
|
|
1536
1561
|
type: "object",
|
|
1537
1562
|
properties: {
|
|
@@ -1622,7 +1647,7 @@ export const TOOLS = {
|
|
|
1622
1647
|
},
|
|
1623
1648
|
harmony_get_pending_messages: {
|
|
1624
1649
|
description:
|
|
1625
|
-
"Drain queued steering messages a teammate sent to your live agent session (
|
|
1650
|
+
"Drain queued steering messages a teammate sent to your live agent session, and check whether that session is still live (cards #473, #770). Call at your progress checkpoints with the session id from harmony_start_agent_session and the highest seq you've already consumed; returns user messages with seq > sinceSeq, oldest first. Fold them into your next step and advance sinceSeq to the largest returned seq so each is handled exactly once. Two liveness flags come back and mean different things: `stopped` (a human pressed Stop) is TERMINAL — stop work at once, make no further edits, commits, pushes, or progress writes, don't move the card, and report what you had done and where any uncommitted work lives. `sessionStale` only means your session id went stale (usually the 30-minute inactivity cron) — nobody stopped you, so do NOT abandon the work: open a fresh session with harmony_start_agent_session and carry on with its new id. (Distinct from harmony_move_card's `sessionEnded`, which means the move deliberately closed your session.)",
|
|
1626
1651
|
inputSchema: {
|
|
1627
1652
|
type: "object",
|
|
1628
1653
|
properties: {
|
|
@@ -2609,11 +2634,15 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
2609
2634
|
// the hosted/OAuth path, not just stdio (card #297).
|
|
2610
2635
|
// Optional-chained: the pre-hook is best-effort and must never throw into
|
|
2611
2636
|
// tool dispatch if a transport/wrapper doesn't expose getClientVersion.
|
|
2637
|
+
// `getClientInfo` covers the stateless case, where the handshake this
|
|
2638
|
+
// reads is on a different request entirely (#774).
|
|
2612
2639
|
const cv = server.getClientVersion?.();
|
|
2613
2640
|
trackActivity(cardIdArg, {
|
|
2614
2641
|
autoStart: isAutoStartTrigger,
|
|
2615
2642
|
client: deps.getClient(),
|
|
2616
|
-
clientInfo: cv
|
|
2643
|
+
clientInfo: cv
|
|
2644
|
+
? { name: cv.name, version: cv.version }
|
|
2645
|
+
: (deps.getClientInfo?.() ?? undefined),
|
|
2617
2646
|
scopeId: deps.getScopeId?.(),
|
|
2618
2647
|
}).catch(() => {}); // fire-and-forget
|
|
2619
2648
|
}
|
|
@@ -2642,7 +2671,7 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
2642
2671
|
client: deps.getClient(),
|
|
2643
2672
|
clientInfo: cv
|
|
2644
2673
|
? { name: cv.name, version: cv.version }
|
|
2645
|
-
: undefined,
|
|
2674
|
+
: (deps.getClientInfo?.() ?? undefined),
|
|
2646
2675
|
scopeId: deps.getScopeId?.(),
|
|
2647
2676
|
}).catch(() => {});
|
|
2648
2677
|
}
|
|
@@ -3987,8 +4016,21 @@ async function handleToolCall(
|
|
|
3987
4016
|
),
|
|
3988
4017
|
...(mergedRecentActions && { recentActions: mergedRecentActions }),
|
|
3989
4018
|
...(runActivity.length > 0 && { runActivity }),
|
|
4019
|
+
// Reporting progress must never resurrect a run a human stopped (#770).
|
|
4020
|
+
// This is THE call the surviving client makes at every checkpoint, so it
|
|
4021
|
+
// is both the resurrection vector and the earliest place to hand that
|
|
4022
|
+
// client the news that it was stopped.
|
|
4023
|
+
implicitCreate: true,
|
|
3990
4024
|
});
|
|
3991
4025
|
|
|
4026
|
+
// This handler never sends `noCreate`, so the create branch either creates
|
|
4027
|
+
// a row or refuses — meaning a null session here can only be the human-stop
|
|
4028
|
+
// refusal. Drop it from auto-session tracking so the 60s heartbeat stops
|
|
4029
|
+
// beating a card we're barred from writing to.
|
|
4030
|
+
if (result.session === null) {
|
|
4031
|
+
untrack(cardId, deps.getScopeId?.());
|
|
4032
|
+
}
|
|
4033
|
+
|
|
3992
4034
|
// Phase 0 (memory architecture v2): mid-session learning extraction removed.
|
|
3993
4035
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
3994
4036
|
}
|
|
@@ -4007,8 +4049,13 @@ async function handleToolCall(
|
|
|
4007
4049
|
await flushMemoryActions(client, cardId);
|
|
4008
4050
|
cleanupMemorySession(cardId);
|
|
4009
4051
|
|
|
4010
|
-
// End the session — tolerate failure (e.g., session already ended or not found)
|
|
4011
|
-
|
|
4052
|
+
// End the session — tolerate failure (e.g., session already ended or not found).
|
|
4053
|
+
// Typed off the client so the `ended`/`reason` discriminator (#769) reaches the
|
|
4054
|
+
// tool payload by contract, not by accident of the spread below. Left absent
|
|
4055
|
+
// here on purpose: a throw means we don't know what the server did.
|
|
4056
|
+
let result: Awaited<ReturnType<typeof client.endAgentSession>> = {
|
|
4057
|
+
session: null,
|
|
4058
|
+
};
|
|
4012
4059
|
let sessionEndError: string | null = null;
|
|
4013
4060
|
try {
|
|
4014
4061
|
result = await client.endAgentSession(cardId, {
|
|
@@ -4095,6 +4142,54 @@ async function handleToolCall(
|
|
|
4095
4142
|
sessionId,
|
|
4096
4143
|
sinceSeq,
|
|
4097
4144
|
);
|
|
4145
|
+
|
|
4146
|
+
// Hoist the verdict out of the nested `session` object and say what it
|
|
4147
|
+
// obliges (#770). The poll is the one call a stopped MCP/human client is
|
|
4148
|
+
// guaranteed to make — Stop's Realtime broadcast has no consumer for
|
|
4149
|
+
// `agent_id == null` sessions — so the news has to be impossible to skim
|
|
4150
|
+
// past here.
|
|
4151
|
+
//
|
|
4152
|
+
// Two outcomes, deliberately NOT merged. Only a human Stop is terminal; a
|
|
4153
|
+
// row the 30-minute stale cron closed (or one that vanished) just means the
|
|
4154
|
+
// session id is stale. Telling an agent to abandon its work in that case
|
|
4155
|
+
// would destroy a healthy run — a `blocked` session waiting on a user's
|
|
4156
|
+
// answer is not heartbeated, so it reaches the cron routinely.
|
|
4157
|
+
const stoppedByHuman = result.session?.stoppedByHuman === true;
|
|
4158
|
+
const ended = result.session?.ended === true;
|
|
4159
|
+
// Either verdict means stop beating this card: `ended` because the row is
|
|
4160
|
+
// gone, `stoppedByHuman` because we're about to tell the client to stand
|
|
4161
|
+
// down. Gating on `ended` alone would leave the 60s heartbeat polling a card
|
|
4162
|
+
// this very call declared terminal.
|
|
4163
|
+
if (ended || stoppedByHuman) {
|
|
4164
|
+
untrack(cardId, deps.getScopeId?.());
|
|
4165
|
+
}
|
|
4166
|
+
if (stoppedByHuman) {
|
|
4167
|
+
return {
|
|
4168
|
+
success: true,
|
|
4169
|
+
...result,
|
|
4170
|
+
stopped: true,
|
|
4171
|
+
stopReason: "human_stopped" as const,
|
|
4172
|
+
instruction:
|
|
4173
|
+
"A human stopped this run — stop work now. Make no further edits, commits, pushes, or progress writes, and do not move the card. Commit any uncommitted work in place so it isn't lost, then report to the user what you completed, what is unfinished, and the branch holding it.",
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
if (ended) {
|
|
4177
|
+
// Deliberately NOT `sessionEnded` — `harmony_move_card` already returns a
|
|
4178
|
+
// field by that name meaning "I ended your session for you", which is
|
|
4179
|
+
// close to the opposite of this. Two tools answering the same word with
|
|
4180
|
+
// near-inverse meanings is how a client ends up re-opening a session it
|
|
4181
|
+
// just intentionally closed.
|
|
4182
|
+
return {
|
|
4183
|
+
success: true,
|
|
4184
|
+
...result,
|
|
4185
|
+
sessionStale: true,
|
|
4186
|
+
staleReason: result.session?.status,
|
|
4187
|
+
instruction:
|
|
4188
|
+
'This session id is no longer live (it was closed as "' +
|
|
4189
|
+
(result.session?.status ?? "missing") +
|
|
4190
|
+
'") — but nobody stopped you. Do NOT abandon the work. Open a fresh session with harmony_start_agent_session, use its new id for later polls, and carry on.',
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4098
4193
|
return { success: true, ...result };
|
|
4099
4194
|
}
|
|
4100
4195
|
|
package/src/tui/setup.ts
CHANGED
|
@@ -515,14 +515,29 @@ async function getAgentFiles(
|
|
|
515
515
|
|
|
516
516
|
This project uses Harmony for task management. When working on tasks:
|
|
517
517
|
|
|
518
|
+
## Agent identity — always identify as yourself
|
|
519
|
+
|
|
520
|
+
Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
|
|
521
|
+
identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
|
|
522
|
+
more than one kind of agent will read it; the board shows agents as teammates, and a session
|
|
523
|
+
attributed to the wrong runtime misattributes the work in front of the whole team.
|
|
524
|
+
|
|
525
|
+
- \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
|
|
526
|
+
- \`agentName\` — its human-readable name
|
|
527
|
+
|
|
528
|
+
Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
|
|
529
|
+
\`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
|
|
530
|
+
than borrowing the closest entry.
|
|
531
|
+
|
|
518
532
|
## Starting Work on a Card
|
|
519
533
|
|
|
520
534
|
When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
521
535
|
|
|
522
|
-
1. Use \`
|
|
536
|
+
1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
|
|
523
537
|
2. Move the card to "In Progress" using \`harmony_move_card\`
|
|
524
538
|
3. Add the "agent" label using \`harmony_add_label_to_card\`
|
|
525
|
-
4. Start a session with \`harmony_start_agent_session
|
|
539
|
+
4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
|
|
540
|
+
\`agentName\` (see "Agent identity" above)
|
|
526
541
|
5. Show the card details to the user
|
|
527
542
|
6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
|
|
528
543
|
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
@@ -533,7 +548,8 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
|
533
548
|
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
534
549
|
|
|
535
550
|
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
536
|
-
2. If a match is found, call \`harmony_start_agent_session\`
|
|
551
|
+
2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
|
|
552
|
+
\`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
537
553
|
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
538
554
|
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
539
555
|
|