@gethmy/mcp 2.9.5 → 2.9.7

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 CHANGED
@@ -5,7 +5,7 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
5
5
 
6
6
  ## Features
7
7
 
8
- - **56 MCP Tools** for full board control, knowledge graph, and workflow plans
8
+ - **69 MCP Tools** for full board control, knowledge graph, and workflow plans
9
9
  - **5 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, `/hmy-memory-prune`, 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
package/dist/cli.js CHANGED
@@ -1815,6 +1815,16 @@ class HarmonyApiClient {
1815
1815
  async getCardByShortId(projectId, shortId) {
1816
1816
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1817
1817
  }
1818
+ async bulkGetCards(projectId, shortIds) {
1819
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1820
+ shortIds
1821
+ });
1822
+ }
1823
+ async bulkArchiveCards(projectId, shortIds) {
1824
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
1825
+ shortIds
1826
+ });
1827
+ }
1818
1828
  async searchCards(query, options) {
1819
1829
  const params = new URLSearchParams({ q: query });
1820
1830
  if (options?.projectId) {
@@ -3680,6 +3690,34 @@ var TOOLS = {
3680
3690
  required: ["shortId"]
3681
3691
  }
3682
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
+ },
3683
3721
  harmony_create_column: {
3684
3722
  description: "Create a new column in a project",
3685
3723
  inputSchema: {
@@ -5169,6 +5207,18 @@ async function handleToolCall(name, args, deps) {
5169
5207
  const result = await client3.getCardByShortId(projectId, shortId);
5170
5208
  return { success: true, ...result };
5171
5209
  }
5210
+ case "harmony_bulk_get_cards": {
5211
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5212
+ const projectId = getProjectId();
5213
+ const result = await client3.bulkGetCards(projectId, shortIds);
5214
+ return { success: true, ...result };
5215
+ }
5216
+ case "harmony_bulk_archive_cards": {
5217
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5218
+ const projectId = getProjectId();
5219
+ const result = await client3.bulkArchiveCards(projectId, shortIds);
5220
+ return { success: true, ...result };
5221
+ }
5172
5222
  case "harmony_create_column": {
5173
5223
  const name2 = z.string().min(1).max(100).parse(args.name);
5174
5224
  const projectId = args.projectId || getProjectId();
@@ -7473,12 +7523,23 @@ async function resolveProjectSlug(apiKey, slug) {
7473
7523
  }
7474
7524
  });
7475
7525
  if (response.status === 404)
7476
- return null;
7526
+ return { status: "not_found" };
7527
+ if (response.status === 409) {
7528
+ const data2 = await response.json().catch(() => ({}));
7529
+ return {
7530
+ status: "ambiguous",
7531
+ candidates: Array.isArray(data2.candidates) ? data2.candidates : []
7532
+ };
7533
+ }
7477
7534
  if (!response.ok) {
7478
7535
  throw new Error(`Failed to resolve project slug: ${response.status}`);
7479
7536
  }
7480
7537
  const data = await response.json();
7481
- return { workspaceId: data.workspaceId, projectId: data.projectId };
7538
+ return {
7539
+ status: "found",
7540
+ workspaceId: data.workspaceId,
7541
+ projectId: data.projectId
7542
+ };
7482
7543
  }
7483
7544
  async function getAgentFiles(agentId, cwd, installMode = "global") {
7484
7545
  const home = homedir6();
@@ -8002,14 +8063,23 @@ ${colors.dim(url)}`);
8002
8063
  let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
8003
8064
  let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
8004
8065
  let selectedProjectName = selectedProjectNameFromSignup;
8066
+ let ambiguousCandidates;
8005
8067
  if (options.projectSlug && apiKey && (!selectedWorkspaceId || !selectedProjectId)) {
8006
8068
  spinner3.start(`Resolving project slug "${options.projectSlug}"...`);
8007
8069
  try {
8008
8070
  const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
8009
- if (resolved) {
8071
+ if (resolved.status === "found") {
8010
8072
  selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
8011
8073
  selectedProjectId = selectedProjectId || resolved.projectId;
8012
8074
  spinner3.stop(colors.success(`Resolved "${options.projectSlug}"`));
8075
+ } else if (resolved.status === "ambiguous") {
8076
+ ambiguousCandidates = resolved.candidates;
8077
+ spinner3.stop(colors.warning(`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`));
8078
+ const list = resolved.candidates.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`).join(`
8079
+ `);
8080
+ p3.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
8081
+ ${list}
8082
+ Specify the workspace with --workspace <id>, or select one below.`);
8013
8083
  } else {
8014
8084
  spinner3.stop(colors.warning(`No project found for slug "${options.projectSlug}"`));
8015
8085
  }
@@ -8033,12 +8103,15 @@ ${colors.dim(url)}`);
8033
8103
  }
8034
8104
  if (needsContext && workspaces.length > 0) {
8035
8105
  if (!selectedWorkspaceId) {
8036
- const workspaceOptions = workspaces.map((ws) => ({
8106
+ const candidateIds = new Set((ambiguousCandidates ?? []).map((c) => c.workspaceId));
8107
+ const pickable = candidateIds.size > 0 ? workspaces.filter((ws) => candidateIds.has(ws.id)) : workspaces;
8108
+ const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
8109
+ const workspaceOptions = workspaceChoices.map((ws) => ({
8037
8110
  value: ws.id,
8038
8111
  label: ws.name
8039
8112
  }));
8040
8113
  const workspaceSelection = await p3.select({
8041
- message: "Select workspace",
8114
+ message: candidateIds.size > 0 ? `Select workspace for "${options.projectSlug}"` : "Select workspace",
8042
8115
  options: workspaceOptions
8043
8116
  });
8044
8117
  if (p3.isCancel(workspaceSelection)) {
@@ -8048,6 +8121,12 @@ ${colors.dim(url)}`);
8048
8121
  selectedWorkspaceId = workspaceSelection;
8049
8122
  }
8050
8123
  selectedWorkspaceName = workspaces.find((w) => w.id === selectedWorkspaceId)?.name;
8124
+ if (!selectedProjectId) {
8125
+ const chosenCandidate = ambiguousCandidates?.find((c) => c.workspaceId === selectedWorkspaceId);
8126
+ if (chosenCandidate) {
8127
+ selectedProjectId = chosenCandidate.projectId;
8128
+ }
8129
+ }
8051
8130
  spinner3.start("Fetching projects...");
8052
8131
  let projects = [];
8053
8132
  try {
@@ -8073,6 +8152,8 @@ ${colors.dim(url)}`);
8073
8152
  }
8074
8153
  selectedProjectId = projectSelection;
8075
8154
  selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
8155
+ } else if (selectedProjectId && !selectedProjectName) {
8156
+ selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
8076
8157
  }
8077
8158
  }
8078
8159
  }
package/dist/index.js CHANGED
@@ -1810,6 +1810,16 @@ class HarmonyApiClient {
1810
1810
  async getCardByShortId(projectId, shortId) {
1811
1811
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1812
1812
  }
1813
+ async bulkGetCards(projectId, shortIds) {
1814
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1815
+ shortIds
1816
+ });
1817
+ }
1818
+ async bulkArchiveCards(projectId, shortIds) {
1819
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
1820
+ shortIds
1821
+ });
1822
+ }
1813
1823
  async searchCards(query, options) {
1814
1824
  const params = new URLSearchParams({ q: query });
1815
1825
  if (options?.projectId) {
@@ -3675,6 +3685,34 @@ var TOOLS = {
3675
3685
  required: ["shortId"]
3676
3686
  }
3677
3687
  },
3688
+ harmony_bulk_get_cards: {
3689
+ 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.",
3690
+ inputSchema: {
3691
+ type: "object",
3692
+ properties: {
3693
+ shortIds: {
3694
+ type: "array",
3695
+ items: { type: "number" },
3696
+ description: "Card short ids, e.g. [400, 401, 402]. Max 100 per call."
3697
+ }
3698
+ },
3699
+ required: ["shortIds"]
3700
+ }
3701
+ },
3702
+ harmony_bulk_archive_cards: {
3703
+ 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.",
3704
+ inputSchema: {
3705
+ type: "object",
3706
+ properties: {
3707
+ shortIds: {
3708
+ type: "array",
3709
+ items: { type: "number" },
3710
+ description: "Card short ids to archive, e.g. [400, 401]. Max 100 per call."
3711
+ }
3712
+ },
3713
+ required: ["shortIds"]
3714
+ }
3715
+ },
3678
3716
  harmony_create_column: {
3679
3717
  description: "Create a new column in a project",
3680
3718
  inputSchema: {
@@ -5164,6 +5202,18 @@ async function handleToolCall(name, args, deps) {
5164
5202
  const result = await client3.getCardByShortId(projectId, shortId);
5165
5203
  return { success: true, ...result };
5166
5204
  }
5205
+ case "harmony_bulk_get_cards": {
5206
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5207
+ const projectId = getProjectId();
5208
+ const result = await client3.bulkGetCards(projectId, shortIds);
5209
+ return { success: true, ...result };
5210
+ }
5211
+ case "harmony_bulk_archive_cards": {
5212
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5213
+ const projectId = getProjectId();
5214
+ const result = await client3.bulkArchiveCards(projectId, shortIds);
5215
+ return { success: true, ...result };
5216
+ }
5167
5217
  case "harmony_create_column": {
5168
5218
  const name2 = z.string().min(1).max(100).parse(args.name);
5169
5219
  const projectId = args.projectId || getProjectId();
@@ -1263,6 +1263,16 @@ class HarmonyApiClient {
1263
1263
  async getCardByShortId(projectId, shortId) {
1264
1264
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1265
1265
  }
1266
+ async bulkGetCards(projectId, shortIds) {
1267
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1268
+ shortIds
1269
+ });
1270
+ }
1271
+ async bulkArchiveCards(projectId, shortIds) {
1272
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
1273
+ shortIds
1274
+ });
1275
+ }
1266
1276
  async searchCards(query, options) {
1267
1277
  const params = new URLSearchParams({ q: query });
1268
1278
  if (options?.projectId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.9.5",
3
+ "version": "2.9.7",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -75,6 +75,7 @@
75
75
  },
76
76
  "devDependencies": {
77
77
  "@harmony/memory": "workspace:*",
78
+ "@types/bun": "^1.3.13",
78
79
  "@types/node": "^25.5.0",
79
80
  "typescript": "^6.0.1"
80
81
  }
package/src/api-client.ts CHANGED
@@ -570,6 +570,39 @@ export class HarmonyApiClient {
570
570
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
571
571
  }
572
572
 
573
+ async bulkGetCards(
574
+ projectId: string,
575
+ shortIds: number[],
576
+ ): Promise<{
577
+ cards: Array<{
578
+ id: string;
579
+ shortId: number;
580
+ title: string;
581
+ column: string | null;
582
+ priority: string | null;
583
+ assignee: string | null;
584
+ labels: string[];
585
+ archived: boolean;
586
+ }>;
587
+ notFound: number[];
588
+ }> {
589
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
590
+ shortIds,
591
+ });
592
+ }
593
+
594
+ async bulkArchiveCards(
595
+ projectId: string,
596
+ shortIds: number[],
597
+ ): Promise<{
598
+ succeeded: number[];
599
+ failed: Array<{ shortId: number; reason: string }>;
600
+ }> {
601
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
602
+ shortIds,
603
+ });
604
+ }
605
+
573
606
  async searchCards(
574
607
  query: string,
575
608
  options?: { projectId?: 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: {
@@ -2241,6 +2278,28 @@ async function handleToolCall(
2241
2278
  return { success: true, ...result };
2242
2279
  }
2243
2280
 
2281
+ case "harmony_bulk_get_cards": {
2282
+ const shortIds = z
2283
+ .array(z.number().int().positive())
2284
+ .min(1)
2285
+ .max(100)
2286
+ .parse(args.shortIds);
2287
+ const projectId = getProjectId();
2288
+ const result = await client.bulkGetCards(projectId, shortIds);
2289
+ return { success: true, ...result };
2290
+ }
2291
+
2292
+ case "harmony_bulk_archive_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.bulkArchiveCards(projectId, shortIds);
2300
+ return { success: true, ...result };
2301
+ }
2302
+
2244
2303
  // Column operations
2245
2304
  case "harmony_create_column": {
2246
2305
  const name = z.string().min(1).max(100).parse(args.name);
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<{ workspaceId: string; projectId: string } | null> {
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 null;
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 { workspaceId: data.workspaceId, projectId: data.projectId };
385
+ return {
386
+ status: "found",
387
+ workspaceId: data.workspaceId,
388
+ projectId: data.projectId,
389
+ };
360
390
  }
361
391
 
362
392
  export interface FileToWrite {
@@ -1056,6 +1086,10 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1056
1086
  let selectedWorkspaceName: string | undefined =
1057
1087
  selectedWorkspaceNameFromSignup;
1058
1088
  let selectedProjectName: string | undefined = selectedProjectNameFromSignup;
1089
+ // When a slug is ambiguous, the candidate workspaces (each already carrying
1090
+ // the matching projectId) narrow the interactive picker below to just those
1091
+ // workspaces instead of every workspace the user belongs to.
1092
+ let ambiguousCandidates: SlugCandidate[] | undefined;
1059
1093
 
1060
1094
  // Resolve project slug shorthand (e.g. `npx @gethmy/mcp setup harmony-6590761b`).
1061
1095
  // Slug wins over --workspace/--project flags only when those aren't already set
@@ -1068,10 +1102,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1068
1102
  spinner.start(`Resolving project slug "${options.projectSlug}"...`);
1069
1103
  try {
1070
1104
  const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
1071
- if (resolved) {
1105
+ if (resolved.status === "found") {
1072
1106
  selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
1073
1107
  selectedProjectId = selectedProjectId || resolved.projectId;
1074
1108
  spinner.stop(colors.success(`Resolved "${options.projectSlug}"`));
1109
+ } else if (resolved.status === "ambiguous") {
1110
+ // Same slug in multiple workspaces — don't silently pick one. Surface
1111
+ // the candidates so the user knows to specify a workspace; interactive
1112
+ // context selection below still lets them recover in a TTY, narrowed to
1113
+ // just these workspaces.
1114
+ ambiguousCandidates = resolved.candidates;
1115
+ spinner.stop(
1116
+ colors.warning(
1117
+ `Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`,
1118
+ ),
1119
+ );
1120
+ const list = resolved.candidates
1121
+ .map((c) => ` • ${c.workspaceName ?? c.workspaceId}`)
1122
+ .join("\n");
1123
+ p.log.warning(
1124
+ `"${options.projectSlug}" matches projects in multiple workspaces:\n${list}\n` +
1125
+ "Specify the workspace with --workspace <id>, or select one below.",
1126
+ );
1075
1127
  } else {
1076
1128
  spinner.stop(
1077
1129
  colors.warning(`No project found for slug "${options.projectSlug}"`),
@@ -1108,15 +1160,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1108
1160
  }
1109
1161
 
1110
1162
  if (needsContext && workspaces.length > 0) {
1111
- // Select workspace
1163
+ // Select workspace. When the slug was ambiguous, restrict the choices to
1164
+ // the candidate workspaces so the user picks among the ones that actually
1165
+ // contain the slug (falling back to all workspaces if none still match).
1112
1166
  if (!selectedWorkspaceId) {
1113
- const workspaceOptions = workspaces.map((ws) => ({
1167
+ const candidateIds = new Set(
1168
+ (ambiguousCandidates ?? []).map((c) => c.workspaceId),
1169
+ );
1170
+ const pickable =
1171
+ candidateIds.size > 0
1172
+ ? workspaces.filter((ws) => candidateIds.has(ws.id))
1173
+ : workspaces;
1174
+ const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
1175
+ const workspaceOptions = workspaceChoices.map((ws) => ({
1114
1176
  value: ws.id,
1115
1177
  label: ws.name,
1116
1178
  }));
1117
1179
 
1118
1180
  const workspaceSelection = await p.select({
1119
- message: "Select workspace",
1181
+ message:
1182
+ candidateIds.size > 0
1183
+ ? `Select workspace for "${options.projectSlug}"`
1184
+ : "Select workspace",
1120
1185
  options: workspaceOptions,
1121
1186
  });
1122
1187
 
@@ -1132,6 +1197,17 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1132
1197
  (w) => w.id === selectedWorkspaceId,
1133
1198
  )?.name;
1134
1199
 
1200
+ // For an ambiguous slug, the chosen workspace already pins the project —
1201
+ // each candidate carries its projectId — so skip the project picker.
1202
+ if (!selectedProjectId) {
1203
+ const chosenCandidate = ambiguousCandidates?.find(
1204
+ (c) => c.workspaceId === selectedWorkspaceId,
1205
+ );
1206
+ if (chosenCandidate) {
1207
+ selectedProjectId = chosenCandidate.projectId;
1208
+ }
1209
+ }
1210
+
1135
1211
  // Fetch and select project
1136
1212
  spinner.start("Fetching projects...");
1137
1213
  let projects: Project[] = [];
@@ -1167,6 +1243,12 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1167
1243
  selectedProjectName = projects.find(
1168
1244
  (p) => p.id === selectedProjectId,
1169
1245
  )?.name;
1246
+ } else if (selectedProjectId && !selectedProjectName) {
1247
+ // Project was pre-resolved (e.g. from an ambiguous-slug candidate) so
1248
+ // the picker was skipped — backfill the name for the summary display.
1249
+ selectedProjectName = projects.find(
1250
+ (p) => p.id === selectedProjectId,
1251
+ )?.name;
1170
1252
  }
1171
1253
  }
1172
1254
  }