@gethmy/mcp 2.18.0 → 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 +96 -77
- package/package.json +2 -2
- package/src/cli.ts +5 -0
- package/src/tui/confirm.ts +33 -0
- package/src/tui/setup.ts +25 -3
package/dist/cli.js
CHANGED
|
@@ -7261,7 +7261,7 @@ import {
|
|
|
7261
7261
|
} from "node:fs";
|
|
7262
7262
|
import { homedir as homedir6 } from "node:os";
|
|
7263
7263
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
7264
|
-
import * as
|
|
7264
|
+
import * as p4 from "@clack/prompts";
|
|
7265
7265
|
init_config();
|
|
7266
7266
|
init_oauth_login();
|
|
7267
7267
|
|
|
@@ -7319,10 +7319,21 @@ function detectAgents(cwd = process.cwd()) {
|
|
|
7319
7319
|
});
|
|
7320
7320
|
}
|
|
7321
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
|
+
|
|
7322
7333
|
// src/tui/docs.ts
|
|
7323
7334
|
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
7324
7335
|
import { isAbsolute, join as join7, resolve, sep as sep2 } from "node:path";
|
|
7325
|
-
import * as
|
|
7336
|
+
import * as p2 from "@clack/prompts";
|
|
7326
7337
|
|
|
7327
7338
|
// src/tui/theme.ts
|
|
7328
7339
|
import pc from "picocolors";
|
|
@@ -7970,11 +7981,11 @@ async function runDocsStep(cwd) {
|
|
|
7970
7981
|
const info = scanProject(cwd);
|
|
7971
7982
|
const hasDocs = info.existingDocs.agentsMd || info.existingDocs.claudeMd;
|
|
7972
7983
|
if (!hasDocs) {
|
|
7973
|
-
const shouldGenerate = await
|
|
7984
|
+
const shouldGenerate = await p2.confirm({
|
|
7974
7985
|
message: "No project docs found. Generate AGENTS.md and CLAUDE.md?",
|
|
7975
7986
|
initialValue: true
|
|
7976
7987
|
});
|
|
7977
|
-
if (
|
|
7988
|
+
if (p2.isCancel(shouldGenerate) || !shouldGenerate) {
|
|
7978
7989
|
return { files: [], issues: [], skipped: true };
|
|
7979
7990
|
}
|
|
7980
7991
|
const files = [];
|
|
@@ -7995,32 +8006,32 @@ async function runDocsStep(cwd) {
|
|
|
7995
8006
|
type: "text"
|
|
7996
8007
|
});
|
|
7997
8008
|
}
|
|
7998
|
-
|
|
8009
|
+
p2.log.success(`Generated ${files.length} doc file(s): ${files.map((f) => f.path.replace(cwd + "/", "")).join(", ")}`);
|
|
7999
8010
|
return { files, issues: [], skipped: false };
|
|
8000
8011
|
}
|
|
8001
|
-
const shouldVerify = await
|
|
8012
|
+
const shouldVerify = await p2.confirm({
|
|
8002
8013
|
message: "Project docs found. Verify for issues?",
|
|
8003
8014
|
initialValue: false
|
|
8004
8015
|
});
|
|
8005
|
-
if (
|
|
8016
|
+
if (p2.isCancel(shouldVerify) || !shouldVerify) {
|
|
8006
8017
|
return { files: [], issues: [], skipped: true };
|
|
8007
8018
|
}
|
|
8008
8019
|
const issues = verifyDocs(cwd);
|
|
8009
8020
|
if (issues.length === 0) {
|
|
8010
|
-
|
|
8021
|
+
p2.log.success("No issues found in project docs.");
|
|
8011
8022
|
} else {
|
|
8012
8023
|
for (const issue of issues) {
|
|
8013
8024
|
const prefix = `${colors.bold(issue.file)}:`;
|
|
8014
8025
|
if (issue.severity === "error") {
|
|
8015
|
-
|
|
8026
|
+
p2.log.error(`${prefix} ${issue.message}`);
|
|
8016
8027
|
} else {
|
|
8017
|
-
|
|
8028
|
+
p2.log.warning(`${prefix} ${issue.message}`);
|
|
8018
8029
|
}
|
|
8019
8030
|
if (issue.fix) {
|
|
8020
|
-
|
|
8031
|
+
p2.log.message(` ${symbols.arrow} ${colors.dim(issue.fix)}`);
|
|
8021
8032
|
}
|
|
8022
8033
|
}
|
|
8023
|
-
|
|
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)`);
|
|
8024
8035
|
}
|
|
8025
8036
|
return { files: [], issues, skipped: false };
|
|
8026
8037
|
}
|
|
@@ -8035,7 +8046,7 @@ import {
|
|
|
8035
8046
|
} from "node:fs";
|
|
8036
8047
|
import { homedir as homedir5 } from "node:os";
|
|
8037
8048
|
import { dirname as dirname2 } from "node:path";
|
|
8038
|
-
import * as
|
|
8049
|
+
import * as p3 from "@clack/prompts";
|
|
8039
8050
|
function ensureDir(dirPath) {
|
|
8040
8051
|
if (!existsSync7(dirPath)) {
|
|
8041
8052
|
mkdirSync4(dirPath, { recursive: true, mode: 493 });
|
|
@@ -8155,7 +8166,7 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
8155
8166
|
async function writeFilesWithProgress(files, options = {}) {
|
|
8156
8167
|
const results = [];
|
|
8157
8168
|
const home = homedir5();
|
|
8158
|
-
const spinner2 =
|
|
8169
|
+
const spinner2 = p3.spinner();
|
|
8159
8170
|
spinner2.start("Writing configuration files...");
|
|
8160
8171
|
for (const file of files) {
|
|
8161
8172
|
let result;
|
|
@@ -8647,6 +8658,10 @@ async function runSetup(options = {}) {
|
|
|
8647
8658
|
const home = homedir6();
|
|
8648
8659
|
console.clear();
|
|
8649
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
|
+
}
|
|
8650
8665
|
const existingConfig = loadConfig();
|
|
8651
8666
|
const alreadyConfigured = isConfigured();
|
|
8652
8667
|
const skillsStatus = areSkillsInstalled(cwd);
|
|
@@ -8666,7 +8681,7 @@ async function runSetup(options = {}) {
|
|
|
8666
8681
|
let createdNewAccount = false;
|
|
8667
8682
|
let oauthTokens;
|
|
8668
8683
|
if (options.apiKey) {
|
|
8669
|
-
|
|
8684
|
+
p4.log.warn(colors.warning(`--api-key is deprecated and insecure: the key is exposed in your shell
|
|
8670
8685
|
history, terminal scrollback, and the process list. Prefer the browser
|
|
8671
8686
|
sign-in (run \`npx @gethmy/mcp setup\` with no --api-key). Use --api-key
|
|
8672
8687
|
only for unattended CI where you accept that risk.`));
|
|
@@ -8677,7 +8692,7 @@ only for unattended CI where you accept that risk.`));
|
|
|
8677
8692
|
if (!useNewAccount && options.apiKey) {
|
|
8678
8693
|
useNewAccount = false;
|
|
8679
8694
|
} else if (!useNewAccount && !options.apiKey) {
|
|
8680
|
-
const getStarted = await
|
|
8695
|
+
const getStarted = await p4.select({
|
|
8681
8696
|
message: "How would you like to connect?",
|
|
8682
8697
|
options: [
|
|
8683
8698
|
{
|
|
@@ -8698,15 +8713,15 @@ only for unattended CI where you accept that risk.`));
|
|
|
8698
8713
|
],
|
|
8699
8714
|
initialValue: "browser"
|
|
8700
8715
|
});
|
|
8701
|
-
if (
|
|
8702
|
-
|
|
8716
|
+
if (p4.isCancel(getStarted)) {
|
|
8717
|
+
p4.cancel("Setup cancelled");
|
|
8703
8718
|
process.exit(0);
|
|
8704
8719
|
}
|
|
8705
8720
|
useNewAccount = getStarted === "create";
|
|
8706
8721
|
useBrowserAuth = getStarted === "browser";
|
|
8707
8722
|
}
|
|
8708
8723
|
if (useBrowserAuth) {
|
|
8709
|
-
const spinner4 =
|
|
8724
|
+
const spinner4 = p4.spinner();
|
|
8710
8725
|
spinner4.start("Opening your browser to authorize…");
|
|
8711
8726
|
try {
|
|
8712
8727
|
oauthTokens = await loginWithBrowser({
|
|
@@ -8731,12 +8746,12 @@ ${colors.dim(url)}`);
|
|
|
8731
8746
|
} catch (error) {
|
|
8732
8747
|
spinner4.stop(colors.error("Browser authorization failed"));
|
|
8733
8748
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
8734
|
-
|
|
8735
|
-
|
|
8749
|
+
p4.log.error(msg);
|
|
8750
|
+
p4.log.info("You can retry, or run with --api-key for unattended setup.");
|
|
8736
8751
|
process.exit(1);
|
|
8737
8752
|
}
|
|
8738
8753
|
} else if (useNewAccount) {
|
|
8739
|
-
const fullName = options.name || await
|
|
8754
|
+
const fullName = options.name || await p4.text({
|
|
8740
8755
|
message: "Full name",
|
|
8741
8756
|
placeholder: "Jane Smith",
|
|
8742
8757
|
validate: (v) => {
|
|
@@ -8747,11 +8762,11 @@ ${colors.dim(url)}`);
|
|
|
8747
8762
|
return;
|
|
8748
8763
|
}
|
|
8749
8764
|
});
|
|
8750
|
-
if (
|
|
8751
|
-
|
|
8765
|
+
if (p4.isCancel(fullName)) {
|
|
8766
|
+
p4.cancel("Setup cancelled");
|
|
8752
8767
|
process.exit(0);
|
|
8753
8768
|
}
|
|
8754
|
-
const email = options.userEmail || await
|
|
8769
|
+
const email = options.userEmail || await p4.text({
|
|
8755
8770
|
message: "Email",
|
|
8756
8771
|
placeholder: "you@example.com",
|
|
8757
8772
|
validate: (v) => {
|
|
@@ -8764,11 +8779,11 @@ ${colors.dim(url)}`);
|
|
|
8764
8779
|
return;
|
|
8765
8780
|
}
|
|
8766
8781
|
});
|
|
8767
|
-
if (
|
|
8768
|
-
|
|
8782
|
+
if (p4.isCancel(email)) {
|
|
8783
|
+
p4.cancel("Setup cancelled");
|
|
8769
8784
|
process.exit(0);
|
|
8770
8785
|
}
|
|
8771
|
-
const password2 = await
|
|
8786
|
+
const password2 = await p4.password({
|
|
8772
8787
|
message: "Password",
|
|
8773
8788
|
validate: (v) => {
|
|
8774
8789
|
if (!v)
|
|
@@ -8780,11 +8795,11 @@ ${colors.dim(url)}`);
|
|
|
8780
8795
|
return;
|
|
8781
8796
|
}
|
|
8782
8797
|
});
|
|
8783
|
-
if (
|
|
8784
|
-
|
|
8798
|
+
if (p4.isCancel(password2)) {
|
|
8799
|
+
p4.cancel("Setup cancelled");
|
|
8785
8800
|
process.exit(0);
|
|
8786
8801
|
}
|
|
8787
|
-
const spinner4 =
|
|
8802
|
+
const spinner4 = p4.spinner();
|
|
8788
8803
|
spinner4.start("Creating your account...");
|
|
8789
8804
|
try {
|
|
8790
8805
|
const result = await onboardNewUser({
|
|
@@ -8804,15 +8819,15 @@ ${colors.dim(url)}`);
|
|
|
8804
8819
|
saveConfig({ apiKey, userEmail, apiUrl: API_URL });
|
|
8805
8820
|
setActiveWorkspace(selectedWorkspaceIdFromSignup);
|
|
8806
8821
|
setActiveProject(selectedProjectIdFromSignup);
|
|
8807
|
-
|
|
8822
|
+
p4.log.success("Workspace and board created");
|
|
8808
8823
|
} catch (error) {
|
|
8809
8824
|
spinner4.stop(colors.error("Account creation failed"));
|
|
8810
8825
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
8811
8826
|
if (msg.includes("already") || msg.includes("409")) {
|
|
8812
|
-
|
|
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'.");
|
|
8813
8828
|
} else {
|
|
8814
|
-
|
|
8815
|
-
|
|
8829
|
+
p4.log.error(msg);
|
|
8830
|
+
p4.log.info("Please try again or visit https://app.gethmy.com");
|
|
8816
8831
|
}
|
|
8817
8832
|
process.exit(1);
|
|
8818
8833
|
}
|
|
@@ -8820,7 +8835,7 @@ ${colors.dim(url)}`);
|
|
|
8820
8835
|
apiKey = options.apiKey;
|
|
8821
8836
|
needsApiKey = true;
|
|
8822
8837
|
} else {
|
|
8823
|
-
const keyInput = await
|
|
8838
|
+
const keyInput = await p4.text({
|
|
8824
8839
|
message: "Enter your Harmony API key",
|
|
8825
8840
|
placeholder: "hmy_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
8826
8841
|
validate: (value) => {
|
|
@@ -8833,24 +8848,24 @@ ${colors.dim(url)}`);
|
|
|
8833
8848
|
return;
|
|
8834
8849
|
}
|
|
8835
8850
|
});
|
|
8836
|
-
if (
|
|
8837
|
-
|
|
8851
|
+
if (p4.isCancel(keyInput)) {
|
|
8852
|
+
p4.cancel("Setup cancelled");
|
|
8838
8853
|
process.exit(0);
|
|
8839
8854
|
}
|
|
8840
8855
|
apiKey = keyInput;
|
|
8841
8856
|
needsApiKey = true;
|
|
8842
8857
|
}
|
|
8843
8858
|
} else {
|
|
8844
|
-
|
|
8859
|
+
p4.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
|
|
8845
8860
|
}
|
|
8846
|
-
const spinner3 =
|
|
8861
|
+
const spinner3 = p4.spinner();
|
|
8847
8862
|
if (!createdNewAccount) {
|
|
8848
8863
|
spinner3.start("Validating API key...");
|
|
8849
8864
|
const validation = await validateApiKey(apiKey);
|
|
8850
8865
|
if (!validation.valid) {
|
|
8851
8866
|
spinner3.stop(colors.error("API key validation failed"));
|
|
8852
|
-
|
|
8853
|
-
|
|
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");
|
|
8854
8869
|
process.exit(1);
|
|
8855
8870
|
}
|
|
8856
8871
|
if (!userEmail) {
|
|
@@ -8861,13 +8876,13 @@ ${colors.dim(url)}`);
|
|
|
8861
8876
|
let selectedAgents = [];
|
|
8862
8877
|
let installMode = options.installMode || "global";
|
|
8863
8878
|
if (skillsStatus.installed && !options.force) {
|
|
8864
|
-
|
|
8865
|
-
const reinstall = await
|
|
8879
|
+
p4.log.success(`Skills already installed (${skillsStatus.location})`);
|
|
8880
|
+
const reinstall = await confirmOrDefault(assumeYes, {
|
|
8866
8881
|
message: "Reinstall skills?",
|
|
8867
8882
|
initialValue: false
|
|
8868
8883
|
});
|
|
8869
|
-
if (
|
|
8870
|
-
|
|
8884
|
+
if (p4.isCancel(reinstall)) {
|
|
8885
|
+
p4.cancel("Setup cancelled");
|
|
8871
8886
|
process.exit(0);
|
|
8872
8887
|
}
|
|
8873
8888
|
needsSkills = reinstall;
|
|
@@ -8882,23 +8897,23 @@ ${colors.dim(url)}`);
|
|
|
8882
8897
|
label: agent.name,
|
|
8883
8898
|
hint: agent.detected ? colors.success(`${agent.description} (detected)`) : colors.dim(`${agent.description}`)
|
|
8884
8899
|
}));
|
|
8885
|
-
const agentSelection = await
|
|
8900
|
+
const agentSelection = await p4.multiselect({
|
|
8886
8901
|
message: "Select agents to configure",
|
|
8887
8902
|
options: agentOptions,
|
|
8888
8903
|
initialValues: detectedAgents2.filter((a) => a.detected).map((a) => a.id),
|
|
8889
8904
|
required: true
|
|
8890
8905
|
});
|
|
8891
|
-
if (
|
|
8892
|
-
|
|
8906
|
+
if (p4.isCancel(agentSelection)) {
|
|
8907
|
+
p4.cancel("Setup cancelled");
|
|
8893
8908
|
process.exit(0);
|
|
8894
8909
|
}
|
|
8895
8910
|
selectedAgents = agentSelection;
|
|
8896
8911
|
}
|
|
8897
8912
|
if (selectedAgents.length === 0) {
|
|
8898
|
-
|
|
8913
|
+
p4.log.warning("No agents selected. Skipping skills installation.");
|
|
8899
8914
|
needsSkills = false;
|
|
8900
8915
|
} else if (!options.installMode) {
|
|
8901
|
-
const modeSelection = await
|
|
8916
|
+
const modeSelection = await p4.select({
|
|
8902
8917
|
message: "Where should Harmony skills be installed?",
|
|
8903
8918
|
options: [
|
|
8904
8919
|
{
|
|
@@ -8914,8 +8929,8 @@ ${colors.dim(url)}`);
|
|
|
8914
8929
|
],
|
|
8915
8930
|
initialValue: "global"
|
|
8916
8931
|
});
|
|
8917
|
-
if (
|
|
8918
|
-
|
|
8932
|
+
if (p4.isCancel(modeSelection)) {
|
|
8933
|
+
p4.cancel("Setup cancelled");
|
|
8919
8934
|
process.exit(0);
|
|
8920
8935
|
}
|
|
8921
8936
|
installMode = modeSelection;
|
|
@@ -8939,7 +8954,7 @@ ${colors.dim(url)}`);
|
|
|
8939
8954
|
spinner3.stop(colors.warning(`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`));
|
|
8940
8955
|
const list = resolved.candidates.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`).join(`
|
|
8941
8956
|
`);
|
|
8942
|
-
|
|
8957
|
+
p4.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
|
|
8943
8958
|
${list}
|
|
8944
8959
|
Specify the workspace with --workspace <id>, or select one below.`);
|
|
8945
8960
|
} else {
|
|
@@ -8960,7 +8975,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
8960
8975
|
spinner3.stop(colors.success(`Found ${workspaces.length} workspace(s)`));
|
|
8961
8976
|
} catch (_error) {
|
|
8962
8977
|
spinner3.stop(colors.warning("Could not fetch workspaces"));
|
|
8963
|
-
|
|
8978
|
+
p4.log.warning("Skipping workspace/project selection. You can set this later.");
|
|
8964
8979
|
needsContext = false;
|
|
8965
8980
|
}
|
|
8966
8981
|
if (needsContext && workspaces.length > 0) {
|
|
@@ -8972,12 +8987,12 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
8972
8987
|
value: ws.id,
|
|
8973
8988
|
label: ws.name
|
|
8974
8989
|
}));
|
|
8975
|
-
const workspaceSelection = await
|
|
8990
|
+
const workspaceSelection = await p4.select({
|
|
8976
8991
|
message: candidateIds.size > 0 ? `Select workspace for "${options.projectSlug}"` : "Select workspace",
|
|
8977
8992
|
options: workspaceOptions
|
|
8978
8993
|
});
|
|
8979
|
-
if (
|
|
8980
|
-
|
|
8994
|
+
if (p4.isCancel(workspaceSelection)) {
|
|
8995
|
+
p4.cancel("Setup cancelled");
|
|
8981
8996
|
process.exit(0);
|
|
8982
8997
|
}
|
|
8983
8998
|
selectedWorkspaceId = workspaceSelection;
|
|
@@ -8996,7 +9011,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
8996
9011
|
spinner3.stop(colors.success(`Found ${projects.length} project(s)`));
|
|
8997
9012
|
} catch (_error) {
|
|
8998
9013
|
spinner3.stop(colors.warning("Could not fetch projects"));
|
|
8999
|
-
|
|
9014
|
+
p4.log.warning("Skipping project selection. You can set this later.");
|
|
9000
9015
|
}
|
|
9001
9016
|
if (projects.length > 0 && !selectedProjectId) {
|
|
9002
9017
|
const projectOptions = projects.map((proj) => ({
|
|
@@ -9004,18 +9019,18 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9004
9019
|
label: proj.name,
|
|
9005
9020
|
hint: proj.description ? colors.dim(proj.description.slice(0, 50)) : undefined
|
|
9006
9021
|
}));
|
|
9007
|
-
const projectSelection = await
|
|
9022
|
+
const projectSelection = await p4.select({
|
|
9008
9023
|
message: "Select project",
|
|
9009
9024
|
options: projectOptions
|
|
9010
9025
|
});
|
|
9011
|
-
if (
|
|
9012
|
-
|
|
9026
|
+
if (p4.isCancel(projectSelection)) {
|
|
9027
|
+
p4.cancel("Setup cancelled");
|
|
9013
9028
|
process.exit(0);
|
|
9014
9029
|
}
|
|
9015
9030
|
selectedProjectId = projectSelection;
|
|
9016
|
-
selectedProjectName = projects.find((
|
|
9031
|
+
selectedProjectName = projects.find((p5) => p5.id === selectedProjectId)?.name;
|
|
9017
9032
|
} else if (selectedProjectId && !selectedProjectName) {
|
|
9018
|
-
selectedProjectName = projects.find((
|
|
9033
|
+
selectedProjectName = projects.find((p5) => p5.id === selectedProjectId)?.name;
|
|
9019
9034
|
}
|
|
9020
9035
|
}
|
|
9021
9036
|
}
|
|
@@ -9062,7 +9077,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9062
9077
|
}
|
|
9063
9078
|
const detectedAgents = detectAgents(cwd);
|
|
9064
9079
|
console.log("");
|
|
9065
|
-
|
|
9080
|
+
p4.log.step("Summary");
|
|
9066
9081
|
console.log("");
|
|
9067
9082
|
if (oauthTokens) {
|
|
9068
9083
|
console.log(` ${colors.bold("Credential:")} Browser sign-in (OAuth, workspace-scoped)`);
|
|
@@ -9118,12 +9133,12 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9118
9133
|
}
|
|
9119
9134
|
}
|
|
9120
9135
|
console.log("");
|
|
9121
|
-
const shouldProceed = await
|
|
9136
|
+
const shouldProceed = await confirmOrDefault(assumeYes, {
|
|
9122
9137
|
message: "Proceed with setup?",
|
|
9123
9138
|
initialValue: true
|
|
9124
9139
|
});
|
|
9125
|
-
if (
|
|
9126
|
-
|
|
9140
|
+
if (p4.isCancel(shouldProceed) || !shouldProceed) {
|
|
9141
|
+
p4.cancel("Setup cancelled");
|
|
9127
9142
|
process.exit(0);
|
|
9128
9143
|
}
|
|
9129
9144
|
console.log("");
|
|
@@ -9153,7 +9168,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9153
9168
|
}
|
|
9154
9169
|
symlinkSync(symlink.target, symlink.link);
|
|
9155
9170
|
} catch {
|
|
9156
|
-
|
|
9171
|
+
p4.log.warning(`Failed to create symlink: ${symlink.link}`);
|
|
9157
9172
|
}
|
|
9158
9173
|
}
|
|
9159
9174
|
}
|
|
@@ -9167,16 +9182,19 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9167
9182
|
await writeMcpConfigFallback(home);
|
|
9168
9183
|
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
|
|
9169
9184
|
} catch {
|
|
9170
|
-
|
|
9185
|
+
p4.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
|
|
9171
9186
|
}
|
|
9172
9187
|
}
|
|
9173
9188
|
}
|
|
9174
9189
|
if (claudeDetected || selectedAgents.includes("claude")) {
|
|
9175
9190
|
const allowAll = options.allowAllTools === true;
|
|
9176
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.";
|
|
9177
|
-
const allowTools = await
|
|
9178
|
-
|
|
9179
|
-
|
|
9192
|
+
const allowTools = await confirmOrDefault(assumeYes, {
|
|
9193
|
+
message,
|
|
9194
|
+
initialValue: true
|
|
9195
|
+
});
|
|
9196
|
+
if (p4.isCancel(allowTools)) {
|
|
9197
|
+
p4.cancel("Setup cancelled.");
|
|
9180
9198
|
process.exit(0);
|
|
9181
9199
|
}
|
|
9182
9200
|
if (allowTools) {
|
|
@@ -9185,7 +9203,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9185
9203
|
const scope = allowAll ? "all tools" : "safe tools";
|
|
9186
9204
|
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
|
|
9187
9205
|
} catch {
|
|
9188
|
-
|
|
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.");
|
|
9189
9207
|
}
|
|
9190
9208
|
} else {
|
|
9191
9209
|
console.log(` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`);
|
|
@@ -9205,7 +9223,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9205
9223
|
setActiveProject(selectedProjectId);
|
|
9206
9224
|
}
|
|
9207
9225
|
console.log("");
|
|
9208
|
-
|
|
9226
|
+
p4.outro(colors.success("Setup complete!"));
|
|
9209
9227
|
if (createdNewAccount && selectedWorkspaceNameFromSignup) {
|
|
9210
9228
|
const wsSlug = selectedWorkspaceNameFromSignup.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
9211
9229
|
const projSlug = (selectedProjectNameFromSignup || "my-first-board").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
@@ -9338,7 +9356,7 @@ program.command("reset").description("Remove stored configuration").action(() =>
|
|
|
9338
9356
|
console.log(`
|
|
9339
9357
|
To reconfigure, run: npx @gethmy/mcp setup`);
|
|
9340
9358
|
});
|
|
9341
|
-
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) => {
|
|
9342
9360
|
await runSetup({
|
|
9343
9361
|
force: options.force,
|
|
9344
9362
|
apiKey: options.apiKey,
|
|
@@ -9352,7 +9370,8 @@ program.command("setup").description("Smart setup wizard for Harmony MCP (recomm
|
|
|
9352
9370
|
skipDocs: options.skipDocs,
|
|
9353
9371
|
newAccount: options.new,
|
|
9354
9372
|
name: options.name,
|
|
9355
|
-
allowAllTools: options.allowAllTools
|
|
9373
|
+
allowAllTools: options.allowAllTools,
|
|
9374
|
+
yes: options.yes
|
|
9356
9375
|
});
|
|
9357
9376
|
});
|
|
9358
9377
|
program.parse();
|
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/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
|
|
|
@@ -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);
|