@gethmy/mcp 2.13.3 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -210,6 +210,140 @@ function getMemoryDir() {
210
210
  var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
211
211
  var init_config = () => {};
212
212
 
213
+ // src/oauth-login.ts
214
+ function oauthBaseFromApiUrl(apiUrl) {
215
+ return `${new URL(apiUrl).origin}/oauth`;
216
+ }
217
+ var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS;
218
+ var init_oauth_login = __esm(() => {
219
+ MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
220
+ LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
221
+ });
222
+
223
+ // src/oauth-refresh.ts
224
+ var exports_oauth_refresh = {};
225
+ __export(exports_oauth_refresh, {
226
+ refreshOAuthToken: () => refreshOAuthToken
227
+ });
228
+ import {
229
+ closeSync,
230
+ openSync,
231
+ renameSync,
232
+ rmSync,
233
+ statSync,
234
+ writeSync
235
+ } from "node:fs";
236
+ import { join as join2 } from "node:path";
237
+ function lockPath() {
238
+ return join2(getConfigDir(), LOCK_FILENAME);
239
+ }
240
+ function sleep(ms) {
241
+ return new Promise((resolve) => setTimeout(resolve, ms));
242
+ }
243
+ async function withRefreshLock(fn) {
244
+ const path = lockPath();
245
+ const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
246
+ let held = false;
247
+ while (Date.now() < deadline) {
248
+ try {
249
+ const fd = openSync(path, "wx");
250
+ writeSync(fd, String(process.pid));
251
+ closeSync(fd);
252
+ held = true;
253
+ break;
254
+ } catch (err) {
255
+ if (err.code !== "EEXIST") {
256
+ break;
257
+ }
258
+ try {
259
+ const age = Date.now() - statSync(path).mtimeMs;
260
+ if (age > LOCK_STALE_MS) {
261
+ const claim = `${path}.stale.${process.pid}`;
262
+ try {
263
+ renameSync(path, claim);
264
+ rmSync(claim, { force: true });
265
+ } catch {}
266
+ continue;
267
+ }
268
+ } catch {
269
+ continue;
270
+ }
271
+ await sleep(LOCK_RETRY_MS);
272
+ }
273
+ }
274
+ try {
275
+ return await fn();
276
+ } finally {
277
+ if (held) {
278
+ try {
279
+ rmSync(path, { force: true });
280
+ } catch {}
281
+ }
282
+ }
283
+ }
284
+ function refreshOAuthToken() {
285
+ if (inFlight)
286
+ return inFlight;
287
+ inFlight = doRefresh().finally(() => {
288
+ inFlight = null;
289
+ });
290
+ return inFlight;
291
+ }
292
+ async function doRefresh() {
293
+ const before = loadConfig();
294
+ if (!before.oauthRefreshToken || !before.oauthClientId)
295
+ return null;
296
+ return withRefreshLock(async () => {
297
+ const config = loadConfig();
298
+ if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
299
+ return config.oauthAccessToken;
300
+ }
301
+ if (!config.oauthRefreshToken || !config.oauthClientId)
302
+ return null;
303
+ const base = oauthBaseFromApiUrl(config.apiUrl);
304
+ let res;
305
+ try {
306
+ res = await fetch(`${base}/token`, {
307
+ method: "POST",
308
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
309
+ body: new URLSearchParams({
310
+ grant_type: "refresh_token",
311
+ refresh_token: config.oauthRefreshToken,
312
+ client_id: config.oauthClientId
313
+ }).toString()
314
+ });
315
+ } catch {
316
+ return null;
317
+ }
318
+ if (!res.ok) {
319
+ const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
320
+ if (errorCode === "invalid_grant") {
321
+ saveConfig({
322
+ oauthAccessToken: null,
323
+ oauthRefreshToken: null,
324
+ oauthExpiresAt: null,
325
+ oauthClientId: null
326
+ });
327
+ }
328
+ return null;
329
+ }
330
+ const body = await res.json().catch(() => null);
331
+ if (!body?.access_token || !body.refresh_token)
332
+ return null;
333
+ saveConfig({
334
+ oauthAccessToken: body.access_token,
335
+ oauthRefreshToken: body.refresh_token,
336
+ oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
337
+ });
338
+ return body.access_token;
339
+ });
340
+ }
341
+ var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
342
+ var init_oauth_refresh = __esm(() => {
343
+ init_config();
344
+ init_oauth_login();
345
+ });
346
+
213
347
  // src/prompt-builder.ts
214
348
  var exports_prompt_builder = {};
215
349
  __export(exports_prompt_builder, {
@@ -699,140 +833,6 @@ var init_prompt_builder = __esm(() => {
699
833
  execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
700
834
  };
701
835
  });
702
-
703
- // src/oauth-login.ts
704
- function oauthBaseFromApiUrl(apiUrl) {
705
- return `${new URL(apiUrl).origin}/oauth`;
706
- }
707
- var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS;
708
- var init_oauth_login = __esm(() => {
709
- MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
710
- LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
711
- });
712
-
713
- // src/oauth-refresh.ts
714
- var exports_oauth_refresh = {};
715
- __export(exports_oauth_refresh, {
716
- refreshOAuthToken: () => refreshOAuthToken
717
- });
718
- import {
719
- closeSync,
720
- openSync,
721
- renameSync,
722
- rmSync,
723
- statSync,
724
- writeSync
725
- } from "node:fs";
726
- import { join as join2 } from "node:path";
727
- function lockPath() {
728
- return join2(getConfigDir(), LOCK_FILENAME);
729
- }
730
- function sleep(ms) {
731
- return new Promise((resolve) => setTimeout(resolve, ms));
732
- }
733
- async function withRefreshLock(fn) {
734
- const path = lockPath();
735
- const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
736
- let held = false;
737
- while (Date.now() < deadline) {
738
- try {
739
- const fd = openSync(path, "wx");
740
- writeSync(fd, String(process.pid));
741
- closeSync(fd);
742
- held = true;
743
- break;
744
- } catch (err) {
745
- if (err.code !== "EEXIST") {
746
- break;
747
- }
748
- try {
749
- const age = Date.now() - statSync(path).mtimeMs;
750
- if (age > LOCK_STALE_MS) {
751
- const claim = `${path}.stale.${process.pid}`;
752
- try {
753
- renameSync(path, claim);
754
- rmSync(claim, { force: true });
755
- } catch {}
756
- continue;
757
- }
758
- } catch {
759
- continue;
760
- }
761
- await sleep(LOCK_RETRY_MS);
762
- }
763
- }
764
- try {
765
- return await fn();
766
- } finally {
767
- if (held) {
768
- try {
769
- rmSync(path, { force: true });
770
- } catch {}
771
- }
772
- }
773
- }
774
- function refreshOAuthToken() {
775
- if (inFlight)
776
- return inFlight;
777
- inFlight = doRefresh().finally(() => {
778
- inFlight = null;
779
- });
780
- return inFlight;
781
- }
782
- async function doRefresh() {
783
- const before = loadConfig();
784
- if (!before.oauthRefreshToken || !before.oauthClientId)
785
- return null;
786
- return withRefreshLock(async () => {
787
- const config = loadConfig();
788
- if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
789
- return config.oauthAccessToken;
790
- }
791
- if (!config.oauthRefreshToken || !config.oauthClientId)
792
- return null;
793
- const base = oauthBaseFromApiUrl(config.apiUrl);
794
- let res;
795
- try {
796
- res = await fetch(`${base}/token`, {
797
- method: "POST",
798
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
799
- body: new URLSearchParams({
800
- grant_type: "refresh_token",
801
- refresh_token: config.oauthRefreshToken,
802
- client_id: config.oauthClientId
803
- }).toString()
804
- });
805
- } catch {
806
- return null;
807
- }
808
- if (!res.ok) {
809
- const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
810
- if (errorCode === "invalid_grant") {
811
- saveConfig({
812
- oauthAccessToken: null,
813
- oauthRefreshToken: null,
814
- oauthExpiresAt: null,
815
- oauthClientId: null
816
- });
817
- }
818
- return null;
819
- }
820
- const body = await res.json().catch(() => null);
821
- if (!body?.access_token || !body.refresh_token)
822
- return null;
823
- saveConfig({
824
- oauthAccessToken: body.access_token,
825
- oauthRefreshToken: body.refresh_token,
826
- oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
827
- });
828
- return body.access_token;
829
- });
830
- }
831
- var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
832
- var init_oauth_refresh = __esm(() => {
833
- init_config();
834
- init_oauth_login();
835
- });
836
836
  // ../harmony-shared/dist/cardLinks.js
837
837
  var LINK_TYPE_INVERSES = {
838
838
  relates_to: "relates_to",
@@ -904,6 +904,8 @@ function serializeCommentThread(comments, options = {}) {
904
904
  const tags = [];
905
905
  if (c.edited_at)
906
906
  tags.push("edited");
907
+ if (c.reply_to_id)
908
+ tags.push(`reply to ${ref(c.reply_to_id)}`);
907
909
  if (c.supersedes_id)
908
910
  tags.push(`supersedes ${ref(c.supersedes_id)}`);
909
911
  if (c.confirms_id)
@@ -1315,6 +1317,12 @@ class HarmonyApiClient {
1315
1317
  async getCardExternalLinks(cardId) {
1316
1318
  return this.request("GET", `/cards/${cardId}/external-links`);
1317
1319
  }
1320
+ async addExternalLink(cardId, url, title) {
1321
+ return this.request("POST", `/cards/${cardId}/external-links`, {
1322
+ url,
1323
+ title
1324
+ });
1325
+ }
1318
1326
  async uploadArtifact(data) {
1319
1327
  return this.request("POST", "/artifacts", data);
1320
1328
  }
@@ -1366,6 +1374,7 @@ class HarmonyApiClient {
1366
1374
  commentType: opts?.commentType,
1367
1375
  supersedesId: opts?.supersedesId,
1368
1376
  confirmsId: opts?.confirmsId,
1377
+ replyToId: opts?.replyToId,
1369
1378
  agentSessionId: opts?.agentSessionId
1370
1379
  });
1371
1380
  }
@@ -1818,12 +1827,6 @@ ${planContent.trim()}`;
1818
1827
  async updatePlaybook(playbookId, updates) {
1819
1828
  return this.request("PATCH", `/playbooks/${playbookId}`, updates);
1820
1829
  }
1821
- async runPlaybook(playbookId) {
1822
- return this.request("POST", `/playbooks/${playbookId}/run`);
1823
- }
1824
- async savePlaybookFromCard(data) {
1825
- return this.request("POST", "/playbooks/from-card", data);
1826
- }
1827
1830
  }
1828
1831
  var _promptModules = null;
1829
1832
  async function loadPromptModules() {
@@ -0,0 +1,350 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __returnValue = (v) => v;
3
+ function __exportSetter(name, newValue) {
4
+ this[name] = __returnValue.bind(null, newValue);
5
+ }
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, {
9
+ get: all[name],
10
+ enumerable: true,
11
+ configurable: true,
12
+ set: __exportSetter.bind(all, name)
13
+ });
14
+ };
15
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
+
17
+ // src/config.ts
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ function getConfigDir() {
22
+ return join(homedir(), ".harmony-mcp");
23
+ }
24
+ function getConfigPath() {
25
+ return join(getConfigDir(), "config.json");
26
+ }
27
+ function getLocalConfigPath(cwd) {
28
+ return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
29
+ }
30
+ function emptyConfig() {
31
+ return {
32
+ apiKey: null,
33
+ apiUrl: DEFAULT_API_URL,
34
+ activeWorkspaceId: null,
35
+ activeProjectId: null,
36
+ userEmail: null,
37
+ memoryDir: null,
38
+ oauthAccessToken: null,
39
+ oauthRefreshToken: null,
40
+ oauthExpiresAt: null,
41
+ oauthClientId: null
42
+ };
43
+ }
44
+ function loadConfig() {
45
+ const configPath = getConfigPath();
46
+ if (!existsSync(configPath)) {
47
+ return emptyConfig();
48
+ }
49
+ try {
50
+ const data = readFileSync(configPath, "utf-8");
51
+ const config = JSON.parse(data);
52
+ return {
53
+ apiKey: config.apiKey || null,
54
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
55
+ activeWorkspaceId: config.activeWorkspaceId || null,
56
+ activeProjectId: config.activeProjectId || null,
57
+ userEmail: config.userEmail || null,
58
+ memoryDir: config.memoryDir || null,
59
+ oauthAccessToken: config.oauthAccessToken || null,
60
+ oauthRefreshToken: config.oauthRefreshToken || null,
61
+ oauthExpiresAt: typeof config.oauthExpiresAt === "number" ? config.oauthExpiresAt : null,
62
+ oauthClientId: config.oauthClientId || null
63
+ };
64
+ } catch {
65
+ return emptyConfig();
66
+ }
67
+ }
68
+ function saveConfig(config) {
69
+ const configDir = getConfigDir();
70
+ const configPath = getConfigPath();
71
+ if (!existsSync(configDir)) {
72
+ mkdirSync(configDir, { recursive: true, mode: 448 });
73
+ }
74
+ const existingConfig = loadConfig();
75
+ const newConfig = { ...existingConfig, ...config };
76
+ writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
77
+ mode: 384
78
+ });
79
+ }
80
+ function loadLocalConfig(cwd) {
81
+ const localConfigPath = getLocalConfigPath(cwd);
82
+ if (!existsSync(localConfigPath)) {
83
+ return null;
84
+ }
85
+ try {
86
+ const data = readFileSync(localConfigPath, "utf-8");
87
+ const config = JSON.parse(data);
88
+ return {
89
+ workspaceId: config.workspaceId || null,
90
+ projectId: config.projectId || null
91
+ };
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ function saveLocalConfig(config, cwd) {
97
+ const localConfigPath = getLocalConfigPath(cwd);
98
+ const existingConfig = loadLocalConfig(cwd) || {
99
+ workspaceId: null,
100
+ projectId: null
101
+ };
102
+ const newConfig = { ...existingConfig, ...config };
103
+ const cleanConfig = {};
104
+ if (newConfig.workspaceId)
105
+ cleanConfig.workspaceId = newConfig.workspaceId;
106
+ if (newConfig.projectId)
107
+ cleanConfig.projectId = newConfig.projectId;
108
+ writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
109
+ }
110
+ function hasLocalConfig(cwd) {
111
+ return existsSync(getLocalConfigPath(cwd));
112
+ }
113
+ function getActiveCredential() {
114
+ const config = loadConfig();
115
+ if (config.oauthAccessToken)
116
+ return config.oauthAccessToken;
117
+ if (config.apiKey)
118
+ return config.apiKey;
119
+ throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to connect Harmony.
120
+ ` + "Setup authorizes in your browser — no API key handling required.");
121
+ }
122
+ function getApiKey() {
123
+ return getActiveCredential();
124
+ }
125
+ function getApiUrl() {
126
+ const config = loadConfig();
127
+ return config.apiUrl;
128
+ }
129
+ function getUserEmail() {
130
+ const config = loadConfig();
131
+ return config.userEmail;
132
+ }
133
+ function setUserEmail(email) {
134
+ saveConfig({ userEmail: email });
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
+
213
+ // src/oauth-login.ts
214
+ function oauthBaseFromApiUrl(apiUrl) {
215
+ return `${new URL(apiUrl).origin}/oauth`;
216
+ }
217
+ var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS;
218
+ var init_oauth_login = __esm(() => {
219
+ MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
220
+ LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
221
+ });
222
+
223
+ // src/oauth-refresh.ts
224
+ var exports_oauth_refresh = {};
225
+ __export(exports_oauth_refresh, {
226
+ refreshOAuthToken: () => refreshOAuthToken
227
+ });
228
+ import {
229
+ closeSync,
230
+ openSync,
231
+ renameSync,
232
+ rmSync,
233
+ statSync,
234
+ writeSync
235
+ } from "node:fs";
236
+ import { join as join2 } from "node:path";
237
+ function lockPath() {
238
+ return join2(getConfigDir(), LOCK_FILENAME);
239
+ }
240
+ function sleep(ms) {
241
+ return new Promise((resolve) => setTimeout(resolve, ms));
242
+ }
243
+ async function withRefreshLock(fn) {
244
+ const path = lockPath();
245
+ const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
246
+ let held = false;
247
+ while (Date.now() < deadline) {
248
+ try {
249
+ const fd = openSync(path, "wx");
250
+ writeSync(fd, String(process.pid));
251
+ closeSync(fd);
252
+ held = true;
253
+ break;
254
+ } catch (err) {
255
+ if (err.code !== "EEXIST") {
256
+ break;
257
+ }
258
+ try {
259
+ const age = Date.now() - statSync(path).mtimeMs;
260
+ if (age > LOCK_STALE_MS) {
261
+ const claim = `${path}.stale.${process.pid}`;
262
+ try {
263
+ renameSync(path, claim);
264
+ rmSync(claim, { force: true });
265
+ } catch {}
266
+ continue;
267
+ }
268
+ } catch {
269
+ continue;
270
+ }
271
+ await sleep(LOCK_RETRY_MS);
272
+ }
273
+ }
274
+ try {
275
+ return await fn();
276
+ } finally {
277
+ if (held) {
278
+ try {
279
+ rmSync(path, { force: true });
280
+ } catch {}
281
+ }
282
+ }
283
+ }
284
+ function refreshOAuthToken() {
285
+ if (inFlight)
286
+ return inFlight;
287
+ inFlight = doRefresh().finally(() => {
288
+ inFlight = null;
289
+ });
290
+ return inFlight;
291
+ }
292
+ async function doRefresh() {
293
+ const before = loadConfig();
294
+ if (!before.oauthRefreshToken || !before.oauthClientId)
295
+ return null;
296
+ return withRefreshLock(async () => {
297
+ const config = loadConfig();
298
+ if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
299
+ return config.oauthAccessToken;
300
+ }
301
+ if (!config.oauthRefreshToken || !config.oauthClientId)
302
+ return null;
303
+ const base = oauthBaseFromApiUrl(config.apiUrl);
304
+ let res;
305
+ try {
306
+ res = await fetch(`${base}/token`, {
307
+ method: "POST",
308
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
309
+ body: new URLSearchParams({
310
+ grant_type: "refresh_token",
311
+ refresh_token: config.oauthRefreshToken,
312
+ client_id: config.oauthClientId
313
+ }).toString()
314
+ });
315
+ } catch {
316
+ return null;
317
+ }
318
+ if (!res.ok) {
319
+ const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
320
+ if (errorCode === "invalid_grant") {
321
+ saveConfig({
322
+ oauthAccessToken: null,
323
+ oauthRefreshToken: null,
324
+ oauthExpiresAt: null,
325
+ oauthClientId: null
326
+ });
327
+ }
328
+ return null;
329
+ }
330
+ const body = await res.json().catch(() => null);
331
+ if (!body?.access_token || !body.refresh_token)
332
+ return null;
333
+ saveConfig({
334
+ oauthAccessToken: body.access_token,
335
+ oauthRefreshToken: body.refresh_token,
336
+ oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
337
+ });
338
+ return body.access_token;
339
+ });
340
+ }
341
+ var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
342
+ var init_oauth_refresh = __esm(() => {
343
+ init_config();
344
+ init_oauth_login();
345
+ });
346
+ init_oauth_refresh();
347
+
348
+ export {
349
+ refreshOAuthToken
350
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.13.3",
3
+ "version": "2.14.0",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,6 +16,10 @@
16
16
  "./src/config.js": {
17
17
  "types": "./src/config.ts",
18
18
  "default": "./dist/lib/config.js"
19
+ },
20
+ "./src/oauth-refresh.js": {
21
+ "types": "./src/oauth-refresh.ts",
22
+ "default": "./dist/lib/oauth-refresh.js"
19
23
  }
20
24
  },
21
25
  "bin": {
@@ -55,7 +59,7 @@
55
59
  "bun": ">=1.0.0"
56
60
  },
57
61
  "scripts": {
58
- "build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod && bun build src/api-client.ts src/config.ts --outdir dist/lib --root src --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod",
62
+ "build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod && bun build src/api-client.ts src/config.ts src/oauth-refresh.ts --outdir dist/lib --root src --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod",
59
63
  "build:bun": "bun build src/index.ts src/http.ts src/remote.ts src/cli.ts --outdir dist --target bun",
60
64
  "serve:remote": "bun src/remote.ts",
61
65
  "dev": "bun --watch src/index.ts",