@gethmy/mcp 2.9.6 → 2.9.8
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 +4 -4
- package/dist/cli.js +116 -9
- package/dist/index.js +71 -1
- package/dist/lib/api-client.js +16 -1
- package/package.json +1 -1
- package/src/api-client.ts +66 -0
- package/src/server.ts +76 -0
- package/src/tui/setup.ts +106 -11
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
|
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
8
|
+
- **70 MCP Tools** for full board control, knowledge graph, and workflow plans
|
|
9
|
+
- **7 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, `/hmy-memory-prune`, `/hmy-upgrade`, `/hmy-review`, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
|
|
10
10
|
- **Knowledge Graph Memory** — Phase 1 surface: hybrid retrieval (vector + lexical + RRF), session-scoped working memory, activity feed. See [docs/memory.md](../../docs/memory.md)
|
|
11
11
|
- **GSD Workflow Plans** - plan/execute/verify/done lifecycle with auto card creation
|
|
12
12
|
- **Card Linking** - create relationships between cards (blocks, relates_to, duplicates, is_part_of)
|
|
@@ -89,7 +89,7 @@ If you prefer to configure manually (e.g., in Claude.ai's UI):
|
|
|
89
89
|
1. Get an API key from [Harmony](https://gethmy.com/user/keys)
|
|
90
90
|
2. In Claude.ai, add a remote MCP server with URL `https://mcp.gethmy.com/mcp`
|
|
91
91
|
3. Set the Authorization header to `Bearer hmy_your_key_here`
|
|
92
|
-
4. All
|
|
92
|
+
4. All 70 Harmony tools become available in your conversation
|
|
93
93
|
|
|
94
94
|
**Session management** is automatic - sessions have a 1-hour TTL and are created/renewed transparently.
|
|
95
95
|
|
|
@@ -150,7 +150,7 @@ npx @gethmy/mcp serve # Start MCP server
|
|
|
150
150
|
|
|
151
151
|
## Skills
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
Seven global skills ship with the MCP server and are installed automatically by `npx @gethmy/mcp setup`. They live in the `skill_resource` Postgres table, are fetched via `GET /v1/skills/<name>`, and render-time composed with a shared auto-update preamble.
|
|
154
154
|
|
|
155
155
|
For the full skill hub architecture (storage, versioning, auto-update, admin management), see [docs/skills.md](../../docs/skills.md).
|
|
156
156
|
|
package/dist/cli.js
CHANGED
|
@@ -1385,7 +1385,6 @@ import {
|
|
|
1385
1385
|
ReadResourceRequestSchema
|
|
1386
1386
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1387
1387
|
import { z } from "zod";
|
|
1388
|
-
|
|
1389
1388
|
// ../harmony-shared/dist/cardLinks.js
|
|
1390
1389
|
var LINK_TYPE_INVERSES = {
|
|
1391
1390
|
relates_to: "relates_to",
|
|
@@ -1815,6 +1814,16 @@ class HarmonyApiClient {
|
|
|
1815
1814
|
async getCardByShortId(projectId, shortId) {
|
|
1816
1815
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1817
1816
|
}
|
|
1817
|
+
async bulkGetCards(projectId, shortIds) {
|
|
1818
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1819
|
+
shortIds
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
async bulkArchiveCards(projectId, shortIds) {
|
|
1823
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
1824
|
+
shortIds
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1818
1827
|
async searchCards(query, options) {
|
|
1819
1828
|
const params = new URLSearchParams({ q: query });
|
|
1820
1829
|
if (options?.projectId) {
|
|
@@ -1849,6 +1858,9 @@ class HarmonyApiClient {
|
|
|
1849
1858
|
async getCardExternalLinks(cardId) {
|
|
1850
1859
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1851
1860
|
}
|
|
1861
|
+
async classifyCard(cardId) {
|
|
1862
|
+
return this.request("POST", `/cards/${cardId}/classify`);
|
|
1863
|
+
}
|
|
1852
1864
|
async createColumn(projectId, name) {
|
|
1853
1865
|
return this.request("POST", "/columns", { projectId, name });
|
|
1854
1866
|
}
|
|
@@ -1910,6 +1922,9 @@ class HarmonyApiClient {
|
|
|
1910
1922
|
async flushActivityLog(cardId, data) {
|
|
1911
1923
|
return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
|
|
1912
1924
|
}
|
|
1925
|
+
async appendAgentRunEvents(cardId, data) {
|
|
1926
|
+
return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
|
|
1927
|
+
}
|
|
1913
1928
|
async getActivityLog(cardId, sessionId) {
|
|
1914
1929
|
return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
|
|
1915
1930
|
}
|
|
@@ -3680,6 +3695,34 @@ var TOOLS = {
|
|
|
3680
3695
|
required: ["shortId"]
|
|
3681
3696
|
}
|
|
3682
3697
|
},
|
|
3698
|
+
harmony_bulk_get_cards: {
|
|
3699
|
+
description: "Fetch multiple cards by short id in one call. Returns compact summaries " + "(id, shortId, title, column, priority, assignee, labels, archived) plus " + "any short ids not found. Requires project context. Prefer this over " + "repeated harmony_get_card_by_short_id when referencing multiple cards.",
|
|
3700
|
+
inputSchema: {
|
|
3701
|
+
type: "object",
|
|
3702
|
+
properties: {
|
|
3703
|
+
shortIds: {
|
|
3704
|
+
type: "array",
|
|
3705
|
+
items: { type: "number" },
|
|
3706
|
+
description: "Card short ids, e.g. [400, 401, 402]. Max 100 per call."
|
|
3707
|
+
}
|
|
3708
|
+
},
|
|
3709
|
+
required: ["shortIds"]
|
|
3710
|
+
}
|
|
3711
|
+
},
|
|
3712
|
+
harmony_bulk_archive_cards: {
|
|
3713
|
+
description: "Archive (soft-delete) multiple cards by short id in one call. Best-effort: " + "returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " + "cards can be restored with unarchive. Requires project context.",
|
|
3714
|
+
inputSchema: {
|
|
3715
|
+
type: "object",
|
|
3716
|
+
properties: {
|
|
3717
|
+
shortIds: {
|
|
3718
|
+
type: "array",
|
|
3719
|
+
items: { type: "number" },
|
|
3720
|
+
description: "Card short ids to archive, e.g. [400, 401]. Max 100 per call."
|
|
3721
|
+
}
|
|
3722
|
+
},
|
|
3723
|
+
required: ["shortIds"]
|
|
3724
|
+
}
|
|
3725
|
+
},
|
|
3683
3726
|
harmony_create_column: {
|
|
3684
3727
|
description: "Create a new column in a project",
|
|
3685
3728
|
inputSchema: {
|
|
@@ -3844,6 +3887,16 @@ var TOOLS = {
|
|
|
3844
3887
|
required: ["cardId"]
|
|
3845
3888
|
}
|
|
3846
3889
|
},
|
|
3890
|
+
harmony_classify_card: {
|
|
3891
|
+
description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
|
|
3892
|
+
inputSchema: {
|
|
3893
|
+
type: "object",
|
|
3894
|
+
properties: {
|
|
3895
|
+
cardId: { type: "string", description: "Card UUID" }
|
|
3896
|
+
},
|
|
3897
|
+
required: ["cardId"]
|
|
3898
|
+
}
|
|
3899
|
+
},
|
|
3847
3900
|
harmony_get_card_external_links: {
|
|
3848
3901
|
description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
|
|
3849
3902
|
inputSchema: {
|
|
@@ -5169,6 +5222,18 @@ async function handleToolCall(name, args, deps) {
|
|
|
5169
5222
|
const result = await client3.getCardByShortId(projectId, shortId);
|
|
5170
5223
|
return { success: true, ...result };
|
|
5171
5224
|
}
|
|
5225
|
+
case "harmony_bulk_get_cards": {
|
|
5226
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5227
|
+
const projectId = getProjectId();
|
|
5228
|
+
const result = await client3.bulkGetCards(projectId, shortIds);
|
|
5229
|
+
return { success: true, ...result };
|
|
5230
|
+
}
|
|
5231
|
+
case "harmony_bulk_archive_cards": {
|
|
5232
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5233
|
+
const projectId = getProjectId();
|
|
5234
|
+
const result = await client3.bulkArchiveCards(projectId, shortIds);
|
|
5235
|
+
return { success: true, ...result };
|
|
5236
|
+
}
|
|
5172
5237
|
case "harmony_create_column": {
|
|
5173
5238
|
const name2 = z.string().min(1).max(100).parse(args.name);
|
|
5174
5239
|
const projectId = args.projectId || getProjectId();
|
|
@@ -5299,6 +5364,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
5299
5364
|
const result = await client3.getCardExternalLinks(cardId);
|
|
5300
5365
|
return result;
|
|
5301
5366
|
}
|
|
5367
|
+
case "harmony_classify_card": {
|
|
5368
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
5369
|
+
const result = await client3.classifyCard(cardId);
|
|
5370
|
+
return result;
|
|
5371
|
+
}
|
|
5302
5372
|
case "harmony_create_subtask": {
|
|
5303
5373
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5304
5374
|
const title = z.string().min(1).max(500).parse(args.title);
|
|
@@ -7473,12 +7543,23 @@ async function resolveProjectSlug(apiKey, slug) {
|
|
|
7473
7543
|
}
|
|
7474
7544
|
});
|
|
7475
7545
|
if (response.status === 404)
|
|
7476
|
-
return
|
|
7546
|
+
return { status: "not_found" };
|
|
7547
|
+
if (response.status === 409) {
|
|
7548
|
+
const data2 = await response.json().catch(() => ({}));
|
|
7549
|
+
return {
|
|
7550
|
+
status: "ambiguous",
|
|
7551
|
+
candidates: Array.isArray(data2.candidates) ? data2.candidates : []
|
|
7552
|
+
};
|
|
7553
|
+
}
|
|
7477
7554
|
if (!response.ok) {
|
|
7478
7555
|
throw new Error(`Failed to resolve project slug: ${response.status}`);
|
|
7479
7556
|
}
|
|
7480
7557
|
const data = await response.json();
|
|
7481
|
-
return {
|
|
7558
|
+
return {
|
|
7559
|
+
status: "found",
|
|
7560
|
+
workspaceId: data.workspaceId,
|
|
7561
|
+
projectId: data.projectId
|
|
7562
|
+
};
|
|
7482
7563
|
}
|
|
7483
7564
|
async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
7484
7565
|
const home = homedir6();
|
|
@@ -7760,12 +7841,13 @@ only for unattended CI where you accept that risk.`));
|
|
|
7760
7841
|
options: [
|
|
7761
7842
|
{
|
|
7762
7843
|
value: "browser",
|
|
7763
|
-
label: "Sign in
|
|
7764
|
-
hint: "recommended —
|
|
7844
|
+
label: "Sign in or create an account in your browser",
|
|
7845
|
+
hint: "recommended — new or existing users, no key handling"
|
|
7765
7846
|
},
|
|
7766
7847
|
{
|
|
7767
7848
|
value: "create",
|
|
7768
|
-
label: "Create
|
|
7849
|
+
label: "Create an account here in the terminal",
|
|
7850
|
+
hint: "no browser"
|
|
7769
7851
|
},
|
|
7770
7852
|
{
|
|
7771
7853
|
value: "apikey",
|
|
@@ -8002,14 +8084,23 @@ ${colors.dim(url)}`);
|
|
|
8002
8084
|
let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
|
|
8003
8085
|
let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
|
|
8004
8086
|
let selectedProjectName = selectedProjectNameFromSignup;
|
|
8087
|
+
let ambiguousCandidates;
|
|
8005
8088
|
if (options.projectSlug && apiKey && (!selectedWorkspaceId || !selectedProjectId)) {
|
|
8006
8089
|
spinner3.start(`Resolving project slug "${options.projectSlug}"...`);
|
|
8007
8090
|
try {
|
|
8008
8091
|
const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
|
|
8009
|
-
if (resolved) {
|
|
8092
|
+
if (resolved.status === "found") {
|
|
8010
8093
|
selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
|
|
8011
8094
|
selectedProjectId = selectedProjectId || resolved.projectId;
|
|
8012
8095
|
spinner3.stop(colors.success(`Resolved "${options.projectSlug}"`));
|
|
8096
|
+
} else if (resolved.status === "ambiguous") {
|
|
8097
|
+
ambiguousCandidates = resolved.candidates;
|
|
8098
|
+
spinner3.stop(colors.warning(`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`));
|
|
8099
|
+
const list = resolved.candidates.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`).join(`
|
|
8100
|
+
`);
|
|
8101
|
+
p3.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
|
|
8102
|
+
${list}
|
|
8103
|
+
Specify the workspace with --workspace <id>, or select one below.`);
|
|
8013
8104
|
} else {
|
|
8014
8105
|
spinner3.stop(colors.warning(`No project found for slug "${options.projectSlug}"`));
|
|
8015
8106
|
}
|
|
@@ -8033,12 +8124,15 @@ ${colors.dim(url)}`);
|
|
|
8033
8124
|
}
|
|
8034
8125
|
if (needsContext && workspaces.length > 0) {
|
|
8035
8126
|
if (!selectedWorkspaceId) {
|
|
8036
|
-
const
|
|
8127
|
+
const candidateIds = new Set((ambiguousCandidates ?? []).map((c) => c.workspaceId));
|
|
8128
|
+
const pickable = candidateIds.size > 0 ? workspaces.filter((ws) => candidateIds.has(ws.id)) : workspaces;
|
|
8129
|
+
const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
|
|
8130
|
+
const workspaceOptions = workspaceChoices.map((ws) => ({
|
|
8037
8131
|
value: ws.id,
|
|
8038
8132
|
label: ws.name
|
|
8039
8133
|
}));
|
|
8040
8134
|
const workspaceSelection = await p3.select({
|
|
8041
|
-
message: "Select workspace",
|
|
8135
|
+
message: candidateIds.size > 0 ? `Select workspace for "${options.projectSlug}"` : "Select workspace",
|
|
8042
8136
|
options: workspaceOptions
|
|
8043
8137
|
});
|
|
8044
8138
|
if (p3.isCancel(workspaceSelection)) {
|
|
@@ -8048,6 +8142,12 @@ ${colors.dim(url)}`);
|
|
|
8048
8142
|
selectedWorkspaceId = workspaceSelection;
|
|
8049
8143
|
}
|
|
8050
8144
|
selectedWorkspaceName = workspaces.find((w) => w.id === selectedWorkspaceId)?.name;
|
|
8145
|
+
if (!selectedProjectId) {
|
|
8146
|
+
const chosenCandidate = ambiguousCandidates?.find((c) => c.workspaceId === selectedWorkspaceId);
|
|
8147
|
+
if (chosenCandidate) {
|
|
8148
|
+
selectedProjectId = chosenCandidate.projectId;
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8051
8151
|
spinner3.start("Fetching projects...");
|
|
8052
8152
|
let projects = [];
|
|
8053
8153
|
try {
|
|
@@ -8073,6 +8173,8 @@ ${colors.dim(url)}`);
|
|
|
8073
8173
|
}
|
|
8074
8174
|
selectedProjectId = projectSelection;
|
|
8075
8175
|
selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
|
|
8176
|
+
} else if (selectedProjectId && !selectedProjectName) {
|
|
8177
|
+
selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
|
|
8076
8178
|
}
|
|
8077
8179
|
}
|
|
8078
8180
|
}
|
|
@@ -8297,6 +8399,11 @@ ${colors.dim(url)}`);
|
|
|
8297
8399
|
console.log(` ${colors.brand("Cursor:")} MCP tools available automatically`);
|
|
8298
8400
|
}
|
|
8299
8401
|
console.log("");
|
|
8402
|
+
console.log(` ${colors.bold("Next steps:")}`);
|
|
8403
|
+
console.log(` 1. Open Claude Code and say: ${colors.highlight('"Show me my board"')}`);
|
|
8404
|
+
console.log(` 2. Create a card: ${colors.highlight('"Create a card called Auth token refresh"')}`);
|
|
8405
|
+
console.log(` 3. Start the daemon: ${colors.highlight("npx @gethmy/agent")}`);
|
|
8406
|
+
console.log("");
|
|
8300
8407
|
console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
|
|
8301
8408
|
console.log(` ${colors.dim("Need help? Visit https://app.gethmy.com/docs/mcp")}`);
|
|
8302
8409
|
}
|
package/dist/index.js
CHANGED
|
@@ -1380,7 +1380,6 @@ import {
|
|
|
1380
1380
|
ReadResourceRequestSchema
|
|
1381
1381
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1382
1382
|
import { z } from "zod";
|
|
1383
|
-
|
|
1384
1383
|
// ../harmony-shared/dist/cardLinks.js
|
|
1385
1384
|
var LINK_TYPE_INVERSES = {
|
|
1386
1385
|
relates_to: "relates_to",
|
|
@@ -1810,6 +1809,16 @@ class HarmonyApiClient {
|
|
|
1810
1809
|
async getCardByShortId(projectId, shortId) {
|
|
1811
1810
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1812
1811
|
}
|
|
1812
|
+
async bulkGetCards(projectId, shortIds) {
|
|
1813
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1814
|
+
shortIds
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
async bulkArchiveCards(projectId, shortIds) {
|
|
1818
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
1819
|
+
shortIds
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1813
1822
|
async searchCards(query, options) {
|
|
1814
1823
|
const params = new URLSearchParams({ q: query });
|
|
1815
1824
|
if (options?.projectId) {
|
|
@@ -1844,6 +1853,9 @@ class HarmonyApiClient {
|
|
|
1844
1853
|
async getCardExternalLinks(cardId) {
|
|
1845
1854
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1846
1855
|
}
|
|
1856
|
+
async classifyCard(cardId) {
|
|
1857
|
+
return this.request("POST", `/cards/${cardId}/classify`);
|
|
1858
|
+
}
|
|
1847
1859
|
async createColumn(projectId, name) {
|
|
1848
1860
|
return this.request("POST", "/columns", { projectId, name });
|
|
1849
1861
|
}
|
|
@@ -1905,6 +1917,9 @@ class HarmonyApiClient {
|
|
|
1905
1917
|
async flushActivityLog(cardId, data) {
|
|
1906
1918
|
return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
|
|
1907
1919
|
}
|
|
1920
|
+
async appendAgentRunEvents(cardId, data) {
|
|
1921
|
+
return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
|
|
1922
|
+
}
|
|
1908
1923
|
async getActivityLog(cardId, sessionId) {
|
|
1909
1924
|
return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
|
|
1910
1925
|
}
|
|
@@ -3675,6 +3690,34 @@ var TOOLS = {
|
|
|
3675
3690
|
required: ["shortId"]
|
|
3676
3691
|
}
|
|
3677
3692
|
},
|
|
3693
|
+
harmony_bulk_get_cards: {
|
|
3694
|
+
description: "Fetch multiple cards by short id in one call. Returns compact summaries " + "(id, shortId, title, column, priority, assignee, labels, archived) plus " + "any short ids not found. Requires project context. Prefer this over " + "repeated harmony_get_card_by_short_id when referencing multiple cards.",
|
|
3695
|
+
inputSchema: {
|
|
3696
|
+
type: "object",
|
|
3697
|
+
properties: {
|
|
3698
|
+
shortIds: {
|
|
3699
|
+
type: "array",
|
|
3700
|
+
items: { type: "number" },
|
|
3701
|
+
description: "Card short ids, e.g. [400, 401, 402]. Max 100 per call."
|
|
3702
|
+
}
|
|
3703
|
+
},
|
|
3704
|
+
required: ["shortIds"]
|
|
3705
|
+
}
|
|
3706
|
+
},
|
|
3707
|
+
harmony_bulk_archive_cards: {
|
|
3708
|
+
description: "Archive (soft-delete) multiple cards by short id in one call. Best-effort: " + "returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " + "cards can be restored with unarchive. Requires project context.",
|
|
3709
|
+
inputSchema: {
|
|
3710
|
+
type: "object",
|
|
3711
|
+
properties: {
|
|
3712
|
+
shortIds: {
|
|
3713
|
+
type: "array",
|
|
3714
|
+
items: { type: "number" },
|
|
3715
|
+
description: "Card short ids to archive, e.g. [400, 401]. Max 100 per call."
|
|
3716
|
+
}
|
|
3717
|
+
},
|
|
3718
|
+
required: ["shortIds"]
|
|
3719
|
+
}
|
|
3720
|
+
},
|
|
3678
3721
|
harmony_create_column: {
|
|
3679
3722
|
description: "Create a new column in a project",
|
|
3680
3723
|
inputSchema: {
|
|
@@ -3839,6 +3882,16 @@ var TOOLS = {
|
|
|
3839
3882
|
required: ["cardId"]
|
|
3840
3883
|
}
|
|
3841
3884
|
},
|
|
3885
|
+
harmony_classify_card: {
|
|
3886
|
+
description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
|
|
3887
|
+
inputSchema: {
|
|
3888
|
+
type: "object",
|
|
3889
|
+
properties: {
|
|
3890
|
+
cardId: { type: "string", description: "Card UUID" }
|
|
3891
|
+
},
|
|
3892
|
+
required: ["cardId"]
|
|
3893
|
+
}
|
|
3894
|
+
},
|
|
3842
3895
|
harmony_get_card_external_links: {
|
|
3843
3896
|
description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
|
|
3844
3897
|
inputSchema: {
|
|
@@ -5164,6 +5217,18 @@ async function handleToolCall(name, args, deps) {
|
|
|
5164
5217
|
const result = await client3.getCardByShortId(projectId, shortId);
|
|
5165
5218
|
return { success: true, ...result };
|
|
5166
5219
|
}
|
|
5220
|
+
case "harmony_bulk_get_cards": {
|
|
5221
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5222
|
+
const projectId = getProjectId();
|
|
5223
|
+
const result = await client3.bulkGetCards(projectId, shortIds);
|
|
5224
|
+
return { success: true, ...result };
|
|
5225
|
+
}
|
|
5226
|
+
case "harmony_bulk_archive_cards": {
|
|
5227
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5228
|
+
const projectId = getProjectId();
|
|
5229
|
+
const result = await client3.bulkArchiveCards(projectId, shortIds);
|
|
5230
|
+
return { success: true, ...result };
|
|
5231
|
+
}
|
|
5167
5232
|
case "harmony_create_column": {
|
|
5168
5233
|
const name2 = z.string().min(1).max(100).parse(args.name);
|
|
5169
5234
|
const projectId = args.projectId || getProjectId();
|
|
@@ -5294,6 +5359,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
5294
5359
|
const result = await client3.getCardExternalLinks(cardId);
|
|
5295
5360
|
return result;
|
|
5296
5361
|
}
|
|
5362
|
+
case "harmony_classify_card": {
|
|
5363
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
5364
|
+
const result = await client3.classifyCard(cardId);
|
|
5365
|
+
return result;
|
|
5366
|
+
}
|
|
5297
5367
|
case "harmony_create_subtask": {
|
|
5298
5368
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5299
5369
|
const title = z.string().min(1).max(500).parse(args.title);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -833,7 +833,6 @@ var init_oauth_refresh = __esm(() => {
|
|
|
833
833
|
init_config();
|
|
834
834
|
init_oauth_login();
|
|
835
835
|
});
|
|
836
|
-
|
|
837
836
|
// ../harmony-shared/dist/cardLinks.js
|
|
838
837
|
var LINK_TYPE_INVERSES = {
|
|
839
838
|
relates_to: "relates_to",
|
|
@@ -1263,6 +1262,16 @@ class HarmonyApiClient {
|
|
|
1263
1262
|
async getCardByShortId(projectId, shortId) {
|
|
1264
1263
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1265
1264
|
}
|
|
1265
|
+
async bulkGetCards(projectId, shortIds) {
|
|
1266
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1267
|
+
shortIds
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
async bulkArchiveCards(projectId, shortIds) {
|
|
1271
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
1272
|
+
shortIds
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1266
1275
|
async searchCards(query, options) {
|
|
1267
1276
|
const params = new URLSearchParams({ q: query });
|
|
1268
1277
|
if (options?.projectId) {
|
|
@@ -1297,6 +1306,9 @@ class HarmonyApiClient {
|
|
|
1297
1306
|
async getCardExternalLinks(cardId) {
|
|
1298
1307
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1299
1308
|
}
|
|
1309
|
+
async classifyCard(cardId) {
|
|
1310
|
+
return this.request("POST", `/cards/${cardId}/classify`);
|
|
1311
|
+
}
|
|
1300
1312
|
async createColumn(projectId, name) {
|
|
1301
1313
|
return this.request("POST", "/columns", { projectId, name });
|
|
1302
1314
|
}
|
|
@@ -1358,6 +1370,9 @@ class HarmonyApiClient {
|
|
|
1358
1370
|
async flushActivityLog(cardId, data) {
|
|
1359
1371
|
return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
|
|
1360
1372
|
}
|
|
1373
|
+
async appendAgentRunEvents(cardId, data) {
|
|
1374
|
+
return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
|
|
1375
|
+
}
|
|
1361
1376
|
async getActivityLog(cardId, sessionId) {
|
|
1362
1377
|
return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
|
|
1363
1378
|
}
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AgentRunEventDraft,
|
|
2
3
|
type Comment,
|
|
3
4
|
getDisplayLinkType,
|
|
4
5
|
serializeCommentThread,
|
|
@@ -151,6 +152,18 @@ export interface CardExternalLinkRow {
|
|
|
151
152
|
created_at: string;
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
/** Result of the classify-card classifier (card #415). Any field may be null
|
|
156
|
+
* if the LLM didn't return a usable value. `model_override` is never touched. */
|
|
157
|
+
export interface CardClassificationResult {
|
|
158
|
+
type: "feature" | "bug" | "idea" | null;
|
|
159
|
+
intent: "plan" | "think" | "implement" | "review" | null;
|
|
160
|
+
complexity_score: number | null;
|
|
161
|
+
model_tier: "simple" | "advanced" | "research" | null;
|
|
162
|
+
classified_at: string;
|
|
163
|
+
applied_type_label_id: string | null;
|
|
164
|
+
reasoning: string | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
154
167
|
export class HarmonyApiClient {
|
|
155
168
|
private apiKey: string;
|
|
156
169
|
private apiUrl: string;
|
|
@@ -570,6 +583,39 @@ export class HarmonyApiClient {
|
|
|
570
583
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
571
584
|
}
|
|
572
585
|
|
|
586
|
+
async bulkGetCards(
|
|
587
|
+
projectId: string,
|
|
588
|
+
shortIds: number[],
|
|
589
|
+
): Promise<{
|
|
590
|
+
cards: Array<{
|
|
591
|
+
id: string;
|
|
592
|
+
shortId: number;
|
|
593
|
+
title: string;
|
|
594
|
+
column: string | null;
|
|
595
|
+
priority: string | null;
|
|
596
|
+
assignee: string | null;
|
|
597
|
+
labels: string[];
|
|
598
|
+
archived: boolean;
|
|
599
|
+
}>;
|
|
600
|
+
notFound: number[];
|
|
601
|
+
}> {
|
|
602
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
603
|
+
shortIds,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async bulkArchiveCards(
|
|
608
|
+
projectId: string,
|
|
609
|
+
shortIds: number[],
|
|
610
|
+
): Promise<{
|
|
611
|
+
succeeded: number[];
|
|
612
|
+
failed: Array<{ shortId: number; reason: string }>;
|
|
613
|
+
}> {
|
|
614
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
615
|
+
shortIds,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
573
619
|
async searchCards(
|
|
574
620
|
query: string,
|
|
575
621
|
options?: { projectId?: string },
|
|
@@ -635,6 +681,12 @@ export class HarmonyApiClient {
|
|
|
635
681
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
636
682
|
}
|
|
637
683
|
|
|
684
|
+
async classifyCard(
|
|
685
|
+
cardId: string,
|
|
686
|
+
): Promise<{ classification: CardClassificationResult }> {
|
|
687
|
+
return this.request("POST", `/cards/${cardId}/classify`);
|
|
688
|
+
}
|
|
689
|
+
|
|
638
690
|
// ============ COLUMN OPERATIONS ============
|
|
639
691
|
|
|
640
692
|
async createColumn(
|
|
@@ -847,6 +899,20 @@ export class HarmonyApiClient {
|
|
|
847
899
|
return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
|
|
848
900
|
}
|
|
849
901
|
|
|
902
|
+
/**
|
|
903
|
+
* Append events to a run's agent_run_events stream (card #417). Send drafts in
|
|
904
|
+
* chronological order — the server's seq trigger assigns the monotonic per-run order.
|
|
905
|
+
*/
|
|
906
|
+
async appendAgentRunEvents(
|
|
907
|
+
cardId: string,
|
|
908
|
+
data: {
|
|
909
|
+
sessionId: string;
|
|
910
|
+
events: (AgentRunEventDraft & { createdAt?: string })[];
|
|
911
|
+
},
|
|
912
|
+
): Promise<{ inserted: number }> {
|
|
913
|
+
return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
|
|
914
|
+
}
|
|
915
|
+
|
|
850
916
|
async getActivityLog(
|
|
851
917
|
cardId: string,
|
|
852
918
|
sessionId: string,
|
package/src/server.ts
CHANGED
|
@@ -469,6 +469,43 @@ export const TOOLS = {
|
|
|
469
469
|
required: ["shortId"],
|
|
470
470
|
},
|
|
471
471
|
},
|
|
472
|
+
harmony_bulk_get_cards: {
|
|
473
|
+
description:
|
|
474
|
+
"Fetch multiple cards by short id in one call. Returns compact summaries " +
|
|
475
|
+
"(id, shortId, title, column, priority, assignee, labels, archived) plus " +
|
|
476
|
+
"any short ids not found. Requires project context. Prefer this over " +
|
|
477
|
+
"repeated harmony_get_card_by_short_id when referencing multiple cards.",
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: "object",
|
|
480
|
+
properties: {
|
|
481
|
+
shortIds: {
|
|
482
|
+
type: "array",
|
|
483
|
+
items: { type: "number" },
|
|
484
|
+
description:
|
|
485
|
+
"Card short ids, e.g. [400, 401, 402]. Max 100 per call.",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
required: ["shortIds"],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
harmony_bulk_archive_cards: {
|
|
492
|
+
description:
|
|
493
|
+
"Archive (soft-delete) multiple cards by short id in one call. Best-effort: " +
|
|
494
|
+
"returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " +
|
|
495
|
+
"cards can be restored with unarchive. Requires project context.",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {
|
|
499
|
+
shortIds: {
|
|
500
|
+
type: "array",
|
|
501
|
+
items: { type: "number" },
|
|
502
|
+
description:
|
|
503
|
+
"Card short ids to archive, e.g. [400, 401]. Max 100 per call.",
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
required: ["shortIds"],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
472
509
|
|
|
473
510
|
// Column operations
|
|
474
511
|
harmony_create_column: {
|
|
@@ -649,6 +686,17 @@ export const TOOLS = {
|
|
|
649
686
|
required: ["cardId"],
|
|
650
687
|
},
|
|
651
688
|
},
|
|
689
|
+
harmony_classify_card: {
|
|
690
|
+
description:
|
|
691
|
+
"Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
|
|
692
|
+
inputSchema: {
|
|
693
|
+
type: "object",
|
|
694
|
+
properties: {
|
|
695
|
+
cardId: { type: "string", description: "Card UUID" },
|
|
696
|
+
},
|
|
697
|
+
required: ["cardId"],
|
|
698
|
+
},
|
|
699
|
+
},
|
|
652
700
|
harmony_get_card_external_links: {
|
|
653
701
|
description:
|
|
654
702
|
"Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
|
|
@@ -2241,6 +2289,28 @@ async function handleToolCall(
|
|
|
2241
2289
|
return { success: true, ...result };
|
|
2242
2290
|
}
|
|
2243
2291
|
|
|
2292
|
+
case "harmony_bulk_get_cards": {
|
|
2293
|
+
const shortIds = z
|
|
2294
|
+
.array(z.number().int().positive())
|
|
2295
|
+
.min(1)
|
|
2296
|
+
.max(100)
|
|
2297
|
+
.parse(args.shortIds);
|
|
2298
|
+
const projectId = getProjectId();
|
|
2299
|
+
const result = await client.bulkGetCards(projectId, shortIds);
|
|
2300
|
+
return { success: true, ...result };
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
case "harmony_bulk_archive_cards": {
|
|
2304
|
+
const shortIds = z
|
|
2305
|
+
.array(z.number().int().positive())
|
|
2306
|
+
.min(1)
|
|
2307
|
+
.max(100)
|
|
2308
|
+
.parse(args.shortIds);
|
|
2309
|
+
const projectId = getProjectId();
|
|
2310
|
+
const result = await client.bulkArchiveCards(projectId, shortIds);
|
|
2311
|
+
return { success: true, ...result };
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2244
2314
|
// Column operations
|
|
2245
2315
|
case "harmony_create_column": {
|
|
2246
2316
|
const name = z.string().min(1).max(100).parse(args.name);
|
|
@@ -2425,6 +2495,12 @@ async function handleToolCall(
|
|
|
2425
2495
|
return result;
|
|
2426
2496
|
}
|
|
2427
2497
|
|
|
2498
|
+
case "harmony_classify_card": {
|
|
2499
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
2500
|
+
const result = await client.classifyCard(cardId);
|
|
2501
|
+
return result;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2428
2504
|
// Subtask operations
|
|
2429
2505
|
case "harmony_create_subtask": {
|
|
2430
2506
|
const cardId = z.string().uuid().parse(args.cardId);
|
package/src/tui/setup.ts
CHANGED
|
@@ -331,14 +331,31 @@ async function fetchProjects(
|
|
|
331
331
|
return data.projects || [];
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
export interface SlugCandidate {
|
|
335
|
+
workspaceId: string;
|
|
336
|
+
workspaceName: string | null;
|
|
337
|
+
projectId: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Outcome of resolving a project slug. `ambiguous` means the same slug exists
|
|
342
|
+
* in more than one of the caller's workspaces (HTTP 409) — common for bare
|
|
343
|
+
* web-onboarded slugs like "my-first-board" when a user belongs to several
|
|
344
|
+
* teams. The caller must disambiguate rather than treat it as not-found.
|
|
345
|
+
*/
|
|
346
|
+
export type ResolveSlugResult =
|
|
347
|
+
| { status: "found"; workspaceId: string; projectId: string }
|
|
348
|
+
| { status: "not_found" }
|
|
349
|
+
| { status: "ambiguous"; candidates: SlugCandidate[] };
|
|
350
|
+
|
|
334
351
|
/**
|
|
335
352
|
* Resolve a project slug to {workspaceId, projectId}. Used by
|
|
336
353
|
* `npx @gethmy/mcp setup <slug>` so users don't have to copy raw UUIDs.
|
|
337
354
|
*/
|
|
338
|
-
async function resolveProjectSlug(
|
|
355
|
+
export async function resolveProjectSlug(
|
|
339
356
|
apiKey: string,
|
|
340
357
|
slug: string,
|
|
341
|
-
): Promise<
|
|
358
|
+
): Promise<ResolveSlugResult> {
|
|
342
359
|
const response = await fetch(
|
|
343
360
|
`${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`,
|
|
344
361
|
{
|
|
@@ -350,13 +367,26 @@ async function resolveProjectSlug(
|
|
|
350
367
|
},
|
|
351
368
|
);
|
|
352
369
|
|
|
353
|
-
if (response.status === 404) return
|
|
370
|
+
if (response.status === 404) return { status: "not_found" };
|
|
371
|
+
// 409 = slug matches projects in multiple workspaces. The body carries the
|
|
372
|
+
// candidate workspaces so we can tell the user which ones to pick from.
|
|
373
|
+
if (response.status === 409) {
|
|
374
|
+
const data = await response.json().catch(() => ({}));
|
|
375
|
+
return {
|
|
376
|
+
status: "ambiguous",
|
|
377
|
+
candidates: Array.isArray(data.candidates) ? data.candidates : [],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
354
380
|
if (!response.ok) {
|
|
355
381
|
throw new Error(`Failed to resolve project slug: ${response.status}`);
|
|
356
382
|
}
|
|
357
383
|
|
|
358
384
|
const data = await response.json();
|
|
359
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
status: "found",
|
|
387
|
+
workspaceId: data.workspaceId,
|
|
388
|
+
projectId: data.projectId,
|
|
389
|
+
};
|
|
360
390
|
}
|
|
361
391
|
|
|
362
392
|
export interface FileToWrite {
|
|
@@ -753,12 +783,13 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
753
783
|
options: [
|
|
754
784
|
{
|
|
755
785
|
value: "browser",
|
|
756
|
-
label: "Sign in
|
|
757
|
-
hint: "recommended —
|
|
786
|
+
label: "Sign in or create an account in your browser",
|
|
787
|
+
hint: "recommended — new or existing users, no key handling",
|
|
758
788
|
},
|
|
759
789
|
{
|
|
760
790
|
value: "create",
|
|
761
|
-
label: "Create
|
|
791
|
+
label: "Create an account here in the terminal",
|
|
792
|
+
hint: "no browser",
|
|
762
793
|
},
|
|
763
794
|
{
|
|
764
795
|
value: "apikey",
|
|
@@ -1056,6 +1087,10 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1056
1087
|
let selectedWorkspaceName: string | undefined =
|
|
1057
1088
|
selectedWorkspaceNameFromSignup;
|
|
1058
1089
|
let selectedProjectName: string | undefined = selectedProjectNameFromSignup;
|
|
1090
|
+
// When a slug is ambiguous, the candidate workspaces (each already carrying
|
|
1091
|
+
// the matching projectId) narrow the interactive picker below to just those
|
|
1092
|
+
// workspaces instead of every workspace the user belongs to.
|
|
1093
|
+
let ambiguousCandidates: SlugCandidate[] | undefined;
|
|
1059
1094
|
|
|
1060
1095
|
// Resolve project slug shorthand (e.g. `npx @gethmy/mcp setup harmony-6590761b`).
|
|
1061
1096
|
// Slug wins over --workspace/--project flags only when those aren't already set
|
|
@@ -1068,10 +1103,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1068
1103
|
spinner.start(`Resolving project slug "${options.projectSlug}"...`);
|
|
1069
1104
|
try {
|
|
1070
1105
|
const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
|
|
1071
|
-
if (resolved) {
|
|
1106
|
+
if (resolved.status === "found") {
|
|
1072
1107
|
selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
|
|
1073
1108
|
selectedProjectId = selectedProjectId || resolved.projectId;
|
|
1074
1109
|
spinner.stop(colors.success(`Resolved "${options.projectSlug}"`));
|
|
1110
|
+
} else if (resolved.status === "ambiguous") {
|
|
1111
|
+
// Same slug in multiple workspaces — don't silently pick one. Surface
|
|
1112
|
+
// the candidates so the user knows to specify a workspace; interactive
|
|
1113
|
+
// context selection below still lets them recover in a TTY, narrowed to
|
|
1114
|
+
// just these workspaces.
|
|
1115
|
+
ambiguousCandidates = resolved.candidates;
|
|
1116
|
+
spinner.stop(
|
|
1117
|
+
colors.warning(
|
|
1118
|
+
`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`,
|
|
1119
|
+
),
|
|
1120
|
+
);
|
|
1121
|
+
const list = resolved.candidates
|
|
1122
|
+
.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`)
|
|
1123
|
+
.join("\n");
|
|
1124
|
+
p.log.warning(
|
|
1125
|
+
`"${options.projectSlug}" matches projects in multiple workspaces:\n${list}\n` +
|
|
1126
|
+
"Specify the workspace with --workspace <id>, or select one below.",
|
|
1127
|
+
);
|
|
1075
1128
|
} else {
|
|
1076
1129
|
spinner.stop(
|
|
1077
1130
|
colors.warning(`No project found for slug "${options.projectSlug}"`),
|
|
@@ -1108,15 +1161,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1108
1161
|
}
|
|
1109
1162
|
|
|
1110
1163
|
if (needsContext && workspaces.length > 0) {
|
|
1111
|
-
// Select workspace
|
|
1164
|
+
// Select workspace. When the slug was ambiguous, restrict the choices to
|
|
1165
|
+
// the candidate workspaces so the user picks among the ones that actually
|
|
1166
|
+
// contain the slug (falling back to all workspaces if none still match).
|
|
1112
1167
|
if (!selectedWorkspaceId) {
|
|
1113
|
-
const
|
|
1168
|
+
const candidateIds = new Set(
|
|
1169
|
+
(ambiguousCandidates ?? []).map((c) => c.workspaceId),
|
|
1170
|
+
);
|
|
1171
|
+
const pickable =
|
|
1172
|
+
candidateIds.size > 0
|
|
1173
|
+
? workspaces.filter((ws) => candidateIds.has(ws.id))
|
|
1174
|
+
: workspaces;
|
|
1175
|
+
const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
|
|
1176
|
+
const workspaceOptions = workspaceChoices.map((ws) => ({
|
|
1114
1177
|
value: ws.id,
|
|
1115
1178
|
label: ws.name,
|
|
1116
1179
|
}));
|
|
1117
1180
|
|
|
1118
1181
|
const workspaceSelection = await p.select({
|
|
1119
|
-
message:
|
|
1182
|
+
message:
|
|
1183
|
+
candidateIds.size > 0
|
|
1184
|
+
? `Select workspace for "${options.projectSlug}"`
|
|
1185
|
+
: "Select workspace",
|
|
1120
1186
|
options: workspaceOptions,
|
|
1121
1187
|
});
|
|
1122
1188
|
|
|
@@ -1132,6 +1198,17 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1132
1198
|
(w) => w.id === selectedWorkspaceId,
|
|
1133
1199
|
)?.name;
|
|
1134
1200
|
|
|
1201
|
+
// For an ambiguous slug, the chosen workspace already pins the project —
|
|
1202
|
+
// each candidate carries its projectId — so skip the project picker.
|
|
1203
|
+
if (!selectedProjectId) {
|
|
1204
|
+
const chosenCandidate = ambiguousCandidates?.find(
|
|
1205
|
+
(c) => c.workspaceId === selectedWorkspaceId,
|
|
1206
|
+
);
|
|
1207
|
+
if (chosenCandidate) {
|
|
1208
|
+
selectedProjectId = chosenCandidate.projectId;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1135
1212
|
// Fetch and select project
|
|
1136
1213
|
spinner.start("Fetching projects...");
|
|
1137
1214
|
let projects: Project[] = [];
|
|
@@ -1167,6 +1244,12 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1167
1244
|
selectedProjectName = projects.find(
|
|
1168
1245
|
(p) => p.id === selectedProjectId,
|
|
1169
1246
|
)?.name;
|
|
1247
|
+
} else if (selectedProjectId && !selectedProjectName) {
|
|
1248
|
+
// Project was pre-resolved (e.g. from an ambiguous-slug candidate) so
|
|
1249
|
+
// the picker was skipped — backfill the name for the summary display.
|
|
1250
|
+
selectedProjectName = projects.find(
|
|
1251
|
+
(p) => p.id === selectedProjectId,
|
|
1252
|
+
)?.name;
|
|
1170
1253
|
}
|
|
1171
1254
|
}
|
|
1172
1255
|
}
|
|
@@ -1517,6 +1600,18 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1517
1600
|
);
|
|
1518
1601
|
}
|
|
1519
1602
|
|
|
1603
|
+
console.log("");
|
|
1604
|
+
console.log(` ${colors.bold("Next steps:")}`);
|
|
1605
|
+
console.log(
|
|
1606
|
+
` 1. Open Claude Code and say: ${colors.highlight('"Show me my board"')}`,
|
|
1607
|
+
);
|
|
1608
|
+
console.log(
|
|
1609
|
+
` 2. Create a card: ${colors.highlight('"Create a card called Auth token refresh"')}`,
|
|
1610
|
+
);
|
|
1611
|
+
console.log(
|
|
1612
|
+
` 3. Start the daemon: ${colors.highlight("npx @gethmy/agent")}`,
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1520
1615
|
console.log("");
|
|
1521
1616
|
console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
|
|
1522
1617
|
console.log(
|