@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/index.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 as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { join as join2 } from "node:path";
24
+ function getConfigDir() {
25
+ return join2(homedir(), ".harmony-mcp");
26
+ }
27
+ function getConfigPath() {
28
+ return join2(getConfigDir(), "config.json");
29
+ }
30
+ function getLocalConfigPath(cwd) {
31
+ return join2(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 (!existsSync2(configPath)) {
50
+ return emptyConfig();
51
+ }
52
+ try {
53
+ const data = readFileSync2(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 (!existsSync2(configDir)) {
75
+ mkdirSync2(configDir, { recursive: true, mode: 448 });
76
+ }
77
+ const existingConfig = loadConfig();
78
+ const newConfig = { ...existingConfig, ...config };
79
+ writeFileSync2(configPath, JSON.stringify(newConfig, null, 2), {
80
+ mode: 384
81
+ });
82
+ }
83
+ function loadLocalConfig(cwd) {
84
+ const localConfigPath = getLocalConfigPath(cwd);
85
+ if (!existsSync2(localConfigPath)) {
86
+ return null;
87
+ }
88
+ try {
89
+ const data = readFileSync2(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
+ writeFileSync2(localConfigPath, JSON.stringify(cleanConfig, null, 2));
112
+ }
113
+ function hasLocalConfig(cwd) {
114
+ return existsSync2(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 = join2(home, ".agents", "skills");
173
+ const globalSkillPath = join2(globalSkillsDir, "hmy", "SKILL.md");
174
+ if (existsSync2(globalSkillPath)) {
175
+ foundPaths.push(globalSkillPath);
176
+ return { installed: true, location: "global", paths: foundPaths };
177
+ }
178
+ const claudeGlobalSkill = join2(home, ".claude", "skills", "hmy.md");
179
+ if (existsSync2(claudeGlobalSkill)) {
180
+ foundPaths.push(claudeGlobalSkill);
181
+ return { installed: true, location: "global", paths: foundPaths };
182
+ }
183
+ const claudeGlobalSkillAlt = join2(home, ".claude", "skills", "hmy", "SKILL.md");
184
+ if (existsSync2(claudeGlobalSkillAlt)) {
185
+ foundPaths.push(claudeGlobalSkillAlt);
186
+ return { installed: true, location: "global", paths: foundPaths };
187
+ }
188
+ const localSkillPath = join2(workingDir, ".claude", "skills", "hmy.md");
189
+ if (existsSync2(localSkillPath)) {
190
+ foundPaths.push(localSkillPath);
191
+ return { installed: true, location: "local", paths: foundPaths };
192
+ }
193
+ const localSkillPathAlt = join2(workingDir, ".claude", "skills", "hmy", "SKILL.md");
194
+ if (existsSync2(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 join2(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,6 +700,290 @@ var init_prompt_builder = __esm(() => {
507
700
  };
508
701
  });
509
702
 
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];
722
+ try {
723
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
724
+ child.on("error", () => {});
725
+ child.unref();
726
+ } catch {}
727
+ }
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`;
734
+ try {
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();
760
+ }
761
+ }
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})`);
777
+ }
778
+ return body.client_id;
779
+ }
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})`);
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
+ };
802
+ }
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
+ });
850
+ }
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);
879
+ }
880
+ function sleep(ms) {
881
+ return new Promise((resolve) => setTimeout(resolve, ms));
882
+ }
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
+ }
913
+ }
914
+ try {
915
+ return await fn();
916
+ } finally {
917
+ if (held) {
918
+ try {
919
+ rmSync2(path, { force: true });
920
+ } catch {}
921
+ }
922
+ }
923
+ }
924
+ function refreshOAuthToken() {
925
+ if (inFlight)
926
+ return inFlight;
927
+ inFlight = doRefresh().finally(() => {
928
+ inFlight = null;
929
+ });
930
+ return inFlight;
931
+ }
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
+ });
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
+
510
987
  // src/server.ts
511
988
  import { readFile } from "node:fs/promises";
512
989
  import { basename } from "node:path";
@@ -1015,192 +1492,8 @@ var TIMINGS = {
1015
1492
  QUERY_STALE_TIME: 1000 * 60 * 5,
1016
1493
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
1017
1494
  };
1018
- // src/config.ts
1019
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1020
- import { homedir } from "node:os";
1021
- import { join as join2 } from "node:path";
1022
- var DEFAULT_API_URL = "https://app.gethmy.com/api";
1023
- var LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
1024
- function getConfigDir() {
1025
- return join2(homedir(), ".harmony-mcp");
1026
- }
1027
- function getConfigPath() {
1028
- return join2(getConfigDir(), "config.json");
1029
- }
1030
- function getLocalConfigPath(cwd) {
1031
- return join2(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
1032
- }
1033
- function loadConfig() {
1034
- const configPath = getConfigPath();
1035
- if (!existsSync2(configPath)) {
1036
- return {
1037
- apiKey: null,
1038
- apiUrl: DEFAULT_API_URL,
1039
- activeWorkspaceId: null,
1040
- activeProjectId: null,
1041
- userEmail: null,
1042
- memoryDir: null
1043
- };
1044
- }
1045
- try {
1046
- const data = readFileSync2(configPath, "utf-8");
1047
- const config = JSON.parse(data);
1048
- return {
1049
- apiKey: config.apiKey || null,
1050
- apiUrl: config.apiUrl || DEFAULT_API_URL,
1051
- activeWorkspaceId: config.activeWorkspaceId || null,
1052
- activeProjectId: config.activeProjectId || null,
1053
- userEmail: config.userEmail || null,
1054
- memoryDir: config.memoryDir || null
1055
- };
1056
- } catch {
1057
- return {
1058
- apiKey: null,
1059
- apiUrl: DEFAULT_API_URL,
1060
- activeWorkspaceId: null,
1061
- activeProjectId: null,
1062
- userEmail: null,
1063
- memoryDir: null
1064
- };
1065
- }
1066
- }
1067
- function saveConfig(config) {
1068
- const configDir = getConfigDir();
1069
- const configPath = getConfigPath();
1070
- if (!existsSync2(configDir)) {
1071
- mkdirSync2(configDir, { recursive: true, mode: 448 });
1072
- }
1073
- const existingConfig = loadConfig();
1074
- const newConfig = { ...existingConfig, ...config };
1075
- writeFileSync2(configPath, JSON.stringify(newConfig, null, 2), {
1076
- mode: 384
1077
- });
1078
- }
1079
- function loadLocalConfig(cwd) {
1080
- const localConfigPath = getLocalConfigPath(cwd);
1081
- if (!existsSync2(localConfigPath)) {
1082
- return null;
1083
- }
1084
- try {
1085
- const data = readFileSync2(localConfigPath, "utf-8");
1086
- const config = JSON.parse(data);
1087
- return {
1088
- workspaceId: config.workspaceId || null,
1089
- projectId: config.projectId || null
1090
- };
1091
- } catch {
1092
- return null;
1093
- }
1094
- }
1095
- function saveLocalConfig(config, cwd) {
1096
- const localConfigPath = getLocalConfigPath(cwd);
1097
- const existingConfig = loadLocalConfig(cwd) || {
1098
- workspaceId: null,
1099
- projectId: null
1100
- };
1101
- const newConfig = { ...existingConfig, ...config };
1102
- const cleanConfig = {};
1103
- if (newConfig.workspaceId)
1104
- cleanConfig.workspaceId = newConfig.workspaceId;
1105
- if (newConfig.projectId)
1106
- cleanConfig.projectId = newConfig.projectId;
1107
- writeFileSync2(localConfigPath, JSON.stringify(cleanConfig, null, 2));
1108
- }
1109
- function hasLocalConfig(cwd) {
1110
- return existsSync2(getLocalConfigPath(cwd));
1111
- }
1112
- function getApiKey() {
1113
- const config = loadConfig();
1114
- if (!config.apiKey) {
1115
- throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to set your API key.
1116
- ` + "You can generate an API key at https://gethmy.com → Settings → API Keys.");
1117
- }
1118
- return config.apiKey;
1119
- }
1120
- function getApiUrl() {
1121
- const config = loadConfig();
1122
- return config.apiUrl;
1123
- }
1124
- function getUserEmail() {
1125
- const config = loadConfig();
1126
- return config.userEmail;
1127
- }
1128
- function setActiveWorkspace(workspaceId, options) {
1129
- if (options?.local) {
1130
- saveLocalConfig({ workspaceId }, options.cwd);
1131
- } else {
1132
- saveConfig({ activeWorkspaceId: workspaceId });
1133
- }
1134
- }
1135
- function setActiveProject(projectId, options) {
1136
- if (options?.local) {
1137
- saveLocalConfig({ projectId }, options.cwd);
1138
- } else {
1139
- saveConfig({ activeProjectId: projectId });
1140
- }
1141
- }
1142
- function getActiveWorkspaceId(cwd) {
1143
- const localConfig = loadLocalConfig(cwd);
1144
- if (localConfig?.workspaceId) {
1145
- return localConfig.workspaceId;
1146
- }
1147
- return loadConfig().activeWorkspaceId;
1148
- }
1149
- function getActiveProjectId(cwd) {
1150
- const localConfig = loadLocalConfig(cwd);
1151
- if (localConfig?.projectId) {
1152
- return localConfig.projectId;
1153
- }
1154
- return loadConfig().activeProjectId;
1155
- }
1156
- function isConfigured() {
1157
- const config = loadConfig();
1158
- return !!config.apiKey;
1159
- }
1160
- function areSkillsInstalled(cwd) {
1161
- const home = homedir();
1162
- const workingDir = cwd || process.cwd();
1163
- const foundPaths = [];
1164
- const globalSkillsDir = join2(home, ".agents", "skills");
1165
- const globalSkillPath = join2(globalSkillsDir, "hmy", "SKILL.md");
1166
- if (existsSync2(globalSkillPath)) {
1167
- foundPaths.push(globalSkillPath);
1168
- return { installed: true, location: "global", paths: foundPaths };
1169
- }
1170
- const claudeGlobalSkill = join2(home, ".claude", "skills", "hmy.md");
1171
- if (existsSync2(claudeGlobalSkill)) {
1172
- foundPaths.push(claudeGlobalSkill);
1173
- return { installed: true, location: "global", paths: foundPaths };
1174
- }
1175
- const claudeGlobalSkillAlt = join2(home, ".claude", "skills", "hmy", "SKILL.md");
1176
- if (existsSync2(claudeGlobalSkillAlt)) {
1177
- foundPaths.push(claudeGlobalSkillAlt);
1178
- return { installed: true, location: "global", paths: foundPaths };
1179
- }
1180
- const localSkillPath = join2(workingDir, ".claude", "skills", "hmy.md");
1181
- if (existsSync2(localSkillPath)) {
1182
- foundPaths.push(localSkillPath);
1183
- return { installed: true, location: "local", paths: foundPaths };
1184
- }
1185
- const localSkillPathAlt = join2(workingDir, ".claude", "skills", "hmy", "SKILL.md");
1186
- if (existsSync2(localSkillPathAlt)) {
1187
- foundPaths.push(localSkillPathAlt);
1188
- return { installed: true, location: "local", paths: foundPaths };
1189
- }
1190
- return { installed: false, location: null, paths: [] };
1191
- }
1192
- function hasProjectContext(cwd) {
1193
- const localConfig = loadLocalConfig(cwd);
1194
- return !!(localConfig?.workspaceId || localConfig?.projectId);
1195
- }
1196
- function getMemoryDir() {
1197
- const config = loadConfig();
1198
- if (config.memoryDir)
1199
- return config.memoryDir;
1200
- return join2(homedir(), ".harmony", "memory");
1201
- }
1202
-
1203
1495
  // src/api-client.ts
1496
+ init_config();
1204
1497
  var RETRY_CONFIG = {
1205
1498
  maxRetries: 3,
1206
1499
  baseDelayMs: 1000,
@@ -1220,7 +1513,7 @@ function getRetryDelay(attempt) {
1220
1513
  const delay = Math.min(RETRY_CONFIG.baseDelayMs * 2 ** attempt, RETRY_CONFIG.maxDelayMs);
1221
1514
  return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
1222
1515
  }
1223
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1516
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1224
1517
 
1225
1518
  class Semaphore {
1226
1519
  permits;
@@ -1285,10 +1578,12 @@ class HarmonyApiClient {
1285
1578
  apiKey;
1286
1579
  apiUrl;
1287
1580
  onUnauthorized;
1581
+ refreshCredential;
1288
1582
  constructor(options) {
1289
1583
  this.apiKey = options?.apiKey ?? getApiKey();
1290
1584
  this.apiUrl = options?.apiUrl ?? getApiUrl();
1291
1585
  this.onUnauthorized = options?.onUnauthorized;
1586
+ this.refreshCredential = options?.refreshCredential;
1292
1587
  }
1293
1588
  getApiUrl() {
1294
1589
  return this.apiUrl;
@@ -1318,6 +1613,7 @@ class HarmonyApiClient {
1318
1613
  async requestWithRetry(method, path, body, options) {
1319
1614
  const url = `${this.apiUrl}/v1${path}`;
1320
1615
  let lastError = null;
1616
+ let refreshed = false;
1321
1617
  const contentType = options?.contentType || "application/json";
1322
1618
  const accept = options?.accept || "application/json";
1323
1619
  for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
@@ -1346,6 +1642,15 @@ class HarmonyApiClient {
1346
1642
  if (!response.ok) {
1347
1643
  const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
1348
1644
  if (response.status === 401) {
1645
+ if (this.refreshCredential && !refreshed) {
1646
+ refreshed = true;
1647
+ const fresh = await this.refreshCredential();
1648
+ if (fresh) {
1649
+ this.apiKey = fresh;
1650
+ attempt--;
1651
+ continue;
1652
+ }
1653
+ }
1349
1654
  this.onUnauthorized?.();
1350
1655
  throw new HarmonyUnauthorizedError(errorMsg);
1351
1656
  }
@@ -1354,7 +1659,7 @@ class HarmonyApiClient {
1354
1659
  }
1355
1660
  lastError = new Error(errorMsg);
1356
1661
  if (attempt < RETRY_CONFIG.maxRetries) {
1357
- await sleep(getRetryDelay(attempt));
1662
+ await sleep2(getRetryDelay(attempt));
1358
1663
  continue;
1359
1664
  }
1360
1665
  throw lastError;
@@ -1368,7 +1673,7 @@ class HarmonyApiClient {
1368
1673
  if (!isRetryableError(error))
1369
1674
  throw lastError;
1370
1675
  if (attempt < RETRY_CONFIG.maxRetries) {
1371
- await sleep(getRetryDelay(attempt));
1676
+ await sleep2(getRetryDelay(attempt));
1372
1677
  }
1373
1678
  }
1374
1679
  }
@@ -1377,6 +1682,7 @@ class HarmonyApiClient {
1377
1682
  async requestRawWithRetry(method, path, body, options) {
1378
1683
  const url = `${this.apiUrl}/v1${path}`;
1379
1684
  let lastError = null;
1685
+ let refreshed = false;
1380
1686
  const contentType = options?.contentType || "application/json";
1381
1687
  const accept = options?.accept || "text/markdown";
1382
1688
  for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
@@ -1399,6 +1705,15 @@ class HarmonyApiClient {
1399
1705
  errorMsg = text || `API error: ${response.status}`;
1400
1706
  }
1401
1707
  if (response.status === 401) {
1708
+ if (this.refreshCredential && !refreshed) {
1709
+ refreshed = true;
1710
+ const fresh = await this.refreshCredential();
1711
+ if (fresh) {
1712
+ this.apiKey = fresh;
1713
+ attempt--;
1714
+ continue;
1715
+ }
1716
+ }
1402
1717
  this.onUnauthorized?.();
1403
1718
  throw new HarmonyUnauthorizedError(errorMsg);
1404
1719
  }
@@ -1407,7 +1722,7 @@ class HarmonyApiClient {
1407
1722
  }
1408
1723
  lastError = new Error(errorMsg);
1409
1724
  if (attempt < RETRY_CONFIG.maxRetries) {
1410
- await sleep(getRetryDelay(attempt));
1725
+ await sleep2(getRetryDelay(attempt));
1411
1726
  continue;
1412
1727
  }
1413
1728
  throw lastError;
@@ -1418,7 +1733,7 @@ class HarmonyApiClient {
1418
1733
  if (!isRetryableError(error))
1419
1734
  throw lastError;
1420
1735
  if (attempt < RETRY_CONFIG.maxRetries) {
1421
- await sleep(getRetryDelay(attempt));
1736
+ await sleep2(getRetryDelay(attempt));
1422
1737
  }
1423
1738
  }
1424
1739
  }
@@ -2000,7 +2315,12 @@ async function loadPromptModules() {
2000
2315
  var client2 = null;
2001
2316
  function getClient() {
2002
2317
  if (!client2) {
2003
- client2 = new HarmonyApiClient;
2318
+ client2 = new HarmonyApiClient({
2319
+ refreshCredential: async () => {
2320
+ const { refreshOAuthToken: refreshOAuthToken2 } = await Promise.resolve().then(() => (init_oauth_refresh(), exports_oauth_refresh));
2321
+ return refreshOAuthToken2();
2322
+ }
2323
+ });
2004
2324
  }
2005
2325
  return client2;
2006
2326
  }
@@ -2179,6 +2499,9 @@ async function autoEndSession(scope, client3, cardId, status) {
2179
2499
  } catch {}
2180
2500
  }
2181
2501
 
2502
+ // src/server.ts
2503
+ init_config();
2504
+
2182
2505
  // src/graph-expansion.ts
2183
2506
  async function autoExpandGraph(client3, entityId, title, content, _tags, workspaceId, projectId, maxRelations = 5) {
2184
2507
  try {
@@ -2189,14 +2512,14 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
2189
2512
  project_id: projectId,
2190
2513
  limit: 20
2191
2514
  });
2192
- candidates = entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
2515
+ candidates = entities.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4).slice(0, maxRelations);
2193
2516
  if (candidates.length === 0) {
2194
2517
  await new Promise((resolve) => setTimeout(resolve, 2000));
2195
2518
  const retry = await client3.searchMemoryEntities(workspaceId, query, {
2196
2519
  project_id: projectId,
2197
2520
  limit: 20
2198
2521
  });
2199
- candidates = retry.entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
2522
+ candidates = retry.entities.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4).slice(0, maxRelations);
2200
2523
  }
2201
2524
  let relationsCreated = 0;
2202
2525
  for (const candidate of candidates) {
@@ -2733,6 +3056,7 @@ function lintTags(tags) {
2733
3056
  }
2734
3057
 
2735
3058
  // src/onboard.ts
3059
+ init_config();
2736
3060
  async function onboardNewUser(params) {
2737
3061
  const {
2738
3062
  email,
@@ -2778,19 +3102,20 @@ import {
2778
3102
  existsSync as existsSync4,
2779
3103
  mkdirSync as mkdirSync3,
2780
3104
  readFileSync as readFileSync4,
2781
- renameSync,
3105
+ renameSync as renameSync2,
2782
3106
  writeFileSync as writeFileSync3
2783
3107
  } from "node:fs";
2784
3108
  import { homedir as homedir3 } from "node:os";
2785
- import { dirname, join as join4 } from "node:path";
3109
+ import { dirname, join as join5 } from "node:path";
3110
+ init_config();
2786
3111
 
2787
3112
  // src/hmy-config.ts
2788
3113
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
2789
3114
  import { homedir as homedir2 } from "node:os";
2790
- import { join as join3 } from "node:path";
3115
+ import { join as join4 } from "node:path";
2791
3116
  var DEFAULTS = { updateCheck: true, pin: null };
2792
3117
  function getHmyConfigPath() {
2793
- return join3(homedir2(), ".hmy", "config.yaml");
3118
+ return join4(homedir2(), ".hmy", "config.yaml");
2794
3119
  }
2795
3120
  function loadHmyConfig() {
2796
3121
  const path = getHmyConfigPath();
@@ -2933,7 +3258,7 @@ function atomicWrite(filePath, content) {
2933
3258
  mkdirSync3(dir, { recursive: true });
2934
3259
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
2935
3260
  writeFileSync3(tmp, content);
2936
- renameSync(tmp, filePath);
3261
+ renameSync2(tmp, filePath);
2937
3262
  }
2938
3263
  function hasMetadataVersion(content) {
2939
3264
  return parseSkillVersion(content) !== null;
@@ -2973,9 +3298,9 @@ function findSkillFiles(paths, knownNames) {
2973
3298
  }
2974
3299
  return results;
2975
3300
  }
2976
- var HMY_DIR = join4(homedir3(), ".hmy");
2977
- var HMY_VERSION_FILE = join4(HMY_DIR, "VERSION");
2978
- var LAST_CHECK_FILE = join4(HMY_DIR, "last-update-check");
3301
+ var HMY_DIR = join5(homedir3(), ".hmy");
3302
+ var HMY_VERSION_FILE = join5(HMY_DIR, "VERSION");
3303
+ var LAST_CHECK_FILE = join5(HMY_DIR, "last-update-check");
2979
3304
  var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
2980
3305
  function checkedRecently(now = Date.now()) {
2981
3306
  try {
@@ -3008,7 +3333,7 @@ async function refreshSkills(opts = {}) {
3008
3333
  const status = areSkillsInstalled();
3009
3334
  if (!status.installed)
3010
3335
  return { updated: false };
3011
- const client3 = new HarmonyApiClient;
3336
+ const client3 = getClient();
3012
3337
  const versionInfo = await client3.fetchSkillsVersion();
3013
3338
  recordCheck();
3014
3339
  const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
@@ -4871,7 +5196,7 @@ async function handleToolCall(name, args, deps) {
4871
5196
  case "harmony_delete_label": {
4872
5197
  const labelId = z.string().uuid().parse(args.labelId);
4873
5198
  const result = await client3.deleteLabel(labelId);
4874
- return { success: true, ...result };
5199
+ return { ...result };
4875
5200
  }
4876
5201
  case "harmony_add_label_to_card": {
4877
5202
  const cardId = z.string().uuid().parse(args.cardId);
@@ -5828,8 +6153,8 @@ async function handleToolCall(name, args, deps) {
5828
6153
  content: summary,
5829
6154
  type: "lesson",
5830
6155
  scope: "project",
5831
- workspaceId,
5832
- projectId: plan.project_id,
6156
+ workspace_id: workspaceId,
6157
+ project_id: plan.project_id,
5833
6158
  tags: ["plan", "archived"],
5834
6159
  confidence: 0.8
5835
6160
  });