@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 +145 -80
- package/dist/index.js +49 -3
- package/dist/lib/api-client.js +4 -0
- package/package.json +2 -2
- package/src/api-client.ts +55 -0
- package/src/cli.ts +5 -0
- package/src/server.ts +107 -3
- package/src/tui/confirm.ts +33 -0
- package/src/tui/setup.ts +25 -3
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
|
|
5767
|
-
|
|
5768
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
8012
|
+
const shouldVerify = await p2.confirm({
|
|
7956
8013
|
message: "Project docs found. Verify for issues?",
|
|
7957
8014
|
initialValue: false
|
|
7958
8015
|
});
|
|
7959
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
8026
|
+
p2.log.error(`${prefix} ${issue.message}`);
|
|
7970
8027
|
} else {
|
|
7971
|
-
|
|
8028
|
+
p2.log.warning(`${prefix} ${issue.message}`);
|
|
7972
8029
|
}
|
|
7973
8030
|
if (issue.fix) {
|
|
7974
|
-
|
|
8031
|
+
p2.log.message(` ${symbols.arrow} ${colors.dim(issue.fix)}`);
|
|
7975
8032
|
}
|
|
7976
8033
|
}
|
|
7977
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (
|
|
8656
|
-
|
|
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 =
|
|
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
|
-
|
|
8689
|
-
|
|
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
|
|
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 (
|
|
8705
|
-
|
|
8765
|
+
if (p4.isCancel(fullName)) {
|
|
8766
|
+
p4.cancel("Setup cancelled");
|
|
8706
8767
|
process.exit(0);
|
|
8707
8768
|
}
|
|
8708
|
-
const email = options.userEmail || await
|
|
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 (
|
|
8722
|
-
|
|
8782
|
+
if (p4.isCancel(email)) {
|
|
8783
|
+
p4.cancel("Setup cancelled");
|
|
8723
8784
|
process.exit(0);
|
|
8724
8785
|
}
|
|
8725
|
-
const password2 = await
|
|
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 (
|
|
8738
|
-
|
|
8798
|
+
if (p4.isCancel(password2)) {
|
|
8799
|
+
p4.cancel("Setup cancelled");
|
|
8739
8800
|
process.exit(0);
|
|
8740
8801
|
}
|
|
8741
|
-
const spinner4 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8769
|
-
|
|
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
|
|
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 (
|
|
8791
|
-
|
|
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
|
-
|
|
8859
|
+
p4.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
|
|
8799
8860
|
}
|
|
8800
|
-
const spinner3 =
|
|
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
|
-
|
|
8807
|
-
|
|
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
|
-
|
|
8819
|
-
const reinstall = await
|
|
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 (
|
|
8824
|
-
|
|
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
|
|
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 (
|
|
8846
|
-
|
|
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
|
-
|
|
8913
|
+
p4.log.warning("No agents selected. Skipping skills installation.");
|
|
8853
8914
|
needsSkills = false;
|
|
8854
8915
|
} else if (!options.installMode) {
|
|
8855
|
-
const modeSelection = await
|
|
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 (
|
|
8872
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
8934
|
-
|
|
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
|
-
|
|
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
|
|
9022
|
+
const projectSelection = await p4.select({
|
|
8962
9023
|
message: "Select project",
|
|
8963
9024
|
options: projectOptions
|
|
8964
9025
|
});
|
|
8965
|
-
if (
|
|
8966
|
-
|
|
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((
|
|
9031
|
+
selectedProjectName = projects.find((p5) => p5.id === selectedProjectId)?.name;
|
|
8971
9032
|
} else if (selectedProjectId && !selectedProjectName) {
|
|
8972
|
-
selectedProjectName = projects.find((
|
|
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
|
-
|
|
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
|
|
9136
|
+
const shouldProceed = await confirmOrDefault(assumeYes, {
|
|
9076
9137
|
message: "Proceed with setup?",
|
|
9077
9138
|
initialValue: true
|
|
9078
9139
|
});
|
|
9079
|
-
if (
|
|
9080
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9132
|
-
|
|
9133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5762
|
-
|
|
5763
|
-
|
|
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);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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.
|
|
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
|
|
2876
|
-
|
|
2877
|
-
|
|
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
|
|
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
|
|
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
|
|
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);
|