@gethmy/mcp 2.8.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +393 -276
- package/dist/index.js +301 -5
- package/package.json +2 -2
- package/src/api-client.ts +1 -0
- package/src/cli.ts +2 -2
- package/src/hmy-config.ts +70 -0
- package/src/server.ts +27 -3
- package/src/skills.ts +115 -19
- package/src/tui/setup.ts +8 -8
package/src/skills.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
3
10
|
import { HarmonyApiClient } from "./api-client.js";
|
|
4
11
|
import { areSkillsInstalled, isConfigured } from "./config.js";
|
|
12
|
+
import { loadHmyConfig } from "./hmy-config.js";
|
|
5
13
|
|
|
6
14
|
/**
|
|
7
15
|
* Legacy workflow prompt used by Codex / Cursor agents. Kept inline because
|
|
@@ -112,10 +120,45 @@ export interface FetchedSkill {
|
|
|
112
120
|
* deploys where the API hasn't been upgraded yet.
|
|
113
121
|
*/
|
|
114
122
|
export function buildSkillFile(skill: FetchedSkill): string {
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
const content = stripSkillPreamble(skill.content);
|
|
124
|
+
if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
|
|
125
|
+
return injectMetadataVersion(content, skill.skillVersion);
|
|
117
126
|
}
|
|
118
|
-
return
|
|
127
|
+
return content;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const PREAMBLE_START = "<!-- hmy-skills-preamble:start -->";
|
|
131
|
+
const PREAMBLE_END = "<!-- hmy-skills-preamble:end -->";
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove the legacy auto-update bash block from a rendered SKILL.md before it
|
|
135
|
+
* lands on disk. harmony-api no longer emits this block, but cached renders and
|
|
136
|
+
* already-installed files may still carry it — and that block is exactly what
|
|
137
|
+
* Claude Code's auto-mode classifier denies (curl → chmod +x → exec). Stripping
|
|
138
|
+
* on write means upgraded clients stop hitting the classifier on the next
|
|
139
|
+
* refresh. No-op when the markers are absent.
|
|
140
|
+
*/
|
|
141
|
+
export function stripSkillPreamble(content: string): string {
|
|
142
|
+
const start = content.indexOf(PREAMBLE_START);
|
|
143
|
+
const end = content.indexOf(PREAMBLE_END);
|
|
144
|
+
if (start === -1 || end === -1 || end < start) return content;
|
|
145
|
+
const stripped =
|
|
146
|
+
content.slice(0, start) + content.slice(end + PREAMBLE_END.length);
|
|
147
|
+
// Collapse the blank-line run the removal leaves behind; keep a trailing \n.
|
|
148
|
+
return `${stripped.replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "")}\n`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Write a file atomically: write to a sibling temp file, then rename over the
|
|
153
|
+
* target (atomic on the same filesystem). Avoids a half-written SKILL.md if the
|
|
154
|
+
* process dies mid-write during a silent background refresh.
|
|
155
|
+
*/
|
|
156
|
+
function atomicWrite(filePath: string, content: string): void {
|
|
157
|
+
const dir = dirname(filePath);
|
|
158
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
159
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
160
|
+
writeFileSync(tmp, content);
|
|
161
|
+
renameSync(tmp, filePath);
|
|
119
162
|
}
|
|
120
163
|
|
|
121
164
|
function hasMetadataVersion(content: string): boolean {
|
|
@@ -181,25 +224,72 @@ function findSkillFiles(
|
|
|
181
224
|
return results;
|
|
182
225
|
}
|
|
183
226
|
|
|
227
|
+
const HMY_DIR = join(homedir(), ".hmy");
|
|
228
|
+
const HMY_VERSION_FILE = join(HMY_DIR, "VERSION");
|
|
229
|
+
const LAST_CHECK_FILE = join(HMY_DIR, "last-update-check");
|
|
230
|
+
/** Probe the API for skill updates at most once per day per client. */
|
|
231
|
+
const CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
232
|
+
|
|
233
|
+
/** True if we performed a network check within CHECK_TTL_MS. */
|
|
234
|
+
function checkedRecently(now: number = Date.now()): boolean {
|
|
235
|
+
try {
|
|
236
|
+
if (!existsSync(LAST_CHECK_FILE)) return false;
|
|
237
|
+
const ts = Number.parseInt(
|
|
238
|
+
readFileSync(LAST_CHECK_FILE, "utf-8").trim(),
|
|
239
|
+
10,
|
|
240
|
+
);
|
|
241
|
+
if (!Number.isFinite(ts)) return false;
|
|
242
|
+
return now - ts < CHECK_TTL_MS;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Record that a network check just happened, resetting the TTL window. */
|
|
249
|
+
function recordCheck(now: number = Date.now()): void {
|
|
250
|
+
try {
|
|
251
|
+
if (!existsSync(HMY_DIR)) mkdirSync(HMY_DIR, { recursive: true });
|
|
252
|
+
writeFileSync(LAST_CHECK_FILE, String(now));
|
|
253
|
+
} catch {
|
|
254
|
+
// Best effort — a failed timestamp write only costs an extra probe.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
184
258
|
/**
|
|
185
259
|
* Silently refresh installed skill files if remote versions are newer.
|
|
186
|
-
* Called at MCP server startup. Non-blocking — any error
|
|
187
|
-
* partial install) is caught and the existing files are left in
|
|
260
|
+
* Called at MCP server `serve` startup (cli.ts). Non-blocking — any error
|
|
261
|
+
* (offline, 401, partial install) is caught and the existing files are left in
|
|
262
|
+
* place. Returns whether any file was rewritten so the caller can fire a
|
|
263
|
+
* `notifications/resources/list_changed` once the transport is connected.
|
|
264
|
+
*
|
|
265
|
+
* Honors `~/.hmy/config.yaml`: `update_check: false` disables it entirely and a
|
|
266
|
+
* `pin` freezes the installed version. The network probe is gated to once per
|
|
267
|
+
* `CHECK_TTL_MS` (tracked in `~/.hmy/last-update-check`) so starting many
|
|
268
|
+
* sessions in a day doesn't re-probe the API each time — pass `force` to skip
|
|
269
|
+
* the gate.
|
|
188
270
|
*
|
|
189
271
|
* Phase 3b: per-skill compare. Each skill_resource row carries its own
|
|
190
|
-
* monotonic `version
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* triggers a re-fetch of `hmy-plan`, `hmy-cleanup`, etc.
|
|
272
|
+
* monotonic `version`; only files that are behind are rewritten. Writes are
|
|
273
|
+
* atomic (temp + rename) and the legacy auto-update bash block is stripped on
|
|
274
|
+
* the way to disk (see buildSkillFile / stripSkillPreamble).
|
|
194
275
|
*/
|
|
195
|
-
export async function refreshSkills(
|
|
276
|
+
export async function refreshSkills(
|
|
277
|
+
opts: { force?: boolean } = {},
|
|
278
|
+
): Promise<{ updated: boolean }> {
|
|
196
279
|
try {
|
|
197
|
-
if (!isConfigured()) return;
|
|
280
|
+
if (!isConfigured()) return { updated: false };
|
|
281
|
+
|
|
282
|
+
const cfg = loadHmyConfig();
|
|
283
|
+
if (!cfg.updateCheck || cfg.pin) return { updated: false };
|
|
284
|
+
if (!opts.force && checkedRecently()) return { updated: false };
|
|
285
|
+
|
|
198
286
|
const status = areSkillsInstalled();
|
|
199
|
-
if (!status.installed) return;
|
|
287
|
+
if (!status.installed) return { updated: false };
|
|
200
288
|
|
|
201
289
|
const client = new HarmonyApiClient();
|
|
202
290
|
const versionInfo = await client.fetchSkillsVersion();
|
|
291
|
+
// We hit the network — reset the TTL window regardless of outcome.
|
|
292
|
+
recordCheck();
|
|
203
293
|
|
|
204
294
|
const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
|
|
205
295
|
|
|
@@ -222,7 +312,7 @@ export async function refreshSkills(): Promise<void> {
|
|
|
222
312
|
}
|
|
223
313
|
}
|
|
224
314
|
}
|
|
225
|
-
if (skillFiles.length === 0) return;
|
|
315
|
+
if (skillFiles.length === 0) return { updated: false };
|
|
226
316
|
|
|
227
317
|
let updated = false;
|
|
228
318
|
for (const { name, filePath } of skillFiles) {
|
|
@@ -243,10 +333,7 @@ export async function refreshSkills(): Promise<void> {
|
|
|
243
333
|
continue;
|
|
244
334
|
}
|
|
245
335
|
|
|
246
|
-
|
|
247
|
-
const dir = dirname(filePath);
|
|
248
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
249
|
-
writeFileSync(filePath, newContent);
|
|
336
|
+
atomicWrite(filePath, buildSkillFile(fetched));
|
|
250
337
|
updated = true;
|
|
251
338
|
} catch (err) {
|
|
252
339
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -254,9 +341,18 @@ export async function refreshSkills(): Promise<void> {
|
|
|
254
341
|
}
|
|
255
342
|
}
|
|
256
343
|
if (updated) {
|
|
344
|
+
// VERSION is diagnostic now (the bash preamble that read it is gone),
|
|
345
|
+
// but keep it current so `hmy-mcp status` and support reflect reality.
|
|
346
|
+
try {
|
|
347
|
+
atomicWrite(HMY_VERSION_FILE, versionInfo.version);
|
|
348
|
+
} catch {
|
|
349
|
+
// best effort
|
|
350
|
+
}
|
|
257
351
|
console.error("Harmony: Refreshed skills from server");
|
|
258
352
|
}
|
|
353
|
+
return { updated };
|
|
259
354
|
} catch {
|
|
260
355
|
// Non-blocking — offline / 401 / partial install all fall through here.
|
|
356
|
+
return { updated: false };
|
|
261
357
|
}
|
|
262
358
|
}
|
package/src/tui/setup.ts
CHANGED
|
@@ -271,9 +271,9 @@ async function getAgentFiles(
|
|
|
271
271
|
switch (agentId) {
|
|
272
272
|
case "claude": {
|
|
273
273
|
// Claude Code skills come from /v1/skills (DB-backed). Fetch the
|
|
274
|
-
// canonical list, then each body. hmy-update-check is
|
|
275
|
-
//
|
|
276
|
-
//
|
|
274
|
+
// canonical list, then each body. hmy-update-check is skipped here — it's
|
|
275
|
+
// a shell script, not an installed SKILL.md. buildSkillFile() strips any
|
|
276
|
+
// legacy auto-update bash block before the content lands on disk.
|
|
277
277
|
const client = new HarmonyApiClient();
|
|
278
278
|
const versionInfo = await client.fetchSkillsVersion();
|
|
279
279
|
const installableNames = versionInfo.skills.filter(
|
|
@@ -319,11 +319,11 @@ async function getAgentFiles(
|
|
|
319
319
|
);
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
322
|
+
// Seed ~/.hmy/VERSION (the baseline the server's refreshSkills compares
|
|
323
|
+
// against and keeps current) and ~/.hmy/bin/hmy-update-check. The script
|
|
324
|
+
// is no longer invoked by a skill preamble — auto-update now runs in the
|
|
325
|
+
// MCP server process — but it's kept as a standalone diagnostic and is
|
|
326
|
+
// integrity-checked here against its published sha256.
|
|
327
327
|
try {
|
|
328
328
|
const updateCheckFetched = await client.fetchSkill("hmy-update-check");
|
|
329
329
|
const actualHash = createHash("sha256")
|