@gethmy/mcp 3.3.0 → 3.4.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.
- package/README.md +39 -0
- package/dist/cli.js +1012 -178
- package/dist/index.js +630 -63
- package/dist/run-hook-cli.js +742 -0
- package/package.json +4 -3
- package/src/api-client.ts +26 -1
- package/src/auto-session.ts +33 -0
- package/src/cli.ts +104 -0
- package/src/hook-install.ts +388 -0
- package/src/plan-task-link.ts +3 -1
- package/src/run-event-forwarder.ts +363 -0
- package/src/run-hook-cli.ts +55 -0
- package/src/run-hook-main.ts +159 -0
- package/src/run-hook.ts +203 -0
- package/src/run-redaction.ts +461 -0
- package/src/run-state.ts +679 -0
- package/src/server.ts +85 -5
package/dist/cli.js
CHANGED
|
@@ -978,6 +978,565 @@ var init_oauth_refresh = __esm(() => {
|
|
|
978
978
|
init_oauth_login();
|
|
979
979
|
});
|
|
980
980
|
|
|
981
|
+
// src/run-state.ts
|
|
982
|
+
var exports_run_state = {};
|
|
983
|
+
__export(exports_run_state, {
|
|
984
|
+
writeSpoolBatch: () => writeSpoolBatch,
|
|
985
|
+
writeRouteMemo: () => writeRouteMemo,
|
|
986
|
+
trimSpool: () => trimSpool,
|
|
987
|
+
spoolDir: () => spoolDir,
|
|
988
|
+
sessionsDir: () => sessionsDir,
|
|
989
|
+
runStateExists: () => runStateExists,
|
|
990
|
+
runStateDir: () => runStateDir,
|
|
991
|
+
removeSpoolBatches: () => removeSpoolBatches,
|
|
992
|
+
readSpoolBatches: () => readSpoolBatches,
|
|
993
|
+
readRouteMemo: () => readRouteMemo,
|
|
994
|
+
readPublishedSessions: () => readPublishedSessions,
|
|
995
|
+
publishRunSession: () => publishRunSession,
|
|
996
|
+
pidIsAlive: () => pidIsAlive,
|
|
997
|
+
clearRunSession: () => clearRunSession,
|
|
998
|
+
chooseRunSessionForHook: () => chooseRunSessionForHook,
|
|
999
|
+
ancestorPids: () => ancestorPids,
|
|
1000
|
+
RUN_STATE_DIR_ENV: () => RUN_STATE_DIR_ENV,
|
|
1001
|
+
MAX_POINTER_AGE_MS: () => MAX_POINTER_AGE_MS
|
|
1002
|
+
});
|
|
1003
|
+
import { execFileSync } from "node:child_process";
|
|
1004
|
+
import {
|
|
1005
|
+
existsSync as existsSync3,
|
|
1006
|
+
mkdirSync as mkdirSync3,
|
|
1007
|
+
readdirSync as readdirSync2,
|
|
1008
|
+
readFileSync as readFileSync3,
|
|
1009
|
+
renameSync as renameSync2,
|
|
1010
|
+
rmSync as rmSync3,
|
|
1011
|
+
statSync as statSync2,
|
|
1012
|
+
unlinkSync,
|
|
1013
|
+
writeFileSync as writeFileSync3
|
|
1014
|
+
} from "node:fs";
|
|
1015
|
+
import { homedir as homedir2 } from "node:os";
|
|
1016
|
+
import { join as join4 } from "node:path";
|
|
1017
|
+
function runStateDir(env = process.env) {
|
|
1018
|
+
const override = env[RUN_STATE_DIR_ENV]?.trim();
|
|
1019
|
+
if (override)
|
|
1020
|
+
return override;
|
|
1021
|
+
return join4(homedir2(), ".harmony", "runs");
|
|
1022
|
+
}
|
|
1023
|
+
function sessionsDir(stateDir) {
|
|
1024
|
+
return join4(stateDir, "sessions");
|
|
1025
|
+
}
|
|
1026
|
+
function spoolDir(stateDir, agentSessionId) {
|
|
1027
|
+
return join4(stateDir, "spool", sanitizeIdForPath(agentSessionId));
|
|
1028
|
+
}
|
|
1029
|
+
function sanitizeIdForPath(id) {
|
|
1030
|
+
return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 100);
|
|
1031
|
+
}
|
|
1032
|
+
function pointerFileName(publisherPid, cardId) {
|
|
1033
|
+
return `${publisherPid}.${sanitizeIdForPath(cardId)}.json`;
|
|
1034
|
+
}
|
|
1035
|
+
function pidIsAlive(pid) {
|
|
1036
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
1037
|
+
return false;
|
|
1038
|
+
try {
|
|
1039
|
+
process.kill(pid, 0);
|
|
1040
|
+
return true;
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
return err?.code === "EPERM";
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function readProcParent(pid) {
|
|
1046
|
+
try {
|
|
1047
|
+
const stat = readFileSync3(`/proc/${pid}/stat`, "utf-8");
|
|
1048
|
+
const tail = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
|
|
1049
|
+
const ppid = Number.parseInt(tail[1] ?? "", 10);
|
|
1050
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
1051
|
+
} catch {
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function psParentTable() {
|
|
1056
|
+
if (psTableCache)
|
|
1057
|
+
return psTableCache;
|
|
1058
|
+
const table = new Map;
|
|
1059
|
+
try {
|
|
1060
|
+
const out = execFileSync("ps", ["-Ao", "pid=,ppid="], {
|
|
1061
|
+
encoding: "utf-8",
|
|
1062
|
+
timeout: 2000,
|
|
1063
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1064
|
+
});
|
|
1065
|
+
for (const line of out.split(`
|
|
1066
|
+
`)) {
|
|
1067
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
1068
|
+
if (!match)
|
|
1069
|
+
continue;
|
|
1070
|
+
table.set(Number(match[1]), Number(match[2]));
|
|
1071
|
+
}
|
|
1072
|
+
} catch {}
|
|
1073
|
+
psTableCache = table;
|
|
1074
|
+
return table;
|
|
1075
|
+
}
|
|
1076
|
+
function ancestorPids(pid, readParent) {
|
|
1077
|
+
const parentOf = readParent ?? ((child) => {
|
|
1078
|
+
const viaProc = readProcParent(child);
|
|
1079
|
+
if (viaProc !== null)
|
|
1080
|
+
return viaProc;
|
|
1081
|
+
return psParentTable().get(child) ?? null;
|
|
1082
|
+
});
|
|
1083
|
+
const chain2 = [];
|
|
1084
|
+
const seen = new Set([pid]);
|
|
1085
|
+
let current = pid;
|
|
1086
|
+
if (!readParent && pid === process.pid) {
|
|
1087
|
+
const ppid = process.ppid;
|
|
1088
|
+
if (Number.isInteger(ppid) && ppid > 1) {
|
|
1089
|
+
chain2.push(ppid);
|
|
1090
|
+
seen.add(ppid);
|
|
1091
|
+
current = ppid;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
for (let depth = chain2.length;depth < MAX_ANCESTOR_DEPTH; depth++) {
|
|
1095
|
+
const parent = parentOf(current);
|
|
1096
|
+
if (parent === null || parent <= 1 || seen.has(parent))
|
|
1097
|
+
break;
|
|
1098
|
+
chain2.push(parent);
|
|
1099
|
+
seen.add(parent);
|
|
1100
|
+
current = parent;
|
|
1101
|
+
}
|
|
1102
|
+
return chain2;
|
|
1103
|
+
}
|
|
1104
|
+
function publishRunSession(session, options) {
|
|
1105
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
1106
|
+
const pid = options?.pid ?? process.pid;
|
|
1107
|
+
const record = {
|
|
1108
|
+
cardId: session.cardId,
|
|
1109
|
+
agentSessionId: session.agentSessionId,
|
|
1110
|
+
publisherPid: pid,
|
|
1111
|
+
ancestorPids: options?.ancestors ?? ancestorPids(pid),
|
|
1112
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
1113
|
+
updatedAt: new Date().toISOString()
|
|
1114
|
+
};
|
|
1115
|
+
try {
|
|
1116
|
+
const dir = sessionsDir(stateDir);
|
|
1117
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
1118
|
+
const target = join4(dir, pointerFileName(pid, session.cardId));
|
|
1119
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
1120
|
+
writeFileSync3(temp, JSON.stringify(record), { mode: 384 });
|
|
1121
|
+
renameSync2(temp, target);
|
|
1122
|
+
return record;
|
|
1123
|
+
} catch {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function clearRunSession(cardId, options) {
|
|
1128
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
1129
|
+
const pid = options?.pid ?? process.pid;
|
|
1130
|
+
if (!options?.keepPointer) {
|
|
1131
|
+
try {
|
|
1132
|
+
unlinkSync(join4(sessionsDir(stateDir), pointerFileName(pid, cardId)));
|
|
1133
|
+
} catch {}
|
|
1134
|
+
}
|
|
1135
|
+
if (options?.agentSessionId) {
|
|
1136
|
+
try {
|
|
1137
|
+
rmSync3(spoolDir(stateDir, options.agentSessionId), {
|
|
1138
|
+
recursive: true,
|
|
1139
|
+
force: true
|
|
1140
|
+
});
|
|
1141
|
+
} catch {}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function readPublishedSessions(options) {
|
|
1145
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
1146
|
+
const now = options?.now ?? Date.now();
|
|
1147
|
+
const dir = sessionsDir(stateDir);
|
|
1148
|
+
let names;
|
|
1149
|
+
try {
|
|
1150
|
+
names = readdirSync2(dir);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return [];
|
|
1153
|
+
}
|
|
1154
|
+
const live = [];
|
|
1155
|
+
for (const name of names) {
|
|
1156
|
+
if (!name.endsWith(".json"))
|
|
1157
|
+
continue;
|
|
1158
|
+
const path = join4(dir, name);
|
|
1159
|
+
let record;
|
|
1160
|
+
try {
|
|
1161
|
+
record = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1162
|
+
} catch {
|
|
1163
|
+
safeUnlink(path);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if (typeof record?.cardId !== "string" || typeof record?.agentSessionId !== "string" || !record.cardId || !record.agentSessionId) {
|
|
1167
|
+
safeUnlink(path);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
const age = now - Date.parse(record.updatedAt ?? "");
|
|
1171
|
+
if (!Number.isFinite(age) || age > MAX_POINTER_AGE_MS) {
|
|
1172
|
+
safeUnlink(path);
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (!pidIsAlive(record.publisherPid)) {
|
|
1176
|
+
safeUnlink(path);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
live.push({ ...record, ancestorPids: record.ancestorPids ?? [] });
|
|
1180
|
+
}
|
|
1181
|
+
return live;
|
|
1182
|
+
}
|
|
1183
|
+
function safeUnlink(path) {
|
|
1184
|
+
try {
|
|
1185
|
+
unlinkSync(path);
|
|
1186
|
+
} catch {}
|
|
1187
|
+
}
|
|
1188
|
+
function chooseRunSessionForHook(args) {
|
|
1189
|
+
const { candidates, hookAncestorPids } = args;
|
|
1190
|
+
if (candidates.length === 0)
|
|
1191
|
+
return null;
|
|
1192
|
+
const hasCwd = typeof args.cwd === "string" && args.cwd.length > 0;
|
|
1193
|
+
const pool = hasCwd ? candidates.filter((candidate) => candidate.cwd === args.cwd) : candidates;
|
|
1194
|
+
if (pool.length === 0)
|
|
1195
|
+
return null;
|
|
1196
|
+
let best = null;
|
|
1197
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1198
|
+
for (const candidate of pool) {
|
|
1199
|
+
const claimed = new Set([
|
|
1200
|
+
candidate.publisherPid,
|
|
1201
|
+
...candidate.ancestorPids ?? []
|
|
1202
|
+
]);
|
|
1203
|
+
let score = Number.POSITIVE_INFINITY;
|
|
1204
|
+
for (let i = 0;i < hookAncestorPids.length; i++) {
|
|
1205
|
+
if (claimed.has(hookAncestorPids[i])) {
|
|
1206
|
+
score = i;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (score === Number.POSITIVE_INFINITY)
|
|
1211
|
+
continue;
|
|
1212
|
+
if (score < bestScore || score === bestScore && best !== null && Date.parse(candidate.updatedAt) > Date.parse(best.updatedAt)) {
|
|
1213
|
+
best = candidate;
|
|
1214
|
+
bestScore = score;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (best)
|
|
1218
|
+
return best;
|
|
1219
|
+
if (hasCwd && pool.length === 1)
|
|
1220
|
+
return pool[0];
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
function writeSpoolBatch(dir, events, options) {
|
|
1224
|
+
if (events.length === 0)
|
|
1225
|
+
return null;
|
|
1226
|
+
const now = options?.now ?? Date.now();
|
|
1227
|
+
const pid = options?.pid ?? process.pid;
|
|
1228
|
+
const nonce = options?.nonce ?? Math.random().toString(36).slice(2, 8).padEnd(6, "0");
|
|
1229
|
+
try {
|
|
1230
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
1231
|
+
const name = `${String(now).padStart(14, "0")}-${pid}-${nonce}.json`;
|
|
1232
|
+
const target = join4(dir, name);
|
|
1233
|
+
const temp = `${target}.tmp`;
|
|
1234
|
+
writeFileSync3(temp, JSON.stringify(events), { mode: 384 });
|
|
1235
|
+
renameSync2(temp, target);
|
|
1236
|
+
return target;
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function readSpoolBatches(dir, limit = 200) {
|
|
1242
|
+
let names;
|
|
1243
|
+
try {
|
|
1244
|
+
names = readdirSync2(dir);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
const batches = [];
|
|
1249
|
+
for (const name of names.filter((n) => n.endsWith(".json")).sort()) {
|
|
1250
|
+
if (batches.length >= limit)
|
|
1251
|
+
break;
|
|
1252
|
+
const path = join4(dir, name);
|
|
1253
|
+
try {
|
|
1254
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1255
|
+
if (!Array.isArray(parsed)) {
|
|
1256
|
+
safeUnlink(path);
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
batches.push({ path, events: parsed });
|
|
1260
|
+
} catch {
|
|
1261
|
+
safeUnlink(path);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return batches;
|
|
1265
|
+
}
|
|
1266
|
+
function removeSpoolBatches(paths) {
|
|
1267
|
+
for (const path of paths)
|
|
1268
|
+
safeUnlink(path);
|
|
1269
|
+
}
|
|
1270
|
+
function trimSpool(dir, max) {
|
|
1271
|
+
let names;
|
|
1272
|
+
try {
|
|
1273
|
+
names = readdirSync2(dir).filter((n) => n.endsWith(".json")).sort();
|
|
1274
|
+
} catch {
|
|
1275
|
+
return 0;
|
|
1276
|
+
}
|
|
1277
|
+
if (names.length <= max)
|
|
1278
|
+
return 0;
|
|
1279
|
+
const excess = names.slice(0, names.length - max);
|
|
1280
|
+
for (const name of excess)
|
|
1281
|
+
safeUnlink(join4(dir, name));
|
|
1282
|
+
return excess.length;
|
|
1283
|
+
}
|
|
1284
|
+
function routeMemoPath(stateDir, harnessSessionId) {
|
|
1285
|
+
return join4(stateDir, "routes", `${sanitizeIdForPath(harnessSessionId)}.json`);
|
|
1286
|
+
}
|
|
1287
|
+
function readRouteMemo(stateDir, harnessSessionId) {
|
|
1288
|
+
try {
|
|
1289
|
+
const raw = JSON.parse(readFileSync3(routeMemoPath(stateDir, harnessSessionId), "utf-8"));
|
|
1290
|
+
const pid = raw?.publisherPid;
|
|
1291
|
+
const cardId = raw?.cardId;
|
|
1292
|
+
const agentSessionId = raw?.agentSessionId;
|
|
1293
|
+
if (typeof pid !== "number" || !Number.isInteger(pid))
|
|
1294
|
+
return null;
|
|
1295
|
+
if (typeof cardId !== "string" || cardId.length === 0)
|
|
1296
|
+
return null;
|
|
1297
|
+
if (typeof agentSessionId !== "string" || agentSessionId.length === 0) {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
return { publisherPid: pid, cardId, agentSessionId };
|
|
1301
|
+
} catch {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function writeRouteMemo(stateDir, harnessSessionId, memo) {
|
|
1306
|
+
try {
|
|
1307
|
+
const path = routeMemoPath(stateDir, harnessSessionId);
|
|
1308
|
+
mkdirSync3(join4(stateDir, "routes"), { recursive: true, mode: 448 });
|
|
1309
|
+
writeFileSync3(path, JSON.stringify({
|
|
1310
|
+
publisherPid: memo.publisherPid,
|
|
1311
|
+
cardId: memo.cardId,
|
|
1312
|
+
agentSessionId: memo.agentSessionId
|
|
1313
|
+
}), { mode: 384 });
|
|
1314
|
+
} catch {}
|
|
1315
|
+
}
|
|
1316
|
+
function runStateExists(stateDir = runStateDir()) {
|
|
1317
|
+
try {
|
|
1318
|
+
return existsSync3(sessionsDir(stateDir)) && statSync2(sessionsDir(stateDir)).isDirectory();
|
|
1319
|
+
} catch {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
var RUN_STATE_DIR_ENV = "HARMONY_RUN_STATE_DIR", MAX_POINTER_AGE_MS, MAX_ANCESTOR_DEPTH = 12, psTableCache = null;
|
|
1324
|
+
var init_run_state = __esm(() => {
|
|
1325
|
+
MAX_POINTER_AGE_MS = 10 * 60000;
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// src/hook-install.ts
|
|
1329
|
+
var exports_hook_install = {};
|
|
1330
|
+
__export(exports_hook_install, {
|
|
1331
|
+
userSettingsPath: () => userSettingsPath,
|
|
1332
|
+
uninstallUserHook: () => uninstallUserHook,
|
|
1333
|
+
removeHarmonyHook: () => removeHarmonyHook,
|
|
1334
|
+
installUserHook: () => installUserHook,
|
|
1335
|
+
hookInstallStatus: () => hookInstallStatus,
|
|
1336
|
+
hookCommandBinary: () => hookCommandBinary,
|
|
1337
|
+
hookCommand: () => hookCommand,
|
|
1338
|
+
hookBinaryPath: () => hookBinaryPath,
|
|
1339
|
+
hasHarmonyHook: () => hasHarmonyHook,
|
|
1340
|
+
harmonyHookCommand: () => harmonyHookCommand,
|
|
1341
|
+
addHarmonyHook: () => addHarmonyHook,
|
|
1342
|
+
HOOK_MARKER: () => HOOK_MARKER
|
|
1343
|
+
});
|
|
1344
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1345
|
+
import { homedir as homedir8 } from "node:os";
|
|
1346
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
1347
|
+
import { fileURLToPath } from "node:url";
|
|
1348
|
+
function userSettingsPath(home = homedir8()) {
|
|
1349
|
+
return join10(home, ".claude", "settings.json");
|
|
1350
|
+
}
|
|
1351
|
+
function hookBinaryPath(moduleUrl = import.meta.url) {
|
|
1352
|
+
const here = dirname5(fileURLToPath(moduleUrl));
|
|
1353
|
+
const candidates = [
|
|
1354
|
+
join10(here, "run-hook-cli.js"),
|
|
1355
|
+
join10(here, "..", "dist", "run-hook-cli.js"),
|
|
1356
|
+
join10(here, "run-hook-cli.ts")
|
|
1357
|
+
];
|
|
1358
|
+
return candidates.find((path) => existsSync10(path)) ?? candidates[0];
|
|
1359
|
+
}
|
|
1360
|
+
function hookCommand(binary, execPath = process.execPath) {
|
|
1361
|
+
return `[ -x "${execPath}" ] && [ -f "${binary}" ] && "${execPath}" "${binary}" ${HOOK_MARKER} || exit 0`;
|
|
1362
|
+
}
|
|
1363
|
+
function hookCommandBinary(command) {
|
|
1364
|
+
const match = /\[ -f "([^"]+)" \]/.exec(command);
|
|
1365
|
+
return match?.[1] ?? null;
|
|
1366
|
+
}
|
|
1367
|
+
function isHarmonyHook(entry) {
|
|
1368
|
+
return typeof entry?.command === "string" && entry.command.includes(HOOK_MARKER);
|
|
1369
|
+
}
|
|
1370
|
+
function addHarmonyHook(settings, command) {
|
|
1371
|
+
const next = { ...settings };
|
|
1372
|
+
const hooks = {
|
|
1373
|
+
...next.hooks ?? {}
|
|
1374
|
+
};
|
|
1375
|
+
const postToolUse = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
|
|
1376
|
+
const ours = {
|
|
1377
|
+
type: "command",
|
|
1378
|
+
command,
|
|
1379
|
+
timeout: HOOK_TIMEOUT_SECONDS
|
|
1380
|
+
};
|
|
1381
|
+
let changed = false;
|
|
1382
|
+
let placed = false;
|
|
1383
|
+
for (let i = 0;i < postToolUse.length; i++) {
|
|
1384
|
+
const group = postToolUse[i];
|
|
1385
|
+
const inner = Array.isArray(group?.hooks) ? group.hooks : [];
|
|
1386
|
+
const index = inner.findIndex(isHarmonyHook);
|
|
1387
|
+
if (index === -1)
|
|
1388
|
+
continue;
|
|
1389
|
+
placed = true;
|
|
1390
|
+
if (inner[index]?.command !== command) {
|
|
1391
|
+
const updated = [...inner];
|
|
1392
|
+
updated[index] = ours;
|
|
1393
|
+
postToolUse[i] = { ...group, hooks: updated };
|
|
1394
|
+
changed = true;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (!placed) {
|
|
1398
|
+
postToolUse.push({ matcher: HOOK_MATCHER, hooks: [ours] });
|
|
1399
|
+
changed = true;
|
|
1400
|
+
}
|
|
1401
|
+
hooks.PostToolUse = postToolUse;
|
|
1402
|
+
next.hooks = hooks;
|
|
1403
|
+
return { settings: next, changed };
|
|
1404
|
+
}
|
|
1405
|
+
function removeHarmonyHook(settings) {
|
|
1406
|
+
const hooksValue = settings.hooks;
|
|
1407
|
+
if (hooksValue === null || typeof hooksValue !== "object") {
|
|
1408
|
+
return { settings, changed: false };
|
|
1409
|
+
}
|
|
1410
|
+
const hooks = {
|
|
1411
|
+
...hooksValue
|
|
1412
|
+
};
|
|
1413
|
+
if (!Array.isArray(hooks.PostToolUse)) {
|
|
1414
|
+
return { settings, changed: false };
|
|
1415
|
+
}
|
|
1416
|
+
let changed = false;
|
|
1417
|
+
const groups = [];
|
|
1418
|
+
for (const group of hooks.PostToolUse) {
|
|
1419
|
+
const inner = Array.isArray(group?.hooks) ? group.hooks : [];
|
|
1420
|
+
const kept = inner.filter((entry) => !isHarmonyHook(entry));
|
|
1421
|
+
if (kept.length !== inner.length)
|
|
1422
|
+
changed = true;
|
|
1423
|
+
if (kept.length === 0 && inner.length > 0)
|
|
1424
|
+
continue;
|
|
1425
|
+
groups.push(kept.length === inner.length ? group : { ...group, hooks: kept });
|
|
1426
|
+
}
|
|
1427
|
+
if (!changed)
|
|
1428
|
+
return { settings, changed: false };
|
|
1429
|
+
if (groups.length > 0) {
|
|
1430
|
+
hooks.PostToolUse = groups;
|
|
1431
|
+
} else {
|
|
1432
|
+
delete hooks.PostToolUse;
|
|
1433
|
+
}
|
|
1434
|
+
const next = { ...settings };
|
|
1435
|
+
if (Object.keys(hooks).length > 0) {
|
|
1436
|
+
next.hooks = hooks;
|
|
1437
|
+
} else {
|
|
1438
|
+
delete next.hooks;
|
|
1439
|
+
}
|
|
1440
|
+
return { settings: next, changed: true };
|
|
1441
|
+
}
|
|
1442
|
+
function hasHarmonyHook(settings) {
|
|
1443
|
+
return harmonyHookCommand(settings) !== null;
|
|
1444
|
+
}
|
|
1445
|
+
function harmonyHookCommand(settings) {
|
|
1446
|
+
const hooks = settings?.hooks;
|
|
1447
|
+
const groups = hooks?.PostToolUse;
|
|
1448
|
+
if (!Array.isArray(groups))
|
|
1449
|
+
return null;
|
|
1450
|
+
for (const group of groups) {
|
|
1451
|
+
for (const entry of Array.isArray(group?.hooks) ? group.hooks : []) {
|
|
1452
|
+
if (isHarmonyHook(entry))
|
|
1453
|
+
return entry.command;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
function hookInstallStatus(settings) {
|
|
1459
|
+
const command = harmonyHookCommand(settings);
|
|
1460
|
+
if (command === null) {
|
|
1461
|
+
return { installed: false, binary: null, binaryExists: false };
|
|
1462
|
+
}
|
|
1463
|
+
const binary = hookCommandBinary(command);
|
|
1464
|
+
return {
|
|
1465
|
+
installed: true,
|
|
1466
|
+
binary,
|
|
1467
|
+
binaryExists: binary !== null && existsSync10(binary)
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function readSettings(path) {
|
|
1471
|
+
try {
|
|
1472
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
1473
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1474
|
+
} catch {
|
|
1475
|
+
return {};
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function writeSettings(path, settings) {
|
|
1479
|
+
mkdirSync7(dirname5(path), { recursive: true });
|
|
1480
|
+
writeFileSync6(path, `${JSON.stringify(settings, null, 2)}
|
|
1481
|
+
`, "utf-8");
|
|
1482
|
+
}
|
|
1483
|
+
function installUserHook(options) {
|
|
1484
|
+
const path = options?.settingsPath ?? userSettingsPath();
|
|
1485
|
+
const command = hookCommand(options?.binary ?? hookBinaryPath());
|
|
1486
|
+
if (existsSync10(path)) {
|
|
1487
|
+
try {
|
|
1488
|
+
JSON.parse(readFileSync8(path, "utf-8"));
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
return {
|
|
1491
|
+
ok: false,
|
|
1492
|
+
path,
|
|
1493
|
+
error: `${path} is not valid JSON, so it was left untouched: ${err instanceof Error ? err.message : String(err)}`
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
const { settings, changed } = addHarmonyHook(readSettings(path), command);
|
|
1499
|
+
if (changed)
|
|
1500
|
+
writeSettings(path, settings);
|
|
1501
|
+
return { ok: true, path, changed, command };
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
return {
|
|
1504
|
+
ok: false,
|
|
1505
|
+
path,
|
|
1506
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function uninstallUserHook(options) {
|
|
1511
|
+
const path = options?.settingsPath ?? userSettingsPath();
|
|
1512
|
+
if (!existsSync10(path)) {
|
|
1513
|
+
return { ok: true, path, changed: false, command: "" };
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
JSON.parse(readFileSync8(path, "utf-8"));
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
return {
|
|
1519
|
+
ok: false,
|
|
1520
|
+
path,
|
|
1521
|
+
error: `${path} is not valid JSON, so it was left untouched: ${err instanceof Error ? err.message : String(err)}`
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
const { settings, changed } = removeHarmonyHook(readSettings(path));
|
|
1526
|
+
if (changed)
|
|
1527
|
+
writeSettings(path, settings);
|
|
1528
|
+
return { ok: true, path, changed, command: "" };
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
return {
|
|
1531
|
+
ok: false,
|
|
1532
|
+
path,
|
|
1533
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
var HOOK_MARKER = "--harmony-post-tool-use", HOOK_TIMEOUT_SECONDS = 10, HOOK_MATCHER = "*";
|
|
1538
|
+
var init_hook_install = () => {};
|
|
1539
|
+
|
|
981
1540
|
// src/cli.ts
|
|
982
1541
|
init_config();
|
|
983
1542
|
import { createRequire as createRequire2 } from "node:module";
|
|
@@ -2646,6 +3205,222 @@ function resetClient() {
|
|
|
2646
3205
|
client2 = null;
|
|
2647
3206
|
}
|
|
2648
3207
|
|
|
3208
|
+
// src/comment-session.ts
|
|
3209
|
+
var RUN_SESSION_CARD_ENV = "HARMONY_AGENT_CARD_ID";
|
|
3210
|
+
var RUN_SESSION_ID_ENV = "HARMONY_AGENT_SESSION_ID";
|
|
3211
|
+
function readDeclaredRunSession(env = process.env) {
|
|
3212
|
+
const cardId = env[RUN_SESSION_CARD_ENV]?.trim();
|
|
3213
|
+
const agentSessionId = env[RUN_SESSION_ID_ENV]?.trim();
|
|
3214
|
+
if (!cardId || !agentSessionId)
|
|
3215
|
+
return null;
|
|
3216
|
+
return { cardId, agentSessionId };
|
|
3217
|
+
}
|
|
3218
|
+
function chooseCommentSession(args) {
|
|
3219
|
+
const tracked = args.tracked;
|
|
3220
|
+
if (tracked?.agentSessionId && tracked.scopeId === args.callerScopeId) {
|
|
3221
|
+
return {
|
|
3222
|
+
kind: "session",
|
|
3223
|
+
agentSessionId: tracked.agentSessionId,
|
|
3224
|
+
source: "tracked"
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
const declared = args.declared;
|
|
3228
|
+
if (declared && declared.cardId === args.cardId) {
|
|
3229
|
+
return {
|
|
3230
|
+
kind: "session",
|
|
3231
|
+
agentSessionId: declared.agentSessionId,
|
|
3232
|
+
source: "declared"
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
return { kind: "sessionless" };
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
// src/run-event-forwarder.ts
|
|
3239
|
+
init_run_state();
|
|
3240
|
+
var FLUSH_INTERVAL_MS = 2000;
|
|
3241
|
+
var MAX_INTERVAL_MS = 60000;
|
|
3242
|
+
var MAX_EVENTS_PER_REQUEST = 500;
|
|
3243
|
+
var MAX_BATCHES_PER_FLUSH = 200;
|
|
3244
|
+
var MAX_BATCH_ATTEMPTS = 20;
|
|
3245
|
+
var POINTER_REFRESH_MS = 60000;
|
|
3246
|
+
|
|
3247
|
+
class RunEventForwarder {
|
|
3248
|
+
cardId;
|
|
3249
|
+
agentSessionId;
|
|
3250
|
+
getClient;
|
|
3251
|
+
stateDir;
|
|
3252
|
+
baseIntervalMs;
|
|
3253
|
+
timer = null;
|
|
3254
|
+
flushing = false;
|
|
3255
|
+
stopped = false;
|
|
3256
|
+
consecutiveFailures = 0;
|
|
3257
|
+
lastPointerRefresh = Date.now();
|
|
3258
|
+
attempts = new Map;
|
|
3259
|
+
constructor(options) {
|
|
3260
|
+
this.cardId = options.cardId;
|
|
3261
|
+
this.agentSessionId = options.agentSessionId;
|
|
3262
|
+
this.getClient = options.getClient;
|
|
3263
|
+
this.stateDir = options.stateDir ?? runStateDir();
|
|
3264
|
+
this.baseIntervalMs = options.intervalMs ?? FLUSH_INTERVAL_MS;
|
|
3265
|
+
}
|
|
3266
|
+
get dir() {
|
|
3267
|
+
return spoolDir(this.stateDir, this.agentSessionId);
|
|
3268
|
+
}
|
|
3269
|
+
nextDelay() {
|
|
3270
|
+
if (this.consecutiveFailures === 0)
|
|
3271
|
+
return this.baseIntervalMs;
|
|
3272
|
+
const scaled = this.baseIntervalMs * 2 ** Math.min(this.consecutiveFailures, 6);
|
|
3273
|
+
return Math.min(scaled, MAX_INTERVAL_MS);
|
|
3274
|
+
}
|
|
3275
|
+
start() {
|
|
3276
|
+
if (this.timer || this.stopped)
|
|
3277
|
+
return;
|
|
3278
|
+
this.schedule();
|
|
3279
|
+
}
|
|
3280
|
+
schedule() {
|
|
3281
|
+
if (this.stopped)
|
|
3282
|
+
return;
|
|
3283
|
+
this.timer = setTimeout(() => {
|
|
3284
|
+
this.timer = null;
|
|
3285
|
+
this.flush().finally(() => this.schedule());
|
|
3286
|
+
}, this.nextDelay());
|
|
3287
|
+
this.timer.unref?.();
|
|
3288
|
+
}
|
|
3289
|
+
async flush() {
|
|
3290
|
+
if (this.flushing)
|
|
3291
|
+
return 0;
|
|
3292
|
+
this.flushing = true;
|
|
3293
|
+
try {
|
|
3294
|
+
this.refreshPointer();
|
|
3295
|
+
const batches = readSpoolBatches(this.dir, MAX_BATCHES_PER_FLUSH);
|
|
3296
|
+
if (batches.length === 0) {
|
|
3297
|
+
this.consecutiveFailures = 0;
|
|
3298
|
+
return 0;
|
|
3299
|
+
}
|
|
3300
|
+
const paths = [];
|
|
3301
|
+
const events = [];
|
|
3302
|
+
for (const batch of batches) {
|
|
3303
|
+
if (events.length > 0 && events.length + batch.events.length > MAX_EVENTS_PER_REQUEST) {
|
|
3304
|
+
break;
|
|
3305
|
+
}
|
|
3306
|
+
paths.push(batch.path);
|
|
3307
|
+
events.push(...batch.events);
|
|
3308
|
+
}
|
|
3309
|
+
if (events.length === 0)
|
|
3310
|
+
return 0;
|
|
3311
|
+
try {
|
|
3312
|
+
await this.getClient().appendAgentRunEvents(this.cardId, {
|
|
3313
|
+
sessionId: this.agentSessionId,
|
|
3314
|
+
events
|
|
3315
|
+
});
|
|
3316
|
+
removeSpoolBatches(paths);
|
|
3317
|
+
for (const path of paths)
|
|
3318
|
+
this.attempts.delete(path);
|
|
3319
|
+
this.consecutiveFailures = 0;
|
|
3320
|
+
return events.length;
|
|
3321
|
+
} catch {
|
|
3322
|
+
this.consecutiveFailures++;
|
|
3323
|
+
const poisoned = [];
|
|
3324
|
+
for (const path of paths) {
|
|
3325
|
+
const next = (this.attempts.get(path) ?? 0) + 1;
|
|
3326
|
+
this.attempts.set(path, next);
|
|
3327
|
+
if (next >= MAX_BATCH_ATTEMPTS)
|
|
3328
|
+
poisoned.push(path);
|
|
3329
|
+
}
|
|
3330
|
+
if (poisoned.length > 0) {
|
|
3331
|
+
removeSpoolBatches(poisoned);
|
|
3332
|
+
for (const path of poisoned)
|
|
3333
|
+
this.attempts.delete(path);
|
|
3334
|
+
}
|
|
3335
|
+
return 0;
|
|
3336
|
+
}
|
|
3337
|
+
} finally {
|
|
3338
|
+
this.flushing = false;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
refreshPointer() {
|
|
3342
|
+
const now = Date.now();
|
|
3343
|
+
if (now - this.lastPointerRefresh < POINTER_REFRESH_MS)
|
|
3344
|
+
return;
|
|
3345
|
+
this.lastPointerRefresh = now;
|
|
3346
|
+
publishRunSession({ cardId: this.cardId, agentSessionId: this.agentSessionId }, { stateDir: this.stateDir });
|
|
3347
|
+
}
|
|
3348
|
+
async stop(handover) {
|
|
3349
|
+
if (this.stopped)
|
|
3350
|
+
return;
|
|
3351
|
+
this.stopped = true;
|
|
3352
|
+
if (this.timer) {
|
|
3353
|
+
clearTimeout(this.timer);
|
|
3354
|
+
this.timer = null;
|
|
3355
|
+
}
|
|
3356
|
+
try {
|
|
3357
|
+
await this.flush();
|
|
3358
|
+
} catch {}
|
|
3359
|
+
const handoverTo = handover?.handoverTo;
|
|
3360
|
+
const keepPointer = handoverTo !== undefined;
|
|
3361
|
+
const keepSpool = handoverTo === this.agentSessionId;
|
|
3362
|
+
clearRunSession(this.cardId, {
|
|
3363
|
+
stateDir: this.stateDir,
|
|
3364
|
+
...keepSpool ? {} : { agentSessionId: this.agentSessionId },
|
|
3365
|
+
...keepPointer ? { keepPointer: true } : {}
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
var forwarders = new Map;
|
|
3370
|
+
var DISABLE_RUN_HOOK_ENV = "HARMONY_DISABLE_RUN_HOOK";
|
|
3371
|
+
function hookTimelineDisabled(env, explicitStateDir) {
|
|
3372
|
+
const off = env[DISABLE_RUN_HOOK_ENV]?.trim().toLowerCase();
|
|
3373
|
+
if (off && off !== "0" && off !== "false")
|
|
3374
|
+
return true;
|
|
3375
|
+
if (env.NODE_ENV === "test" && !explicitStateDir)
|
|
3376
|
+
return true;
|
|
3377
|
+
return false;
|
|
3378
|
+
}
|
|
3379
|
+
function startRunEventForwarder(options) {
|
|
3380
|
+
const existing = forwarders.get(options.cardId);
|
|
3381
|
+
if (existing)
|
|
3382
|
+
existing.stop({ handoverTo: options.agentSessionId });
|
|
3383
|
+
const forwarder = new RunEventForwarder(options);
|
|
3384
|
+
forwarders.set(options.cardId, forwarder);
|
|
3385
|
+
forwarder.start();
|
|
3386
|
+
return forwarder;
|
|
3387
|
+
}
|
|
3388
|
+
async function stopRunEventForwarder(cardId) {
|
|
3389
|
+
const forwarder = forwarders.get(cardId);
|
|
3390
|
+
if (!forwarder)
|
|
3391
|
+
return;
|
|
3392
|
+
forwarders.delete(cardId);
|
|
3393
|
+
await forwarder.stop();
|
|
3394
|
+
}
|
|
3395
|
+
async function stopAllRunEventForwarders() {
|
|
3396
|
+
const all = [...forwarders.values()];
|
|
3397
|
+
forwarders.clear();
|
|
3398
|
+
await Promise.all(all.map((f) => f.stop().catch(() => {
|
|
3399
|
+
return;
|
|
3400
|
+
})));
|
|
3401
|
+
}
|
|
3402
|
+
function beginHookTimeline(args) {
|
|
3403
|
+
if (!args.agentSessionId)
|
|
3404
|
+
return null;
|
|
3405
|
+
const env = args.env ?? process.env;
|
|
3406
|
+
if (readDeclaredRunSession(env))
|
|
3407
|
+
return null;
|
|
3408
|
+
if (hookTimelineDisabled(env, args.stateDir))
|
|
3409
|
+
return null;
|
|
3410
|
+
const published = publishRunSession({ cardId: args.cardId, agentSessionId: args.agentSessionId }, args.stateDir ? { stateDir: args.stateDir } : undefined);
|
|
3411
|
+
if (!published)
|
|
3412
|
+
return null;
|
|
3413
|
+
return startRunEventForwarder({
|
|
3414
|
+
cardId: args.cardId,
|
|
3415
|
+
agentSessionId: args.agentSessionId,
|
|
3416
|
+
getClient: args.getClient,
|
|
3417
|
+
...args.stateDir ? { stateDir: args.stateDir } : {}
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
async function endHookTimeline(cardId) {
|
|
3421
|
+
await stopRunEventForwarder(cardId);
|
|
3422
|
+
}
|
|
3423
|
+
|
|
2649
3424
|
// src/auto-session.ts
|
|
2650
3425
|
var CLIENT_DISPLAY_NAMES = {
|
|
2651
3426
|
"claude-code": "Claude Code",
|
|
@@ -2729,6 +3504,7 @@ async function trackActivity(cardId, options) {
|
|
|
2729
3504
|
for (const otherCardId of toEnd) {
|
|
2730
3505
|
await autoEndSession(scope, client3, otherCardId, "completed");
|
|
2731
3506
|
}
|
|
3507
|
+
let agentSessionId;
|
|
2732
3508
|
try {
|
|
2733
3509
|
const started = await client3.startAgentSession(cardId, {
|
|
2734
3510
|
agentIdentifier,
|
|
@@ -2738,6 +3514,7 @@ async function trackActivity(cardId, options) {
|
|
|
2738
3514
|
});
|
|
2739
3515
|
if (started?.session === null)
|
|
2740
3516
|
return;
|
|
3517
|
+
agentSessionId = started?.session?.id;
|
|
2741
3518
|
} catch {}
|
|
2742
3519
|
scope.sessions.set(cardId, {
|
|
2743
3520
|
cardId,
|
|
@@ -2746,8 +3523,14 @@ async function trackActivity(cardId, options) {
|
|
|
2746
3523
|
isExplicit: false,
|
|
2747
3524
|
agentIdentifier,
|
|
2748
3525
|
agentName,
|
|
3526
|
+
agentSessionId,
|
|
2749
3527
|
status: "working"
|
|
2750
3528
|
});
|
|
3529
|
+
beginHookTimeline({
|
|
3530
|
+
cardId,
|
|
3531
|
+
agentSessionId,
|
|
3532
|
+
getClient: () => client3
|
|
3533
|
+
});
|
|
2751
3534
|
}
|
|
2752
3535
|
function markExplicit(cardId, options) {
|
|
2753
3536
|
const scope = getOrCreateScope(options?.scopeId ?? DEFAULT_SCOPE);
|
|
@@ -2848,6 +3631,9 @@ function sweepTick() {
|
|
|
2848
3631
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2849
3632
|
if (!scope.sessions.delete(cardId))
|
|
2850
3633
|
return;
|
|
3634
|
+
try {
|
|
3635
|
+
await endHookTimeline(cardId);
|
|
3636
|
+
} catch {}
|
|
2851
3637
|
try {
|
|
2852
3638
|
await client3.endAgentSession(cardId, { status });
|
|
2853
3639
|
} catch {}
|
|
@@ -2856,36 +3642,6 @@ async function autoEndSession(scope, client3, cardId, status) {
|
|
|
2856
3642
|
} catch {}
|
|
2857
3643
|
}
|
|
2858
3644
|
|
|
2859
|
-
// src/comment-session.ts
|
|
2860
|
-
var RUN_SESSION_CARD_ENV = "HARMONY_AGENT_CARD_ID";
|
|
2861
|
-
var RUN_SESSION_ID_ENV = "HARMONY_AGENT_SESSION_ID";
|
|
2862
|
-
function readDeclaredRunSession(env = process.env) {
|
|
2863
|
-
const cardId = env[RUN_SESSION_CARD_ENV]?.trim();
|
|
2864
|
-
const agentSessionId = env[RUN_SESSION_ID_ENV]?.trim();
|
|
2865
|
-
if (!cardId || !agentSessionId)
|
|
2866
|
-
return null;
|
|
2867
|
-
return { cardId, agentSessionId };
|
|
2868
|
-
}
|
|
2869
|
-
function chooseCommentSession(args) {
|
|
2870
|
-
const tracked = args.tracked;
|
|
2871
|
-
if (tracked?.agentSessionId && tracked.scopeId === args.callerScopeId) {
|
|
2872
|
-
return {
|
|
2873
|
-
kind: "session",
|
|
2874
|
-
agentSessionId: tracked.agentSessionId,
|
|
2875
|
-
source: "tracked"
|
|
2876
|
-
};
|
|
2877
|
-
}
|
|
2878
|
-
const declared = args.declared;
|
|
2879
|
-
if (declared && declared.cardId === args.cardId) {
|
|
2880
|
-
return {
|
|
2881
|
-
kind: "session",
|
|
2882
|
-
agentSessionId: declared.agentSessionId,
|
|
2883
|
-
source: "declared"
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
2886
|
-
return { kind: "sessionless" };
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
3645
|
// src/server.ts
|
|
2890
3646
|
init_config();
|
|
2891
3647
|
|
|
@@ -3559,30 +4315,30 @@ async function collectPlaybookMetricWarnings(client3, workspaceId, steps) {
|
|
|
3559
4315
|
|
|
3560
4316
|
// src/skills.ts
|
|
3561
4317
|
import {
|
|
3562
|
-
existsSync as
|
|
3563
|
-
mkdirSync as
|
|
3564
|
-
readFileSync as
|
|
3565
|
-
renameSync as
|
|
3566
|
-
writeFileSync as
|
|
4318
|
+
existsSync as existsSync5,
|
|
4319
|
+
mkdirSync as mkdirSync4,
|
|
4320
|
+
readFileSync as readFileSync5,
|
|
4321
|
+
renameSync as renameSync3,
|
|
4322
|
+
writeFileSync as writeFileSync4
|
|
3567
4323
|
} from "node:fs";
|
|
3568
|
-
import { homedir as
|
|
3569
|
-
import { dirname as dirname2, join as
|
|
4324
|
+
import { homedir as homedir4 } from "node:os";
|
|
4325
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
3570
4326
|
init_config();
|
|
3571
4327
|
|
|
3572
4328
|
// src/hmy-config.ts
|
|
3573
|
-
import { existsSync as
|
|
3574
|
-
import { homedir as
|
|
3575
|
-
import { join as
|
|
4329
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
4330
|
+
import { homedir as homedir3 } from "node:os";
|
|
4331
|
+
import { join as join5 } from "node:path";
|
|
3576
4332
|
var DEFAULTS = { updateCheck: true, pin: null };
|
|
3577
4333
|
function getHmyConfigPath() {
|
|
3578
|
-
return
|
|
4334
|
+
return join5(homedir3(), ".hmy", "config.yaml");
|
|
3579
4335
|
}
|
|
3580
4336
|
function loadHmyConfig() {
|
|
3581
4337
|
const path = getHmyConfigPath();
|
|
3582
|
-
if (!
|
|
4338
|
+
if (!existsSync4(path))
|
|
3583
4339
|
return { ...DEFAULTS };
|
|
3584
4340
|
try {
|
|
3585
|
-
return parseHmyConfig(
|
|
4341
|
+
return parseHmyConfig(readFileSync4(path, "utf-8"));
|
|
3586
4342
|
} catch {
|
|
3587
4343
|
return { ...DEFAULTS };
|
|
3588
4344
|
}
|
|
@@ -3714,11 +4470,11 @@ function stripSkillPreamble(content) {
|
|
|
3714
4470
|
}
|
|
3715
4471
|
function atomicWrite(filePath, content) {
|
|
3716
4472
|
const dir = dirname2(filePath);
|
|
3717
|
-
if (!
|
|
3718
|
-
|
|
4473
|
+
if (!existsSync5(dir))
|
|
4474
|
+
mkdirSync4(dir, { recursive: true });
|
|
3719
4475
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
3720
|
-
|
|
3721
|
-
|
|
4476
|
+
writeFileSync4(tmp, content);
|
|
4477
|
+
renameSync3(tmp, filePath);
|
|
3722
4478
|
}
|
|
3723
4479
|
function hasMetadataVersion(content) {
|
|
3724
4480
|
return parseSkillVersion(content) !== null;
|
|
@@ -3747,7 +4503,7 @@ function parseSkillVersion(content) {
|
|
|
3747
4503
|
function findSkillFiles(paths, knownNames) {
|
|
3748
4504
|
const results = [];
|
|
3749
4505
|
for (const filePath of paths) {
|
|
3750
|
-
if (!
|
|
4506
|
+
if (!existsSync5(filePath))
|
|
3751
4507
|
continue;
|
|
3752
4508
|
for (const name of knownNames) {
|
|
3753
4509
|
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
@@ -3758,15 +4514,15 @@ function findSkillFiles(paths, knownNames) {
|
|
|
3758
4514
|
}
|
|
3759
4515
|
return results;
|
|
3760
4516
|
}
|
|
3761
|
-
var HMY_DIR =
|
|
3762
|
-
var HMY_VERSION_FILE =
|
|
3763
|
-
var LAST_CHECK_FILE =
|
|
4517
|
+
var HMY_DIR = join6(homedir4(), ".hmy");
|
|
4518
|
+
var HMY_VERSION_FILE = join6(HMY_DIR, "VERSION");
|
|
4519
|
+
var LAST_CHECK_FILE = join6(HMY_DIR, "last-update-check");
|
|
3764
4520
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3765
4521
|
function checkedRecently(now = Date.now()) {
|
|
3766
4522
|
try {
|
|
3767
|
-
if (!
|
|
4523
|
+
if (!existsSync5(LAST_CHECK_FILE))
|
|
3768
4524
|
return false;
|
|
3769
|
-
const ts = Number.parseInt(
|
|
4525
|
+
const ts = Number.parseInt(readFileSync5(LAST_CHECK_FILE, "utf-8").trim(), 10);
|
|
3770
4526
|
if (!Number.isFinite(ts))
|
|
3771
4527
|
return false;
|
|
3772
4528
|
return now - ts < CHECK_TTL_MS;
|
|
@@ -3776,9 +4532,9 @@ function checkedRecently(now = Date.now()) {
|
|
|
3776
4532
|
}
|
|
3777
4533
|
function recordCheck(now = Date.now()) {
|
|
3778
4534
|
try {
|
|
3779
|
-
if (!
|
|
3780
|
-
|
|
3781
|
-
|
|
4535
|
+
if (!existsSync5(HMY_DIR))
|
|
4536
|
+
mkdirSync4(HMY_DIR, { recursive: true });
|
|
4537
|
+
writeFileSync4(LAST_CHECK_FILE, String(now));
|
|
3782
4538
|
} catch {}
|
|
3783
4539
|
}
|
|
3784
4540
|
async function refreshSkills(opts = {}) {
|
|
@@ -3810,7 +4566,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3810
4566
|
const parentDir = dirname2(samplePath);
|
|
3811
4567
|
siblingPath = `${parentDir}/${name}.md`;
|
|
3812
4568
|
}
|
|
3813
|
-
if (
|
|
4569
|
+
if (existsSync5(siblingPath)) {
|
|
3814
4570
|
skillFiles.push({ name, filePath: siblingPath });
|
|
3815
4571
|
}
|
|
3816
4572
|
}
|
|
@@ -3820,7 +4576,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3820
4576
|
let updated = false;
|
|
3821
4577
|
for (const { name, filePath } of skillFiles) {
|
|
3822
4578
|
try {
|
|
3823
|
-
const currentContent =
|
|
4579
|
+
const currentContent = readFileSync5(filePath, "utf-8");
|
|
3824
4580
|
const localVersion = parseSkillVersion(currentContent);
|
|
3825
4581
|
const fetched = await client3.fetchSkill(name);
|
|
3826
4582
|
const remoteVersion = fetched.skillVersion;
|
|
@@ -5626,7 +6382,7 @@ var TOOLS = {
|
|
|
5626
6382
|
}
|
|
5627
6383
|
},
|
|
5628
6384
|
harmony_link_plan_task: {
|
|
5629
|
-
description: "Point a plan CRITERION at the card that fulfils it, and/or set the criterion's status. " + "A plan task is a success criterion, not a work item: `cards.plan_id` says which plan a card " + "belongs to, and this sets the return leg the plan needs to show real progress instead of " + "guessing from board-column names. Use it to repair a link, to re-point a criterion at a " + "different card, or to mark a criterion `completed` once its card actually delivered it. " + "Leave a criterion the card did NOT deliver open — an open criterion is the signal.",
|
|
6385
|
+
description: "Point a plan CRITERION at the card that fulfils it, and/or set the criterion's status. " + "A plan task is a success criterion, not a work item: `cards.plan_id` says which plan a card " + "belongs to, and this sets the return leg the plan needs to show real progress instead of " + "guessing from board-column names. Use it to repair a link, to re-point a criterion at a " + "different card, or to mark a criterion `completed` once its card actually delivered it. " + "Leave a criterion the card did NOT deliver open — an open criterion is the signal. " + "Linking a card that belongs to no plan yet also adopts it into this one (reported as " + "`cardPlanAdopted`), and linking a card that already belongs to a DIFFERENT plan is " + "refused — move it with harmony_update_card first, so the two directions cannot drift.",
|
|
5630
6386
|
inputSchema: {
|
|
5631
6387
|
type: "object",
|
|
5632
6388
|
properties: {
|
|
@@ -5637,7 +6393,7 @@ var TOOLS = {
|
|
|
5637
6393
|
},
|
|
5638
6394
|
cardId: {
|
|
5639
6395
|
type: "string",
|
|
5640
|
-
description: "Card that fulfils this criterion. Must live in the plan's own project."
|
|
6396
|
+
description: "Card that fulfils this criterion. Must live in the plan's own project, and " + "must belong to this plan or to no plan yet — a card already in another plan " + "is refused rather than silently re-pointed."
|
|
5641
6397
|
},
|
|
5642
6398
|
status: {
|
|
5643
6399
|
type: "string",
|
|
@@ -6841,6 +7597,11 @@ ${options}
|
|
|
6841
7597
|
});
|
|
6842
7598
|
const agentSessionId = result.session?.id;
|
|
6843
7599
|
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, deps.getScopeId?.());
|
|
7600
|
+
beginHookTimeline({
|
|
7601
|
+
cardId,
|
|
7602
|
+
agentSessionId,
|
|
7603
|
+
getClient: () => client3
|
|
7604
|
+
});
|
|
6844
7605
|
return {
|
|
6845
7606
|
success: true,
|
|
6846
7607
|
assignedTo,
|
|
@@ -6902,6 +7663,7 @@ ${options}
|
|
|
6902
7663
|
const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
6903
7664
|
await flushMemoryActions(client3, cardId);
|
|
6904
7665
|
cleanupMemorySession(cardId);
|
|
7666
|
+
await endHookTimeline(cardId);
|
|
6905
7667
|
let result = {
|
|
6906
7668
|
session: null
|
|
6907
7669
|
};
|
|
@@ -7511,7 +8273,7 @@ ${options}
|
|
|
7511
8273
|
} else if (args.cardId) {
|
|
7512
8274
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
7513
8275
|
result = await client3.getPlanByCardId(cardId);
|
|
7514
|
-
if (!result) {
|
|
8276
|
+
if (!result || result.plan == null && !result.foreign_criteria?.length) {
|
|
7515
8277
|
return {
|
|
7516
8278
|
success: true,
|
|
7517
8279
|
plan: null,
|
|
@@ -7519,13 +8281,23 @@ ${options}
|
|
|
7519
8281
|
message: "No plan linked to this card"
|
|
7520
8282
|
};
|
|
7521
8283
|
}
|
|
8284
|
+
if (result.plan == null && result.foreign_criteria?.length) {
|
|
8285
|
+
return {
|
|
8286
|
+
success: true,
|
|
8287
|
+
plan: null,
|
|
8288
|
+
tasks: [],
|
|
8289
|
+
divergentCriteria: result.foreign_criteria ?? [],
|
|
8290
|
+
message: "This card belongs to no plan, but plan criteria point at it. " + "Repair it with harmony_update_card (planId) or harmony_link_plan_task."
|
|
8291
|
+
};
|
|
8292
|
+
}
|
|
7522
8293
|
} else {
|
|
7523
8294
|
throw new Error("Either planId or cardId must be provided");
|
|
7524
8295
|
}
|
|
7525
8296
|
return {
|
|
7526
8297
|
success: true,
|
|
7527
8298
|
plan: result.plan,
|
|
7528
|
-
tasks: result.tasks
|
|
8299
|
+
tasks: result.tasks,
|
|
8300
|
+
...result.foreign_criteria?.length ? { divergentCriteria: result.foreign_criteria } : {}
|
|
7529
8301
|
};
|
|
7530
8302
|
}
|
|
7531
8303
|
case "harmony_update_plan": {
|
|
@@ -7553,7 +8325,10 @@ ${options}
|
|
|
7553
8325
|
const found = findPlanTask(tasks, taskId);
|
|
7554
8326
|
if (!found.ok)
|
|
7555
8327
|
throw new Error(found.reason);
|
|
7556
|
-
await client3.updatePlanTask(planId, taskId, {
|
|
8328
|
+
const result = await client3.updatePlanTask(planId, taskId, {
|
|
8329
|
+
cardId,
|
|
8330
|
+
status
|
|
8331
|
+
});
|
|
7557
8332
|
return {
|
|
7558
8333
|
success: true,
|
|
7559
8334
|
planTask: {
|
|
@@ -7561,6 +8336,7 @@ ${options}
|
|
|
7561
8336
|
taskId,
|
|
7562
8337
|
criterion: found.task.content ?? null,
|
|
7563
8338
|
...cardId ? linkedReport(planId, found.task, cardId) : { linked: found.task.card_id != null },
|
|
8339
|
+
...cardId && result.cardPlanAdopted ? { cardPlanAdopted: true } : {},
|
|
7564
8340
|
...status ? { status } : {}
|
|
7565
8341
|
}
|
|
7566
8342
|
};
|
|
@@ -7795,6 +8571,9 @@ class HarmonyMCPServer {
|
|
|
7795
8571
|
try {
|
|
7796
8572
|
await shutdownAllSessions();
|
|
7797
8573
|
} catch {}
|
|
8574
|
+
try {
|
|
8575
|
+
await stopAllRunEventForwarders();
|
|
8576
|
+
} catch {}
|
|
7798
8577
|
destroyAutoSession();
|
|
7799
8578
|
process.exit(exitCode);
|
|
7800
8579
|
};
|
|
@@ -7832,29 +8611,29 @@ class HarmonyMCPServer {
|
|
|
7832
8611
|
// src/tui/setup.ts
|
|
7833
8612
|
import { createHash as createHash5 } from "node:crypto";
|
|
7834
8613
|
import {
|
|
7835
|
-
existsSync as
|
|
8614
|
+
existsSync as existsSync9,
|
|
7836
8615
|
lstatSync,
|
|
7837
|
-
mkdirSync as
|
|
8616
|
+
mkdirSync as mkdirSync6,
|
|
7838
8617
|
symlinkSync,
|
|
7839
|
-
unlinkSync
|
|
8618
|
+
unlinkSync as unlinkSync2
|
|
7840
8619
|
} from "node:fs";
|
|
7841
|
-
import { homedir as
|
|
7842
|
-
import { dirname as dirname4, join as
|
|
8620
|
+
import { homedir as homedir7 } from "node:os";
|
|
8621
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
7843
8622
|
import * as p4 from "@clack/prompts";
|
|
7844
8623
|
init_config();
|
|
7845
8624
|
init_oauth_login();
|
|
7846
8625
|
|
|
7847
8626
|
// src/tui/agents.ts
|
|
7848
|
-
import { existsSync as
|
|
7849
|
-
import { homedir as
|
|
7850
|
-
import { join as
|
|
8627
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
8628
|
+
import { homedir as homedir5 } from "node:os";
|
|
8629
|
+
import { join as join7 } from "node:path";
|
|
7851
8630
|
var AGENT_DEFINITIONS = [
|
|
7852
8631
|
{
|
|
7853
8632
|
id: "claude",
|
|
7854
8633
|
name: "Claude Code",
|
|
7855
8634
|
description: "Anthropic CLI agent",
|
|
7856
8635
|
hint: "/hmy <card>",
|
|
7857
|
-
globalPaths: [
|
|
8636
|
+
globalPaths: [join7(homedir5(), ".claude")],
|
|
7858
8637
|
localPaths: [".claude"]
|
|
7859
8638
|
},
|
|
7860
8639
|
{
|
|
@@ -7862,7 +8641,7 @@ var AGENT_DEFINITIONS = [
|
|
|
7862
8641
|
name: "Codex",
|
|
7863
8642
|
description: "OpenAI coding agent",
|
|
7864
8643
|
hint: "/prompts:hmy <card>",
|
|
7865
|
-
globalPaths: [
|
|
8644
|
+
globalPaths: [join7(homedir5(), ".codex")],
|
|
7866
8645
|
localPaths: ["AGENTS.md"]
|
|
7867
8646
|
},
|
|
7868
8647
|
{
|
|
@@ -7878,20 +8657,20 @@ var AGENT_DEFINITIONS = [
|
|
|
7878
8657
|
name: "Windsurf",
|
|
7879
8658
|
description: "Codeium AI IDE",
|
|
7880
8659
|
hint: "MCP tools available automatically",
|
|
7881
|
-
globalPaths: [
|
|
8660
|
+
globalPaths: [join7(homedir5(), ".codeium", "windsurf")],
|
|
7882
8661
|
localPaths: [".windsurf", ".windsurfrules"]
|
|
7883
8662
|
}
|
|
7884
8663
|
];
|
|
7885
8664
|
function detectAgents(cwd = process.cwd()) {
|
|
7886
8665
|
return AGENT_DEFINITIONS.map((def) => {
|
|
7887
|
-
const globalPath = def.globalPaths.find((p) =>
|
|
7888
|
-
const localPath = def.localPaths.find((p) =>
|
|
8666
|
+
const globalPath = def.globalPaths.find((p) => existsSync6(p)) || null;
|
|
8667
|
+
const localPath = def.localPaths.find((p) => existsSync6(join7(cwd, p))) || null;
|
|
7889
8668
|
return {
|
|
7890
8669
|
id: def.id,
|
|
7891
8670
|
name: def.name,
|
|
7892
8671
|
detected: !!(globalPath || localPath),
|
|
7893
8672
|
globalPath,
|
|
7894
|
-
localPath: localPath ?
|
|
8673
|
+
localPath: localPath ? join7(cwd, localPath) : null,
|
|
7895
8674
|
description: def.description,
|
|
7896
8675
|
hint: def.hint
|
|
7897
8676
|
};
|
|
@@ -7910,8 +8689,8 @@ async function confirmOrDefault(assumeYes, opts) {
|
|
|
7910
8689
|
}
|
|
7911
8690
|
|
|
7912
8691
|
// src/tui/docs.ts
|
|
7913
|
-
import { existsSync as
|
|
7914
|
-
import { isAbsolute, join as
|
|
8692
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
|
|
8693
|
+
import { isAbsolute, join as join8, resolve as resolve2, sep as sep2 } from "node:path";
|
|
7915
8694
|
import * as p2 from "@clack/prompts";
|
|
7916
8695
|
|
|
7917
8696
|
// src/tui/theme.ts
|
|
@@ -7998,25 +8777,25 @@ var IGNORED_DIRS = new Set([
|
|
|
7998
8777
|
]);
|
|
7999
8778
|
function readJson(filePath) {
|
|
8000
8779
|
try {
|
|
8001
|
-
return JSON.parse(
|
|
8780
|
+
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
8002
8781
|
} catch {
|
|
8003
8782
|
return null;
|
|
8004
8783
|
}
|
|
8005
8784
|
}
|
|
8006
8785
|
function readText(filePath) {
|
|
8007
8786
|
try {
|
|
8008
|
-
return
|
|
8787
|
+
return readFileSync6(filePath, "utf-8");
|
|
8009
8788
|
} catch {
|
|
8010
8789
|
return null;
|
|
8011
8790
|
}
|
|
8012
8791
|
}
|
|
8013
8792
|
function listDirs(dirPath) {
|
|
8014
8793
|
try {
|
|
8015
|
-
return
|
|
8794
|
+
return readdirSync3(dirPath).filter((entry) => {
|
|
8016
8795
|
if (IGNORED_DIRS.has(entry) || entry.startsWith("."))
|
|
8017
8796
|
return false;
|
|
8018
8797
|
try {
|
|
8019
|
-
return
|
|
8798
|
+
return statSync3(join8(dirPath, entry)).isDirectory();
|
|
8020
8799
|
} catch {
|
|
8021
8800
|
return false;
|
|
8022
8801
|
}
|
|
@@ -8064,25 +8843,25 @@ function describeDir(name) {
|
|
|
8064
8843
|
}
|
|
8065
8844
|
function scanProject(cwd) {
|
|
8066
8845
|
let packageManager = null;
|
|
8067
|
-
if (
|
|
8846
|
+
if (existsSync7(join8(cwd, "bun.lock")) || existsSync7(join8(cwd, "bun.lockb"))) {
|
|
8068
8847
|
packageManager = "bun";
|
|
8069
|
-
} else if (
|
|
8848
|
+
} else if (existsSync7(join8(cwd, "pnpm-lock.yaml"))) {
|
|
8070
8849
|
packageManager = "pnpm";
|
|
8071
|
-
} else if (
|
|
8850
|
+
} else if (existsSync7(join8(cwd, "yarn.lock"))) {
|
|
8072
8851
|
packageManager = "yarn";
|
|
8073
|
-
} else if (
|
|
8852
|
+
} else if (existsSync7(join8(cwd, "package.json"))) {
|
|
8074
8853
|
packageManager = "npm";
|
|
8075
8854
|
}
|
|
8076
|
-
const pkg = readJson(
|
|
8855
|
+
const pkg = readJson(join8(cwd, "package.json"));
|
|
8077
8856
|
const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
8078
8857
|
let language = "unknown";
|
|
8079
|
-
if (
|
|
8858
|
+
if (existsSync7(join8(cwd, "tsconfig.json"))) {
|
|
8080
8859
|
language = "typescript";
|
|
8081
|
-
} else if (
|
|
8860
|
+
} else if (existsSync7(join8(cwd, "go.mod"))) {
|
|
8082
8861
|
language = "go";
|
|
8083
|
-
} else if (
|
|
8862
|
+
} else if (existsSync7(join8(cwd, "Cargo.toml"))) {
|
|
8084
8863
|
language = "rust";
|
|
8085
|
-
} else if (
|
|
8864
|
+
} else if (existsSync7(join8(cwd, "setup.py")) || existsSync7(join8(cwd, "pyproject.toml"))) {
|
|
8086
8865
|
language = "python";
|
|
8087
8866
|
} else if (pkg) {
|
|
8088
8867
|
language = "javascript";
|
|
@@ -8118,7 +8897,7 @@ function scanProject(cwd) {
|
|
|
8118
8897
|
}
|
|
8119
8898
|
}
|
|
8120
8899
|
let linter = null;
|
|
8121
|
-
if (
|
|
8900
|
+
if (existsSync7(join8(cwd, "biome.json")) || existsSync7(join8(cwd, "biome.jsonc"))) {
|
|
8122
8901
|
linter = "biome";
|
|
8123
8902
|
} else {
|
|
8124
8903
|
const eslintFiles = [
|
|
@@ -8133,7 +8912,7 @@ function scanProject(cwd) {
|
|
|
8133
8912
|
"eslint.config.cjs",
|
|
8134
8913
|
"eslint.config.ts"
|
|
8135
8914
|
];
|
|
8136
|
-
if (eslintFiles.some((f) =>
|
|
8915
|
+
if (eslintFiles.some((f) => existsSync7(join8(cwd, f)))) {
|
|
8137
8916
|
linter = "eslint";
|
|
8138
8917
|
} else {
|
|
8139
8918
|
const prettierFiles = [
|
|
@@ -8145,13 +8924,13 @@ function scanProject(cwd) {
|
|
|
8145
8924
|
"prettier.config.js",
|
|
8146
8925
|
"prettier.config.mjs"
|
|
8147
8926
|
];
|
|
8148
|
-
if (prettierFiles.some((f) =>
|
|
8927
|
+
if (prettierFiles.some((f) => existsSync7(join8(cwd, f)))) {
|
|
8149
8928
|
linter = "prettier";
|
|
8150
8929
|
}
|
|
8151
8930
|
}
|
|
8152
8931
|
}
|
|
8153
8932
|
let indentStyle = null;
|
|
8154
|
-
const biome = readJson(
|
|
8933
|
+
const biome = readJson(join8(cwd, "biome.json")) ?? readJson(join8(cwd, "biome.jsonc"));
|
|
8155
8934
|
if (biome) {
|
|
8156
8935
|
const formatter = biome.formatter;
|
|
8157
8936
|
if (formatter) {
|
|
@@ -8161,7 +8940,7 @@ function scanProject(cwd) {
|
|
|
8161
8940
|
}
|
|
8162
8941
|
}
|
|
8163
8942
|
if (!indentStyle) {
|
|
8164
|
-
const editorConfig = readText(
|
|
8943
|
+
const editorConfig = readText(join8(cwd, ".editorconfig"));
|
|
8165
8944
|
if (editorConfig) {
|
|
8166
8945
|
const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
|
|
8167
8946
|
const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
|
|
@@ -8174,13 +8953,13 @@ function scanProject(cwd) {
|
|
|
8174
8953
|
}
|
|
8175
8954
|
}
|
|
8176
8955
|
const dirs = listDirs(cwd);
|
|
8177
|
-
const srcDirs =
|
|
8178
|
-
const monorepo =
|
|
8956
|
+
const srcDirs = existsSync7(join8(cwd, "src")) ? listDirs(join8(cwd, "src")) : [];
|
|
8957
|
+
const monorepo = existsSync7(join8(cwd, "packages")) || existsSync7(join8(cwd, "apps"));
|
|
8179
8958
|
const existingDocs = {
|
|
8180
|
-
agentsMd:
|
|
8181
|
-
claudeMd:
|
|
8182
|
-
docsDir:
|
|
8183
|
-
architectureMd:
|
|
8959
|
+
agentsMd: existsSync7(join8(cwd, "AGENTS.md")),
|
|
8960
|
+
claudeMd: existsSync7(join8(cwd, "CLAUDE.md")),
|
|
8961
|
+
docsDir: existsSync7(join8(cwd, "docs")),
|
|
8962
|
+
architectureMd: existsSync7(join8(cwd, "docs", "architecture.md"))
|
|
8184
8963
|
};
|
|
8185
8964
|
return {
|
|
8186
8965
|
packageManager,
|
|
@@ -8331,9 +9110,9 @@ var VAGUE_STANDARDS = [
|
|
|
8331
9110
|
];
|
|
8332
9111
|
function verifyDocs(cwd) {
|
|
8333
9112
|
const issues = [];
|
|
8334
|
-
const claudeMd = readText(
|
|
8335
|
-
const agentsMd = readText(
|
|
8336
|
-
const pkg = readJson(
|
|
9113
|
+
const claudeMd = readText(join8(cwd, "CLAUDE.md"));
|
|
9114
|
+
const agentsMd = readText(join8(cwd, "AGENTS.md"));
|
|
9115
|
+
const pkg = readJson(join8(cwd, "package.json"));
|
|
8337
9116
|
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
8338
9117
|
const projectRoot = resolve2(cwd);
|
|
8339
9118
|
if (claudeMd) {
|
|
@@ -8363,7 +9142,7 @@ function verifyDocs(cwd) {
|
|
|
8363
9142
|
continue;
|
|
8364
9143
|
}
|
|
8365
9144
|
importedFiles.push({ ref: refPath, resolved: resolvedPath });
|
|
8366
|
-
if (!
|
|
9145
|
+
if (!existsSync7(resolvedPath)) {
|
|
8367
9146
|
issues.push({
|
|
8368
9147
|
severity: "error",
|
|
8369
9148
|
file: "CLAUDE.md",
|
|
@@ -8499,7 +9278,7 @@ function verifyDocs(cwd) {
|
|
|
8499
9278
|
}
|
|
8500
9279
|
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
8501
9280
|
}
|
|
8502
|
-
const archMd = readText(
|
|
9281
|
+
const archMd = readText(join8(cwd, "docs", "architecture.md"));
|
|
8503
9282
|
if (archMd) {
|
|
8504
9283
|
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
8505
9284
|
}
|
|
@@ -8546,7 +9325,7 @@ function checkBacktickPaths(content, file, cwd, issues) {
|
|
|
8546
9325
|
const resolvedRef = resolve2(root, refPath);
|
|
8547
9326
|
if (resolvedRef !== root && !resolvedRef.startsWith(root + sep2))
|
|
8548
9327
|
continue;
|
|
8549
|
-
if (!
|
|
9328
|
+
if (!existsSync7(resolvedRef)) {
|
|
8550
9329
|
issues.push({
|
|
8551
9330
|
severity: "warning",
|
|
8552
9331
|
file,
|
|
@@ -8569,18 +9348,18 @@ async function runDocsStep(cwd) {
|
|
|
8569
9348
|
}
|
|
8570
9349
|
const files = [];
|
|
8571
9350
|
files.push({
|
|
8572
|
-
path:
|
|
9351
|
+
path: join8(cwd, "AGENTS.md"),
|
|
8573
9352
|
content: generateAgentsMd(info, cwd),
|
|
8574
9353
|
type: "text"
|
|
8575
9354
|
});
|
|
8576
9355
|
files.push({
|
|
8577
|
-
path:
|
|
9356
|
+
path: join8(cwd, "CLAUDE.md"),
|
|
8578
9357
|
content: generateClaudeMd(info),
|
|
8579
9358
|
type: "text"
|
|
8580
9359
|
});
|
|
8581
9360
|
if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
|
|
8582
9361
|
files.push({
|
|
8583
|
-
path:
|
|
9362
|
+
path: join8(cwd, "docs", "architecture.md"),
|
|
8584
9363
|
content: generateArchitectureMd(info, cwd),
|
|
8585
9364
|
type: "text"
|
|
8586
9365
|
});
|
|
@@ -8618,21 +9397,21 @@ async function runDocsStep(cwd) {
|
|
|
8618
9397
|
// src/tui/writer.ts
|
|
8619
9398
|
import {
|
|
8620
9399
|
chmodSync,
|
|
8621
|
-
existsSync as
|
|
8622
|
-
mkdirSync as
|
|
8623
|
-
readFileSync as
|
|
8624
|
-
writeFileSync as
|
|
9400
|
+
existsSync as existsSync8,
|
|
9401
|
+
mkdirSync as mkdirSync5,
|
|
9402
|
+
readFileSync as readFileSync7,
|
|
9403
|
+
writeFileSync as writeFileSync5
|
|
8625
9404
|
} from "node:fs";
|
|
8626
|
-
import { homedir as
|
|
9405
|
+
import { homedir as homedir6 } from "node:os";
|
|
8627
9406
|
import { dirname as dirname3 } from "node:path";
|
|
8628
9407
|
import * as p3 from "@clack/prompts";
|
|
8629
9408
|
function ensureDir(dirPath) {
|
|
8630
|
-
if (!
|
|
8631
|
-
|
|
9409
|
+
if (!existsSync8(dirPath)) {
|
|
9410
|
+
mkdirSync5(dirPath, { recursive: true, mode: 493 });
|
|
8632
9411
|
}
|
|
8633
9412
|
}
|
|
8634
9413
|
function writeFile(filePath, content, options = {}) {
|
|
8635
|
-
const exists =
|
|
9414
|
+
const exists = existsSync8(filePath);
|
|
8636
9415
|
if (exists && !options.force) {
|
|
8637
9416
|
return { path: filePath, action: "skip" };
|
|
8638
9417
|
}
|
|
@@ -8640,7 +9419,7 @@ function writeFile(filePath, content, options = {}) {
|
|
|
8640
9419
|
ensureDir(dirname3(filePath));
|
|
8641
9420
|
const defaultMode = filePath.includes(".harmony-mcp") ? 384 : 420;
|
|
8642
9421
|
const mode = options.mode ?? defaultMode;
|
|
8643
|
-
|
|
9422
|
+
writeFileSync5(filePath, content, { mode });
|
|
8644
9423
|
if (options.mode !== undefined) {
|
|
8645
9424
|
chmodSync(filePath, options.mode);
|
|
8646
9425
|
}
|
|
@@ -8654,11 +9433,11 @@ function writeFile(filePath, content, options = {}) {
|
|
|
8654
9433
|
}
|
|
8655
9434
|
}
|
|
8656
9435
|
function mergeJsonFile(filePath, updates, options = {}) {
|
|
8657
|
-
const exists =
|
|
9436
|
+
const exists = existsSync8(filePath);
|
|
8658
9437
|
if (!exists) {
|
|
8659
9438
|
try {
|
|
8660
9439
|
ensureDir(dirname3(filePath));
|
|
8661
|
-
|
|
9440
|
+
writeFileSync5(filePath, JSON.stringify(updates, null, 2), {
|
|
8662
9441
|
mode: 420
|
|
8663
9442
|
});
|
|
8664
9443
|
return { path: filePath, action: "create" };
|
|
@@ -8671,7 +9450,7 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
8671
9450
|
}
|
|
8672
9451
|
}
|
|
8673
9452
|
try {
|
|
8674
|
-
const existing = JSON.parse(
|
|
9453
|
+
const existing = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
8675
9454
|
if (updates.mcpServers && existing.mcpServers) {
|
|
8676
9455
|
const existingServers = existing.mcpServers;
|
|
8677
9456
|
const updateServers = updates.mcpServers;
|
|
@@ -8679,12 +9458,12 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
8679
9458
|
} else {
|
|
8680
9459
|
Object.assign(existing, updates);
|
|
8681
9460
|
}
|
|
8682
|
-
|
|
9461
|
+
writeFileSync5(filePath, JSON.stringify(existing, null, 2), { mode: 420 });
|
|
8683
9462
|
return { path: filePath, action: "merge" };
|
|
8684
9463
|
} catch {
|
|
8685
9464
|
if (options.force) {
|
|
8686
9465
|
try {
|
|
8687
|
-
|
|
9466
|
+
writeFileSync5(filePath, JSON.stringify(updates, null, 2), {
|
|
8688
9467
|
mode: 420
|
|
8689
9468
|
});
|
|
8690
9469
|
return { path: filePath, action: "update" };
|
|
@@ -8704,11 +9483,11 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
8704
9483
|
}
|
|
8705
9484
|
}
|
|
8706
9485
|
function appendToToml(filePath, section, content, options = {}) {
|
|
8707
|
-
const exists =
|
|
9486
|
+
const exists = existsSync8(filePath);
|
|
8708
9487
|
if (!exists) {
|
|
8709
9488
|
try {
|
|
8710
9489
|
ensureDir(dirname3(filePath));
|
|
8711
|
-
|
|
9490
|
+
writeFileSync5(filePath, content, { mode: 420 });
|
|
8712
9491
|
return { path: filePath, action: "create" };
|
|
8713
9492
|
} catch (error) {
|
|
8714
9493
|
return {
|
|
@@ -8719,19 +9498,19 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
8719
9498
|
}
|
|
8720
9499
|
}
|
|
8721
9500
|
try {
|
|
8722
|
-
const existing =
|
|
9501
|
+
const existing = readFileSync7(filePath, "utf-8");
|
|
8723
9502
|
if (existing.includes(`[${section}]`)) {
|
|
8724
9503
|
if (options.force) {
|
|
8725
9504
|
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8726
9505
|
const updated = existing.replace(new RegExp(`(?:#[^\\n]*\\n)*\\[${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`), content.trim() + `
|
|
8727
9506
|
|
|
8728
9507
|
`);
|
|
8729
|
-
|
|
9508
|
+
writeFileSync5(filePath, updated, { mode: 420 });
|
|
8730
9509
|
return { path: filePath, action: "update" };
|
|
8731
9510
|
}
|
|
8732
9511
|
return { path: filePath, action: "skip" };
|
|
8733
9512
|
}
|
|
8734
|
-
|
|
9513
|
+
writeFileSync5(filePath, existing + `
|
|
8735
9514
|
` + content, { mode: 420 });
|
|
8736
9515
|
return { path: filePath, action: "merge" };
|
|
8737
9516
|
} catch (error) {
|
|
@@ -8744,7 +9523,7 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
8744
9523
|
}
|
|
8745
9524
|
async function writeFilesWithProgress(files, options = {}) {
|
|
8746
9525
|
const results = [];
|
|
8747
|
-
const home =
|
|
9526
|
+
const home = homedir6();
|
|
8748
9527
|
const spinner2 = p3.spinner();
|
|
8749
9528
|
spinner2.start("Writing configuration files...");
|
|
8750
9529
|
for (const file of files) {
|
|
@@ -8781,10 +9560,10 @@ function getWriteSummary(files, options = {}) {
|
|
|
8781
9560
|
const toCreate = [];
|
|
8782
9561
|
const toUpdate = [];
|
|
8783
9562
|
const toSkip = [];
|
|
8784
|
-
const home =
|
|
9563
|
+
const home = homedir6();
|
|
8785
9564
|
for (const file of files) {
|
|
8786
9565
|
const displayPath = formatPath(file.path, home);
|
|
8787
|
-
const exists =
|
|
9566
|
+
const exists = existsSync8(file.path);
|
|
8788
9567
|
if (exists && !options.force) {
|
|
8789
9568
|
toSkip.push(displayPath);
|
|
8790
9569
|
} else if (exists) {
|
|
@@ -8846,7 +9625,7 @@ var SAFE_HARMONY_TOOLS = [
|
|
|
8846
9625
|
"harmony_process_command",
|
|
8847
9626
|
"harmony_sync"
|
|
8848
9627
|
];
|
|
8849
|
-
var GLOBAL_SKILLS_DIR =
|
|
9628
|
+
var GLOBAL_SKILLS_DIR = join9(homedir7(), ".agents", "skills");
|
|
8850
9629
|
var API_URL = "https://app.gethmy.com/api";
|
|
8851
9630
|
async function registerMcpServer() {
|
|
8852
9631
|
try {
|
|
@@ -8860,15 +9639,15 @@ async function registerMcpServer() {
|
|
|
8860
9639
|
}
|
|
8861
9640
|
}
|
|
8862
9641
|
async function writeMcpConfigFallback(home) {
|
|
8863
|
-
const { readFileSync:
|
|
8864
|
-
const settingsPath =
|
|
9642
|
+
const { readFileSync: readFileSync8, writeFileSync: writeFileSync6, mkdirSync: mkdirSync7, existsSync: existsSync10 } = await import("node:fs");
|
|
9643
|
+
const settingsPath = join9(home, ".claude", "settings.json");
|
|
8865
9644
|
const settingsDir = dirname4(settingsPath);
|
|
8866
|
-
if (!
|
|
8867
|
-
|
|
9645
|
+
if (!existsSync10(settingsDir)) {
|
|
9646
|
+
mkdirSync7(settingsDir, { recursive: true });
|
|
8868
9647
|
}
|
|
8869
9648
|
let settings = {};
|
|
8870
|
-
if (
|
|
8871
|
-
settings = JSON.parse(
|
|
9649
|
+
if (existsSync10(settingsPath)) {
|
|
9650
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
8872
9651
|
}
|
|
8873
9652
|
const mcpServers = settings.mcpServers || {};
|
|
8874
9653
|
mcpServers.harmony = {
|
|
@@ -8876,18 +9655,18 @@ async function writeMcpConfigFallback(home) {
|
|
|
8876
9655
|
args: ["-y", "@gethmy/mcp@latest", "serve"]
|
|
8877
9656
|
};
|
|
8878
9657
|
settings.mcpServers = mcpServers;
|
|
8879
|
-
|
|
9658
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2));
|
|
8880
9659
|
}
|
|
8881
9660
|
async function allowlistHarmonyTools(home, allowAll) {
|
|
8882
|
-
const { readFileSync:
|
|
8883
|
-
const settingsPath =
|
|
9661
|
+
const { readFileSync: readFileSync8, writeFileSync: writeFileSync6, mkdirSync: mkdirSync7, existsSync: existsSync10 } = await import("node:fs");
|
|
9662
|
+
const settingsPath = join9(home, ".claude", "settings.json");
|
|
8884
9663
|
const settingsDir = dirname4(settingsPath);
|
|
8885
|
-
if (!
|
|
8886
|
-
|
|
9664
|
+
if (!existsSync10(settingsDir)) {
|
|
9665
|
+
mkdirSync7(settingsDir, { recursive: true });
|
|
8887
9666
|
}
|
|
8888
9667
|
let settings = {};
|
|
8889
|
-
if (
|
|
8890
|
-
settings = JSON.parse(
|
|
9668
|
+
if (existsSync10(settingsPath)) {
|
|
9669
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
8891
9670
|
}
|
|
8892
9671
|
const permissions = settings.permissions || {};
|
|
8893
9672
|
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
@@ -8900,7 +9679,7 @@ async function allowlistHarmonyTools(home, allowAll) {
|
|
|
8900
9679
|
allow.push(...missing);
|
|
8901
9680
|
permissions.allow = allow;
|
|
8902
9681
|
settings.permissions = permissions;
|
|
8903
|
-
|
|
9682
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2));
|
|
8904
9683
|
return "added";
|
|
8905
9684
|
}
|
|
8906
9685
|
async function validateApiKey(apiKey, apiUrl = API_URL) {
|
|
@@ -8994,7 +9773,7 @@ async function resolveProjectSlug(apiKey, slug) {
|
|
|
8994
9773
|
};
|
|
8995
9774
|
}
|
|
8996
9775
|
async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
8997
|
-
const home =
|
|
9776
|
+
const home = homedir7();
|
|
8998
9777
|
const files = [];
|
|
8999
9778
|
const symlinks = [];
|
|
9000
9779
|
switch (agentId) {
|
|
@@ -9009,17 +9788,17 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
|
9009
9788
|
const content = buildSkillFile(fetched);
|
|
9010
9789
|
if (installMode === "global") {
|
|
9011
9790
|
files.push({
|
|
9012
|
-
path:
|
|
9791
|
+
path: join9(GLOBAL_SKILLS_DIR, name, "SKILL.md"),
|
|
9013
9792
|
content,
|
|
9014
9793
|
type: "text"
|
|
9015
9794
|
});
|
|
9016
9795
|
symlinks.push({
|
|
9017
|
-
target:
|
|
9018
|
-
link:
|
|
9796
|
+
target: join9(GLOBAL_SKILLS_DIR, name),
|
|
9797
|
+
link: join9(home, ".claude", "skills", name)
|
|
9019
9798
|
});
|
|
9020
9799
|
} else {
|
|
9021
9800
|
files.push({
|
|
9022
|
-
path:
|
|
9801
|
+
path: join9(cwd, ".claude", "skills", name, "SKILL.md"),
|
|
9023
9802
|
content,
|
|
9024
9803
|
type: "text"
|
|
9025
9804
|
});
|
|
@@ -9042,13 +9821,13 @@ ${summary}`);
|
|
|
9042
9821
|
throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
|
|
9043
9822
|
}
|
|
9044
9823
|
files.push({
|
|
9045
|
-
path:
|
|
9824
|
+
path: join9(home, ".hmy", "bin", "hmy-update-check"),
|
|
9046
9825
|
content: updateCheckFetched.content,
|
|
9047
9826
|
type: "text",
|
|
9048
9827
|
mode: 493
|
|
9049
9828
|
});
|
|
9050
9829
|
files.push({
|
|
9051
|
-
path:
|
|
9830
|
+
path: join9(home, ".hmy", "VERSION"),
|
|
9052
9831
|
content: versionInfo.version,
|
|
9053
9832
|
type: "text"
|
|
9054
9833
|
});
|
|
@@ -9110,7 +9889,7 @@ Skip if: work was already started with a card reference, or no matching card exi
|
|
|
9110
9889
|
- \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
|
|
9111
9890
|
`;
|
|
9112
9891
|
files.push({
|
|
9113
|
-
path:
|
|
9892
|
+
path: join9(cwd, "AGENTS.md"),
|
|
9114
9893
|
content: agentsContent,
|
|
9115
9894
|
type: "text"
|
|
9116
9895
|
});
|
|
@@ -9127,17 +9906,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent
|
|
|
9127
9906
|
`;
|
|
9128
9907
|
if (installMode === "global") {
|
|
9129
9908
|
files.push({
|
|
9130
|
-
path:
|
|
9909
|
+
path: join9(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
9131
9910
|
content: promptContent,
|
|
9132
9911
|
type: "text"
|
|
9133
9912
|
});
|
|
9134
9913
|
symlinks.push({
|
|
9135
|
-
target:
|
|
9136
|
-
link:
|
|
9914
|
+
target: join9(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
9915
|
+
link: join9(home, ".codex", "prompts", "hmy.md")
|
|
9137
9916
|
});
|
|
9138
9917
|
} else {
|
|
9139
9918
|
files.push({
|
|
9140
|
-
path:
|
|
9919
|
+
path: join9(home, ".codex", "prompts", "hmy.md"),
|
|
9141
9920
|
content: promptContent,
|
|
9142
9921
|
type: "text"
|
|
9143
9922
|
});
|
|
@@ -9149,7 +9928,7 @@ command = "npx"
|
|
|
9149
9928
|
args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
9150
9929
|
`;
|
|
9151
9930
|
files.push({
|
|
9152
|
-
path:
|
|
9931
|
+
path: join9(home, ".codex", "config.toml"),
|
|
9153
9932
|
content: tomlContent,
|
|
9154
9933
|
type: "toml",
|
|
9155
9934
|
tomlSection: "mcp_servers.harmony"
|
|
@@ -9158,7 +9937,7 @@ args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
|
9158
9937
|
}
|
|
9159
9938
|
case "cursor": {
|
|
9160
9939
|
files.push({
|
|
9161
|
-
path:
|
|
9940
|
+
path: join9(cwd, ".cursor", "mcp.json"),
|
|
9162
9941
|
content: JSON.stringify({
|
|
9163
9942
|
mcpServers: {
|
|
9164
9943
|
harmony: {
|
|
@@ -9184,17 +9963,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
9184
9963
|
`;
|
|
9185
9964
|
if (installMode === "global") {
|
|
9186
9965
|
files.push({
|
|
9187
|
-
path:
|
|
9966
|
+
path: join9(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
9188
9967
|
content: ruleContent,
|
|
9189
9968
|
type: "text"
|
|
9190
9969
|
});
|
|
9191
9970
|
symlinks.push({
|
|
9192
|
-
target:
|
|
9193
|
-
link:
|
|
9971
|
+
target: join9(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
9972
|
+
link: join9(home, ".cursor", "rules", "harmony.mdc")
|
|
9194
9973
|
});
|
|
9195
9974
|
} else {
|
|
9196
9975
|
files.push({
|
|
9197
|
-
path:
|
|
9976
|
+
path: join9(cwd, ".cursor", "rules", "harmony.mdc"),
|
|
9198
9977
|
content: ruleContent,
|
|
9199
9978
|
type: "text"
|
|
9200
9979
|
});
|
|
@@ -9203,7 +9982,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
9203
9982
|
}
|
|
9204
9983
|
case "windsurf": {
|
|
9205
9984
|
files.push({
|
|
9206
|
-
path:
|
|
9985
|
+
path: join9(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
9207
9986
|
content: JSON.stringify({
|
|
9208
9987
|
mcpServers: {
|
|
9209
9988
|
harmony: {
|
|
@@ -9229,17 +10008,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
9229
10008
|
`;
|
|
9230
10009
|
if (installMode === "global") {
|
|
9231
10010
|
files.push({
|
|
9232
|
-
path:
|
|
10011
|
+
path: join9(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
9233
10012
|
content: ruleContent,
|
|
9234
10013
|
type: "text"
|
|
9235
10014
|
});
|
|
9236
10015
|
symlinks.push({
|
|
9237
|
-
target:
|
|
9238
|
-
link:
|
|
10016
|
+
target: join9(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
10017
|
+
link: join9(home, ".codeium", "windsurf", "rules", "harmony.md")
|
|
9239
10018
|
});
|
|
9240
10019
|
} else {
|
|
9241
10020
|
files.push({
|
|
9242
|
-
path:
|
|
10021
|
+
path: join9(cwd, ".windsurf", "rules", "harmony.md"),
|
|
9243
10022
|
content: ruleContent,
|
|
9244
10023
|
type: "text"
|
|
9245
10024
|
});
|
|
@@ -9251,7 +10030,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
9251
10030
|
}
|
|
9252
10031
|
async function runSetup(options = {}) {
|
|
9253
10032
|
const cwd = process.cwd();
|
|
9254
|
-
const home =
|
|
10033
|
+
const home = homedir7();
|
|
9255
10034
|
console.clear();
|
|
9256
10035
|
console.log(messages.header());
|
|
9257
10036
|
const assumeYes = shouldAssumeYes(options.yes, process.stdin.isTTY);
|
|
@@ -9749,8 +10528,8 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9749
10528
|
for (const symlink of allSymlinks) {
|
|
9750
10529
|
try {
|
|
9751
10530
|
const linkDir = dirname4(symlink.link);
|
|
9752
|
-
if (!
|
|
9753
|
-
|
|
10531
|
+
if (!existsSync9(linkDir)) {
|
|
10532
|
+
mkdirSync6(linkDir, { recursive: true });
|
|
9754
10533
|
}
|
|
9755
10534
|
let linkExists = false;
|
|
9756
10535
|
try {
|
|
@@ -9759,7 +10538,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9759
10538
|
} catch {}
|
|
9760
10539
|
if (linkExists) {
|
|
9761
10540
|
if (options.force) {
|
|
9762
|
-
|
|
10541
|
+
unlinkSync2(symlink.link);
|
|
9763
10542
|
} else {
|
|
9764
10543
|
continue;
|
|
9765
10544
|
}
|
|
@@ -9778,7 +10557,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9778
10557
|
} else {
|
|
9779
10558
|
try {
|
|
9780
10559
|
await writeMcpConfigFallback(home);
|
|
9781
|
-
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(
|
|
10560
|
+
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join9(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
|
|
9782
10561
|
} catch {
|
|
9783
10562
|
p4.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
|
|
9784
10563
|
}
|
|
@@ -9799,7 +10578,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9799
10578
|
try {
|
|
9800
10579
|
const result = await allowlistHarmonyTools(home, allowAll);
|
|
9801
10580
|
const scope = allowAll ? "all tools" : "safe tools";
|
|
9802
|
-
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(
|
|
10581
|
+
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join9(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
|
|
9803
10582
|
} catch {
|
|
9804
10583
|
p4.log.warning("Could not allowlist Harmony tools. Run /permissions in Claude Code and choose “always allow” for Harmony, or add mcp__harmony to permissions.allow in ~/.claude/settings.json.");
|
|
9805
10584
|
}
|
|
@@ -9980,4 +10759,59 @@ program.command("setup").description("Setup wizard for Harmony MCP (recommended)
|
|
|
9980
10759
|
yes: options.yes
|
|
9981
10760
|
});
|
|
9982
10761
|
});
|
|
10762
|
+
var hook = program.command("hook").description("Manage the PostToolUse hook that streams tool calls to a card's run timeline");
|
|
10763
|
+
hook.command("install").description("Install the hook into ~/.claude/settings.json (the user layer, never a project)").action(async () => {
|
|
10764
|
+
const { installUserHook: installUserHook2 } = await Promise.resolve().then(() => (init_hook_install(), exports_hook_install));
|
|
10765
|
+
const result = installUserHook2();
|
|
10766
|
+
if (!result.ok) {
|
|
10767
|
+
console.error(`Could not install the hook: ${result.error}`);
|
|
10768
|
+
process.exit(1);
|
|
10769
|
+
}
|
|
10770
|
+
console.log(result.changed ? `Installed the Harmony PostToolUse hook in ${result.path}` : `The Harmony PostToolUse hook is already installed in ${result.path}`);
|
|
10771
|
+
console.log(` command: ${result.command}`);
|
|
10772
|
+
console.log(`
|
|
10773
|
+
Tool calls from an MCP session will now appear on the card's run timeline.`);
|
|
10774
|
+
console.log("The user settings layer is deliberate: a daemon run never loads it, so it");
|
|
10775
|
+
console.log("cannot double-report the stream it already sends itself.");
|
|
10776
|
+
});
|
|
10777
|
+
hook.command("uninstall").description("Remove the hook from ~/.claude/settings.json").action(async () => {
|
|
10778
|
+
const { uninstallUserHook: uninstallUserHook2 } = await Promise.resolve().then(() => (init_hook_install(), exports_hook_install));
|
|
10779
|
+
const result = uninstallUserHook2();
|
|
10780
|
+
if (!result.ok) {
|
|
10781
|
+
console.error(`Could not remove the hook: ${result.error}`);
|
|
10782
|
+
process.exit(1);
|
|
10783
|
+
}
|
|
10784
|
+
console.log(result.changed ? `Removed the Harmony PostToolUse hook from ${result.path}` : "The Harmony PostToolUse hook was not installed.");
|
|
10785
|
+
});
|
|
10786
|
+
hook.command("status").description("Report whether the hook is installed, and any live sessions").action(async () => {
|
|
10787
|
+
const { hookInstallStatus: hookInstallStatus2, userSettingsPath: userSettingsPath2 } = await Promise.resolve().then(() => (init_hook_install(), exports_hook_install));
|
|
10788
|
+
const { readPublishedSessions: readPublishedSessions2, runStateDir: runStateDir2 } = await Promise.resolve().then(() => (init_run_state(), exports_run_state));
|
|
10789
|
+
const { readFileSync: readFileSync9 } = await import("node:fs");
|
|
10790
|
+
const path = userSettingsPath2();
|
|
10791
|
+
let status = {
|
|
10792
|
+
installed: false,
|
|
10793
|
+
binary: null,
|
|
10794
|
+
binaryExists: false
|
|
10795
|
+
};
|
|
10796
|
+
try {
|
|
10797
|
+
status = hookInstallStatus2(JSON.parse(readFileSync9(path, "utf-8")));
|
|
10798
|
+
} catch {}
|
|
10799
|
+
console.log(`Hook: ${status.installed ? "installed" : "not installed"} (${path})`);
|
|
10800
|
+
if (status.installed && !status.binaryExists) {
|
|
10801
|
+
console.log(` ! its hook binary is gone: ${status.binary ?? "unparseable command"}`);
|
|
10802
|
+
console.log(" The hook is INERT until you re-run `npx @gethmy/mcp hook install`.");
|
|
10803
|
+
console.log(" An upgrade or a cleared npx/bunx cache moves the binary; the installed");
|
|
10804
|
+
console.log(" command guards its own paths, so nothing errors in the meantime.");
|
|
10805
|
+
}
|
|
10806
|
+
console.log(`State: ${runStateDir2()}`);
|
|
10807
|
+
const sessions = readPublishedSessions2();
|
|
10808
|
+
if (sessions.length === 0) {
|
|
10809
|
+
console.log("Live sessions: none");
|
|
10810
|
+
return;
|
|
10811
|
+
}
|
|
10812
|
+
console.log(`Live sessions: ${sessions.length}`);
|
|
10813
|
+
for (const session of sessions) {
|
|
10814
|
+
console.log(` card ${session.cardId} · session ${session.agentSessionId} · pid ${session.publisherPid} · ${session.cwd}`);
|
|
10815
|
+
}
|
|
10816
|
+
});
|
|
9983
10817
|
program.parse();
|