@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/README.md +15 -15
- package/dist/cli.js +674 -275
- package/dist/index.js +530 -205
- package/dist/lib/api-client.js +352 -182
- package/dist/lib/config.js +37 -26
- package/package.json +2 -2
- package/src/api-client.ts +52 -4
- package/src/cli.ts +13 -2
- package/src/config.ts +53 -25
- package/src/graph-expansion.ts +7 -4
- package/src/http.ts +3 -14
- package/src/memory-floor.ts +2 -1
- package/src/oauth-login.ts +288 -0
- package/src/oauth-refresh.ts +209 -0
- package/src/server.ts +6 -6
- package/src/skills.ts +4 -2
- package/src/tui/setup.ts +106 -16
package/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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3115
|
+
import { join as join4 } from "node:path";
|
|
2791
3116
|
var DEFAULTS = { updateCheck: true, pin: null };
|
|
2792
3117
|
function getHmyConfigPath() {
|
|
2793
|
-
return
|
|
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
|
-
|
|
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 =
|
|
2977
|
-
var HMY_VERSION_FILE =
|
|
2978
|
-
var LAST_CHECK_FILE =
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
6156
|
+
workspace_id: workspaceId,
|
|
6157
|
+
project_id: plan.project_id,
|
|
5833
6158
|
tags: ["plan", "archived"],
|
|
5834
6159
|
confidence: 0.8
|
|
5835
6160
|
});
|