@gethmy/mcp 2.17.1 → 2.19.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 CHANGED
@@ -1869,6 +1869,10 @@ class HarmonyApiClient {
1869
1869
  async getCardByShortId(projectId, shortId) {
1870
1870
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1871
1871
  }
1872
+ async resolveCardByShortId(shortId, preferredProjectId) {
1873
+ const qs = preferredProjectId ? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}` : "";
1874
+ return this.request("GET", `/cards/resolve/${shortId}${qs}`);
1875
+ }
1872
1876
  async bulkGetCards(projectId, shortIds) {
1873
1877
  return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1874
1878
  shortIds
@@ -5763,9 +5767,51 @@ async function handleToolCall(name, args, deps) {
5763
5767
  }
5764
5768
  if (hasShortId) {
5765
5769
  const shortId = z.number().int().positive().parse(args.shortId);
5766
- const projectId = args.projectId || getProjectId();
5767
- const result2 = await client3.getCardByShortId(projectId, shortId);
5768
- return { success: true, ...result2 };
5770
+ const explicitProjectId = args.projectId;
5771
+ if (explicitProjectId) {
5772
+ const result2 = await client3.getCardByShortId(explicitProjectId, shortId);
5773
+ return { success: true, ...result2 };
5774
+ }
5775
+ const activeProjectId = deps.getActiveProjectId();
5776
+ const resolved = await client3.resolveCardByShortId(shortId, activeProjectId);
5777
+ if (resolved.kind === "found") {
5778
+ const cardTitle = resolved.card?.title ?? `#${shortId}`;
5779
+ const where = resolved.project.workspaceName ? `project "${resolved.project.name ?? resolved.project.id}" (workspace "${resolved.project.workspaceName}")` : `project "${resolved.project.name ?? resolved.project.id}"`;
5780
+ const established = activeProjectId == null;
5781
+ if (established) {
5782
+ deps.setActiveProject(resolved.project.id);
5783
+ }
5784
+ return {
5785
+ success: true,
5786
+ card: resolved.card,
5787
+ resolvedProject: resolved.project,
5788
+ activeProjectId: resolved.project.id,
5789
+ note: established ? `Resolved #${shortId} → "${cardTitle}" in ${where}. No active project was set — set to this for follow-up references.` : `Resolved #${shortId} → "${cardTitle}" in ${where} (your active project).`
5790
+ };
5791
+ }
5792
+ if (resolved.kind === "not_in_preferred") {
5793
+ const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
5794
+ `);
5795
+ throw new Error(`#${shortId} is not in your active project (projectId: ${resolved.preferredProjectId}). ` + `It exists in ${resolved.candidates.length} other project(s) you can access:
5796
+ ${list}
5797
+
5798
+ ` + `Switch with harmony_set_project_context, or pass an explicit projectId to fetch it directly.`);
5799
+ }
5800
+ if (resolved.kind === "ambiguous") {
5801
+ const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
5802
+ `);
5803
+ return {
5804
+ success: true,
5805
+ needsDisambiguation: true,
5806
+ shortId,
5807
+ candidates: resolved.candidates,
5808
+ message: `#${shortId} exists in ${resolved.candidates.length} projects you can access:
5809
+ ${list}
5810
+
5811
+ ` + `Ask which one is meant, then re-fetch with an explicit projectId ` + `(or call harmony_set_project_context first).`
5812
+ };
5813
+ }
5814
+ throw new Error(resolved.searchedProjectCount === 0 ? `#${shortId} can't be resolved: no project is accessible to this connection. ` + `Check the workspace this connection is authorized for with harmony_list_workspaces.` : `Card #${shortId} was not found in any of the ${resolved.searchedProjectCount} ` + `project(s) across ${resolved.searchedWorkspaceCount} workspace(s) this connection can access. ` + `Use harmony_list_projects to see them, or pass an explicit projectId.`);
5769
5815
  }
5770
5816
  const cardId = z.string().uuid().parse(args.cardId);
5771
5817
  const result = await client3.getCard(cardId);
@@ -7215,7 +7261,7 @@ import {
7215
7261
  } from "node:fs";
7216
7262
  import { homedir as homedir6 } from "node:os";
7217
7263
  import { dirname as dirname3, join as join8 } from "node:path";
7218
- import * as p3 from "@clack/prompts";
7264
+ import * as p4 from "@clack/prompts";
7219
7265
  init_config();
7220
7266
  init_oauth_login();
7221
7267
 
@@ -7273,10 +7319,21 @@ function detectAgents(cwd = process.cwd()) {
7273
7319
  });
7274
7320
  }
7275
7321
 
7322
+ // src/tui/confirm.ts
7323
+ import * as p from "@clack/prompts";
7324
+ function shouldAssumeYes(yesFlag, isTTY) {
7325
+ return yesFlag === true || isTTY !== true;
7326
+ }
7327
+ async function confirmOrDefault(assumeYes, opts) {
7328
+ if (assumeYes)
7329
+ return opts.initialValue ?? true;
7330
+ return p.confirm(opts);
7331
+ }
7332
+
7276
7333
  // src/tui/docs.ts
7277
7334
  import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
7278
7335
  import { isAbsolute, join as join7, resolve, sep as sep2 } from "node:path";
7279
- import * as p from "@clack/prompts";
7336
+ import * as p2 from "@clack/prompts";
7280
7337
 
7281
7338
  // src/tui/theme.ts
7282
7339
  import pc from "picocolors";
@@ -7924,11 +7981,11 @@ async function runDocsStep(cwd) {
7924
7981
  const info = scanProject(cwd);
7925
7982
  const hasDocs = info.existingDocs.agentsMd || info.existingDocs.claudeMd;
7926
7983
  if (!hasDocs) {
7927
- const shouldGenerate = await p.confirm({
7984
+ const shouldGenerate = await p2.confirm({
7928
7985
  message: "No project docs found. Generate AGENTS.md and CLAUDE.md?",
7929
7986
  initialValue: true
7930
7987
  });
7931
- if (p.isCancel(shouldGenerate) || !shouldGenerate) {
7988
+ if (p2.isCancel(shouldGenerate) || !shouldGenerate) {
7932
7989
  return { files: [], issues: [], skipped: true };
7933
7990
  }
7934
7991
  const files = [];
@@ -7949,32 +8006,32 @@ async function runDocsStep(cwd) {
7949
8006
  type: "text"
7950
8007
  });
7951
8008
  }
7952
- p.log.success(`Generated ${files.length} doc file(s): ${files.map((f) => f.path.replace(cwd + "/", "")).join(", ")}`);
8009
+ p2.log.success(`Generated ${files.length} doc file(s): ${files.map((f) => f.path.replace(cwd + "/", "")).join(", ")}`);
7953
8010
  return { files, issues: [], skipped: false };
7954
8011
  }
7955
- const shouldVerify = await p.confirm({
8012
+ const shouldVerify = await p2.confirm({
7956
8013
  message: "Project docs found. Verify for issues?",
7957
8014
  initialValue: false
7958
8015
  });
7959
- if (p.isCancel(shouldVerify) || !shouldVerify) {
8016
+ if (p2.isCancel(shouldVerify) || !shouldVerify) {
7960
8017
  return { files: [], issues: [], skipped: true };
7961
8018
  }
7962
8019
  const issues = verifyDocs(cwd);
7963
8020
  if (issues.length === 0) {
7964
- p.log.success("No issues found in project docs.");
8021
+ p2.log.success("No issues found in project docs.");
7965
8022
  } else {
7966
8023
  for (const issue of issues) {
7967
8024
  const prefix = `${colors.bold(issue.file)}:`;
7968
8025
  if (issue.severity === "error") {
7969
- p.log.error(`${prefix} ${issue.message}`);
8026
+ p2.log.error(`${prefix} ${issue.message}`);
7970
8027
  } else {
7971
- p.log.warning(`${prefix} ${issue.message}`);
8028
+ p2.log.warning(`${prefix} ${issue.message}`);
7972
8029
  }
7973
8030
  if (issue.fix) {
7974
- p.log.message(` ${symbols.arrow} ${colors.dim(issue.fix)}`);
8031
+ p2.log.message(` ${symbols.arrow} ${colors.dim(issue.fix)}`);
7975
8032
  }
7976
8033
  }
7977
- p.log.info(`Found ${issues.length} issue(s) (${issues.filter((i) => i.severity === "error").length} errors, ${issues.filter((i) => i.severity === "warning").length} warnings)`);
8034
+ p2.log.info(`Found ${issues.length} issue(s) (${issues.filter((i) => i.severity === "error").length} errors, ${issues.filter((i) => i.severity === "warning").length} warnings)`);
7978
8035
  }
7979
8036
  return { files: [], issues, skipped: false };
7980
8037
  }
@@ -7989,7 +8046,7 @@ import {
7989
8046
  } from "node:fs";
7990
8047
  import { homedir as homedir5 } from "node:os";
7991
8048
  import { dirname as dirname2 } from "node:path";
7992
- import * as p2 from "@clack/prompts";
8049
+ import * as p3 from "@clack/prompts";
7993
8050
  function ensureDir(dirPath) {
7994
8051
  if (!existsSync7(dirPath)) {
7995
8052
  mkdirSync4(dirPath, { recursive: true, mode: 493 });
@@ -8109,7 +8166,7 @@ function appendToToml(filePath, section, content, options = {}) {
8109
8166
  async function writeFilesWithProgress(files, options = {}) {
8110
8167
  const results = [];
8111
8168
  const home = homedir5();
8112
- const spinner2 = p2.spinner();
8169
+ const spinner2 = p3.spinner();
8113
8170
  spinner2.start("Writing configuration files...");
8114
8171
  for (const file of files) {
8115
8172
  let result;
@@ -8601,6 +8658,10 @@ async function runSetup(options = {}) {
8601
8658
  const home = homedir6();
8602
8659
  console.clear();
8603
8660
  console.log(messages.header());
8661
+ const assumeYes = shouldAssumeYes(options.yes, process.stdin.isTTY);
8662
+ if (assumeYes) {
8663
+ p4.log.info(options.yes ? "Non-interactive mode (--yes): using the default answer for each confirmation." : "No interactive terminal detected: using the default answer for each confirmation.");
8664
+ }
8604
8665
  const existingConfig = loadConfig();
8605
8666
  const alreadyConfigured = isConfigured();
8606
8667
  const skillsStatus = areSkillsInstalled(cwd);
@@ -8620,7 +8681,7 @@ async function runSetup(options = {}) {
8620
8681
  let createdNewAccount = false;
8621
8682
  let oauthTokens;
8622
8683
  if (options.apiKey) {
8623
- p3.log.warn(colors.warning(`--api-key is deprecated and insecure: the key is exposed in your shell
8684
+ p4.log.warn(colors.warning(`--api-key is deprecated and insecure: the key is exposed in your shell
8624
8685
  history, terminal scrollback, and the process list. Prefer the browser
8625
8686
  sign-in (run \`npx @gethmy/mcp setup\` with no --api-key). Use --api-key
8626
8687
  only for unattended CI where you accept that risk.`));
@@ -8631,7 +8692,7 @@ only for unattended CI where you accept that risk.`));
8631
8692
  if (!useNewAccount && options.apiKey) {
8632
8693
  useNewAccount = false;
8633
8694
  } else if (!useNewAccount && !options.apiKey) {
8634
- const getStarted = await p3.select({
8695
+ const getStarted = await p4.select({
8635
8696
  message: "How would you like to connect?",
8636
8697
  options: [
8637
8698
  {
@@ -8652,15 +8713,15 @@ only for unattended CI where you accept that risk.`));
8652
8713
  ],
8653
8714
  initialValue: "browser"
8654
8715
  });
8655
- if (p3.isCancel(getStarted)) {
8656
- p3.cancel("Setup cancelled");
8716
+ if (p4.isCancel(getStarted)) {
8717
+ p4.cancel("Setup cancelled");
8657
8718
  process.exit(0);
8658
8719
  }
8659
8720
  useNewAccount = getStarted === "create";
8660
8721
  useBrowserAuth = getStarted === "browser";
8661
8722
  }
8662
8723
  if (useBrowserAuth) {
8663
- const spinner4 = p3.spinner();
8724
+ const spinner4 = p4.spinner();
8664
8725
  spinner4.start("Opening your browser to authorize…");
8665
8726
  try {
8666
8727
  oauthTokens = await loginWithBrowser({
@@ -8685,12 +8746,12 @@ ${colors.dim(url)}`);
8685
8746
  } catch (error) {
8686
8747
  spinner4.stop(colors.error("Browser authorization failed"));
8687
8748
  const msg = error instanceof Error ? error.message : "Unknown error";
8688
- p3.log.error(msg);
8689
- p3.log.info("You can retry, or run with --api-key for unattended setup.");
8749
+ p4.log.error(msg);
8750
+ p4.log.info("You can retry, or run with --api-key for unattended setup.");
8690
8751
  process.exit(1);
8691
8752
  }
8692
8753
  } else if (useNewAccount) {
8693
- const fullName = options.name || await p3.text({
8754
+ const fullName = options.name || await p4.text({
8694
8755
  message: "Full name",
8695
8756
  placeholder: "Jane Smith",
8696
8757
  validate: (v) => {
@@ -8701,11 +8762,11 @@ ${colors.dim(url)}`);
8701
8762
  return;
8702
8763
  }
8703
8764
  });
8704
- if (p3.isCancel(fullName)) {
8705
- p3.cancel("Setup cancelled");
8765
+ if (p4.isCancel(fullName)) {
8766
+ p4.cancel("Setup cancelled");
8706
8767
  process.exit(0);
8707
8768
  }
8708
- const email = options.userEmail || await p3.text({
8769
+ const email = options.userEmail || await p4.text({
8709
8770
  message: "Email",
8710
8771
  placeholder: "you@example.com",
8711
8772
  validate: (v) => {
@@ -8718,11 +8779,11 @@ ${colors.dim(url)}`);
8718
8779
  return;
8719
8780
  }
8720
8781
  });
8721
- if (p3.isCancel(email)) {
8722
- p3.cancel("Setup cancelled");
8782
+ if (p4.isCancel(email)) {
8783
+ p4.cancel("Setup cancelled");
8723
8784
  process.exit(0);
8724
8785
  }
8725
- const password2 = await p3.password({
8786
+ const password2 = await p4.password({
8726
8787
  message: "Password",
8727
8788
  validate: (v) => {
8728
8789
  if (!v)
@@ -8734,11 +8795,11 @@ ${colors.dim(url)}`);
8734
8795
  return;
8735
8796
  }
8736
8797
  });
8737
- if (p3.isCancel(password2)) {
8738
- p3.cancel("Setup cancelled");
8798
+ if (p4.isCancel(password2)) {
8799
+ p4.cancel("Setup cancelled");
8739
8800
  process.exit(0);
8740
8801
  }
8741
- const spinner4 = p3.spinner();
8802
+ const spinner4 = p4.spinner();
8742
8803
  spinner4.start("Creating your account...");
8743
8804
  try {
8744
8805
  const result = await onboardNewUser({
@@ -8758,15 +8819,15 @@ ${colors.dim(url)}`);
8758
8819
  saveConfig({ apiKey, userEmail, apiUrl: API_URL });
8759
8820
  setActiveWorkspace(selectedWorkspaceIdFromSignup);
8760
8821
  setActiveProject(selectedProjectIdFromSignup);
8761
- p3.log.success("Workspace and board created");
8822
+ p4.log.success("Workspace and board created");
8762
8823
  } catch (error) {
8763
8824
  spinner4.stop(colors.error("Account creation failed"));
8764
8825
  const msg = error instanceof Error ? error.message : "Unknown error";
8765
8826
  if (msg.includes("already") || msg.includes("409")) {
8766
- p3.log.error("Account already exists. Sign in at app.gethmy.com to get your API key, or re-run setup and choose 'I already have an API key'.");
8827
+ p4.log.error("Account already exists. Sign in at app.gethmy.com to get your API key, or re-run setup and choose 'I already have an API key'.");
8767
8828
  } else {
8768
- p3.log.error(msg);
8769
- p3.log.info("Please try again or visit https://app.gethmy.com");
8829
+ p4.log.error(msg);
8830
+ p4.log.info("Please try again or visit https://app.gethmy.com");
8770
8831
  }
8771
8832
  process.exit(1);
8772
8833
  }
@@ -8774,7 +8835,7 @@ ${colors.dim(url)}`);
8774
8835
  apiKey = options.apiKey;
8775
8836
  needsApiKey = true;
8776
8837
  } else {
8777
- const keyInput = await p3.text({
8838
+ const keyInput = await p4.text({
8778
8839
  message: "Enter your Harmony API key",
8779
8840
  placeholder: "hmy_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
8780
8841
  validate: (value) => {
@@ -8787,24 +8848,24 @@ ${colors.dim(url)}`);
8787
8848
  return;
8788
8849
  }
8789
8850
  });
8790
- if (p3.isCancel(keyInput)) {
8791
- p3.cancel("Setup cancelled");
8851
+ if (p4.isCancel(keyInput)) {
8852
+ p4.cancel("Setup cancelled");
8792
8853
  process.exit(0);
8793
8854
  }
8794
8855
  apiKey = keyInput;
8795
8856
  needsApiKey = true;
8796
8857
  }
8797
8858
  } else {
8798
- p3.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
8859
+ p4.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
8799
8860
  }
8800
- const spinner3 = p3.spinner();
8861
+ const spinner3 = p4.spinner();
8801
8862
  if (!createdNewAccount) {
8802
8863
  spinner3.start("Validating API key...");
8803
8864
  const validation = await validateApiKey(apiKey);
8804
8865
  if (!validation.valid) {
8805
8866
  spinner3.stop(colors.error("API key validation failed"));
8806
- p3.log.error(validation.error || "Could not connect to Harmony API");
8807
- p3.log.info("Get an API key at: https://app.gethmy.com/user/keys");
8867
+ p4.log.error(validation.error || "Could not connect to Harmony API");
8868
+ p4.log.info("Get an API key at: https://app.gethmy.com/user/keys");
8808
8869
  process.exit(1);
8809
8870
  }
8810
8871
  if (!userEmail) {
@@ -8815,13 +8876,13 @@ ${colors.dim(url)}`);
8815
8876
  let selectedAgents = [];
8816
8877
  let installMode = options.installMode || "global";
8817
8878
  if (skillsStatus.installed && !options.force) {
8818
- p3.log.success(`Skills already installed (${skillsStatus.location})`);
8819
- const reinstall = await p3.confirm({
8879
+ p4.log.success(`Skills already installed (${skillsStatus.location})`);
8880
+ const reinstall = await confirmOrDefault(assumeYes, {
8820
8881
  message: "Reinstall skills?",
8821
8882
  initialValue: false
8822
8883
  });
8823
- if (p3.isCancel(reinstall)) {
8824
- p3.cancel("Setup cancelled");
8884
+ if (p4.isCancel(reinstall)) {
8885
+ p4.cancel("Setup cancelled");
8825
8886
  process.exit(0);
8826
8887
  }
8827
8888
  needsSkills = reinstall;
@@ -8836,23 +8897,23 @@ ${colors.dim(url)}`);
8836
8897
  label: agent.name,
8837
8898
  hint: agent.detected ? colors.success(`${agent.description} (detected)`) : colors.dim(`${agent.description}`)
8838
8899
  }));
8839
- const agentSelection = await p3.multiselect({
8900
+ const agentSelection = await p4.multiselect({
8840
8901
  message: "Select agents to configure",
8841
8902
  options: agentOptions,
8842
8903
  initialValues: detectedAgents2.filter((a) => a.detected).map((a) => a.id),
8843
8904
  required: true
8844
8905
  });
8845
- if (p3.isCancel(agentSelection)) {
8846
- p3.cancel("Setup cancelled");
8906
+ if (p4.isCancel(agentSelection)) {
8907
+ p4.cancel("Setup cancelled");
8847
8908
  process.exit(0);
8848
8909
  }
8849
8910
  selectedAgents = agentSelection;
8850
8911
  }
8851
8912
  if (selectedAgents.length === 0) {
8852
- p3.log.warning("No agents selected. Skipping skills installation.");
8913
+ p4.log.warning("No agents selected. Skipping skills installation.");
8853
8914
  needsSkills = false;
8854
8915
  } else if (!options.installMode) {
8855
- const modeSelection = await p3.select({
8916
+ const modeSelection = await p4.select({
8856
8917
  message: "Where should Harmony skills be installed?",
8857
8918
  options: [
8858
8919
  {
@@ -8868,8 +8929,8 @@ ${colors.dim(url)}`);
8868
8929
  ],
8869
8930
  initialValue: "global"
8870
8931
  });
8871
- if (p3.isCancel(modeSelection)) {
8872
- p3.cancel("Setup cancelled");
8932
+ if (p4.isCancel(modeSelection)) {
8933
+ p4.cancel("Setup cancelled");
8873
8934
  process.exit(0);
8874
8935
  }
8875
8936
  installMode = modeSelection;
@@ -8893,7 +8954,7 @@ ${colors.dim(url)}`);
8893
8954
  spinner3.stop(colors.warning(`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`));
8894
8955
  const list = resolved.candidates.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`).join(`
8895
8956
  `);
8896
- p3.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
8957
+ p4.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
8897
8958
  ${list}
8898
8959
  Specify the workspace with --workspace <id>, or select one below.`);
8899
8960
  } else {
@@ -8914,7 +8975,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
8914
8975
  spinner3.stop(colors.success(`Found ${workspaces.length} workspace(s)`));
8915
8976
  } catch (_error) {
8916
8977
  spinner3.stop(colors.warning("Could not fetch workspaces"));
8917
- p3.log.warning("Skipping workspace/project selection. You can set this later.");
8978
+ p4.log.warning("Skipping workspace/project selection. You can set this later.");
8918
8979
  needsContext = false;
8919
8980
  }
8920
8981
  if (needsContext && workspaces.length > 0) {
@@ -8926,12 +8987,12 @@ Specify the workspace with --workspace <id>, or select one below.`);
8926
8987
  value: ws.id,
8927
8988
  label: ws.name
8928
8989
  }));
8929
- const workspaceSelection = await p3.select({
8990
+ const workspaceSelection = await p4.select({
8930
8991
  message: candidateIds.size > 0 ? `Select workspace for "${options.projectSlug}"` : "Select workspace",
8931
8992
  options: workspaceOptions
8932
8993
  });
8933
- if (p3.isCancel(workspaceSelection)) {
8934
- p3.cancel("Setup cancelled");
8994
+ if (p4.isCancel(workspaceSelection)) {
8995
+ p4.cancel("Setup cancelled");
8935
8996
  process.exit(0);
8936
8997
  }
8937
8998
  selectedWorkspaceId = workspaceSelection;
@@ -8950,7 +9011,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
8950
9011
  spinner3.stop(colors.success(`Found ${projects.length} project(s)`));
8951
9012
  } catch (_error) {
8952
9013
  spinner3.stop(colors.warning("Could not fetch projects"));
8953
- p3.log.warning("Skipping project selection. You can set this later.");
9014
+ p4.log.warning("Skipping project selection. You can set this later.");
8954
9015
  }
8955
9016
  if (projects.length > 0 && !selectedProjectId) {
8956
9017
  const projectOptions = projects.map((proj) => ({
@@ -8958,18 +9019,18 @@ Specify the workspace with --workspace <id>, or select one below.`);
8958
9019
  label: proj.name,
8959
9020
  hint: proj.description ? colors.dim(proj.description.slice(0, 50)) : undefined
8960
9021
  }));
8961
- const projectSelection = await p3.select({
9022
+ const projectSelection = await p4.select({
8962
9023
  message: "Select project",
8963
9024
  options: projectOptions
8964
9025
  });
8965
- if (p3.isCancel(projectSelection)) {
8966
- p3.cancel("Setup cancelled");
9026
+ if (p4.isCancel(projectSelection)) {
9027
+ p4.cancel("Setup cancelled");
8967
9028
  process.exit(0);
8968
9029
  }
8969
9030
  selectedProjectId = projectSelection;
8970
- selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
9031
+ selectedProjectName = projects.find((p5) => p5.id === selectedProjectId)?.name;
8971
9032
  } else if (selectedProjectId && !selectedProjectName) {
8972
- selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
9033
+ selectedProjectName = projects.find((p5) => p5.id === selectedProjectId)?.name;
8973
9034
  }
8974
9035
  }
8975
9036
  }
@@ -9016,7 +9077,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
9016
9077
  }
9017
9078
  const detectedAgents = detectAgents(cwd);
9018
9079
  console.log("");
9019
- p3.log.step("Summary");
9080
+ p4.log.step("Summary");
9020
9081
  console.log("");
9021
9082
  if (oauthTokens) {
9022
9083
  console.log(` ${colors.bold("Credential:")} Browser sign-in (OAuth, workspace-scoped)`);
@@ -9072,12 +9133,12 @@ Specify the workspace with --workspace <id>, or select one below.`);
9072
9133
  }
9073
9134
  }
9074
9135
  console.log("");
9075
- const shouldProceed = await p3.confirm({
9136
+ const shouldProceed = await confirmOrDefault(assumeYes, {
9076
9137
  message: "Proceed with setup?",
9077
9138
  initialValue: true
9078
9139
  });
9079
- if (p3.isCancel(shouldProceed) || !shouldProceed) {
9080
- p3.cancel("Setup cancelled");
9140
+ if (p4.isCancel(shouldProceed) || !shouldProceed) {
9141
+ p4.cancel("Setup cancelled");
9081
9142
  process.exit(0);
9082
9143
  }
9083
9144
  console.log("");
@@ -9107,7 +9168,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
9107
9168
  }
9108
9169
  symlinkSync(symlink.target, symlink.link);
9109
9170
  } catch {
9110
- p3.log.warning(`Failed to create symlink: ${symlink.link}`);
9171
+ p4.log.warning(`Failed to create symlink: ${symlink.link}`);
9111
9172
  }
9112
9173
  }
9113
9174
  }
@@ -9121,16 +9182,19 @@ Specify the workspace with --workspace <id>, or select one below.`);
9121
9182
  await writeMcpConfigFallback(home);
9122
9183
  console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
9123
9184
  } catch {
9124
- p3.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
9185
+ p4.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
9125
9186
  }
9126
9187
  }
9127
9188
  }
9128
9189
  if (claudeDetected || selectedAgents.includes("claude")) {
9129
9190
  const allowAll = options.allowAllTools === true;
9130
9191
  const message = allowAll ? "Allowlist EVERY Harmony tool without confirmation, including destructive ones (delete/archive/api-key/invite)?" : "Allowlist common Harmony tools (reads + create/update/move/comment) so /hmy doesn't prompt each time? Destructive tools (delete/archive/api-key/invite) will still ask.";
9131
- const allowTools = await p3.confirm({ message, initialValue: true });
9132
- if (p3.isCancel(allowTools)) {
9133
- p3.cancel("Setup cancelled.");
9192
+ const allowTools = await confirmOrDefault(assumeYes, {
9193
+ message,
9194
+ initialValue: true
9195
+ });
9196
+ if (p4.isCancel(allowTools)) {
9197
+ p4.cancel("Setup cancelled.");
9134
9198
  process.exit(0);
9135
9199
  }
9136
9200
  if (allowTools) {
@@ -9139,7 +9203,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
9139
9203
  const scope = allowAll ? "all tools" : "safe tools";
9140
9204
  console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
9141
9205
  } catch {
9142
- p3.log.warning("Could not allowlist Harmony tools. Run /permissions in Claude Code and choose “always allow” for Harmony, or add mcp__harmony to permissions.allow in ~/.claude/settings.json.");
9206
+ p4.log.warning("Could not allowlist Harmony tools. Run /permissions in Claude Code and choose “always allow” for Harmony, or add mcp__harmony to permissions.allow in ~/.claude/settings.json.");
9143
9207
  }
9144
9208
  } else {
9145
9209
  console.log(` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`);
@@ -9159,7 +9223,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
9159
9223
  setActiveProject(selectedProjectId);
9160
9224
  }
9161
9225
  console.log("");
9162
- p3.outro(colors.success("Setup complete!"));
9226
+ p4.outro(colors.success("Setup complete!"));
9163
9227
  if (createdNewAccount && selectedWorkspaceNameFromSignup) {
9164
9228
  const wsSlug = selectedWorkspaceNameFromSignup.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
9165
9229
  const projSlug = (selectedProjectNameFromSignup || "my-first-board").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
@@ -9292,7 +9356,7 @@ program.command("reset").description("Remove stored configuration").action(() =>
9292
9356
  console.log(`
9293
9357
  To reconfigure, run: npx @gethmy/mcp setup`);
9294
9358
  });
9295
- program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "DEPRECATED (insecure: key leaks via argv/shell history). For unattended CI only — interactive setup uses browser sign-in.").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context (UUID)").option("-p, --project <id>", "Set project context (UUID)").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").option("--allow-all-tools", "Allowlist every Harmony tool (incl. destructive: delete/archive/api-key/invite) without confirmation. Default allowlists only read + routine-write tools; destructive tools keep prompting.").action(async (slug, options) => {
9359
+ program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "DEPRECATED (insecure: key leaks via argv/shell history). For unattended CI only — interactive setup uses browser sign-in.").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context (UUID)").option("-p, --project <id>", "Set project context (UUID)").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("-y, --yes", "Non-interactive: answer every yes/no confirmation with its default. Implied when there is no TTY (pipe / coding agent / CI). Provide the other inputs via flags (--api-key, --agents, --workspace/--project or --skip-context, --skip-docs).").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").option("--allow-all-tools", "Allowlist every Harmony tool (incl. destructive: delete/archive/api-key/invite) without confirmation. Default allowlists only read + routine-write tools; destructive tools keep prompting.").action(async (slug, options) => {
9296
9360
  await runSetup({
9297
9361
  force: options.force,
9298
9362
  apiKey: options.apiKey,
@@ -9306,7 +9370,8 @@ program.command("setup").description("Smart setup wizard for Harmony MCP (recomm
9306
9370
  skipDocs: options.skipDocs,
9307
9371
  newAccount: options.new,
9308
9372
  name: options.name,
9309
- allowAllTools: options.allowAllTools
9373
+ allowAllTools: options.allowAllTools,
9374
+ yes: options.yes
9310
9375
  });
9311
9376
  });
9312
9377
  program.parse();
package/dist/index.js CHANGED
@@ -1864,6 +1864,10 @@ class HarmonyApiClient {
1864
1864
  async getCardByShortId(projectId, shortId) {
1865
1865
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1866
1866
  }
1867
+ async resolveCardByShortId(shortId, preferredProjectId) {
1868
+ const qs = preferredProjectId ? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}` : "";
1869
+ return this.request("GET", `/cards/resolve/${shortId}${qs}`);
1870
+ }
1867
1871
  async bulkGetCards(projectId, shortIds) {
1868
1872
  return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1869
1873
  shortIds
@@ -5758,9 +5762,51 @@ async function handleToolCall(name, args, deps) {
5758
5762
  }
5759
5763
  if (hasShortId) {
5760
5764
  const shortId = z.number().int().positive().parse(args.shortId);
5761
- const projectId = args.projectId || getProjectId();
5762
- const result2 = await client3.getCardByShortId(projectId, shortId);
5763
- return { success: true, ...result2 };
5765
+ const explicitProjectId = args.projectId;
5766
+ if (explicitProjectId) {
5767
+ const result2 = await client3.getCardByShortId(explicitProjectId, shortId);
5768
+ return { success: true, ...result2 };
5769
+ }
5770
+ const activeProjectId = deps.getActiveProjectId();
5771
+ const resolved = await client3.resolveCardByShortId(shortId, activeProjectId);
5772
+ if (resolved.kind === "found") {
5773
+ const cardTitle = resolved.card?.title ?? `#${shortId}`;
5774
+ const where = resolved.project.workspaceName ? `project "${resolved.project.name ?? resolved.project.id}" (workspace "${resolved.project.workspaceName}")` : `project "${resolved.project.name ?? resolved.project.id}"`;
5775
+ const established = activeProjectId == null;
5776
+ if (established) {
5777
+ deps.setActiveProject(resolved.project.id);
5778
+ }
5779
+ return {
5780
+ success: true,
5781
+ card: resolved.card,
5782
+ resolvedProject: resolved.project,
5783
+ activeProjectId: resolved.project.id,
5784
+ note: established ? `Resolved #${shortId} → "${cardTitle}" in ${where}. No active project was set — set to this for follow-up references.` : `Resolved #${shortId} → "${cardTitle}" in ${where} (your active project).`
5785
+ };
5786
+ }
5787
+ if (resolved.kind === "not_in_preferred") {
5788
+ const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
5789
+ `);
5790
+ throw new Error(`#${shortId} is not in your active project (projectId: ${resolved.preferredProjectId}). ` + `It exists in ${resolved.candidates.length} other project(s) you can access:
5791
+ ${list}
5792
+
5793
+ ` + `Switch with harmony_set_project_context, or pass an explicit projectId to fetch it directly.`);
5794
+ }
5795
+ if (resolved.kind === "ambiguous") {
5796
+ const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
5797
+ `);
5798
+ return {
5799
+ success: true,
5800
+ needsDisambiguation: true,
5801
+ shortId,
5802
+ candidates: resolved.candidates,
5803
+ message: `#${shortId} exists in ${resolved.candidates.length} projects you can access:
5804
+ ${list}
5805
+
5806
+ ` + `Ask which one is meant, then re-fetch with an explicit projectId ` + `(or call harmony_set_project_context first).`
5807
+ };
5808
+ }
5809
+ throw new Error(resolved.searchedProjectCount === 0 ? `#${shortId} can't be resolved: no project is accessible to this connection. ` + `Check the workspace this connection is authorized for with harmony_list_workspaces.` : `Card #${shortId} was not found in any of the ${resolved.searchedProjectCount} ` + `project(s) across ${resolved.searchedWorkspaceCount} workspace(s) this connection can access. ` + `Use harmony_list_projects to see them, or pass an explicit projectId.`);
5764
5810
  }
5765
5811
  const cardId = z.string().uuid().parse(args.cardId);
5766
5812
  const result = await client3.getCard(cardId);
@@ -1316,6 +1316,10 @@ class HarmonyApiClient {
1316
1316
  async getCardByShortId(projectId, shortId) {
1317
1317
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1318
1318
  }
1319
+ async resolveCardByShortId(shortId, preferredProjectId) {
1320
+ const qs = preferredProjectId ? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}` : "";
1321
+ return this.request("GET", `/cards/resolve/${shortId}${qs}`);
1322
+ }
1319
1323
  async bulkGetCards(projectId, shortIds) {
1320
1324
  return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1321
1325
  shortIds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.17.1",
3
+ "version": "2.19.0",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -64,7 +64,7 @@
64
64
  "serve:remote": "bun src/remote.ts",
65
65
  "dev": "bun --watch src/index.ts",
66
66
  "test": "bun run test:unit && bun run test:integration",
67
- "test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts src/__tests__/auto-session.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 src/__tests__/setup-confirm.test.ts",
68
68
  "test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
69
69
  "typecheck": "tsc --noEmit",
70
70
  "prepublishOnly": "bun run typecheck && bun run build"
package/src/api-client.ts CHANGED
@@ -164,6 +164,46 @@ export interface CardExternalLinkRow {
164
164
  created_at: string;
165
165
  }
166
166
 
167
+ /** One candidate card returned by the cross-project short-id resolver (#709). */
168
+ export interface ResolveCardCandidate {
169
+ cardId: string;
170
+ title: string;
171
+ projectId: string;
172
+ projectName: string | null;
173
+ workspaceId: string;
174
+ workspaceName: string | null;
175
+ }
176
+
177
+ /** Discriminated result of GET /cards/resolve/:shortId (#709). `found` carries
178
+ * the hydrated card + its project (for sticky-context + confirm-back);
179
+ * `ambiguous` carries the candidates so the caller asks which one; `not_found`
180
+ * carries the searched scope for a legible failure; `not_in_preferred` (#428)
181
+ * means a deliberately-set active project (the hard scope) doesn't hold the id,
182
+ * and carries the projects that DO so the caller can switch or pass an explicit
183
+ * projectId — the tool never silently hops to another project. */
184
+ export type ResolveCardApiResult =
185
+ | {
186
+ kind: "found";
187
+ card: unknown;
188
+ project: {
189
+ id: string;
190
+ name: string | null;
191
+ workspaceId: string;
192
+ workspaceName: string | null;
193
+ };
194
+ }
195
+ | { kind: "ambiguous"; candidates: ResolveCardCandidate[] }
196
+ | {
197
+ kind: "not_found";
198
+ searchedWorkspaceCount: number;
199
+ searchedProjectCount: number;
200
+ }
201
+ | {
202
+ kind: "not_in_preferred";
203
+ preferredProjectId: string;
204
+ candidates: ResolveCardCandidate[];
205
+ };
206
+
167
207
  /** Result of the classify-card classifier (card #415). Any field may be null
168
208
  * if the LLM didn't return a usable value. `model_override` is never touched. */
169
209
  export interface CardClassificationResult {
@@ -694,6 +734,21 @@ export class HarmonyApiClient {
694
734
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
695
735
  }
696
736
 
737
+ // #709: resolve a `#shortId` across every project the caller can reach when no
738
+ // explicit project is in play (the remote/OAuth MCP seeds a workspace but no
739
+ // project). `preferredProjectId` biases to the session's active/sticky project
740
+ // so a deliberately-set context wins outright instead of reading as ambiguous.
741
+ // Always resolves (HTTP 200) with a discriminated body — the caller decides.
742
+ async resolveCardByShortId(
743
+ shortId: number,
744
+ preferredProjectId?: string | null,
745
+ ): Promise<ResolveCardApiResult> {
746
+ const qs = preferredProjectId
747
+ ? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}`
748
+ : "";
749
+ return this.request("GET", `/cards/resolve/${shortId}${qs}`);
750
+ }
751
+
697
752
  async bulkGetCards(
698
753
  projectId: string,
699
754
  shortIds: number[],
package/src/cli.ts CHANGED
@@ -167,6 +167,10 @@ program
167
167
  .option("-p, --project <id>", "Set project context (UUID)")
168
168
  .option("--skip-context", "Skip workspace/project selection")
169
169
  .option("--skip-docs", "Skip project docs scaffold/verification")
170
+ .option(
171
+ "-y, --yes",
172
+ "Non-interactive: answer every yes/no confirmation with its default. Implied when there is no TTY (pipe / coding agent / CI). Provide the other inputs via flags (--api-key, --agents, --workspace/--project or --skip-context, --skip-docs).",
173
+ )
170
174
  .option("--new", "Create a new account (skip the choice prompt)")
171
175
  .option("-n, --name <name>", "Full name (for account creation)")
172
176
  .option(
@@ -192,6 +196,7 @@ program
192
196
  newAccount: options.new,
193
197
  name: options.name,
194
198
  allowAllTools: options.allowAllTools,
199
+ yes: options.yes,
195
200
  });
196
201
  });
197
202
 
package/src/server.ts CHANGED
@@ -2872,9 +2872,113 @@ async function handleToolCall(
2872
2872
  }
2873
2873
  if (hasShortId) {
2874
2874
  const shortId = z.number().int().positive().parse(args.shortId);
2875
- const projectId = (args.projectId as string) || getProjectId();
2876
- const result = await client.getCardByShortId(projectId, shortId);
2877
- return { success: true, ...result };
2875
+ const explicitProjectId = args.projectId as string | undefined;
2876
+
2877
+ // Explicit projectId is the deterministic override: fetch from exactly
2878
+ // that project and error if it's not there — never look elsewhere.
2879
+ if (explicitProjectId) {
2880
+ const result = await client.getCardByShortId(
2881
+ explicitProjectId,
2882
+ shortId,
2883
+ );
2884
+ return { success: true, ...result };
2885
+ }
2886
+
2887
+ // No explicit project: resolve the `#shortId` (#709). The active/sticky
2888
+ // project is a HARD SCOPE (#428) — when one is set, the resolver only
2889
+ // matches within it (a miss comes back as `not_in_preferred`, handled
2890
+ // below) and this read never repoints it. Cross-project auto-resolution
2891
+ // + sticky happens ONLY when no active project is set — the remote/OAuth
2892
+ // MCP case this feature exists for, where the session seeds a workspace
2893
+ // but no project.
2894
+ const activeProjectId = deps.getActiveProjectId();
2895
+ const resolved = await client.resolveCardByShortId(
2896
+ shortId,
2897
+ activeProjectId,
2898
+ );
2899
+
2900
+ if (resolved.kind === "found") {
2901
+ const cardTitle =
2902
+ (resolved.card as { title?: string } | null)?.title ??
2903
+ `#${shortId}`;
2904
+ const where = resolved.project.workspaceName
2905
+ ? `project "${resolved.project.name ?? resolved.project.id}" (workspace "${resolved.project.workspaceName}")`
2906
+ : `project "${resolved.project.name ?? resolved.project.id}"`;
2907
+ // Sticky ONLY when this ESTABLISHES a context (none was set). A
2908
+ // deliberately-set active project is a hard scope: `found` there means
2909
+ // the card was already in it, so there is nothing to change — and we
2910
+ // never let a read silently repoint a context the user chose (which,
2911
+ // on the local stdio MCP, persists to ~/.harmony-mcp/config.json
2912
+ // across sessions). Confirm the target back either way so a
2913
+ // wrong-context resolve is visible immediately.
2914
+ const established = activeProjectId == null;
2915
+ if (established) {
2916
+ deps.setActiveProject(resolved.project.id);
2917
+ }
2918
+ return {
2919
+ success: true,
2920
+ card: resolved.card,
2921
+ resolvedProject: resolved.project,
2922
+ activeProjectId: resolved.project.id,
2923
+ note: established
2924
+ ? `Resolved #${shortId} → "${cardTitle}" in ${where}. No active project was set — set to this for follow-up references.`
2925
+ : `Resolved #${shortId} → "${cardTitle}" in ${where} (your active project).`,
2926
+ };
2927
+ }
2928
+
2929
+ if (resolved.kind === "not_in_preferred") {
2930
+ // Hard scope (#428): the active project is a constraint the user set,
2931
+ // so a `#shortId` that isn't in it is an error — NOT a silent hop to
2932
+ // whatever other project happens to carry that number, and NOT a
2933
+ // change to the active project. Name where it *does* live so the
2934
+ // caller can switch context or fetch it explicitly.
2935
+ const list = resolved.candidates
2936
+ .map(
2937
+ (c) =>
2938
+ ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${
2939
+ c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""
2940
+ } (projectId: ${c.projectId})`,
2941
+ )
2942
+ .join("\n");
2943
+ throw new Error(
2944
+ `#${shortId} is not in your active project (projectId: ${resolved.preferredProjectId}). ` +
2945
+ `It exists in ${resolved.candidates.length} other project(s) you can access:\n${list}\n\n` +
2946
+ `Switch with harmony_set_project_context, or pass an explicit projectId to fetch it directly.`,
2947
+ );
2948
+ }
2949
+
2950
+ if (resolved.kind === "ambiguous") {
2951
+ // Never guess: hand the candidates back so the caller can disambiguate.
2952
+ const list = resolved.candidates
2953
+ .map(
2954
+ (c) =>
2955
+ ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${
2956
+ c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""
2957
+ } (projectId: ${c.projectId})`,
2958
+ )
2959
+ .join("\n");
2960
+ return {
2961
+ success: true,
2962
+ needsDisambiguation: true,
2963
+ shortId,
2964
+ candidates: resolved.candidates,
2965
+ message:
2966
+ `#${shortId} exists in ${resolved.candidates.length} projects you can access:\n${list}\n\n` +
2967
+ `Ask which one is meant, then re-fetch with an explicit projectId ` +
2968
+ `(or call harmony_set_project_context first).`,
2969
+ };
2970
+ }
2971
+
2972
+ // not_found — name the searched scope instead of the bare "No project
2973
+ // specified", and point at the tools that list the caller's options.
2974
+ throw new Error(
2975
+ resolved.searchedProjectCount === 0
2976
+ ? `#${shortId} can't be resolved: no project is accessible to this connection. ` +
2977
+ `Check the workspace this connection is authorized for with harmony_list_workspaces.`
2978
+ : `Card #${shortId} was not found in any of the ${resolved.searchedProjectCount} ` +
2979
+ `project(s) across ${resolved.searchedWorkspaceCount} workspace(s) this connection can access. ` +
2980
+ `Use harmony_list_projects to see them, or pass an explicit projectId.`,
2981
+ );
2878
2982
  }
2879
2983
  const cardId = z.string().uuid().parse(args.cardId);
2880
2984
  const result = await client.getCard(cardId);
@@ -0,0 +1,33 @@
1
+ import * as p from "@clack/prompts";
2
+
3
+ /**
4
+ * Whether yes/no confirmations should resolve to their default without
5
+ * prompting. True when the user passed `--yes`, OR when there is no interactive
6
+ * terminal on stdin (a pipe, a file, or nothing — how a coding agent or CI runs
7
+ * commands). In that second case `@clack/prompts` can't read a keypress and the
8
+ * confirm would otherwise stall or cancel the whole run, so we take the default.
9
+ *
10
+ * `isTTY` is `process.stdin.isTTY`, which Node sets to `true` only for a real
11
+ * TTY and leaves `undefined` otherwise — hence the `!== true` test.
12
+ */
13
+ export function shouldAssumeYes(
14
+ yesFlag: boolean | undefined,
15
+ isTTY: boolean | undefined,
16
+ ): boolean {
17
+ return yesFlag === true || isTTY !== true;
18
+ }
19
+
20
+ /**
21
+ * Resolve a confirmation. In non-interactive mode (see {@link shouldAssumeYes})
22
+ * it returns the prompt's documented default (`initialValue`, or `true` when
23
+ * unset) instead of blocking on a keypress that will never arrive. Interactive
24
+ * runs (a human at a TTY, no `--yes`) delegate to `p.confirm` and behave exactly
25
+ * as before, including returning the cancel symbol on Ctrl-C.
26
+ */
27
+ export async function confirmOrDefault(
28
+ assumeYes: boolean,
29
+ opts: { message: string; initialValue?: boolean },
30
+ ): Promise<boolean | symbol> {
31
+ if (assumeYes) return opts.initialValue ?? true;
32
+ return p.confirm(opts);
33
+ }
package/src/tui/setup.ts CHANGED
@@ -26,6 +26,7 @@ import { loginWithBrowser, type OAuthTokens } from "../oauth-login.js";
26
26
  import { onboardNewUser } from "../onboard.js";
27
27
  import { buildSkillFile, HARMONY_WORKFLOW_PROMPT } from "../skills.js";
28
28
  import { type AgentId, detectAgents } from "./agents.js";
29
+ import { confirmOrDefault, shouldAssumeYes } from "./confirm.js";
29
30
  import { runDocsStep } from "./docs.js";
30
31
  import { colors, formatPath, messages } from "./theme.js";
31
32
  import { getWriteSummary, writeFilesWithProgress } from "./writer.js";
@@ -46,6 +47,11 @@ export interface SetupOptions {
46
47
  newAccount?: boolean;
47
48
  name?: string;
48
49
  allowAllTools?: boolean;
50
+ /**
51
+ * Answer every yes/no confirmation with its default and skip the prompt.
52
+ * Also implied when stdin is not a TTY (pipe/agent/CI). See `./confirm.ts`.
53
+ */
54
+ yes?: boolean;
49
55
  }
50
56
 
51
57
  /**
@@ -723,6 +729,19 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
723
729
  console.clear();
724
730
  console.log(messages.header());
725
731
 
732
+ // Non-interactive mode: `--yes`, or no TTY (pipe / coding agent / CI). Every
733
+ // yes/no confirmation below resolves to its default instead of blocking on a
734
+ // keypress that will never arrive. Selection/text prompts are unaffected —
735
+ // suppress those with their own flags (--api-key, --agents, --skip-context…).
736
+ const assumeYes = shouldAssumeYes(options.yes, process.stdin.isTTY);
737
+ if (assumeYes) {
738
+ p.log.info(
739
+ options.yes
740
+ ? "Non-interactive mode (--yes): using the default answer for each confirmation."
741
+ : "No interactive terminal detected: using the default answer for each confirmation.",
742
+ );
743
+ }
744
+
726
745
  // Check existing configuration
727
746
  const existingConfig = loadConfig();
728
747
  const alreadyConfigured = isConfigured();
@@ -1005,7 +1024,7 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1005
1024
  if (skillsStatus.installed && !options.force) {
1006
1025
  p.log.success(`Skills already installed (${skillsStatus.location})`);
1007
1026
 
1008
- const reinstall = await p.confirm({
1027
+ const reinstall = await confirmOrDefault(assumeYes, {
1009
1028
  message: "Reinstall skills?",
1010
1029
  initialValue: false,
1011
1030
  });
@@ -1397,7 +1416,7 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1397
1416
  console.log("");
1398
1417
 
1399
1418
  // Step 6: Confirm and execute
1400
- const shouldProceed = await p.confirm({
1419
+ const shouldProceed = await confirmOrDefault(assumeYes, {
1401
1420
  message: "Proceed with setup?",
1402
1421
  initialValue: true,
1403
1422
  });
@@ -1486,7 +1505,10 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1486
1505
  const message = allowAll
1487
1506
  ? "Allowlist EVERY Harmony tool without confirmation, including destructive ones (delete/archive/api-key/invite)?"
1488
1507
  : "Allowlist common Harmony tools (reads + create/update/move/comment) so /hmy doesn't prompt each time? Destructive tools (delete/archive/api-key/invite) will still ask.";
1489
- const allowTools = await p.confirm({ message, initialValue: true });
1508
+ const allowTools = await confirmOrDefault(assumeYes, {
1509
+ message,
1510
+ initialValue: true,
1511
+ });
1490
1512
  if (p.isCancel(allowTools)) {
1491
1513
  p.cancel("Setup cancelled.");
1492
1514
  process.exit(0);