@gethmy/mcp 2.9.3 → 2.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -17,6 +17,199 @@ var __export = (target, all) => {
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
+ // src/config.ts
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ function getConfigDir() {
25
+ return join(homedir(), ".harmony-mcp");
26
+ }
27
+ function getConfigPath() {
28
+ return join(getConfigDir(), "config.json");
29
+ }
30
+ function getLocalConfigPath(cwd) {
31
+ return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
32
+ }
33
+ function emptyConfig() {
34
+ return {
35
+ apiKey: null,
36
+ apiUrl: DEFAULT_API_URL,
37
+ activeWorkspaceId: null,
38
+ activeProjectId: null,
39
+ userEmail: null,
40
+ memoryDir: null,
41
+ oauthAccessToken: null,
42
+ oauthRefreshToken: null,
43
+ oauthExpiresAt: null,
44
+ oauthClientId: null
45
+ };
46
+ }
47
+ function loadConfig() {
48
+ const configPath = getConfigPath();
49
+ if (!existsSync(configPath)) {
50
+ return emptyConfig();
51
+ }
52
+ try {
53
+ const data = readFileSync(configPath, "utf-8");
54
+ const config = JSON.parse(data);
55
+ return {
56
+ apiKey: config.apiKey || null,
57
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
58
+ activeWorkspaceId: config.activeWorkspaceId || null,
59
+ activeProjectId: config.activeProjectId || null,
60
+ userEmail: config.userEmail || null,
61
+ memoryDir: config.memoryDir || null,
62
+ oauthAccessToken: config.oauthAccessToken || null,
63
+ oauthRefreshToken: config.oauthRefreshToken || null,
64
+ oauthExpiresAt: typeof config.oauthExpiresAt === "number" ? config.oauthExpiresAt : null,
65
+ oauthClientId: config.oauthClientId || null
66
+ };
67
+ } catch {
68
+ return emptyConfig();
69
+ }
70
+ }
71
+ function saveConfig(config) {
72
+ const configDir = getConfigDir();
73
+ const configPath = getConfigPath();
74
+ if (!existsSync(configDir)) {
75
+ mkdirSync(configDir, { recursive: true, mode: 448 });
76
+ }
77
+ const existingConfig = loadConfig();
78
+ const newConfig = { ...existingConfig, ...config };
79
+ writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
80
+ mode: 384
81
+ });
82
+ }
83
+ function loadLocalConfig(cwd) {
84
+ const localConfigPath = getLocalConfigPath(cwd);
85
+ if (!existsSync(localConfigPath)) {
86
+ return null;
87
+ }
88
+ try {
89
+ const data = readFileSync(localConfigPath, "utf-8");
90
+ const config = JSON.parse(data);
91
+ return {
92
+ workspaceId: config.workspaceId || null,
93
+ projectId: config.projectId || null
94
+ };
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ function saveLocalConfig(config, cwd) {
100
+ const localConfigPath = getLocalConfigPath(cwd);
101
+ const existingConfig = loadLocalConfig(cwd) || {
102
+ workspaceId: null,
103
+ projectId: null
104
+ };
105
+ const newConfig = { ...existingConfig, ...config };
106
+ const cleanConfig = {};
107
+ if (newConfig.workspaceId)
108
+ cleanConfig.workspaceId = newConfig.workspaceId;
109
+ if (newConfig.projectId)
110
+ cleanConfig.projectId = newConfig.projectId;
111
+ writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
112
+ }
113
+ function hasLocalConfig(cwd) {
114
+ return existsSync(getLocalConfigPath(cwd));
115
+ }
116
+ function getActiveCredential() {
117
+ const config = loadConfig();
118
+ if (config.oauthAccessToken)
119
+ return config.oauthAccessToken;
120
+ if (config.apiKey)
121
+ return config.apiKey;
122
+ throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to connect Harmony.
123
+ ` + "Setup authorizes in your browser — no API key handling required.");
124
+ }
125
+ function getApiKey() {
126
+ return getActiveCredential();
127
+ }
128
+ function getApiUrl() {
129
+ const config = loadConfig();
130
+ return config.apiUrl;
131
+ }
132
+ function getUserEmail() {
133
+ const config = loadConfig();
134
+ return config.userEmail;
135
+ }
136
+ function setActiveWorkspace(workspaceId, options) {
137
+ if (options?.local) {
138
+ saveLocalConfig({ workspaceId }, options.cwd);
139
+ } else {
140
+ saveConfig({ activeWorkspaceId: workspaceId });
141
+ }
142
+ }
143
+ function setActiveProject(projectId, options) {
144
+ if (options?.local) {
145
+ saveLocalConfig({ projectId }, options.cwd);
146
+ } else {
147
+ saveConfig({ activeProjectId: projectId });
148
+ }
149
+ }
150
+ function getActiveWorkspaceId(cwd) {
151
+ const localConfig = loadLocalConfig(cwd);
152
+ if (localConfig?.workspaceId) {
153
+ return localConfig.workspaceId;
154
+ }
155
+ return loadConfig().activeWorkspaceId;
156
+ }
157
+ function getActiveProjectId(cwd) {
158
+ const localConfig = loadLocalConfig(cwd);
159
+ if (localConfig?.projectId) {
160
+ return localConfig.projectId;
161
+ }
162
+ return loadConfig().activeProjectId;
163
+ }
164
+ function isConfigured() {
165
+ const config = loadConfig();
166
+ return !!(config.apiKey || config.oauthAccessToken);
167
+ }
168
+ function areSkillsInstalled(cwd) {
169
+ const home = homedir();
170
+ const workingDir = cwd || process.cwd();
171
+ const foundPaths = [];
172
+ const globalSkillsDir = join(home, ".agents", "skills");
173
+ const globalSkillPath = join(globalSkillsDir, "hmy", "SKILL.md");
174
+ if (existsSync(globalSkillPath)) {
175
+ foundPaths.push(globalSkillPath);
176
+ return { installed: true, location: "global", paths: foundPaths };
177
+ }
178
+ const claudeGlobalSkill = join(home, ".claude", "skills", "hmy.md");
179
+ if (existsSync(claudeGlobalSkill)) {
180
+ foundPaths.push(claudeGlobalSkill);
181
+ return { installed: true, location: "global", paths: foundPaths };
182
+ }
183
+ const claudeGlobalSkillAlt = join(home, ".claude", "skills", "hmy", "SKILL.md");
184
+ if (existsSync(claudeGlobalSkillAlt)) {
185
+ foundPaths.push(claudeGlobalSkillAlt);
186
+ return { installed: true, location: "global", paths: foundPaths };
187
+ }
188
+ const localSkillPath = join(workingDir, ".claude", "skills", "hmy.md");
189
+ if (existsSync(localSkillPath)) {
190
+ foundPaths.push(localSkillPath);
191
+ return { installed: true, location: "local", paths: foundPaths };
192
+ }
193
+ const localSkillPathAlt = join(workingDir, ".claude", "skills", "hmy", "SKILL.md");
194
+ if (existsSync(localSkillPathAlt)) {
195
+ foundPaths.push(localSkillPathAlt);
196
+ return { installed: true, location: "local", paths: foundPaths };
197
+ }
198
+ return { installed: false, location: null, paths: [] };
199
+ }
200
+ function hasProjectContext(cwd) {
201
+ const localConfig = loadLocalConfig(cwd);
202
+ return !!(localConfig?.workspaceId || localConfig?.projectId);
203
+ }
204
+ function getMemoryDir() {
205
+ const config = loadConfig();
206
+ if (config.memoryDir)
207
+ return config.memoryDir;
208
+ return join(homedir(), ".harmony", "memory");
209
+ }
210
+ var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
211
+ var init_config = () => {};
212
+
20
213
  // src/prompt-builder.ts
21
214
  var exports_prompt_builder = {};
22
215
  __export(exports_prompt_builder, {
@@ -507,194 +700,294 @@ var init_prompt_builder = __esm(() => {
507
700
  };
508
701
  });
509
702
 
510
- // src/cli.ts
511
- import { createRequire as createRequire2 } from "node:module";
512
- import { program } from "commander";
513
-
514
- // src/config.ts
515
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
516
- import { homedir } from "node:os";
517
- import { join } from "node:path";
518
- var DEFAULT_API_URL = "https://app.gethmy.com/api";
519
- var LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
520
- function getConfigDir() {
521
- return join(homedir(), ".harmony-mcp");
522
- }
523
- function getConfigPath() {
524
- return join(getConfigDir(), "config.json");
525
- }
526
- function getLocalConfigPath(cwd) {
527
- return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
528
- }
529
- function loadConfig() {
530
- const configPath = getConfigPath();
531
- if (!existsSync(configPath)) {
532
- return {
533
- apiKey: null,
534
- apiUrl: DEFAULT_API_URL,
535
- activeWorkspaceId: null,
536
- activeProjectId: null,
537
- userEmail: null,
538
- memoryDir: null
539
- };
540
- }
703
+ // src/oauth-login.ts
704
+ import { spawn } from "node:child_process";
705
+ import { createHash as createHash3, randomBytes } from "node:crypto";
706
+ import { createServer } from "node:http";
707
+ function base64Url(buf) {
708
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
709
+ }
710
+ function generatePkce() {
711
+ const verifier = base64Url(randomBytes(32));
712
+ const challenge = base64Url(createHash3("sha256").update(verifier).digest());
713
+ return { verifier, challenge };
714
+ }
715
+ function oauthBaseFromApiUrl(apiUrl) {
716
+ return `${new URL(apiUrl).origin}/oauth`;
717
+ }
718
+ function openBrowser(url) {
719
+ const platform = process.platform;
720
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
721
+ const args = platform === "win32" ? ["/c", "start", "", url.replace(/&/g, "^&")] : [url];
541
722
  try {
542
- const data = readFileSync(configPath, "utf-8");
543
- const config = JSON.parse(data);
544
- return {
545
- apiKey: config.apiKey || null,
546
- apiUrl: config.apiUrl || DEFAULT_API_URL,
547
- activeWorkspaceId: config.activeWorkspaceId || null,
548
- activeProjectId: config.activeProjectId || null,
549
- userEmail: config.userEmail || null,
550
- memoryDir: config.memoryDir || null
551
- };
552
- } catch {
553
- return {
554
- apiKey: null,
555
- apiUrl: DEFAULT_API_URL,
556
- activeWorkspaceId: null,
557
- activeProjectId: null,
558
- userEmail: null,
559
- memoryDir: null
560
- };
561
- }
562
- }
563
- function saveConfig(config) {
564
- const configDir = getConfigDir();
565
- const configPath = getConfigPath();
566
- if (!existsSync(configDir)) {
567
- mkdirSync(configDir, { recursive: true, mode: 448 });
568
- }
569
- const existingConfig = loadConfig();
570
- const newConfig = { ...existingConfig, ...config };
571
- writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
572
- mode: 384
573
- });
723
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
724
+ child.on("error", () => {});
725
+ child.unref();
726
+ } catch {}
574
727
  }
575
- function loadLocalConfig(cwd) {
576
- const localConfigPath = getLocalConfigPath(cwd);
577
- if (!existsSync(localConfigPath)) {
578
- return null;
579
- }
728
+ async function loginWithBrowser(opts) {
729
+ const base = oauthBaseFromApiUrl(opts.apiUrl);
730
+ const { verifier, challenge } = generatePkce();
731
+ const state = base64Url(randomBytes(16));
732
+ const { server, port, waitForCode } = await startLoopbackServer(state);
733
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
580
734
  try {
581
- const data = readFileSync(localConfigPath, "utf-8");
582
- const config = JSON.parse(data);
583
- return {
584
- workspaceId: config.workspaceId || null,
585
- projectId: config.projectId || null
586
- };
587
- } catch {
588
- return null;
589
- }
590
- }
591
- function saveLocalConfig(config, cwd) {
592
- const localConfigPath = getLocalConfigPath(cwd);
593
- const existingConfig = loadLocalConfig(cwd) || {
594
- workspaceId: null,
595
- projectId: null
596
- };
597
- const newConfig = { ...existingConfig, ...config };
598
- const cleanConfig = {};
599
- if (newConfig.workspaceId)
600
- cleanConfig.workspaceId = newConfig.workspaceId;
601
- if (newConfig.projectId)
602
- cleanConfig.projectId = newConfig.projectId;
603
- writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
604
- }
605
- function hasLocalConfig(cwd) {
606
- return existsSync(getLocalConfigPath(cwd));
607
- }
608
- function getApiKey() {
609
- const config = loadConfig();
610
- if (!config.apiKey) {
611
- throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to set your API key.
612
- ` + "You can generate an API key at https://gethmy.com → Settings → API Keys.");
735
+ const clientId = await registerClient(base, redirectUri);
736
+ const authorizeUrl = new URL(`${base}/authorize`);
737
+ authorizeUrl.searchParams.set("response_type", "code");
738
+ authorizeUrl.searchParams.set("client_id", clientId);
739
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
740
+ authorizeUrl.searchParams.set("code_challenge", challenge);
741
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
742
+ authorizeUrl.searchParams.set("state", state);
743
+ authorizeUrl.searchParams.set("scope", "mcp");
744
+ authorizeUrl.searchParams.set("resource", MCP_RESOURCE_URL);
745
+ if (opts.workspaceId) {
746
+ authorizeUrl.searchParams.set("workspace_id", opts.workspaceId);
747
+ }
748
+ const authUrlStr = authorizeUrl.toString();
749
+ opts.onUrl?.(authUrlStr);
750
+ openBrowser(authUrlStr);
751
+ const code = await waitForCode;
752
+ return await exchangeCode(base, {
753
+ code,
754
+ redirectUri,
755
+ clientId,
756
+ verifier
757
+ });
758
+ } finally {
759
+ server.close();
613
760
  }
614
- return config.apiKey;
615
- }
616
- function getApiUrl() {
617
- const config = loadConfig();
618
- return config.apiUrl;
619
761
  }
620
- function getUserEmail() {
621
- const config = loadConfig();
622
- return config.userEmail;
623
- }
624
- function setActiveWorkspace(workspaceId, options) {
625
- if (options?.local) {
626
- saveLocalConfig({ workspaceId }, options.cwd);
627
- } else {
628
- saveConfig({ activeWorkspaceId: workspaceId });
762
+ async function registerClient(base, redirectUri) {
763
+ const res = await fetch(`${base}/register`, {
764
+ method: "POST",
765
+ headers: { "Content-Type": "application/json" },
766
+ body: JSON.stringify({
767
+ client_name: "Harmony CLI (setup)",
768
+ redirect_uris: [redirectUri],
769
+ grant_types: ["authorization_code", "refresh_token"],
770
+ response_types: ["code"],
771
+ token_endpoint_auth_method: "none"
772
+ })
773
+ });
774
+ const body = await res.json().catch(() => ({}));
775
+ if (!res.ok || !body.client_id) {
776
+ throw new Error(body.error_description || `Could not register OAuth client (HTTP ${res.status})`);
629
777
  }
778
+ return body.client_id;
630
779
  }
631
- function setActiveProject(projectId, options) {
632
- if (options?.local) {
633
- saveLocalConfig({ projectId }, options.cwd);
634
- } else {
635
- saveConfig({ activeProjectId: projectId });
780
+ async function exchangeCode(base, params) {
781
+ const res = await fetch(`${base}/token`, {
782
+ method: "POST",
783
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
784
+ body: new URLSearchParams({
785
+ grant_type: "authorization_code",
786
+ code: params.code,
787
+ redirect_uri: params.redirectUri,
788
+ client_id: params.clientId,
789
+ code_verifier: params.verifier
790
+ }).toString()
791
+ });
792
+ const body = await res.json().catch(() => ({}));
793
+ if (!res.ok || !body.access_token || !body.refresh_token) {
794
+ throw new Error(body.error_description || `Token exchange failed (HTTP ${res.status})`);
636
795
  }
796
+ return {
797
+ accessToken: body.access_token,
798
+ refreshToken: body.refresh_token,
799
+ expiresAt: Date.now() + (body.expires_in ?? 0) * 1000,
800
+ clientId: params.clientId
801
+ };
637
802
  }
638
- function getActiveWorkspaceId(cwd) {
639
- const localConfig = loadLocalConfig(cwd);
640
- if (localConfig?.workspaceId) {
641
- return localConfig.workspaceId;
642
- }
643
- return loadConfig().activeWorkspaceId;
803
+ function startLoopbackServer(expectedState) {
804
+ return new Promise((resolveOuter, rejectOuter) => {
805
+ let resolveCode;
806
+ let rejectCode;
807
+ const waitForCode = new Promise((res, rej) => {
808
+ resolveCode = res;
809
+ rejectCode = rej;
810
+ });
811
+ const timeout = setTimeout(() => {
812
+ rejectCode(new Error("Timed out waiting for browser authorization (5 min)."));
813
+ }, LOGIN_TIMEOUT_MS);
814
+ timeout.unref?.();
815
+ const server = createServer((req, res) => {
816
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
817
+ if (url.pathname !== "/callback") {
818
+ res.writeHead(404).end();
819
+ return;
820
+ }
821
+ clearTimeout(timeout);
822
+ const error = url.searchParams.get("error");
823
+ const code = url.searchParams.get("code");
824
+ const state = url.searchParams.get("state");
825
+ const fail = (msg) => {
826
+ res.writeHead(400, { "Content-Type": "text/html" }).end(FAILURE_HTML);
827
+ rejectCode(new Error(msg));
828
+ };
829
+ if (error) {
830
+ return fail(`Authorization denied: ${url.searchParams.get("error_description") || error}`);
831
+ }
832
+ if (!state || state !== expectedState) {
833
+ return fail("State mismatch — possible CSRF, aborting.");
834
+ }
835
+ if (!code) {
836
+ return fail("No authorization code returned.");
837
+ }
838
+ res.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_HTML);
839
+ resolveCode(code);
840
+ });
841
+ server.on("error", (err) => {
842
+ clearTimeout(timeout);
843
+ rejectOuter(err);
844
+ });
845
+ server.listen(0, "127.0.0.1", () => {
846
+ const addr = server.address();
847
+ resolveOuter({ server, port: addr.port, waitForCode });
848
+ });
849
+ });
644
850
  }
645
- function getActiveProjectId(cwd) {
646
- const localConfig = loadLocalConfig(cwd);
647
- if (localConfig?.projectId) {
648
- return localConfig.projectId;
649
- }
650
- return loadConfig().activeProjectId;
851
+ var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS, SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Harmony connected</title></head>
852
+ <body style="font-family:system-ui,sans-serif;background:#0f1729;color:#dce4ef;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
853
+ <div style="text-align:center"><h1 style="color:#57b8a5">✓ Connected to Harmony</h1>
854
+ <p>You can close this tab and return to your terminal.</p></div></body></html>`, FAILURE_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Harmony</title></head>
855
+ <body style="font-family:system-ui,sans-serif;background:#0f1729;color:#dce4ef;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
856
+ <div style="text-align:center"><h1 style="color:#ff6b6b">Authorization failed</h1>
857
+ <p>Return to your terminal for details.</p></div></body></html>`;
858
+ var init_oauth_login = __esm(() => {
859
+ MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
860
+ LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
861
+ });
862
+
863
+ // src/oauth-refresh.ts
864
+ var exports_oauth_refresh = {};
865
+ __export(exports_oauth_refresh, {
866
+ refreshOAuthToken: () => refreshOAuthToken
867
+ });
868
+ import {
869
+ closeSync,
870
+ openSync,
871
+ renameSync,
872
+ rmSync as rmSync2,
873
+ statSync,
874
+ writeSync
875
+ } from "node:fs";
876
+ import { join as join3 } from "node:path";
877
+ function lockPath() {
878
+ return join3(getConfigDir(), LOCK_FILENAME);
651
879
  }
652
- function isConfigured() {
653
- const config = loadConfig();
654
- return !!config.apiKey;
880
+ function sleep(ms) {
881
+ return new Promise((resolve) => setTimeout(resolve, ms));
655
882
  }
656
- function areSkillsInstalled(cwd) {
657
- const home = homedir();
658
- const workingDir = cwd || process.cwd();
659
- const foundPaths = [];
660
- const globalSkillsDir = join(home, ".agents", "skills");
661
- const globalSkillPath = join(globalSkillsDir, "hmy", "SKILL.md");
662
- if (existsSync(globalSkillPath)) {
663
- foundPaths.push(globalSkillPath);
664
- return { installed: true, location: "global", paths: foundPaths };
665
- }
666
- const claudeGlobalSkill = join(home, ".claude", "skills", "hmy.md");
667
- if (existsSync(claudeGlobalSkill)) {
668
- foundPaths.push(claudeGlobalSkill);
669
- return { installed: true, location: "global", paths: foundPaths };
670
- }
671
- const claudeGlobalSkillAlt = join(home, ".claude", "skills", "hmy", "SKILL.md");
672
- if (existsSync(claudeGlobalSkillAlt)) {
673
- foundPaths.push(claudeGlobalSkillAlt);
674
- return { installed: true, location: "global", paths: foundPaths };
675
- }
676
- const localSkillPath = join(workingDir, ".claude", "skills", "hmy.md");
677
- if (existsSync(localSkillPath)) {
678
- foundPaths.push(localSkillPath);
679
- return { installed: true, location: "local", paths: foundPaths };
883
+ async function withRefreshLock(fn) {
884
+ const path = lockPath();
885
+ const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
886
+ let held = false;
887
+ while (Date.now() < deadline) {
888
+ try {
889
+ const fd = openSync(path, "wx");
890
+ writeSync(fd, String(process.pid));
891
+ closeSync(fd);
892
+ held = true;
893
+ break;
894
+ } catch (err) {
895
+ if (err.code !== "EEXIST") {
896
+ break;
897
+ }
898
+ try {
899
+ const age = Date.now() - statSync(path).mtimeMs;
900
+ if (age > LOCK_STALE_MS) {
901
+ const claim = `${path}.stale.${process.pid}`;
902
+ try {
903
+ renameSync(path, claim);
904
+ rmSync2(claim, { force: true });
905
+ } catch {}
906
+ continue;
907
+ }
908
+ } catch {
909
+ continue;
910
+ }
911
+ await sleep(LOCK_RETRY_MS);
912
+ }
680
913
  }
681
- const localSkillPathAlt = join(workingDir, ".claude", "skills", "hmy", "SKILL.md");
682
- if (existsSync(localSkillPathAlt)) {
683
- foundPaths.push(localSkillPathAlt);
684
- return { installed: true, location: "local", paths: foundPaths };
914
+ try {
915
+ return await fn();
916
+ } finally {
917
+ if (held) {
918
+ try {
919
+ rmSync2(path, { force: true });
920
+ } catch {}
921
+ }
685
922
  }
686
- return { installed: false, location: null, paths: [] };
687
923
  }
688
- function hasProjectContext(cwd) {
689
- const localConfig = loadLocalConfig(cwd);
690
- return !!(localConfig?.workspaceId || localConfig?.projectId);
924
+ function refreshOAuthToken() {
925
+ if (inFlight)
926
+ return inFlight;
927
+ inFlight = doRefresh().finally(() => {
928
+ inFlight = null;
929
+ });
930
+ return inFlight;
691
931
  }
692
- function getMemoryDir() {
693
- const config = loadConfig();
694
- if (config.memoryDir)
695
- return config.memoryDir;
696
- return join(homedir(), ".harmony", "memory");
932
+ async function doRefresh() {
933
+ const before = loadConfig();
934
+ if (!before.oauthRefreshToken || !before.oauthClientId)
935
+ return null;
936
+ return withRefreshLock(async () => {
937
+ const config = loadConfig();
938
+ if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
939
+ return config.oauthAccessToken;
940
+ }
941
+ if (!config.oauthRefreshToken || !config.oauthClientId)
942
+ return null;
943
+ const base = oauthBaseFromApiUrl(config.apiUrl);
944
+ let res;
945
+ try {
946
+ res = await fetch(`${base}/token`, {
947
+ method: "POST",
948
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
949
+ body: new URLSearchParams({
950
+ grant_type: "refresh_token",
951
+ refresh_token: config.oauthRefreshToken,
952
+ client_id: config.oauthClientId
953
+ }).toString()
954
+ });
955
+ } catch {
956
+ return null;
957
+ }
958
+ if (!res.ok) {
959
+ const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
960
+ if (errorCode === "invalid_grant") {
961
+ saveConfig({
962
+ oauthAccessToken: null,
963
+ oauthRefreshToken: null,
964
+ oauthExpiresAt: null,
965
+ oauthClientId: null
966
+ });
967
+ }
968
+ return null;
969
+ }
970
+ const body = await res.json().catch(() => null);
971
+ if (!body?.access_token || !body.refresh_token)
972
+ return null;
973
+ saveConfig({
974
+ oauthAccessToken: body.access_token,
975
+ oauthRefreshToken: body.refresh_token,
976
+ oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
977
+ });
978
+ return body.access_token;
979
+ });
697
980
  }
981
+ var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
982
+ var init_oauth_refresh = __esm(() => {
983
+ init_config();
984
+ init_oauth_login();
985
+ });
986
+
987
+ // src/cli.ts
988
+ init_config();
989
+ import { createRequire as createRequire2 } from "node:module";
990
+ import { program } from "commander";
698
991
 
699
992
  // src/server.ts
700
993
  import { readFile } from "node:fs/promises";
@@ -1205,6 +1498,7 @@ var TIMINGS = {
1205
1498
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
1206
1499
  };
1207
1500
  // src/api-client.ts
1501
+ init_config();
1208
1502
  var RETRY_CONFIG = {
1209
1503
  maxRetries: 3,
1210
1504
  baseDelayMs: 1000,
@@ -1224,7 +1518,7 @@ function getRetryDelay(attempt) {
1224
1518
  const delay = Math.min(RETRY_CONFIG.baseDelayMs * 2 ** attempt, RETRY_CONFIG.maxDelayMs);
1225
1519
  return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
1226
1520
  }
1227
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1521
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1228
1522
 
1229
1523
  class Semaphore {
1230
1524
  permits;
@@ -1289,10 +1583,12 @@ class HarmonyApiClient {
1289
1583
  apiKey;
1290
1584
  apiUrl;
1291
1585
  onUnauthorized;
1586
+ refreshCredential;
1292
1587
  constructor(options) {
1293
1588
  this.apiKey = options?.apiKey ?? getApiKey();
1294
1589
  this.apiUrl = options?.apiUrl ?? getApiUrl();
1295
1590
  this.onUnauthorized = options?.onUnauthorized;
1591
+ this.refreshCredential = options?.refreshCredential;
1296
1592
  }
1297
1593
  getApiUrl() {
1298
1594
  return this.apiUrl;
@@ -1322,6 +1618,7 @@ class HarmonyApiClient {
1322
1618
  async requestWithRetry(method, path, body, options) {
1323
1619
  const url = `${this.apiUrl}/v1${path}`;
1324
1620
  let lastError = null;
1621
+ let refreshed = false;
1325
1622
  const contentType = options?.contentType || "application/json";
1326
1623
  const accept = options?.accept || "application/json";
1327
1624
  for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
@@ -1350,6 +1647,15 @@ class HarmonyApiClient {
1350
1647
  if (!response.ok) {
1351
1648
  const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
1352
1649
  if (response.status === 401) {
1650
+ if (this.refreshCredential && !refreshed) {
1651
+ refreshed = true;
1652
+ const fresh = await this.refreshCredential();
1653
+ if (fresh) {
1654
+ this.apiKey = fresh;
1655
+ attempt--;
1656
+ continue;
1657
+ }
1658
+ }
1353
1659
  this.onUnauthorized?.();
1354
1660
  throw new HarmonyUnauthorizedError(errorMsg);
1355
1661
  }
@@ -1358,7 +1664,7 @@ class HarmonyApiClient {
1358
1664
  }
1359
1665
  lastError = new Error(errorMsg);
1360
1666
  if (attempt < RETRY_CONFIG.maxRetries) {
1361
- await sleep(getRetryDelay(attempt));
1667
+ await sleep2(getRetryDelay(attempt));
1362
1668
  continue;
1363
1669
  }
1364
1670
  throw lastError;
@@ -1372,7 +1678,7 @@ class HarmonyApiClient {
1372
1678
  if (!isRetryableError(error))
1373
1679
  throw lastError;
1374
1680
  if (attempt < RETRY_CONFIG.maxRetries) {
1375
- await sleep(getRetryDelay(attempt));
1681
+ await sleep2(getRetryDelay(attempt));
1376
1682
  }
1377
1683
  }
1378
1684
  }
@@ -1381,6 +1687,7 @@ class HarmonyApiClient {
1381
1687
  async requestRawWithRetry(method, path, body, options) {
1382
1688
  const url = `${this.apiUrl}/v1${path}`;
1383
1689
  let lastError = null;
1690
+ let refreshed = false;
1384
1691
  const contentType = options?.contentType || "application/json";
1385
1692
  const accept = options?.accept || "text/markdown";
1386
1693
  for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
@@ -1403,6 +1710,15 @@ class HarmonyApiClient {
1403
1710
  errorMsg = text || `API error: ${response.status}`;
1404
1711
  }
1405
1712
  if (response.status === 401) {
1713
+ if (this.refreshCredential && !refreshed) {
1714
+ refreshed = true;
1715
+ const fresh = await this.refreshCredential();
1716
+ if (fresh) {
1717
+ this.apiKey = fresh;
1718
+ attempt--;
1719
+ continue;
1720
+ }
1721
+ }
1406
1722
  this.onUnauthorized?.();
1407
1723
  throw new HarmonyUnauthorizedError(errorMsg);
1408
1724
  }
@@ -1411,7 +1727,7 @@ class HarmonyApiClient {
1411
1727
  }
1412
1728
  lastError = new Error(errorMsg);
1413
1729
  if (attempt < RETRY_CONFIG.maxRetries) {
1414
- await sleep(getRetryDelay(attempt));
1730
+ await sleep2(getRetryDelay(attempt));
1415
1731
  continue;
1416
1732
  }
1417
1733
  throw lastError;
@@ -1422,7 +1738,7 @@ class HarmonyApiClient {
1422
1738
  if (!isRetryableError(error))
1423
1739
  throw lastError;
1424
1740
  if (attempt < RETRY_CONFIG.maxRetries) {
1425
- await sleep(getRetryDelay(attempt));
1741
+ await sleep2(getRetryDelay(attempt));
1426
1742
  }
1427
1743
  }
1428
1744
  }
@@ -2004,7 +2320,12 @@ async function loadPromptModules() {
2004
2320
  var client2 = null;
2005
2321
  function getClient() {
2006
2322
  if (!client2) {
2007
- client2 = new HarmonyApiClient;
2323
+ client2 = new HarmonyApiClient({
2324
+ refreshCredential: async () => {
2325
+ const { refreshOAuthToken: refreshOAuthToken2 } = await Promise.resolve().then(() => (init_oauth_refresh(), exports_oauth_refresh));
2326
+ return refreshOAuthToken2();
2327
+ }
2328
+ });
2008
2329
  }
2009
2330
  return client2;
2010
2331
  }
@@ -2183,6 +2504,9 @@ async function autoEndSession(scope, client3, cardId, status) {
2183
2504
  } catch {}
2184
2505
  }
2185
2506
 
2507
+ // src/server.ts
2508
+ init_config();
2509
+
2186
2510
  // src/graph-expansion.ts
2187
2511
  async function autoExpandGraph(client3, entityId, title, content, _tags, workspaceId, projectId, maxRelations = 5) {
2188
2512
  try {
@@ -2193,14 +2517,14 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
2193
2517
  project_id: projectId,
2194
2518
  limit: 20
2195
2519
  });
2196
- candidates = entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
2520
+ candidates = entities.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4).slice(0, maxRelations);
2197
2521
  if (candidates.length === 0) {
2198
2522
  await new Promise((resolve) => setTimeout(resolve, 2000));
2199
2523
  const retry = await client3.searchMemoryEntities(workspaceId, query, {
2200
2524
  project_id: projectId,
2201
2525
  limit: 20
2202
2526
  });
2203
- candidates = retry.entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
2527
+ candidates = retry.entities.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4).slice(0, maxRelations);
2204
2528
  }
2205
2529
  let relationsCreated = 0;
2206
2530
  for (const candidate of candidates) {
@@ -2737,6 +3061,7 @@ function lintTags(tags) {
2737
3061
  }
2738
3062
 
2739
3063
  // src/onboard.ts
3064
+ init_config();
2740
3065
  async function onboardNewUser(params) {
2741
3066
  const {
2742
3067
  email,
@@ -2782,19 +3107,20 @@ import {
2782
3107
  existsSync as existsSync4,
2783
3108
  mkdirSync as mkdirSync3,
2784
3109
  readFileSync as readFileSync4,
2785
- renameSync,
3110
+ renameSync as renameSync2,
2786
3111
  writeFileSync as writeFileSync3
2787
3112
  } from "node:fs";
2788
3113
  import { homedir as homedir3 } from "node:os";
2789
- import { dirname, join as join4 } from "node:path";
3114
+ import { dirname, join as join5 } from "node:path";
3115
+ init_config();
2790
3116
 
2791
3117
  // src/hmy-config.ts
2792
3118
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
2793
3119
  import { homedir as homedir2 } from "node:os";
2794
- import { join as join3 } from "node:path";
3120
+ import { join as join4 } from "node:path";
2795
3121
  var DEFAULTS = { updateCheck: true, pin: null };
2796
3122
  function getHmyConfigPath() {
2797
- return join3(homedir2(), ".hmy", "config.yaml");
3123
+ return join4(homedir2(), ".hmy", "config.yaml");
2798
3124
  }
2799
3125
  function loadHmyConfig() {
2800
3126
  const path = getHmyConfigPath();
@@ -2937,7 +3263,7 @@ function atomicWrite(filePath, content) {
2937
3263
  mkdirSync3(dir, { recursive: true });
2938
3264
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
2939
3265
  writeFileSync3(tmp, content);
2940
- renameSync(tmp, filePath);
3266
+ renameSync2(tmp, filePath);
2941
3267
  }
2942
3268
  function hasMetadataVersion(content) {
2943
3269
  return parseSkillVersion(content) !== null;
@@ -2977,9 +3303,9 @@ function findSkillFiles(paths, knownNames) {
2977
3303
  }
2978
3304
  return results;
2979
3305
  }
2980
- var HMY_DIR = join4(homedir3(), ".hmy");
2981
- var HMY_VERSION_FILE = join4(HMY_DIR, "VERSION");
2982
- var LAST_CHECK_FILE = join4(HMY_DIR, "last-update-check");
3306
+ var HMY_DIR = join5(homedir3(), ".hmy");
3307
+ var HMY_VERSION_FILE = join5(HMY_DIR, "VERSION");
3308
+ var LAST_CHECK_FILE = join5(HMY_DIR, "last-update-check");
2983
3309
  var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
2984
3310
  function checkedRecently(now = Date.now()) {
2985
3311
  try {
@@ -3012,7 +3338,7 @@ async function refreshSkills(opts = {}) {
3012
3338
  const status = areSkillsInstalled();
3013
3339
  if (!status.installed)
3014
3340
  return { updated: false };
3015
- const client3 = new HarmonyApiClient;
3341
+ const client3 = getClient();
3016
3342
  const versionInfo = await client3.fetchSkillsVersion();
3017
3343
  recordCheck();
3018
3344
  const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
@@ -4875,7 +5201,7 @@ async function handleToolCall(name, args, deps) {
4875
5201
  case "harmony_delete_label": {
4876
5202
  const labelId = z.string().uuid().parse(args.labelId);
4877
5203
  const result = await client3.deleteLabel(labelId);
4878
- return { success: true, ...result };
5204
+ return { ...result };
4879
5205
  }
4880
5206
  case "harmony_add_label_to_card": {
4881
5207
  const cardId = z.string().uuid().parse(args.cardId);
@@ -5832,8 +6158,8 @@ async function handleToolCall(name, args, deps) {
5832
6158
  content: summary,
5833
6159
  type: "lesson",
5834
6160
  scope: "project",
5835
- workspaceId,
5836
- projectId: plan.project_id,
6161
+ workspace_id: workspaceId,
6162
+ project_id: plan.project_id,
5837
6163
  tags: ["plan", "archived"],
5838
6164
  confidence: 0.8
5839
6165
  });
@@ -6015,7 +6341,7 @@ class HarmonyMCPServer {
6015
6341
  }
6016
6342
 
6017
6343
  // src/tui/setup.ts
6018
- import { createHash as createHash3 } from "node:crypto";
6344
+ import { createHash as createHash4 } from "node:crypto";
6019
6345
  import {
6020
6346
  existsSync as existsSync8,
6021
6347
  lstatSync,
@@ -6024,20 +6350,22 @@ import {
6024
6350
  unlinkSync
6025
6351
  } from "node:fs";
6026
6352
  import { homedir as homedir6 } from "node:os";
6027
- import { dirname as dirname3, join as join7 } from "node:path";
6353
+ import { dirname as dirname3, join as join8 } from "node:path";
6028
6354
  import * as p3 from "@clack/prompts";
6355
+ init_config();
6356
+ init_oauth_login();
6029
6357
 
6030
6358
  // src/tui/agents.ts
6031
6359
  import { existsSync as existsSync5 } from "node:fs";
6032
6360
  import { homedir as homedir4 } from "node:os";
6033
- import { join as join5 } from "node:path";
6361
+ import { join as join6 } from "node:path";
6034
6362
  var AGENT_DEFINITIONS = [
6035
6363
  {
6036
6364
  id: "claude",
6037
6365
  name: "Claude Code",
6038
6366
  description: "Anthropic CLI agent",
6039
6367
  hint: "/hmy <card>",
6040
- globalPaths: [join5(homedir4(), ".claude")],
6368
+ globalPaths: [join6(homedir4(), ".claude")],
6041
6369
  localPaths: [".claude"]
6042
6370
  },
6043
6371
  {
@@ -6045,7 +6373,7 @@ var AGENT_DEFINITIONS = [
6045
6373
  name: "Codex",
6046
6374
  description: "OpenAI coding agent",
6047
6375
  hint: "/prompts:hmy <card>",
6048
- globalPaths: [join5(homedir4(), ".codex")],
6376
+ globalPaths: [join6(homedir4(), ".codex")],
6049
6377
  localPaths: ["AGENTS.md"]
6050
6378
  },
6051
6379
  {
@@ -6061,20 +6389,20 @@ var AGENT_DEFINITIONS = [
6061
6389
  name: "Windsurf",
6062
6390
  description: "Codeium AI IDE",
6063
6391
  hint: "MCP tools available automatically",
6064
- globalPaths: [join5(homedir4(), ".codeium", "windsurf")],
6392
+ globalPaths: [join6(homedir4(), ".codeium", "windsurf")],
6065
6393
  localPaths: [".windsurf", ".windsurfrules"]
6066
6394
  }
6067
6395
  ];
6068
6396
  function detectAgents(cwd = process.cwd()) {
6069
6397
  return AGENT_DEFINITIONS.map((def) => {
6070
6398
  const globalPath = def.globalPaths.find((p) => existsSync5(p)) || null;
6071
- const localPath = def.localPaths.find((p) => existsSync5(join5(cwd, p))) || null;
6399
+ const localPath = def.localPaths.find((p) => existsSync5(join6(cwd, p))) || null;
6072
6400
  return {
6073
6401
  id: def.id,
6074
6402
  name: def.name,
6075
6403
  detected: !!(globalPath || localPath),
6076
6404
  globalPath,
6077
- localPath: localPath ? join5(cwd, localPath) : null,
6405
+ localPath: localPath ? join6(cwd, localPath) : null,
6078
6406
  description: def.description,
6079
6407
  hint: def.hint
6080
6408
  };
@@ -6082,8 +6410,8 @@ function detectAgents(cwd = process.cwd()) {
6082
6410
  }
6083
6411
 
6084
6412
  // src/tui/docs.ts
6085
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync } from "node:fs";
6086
- import { isAbsolute, join as join6, resolve, sep as sep2 } from "node:path";
6413
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
6414
+ import { isAbsolute, join as join7, resolve, sep as sep2 } from "node:path";
6087
6415
  import * as p from "@clack/prompts";
6088
6416
 
6089
6417
  // src/tui/theme.ts
@@ -6188,7 +6516,7 @@ function listDirs(dirPath) {
6188
6516
  if (IGNORED_DIRS.has(entry) || entry.startsWith("."))
6189
6517
  return false;
6190
6518
  try {
6191
- return statSync(join6(dirPath, entry)).isDirectory();
6519
+ return statSync2(join7(dirPath, entry)).isDirectory();
6192
6520
  } catch {
6193
6521
  return false;
6194
6522
  }
@@ -6236,25 +6564,25 @@ function describeDir(name) {
6236
6564
  }
6237
6565
  function scanProject(cwd) {
6238
6566
  let packageManager = null;
6239
- if (existsSync6(join6(cwd, "bun.lock")) || existsSync6(join6(cwd, "bun.lockb"))) {
6567
+ if (existsSync6(join7(cwd, "bun.lock")) || existsSync6(join7(cwd, "bun.lockb"))) {
6240
6568
  packageManager = "bun";
6241
- } else if (existsSync6(join6(cwd, "pnpm-lock.yaml"))) {
6569
+ } else if (existsSync6(join7(cwd, "pnpm-lock.yaml"))) {
6242
6570
  packageManager = "pnpm";
6243
- } else if (existsSync6(join6(cwd, "yarn.lock"))) {
6571
+ } else if (existsSync6(join7(cwd, "yarn.lock"))) {
6244
6572
  packageManager = "yarn";
6245
- } else if (existsSync6(join6(cwd, "package.json"))) {
6573
+ } else if (existsSync6(join7(cwd, "package.json"))) {
6246
6574
  packageManager = "npm";
6247
6575
  }
6248
- const pkg = readJson(join6(cwd, "package.json"));
6576
+ const pkg = readJson(join7(cwd, "package.json"));
6249
6577
  const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
6250
6578
  let language = "unknown";
6251
- if (existsSync6(join6(cwd, "tsconfig.json"))) {
6579
+ if (existsSync6(join7(cwd, "tsconfig.json"))) {
6252
6580
  language = "typescript";
6253
- } else if (existsSync6(join6(cwd, "go.mod"))) {
6581
+ } else if (existsSync6(join7(cwd, "go.mod"))) {
6254
6582
  language = "go";
6255
- } else if (existsSync6(join6(cwd, "Cargo.toml"))) {
6583
+ } else if (existsSync6(join7(cwd, "Cargo.toml"))) {
6256
6584
  language = "rust";
6257
- } else if (existsSync6(join6(cwd, "setup.py")) || existsSync6(join6(cwd, "pyproject.toml"))) {
6585
+ } else if (existsSync6(join7(cwd, "setup.py")) || existsSync6(join7(cwd, "pyproject.toml"))) {
6258
6586
  language = "python";
6259
6587
  } else if (pkg) {
6260
6588
  language = "javascript";
@@ -6290,7 +6618,7 @@ function scanProject(cwd) {
6290
6618
  }
6291
6619
  }
6292
6620
  let linter = null;
6293
- if (existsSync6(join6(cwd, "biome.json")) || existsSync6(join6(cwd, "biome.jsonc"))) {
6621
+ if (existsSync6(join7(cwd, "biome.json")) || existsSync6(join7(cwd, "biome.jsonc"))) {
6294
6622
  linter = "biome";
6295
6623
  } else {
6296
6624
  const eslintFiles = [
@@ -6305,7 +6633,7 @@ function scanProject(cwd) {
6305
6633
  "eslint.config.cjs",
6306
6634
  "eslint.config.ts"
6307
6635
  ];
6308
- if (eslintFiles.some((f) => existsSync6(join6(cwd, f)))) {
6636
+ if (eslintFiles.some((f) => existsSync6(join7(cwd, f)))) {
6309
6637
  linter = "eslint";
6310
6638
  } else {
6311
6639
  const prettierFiles = [
@@ -6317,13 +6645,13 @@ function scanProject(cwd) {
6317
6645
  "prettier.config.js",
6318
6646
  "prettier.config.mjs"
6319
6647
  ];
6320
- if (prettierFiles.some((f) => existsSync6(join6(cwd, f)))) {
6648
+ if (prettierFiles.some((f) => existsSync6(join7(cwd, f)))) {
6321
6649
  linter = "prettier";
6322
6650
  }
6323
6651
  }
6324
6652
  }
6325
6653
  let indentStyle = null;
6326
- const biome = readJson(join6(cwd, "biome.json")) ?? readJson(join6(cwd, "biome.jsonc"));
6654
+ const biome = readJson(join7(cwd, "biome.json")) ?? readJson(join7(cwd, "biome.jsonc"));
6327
6655
  if (biome) {
6328
6656
  const formatter = biome.formatter;
6329
6657
  if (formatter) {
@@ -6333,7 +6661,7 @@ function scanProject(cwd) {
6333
6661
  }
6334
6662
  }
6335
6663
  if (!indentStyle) {
6336
- const editorConfig = readText(join6(cwd, ".editorconfig"));
6664
+ const editorConfig = readText(join7(cwd, ".editorconfig"));
6337
6665
  if (editorConfig) {
6338
6666
  const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
6339
6667
  const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
@@ -6346,13 +6674,13 @@ function scanProject(cwd) {
6346
6674
  }
6347
6675
  }
6348
6676
  const dirs = listDirs(cwd);
6349
- const srcDirs = existsSync6(join6(cwd, "src")) ? listDirs(join6(cwd, "src")) : [];
6350
- const monorepo = existsSync6(join6(cwd, "packages")) || existsSync6(join6(cwd, "apps"));
6677
+ const srcDirs = existsSync6(join7(cwd, "src")) ? listDirs(join7(cwd, "src")) : [];
6678
+ const monorepo = existsSync6(join7(cwd, "packages")) || existsSync6(join7(cwd, "apps"));
6351
6679
  const existingDocs = {
6352
- agentsMd: existsSync6(join6(cwd, "AGENTS.md")),
6353
- claudeMd: existsSync6(join6(cwd, "CLAUDE.md")),
6354
- docsDir: existsSync6(join6(cwd, "docs")),
6355
- architectureMd: existsSync6(join6(cwd, "docs", "architecture.md"))
6680
+ agentsMd: existsSync6(join7(cwd, "AGENTS.md")),
6681
+ claudeMd: existsSync6(join7(cwd, "CLAUDE.md")),
6682
+ docsDir: existsSync6(join7(cwd, "docs")),
6683
+ architectureMd: existsSync6(join7(cwd, "docs", "architecture.md"))
6356
6684
  };
6357
6685
  return {
6358
6686
  packageManager,
@@ -6503,9 +6831,9 @@ var VAGUE_STANDARDS = [
6503
6831
  ];
6504
6832
  function verifyDocs(cwd) {
6505
6833
  const issues = [];
6506
- const claudeMd = readText(join6(cwd, "CLAUDE.md"));
6507
- const agentsMd = readText(join6(cwd, "AGENTS.md"));
6508
- const pkg = readJson(join6(cwd, "package.json"));
6834
+ const claudeMd = readText(join7(cwd, "CLAUDE.md"));
6835
+ const agentsMd = readText(join7(cwd, "AGENTS.md"));
6836
+ const pkg = readJson(join7(cwd, "package.json"));
6509
6837
  const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
6510
6838
  const projectRoot = resolve(cwd);
6511
6839
  if (claudeMd) {
@@ -6671,7 +6999,7 @@ function verifyDocs(cwd) {
6671
6999
  }
6672
7000
  checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
6673
7001
  }
6674
- const archMd = readText(join6(cwd, "docs", "architecture.md"));
7002
+ const archMd = readText(join7(cwd, "docs", "architecture.md"));
6675
7003
  if (archMd) {
6676
7004
  checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
6677
7005
  }
@@ -6741,18 +7069,18 @@ async function runDocsStep(cwd) {
6741
7069
  }
6742
7070
  const files = [];
6743
7071
  files.push({
6744
- path: join6(cwd, "AGENTS.md"),
7072
+ path: join7(cwd, "AGENTS.md"),
6745
7073
  content: generateAgentsMd(info, cwd),
6746
7074
  type: "text"
6747
7075
  });
6748
7076
  files.push({
6749
- path: join6(cwd, "CLAUDE.md"),
7077
+ path: join7(cwd, "CLAUDE.md"),
6750
7078
  content: generateClaudeMd(info),
6751
7079
  type: "text"
6752
7080
  });
6753
7081
  if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
6754
7082
  files.push({
6755
- path: join6(cwd, "docs", "architecture.md"),
7083
+ path: join7(cwd, "docs", "architecture.md"),
6756
7084
  content: generateArchitectureMd(info, cwd),
6757
7085
  type: "text"
6758
7086
  });
@@ -7016,7 +7344,7 @@ var SAFE_HARMONY_TOOLS = [
7016
7344
  "harmony_process_command",
7017
7345
  "harmony_sync"
7018
7346
  ];
7019
- var GLOBAL_SKILLS_DIR = join7(homedir6(), ".agents", "skills");
7347
+ var GLOBAL_SKILLS_DIR = join8(homedir6(), ".agents", "skills");
7020
7348
  var API_URL = "https://app.gethmy.com/api";
7021
7349
  async function registerMcpServer() {
7022
7350
  try {
@@ -7031,7 +7359,7 @@ async function registerMcpServer() {
7031
7359
  }
7032
7360
  async function writeMcpConfigFallback(home) {
7033
7361
  const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
7034
- const settingsPath = join7(home, ".claude", "settings.json");
7362
+ const settingsPath = join8(home, ".claude", "settings.json");
7035
7363
  const settingsDir = dirname3(settingsPath);
7036
7364
  if (!existsSync9(settingsDir)) {
7037
7365
  mkdirSync6(settingsDir, { recursive: true });
@@ -7050,7 +7378,7 @@ async function writeMcpConfigFallback(home) {
7050
7378
  }
7051
7379
  async function allowlistHarmonyTools(home, allowAll) {
7052
7380
  const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
7053
- const settingsPath = join7(home, ".claude", "settings.json");
7381
+ const settingsPath = join8(home, ".claude", "settings.json");
7054
7382
  const settingsDir = dirname3(settingsPath);
7055
7383
  if (!existsSync9(settingsDir)) {
7056
7384
  mkdirSync6(settingsDir, { recursive: true });
@@ -7168,17 +7496,17 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
7168
7496
  const content = buildSkillFile(fetched);
7169
7497
  if (installMode === "global") {
7170
7498
  files.push({
7171
- path: join7(GLOBAL_SKILLS_DIR, name, "SKILL.md"),
7499
+ path: join8(GLOBAL_SKILLS_DIR, name, "SKILL.md"),
7172
7500
  content,
7173
7501
  type: "text"
7174
7502
  });
7175
7503
  symlinks.push({
7176
- target: join7(GLOBAL_SKILLS_DIR, name),
7177
- link: join7(home, ".claude", "skills", name)
7504
+ target: join8(GLOBAL_SKILLS_DIR, name),
7505
+ link: join8(home, ".claude", "skills", name)
7178
7506
  });
7179
7507
  } else {
7180
7508
  files.push({
7181
- path: join7(cwd, ".claude", "skills", name, "SKILL.md"),
7509
+ path: join8(cwd, ".claude", "skills", name, "SKILL.md"),
7182
7510
  content,
7183
7511
  type: "text"
7184
7512
  });
@@ -7196,18 +7524,18 @@ ${summary}`);
7196
7524
  }
7197
7525
  try {
7198
7526
  const updateCheckFetched = await client3.fetchSkill("hmy-update-check");
7199
- const actualHash = createHash3("sha256").update(updateCheckFetched.content).digest("hex");
7527
+ const actualHash = createHash4("sha256").update(updateCheckFetched.content).digest("hex");
7200
7528
  if (actualHash !== updateCheckFetched.sha256) {
7201
7529
  throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
7202
7530
  }
7203
7531
  files.push({
7204
- path: join7(home, ".hmy", "bin", "hmy-update-check"),
7532
+ path: join8(home, ".hmy", "bin", "hmy-update-check"),
7205
7533
  content: updateCheckFetched.content,
7206
7534
  type: "text",
7207
7535
  mode: 493
7208
7536
  });
7209
7537
  files.push({
7210
- path: join7(home, ".hmy", "VERSION"),
7538
+ path: join8(home, ".hmy", "VERSION"),
7211
7539
  content: versionInfo.version,
7212
7540
  type: "text"
7213
7541
  });
@@ -7253,7 +7581,7 @@ Skip if: work was already started with a card reference, or no matching card exi
7253
7581
  - \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
7254
7582
  `;
7255
7583
  files.push({
7256
- path: join7(cwd, "AGENTS.md"),
7584
+ path: join8(cwd, "AGENTS.md"),
7257
7585
  content: agentsContent,
7258
7586
  type: "text"
7259
7587
  });
@@ -7270,17 +7598,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent
7270
7598
  `;
7271
7599
  if (installMode === "global") {
7272
7600
  files.push({
7273
- path: join7(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
7601
+ path: join8(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
7274
7602
  content: promptContent,
7275
7603
  type: "text"
7276
7604
  });
7277
7605
  symlinks.push({
7278
- target: join7(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
7279
- link: join7(home, ".codex", "prompts", "hmy.md")
7606
+ target: join8(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
7607
+ link: join8(home, ".codex", "prompts", "hmy.md")
7280
7608
  });
7281
7609
  } else {
7282
7610
  files.push({
7283
- path: join7(home, ".codex", "prompts", "hmy.md"),
7611
+ path: join8(home, ".codex", "prompts", "hmy.md"),
7284
7612
  content: promptContent,
7285
7613
  type: "text"
7286
7614
  });
@@ -7292,7 +7620,7 @@ command = "npx"
7292
7620
  args = ["-y", "@gethmy/mcp@latest", "serve"]
7293
7621
  `;
7294
7622
  files.push({
7295
- path: join7(home, ".codex", "config.toml"),
7623
+ path: join8(home, ".codex", "config.toml"),
7296
7624
  content: tomlContent,
7297
7625
  type: "toml",
7298
7626
  tomlSection: "mcp_servers.harmony"
@@ -7301,7 +7629,7 @@ args = ["-y", "@gethmy/mcp@latest", "serve"]
7301
7629
  }
7302
7630
  case "cursor": {
7303
7631
  files.push({
7304
- path: join7(cwd, ".cursor", "mcp.json"),
7632
+ path: join8(cwd, ".cursor", "mcp.json"),
7305
7633
  content: JSON.stringify({
7306
7634
  mcpServers: {
7307
7635
  harmony: {
@@ -7327,17 +7655,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
7327
7655
  `;
7328
7656
  if (installMode === "global") {
7329
7657
  files.push({
7330
- path: join7(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
7658
+ path: join8(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
7331
7659
  content: ruleContent,
7332
7660
  type: "text"
7333
7661
  });
7334
7662
  symlinks.push({
7335
- target: join7(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
7336
- link: join7(home, ".cursor", "rules", "harmony.mdc")
7663
+ target: join8(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
7664
+ link: join8(home, ".cursor", "rules", "harmony.mdc")
7337
7665
  });
7338
7666
  } else {
7339
7667
  files.push({
7340
- path: join7(cwd, ".cursor", "rules", "harmony.mdc"),
7668
+ path: join8(cwd, ".cursor", "rules", "harmony.mdc"),
7341
7669
  content: ruleContent,
7342
7670
  type: "text"
7343
7671
  });
@@ -7346,7 +7674,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
7346
7674
  }
7347
7675
  case "windsurf": {
7348
7676
  files.push({
7349
- path: join7(home, ".codeium", "windsurf", "mcp_config.json"),
7677
+ path: join8(home, ".codeium", "windsurf", "mcp_config.json"),
7350
7678
  content: JSON.stringify({
7351
7679
  mcpServers: {
7352
7680
  harmony: {
@@ -7372,17 +7700,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
7372
7700
  `;
7373
7701
  if (installMode === "global") {
7374
7702
  files.push({
7375
- path: join7(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
7703
+ path: join8(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
7376
7704
  content: ruleContent,
7377
7705
  type: "text"
7378
7706
  });
7379
7707
  symlinks.push({
7380
- target: join7(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
7381
- link: join7(home, ".codeium", "windsurf", "rules", "harmony.md")
7708
+ target: join8(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
7709
+ link: join8(home, ".codeium", "windsurf", "rules", "harmony.md")
7382
7710
  });
7383
7711
  } else {
7384
7712
  files.push({
7385
- path: join7(cwd, ".windsurf", "rules", "harmony.md"),
7713
+ path: join8(cwd, ".windsurf", "rules", "harmony.md"),
7386
7714
  content: ruleContent,
7387
7715
  type: "text"
7388
7716
  });
@@ -7407,39 +7735,84 @@ async function runSetup(options = {}) {
7407
7735
  if (options.workspaceId || options.projectId || options.projectSlug) {
7408
7736
  needsContext = true;
7409
7737
  }
7410
- let apiKey = options.apiKey || existingConfig.apiKey;
7738
+ let apiKey = options.apiKey || existingConfig.oauthAccessToken || existingConfig.apiKey;
7411
7739
  let userEmail = options.userEmail || existingConfig.userEmail || undefined;
7412
7740
  let selectedWorkspaceIdFromSignup;
7413
7741
  let selectedProjectIdFromSignup;
7414
7742
  let selectedWorkspaceNameFromSignup;
7415
7743
  let selectedProjectNameFromSignup;
7416
7744
  let createdNewAccount = false;
7745
+ let oauthTokens;
7746
+ if (options.apiKey) {
7747
+ p3.log.warn(colors.warning(`--api-key is deprecated and insecure: the key is exposed in your shell
7748
+ history, terminal scrollback, and the process list. Prefer the browser
7749
+ sign-in (run \`npx @gethmy/mcp setup\` with no --api-key). Use --api-key
7750
+ only for unattended CI where you accept that risk.`));
7751
+ }
7417
7752
  if (needsApiKey || !apiKey || !apiKey.startsWith("hmy_")) {
7418
7753
  let useNewAccount = options.newAccount === true;
7754
+ let useBrowserAuth = false;
7419
7755
  if (!useNewAccount && options.apiKey) {
7420
7756
  useNewAccount = false;
7421
7757
  } else if (!useNewAccount && !options.apiKey) {
7422
7758
  const getStarted = await p3.select({
7423
- message: "How would you like to get started?",
7759
+ message: "How would you like to connect?",
7424
7760
  options: [
7761
+ {
7762
+ value: "browser",
7763
+ label: "Sign in with your browser",
7764
+ hint: "recommended — secure, no key handling"
7765
+ },
7425
7766
  {
7426
7767
  value: "create",
7427
- label: "Create a free account",
7428
- hint: "recommended for new users"
7768
+ label: "Create a free account"
7429
7769
  },
7430
7770
  {
7431
7771
  value: "apikey",
7432
- label: "I already have an API key"
7772
+ label: "Paste an API key",
7773
+ hint: "advanced / CI"
7433
7774
  }
7434
- ]
7775
+ ],
7776
+ initialValue: "browser"
7435
7777
  });
7436
7778
  if (p3.isCancel(getStarted)) {
7437
7779
  p3.cancel("Setup cancelled");
7438
7780
  process.exit(0);
7439
7781
  }
7440
7782
  useNewAccount = getStarted === "create";
7783
+ useBrowserAuth = getStarted === "browser";
7441
7784
  }
7442
- if (useNewAccount) {
7785
+ if (useBrowserAuth) {
7786
+ const spinner4 = p3.spinner();
7787
+ spinner4.start("Opening your browser to authorize…");
7788
+ try {
7789
+ oauthTokens = await loginWithBrowser({
7790
+ apiUrl: API_URL,
7791
+ workspaceId: options.workspaceId,
7792
+ onUrl: (url) => {
7793
+ spinner4.message(`Authorize in your browser. If it didn't open:
7794
+ ${colors.dim(url)}`);
7795
+ }
7796
+ });
7797
+ apiKey = oauthTokens.accessToken;
7798
+ needsApiKey = true;
7799
+ saveConfig({
7800
+ apiKey: null,
7801
+ oauthAccessToken: oauthTokens.accessToken,
7802
+ oauthRefreshToken: oauthTokens.refreshToken,
7803
+ oauthExpiresAt: oauthTokens.expiresAt,
7804
+ oauthClientId: oauthTokens.clientId,
7805
+ apiUrl: API_URL
7806
+ });
7807
+ spinner4.stop(colors.success("Authorized"));
7808
+ } catch (error) {
7809
+ spinner4.stop(colors.error("Browser authorization failed"));
7810
+ const msg = error instanceof Error ? error.message : "Unknown error";
7811
+ p3.log.error(msg);
7812
+ p3.log.info("You can retry, or run with --api-key for unattended setup.");
7813
+ process.exit(1);
7814
+ }
7815
+ } else if (useNewAccount) {
7443
7816
  const fullName = options.name || await p3.text({
7444
7817
  message: "Full name",
7445
7818
  placeholder: "Jane Smith",
@@ -7520,6 +7893,9 @@ async function runSetup(options = {}) {
7520
7893
  }
7521
7894
  process.exit(1);
7522
7895
  }
7896
+ } else if (options.apiKey) {
7897
+ apiKey = options.apiKey;
7898
+ needsApiKey = true;
7523
7899
  } else {
7524
7900
  const keyInput = await p3.text({
7525
7901
  message: "Enter your Harmony API key",
@@ -7703,15 +8079,26 @@ async function runSetup(options = {}) {
7703
8079
  const allFiles = [];
7704
8080
  const allSymlinks = [];
7705
8081
  if (needsApiKey || !alreadyConfigured) {
8082
+ const configToWrite = oauthTokens ? {
8083
+ apiKey: null,
8084
+ apiUrl: API_URL,
8085
+ userEmail: userEmail || null,
8086
+ activeWorkspaceId: null,
8087
+ activeProjectId: null,
8088
+ oauthAccessToken: oauthTokens.accessToken,
8089
+ oauthRefreshToken: oauthTokens.refreshToken,
8090
+ oauthExpiresAt: oauthTokens.expiresAt,
8091
+ oauthClientId: oauthTokens.clientId
8092
+ } : {
8093
+ apiKey,
8094
+ apiUrl: API_URL,
8095
+ userEmail: userEmail || null,
8096
+ activeWorkspaceId: null,
8097
+ activeProjectId: null
8098
+ };
7706
8099
  allFiles.push({
7707
8100
  path: getConfigPath(),
7708
- content: JSON.stringify({
7709
- apiKey,
7710
- apiUrl: API_URL,
7711
- userEmail: userEmail || null,
7712
- activeWorkspaceId: null,
7713
- activeProjectId: null
7714
- }, null, 2),
8101
+ content: JSON.stringify(configToWrite, null, 2),
7715
8102
  type: "text"
7716
8103
  });
7717
8104
  }
@@ -7734,7 +8121,11 @@ async function runSetup(options = {}) {
7734
8121
  console.log("");
7735
8122
  p3.log.step("Summary");
7736
8123
  console.log("");
7737
- console.log(` ${colors.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
8124
+ if (oauthTokens) {
8125
+ console.log(` ${colors.bold("Credential:")} Browser sign-in (OAuth, workspace-scoped)`);
8126
+ } else {
8127
+ console.log(` ${colors.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
8128
+ }
7738
8129
  if (userEmail) {
7739
8130
  console.log(` ${colors.bold("Email:")} ${userEmail}`);
7740
8131
  }
@@ -7831,7 +8222,7 @@ async function runSetup(options = {}) {
7831
8222
  } else {
7832
8223
  try {
7833
8224
  await writeMcpConfigFallback(home);
7834
- console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join7(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
8225
+ console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
7835
8226
  } catch {
7836
8227
  p3.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
7837
8228
  }
@@ -7849,7 +8240,7 @@ async function runSetup(options = {}) {
7849
8240
  try {
7850
8241
  const result = await allowlistHarmonyTools(home, allowAll);
7851
8242
  const scope = allowAll ? "all tools" : "safe tools";
7852
- console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join7(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
8243
+ console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
7853
8244
  } catch {
7854
8245
  p3.log.warning("Could not allowlist Harmony tools. Run /permissions in Claude Code and choose “always allow” for Harmony, or add mcp__harmony to permissions.allow in ~/.claude/settings.json.");
7855
8246
  }
@@ -7941,7 +8332,11 @@ program.command("status").description("Show configuration status").action(() =>
7941
8332
  console.log(`Status: Configured
7942
8333
  `);
7943
8334
  console.log("API:");
7944
- console.log(` Key: ${globalConfig.apiKey?.slice(0, 8)}...`);
8335
+ if (globalConfig.oauthAccessToken) {
8336
+ console.log(" Credential: Browser sign-in (OAuth, workspace-scoped)");
8337
+ } else {
8338
+ console.log(` Key: ${globalConfig.apiKey?.slice(0, 8)}...`);
8339
+ }
7945
8340
  console.log(` URL: ${globalConfig.apiUrl}`);
7946
8341
  console.log(` Email: ${globalConfig.userEmail ? maskEmail(globalConfig.userEmail) : "(not set)"}`);
7947
8342
  console.log(`
@@ -7985,13 +8380,17 @@ program.command("reset").description("Remove stored configuration").action(() =>
7985
8380
  apiKey: null,
7986
8381
  activeWorkspaceId: null,
7987
8382
  activeProjectId: null,
7988
- userEmail: null
8383
+ userEmail: null,
8384
+ oauthAccessToken: null,
8385
+ oauthRefreshToken: null,
8386
+ oauthExpiresAt: null,
8387
+ oauthClientId: null
7989
8388
  });
7990
8389
  console.log("Configuration reset successfully");
7991
8390
  console.log(`
7992
8391
  To reconfigure, run: npx @gethmy/mcp setup`);
7993
8392
  });
7994
- 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>", "API key (skips prompt)").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) => {
8393
+ 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) => {
7995
8394
  await runSetup({
7996
8395
  force: options.force,
7997
8396
  apiKey: options.apiKey,