@gethmy/mcp 2.18.0 → 2.20.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 +195 -110
- package/dist/index.js +99 -33
- package/package.json +2 -2
- package/src/cli.ts +5 -0
- package/src/server.ts +168 -48
- package/src/tui/confirm.ts +33 -0
- package/src/tui/setup.ts +25 -3
package/dist/cli.js
CHANGED
|
@@ -3644,6 +3644,43 @@ async function putToSignedUrl(uploadUrl, bytes, contentType) {
|
|
|
3644
3644
|
throw new Error(`Direct storage upload failed: ${res.status}${detail ? ` — ${detail}` : ""}`);
|
|
3645
3645
|
}
|
|
3646
3646
|
}
|
|
3647
|
+
async function attachFileToCard(client3, cardId, file) {
|
|
3648
|
+
const { filePath, base64Data, fileName, contentType } = file;
|
|
3649
|
+
if (filePath && base64Data) {
|
|
3650
|
+
throw new Error("Provide either filePath or base64Data, not both.");
|
|
3651
|
+
}
|
|
3652
|
+
if (filePath) {
|
|
3653
|
+
const bytes = await readFileForUpload(filePath, MAX_ATTACHMENT_SIZE, "attachment");
|
|
3654
|
+
const resolvedName = fileName || basename(filePath);
|
|
3655
|
+
const signed = await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
3656
|
+
fileName: resolvedName,
|
|
3657
|
+
fileType: contentType,
|
|
3658
|
+
size: bytes.byteLength
|
|
3659
|
+
});
|
|
3660
|
+
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
3661
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
3662
|
+
storagePath: signed.storagePath,
|
|
3663
|
+
fileName: resolvedName,
|
|
3664
|
+
fileType: contentType || signed.fileType,
|
|
3665
|
+
sha256: sha256Hex(bytes),
|
|
3666
|
+
size: bytes.byteLength
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
if (base64Data) {
|
|
3670
|
+
if (!fileName) {
|
|
3671
|
+
throw new Error("fileName is required when using base64Data.");
|
|
3672
|
+
}
|
|
3673
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
3674
|
+
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
3675
|
+
}
|
|
3676
|
+
return await client3.uploadCardAttachment(cardId, {
|
|
3677
|
+
fileName,
|
|
3678
|
+
data: base64Data,
|
|
3679
|
+
fileType: contentType
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
3683
|
+
}
|
|
3647
3684
|
var memorySessions = new Map;
|
|
3648
3685
|
function parseLabelList(raw) {
|
|
3649
3686
|
if (raw === undefined || raw === null)
|
|
@@ -3800,7 +3837,7 @@ function cleanupMemorySession(cardId) {
|
|
|
3800
3837
|
}
|
|
3801
3838
|
var TOOLS = {
|
|
3802
3839
|
harmony_create_card: {
|
|
3803
|
-
description: "Create a new card in a Kanban column",
|
|
3840
|
+
description: "Create a new card in a Kanban column. Optionally attach reference files " + "(e.g. a screenshot from the prompt) at creation time via `attachments` — " + "the card is created first, then each file is uploaded to it.",
|
|
3804
3841
|
inputSchema: {
|
|
3805
3842
|
type: "object",
|
|
3806
3843
|
properties: {
|
|
@@ -3823,6 +3860,31 @@ var TOOLS = {
|
|
|
3823
3860
|
planId: {
|
|
3824
3861
|
type: "string",
|
|
3825
3862
|
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
3863
|
+
},
|
|
3864
|
+
attachments: {
|
|
3865
|
+
type: "array",
|
|
3866
|
+
description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
3867
|
+
items: {
|
|
3868
|
+
type: "object",
|
|
3869
|
+
properties: {
|
|
3870
|
+
filePath: {
|
|
3871
|
+
type: "string",
|
|
3872
|
+
description: "Absolute path to a local file the server can read. Mutually exclusive with base64Data."
|
|
3873
|
+
},
|
|
3874
|
+
base64Data: {
|
|
3875
|
+
type: "string",
|
|
3876
|
+
description: "Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath."
|
|
3877
|
+
},
|
|
3878
|
+
fileName: {
|
|
3879
|
+
type: "string",
|
|
3880
|
+
description: "File name including extension (required with base64Data; else defaults to the filePath basename)."
|
|
3881
|
+
},
|
|
3882
|
+
contentType: {
|
|
3883
|
+
type: "string",
|
|
3884
|
+
description: "Optional MIME type (inferred from the extension when omitted)."
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3826
3888
|
}
|
|
3827
3889
|
},
|
|
3828
3890
|
required: ["title"]
|
|
@@ -5637,6 +5699,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5637
5699
|
case "harmony_create_card": {
|
|
5638
5700
|
const title = z.string().min(1).max(500).parse(args.title);
|
|
5639
5701
|
const projectId = args.projectId || getProjectId();
|
|
5702
|
+
const attachments = args.attachments != null ? z.array(z.object({
|
|
5703
|
+
filePath: z.string().optional(),
|
|
5704
|
+
base64Data: z.string().optional(),
|
|
5705
|
+
fileName: z.string().optional(),
|
|
5706
|
+
contentType: z.string().optional()
|
|
5707
|
+
})).parse(args.attachments) : [];
|
|
5640
5708
|
const result = await client3.createCard(projectId, {
|
|
5641
5709
|
title,
|
|
5642
5710
|
columnId: args.columnId,
|
|
@@ -5645,7 +5713,30 @@ async function handleToolCall(name, args, deps) {
|
|
|
5645
5713
|
assigneeId: args.assigneeId,
|
|
5646
5714
|
planId: args.planId
|
|
5647
5715
|
});
|
|
5648
|
-
|
|
5716
|
+
if (attachments.length === 0) {
|
|
5717
|
+
return { success: true, ...result };
|
|
5718
|
+
}
|
|
5719
|
+
const cardId = result.card?.id;
|
|
5720
|
+
if (!cardId) {
|
|
5721
|
+
return {
|
|
5722
|
+
success: true,
|
|
5723
|
+
...result,
|
|
5724
|
+
attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
const attachmentResults = await Promise.all(attachments.map(async (file) => {
|
|
5728
|
+
try {
|
|
5729
|
+
const uploaded = await attachFileToCard(client3, cardId, file);
|
|
5730
|
+
return { ok: true, attachment: uploaded.attachment };
|
|
5731
|
+
} catch (err) {
|
|
5732
|
+
return {
|
|
5733
|
+
ok: false,
|
|
5734
|
+
fileName: file.fileName ?? file.filePath ?? "(unnamed)",
|
|
5735
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5736
|
+
};
|
|
5737
|
+
}
|
|
5738
|
+
}));
|
|
5739
|
+
return { success: true, ...result, attachments: attachmentResults };
|
|
5649
5740
|
}
|
|
5650
5741
|
case "harmony_update_card": {
|
|
5651
5742
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -5931,37 +6022,12 @@ ${list}
|
|
|
5931
6022
|
const cardId2 = z.string().uuid().parse(args.cardId);
|
|
5932
6023
|
const fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
|
|
5933
6024
|
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
size: bytes.byteLength
|
|
5941
|
-
});
|
|
5942
|
-
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
5943
|
-
return await client3.finalizeCardAttachment(cardId2, {
|
|
5944
|
-
storagePath: signed.storagePath,
|
|
5945
|
-
fileName: resolvedName,
|
|
5946
|
-
fileType: contentType || signed.fileType,
|
|
5947
|
-
sha256: sha256Hex(bytes),
|
|
5948
|
-
size: bytes.byteLength
|
|
5949
|
-
});
|
|
5950
|
-
}
|
|
5951
|
-
if (base64Data) {
|
|
5952
|
-
if (!fileName) {
|
|
5953
|
-
throw new Error("fileName is required when using base64Data.");
|
|
5954
|
-
}
|
|
5955
|
-
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
5956
|
-
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
5957
|
-
}
|
|
5958
|
-
return await client3.uploadCardAttachment(cardId2, {
|
|
5959
|
-
fileName,
|
|
5960
|
-
data: base64Data,
|
|
5961
|
-
fileType: contentType
|
|
5962
|
-
});
|
|
5963
|
-
}
|
|
5964
|
-
throw new Error("Provide either filePath or base64Data.");
|
|
6025
|
+
return await attachFileToCard(client3, cardId2, {
|
|
6026
|
+
filePath,
|
|
6027
|
+
base64Data,
|
|
6028
|
+
fileName,
|
|
6029
|
+
contentType
|
|
6030
|
+
});
|
|
5965
6031
|
}
|
|
5966
6032
|
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5967
6033
|
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
@@ -7261,7 +7327,7 @@ import {
|
|
|
7261
7327
|
} from "node:fs";
|
|
7262
7328
|
import { homedir as homedir6 } from "node:os";
|
|
7263
7329
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
7264
|
-
import * as
|
|
7330
|
+
import * as p4 from "@clack/prompts";
|
|
7265
7331
|
init_config();
|
|
7266
7332
|
init_oauth_login();
|
|
7267
7333
|
|
|
@@ -7319,10 +7385,21 @@ function detectAgents(cwd = process.cwd()) {
|
|
|
7319
7385
|
});
|
|
7320
7386
|
}
|
|
7321
7387
|
|
|
7388
|
+
// src/tui/confirm.ts
|
|
7389
|
+
import * as p from "@clack/prompts";
|
|
7390
|
+
function shouldAssumeYes(yesFlag, isTTY) {
|
|
7391
|
+
return yesFlag === true || isTTY !== true;
|
|
7392
|
+
}
|
|
7393
|
+
async function confirmOrDefault(assumeYes, opts) {
|
|
7394
|
+
if (assumeYes)
|
|
7395
|
+
return opts.initialValue ?? true;
|
|
7396
|
+
return p.confirm(opts);
|
|
7397
|
+
}
|
|
7398
|
+
|
|
7322
7399
|
// src/tui/docs.ts
|
|
7323
7400
|
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
7324
7401
|
import { isAbsolute, join as join7, resolve, sep as sep2 } from "node:path";
|
|
7325
|
-
import * as
|
|
7402
|
+
import * as p2 from "@clack/prompts";
|
|
7326
7403
|
|
|
7327
7404
|
// src/tui/theme.ts
|
|
7328
7405
|
import pc from "picocolors";
|
|
@@ -7970,11 +8047,11 @@ async function runDocsStep(cwd) {
|
|
|
7970
8047
|
const info = scanProject(cwd);
|
|
7971
8048
|
const hasDocs = info.existingDocs.agentsMd || info.existingDocs.claudeMd;
|
|
7972
8049
|
if (!hasDocs) {
|
|
7973
|
-
const shouldGenerate = await
|
|
8050
|
+
const shouldGenerate = await p2.confirm({
|
|
7974
8051
|
message: "No project docs found. Generate AGENTS.md and CLAUDE.md?",
|
|
7975
8052
|
initialValue: true
|
|
7976
8053
|
});
|
|
7977
|
-
if (
|
|
8054
|
+
if (p2.isCancel(shouldGenerate) || !shouldGenerate) {
|
|
7978
8055
|
return { files: [], issues: [], skipped: true };
|
|
7979
8056
|
}
|
|
7980
8057
|
const files = [];
|
|
@@ -7995,32 +8072,32 @@ async function runDocsStep(cwd) {
|
|
|
7995
8072
|
type: "text"
|
|
7996
8073
|
});
|
|
7997
8074
|
}
|
|
7998
|
-
|
|
8075
|
+
p2.log.success(`Generated ${files.length} doc file(s): ${files.map((f) => f.path.replace(cwd + "/", "")).join(", ")}`);
|
|
7999
8076
|
return { files, issues: [], skipped: false };
|
|
8000
8077
|
}
|
|
8001
|
-
const shouldVerify = await
|
|
8078
|
+
const shouldVerify = await p2.confirm({
|
|
8002
8079
|
message: "Project docs found. Verify for issues?",
|
|
8003
8080
|
initialValue: false
|
|
8004
8081
|
});
|
|
8005
|
-
if (
|
|
8082
|
+
if (p2.isCancel(shouldVerify) || !shouldVerify) {
|
|
8006
8083
|
return { files: [], issues: [], skipped: true };
|
|
8007
8084
|
}
|
|
8008
8085
|
const issues = verifyDocs(cwd);
|
|
8009
8086
|
if (issues.length === 0) {
|
|
8010
|
-
|
|
8087
|
+
p2.log.success("No issues found in project docs.");
|
|
8011
8088
|
} else {
|
|
8012
8089
|
for (const issue of issues) {
|
|
8013
8090
|
const prefix = `${colors.bold(issue.file)}:`;
|
|
8014
8091
|
if (issue.severity === "error") {
|
|
8015
|
-
|
|
8092
|
+
p2.log.error(`${prefix} ${issue.message}`);
|
|
8016
8093
|
} else {
|
|
8017
|
-
|
|
8094
|
+
p2.log.warning(`${prefix} ${issue.message}`);
|
|
8018
8095
|
}
|
|
8019
8096
|
if (issue.fix) {
|
|
8020
|
-
|
|
8097
|
+
p2.log.message(` ${symbols.arrow} ${colors.dim(issue.fix)}`);
|
|
8021
8098
|
}
|
|
8022
8099
|
}
|
|
8023
|
-
|
|
8100
|
+
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
8101
|
}
|
|
8025
8102
|
return { files: [], issues, skipped: false };
|
|
8026
8103
|
}
|
|
@@ -8035,7 +8112,7 @@ import {
|
|
|
8035
8112
|
} from "node:fs";
|
|
8036
8113
|
import { homedir as homedir5 } from "node:os";
|
|
8037
8114
|
import { dirname as dirname2 } from "node:path";
|
|
8038
|
-
import * as
|
|
8115
|
+
import * as p3 from "@clack/prompts";
|
|
8039
8116
|
function ensureDir(dirPath) {
|
|
8040
8117
|
if (!existsSync7(dirPath)) {
|
|
8041
8118
|
mkdirSync4(dirPath, { recursive: true, mode: 493 });
|
|
@@ -8155,7 +8232,7 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
8155
8232
|
async function writeFilesWithProgress(files, options = {}) {
|
|
8156
8233
|
const results = [];
|
|
8157
8234
|
const home = homedir5();
|
|
8158
|
-
const spinner2 =
|
|
8235
|
+
const spinner2 = p3.spinner();
|
|
8159
8236
|
spinner2.start("Writing configuration files...");
|
|
8160
8237
|
for (const file of files) {
|
|
8161
8238
|
let result;
|
|
@@ -8647,6 +8724,10 @@ async function runSetup(options = {}) {
|
|
|
8647
8724
|
const home = homedir6();
|
|
8648
8725
|
console.clear();
|
|
8649
8726
|
console.log(messages.header());
|
|
8727
|
+
const assumeYes = shouldAssumeYes(options.yes, process.stdin.isTTY);
|
|
8728
|
+
if (assumeYes) {
|
|
8729
|
+
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.");
|
|
8730
|
+
}
|
|
8650
8731
|
const existingConfig = loadConfig();
|
|
8651
8732
|
const alreadyConfigured = isConfigured();
|
|
8652
8733
|
const skillsStatus = areSkillsInstalled(cwd);
|
|
@@ -8666,7 +8747,7 @@ async function runSetup(options = {}) {
|
|
|
8666
8747
|
let createdNewAccount = false;
|
|
8667
8748
|
let oauthTokens;
|
|
8668
8749
|
if (options.apiKey) {
|
|
8669
|
-
|
|
8750
|
+
p4.log.warn(colors.warning(`--api-key is deprecated and insecure: the key is exposed in your shell
|
|
8670
8751
|
history, terminal scrollback, and the process list. Prefer the browser
|
|
8671
8752
|
sign-in (run \`npx @gethmy/mcp setup\` with no --api-key). Use --api-key
|
|
8672
8753
|
only for unattended CI where you accept that risk.`));
|
|
@@ -8677,7 +8758,7 @@ only for unattended CI where you accept that risk.`));
|
|
|
8677
8758
|
if (!useNewAccount && options.apiKey) {
|
|
8678
8759
|
useNewAccount = false;
|
|
8679
8760
|
} else if (!useNewAccount && !options.apiKey) {
|
|
8680
|
-
const getStarted = await
|
|
8761
|
+
const getStarted = await p4.select({
|
|
8681
8762
|
message: "How would you like to connect?",
|
|
8682
8763
|
options: [
|
|
8683
8764
|
{
|
|
@@ -8698,15 +8779,15 @@ only for unattended CI where you accept that risk.`));
|
|
|
8698
8779
|
],
|
|
8699
8780
|
initialValue: "browser"
|
|
8700
8781
|
});
|
|
8701
|
-
if (
|
|
8702
|
-
|
|
8782
|
+
if (p4.isCancel(getStarted)) {
|
|
8783
|
+
p4.cancel("Setup cancelled");
|
|
8703
8784
|
process.exit(0);
|
|
8704
8785
|
}
|
|
8705
8786
|
useNewAccount = getStarted === "create";
|
|
8706
8787
|
useBrowserAuth = getStarted === "browser";
|
|
8707
8788
|
}
|
|
8708
8789
|
if (useBrowserAuth) {
|
|
8709
|
-
const spinner4 =
|
|
8790
|
+
const spinner4 = p4.spinner();
|
|
8710
8791
|
spinner4.start("Opening your browser to authorize…");
|
|
8711
8792
|
try {
|
|
8712
8793
|
oauthTokens = await loginWithBrowser({
|
|
@@ -8731,12 +8812,12 @@ ${colors.dim(url)}`);
|
|
|
8731
8812
|
} catch (error) {
|
|
8732
8813
|
spinner4.stop(colors.error("Browser authorization failed"));
|
|
8733
8814
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
8734
|
-
|
|
8735
|
-
|
|
8815
|
+
p4.log.error(msg);
|
|
8816
|
+
p4.log.info("You can retry, or run with --api-key for unattended setup.");
|
|
8736
8817
|
process.exit(1);
|
|
8737
8818
|
}
|
|
8738
8819
|
} else if (useNewAccount) {
|
|
8739
|
-
const fullName = options.name || await
|
|
8820
|
+
const fullName = options.name || await p4.text({
|
|
8740
8821
|
message: "Full name",
|
|
8741
8822
|
placeholder: "Jane Smith",
|
|
8742
8823
|
validate: (v) => {
|
|
@@ -8747,11 +8828,11 @@ ${colors.dim(url)}`);
|
|
|
8747
8828
|
return;
|
|
8748
8829
|
}
|
|
8749
8830
|
});
|
|
8750
|
-
if (
|
|
8751
|
-
|
|
8831
|
+
if (p4.isCancel(fullName)) {
|
|
8832
|
+
p4.cancel("Setup cancelled");
|
|
8752
8833
|
process.exit(0);
|
|
8753
8834
|
}
|
|
8754
|
-
const email = options.userEmail || await
|
|
8835
|
+
const email = options.userEmail || await p4.text({
|
|
8755
8836
|
message: "Email",
|
|
8756
8837
|
placeholder: "you@example.com",
|
|
8757
8838
|
validate: (v) => {
|
|
@@ -8764,11 +8845,11 @@ ${colors.dim(url)}`);
|
|
|
8764
8845
|
return;
|
|
8765
8846
|
}
|
|
8766
8847
|
});
|
|
8767
|
-
if (
|
|
8768
|
-
|
|
8848
|
+
if (p4.isCancel(email)) {
|
|
8849
|
+
p4.cancel("Setup cancelled");
|
|
8769
8850
|
process.exit(0);
|
|
8770
8851
|
}
|
|
8771
|
-
const password2 = await
|
|
8852
|
+
const password2 = await p4.password({
|
|
8772
8853
|
message: "Password",
|
|
8773
8854
|
validate: (v) => {
|
|
8774
8855
|
if (!v)
|
|
@@ -8780,11 +8861,11 @@ ${colors.dim(url)}`);
|
|
|
8780
8861
|
return;
|
|
8781
8862
|
}
|
|
8782
8863
|
});
|
|
8783
|
-
if (
|
|
8784
|
-
|
|
8864
|
+
if (p4.isCancel(password2)) {
|
|
8865
|
+
p4.cancel("Setup cancelled");
|
|
8785
8866
|
process.exit(0);
|
|
8786
8867
|
}
|
|
8787
|
-
const spinner4 =
|
|
8868
|
+
const spinner4 = p4.spinner();
|
|
8788
8869
|
spinner4.start("Creating your account...");
|
|
8789
8870
|
try {
|
|
8790
8871
|
const result = await onboardNewUser({
|
|
@@ -8804,15 +8885,15 @@ ${colors.dim(url)}`);
|
|
|
8804
8885
|
saveConfig({ apiKey, userEmail, apiUrl: API_URL });
|
|
8805
8886
|
setActiveWorkspace(selectedWorkspaceIdFromSignup);
|
|
8806
8887
|
setActiveProject(selectedProjectIdFromSignup);
|
|
8807
|
-
|
|
8888
|
+
p4.log.success("Workspace and board created");
|
|
8808
8889
|
} catch (error) {
|
|
8809
8890
|
spinner4.stop(colors.error("Account creation failed"));
|
|
8810
8891
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
8811
8892
|
if (msg.includes("already") || msg.includes("409")) {
|
|
8812
|
-
|
|
8893
|
+
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
8894
|
} else {
|
|
8814
|
-
|
|
8815
|
-
|
|
8895
|
+
p4.log.error(msg);
|
|
8896
|
+
p4.log.info("Please try again or visit https://app.gethmy.com");
|
|
8816
8897
|
}
|
|
8817
8898
|
process.exit(1);
|
|
8818
8899
|
}
|
|
@@ -8820,7 +8901,7 @@ ${colors.dim(url)}`);
|
|
|
8820
8901
|
apiKey = options.apiKey;
|
|
8821
8902
|
needsApiKey = true;
|
|
8822
8903
|
} else {
|
|
8823
|
-
const keyInput = await
|
|
8904
|
+
const keyInput = await p4.text({
|
|
8824
8905
|
message: "Enter your Harmony API key",
|
|
8825
8906
|
placeholder: "hmy_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
8826
8907
|
validate: (value) => {
|
|
@@ -8833,24 +8914,24 @@ ${colors.dim(url)}`);
|
|
|
8833
8914
|
return;
|
|
8834
8915
|
}
|
|
8835
8916
|
});
|
|
8836
|
-
if (
|
|
8837
|
-
|
|
8917
|
+
if (p4.isCancel(keyInput)) {
|
|
8918
|
+
p4.cancel("Setup cancelled");
|
|
8838
8919
|
process.exit(0);
|
|
8839
8920
|
}
|
|
8840
8921
|
apiKey = keyInput;
|
|
8841
8922
|
needsApiKey = true;
|
|
8842
8923
|
}
|
|
8843
8924
|
} else {
|
|
8844
|
-
|
|
8925
|
+
p4.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
|
|
8845
8926
|
}
|
|
8846
|
-
const spinner3 =
|
|
8927
|
+
const spinner3 = p4.spinner();
|
|
8847
8928
|
if (!createdNewAccount) {
|
|
8848
8929
|
spinner3.start("Validating API key...");
|
|
8849
8930
|
const validation = await validateApiKey(apiKey);
|
|
8850
8931
|
if (!validation.valid) {
|
|
8851
8932
|
spinner3.stop(colors.error("API key validation failed"));
|
|
8852
|
-
|
|
8853
|
-
|
|
8933
|
+
p4.log.error(validation.error || "Could not connect to Harmony API");
|
|
8934
|
+
p4.log.info("Get an API key at: https://app.gethmy.com/user/keys");
|
|
8854
8935
|
process.exit(1);
|
|
8855
8936
|
}
|
|
8856
8937
|
if (!userEmail) {
|
|
@@ -8861,13 +8942,13 @@ ${colors.dim(url)}`);
|
|
|
8861
8942
|
let selectedAgents = [];
|
|
8862
8943
|
let installMode = options.installMode || "global";
|
|
8863
8944
|
if (skillsStatus.installed && !options.force) {
|
|
8864
|
-
|
|
8865
|
-
const reinstall = await
|
|
8945
|
+
p4.log.success(`Skills already installed (${skillsStatus.location})`);
|
|
8946
|
+
const reinstall = await confirmOrDefault(assumeYes, {
|
|
8866
8947
|
message: "Reinstall skills?",
|
|
8867
8948
|
initialValue: false
|
|
8868
8949
|
});
|
|
8869
|
-
if (
|
|
8870
|
-
|
|
8950
|
+
if (p4.isCancel(reinstall)) {
|
|
8951
|
+
p4.cancel("Setup cancelled");
|
|
8871
8952
|
process.exit(0);
|
|
8872
8953
|
}
|
|
8873
8954
|
needsSkills = reinstall;
|
|
@@ -8882,23 +8963,23 @@ ${colors.dim(url)}`);
|
|
|
8882
8963
|
label: agent.name,
|
|
8883
8964
|
hint: agent.detected ? colors.success(`${agent.description} (detected)`) : colors.dim(`${agent.description}`)
|
|
8884
8965
|
}));
|
|
8885
|
-
const agentSelection = await
|
|
8966
|
+
const agentSelection = await p4.multiselect({
|
|
8886
8967
|
message: "Select agents to configure",
|
|
8887
8968
|
options: agentOptions,
|
|
8888
8969
|
initialValues: detectedAgents2.filter((a) => a.detected).map((a) => a.id),
|
|
8889
8970
|
required: true
|
|
8890
8971
|
});
|
|
8891
|
-
if (
|
|
8892
|
-
|
|
8972
|
+
if (p4.isCancel(agentSelection)) {
|
|
8973
|
+
p4.cancel("Setup cancelled");
|
|
8893
8974
|
process.exit(0);
|
|
8894
8975
|
}
|
|
8895
8976
|
selectedAgents = agentSelection;
|
|
8896
8977
|
}
|
|
8897
8978
|
if (selectedAgents.length === 0) {
|
|
8898
|
-
|
|
8979
|
+
p4.log.warning("No agents selected. Skipping skills installation.");
|
|
8899
8980
|
needsSkills = false;
|
|
8900
8981
|
} else if (!options.installMode) {
|
|
8901
|
-
const modeSelection = await
|
|
8982
|
+
const modeSelection = await p4.select({
|
|
8902
8983
|
message: "Where should Harmony skills be installed?",
|
|
8903
8984
|
options: [
|
|
8904
8985
|
{
|
|
@@ -8914,8 +8995,8 @@ ${colors.dim(url)}`);
|
|
|
8914
8995
|
],
|
|
8915
8996
|
initialValue: "global"
|
|
8916
8997
|
});
|
|
8917
|
-
if (
|
|
8918
|
-
|
|
8998
|
+
if (p4.isCancel(modeSelection)) {
|
|
8999
|
+
p4.cancel("Setup cancelled");
|
|
8919
9000
|
process.exit(0);
|
|
8920
9001
|
}
|
|
8921
9002
|
installMode = modeSelection;
|
|
@@ -8939,7 +9020,7 @@ ${colors.dim(url)}`);
|
|
|
8939
9020
|
spinner3.stop(colors.warning(`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`));
|
|
8940
9021
|
const list = resolved.candidates.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`).join(`
|
|
8941
9022
|
`);
|
|
8942
|
-
|
|
9023
|
+
p4.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
|
|
8943
9024
|
${list}
|
|
8944
9025
|
Specify the workspace with --workspace <id>, or select one below.`);
|
|
8945
9026
|
} else {
|
|
@@ -8960,7 +9041,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
8960
9041
|
spinner3.stop(colors.success(`Found ${workspaces.length} workspace(s)`));
|
|
8961
9042
|
} catch (_error) {
|
|
8962
9043
|
spinner3.stop(colors.warning("Could not fetch workspaces"));
|
|
8963
|
-
|
|
9044
|
+
p4.log.warning("Skipping workspace/project selection. You can set this later.");
|
|
8964
9045
|
needsContext = false;
|
|
8965
9046
|
}
|
|
8966
9047
|
if (needsContext && workspaces.length > 0) {
|
|
@@ -8972,12 +9053,12 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
8972
9053
|
value: ws.id,
|
|
8973
9054
|
label: ws.name
|
|
8974
9055
|
}));
|
|
8975
|
-
const workspaceSelection = await
|
|
9056
|
+
const workspaceSelection = await p4.select({
|
|
8976
9057
|
message: candidateIds.size > 0 ? `Select workspace for "${options.projectSlug}"` : "Select workspace",
|
|
8977
9058
|
options: workspaceOptions
|
|
8978
9059
|
});
|
|
8979
|
-
if (
|
|
8980
|
-
|
|
9060
|
+
if (p4.isCancel(workspaceSelection)) {
|
|
9061
|
+
p4.cancel("Setup cancelled");
|
|
8981
9062
|
process.exit(0);
|
|
8982
9063
|
}
|
|
8983
9064
|
selectedWorkspaceId = workspaceSelection;
|
|
@@ -8996,7 +9077,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
8996
9077
|
spinner3.stop(colors.success(`Found ${projects.length} project(s)`));
|
|
8997
9078
|
} catch (_error) {
|
|
8998
9079
|
spinner3.stop(colors.warning("Could not fetch projects"));
|
|
8999
|
-
|
|
9080
|
+
p4.log.warning("Skipping project selection. You can set this later.");
|
|
9000
9081
|
}
|
|
9001
9082
|
if (projects.length > 0 && !selectedProjectId) {
|
|
9002
9083
|
const projectOptions = projects.map((proj) => ({
|
|
@@ -9004,18 +9085,18 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9004
9085
|
label: proj.name,
|
|
9005
9086
|
hint: proj.description ? colors.dim(proj.description.slice(0, 50)) : undefined
|
|
9006
9087
|
}));
|
|
9007
|
-
const projectSelection = await
|
|
9088
|
+
const projectSelection = await p4.select({
|
|
9008
9089
|
message: "Select project",
|
|
9009
9090
|
options: projectOptions
|
|
9010
9091
|
});
|
|
9011
|
-
if (
|
|
9012
|
-
|
|
9092
|
+
if (p4.isCancel(projectSelection)) {
|
|
9093
|
+
p4.cancel("Setup cancelled");
|
|
9013
9094
|
process.exit(0);
|
|
9014
9095
|
}
|
|
9015
9096
|
selectedProjectId = projectSelection;
|
|
9016
|
-
selectedProjectName = projects.find((
|
|
9097
|
+
selectedProjectName = projects.find((p5) => p5.id === selectedProjectId)?.name;
|
|
9017
9098
|
} else if (selectedProjectId && !selectedProjectName) {
|
|
9018
|
-
selectedProjectName = projects.find((
|
|
9099
|
+
selectedProjectName = projects.find((p5) => p5.id === selectedProjectId)?.name;
|
|
9019
9100
|
}
|
|
9020
9101
|
}
|
|
9021
9102
|
}
|
|
@@ -9062,7 +9143,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9062
9143
|
}
|
|
9063
9144
|
const detectedAgents = detectAgents(cwd);
|
|
9064
9145
|
console.log("");
|
|
9065
|
-
|
|
9146
|
+
p4.log.step("Summary");
|
|
9066
9147
|
console.log("");
|
|
9067
9148
|
if (oauthTokens) {
|
|
9068
9149
|
console.log(` ${colors.bold("Credential:")} Browser sign-in (OAuth, workspace-scoped)`);
|
|
@@ -9118,12 +9199,12 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9118
9199
|
}
|
|
9119
9200
|
}
|
|
9120
9201
|
console.log("");
|
|
9121
|
-
const shouldProceed = await
|
|
9202
|
+
const shouldProceed = await confirmOrDefault(assumeYes, {
|
|
9122
9203
|
message: "Proceed with setup?",
|
|
9123
9204
|
initialValue: true
|
|
9124
9205
|
});
|
|
9125
|
-
if (
|
|
9126
|
-
|
|
9206
|
+
if (p4.isCancel(shouldProceed) || !shouldProceed) {
|
|
9207
|
+
p4.cancel("Setup cancelled");
|
|
9127
9208
|
process.exit(0);
|
|
9128
9209
|
}
|
|
9129
9210
|
console.log("");
|
|
@@ -9153,7 +9234,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9153
9234
|
}
|
|
9154
9235
|
symlinkSync(symlink.target, symlink.link);
|
|
9155
9236
|
} catch {
|
|
9156
|
-
|
|
9237
|
+
p4.log.warning(`Failed to create symlink: ${symlink.link}`);
|
|
9157
9238
|
}
|
|
9158
9239
|
}
|
|
9159
9240
|
}
|
|
@@ -9167,16 +9248,19 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9167
9248
|
await writeMcpConfigFallback(home);
|
|
9168
9249
|
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
|
|
9169
9250
|
} catch {
|
|
9170
|
-
|
|
9251
|
+
p4.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
|
|
9171
9252
|
}
|
|
9172
9253
|
}
|
|
9173
9254
|
}
|
|
9174
9255
|
if (claudeDetected || selectedAgents.includes("claude")) {
|
|
9175
9256
|
const allowAll = options.allowAllTools === true;
|
|
9176
9257
|
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
|
-
|
|
9258
|
+
const allowTools = await confirmOrDefault(assumeYes, {
|
|
9259
|
+
message,
|
|
9260
|
+
initialValue: true
|
|
9261
|
+
});
|
|
9262
|
+
if (p4.isCancel(allowTools)) {
|
|
9263
|
+
p4.cancel("Setup cancelled.");
|
|
9180
9264
|
process.exit(0);
|
|
9181
9265
|
}
|
|
9182
9266
|
if (allowTools) {
|
|
@@ -9185,7 +9269,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9185
9269
|
const scope = allowAll ? "all tools" : "safe tools";
|
|
9186
9270
|
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
|
|
9187
9271
|
} catch {
|
|
9188
|
-
|
|
9272
|
+
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
9273
|
}
|
|
9190
9274
|
} else {
|
|
9191
9275
|
console.log(` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`);
|
|
@@ -9205,7 +9289,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9205
9289
|
setActiveProject(selectedProjectId);
|
|
9206
9290
|
}
|
|
9207
9291
|
console.log("");
|
|
9208
|
-
|
|
9292
|
+
p4.outro(colors.success("Setup complete!"));
|
|
9209
9293
|
if (createdNewAccount && selectedWorkspaceNameFromSignup) {
|
|
9210
9294
|
const wsSlug = selectedWorkspaceNameFromSignup.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
9211
9295
|
const projSlug = (selectedProjectNameFromSignup || "my-first-board").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
@@ -9338,7 +9422,7 @@ program.command("reset").description("Remove stored configuration").action(() =>
|
|
|
9338
9422
|
console.log(`
|
|
9339
9423
|
To reconfigure, run: npx @gethmy/mcp setup`);
|
|
9340
9424
|
});
|
|
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) => {
|
|
9425
|
+
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
9426
|
await runSetup({
|
|
9343
9427
|
force: options.force,
|
|
9344
9428
|
apiKey: options.apiKey,
|
|
@@ -9352,7 +9436,8 @@ program.command("setup").description("Smart setup wizard for Harmony MCP (recomm
|
|
|
9352
9436
|
skipDocs: options.skipDocs,
|
|
9353
9437
|
newAccount: options.new,
|
|
9354
9438
|
name: options.name,
|
|
9355
|
-
allowAllTools: options.allowAllTools
|
|
9439
|
+
allowAllTools: options.allowAllTools,
|
|
9440
|
+
yes: options.yes
|
|
9356
9441
|
});
|
|
9357
9442
|
});
|
|
9358
9443
|
program.parse();
|
package/dist/index.js
CHANGED
|
@@ -3639,6 +3639,43 @@ async function putToSignedUrl(uploadUrl, bytes, contentType) {
|
|
|
3639
3639
|
throw new Error(`Direct storage upload failed: ${res.status}${detail ? ` — ${detail}` : ""}`);
|
|
3640
3640
|
}
|
|
3641
3641
|
}
|
|
3642
|
+
async function attachFileToCard(client3, cardId, file) {
|
|
3643
|
+
const { filePath, base64Data, fileName, contentType } = file;
|
|
3644
|
+
if (filePath && base64Data) {
|
|
3645
|
+
throw new Error("Provide either filePath or base64Data, not both.");
|
|
3646
|
+
}
|
|
3647
|
+
if (filePath) {
|
|
3648
|
+
const bytes = await readFileForUpload(filePath, MAX_ATTACHMENT_SIZE, "attachment");
|
|
3649
|
+
const resolvedName = fileName || basename(filePath);
|
|
3650
|
+
const signed = await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
3651
|
+
fileName: resolvedName,
|
|
3652
|
+
fileType: contentType,
|
|
3653
|
+
size: bytes.byteLength
|
|
3654
|
+
});
|
|
3655
|
+
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
3656
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
3657
|
+
storagePath: signed.storagePath,
|
|
3658
|
+
fileName: resolvedName,
|
|
3659
|
+
fileType: contentType || signed.fileType,
|
|
3660
|
+
sha256: sha256Hex(bytes),
|
|
3661
|
+
size: bytes.byteLength
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
if (base64Data) {
|
|
3665
|
+
if (!fileName) {
|
|
3666
|
+
throw new Error("fileName is required when using base64Data.");
|
|
3667
|
+
}
|
|
3668
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
3669
|
+
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
3670
|
+
}
|
|
3671
|
+
return await client3.uploadCardAttachment(cardId, {
|
|
3672
|
+
fileName,
|
|
3673
|
+
data: base64Data,
|
|
3674
|
+
fileType: contentType
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
3678
|
+
}
|
|
3642
3679
|
var memorySessions = new Map;
|
|
3643
3680
|
function parseLabelList(raw) {
|
|
3644
3681
|
if (raw === undefined || raw === null)
|
|
@@ -3795,7 +3832,7 @@ function cleanupMemorySession(cardId) {
|
|
|
3795
3832
|
}
|
|
3796
3833
|
var TOOLS = {
|
|
3797
3834
|
harmony_create_card: {
|
|
3798
|
-
description: "Create a new card in a Kanban column",
|
|
3835
|
+
description: "Create a new card in a Kanban column. Optionally attach reference files " + "(e.g. a screenshot from the prompt) at creation time via `attachments` — " + "the card is created first, then each file is uploaded to it.",
|
|
3799
3836
|
inputSchema: {
|
|
3800
3837
|
type: "object",
|
|
3801
3838
|
properties: {
|
|
@@ -3818,6 +3855,31 @@ var TOOLS = {
|
|
|
3818
3855
|
planId: {
|
|
3819
3856
|
type: "string",
|
|
3820
3857
|
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
3858
|
+
},
|
|
3859
|
+
attachments: {
|
|
3860
|
+
type: "array",
|
|
3861
|
+
description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
3862
|
+
items: {
|
|
3863
|
+
type: "object",
|
|
3864
|
+
properties: {
|
|
3865
|
+
filePath: {
|
|
3866
|
+
type: "string",
|
|
3867
|
+
description: "Absolute path to a local file the server can read. Mutually exclusive with base64Data."
|
|
3868
|
+
},
|
|
3869
|
+
base64Data: {
|
|
3870
|
+
type: "string",
|
|
3871
|
+
description: "Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath."
|
|
3872
|
+
},
|
|
3873
|
+
fileName: {
|
|
3874
|
+
type: "string",
|
|
3875
|
+
description: "File name including extension (required with base64Data; else defaults to the filePath basename)."
|
|
3876
|
+
},
|
|
3877
|
+
contentType: {
|
|
3878
|
+
type: "string",
|
|
3879
|
+
description: "Optional MIME type (inferred from the extension when omitted)."
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3821
3883
|
}
|
|
3822
3884
|
},
|
|
3823
3885
|
required: ["title"]
|
|
@@ -5632,6 +5694,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5632
5694
|
case "harmony_create_card": {
|
|
5633
5695
|
const title = z.string().min(1).max(500).parse(args.title);
|
|
5634
5696
|
const projectId = args.projectId || getProjectId();
|
|
5697
|
+
const attachments = args.attachments != null ? z.array(z.object({
|
|
5698
|
+
filePath: z.string().optional(),
|
|
5699
|
+
base64Data: z.string().optional(),
|
|
5700
|
+
fileName: z.string().optional(),
|
|
5701
|
+
contentType: z.string().optional()
|
|
5702
|
+
})).parse(args.attachments) : [];
|
|
5635
5703
|
const result = await client3.createCard(projectId, {
|
|
5636
5704
|
title,
|
|
5637
5705
|
columnId: args.columnId,
|
|
@@ -5640,7 +5708,30 @@ async function handleToolCall(name, args, deps) {
|
|
|
5640
5708
|
assigneeId: args.assigneeId,
|
|
5641
5709
|
planId: args.planId
|
|
5642
5710
|
});
|
|
5643
|
-
|
|
5711
|
+
if (attachments.length === 0) {
|
|
5712
|
+
return { success: true, ...result };
|
|
5713
|
+
}
|
|
5714
|
+
const cardId = result.card?.id;
|
|
5715
|
+
if (!cardId) {
|
|
5716
|
+
return {
|
|
5717
|
+
success: true,
|
|
5718
|
+
...result,
|
|
5719
|
+
attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
|
|
5720
|
+
};
|
|
5721
|
+
}
|
|
5722
|
+
const attachmentResults = await Promise.all(attachments.map(async (file) => {
|
|
5723
|
+
try {
|
|
5724
|
+
const uploaded = await attachFileToCard(client3, cardId, file);
|
|
5725
|
+
return { ok: true, attachment: uploaded.attachment };
|
|
5726
|
+
} catch (err) {
|
|
5727
|
+
return {
|
|
5728
|
+
ok: false,
|
|
5729
|
+
fileName: file.fileName ?? file.filePath ?? "(unnamed)",
|
|
5730
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5731
|
+
};
|
|
5732
|
+
}
|
|
5733
|
+
}));
|
|
5734
|
+
return { success: true, ...result, attachments: attachmentResults };
|
|
5644
5735
|
}
|
|
5645
5736
|
case "harmony_update_card": {
|
|
5646
5737
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -5926,37 +6017,12 @@ ${list}
|
|
|
5926
6017
|
const cardId2 = z.string().uuid().parse(args.cardId);
|
|
5927
6018
|
const fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
|
|
5928
6019
|
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
size: bytes.byteLength
|
|
5936
|
-
});
|
|
5937
|
-
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
5938
|
-
return await client3.finalizeCardAttachment(cardId2, {
|
|
5939
|
-
storagePath: signed.storagePath,
|
|
5940
|
-
fileName: resolvedName,
|
|
5941
|
-
fileType: contentType || signed.fileType,
|
|
5942
|
-
sha256: sha256Hex(bytes),
|
|
5943
|
-
size: bytes.byteLength
|
|
5944
|
-
});
|
|
5945
|
-
}
|
|
5946
|
-
if (base64Data) {
|
|
5947
|
-
if (!fileName) {
|
|
5948
|
-
throw new Error("fileName is required when using base64Data.");
|
|
5949
|
-
}
|
|
5950
|
-
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
5951
|
-
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
5952
|
-
}
|
|
5953
|
-
return await client3.uploadCardAttachment(cardId2, {
|
|
5954
|
-
fileName,
|
|
5955
|
-
data: base64Data,
|
|
5956
|
-
fileType: contentType
|
|
5957
|
-
});
|
|
5958
|
-
}
|
|
5959
|
-
throw new Error("Provide either filePath or base64Data.");
|
|
6020
|
+
return await attachFileToCard(client3, cardId2, {
|
|
6021
|
+
filePath,
|
|
6022
|
+
base64Data,
|
|
6023
|
+
fileName,
|
|
6024
|
+
contentType
|
|
6025
|
+
});
|
|
5960
6026
|
}
|
|
5961
6027
|
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5962
6028
|
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.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
|
|
package/src/server.ts
CHANGED
|
@@ -221,6 +221,80 @@ async function putToSignedUrl(
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/** One card-attachment input: bytes as a local `filePath` (preferred — read
|
|
225
|
+
* direct-to-storage) or a small `base64Data` blob (requires `fileName`). */
|
|
226
|
+
export interface CardAttachmentInput {
|
|
227
|
+
filePath?: string;
|
|
228
|
+
base64Data?: string;
|
|
229
|
+
fileName?: string;
|
|
230
|
+
contentType?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Attach one file to an existing card, running the same direct-to-storage
|
|
235
|
+
* handshake as `harmony_upload` `{target:"card_attachment"}`. Extracted so
|
|
236
|
+
* `harmony_create_card` can attach reference files (e.g. a prompt screenshot)
|
|
237
|
+
* at creation time without duplicating the orchestration. Requires an existing
|
|
238
|
+
* `cardId` — attachment upload is inherently a second step after the card row
|
|
239
|
+
* exists, so the create path calls this only after `createCard` returns.
|
|
240
|
+
*/
|
|
241
|
+
async function attachFileToCard(
|
|
242
|
+
client: HarmonyApiClient,
|
|
243
|
+
cardId: string,
|
|
244
|
+
file: CardAttachmentInput,
|
|
245
|
+
) {
|
|
246
|
+
const { filePath, base64Data, fileName, contentType } = file;
|
|
247
|
+
if (filePath && base64Data) {
|
|
248
|
+
throw new Error("Provide either filePath or base64Data, not both.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (filePath) {
|
|
252
|
+
// Server can read the file → upload direct-to-storage via the handshake
|
|
253
|
+
// (no base64 through the model context or edge-fn body).
|
|
254
|
+
const bytes = await readFileForUpload(
|
|
255
|
+
filePath,
|
|
256
|
+
MAX_ATTACHMENT_SIZE,
|
|
257
|
+
"attachment",
|
|
258
|
+
);
|
|
259
|
+
const resolvedName = fileName || basename(filePath);
|
|
260
|
+
const signed = await client.requestCardAttachmentUploadUrl(cardId, {
|
|
261
|
+
fileName: resolvedName,
|
|
262
|
+
fileType: contentType,
|
|
263
|
+
size: bytes.byteLength,
|
|
264
|
+
});
|
|
265
|
+
await putToSignedUrl(
|
|
266
|
+
signed.uploadUrl,
|
|
267
|
+
bytes,
|
|
268
|
+
contentType || signed.fileType || "application/octet-stream",
|
|
269
|
+
);
|
|
270
|
+
return await client.finalizeCardAttachment(cardId, {
|
|
271
|
+
storagePath: signed.storagePath,
|
|
272
|
+
fileName: resolvedName,
|
|
273
|
+
fileType: contentType || signed.fileType,
|
|
274
|
+
sha256: sha256Hex(bytes),
|
|
275
|
+
size: bytes.byteLength,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (base64Data) {
|
|
280
|
+
if (!fileName) {
|
|
281
|
+
throw new Error("fileName is required when using base64Data.");
|
|
282
|
+
}
|
|
283
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return await client.uploadCardAttachment(cardId, {
|
|
289
|
+
fileName,
|
|
290
|
+
data: base64Data,
|
|
291
|
+
fileType: contentType,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
296
|
+
}
|
|
297
|
+
|
|
224
298
|
/**
|
|
225
299
|
* Dependencies injected into tool handlers.
|
|
226
300
|
* Allows the same handlers to be used by both stdio and remote (HTTP) transports.
|
|
@@ -524,7 +598,10 @@ function cleanupMemorySession(cardId: string): void {
|
|
|
524
598
|
export const TOOLS = {
|
|
525
599
|
// Card operations
|
|
526
600
|
harmony_create_card: {
|
|
527
|
-
description:
|
|
601
|
+
description:
|
|
602
|
+
"Create a new card in a Kanban column. Optionally attach reference files " +
|
|
603
|
+
"(e.g. a screenshot from the prompt) at creation time via `attachments` — " +
|
|
604
|
+
"the card is created first, then each file is uploaded to it.",
|
|
528
605
|
inputSchema: {
|
|
529
606
|
type: "object",
|
|
530
607
|
properties: {
|
|
@@ -550,6 +627,42 @@ export const TOOLS = {
|
|
|
550
627
|
description:
|
|
551
628
|
"Plan ID to link this card to (optional). Links the card to that plan via its plan_id.",
|
|
552
629
|
},
|
|
630
|
+
attachments: {
|
|
631
|
+
type: "array",
|
|
632
|
+
description:
|
|
633
|
+
"Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " +
|
|
634
|
+
"Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " +
|
|
635
|
+
"come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " +
|
|
636
|
+
"(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " +
|
|
637
|
+
"harness has written it to a local file you can pass as filePath — a model cannot re-emit " +
|
|
638
|
+
"pasted image bytes into base64Data. Per-file failures never block card creation; they are " +
|
|
639
|
+
"reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
640
|
+
items: {
|
|
641
|
+
type: "object",
|
|
642
|
+
properties: {
|
|
643
|
+
filePath: {
|
|
644
|
+
type: "string",
|
|
645
|
+
description:
|
|
646
|
+
"Absolute path to a local file the server can read. Mutually exclusive with base64Data.",
|
|
647
|
+
},
|
|
648
|
+
base64Data: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description:
|
|
651
|
+
"Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath.",
|
|
652
|
+
},
|
|
653
|
+
fileName: {
|
|
654
|
+
type: "string",
|
|
655
|
+
description:
|
|
656
|
+
"File name including extension (required with base64Data; else defaults to the filePath basename).",
|
|
657
|
+
},
|
|
658
|
+
contentType: {
|
|
659
|
+
type: "string",
|
|
660
|
+
description:
|
|
661
|
+
"Optional MIME type (inferred from the extension when omitted).",
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
},
|
|
553
666
|
},
|
|
554
667
|
required: ["title"],
|
|
555
668
|
},
|
|
@@ -2681,6 +2794,19 @@ async function handleToolCall(
|
|
|
2681
2794
|
case "harmony_create_card": {
|
|
2682
2795
|
const title = z.string().min(1).max(500).parse(args.title);
|
|
2683
2796
|
const projectId = (args.projectId as string) || getProjectId();
|
|
2797
|
+
const attachments =
|
|
2798
|
+
args.attachments != null
|
|
2799
|
+
? z
|
|
2800
|
+
.array(
|
|
2801
|
+
z.object({
|
|
2802
|
+
filePath: z.string().optional(),
|
|
2803
|
+
base64Data: z.string().optional(),
|
|
2804
|
+
fileName: z.string().optional(),
|
|
2805
|
+
contentType: z.string().optional(),
|
|
2806
|
+
}),
|
|
2807
|
+
)
|
|
2808
|
+
.parse(args.attachments)
|
|
2809
|
+
: [];
|
|
2684
2810
|
const result = await client.createCard(projectId, {
|
|
2685
2811
|
title,
|
|
2686
2812
|
columnId: args.columnId as string | undefined,
|
|
@@ -2689,7 +2815,40 @@ async function handleToolCall(
|
|
|
2689
2815
|
assigneeId: args.assigneeId as string | undefined,
|
|
2690
2816
|
planId: args.planId as string | undefined,
|
|
2691
2817
|
});
|
|
2692
|
-
|
|
2818
|
+
|
|
2819
|
+
if (attachments.length === 0) {
|
|
2820
|
+
return { success: true, ...result };
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// Attach reference files (e.g. a prompt screenshot) to the freshly
|
|
2824
|
+
// created card. Attachment upload requires an existing cardId, so this
|
|
2825
|
+
// runs only after createCard returns. A bad attachment must never lose
|
|
2826
|
+
// the card — per-file failures are captured and reported alongside the
|
|
2827
|
+
// successes rather than throwing out the whole create.
|
|
2828
|
+
const cardId = (result.card as { id?: string } | null)?.id;
|
|
2829
|
+
if (!cardId) {
|
|
2830
|
+
return {
|
|
2831
|
+
success: true,
|
|
2832
|
+
...result,
|
|
2833
|
+
attachmentWarning:
|
|
2834
|
+
"Card created, but attachments were skipped: no card id was returned to upload against.",
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
const attachmentResults = await Promise.all(
|
|
2838
|
+
attachments.map(async (file) => {
|
|
2839
|
+
try {
|
|
2840
|
+
const uploaded = await attachFileToCard(client, cardId, file);
|
|
2841
|
+
return { ok: true as const, attachment: uploaded.attachment };
|
|
2842
|
+
} catch (err) {
|
|
2843
|
+
return {
|
|
2844
|
+
ok: false as const,
|
|
2845
|
+
fileName: file.fileName ?? file.filePath ?? "(unnamed)",
|
|
2846
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
}),
|
|
2850
|
+
);
|
|
2851
|
+
return { success: true, ...result, attachments: attachmentResults };
|
|
2693
2852
|
}
|
|
2694
2853
|
|
|
2695
2854
|
case "harmony_update_card": {
|
|
@@ -3162,52 +3321,13 @@ async function handleToolCall(
|
|
|
3162
3321
|
args.contentType != null
|
|
3163
3322
|
? z.string().parse(args.contentType)
|
|
3164
3323
|
: undefined;
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
"attachment",
|
|
3173
|
-
);
|
|
3174
|
-
const resolvedName = fileName || basename(filePath);
|
|
3175
|
-
const signed = await client.requestCardAttachmentUploadUrl(cardId, {
|
|
3176
|
-
fileName: resolvedName,
|
|
3177
|
-
fileType: contentType,
|
|
3178
|
-
size: bytes.byteLength,
|
|
3179
|
-
});
|
|
3180
|
-
await putToSignedUrl(
|
|
3181
|
-
signed.uploadUrl,
|
|
3182
|
-
bytes,
|
|
3183
|
-
contentType || signed.fileType || "application/octet-stream",
|
|
3184
|
-
);
|
|
3185
|
-
return await client.finalizeCardAttachment(cardId, {
|
|
3186
|
-
storagePath: signed.storagePath,
|
|
3187
|
-
fileName: resolvedName,
|
|
3188
|
-
fileType: contentType || signed.fileType,
|
|
3189
|
-
sha256: sha256Hex(bytes),
|
|
3190
|
-
size: bytes.byteLength,
|
|
3191
|
-
});
|
|
3192
|
-
}
|
|
3193
|
-
|
|
3194
|
-
if (base64Data) {
|
|
3195
|
-
if (!fileName) {
|
|
3196
|
-
throw new Error("fileName is required when using base64Data.");
|
|
3197
|
-
}
|
|
3198
|
-
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
3199
|
-
throw new Error(
|
|
3200
|
-
`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
|
|
3201
|
-
);
|
|
3202
|
-
}
|
|
3203
|
-
return await client.uploadCardAttachment(cardId, {
|
|
3204
|
-
fileName,
|
|
3205
|
-
data: base64Data,
|
|
3206
|
-
fileType: contentType,
|
|
3207
|
-
});
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
|
-
throw new Error("Provide either filePath or base64Data.");
|
|
3324
|
+
// Same handshake as the create-card `attachments` path (shared helper).
|
|
3325
|
+
return await attachFileToCard(client, cardId, {
|
|
3326
|
+
filePath,
|
|
3327
|
+
base64Data,
|
|
3328
|
+
fileName,
|
|
3329
|
+
contentType,
|
|
3330
|
+
});
|
|
3211
3331
|
}
|
|
3212
3332
|
|
|
3213
3333
|
// target === "artifact"
|
|
@@ -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);
|