@gethmy/mcp 2.9.4 → 2.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/dist/cli.js +674 -275
- package/dist/index.js +530 -205
- package/dist/lib/api-client.js +352 -182
- package/dist/lib/config.js +37 -26
- package/package.json +3 -2
- package/src/api-client.ts +50 -4
- package/src/cli.ts +13 -2
- package/src/config.ts +53 -25
- package/src/graph-expansion.ts +7 -4
- package/src/http.ts +3 -14
- package/src/memory-floor.ts +2 -1
- package/src/oauth-login.ts +288 -0
- package/src/oauth-refresh.ts +209 -0
- package/src/server.ts +6 -6
- package/src/skills.ts +4 -2
- package/src/tui/setup.ts +106 -16
package/src/tui/setup.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
setActiveProject,
|
|
23
23
|
setActiveWorkspace,
|
|
24
24
|
} from "../config.js";
|
|
25
|
+
import { loginWithBrowser, type OAuthTokens } from "../oauth-login.js";
|
|
25
26
|
import { onboardNewUser } from "../onboard.js";
|
|
26
27
|
import { buildSkillFile, HARMONY_WORKFLOW_PROMPT } from "../skills.js";
|
|
27
28
|
import { type AgentId, detectAgents } from "./agents.js";
|
|
@@ -708,7 +709,10 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
708
709
|
}
|
|
709
710
|
|
|
710
711
|
// Step 1: API Key (or create account)
|
|
711
|
-
|
|
712
|
+
// Prefer an existing OAuth access token, then a legacy key. A `hmy_at_` OAuth
|
|
713
|
+
// token also starts with `hmy_`, so the reuse check below accepts both.
|
|
714
|
+
let apiKey =
|
|
715
|
+
options.apiKey || existingConfig.oauthAccessToken || existingConfig.apiKey;
|
|
712
716
|
let userEmail: string | undefined =
|
|
713
717
|
options.userEmail || existingConfig.userEmail || undefined;
|
|
714
718
|
let selectedWorkspaceIdFromSignup: string | undefined;
|
|
@@ -716,28 +720,53 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
716
720
|
let selectedWorkspaceNameFromSignup: string | undefined;
|
|
717
721
|
let selectedProjectNameFromSignup: string | undefined;
|
|
718
722
|
let createdNewAccount = false;
|
|
723
|
+
// Set when the browser OAuth flow ran — persisted to config in Step 4.
|
|
724
|
+
let oauthTokens: OAuthTokens | undefined;
|
|
725
|
+
|
|
726
|
+
if (options.apiKey) {
|
|
727
|
+
// --- Deprecated: key passed via --api-key argv (card #364) ---
|
|
728
|
+
// A long-lived, unscoped account key in argv leaks via shell history,
|
|
729
|
+
// scrollback, and the process list. Kept as a CI/scripting escape hatch,
|
|
730
|
+
// but the browser OAuth flow is the supported onboarding path and the
|
|
731
|
+
// SetupWizard no longer emits this.
|
|
732
|
+
p.log.warn(
|
|
733
|
+
colors.warning(
|
|
734
|
+
"--api-key is deprecated and insecure: the key is exposed in your shell\n" +
|
|
735
|
+
"history, terminal scrollback, and the process list. Prefer the browser\n" +
|
|
736
|
+
"sign-in (run `npx @gethmy/mcp setup` with no --api-key). Use --api-key\n" +
|
|
737
|
+
"only for unattended CI where you accept that risk.",
|
|
738
|
+
),
|
|
739
|
+
);
|
|
740
|
+
}
|
|
719
741
|
|
|
720
742
|
if (needsApiKey || !apiKey || !apiKey.startsWith("hmy_")) {
|
|
721
|
-
// Determine path: create account or
|
|
743
|
+
// Determine path: browser OAuth (default), create account, or paste key.
|
|
722
744
|
let useNewAccount = options.newAccount === true;
|
|
745
|
+
let useBrowserAuth = false;
|
|
723
746
|
|
|
724
747
|
if (!useNewAccount && options.apiKey) {
|
|
725
748
|
// API key passed via flag — skip the choice prompt
|
|
726
749
|
useNewAccount = false;
|
|
727
750
|
} else if (!useNewAccount && !options.apiKey) {
|
|
728
751
|
const getStarted = await p.select({
|
|
729
|
-
message: "How would you like to
|
|
752
|
+
message: "How would you like to connect?",
|
|
730
753
|
options: [
|
|
754
|
+
{
|
|
755
|
+
value: "browser",
|
|
756
|
+
label: "Sign in with your browser",
|
|
757
|
+
hint: "recommended — secure, no key handling",
|
|
758
|
+
},
|
|
731
759
|
{
|
|
732
760
|
value: "create",
|
|
733
761
|
label: "Create a free account",
|
|
734
|
-
hint: "recommended for new users",
|
|
735
762
|
},
|
|
736
763
|
{
|
|
737
764
|
value: "apikey",
|
|
738
|
-
label: "
|
|
765
|
+
label: "Paste an API key",
|
|
766
|
+
hint: "advanced / CI",
|
|
739
767
|
},
|
|
740
768
|
],
|
|
769
|
+
initialValue: "browser",
|
|
741
770
|
});
|
|
742
771
|
|
|
743
772
|
if (p.isCancel(getStarted)) {
|
|
@@ -746,9 +775,47 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
746
775
|
}
|
|
747
776
|
|
|
748
777
|
useNewAccount = getStarted === "create";
|
|
778
|
+
useBrowserAuth = getStarted === "browser";
|
|
749
779
|
}
|
|
750
780
|
|
|
751
|
-
if (
|
|
781
|
+
if (useBrowserAuth) {
|
|
782
|
+
// --- Browser OAuth flow (loopback + PKCE), card #364 ---
|
|
783
|
+
const spinner = p.spinner();
|
|
784
|
+
spinner.start("Opening your browser to authorize…");
|
|
785
|
+
try {
|
|
786
|
+
oauthTokens = await loginWithBrowser({
|
|
787
|
+
apiUrl: API_URL,
|
|
788
|
+
workspaceId: options.workspaceId,
|
|
789
|
+
onUrl: (url) => {
|
|
790
|
+
spinner.message(
|
|
791
|
+
`Authorize in your browser. If it didn't open:\n${colors.dim(url)}`,
|
|
792
|
+
);
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
apiKey = oauthTokens.accessToken;
|
|
796
|
+
needsApiKey = true;
|
|
797
|
+
// Persist immediately — the token is already minted, so saving now
|
|
798
|
+
// survives an abort later in the wizard. Null the legacy key: OAuth is
|
|
799
|
+
// now the active credential.
|
|
800
|
+
saveConfig({
|
|
801
|
+
apiKey: null,
|
|
802
|
+
oauthAccessToken: oauthTokens.accessToken,
|
|
803
|
+
oauthRefreshToken: oauthTokens.refreshToken,
|
|
804
|
+
oauthExpiresAt: oauthTokens.expiresAt,
|
|
805
|
+
oauthClientId: oauthTokens.clientId,
|
|
806
|
+
apiUrl: API_URL,
|
|
807
|
+
});
|
|
808
|
+
spinner.stop(colors.success("Authorized"));
|
|
809
|
+
} catch (error) {
|
|
810
|
+
spinner.stop(colors.error("Browser authorization failed"));
|
|
811
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
812
|
+
p.log.error(msg);
|
|
813
|
+
p.log.info(
|
|
814
|
+
"You can retry, or run with --api-key for unattended setup.",
|
|
815
|
+
);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
}
|
|
818
|
+
} else if (useNewAccount) {
|
|
752
819
|
// --- Create account flow ---
|
|
753
820
|
const fullName =
|
|
754
821
|
options.name ||
|
|
@@ -843,6 +910,12 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
843
910
|
}
|
|
844
911
|
process.exit(1);
|
|
845
912
|
}
|
|
913
|
+
} else if (options.apiKey) {
|
|
914
|
+
// --- Key provided via --api-key: honor it non-interactively ---
|
|
915
|
+
// (the onboarding wizard's copied command depends on this path;
|
|
916
|
+
// validation below still runs against it)
|
|
917
|
+
apiKey = options.apiKey;
|
|
918
|
+
needsApiKey = true;
|
|
846
919
|
} else {
|
|
847
920
|
// --- Existing API key flow ---
|
|
848
921
|
const keyInput = await p.text({
|
|
@@ -1102,21 +1175,32 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1102
1175
|
const allFiles: FileToWrite[] = [];
|
|
1103
1176
|
const allSymlinks: SymlinkToCreate[] = [];
|
|
1104
1177
|
|
|
1105
|
-
// Always save global config if we have an API key change
|
|
1178
|
+
// Always save global config if we have an API key change. This write is
|
|
1179
|
+
// type:"text" (no merge), so it must carry the OAuth fields explicitly or the
|
|
1180
|
+
// browser-auth credential would be clobbered.
|
|
1106
1181
|
if (needsApiKey || !alreadyConfigured) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1182
|
+
const configToWrite = oauthTokens
|
|
1183
|
+
? {
|
|
1184
|
+
apiKey: null,
|
|
1185
|
+
apiUrl: API_URL,
|
|
1186
|
+
userEmail: userEmail || null,
|
|
1187
|
+
activeWorkspaceId: null,
|
|
1188
|
+
activeProjectId: null,
|
|
1189
|
+
oauthAccessToken: oauthTokens.accessToken,
|
|
1190
|
+
oauthRefreshToken: oauthTokens.refreshToken,
|
|
1191
|
+
oauthExpiresAt: oauthTokens.expiresAt,
|
|
1192
|
+
oauthClientId: oauthTokens.clientId,
|
|
1193
|
+
}
|
|
1194
|
+
: {
|
|
1111
1195
|
apiKey,
|
|
1112
1196
|
apiUrl: API_URL,
|
|
1113
1197
|
userEmail: userEmail || null,
|
|
1114
1198
|
activeWorkspaceId: null,
|
|
1115
1199
|
activeProjectId: null,
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
),
|
|
1200
|
+
};
|
|
1201
|
+
allFiles.push({
|
|
1202
|
+
path: getConfigPath(),
|
|
1203
|
+
content: JSON.stringify(configToWrite, null, 2),
|
|
1120
1204
|
type: "text", // Use text to avoid merging
|
|
1121
1205
|
});
|
|
1122
1206
|
}
|
|
@@ -1151,7 +1235,13 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1151
1235
|
p.log.step("Summary");
|
|
1152
1236
|
console.log("");
|
|
1153
1237
|
|
|
1154
|
-
|
|
1238
|
+
if (oauthTokens) {
|
|
1239
|
+
console.log(
|
|
1240
|
+
` ${colors.bold("Credential:")} Browser sign-in (OAuth, workspace-scoped)`,
|
|
1241
|
+
);
|
|
1242
|
+
} else {
|
|
1243
|
+
console.log(` ${colors.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
|
|
1244
|
+
}
|
|
1155
1245
|
if (userEmail) {
|
|
1156
1246
|
console.log(` ${colors.bold("Email:")} ${userEmail}`);
|
|
1157
1247
|
}
|