@gethmy/mcp 3.2.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 +41 -2
- package/dist/cli.js +1188 -157
- package/dist/index.js +805 -42
- package/dist/lib/api-client.js +3 -1
- package/dist/run-hook-cli.js +742 -0
- package/package.json +4 -3
- package/src/api-client.ts +57 -1
- package/src/auto-session.ts +33 -0
- package/src/cli.ts +104 -0
- package/src/comment-session.ts +149 -0
- package/src/hook-install.ts +388 -0
- package/src/plan-task-link.ts +130 -0
- 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 +342 -34
- package/src/tui/setup.ts +3 -0
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";
|
|
@@ -1389,7 +1948,6 @@ var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
|
1389
1948
|
var AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
1390
1949
|
var AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
1391
1950
|
var AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
1392
|
-
var SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
1393
1951
|
var ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
1394
1952
|
// ../harmony-shared/dist/cardLinks.js
|
|
1395
1953
|
var LINK_TYPE_INVERSES = {
|
|
@@ -2617,6 +3175,9 @@ ${untrustedDataBlock(planContent.trim(), {
|
|
|
2617
3175
|
async updatePlaybook(playbookId, updates) {
|
|
2618
3176
|
return this.request("PATCH", `/playbooks/${playbookId}`, updates);
|
|
2619
3177
|
}
|
|
3178
|
+
async deletePlaybook(playbookId) {
|
|
3179
|
+
return this.request("DELETE", `/playbooks/${encodeURIComponent(playbookId)}`);
|
|
3180
|
+
}
|
|
2620
3181
|
}
|
|
2621
3182
|
var _promptModules = null;
|
|
2622
3183
|
async function loadPromptModules() {
|
|
@@ -2644,6 +3205,222 @@ function resetClient() {
|
|
|
2644
3205
|
client2 = null;
|
|
2645
3206
|
}
|
|
2646
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
|
+
|
|
2647
3424
|
// src/auto-session.ts
|
|
2648
3425
|
var CLIENT_DISPLAY_NAMES = {
|
|
2649
3426
|
"claude-code": "Claude Code",
|
|
@@ -2727,6 +3504,7 @@ async function trackActivity(cardId, options) {
|
|
|
2727
3504
|
for (const otherCardId of toEnd) {
|
|
2728
3505
|
await autoEndSession(scope, client3, otherCardId, "completed");
|
|
2729
3506
|
}
|
|
3507
|
+
let agentSessionId;
|
|
2730
3508
|
try {
|
|
2731
3509
|
const started = await client3.startAgentSession(cardId, {
|
|
2732
3510
|
agentIdentifier,
|
|
@@ -2736,6 +3514,7 @@ async function trackActivity(cardId, options) {
|
|
|
2736
3514
|
});
|
|
2737
3515
|
if (started?.session === null)
|
|
2738
3516
|
return;
|
|
3517
|
+
agentSessionId = started?.session?.id;
|
|
2739
3518
|
} catch {}
|
|
2740
3519
|
scope.sessions.set(cardId, {
|
|
2741
3520
|
cardId,
|
|
@@ -2744,8 +3523,14 @@ async function trackActivity(cardId, options) {
|
|
|
2744
3523
|
isExplicit: false,
|
|
2745
3524
|
agentIdentifier,
|
|
2746
3525
|
agentName,
|
|
3526
|
+
agentSessionId,
|
|
2747
3527
|
status: "working"
|
|
2748
3528
|
});
|
|
3529
|
+
beginHookTimeline({
|
|
3530
|
+
cardId,
|
|
3531
|
+
agentSessionId,
|
|
3532
|
+
getClient: () => client3
|
|
3533
|
+
});
|
|
2749
3534
|
}
|
|
2750
3535
|
function markExplicit(cardId, options) {
|
|
2751
3536
|
const scope = getOrCreateScope(options?.scopeId ?? DEFAULT_SCOPE);
|
|
@@ -2846,6 +3631,9 @@ function sweepTick() {
|
|
|
2846
3631
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2847
3632
|
if (!scope.sessions.delete(cardId))
|
|
2848
3633
|
return;
|
|
3634
|
+
try {
|
|
3635
|
+
await endHookTimeline(cardId);
|
|
3636
|
+
} catch {}
|
|
2849
3637
|
try {
|
|
2850
3638
|
await client3.endAgentSession(cardId, { status });
|
|
2851
3639
|
} catch {}
|
|
@@ -3452,6 +4240,51 @@ async function onboardNewUser(params) {
|
|
|
3452
4240
|
};
|
|
3453
4241
|
}
|
|
3454
4242
|
|
|
4243
|
+
// src/plan-task-link.ts
|
|
4244
|
+
function findPlanTask(tasks, taskId) {
|
|
4245
|
+
if (!taskId) {
|
|
4246
|
+
return { ok: false, reason: "No plan task id was given." };
|
|
4247
|
+
}
|
|
4248
|
+
if (!Array.isArray(tasks)) {
|
|
4249
|
+
return {
|
|
4250
|
+
ok: false,
|
|
4251
|
+
reason: "The plan returned no readable criteria list."
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
for (const row of tasks) {
|
|
4255
|
+
if (!row || typeof row !== "object")
|
|
4256
|
+
continue;
|
|
4257
|
+
const candidate = row;
|
|
4258
|
+
if (candidate.id === taskId) {
|
|
4259
|
+
return { ok: true, task: candidate };
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
return {
|
|
4263
|
+
ok: false,
|
|
4264
|
+
reason: `Plan task ${taskId} is not one of this plan's criteria. ` + `Read the plan with harmony_get_plan and use an id from its \`tasks\`.`
|
|
4265
|
+
};
|
|
4266
|
+
}
|
|
4267
|
+
function linkedReport(planId, task, newCardId) {
|
|
4268
|
+
const previous = task.card_id ?? null;
|
|
4269
|
+
return {
|
|
4270
|
+
planId,
|
|
4271
|
+
taskId: task.id,
|
|
4272
|
+
linked: true,
|
|
4273
|
+
criterion: task.content ?? null,
|
|
4274
|
+
...previous && previous !== newCardId ? { replacedCardId: previous } : {}
|
|
4275
|
+
};
|
|
4276
|
+
}
|
|
4277
|
+
function unlinkedReport(planId, task, error) {
|
|
4278
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4279
|
+
return {
|
|
4280
|
+
planId,
|
|
4281
|
+
taskId: task.id,
|
|
4282
|
+
linked: false,
|
|
4283
|
+
criterion: task.content ?? null,
|
|
4284
|
+
error: `The card was created, but the plan criterion still does not point at it: ${message}. ` + `Repair it with harmony_link_plan_task — do not create the card again.`
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
|
|
3455
4288
|
// src/playbook-metric-warnings.ts
|
|
3456
4289
|
function playbookMetricWarnings(agents, steps) {
|
|
3457
4290
|
if (!Array.isArray(steps))
|
|
@@ -3482,30 +4315,30 @@ async function collectPlaybookMetricWarnings(client3, workspaceId, steps) {
|
|
|
3482
4315
|
|
|
3483
4316
|
// src/skills.ts
|
|
3484
4317
|
import {
|
|
3485
|
-
existsSync as
|
|
3486
|
-
mkdirSync as
|
|
3487
|
-
readFileSync as
|
|
3488
|
-
renameSync as
|
|
3489
|
-
writeFileSync as
|
|
4318
|
+
existsSync as existsSync5,
|
|
4319
|
+
mkdirSync as mkdirSync4,
|
|
4320
|
+
readFileSync as readFileSync5,
|
|
4321
|
+
renameSync as renameSync3,
|
|
4322
|
+
writeFileSync as writeFileSync4
|
|
3490
4323
|
} from "node:fs";
|
|
3491
|
-
import { homedir as
|
|
3492
|
-
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";
|
|
3493
4326
|
init_config();
|
|
3494
4327
|
|
|
3495
4328
|
// src/hmy-config.ts
|
|
3496
|
-
import { existsSync as
|
|
3497
|
-
import { homedir as
|
|
3498
|
-
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";
|
|
3499
4332
|
var DEFAULTS = { updateCheck: true, pin: null };
|
|
3500
4333
|
function getHmyConfigPath() {
|
|
3501
|
-
return
|
|
4334
|
+
return join5(homedir3(), ".hmy", "config.yaml");
|
|
3502
4335
|
}
|
|
3503
4336
|
function loadHmyConfig() {
|
|
3504
4337
|
const path = getHmyConfigPath();
|
|
3505
|
-
if (!
|
|
4338
|
+
if (!existsSync4(path))
|
|
3506
4339
|
return { ...DEFAULTS };
|
|
3507
4340
|
try {
|
|
3508
|
-
return parseHmyConfig(
|
|
4341
|
+
return parseHmyConfig(readFileSync4(path, "utf-8"));
|
|
3509
4342
|
} catch {
|
|
3510
4343
|
return { ...DEFAULTS };
|
|
3511
4344
|
}
|
|
@@ -3637,11 +4470,11 @@ function stripSkillPreamble(content) {
|
|
|
3637
4470
|
}
|
|
3638
4471
|
function atomicWrite(filePath, content) {
|
|
3639
4472
|
const dir = dirname2(filePath);
|
|
3640
|
-
if (!
|
|
3641
|
-
|
|
4473
|
+
if (!existsSync5(dir))
|
|
4474
|
+
mkdirSync4(dir, { recursive: true });
|
|
3642
4475
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
3643
|
-
|
|
3644
|
-
|
|
4476
|
+
writeFileSync4(tmp, content);
|
|
4477
|
+
renameSync3(tmp, filePath);
|
|
3645
4478
|
}
|
|
3646
4479
|
function hasMetadataVersion(content) {
|
|
3647
4480
|
return parseSkillVersion(content) !== null;
|
|
@@ -3670,7 +4503,7 @@ function parseSkillVersion(content) {
|
|
|
3670
4503
|
function findSkillFiles(paths, knownNames) {
|
|
3671
4504
|
const results = [];
|
|
3672
4505
|
for (const filePath of paths) {
|
|
3673
|
-
if (!
|
|
4506
|
+
if (!existsSync5(filePath))
|
|
3674
4507
|
continue;
|
|
3675
4508
|
for (const name of knownNames) {
|
|
3676
4509
|
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
@@ -3681,15 +4514,15 @@ function findSkillFiles(paths, knownNames) {
|
|
|
3681
4514
|
}
|
|
3682
4515
|
return results;
|
|
3683
4516
|
}
|
|
3684
|
-
var HMY_DIR =
|
|
3685
|
-
var HMY_VERSION_FILE =
|
|
3686
|
-
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");
|
|
3687
4520
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3688
4521
|
function checkedRecently(now = Date.now()) {
|
|
3689
4522
|
try {
|
|
3690
|
-
if (!
|
|
4523
|
+
if (!existsSync5(LAST_CHECK_FILE))
|
|
3691
4524
|
return false;
|
|
3692
|
-
const ts = Number.parseInt(
|
|
4525
|
+
const ts = Number.parseInt(readFileSync5(LAST_CHECK_FILE, "utf-8").trim(), 10);
|
|
3693
4526
|
if (!Number.isFinite(ts))
|
|
3694
4527
|
return false;
|
|
3695
4528
|
return now - ts < CHECK_TTL_MS;
|
|
@@ -3699,9 +4532,9 @@ function checkedRecently(now = Date.now()) {
|
|
|
3699
4532
|
}
|
|
3700
4533
|
function recordCheck(now = Date.now()) {
|
|
3701
4534
|
try {
|
|
3702
|
-
if (!
|
|
3703
|
-
|
|
3704
|
-
|
|
4535
|
+
if (!existsSync5(HMY_DIR))
|
|
4536
|
+
mkdirSync4(HMY_DIR, { recursive: true });
|
|
4537
|
+
writeFileSync4(LAST_CHECK_FILE, String(now));
|
|
3705
4538
|
} catch {}
|
|
3706
4539
|
}
|
|
3707
4540
|
async function refreshSkills(opts = {}) {
|
|
@@ -3733,7 +4566,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3733
4566
|
const parentDir = dirname2(samplePath);
|
|
3734
4567
|
siblingPath = `${parentDir}/${name}.md`;
|
|
3735
4568
|
}
|
|
3736
|
-
if (
|
|
4569
|
+
if (existsSync5(siblingPath)) {
|
|
3737
4570
|
skillFiles.push({ name, filePath: siblingPath });
|
|
3738
4571
|
}
|
|
3739
4572
|
}
|
|
@@ -3743,7 +4576,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3743
4576
|
let updated = false;
|
|
3744
4577
|
for (const { name, filePath } of skillFiles) {
|
|
3745
4578
|
try {
|
|
3746
|
-
const currentContent =
|
|
4579
|
+
const currentContent = readFileSync5(filePath, "utf-8");
|
|
3747
4580
|
const localVersion = parseSkillVersion(currentContent);
|
|
3748
4581
|
const fetched = await client3.fetchSkill(name);
|
|
3749
4582
|
const remoteVersion = fetched.skillVersion;
|
|
@@ -3949,12 +4782,13 @@ function optionalNonNegativeNumberArg(raw, field) {
|
|
|
3949
4782
|
throw new Error(`${field} must be a non-negative number.`);
|
|
3950
4783
|
return n;
|
|
3951
4784
|
}
|
|
3952
|
-
function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId) {
|
|
4785
|
+
function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, scopeId) {
|
|
3953
4786
|
memorySessions.set(cardId, {
|
|
3954
4787
|
cardId,
|
|
3955
4788
|
agentIdentifier,
|
|
3956
4789
|
agentName,
|
|
3957
4790
|
agentSessionId,
|
|
4791
|
+
scopeId,
|
|
3958
4792
|
memoryReadCount: 0,
|
|
3959
4793
|
pendingActions: [],
|
|
3960
4794
|
allActions: [],
|
|
@@ -4066,6 +4900,10 @@ var TOOLS = {
|
|
|
4066
4900
|
type: "string",
|
|
4067
4901
|
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
4068
4902
|
},
|
|
4903
|
+
planTaskId: {
|
|
4904
|
+
type: "string",
|
|
4905
|
+
description: "Id of the plan CRITERION this card is created to fulfil (optional; requires `planId`). " + "Sets both directions at once: the card's plan_id, and the criterion's card_id — the " + "return leg a card outcome needs to reach the plan. Read the ids from harmony_get_plan's " + "`tasks`. A criterion that is not in the named plan refuses the whole call, so no card is " + "created on a false premise; a failure to write the return leg AFTER the card exists " + "keeps the card and reports it in `planTask` instead."
|
|
4906
|
+
},
|
|
4069
4907
|
attachments: {
|
|
4070
4908
|
type: "array",
|
|
4071
4909
|
description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
@@ -5543,6 +6381,29 @@ var TOOLS = {
|
|
|
5543
6381
|
required: ["planId"]
|
|
5544
6382
|
}
|
|
5545
6383
|
},
|
|
6384
|
+
harmony_link_plan_task: {
|
|
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.",
|
|
6386
|
+
inputSchema: {
|
|
6387
|
+
type: "object",
|
|
6388
|
+
properties: {
|
|
6389
|
+
planId: { type: "string", description: "Plan ID owning the criterion" },
|
|
6390
|
+
taskId: {
|
|
6391
|
+
type: "string",
|
|
6392
|
+
description: "Criterion id, from harmony_get_plan's `tasks`. Must belong to `planId`."
|
|
6393
|
+
},
|
|
6394
|
+
cardId: {
|
|
6395
|
+
type: "string",
|
|
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."
|
|
6397
|
+
},
|
|
6398
|
+
status: {
|
|
6399
|
+
type: "string",
|
|
6400
|
+
enum: ["pending", "in_progress", "completed"],
|
|
6401
|
+
description: "Criterion status. Set `completed` only when the card demonstrably delivered it."
|
|
6402
|
+
}
|
|
6403
|
+
},
|
|
6404
|
+
required: ["planId", "taskId"]
|
|
6405
|
+
}
|
|
6406
|
+
},
|
|
5546
6407
|
harmony_list_playbook: {
|
|
5547
6408
|
description: "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, and state. Read-only.",
|
|
5548
6409
|
inputSchema: {
|
|
@@ -5637,6 +6498,19 @@ var TOOLS = {
|
|
|
5637
6498
|
required: ["playbookId"]
|
|
5638
6499
|
}
|
|
5639
6500
|
},
|
|
6501
|
+
harmony_delete_playbook: {
|
|
6502
|
+
description: "Permanently delete a playbook. IRREVERSIBLE and cascading: its version snapshots and run history are deleted with it, and every card currently running it is unbound — the card stays on the board and keeps its column, but loses its playbook, its pinned version and its stage pointer, so the agent daemon stops treating it as a stage card. Prefer harmony_update_playbook with state='deprecated' unless the playbook should never have existed: a deprecated playbook stays applicable to in-flight cards and is only hidden from new applies. Requires the playbook's creator or a workspace owner/admin — and an ARMED playbook (triggerType 'auto') takes an owner/admin even from its creator, because removing it stops the workspace's automation for everyone. Anyone else is refused and the playbook is left alone. Returns unboundCardCount (the exact number of cards it unbound) and unboundCards (up to 50 of them by id, short_id, title and the stage each one lost).",
|
|
6503
|
+
inputSchema: {
|
|
6504
|
+
type: "object",
|
|
6505
|
+
properties: {
|
|
6506
|
+
playbookId: {
|
|
6507
|
+
type: "string",
|
|
6508
|
+
description: "Playbook ID to delete (UUID)"
|
|
6509
|
+
}
|
|
6510
|
+
},
|
|
6511
|
+
required: ["playbookId"]
|
|
6512
|
+
}
|
|
6513
|
+
},
|
|
5640
6514
|
harmony_signup: {
|
|
5641
6515
|
description: "Create a new user account. Returns a JWT session for subsequent authenticated calls. No API key required.",
|
|
5642
6516
|
inputSchema: {
|
|
@@ -5931,28 +6805,58 @@ async function handleToolCall(name, args, deps) {
|
|
|
5931
6805
|
fileName: z.string().optional(),
|
|
5932
6806
|
contentType: z.string().optional()
|
|
5933
6807
|
})).parse(args.attachments) : [];
|
|
6808
|
+
const planId = args.planId ? z.string().uuid().parse(args.planId) : undefined;
|
|
6809
|
+
const planTaskId = args.planTaskId ? z.string().uuid().parse(args.planTaskId) : undefined;
|
|
6810
|
+
if (planTaskId && !planId) {
|
|
6811
|
+
throw new Error("planTaskId requires planId: no route resolves a plan from a criterion id alone. " + "Pass the plan the criterion belongs to — harmony_get_plan returns both.");
|
|
6812
|
+
}
|
|
6813
|
+
let criterion;
|
|
6814
|
+
if (planTaskId && planId) {
|
|
6815
|
+
const { tasks } = await client3.getPlan(planId);
|
|
6816
|
+
const found = findPlanTask(tasks, planTaskId);
|
|
6817
|
+
if (!found.ok)
|
|
6818
|
+
throw new Error(found.reason);
|
|
6819
|
+
criterion = found.task;
|
|
6820
|
+
}
|
|
5934
6821
|
const result = await client3.createCard(projectId, {
|
|
5935
6822
|
title,
|
|
5936
6823
|
columnId: args.columnId,
|
|
5937
6824
|
description: args.description,
|
|
5938
6825
|
priority: args.priority,
|
|
5939
6826
|
assigneeId: args.assigneeId,
|
|
5940
|
-
planId
|
|
6827
|
+
planId
|
|
5941
6828
|
});
|
|
6829
|
+
const newCardId = result.card?.id;
|
|
6830
|
+
let planTask;
|
|
6831
|
+
if (criterion && planId) {
|
|
6832
|
+
if (!newCardId) {
|
|
6833
|
+
planTask = unlinkedReport(planId, criterion, new Error("no card id was returned to link against"));
|
|
6834
|
+
} else {
|
|
6835
|
+
try {
|
|
6836
|
+
await client3.updatePlanTask(planId, criterion.id, {
|
|
6837
|
+
cardId: newCardId
|
|
6838
|
+
});
|
|
6839
|
+
planTask = linkedReport(planId, criterion, newCardId);
|
|
6840
|
+
} catch (err) {
|
|
6841
|
+
planTask = unlinkedReport(planId, criterion, err);
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
const planTaskField = planTask ? { planTask } : {};
|
|
5942
6846
|
if (attachments.length === 0) {
|
|
5943
|
-
return { success: true, ...result };
|
|
6847
|
+
return { success: true, ...result, ...planTaskField };
|
|
5944
6848
|
}
|
|
5945
|
-
|
|
5946
|
-
if (!cardId) {
|
|
6849
|
+
if (!newCardId) {
|
|
5947
6850
|
return {
|
|
5948
6851
|
success: true,
|
|
5949
6852
|
...result,
|
|
6853
|
+
...planTaskField,
|
|
5950
6854
|
attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
|
|
5951
6855
|
};
|
|
5952
6856
|
}
|
|
5953
6857
|
const attachmentResults = await Promise.all(attachments.map(async (file) => {
|
|
5954
6858
|
try {
|
|
5955
|
-
const uploaded = await attachFileToCard(client3,
|
|
6859
|
+
const uploaded = await attachFileToCard(client3, newCardId, file);
|
|
5956
6860
|
return { ok: true, attachment: uploaded.attachment };
|
|
5957
6861
|
} catch (err) {
|
|
5958
6862
|
return {
|
|
@@ -5962,7 +6866,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5962
6866
|
};
|
|
5963
6867
|
}
|
|
5964
6868
|
}));
|
|
5965
|
-
return {
|
|
6869
|
+
return {
|
|
6870
|
+
success: true,
|
|
6871
|
+
...result,
|
|
6872
|
+
...planTaskField,
|
|
6873
|
+
attachments: attachmentResults
|
|
6874
|
+
};
|
|
5966
6875
|
}
|
|
5967
6876
|
case "harmony_update_card": {
|
|
5968
6877
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -6446,15 +7355,24 @@ ${list}
|
|
|
6446
7355
|
const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
|
|
6447
7356
|
const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
|
|
6448
7357
|
const replyToId = args.replyToId !== undefined ? z.string().uuid().parse(args.replyToId) : undefined;
|
|
6449
|
-
const
|
|
7358
|
+
const sessionChoice = chooseCommentSession({
|
|
7359
|
+
cardId,
|
|
7360
|
+
tracked: getMemorySession(cardId),
|
|
7361
|
+
callerScopeId: deps.getScopeId?.(),
|
|
7362
|
+
declared: readDeclaredRunSession()
|
|
7363
|
+
});
|
|
6450
7364
|
const result = await client3.addComment(cardId, body, {
|
|
6451
7365
|
commentType,
|
|
6452
7366
|
supersedesId,
|
|
6453
7367
|
confirmsId,
|
|
6454
7368
|
replyToId,
|
|
6455
|
-
agentSessionId
|
|
7369
|
+
agentSessionId: sessionChoice.kind === "session" ? sessionChoice.agentSessionId : undefined
|
|
6456
7370
|
});
|
|
6457
|
-
return {
|
|
7371
|
+
return {
|
|
7372
|
+
success: true,
|
|
7373
|
+
...result,
|
|
7374
|
+
sessionAttribution: sessionChoice.kind === "session" ? sessionChoice.source : "none"
|
|
7375
|
+
};
|
|
6458
7376
|
}
|
|
6459
7377
|
case "harmony_get_comments": {
|
|
6460
7378
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -6678,7 +7596,12 @@ ${options}
|
|
|
6678
7596
|
scopeId: deps.getScopeId?.()
|
|
6679
7597
|
});
|
|
6680
7598
|
const agentSessionId = result.session?.id;
|
|
6681
|
-
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId);
|
|
7599
|
+
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, deps.getScopeId?.());
|
|
7600
|
+
beginHookTimeline({
|
|
7601
|
+
cardId,
|
|
7602
|
+
agentSessionId,
|
|
7603
|
+
getClient: () => client3
|
|
7604
|
+
});
|
|
6682
7605
|
return {
|
|
6683
7606
|
success: true,
|
|
6684
7607
|
assignedTo,
|
|
@@ -6740,6 +7663,7 @@ ${options}
|
|
|
6740
7663
|
const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
6741
7664
|
await flushMemoryActions(client3, cardId);
|
|
6742
7665
|
cleanupMemorySession(cardId);
|
|
7666
|
+
await endHookTimeline(cardId);
|
|
6743
7667
|
let result = {
|
|
6744
7668
|
session: null
|
|
6745
7669
|
};
|
|
@@ -7349,7 +8273,7 @@ ${options}
|
|
|
7349
8273
|
} else if (args.cardId) {
|
|
7350
8274
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
7351
8275
|
result = await client3.getPlanByCardId(cardId);
|
|
7352
|
-
if (!result) {
|
|
8276
|
+
if (!result || result.plan == null && !result.foreign_criteria?.length) {
|
|
7353
8277
|
return {
|
|
7354
8278
|
success: true,
|
|
7355
8279
|
plan: null,
|
|
@@ -7357,13 +8281,23 @@ ${options}
|
|
|
7357
8281
|
message: "No plan linked to this card"
|
|
7358
8282
|
};
|
|
7359
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
|
+
}
|
|
7360
8293
|
} else {
|
|
7361
8294
|
throw new Error("Either planId or cardId must be provided");
|
|
7362
8295
|
}
|
|
7363
8296
|
return {
|
|
7364
8297
|
success: true,
|
|
7365
8298
|
plan: result.plan,
|
|
7366
|
-
tasks: result.tasks
|
|
8299
|
+
tasks: result.tasks,
|
|
8300
|
+
...result.foreign_criteria?.length ? { divergentCriteria: result.foreign_criteria } : {}
|
|
7367
8301
|
};
|
|
7368
8302
|
}
|
|
7369
8303
|
case "harmony_update_plan": {
|
|
@@ -7379,6 +8313,34 @@ ${options}
|
|
|
7379
8313
|
const result = await client3.updatePlan(planId, updates);
|
|
7380
8314
|
return { success: true, plan: result.plan };
|
|
7381
8315
|
}
|
|
8316
|
+
case "harmony_link_plan_task": {
|
|
8317
|
+
const planId = z.string().uuid().parse(args.planId);
|
|
8318
|
+
const taskId = z.string().uuid().parse(args.taskId);
|
|
8319
|
+
const cardId = args.cardId ? z.string().uuid().parse(args.cardId) : undefined;
|
|
8320
|
+
const status = args.status ? z.enum(["pending", "in_progress", "completed"]).parse(args.status) : undefined;
|
|
8321
|
+
if (!cardId && !status) {
|
|
8322
|
+
throw new Error("Nothing to do: pass cardId, status, or both.");
|
|
8323
|
+
}
|
|
8324
|
+
const { tasks } = await client3.getPlan(planId);
|
|
8325
|
+
const found = findPlanTask(tasks, taskId);
|
|
8326
|
+
if (!found.ok)
|
|
8327
|
+
throw new Error(found.reason);
|
|
8328
|
+
const result = await client3.updatePlanTask(planId, taskId, {
|
|
8329
|
+
cardId,
|
|
8330
|
+
status
|
|
8331
|
+
});
|
|
8332
|
+
return {
|
|
8333
|
+
success: true,
|
|
8334
|
+
planTask: {
|
|
8335
|
+
planId,
|
|
8336
|
+
taskId,
|
|
8337
|
+
criterion: found.task.content ?? null,
|
|
8338
|
+
...cardId ? linkedReport(planId, found.task, cardId) : { linked: found.task.card_id != null },
|
|
8339
|
+
...cardId && result.cardPlanAdopted ? { cardPlanAdopted: true } : {},
|
|
8340
|
+
...status ? { status } : {}
|
|
8341
|
+
}
|
|
8342
|
+
};
|
|
8343
|
+
}
|
|
7382
8344
|
case "harmony_advance_plan": {
|
|
7383
8345
|
const planId = z.string().uuid().parse(args.planId);
|
|
7384
8346
|
const summary = args.summary;
|
|
@@ -7466,6 +8428,16 @@ ${options}
|
|
|
7466
8428
|
...warnings.length > 0 ? { warnings } : {}
|
|
7467
8429
|
};
|
|
7468
8430
|
}
|
|
8431
|
+
case "harmony_delete_playbook": {
|
|
8432
|
+
const playbookId = z.string().uuid().parse(args.playbookId);
|
|
8433
|
+
const result = await client3.deletePlaybook(playbookId);
|
|
8434
|
+
return {
|
|
8435
|
+
success: true,
|
|
8436
|
+
playbook: result.playbook,
|
|
8437
|
+
unboundCardCount: result.unboundCardCount,
|
|
8438
|
+
unboundCards: result.unboundCards
|
|
8439
|
+
};
|
|
8440
|
+
}
|
|
7469
8441
|
case "harmony_save_card_as_playbook":
|
|
7470
8442
|
return deprecatedRemovedToolResult("harmony_save_card_as_playbook");
|
|
7471
8443
|
case "harmony_signup": {
|
|
@@ -7599,6 +8571,9 @@ class HarmonyMCPServer {
|
|
|
7599
8571
|
try {
|
|
7600
8572
|
await shutdownAllSessions();
|
|
7601
8573
|
} catch {}
|
|
8574
|
+
try {
|
|
8575
|
+
await stopAllRunEventForwarders();
|
|
8576
|
+
} catch {}
|
|
7602
8577
|
destroyAutoSession();
|
|
7603
8578
|
process.exit(exitCode);
|
|
7604
8579
|
};
|
|
@@ -7636,29 +8611,29 @@ class HarmonyMCPServer {
|
|
|
7636
8611
|
// src/tui/setup.ts
|
|
7637
8612
|
import { createHash as createHash5 } from "node:crypto";
|
|
7638
8613
|
import {
|
|
7639
|
-
existsSync as
|
|
8614
|
+
existsSync as existsSync9,
|
|
7640
8615
|
lstatSync,
|
|
7641
|
-
mkdirSync as
|
|
8616
|
+
mkdirSync as mkdirSync6,
|
|
7642
8617
|
symlinkSync,
|
|
7643
|
-
unlinkSync
|
|
8618
|
+
unlinkSync as unlinkSync2
|
|
7644
8619
|
} from "node:fs";
|
|
7645
|
-
import { homedir as
|
|
7646
|
-
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";
|
|
7647
8622
|
import * as p4 from "@clack/prompts";
|
|
7648
8623
|
init_config();
|
|
7649
8624
|
init_oauth_login();
|
|
7650
8625
|
|
|
7651
8626
|
// src/tui/agents.ts
|
|
7652
|
-
import { existsSync as
|
|
7653
|
-
import { homedir as
|
|
7654
|
-
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";
|
|
7655
8630
|
var AGENT_DEFINITIONS = [
|
|
7656
8631
|
{
|
|
7657
8632
|
id: "claude",
|
|
7658
8633
|
name: "Claude Code",
|
|
7659
8634
|
description: "Anthropic CLI agent",
|
|
7660
8635
|
hint: "/hmy <card>",
|
|
7661
|
-
globalPaths: [
|
|
8636
|
+
globalPaths: [join7(homedir5(), ".claude")],
|
|
7662
8637
|
localPaths: [".claude"]
|
|
7663
8638
|
},
|
|
7664
8639
|
{
|
|
@@ -7666,7 +8641,7 @@ var AGENT_DEFINITIONS = [
|
|
|
7666
8641
|
name: "Codex",
|
|
7667
8642
|
description: "OpenAI coding agent",
|
|
7668
8643
|
hint: "/prompts:hmy <card>",
|
|
7669
|
-
globalPaths: [
|
|
8644
|
+
globalPaths: [join7(homedir5(), ".codex")],
|
|
7670
8645
|
localPaths: ["AGENTS.md"]
|
|
7671
8646
|
},
|
|
7672
8647
|
{
|
|
@@ -7682,20 +8657,20 @@ var AGENT_DEFINITIONS = [
|
|
|
7682
8657
|
name: "Windsurf",
|
|
7683
8658
|
description: "Codeium AI IDE",
|
|
7684
8659
|
hint: "MCP tools available automatically",
|
|
7685
|
-
globalPaths: [
|
|
8660
|
+
globalPaths: [join7(homedir5(), ".codeium", "windsurf")],
|
|
7686
8661
|
localPaths: [".windsurf", ".windsurfrules"]
|
|
7687
8662
|
}
|
|
7688
8663
|
];
|
|
7689
8664
|
function detectAgents(cwd = process.cwd()) {
|
|
7690
8665
|
return AGENT_DEFINITIONS.map((def) => {
|
|
7691
|
-
const globalPath = def.globalPaths.find((p) =>
|
|
7692
|
-
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;
|
|
7693
8668
|
return {
|
|
7694
8669
|
id: def.id,
|
|
7695
8670
|
name: def.name,
|
|
7696
8671
|
detected: !!(globalPath || localPath),
|
|
7697
8672
|
globalPath,
|
|
7698
|
-
localPath: localPath ?
|
|
8673
|
+
localPath: localPath ? join7(cwd, localPath) : null,
|
|
7699
8674
|
description: def.description,
|
|
7700
8675
|
hint: def.hint
|
|
7701
8676
|
};
|
|
@@ -7714,8 +8689,8 @@ async function confirmOrDefault(assumeYes, opts) {
|
|
|
7714
8689
|
}
|
|
7715
8690
|
|
|
7716
8691
|
// src/tui/docs.ts
|
|
7717
|
-
import { existsSync as
|
|
7718
|
-
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";
|
|
7719
8694
|
import * as p2 from "@clack/prompts";
|
|
7720
8695
|
|
|
7721
8696
|
// src/tui/theme.ts
|
|
@@ -7802,25 +8777,25 @@ var IGNORED_DIRS = new Set([
|
|
|
7802
8777
|
]);
|
|
7803
8778
|
function readJson(filePath) {
|
|
7804
8779
|
try {
|
|
7805
|
-
return JSON.parse(
|
|
8780
|
+
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
7806
8781
|
} catch {
|
|
7807
8782
|
return null;
|
|
7808
8783
|
}
|
|
7809
8784
|
}
|
|
7810
8785
|
function readText(filePath) {
|
|
7811
8786
|
try {
|
|
7812
|
-
return
|
|
8787
|
+
return readFileSync6(filePath, "utf-8");
|
|
7813
8788
|
} catch {
|
|
7814
8789
|
return null;
|
|
7815
8790
|
}
|
|
7816
8791
|
}
|
|
7817
8792
|
function listDirs(dirPath) {
|
|
7818
8793
|
try {
|
|
7819
|
-
return
|
|
8794
|
+
return readdirSync3(dirPath).filter((entry) => {
|
|
7820
8795
|
if (IGNORED_DIRS.has(entry) || entry.startsWith("."))
|
|
7821
8796
|
return false;
|
|
7822
8797
|
try {
|
|
7823
|
-
return
|
|
8798
|
+
return statSync3(join8(dirPath, entry)).isDirectory();
|
|
7824
8799
|
} catch {
|
|
7825
8800
|
return false;
|
|
7826
8801
|
}
|
|
@@ -7868,25 +8843,25 @@ function describeDir(name) {
|
|
|
7868
8843
|
}
|
|
7869
8844
|
function scanProject(cwd) {
|
|
7870
8845
|
let packageManager = null;
|
|
7871
|
-
if (
|
|
8846
|
+
if (existsSync7(join8(cwd, "bun.lock")) || existsSync7(join8(cwd, "bun.lockb"))) {
|
|
7872
8847
|
packageManager = "bun";
|
|
7873
|
-
} else if (
|
|
8848
|
+
} else if (existsSync7(join8(cwd, "pnpm-lock.yaml"))) {
|
|
7874
8849
|
packageManager = "pnpm";
|
|
7875
|
-
} else if (
|
|
8850
|
+
} else if (existsSync7(join8(cwd, "yarn.lock"))) {
|
|
7876
8851
|
packageManager = "yarn";
|
|
7877
|
-
} else if (
|
|
8852
|
+
} else if (existsSync7(join8(cwd, "package.json"))) {
|
|
7878
8853
|
packageManager = "npm";
|
|
7879
8854
|
}
|
|
7880
|
-
const pkg = readJson(
|
|
8855
|
+
const pkg = readJson(join8(cwd, "package.json"));
|
|
7881
8856
|
const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
7882
8857
|
let language = "unknown";
|
|
7883
|
-
if (
|
|
8858
|
+
if (existsSync7(join8(cwd, "tsconfig.json"))) {
|
|
7884
8859
|
language = "typescript";
|
|
7885
|
-
} else if (
|
|
8860
|
+
} else if (existsSync7(join8(cwd, "go.mod"))) {
|
|
7886
8861
|
language = "go";
|
|
7887
|
-
} else if (
|
|
8862
|
+
} else if (existsSync7(join8(cwd, "Cargo.toml"))) {
|
|
7888
8863
|
language = "rust";
|
|
7889
|
-
} else if (
|
|
8864
|
+
} else if (existsSync7(join8(cwd, "setup.py")) || existsSync7(join8(cwd, "pyproject.toml"))) {
|
|
7890
8865
|
language = "python";
|
|
7891
8866
|
} else if (pkg) {
|
|
7892
8867
|
language = "javascript";
|
|
@@ -7922,7 +8897,7 @@ function scanProject(cwd) {
|
|
|
7922
8897
|
}
|
|
7923
8898
|
}
|
|
7924
8899
|
let linter = null;
|
|
7925
|
-
if (
|
|
8900
|
+
if (existsSync7(join8(cwd, "biome.json")) || existsSync7(join8(cwd, "biome.jsonc"))) {
|
|
7926
8901
|
linter = "biome";
|
|
7927
8902
|
} else {
|
|
7928
8903
|
const eslintFiles = [
|
|
@@ -7937,7 +8912,7 @@ function scanProject(cwd) {
|
|
|
7937
8912
|
"eslint.config.cjs",
|
|
7938
8913
|
"eslint.config.ts"
|
|
7939
8914
|
];
|
|
7940
|
-
if (eslintFiles.some((f) =>
|
|
8915
|
+
if (eslintFiles.some((f) => existsSync7(join8(cwd, f)))) {
|
|
7941
8916
|
linter = "eslint";
|
|
7942
8917
|
} else {
|
|
7943
8918
|
const prettierFiles = [
|
|
@@ -7949,13 +8924,13 @@ function scanProject(cwd) {
|
|
|
7949
8924
|
"prettier.config.js",
|
|
7950
8925
|
"prettier.config.mjs"
|
|
7951
8926
|
];
|
|
7952
|
-
if (prettierFiles.some((f) =>
|
|
8927
|
+
if (prettierFiles.some((f) => existsSync7(join8(cwd, f)))) {
|
|
7953
8928
|
linter = "prettier";
|
|
7954
8929
|
}
|
|
7955
8930
|
}
|
|
7956
8931
|
}
|
|
7957
8932
|
let indentStyle = null;
|
|
7958
|
-
const biome = readJson(
|
|
8933
|
+
const biome = readJson(join8(cwd, "biome.json")) ?? readJson(join8(cwd, "biome.jsonc"));
|
|
7959
8934
|
if (biome) {
|
|
7960
8935
|
const formatter = biome.formatter;
|
|
7961
8936
|
if (formatter) {
|
|
@@ -7965,7 +8940,7 @@ function scanProject(cwd) {
|
|
|
7965
8940
|
}
|
|
7966
8941
|
}
|
|
7967
8942
|
if (!indentStyle) {
|
|
7968
|
-
const editorConfig = readText(
|
|
8943
|
+
const editorConfig = readText(join8(cwd, ".editorconfig"));
|
|
7969
8944
|
if (editorConfig) {
|
|
7970
8945
|
const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
|
|
7971
8946
|
const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
|
|
@@ -7978,13 +8953,13 @@ function scanProject(cwd) {
|
|
|
7978
8953
|
}
|
|
7979
8954
|
}
|
|
7980
8955
|
const dirs = listDirs(cwd);
|
|
7981
|
-
const srcDirs =
|
|
7982
|
-
const monorepo =
|
|
8956
|
+
const srcDirs = existsSync7(join8(cwd, "src")) ? listDirs(join8(cwd, "src")) : [];
|
|
8957
|
+
const monorepo = existsSync7(join8(cwd, "packages")) || existsSync7(join8(cwd, "apps"));
|
|
7983
8958
|
const existingDocs = {
|
|
7984
|
-
agentsMd:
|
|
7985
|
-
claudeMd:
|
|
7986
|
-
docsDir:
|
|
7987
|
-
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"))
|
|
7988
8963
|
};
|
|
7989
8964
|
return {
|
|
7990
8965
|
packageManager,
|
|
@@ -8135,9 +9110,9 @@ var VAGUE_STANDARDS = [
|
|
|
8135
9110
|
];
|
|
8136
9111
|
function verifyDocs(cwd) {
|
|
8137
9112
|
const issues = [];
|
|
8138
|
-
const claudeMd = readText(
|
|
8139
|
-
const agentsMd = readText(
|
|
8140
|
-
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"));
|
|
8141
9116
|
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
8142
9117
|
const projectRoot = resolve2(cwd);
|
|
8143
9118
|
if (claudeMd) {
|
|
@@ -8167,7 +9142,7 @@ function verifyDocs(cwd) {
|
|
|
8167
9142
|
continue;
|
|
8168
9143
|
}
|
|
8169
9144
|
importedFiles.push({ ref: refPath, resolved: resolvedPath });
|
|
8170
|
-
if (!
|
|
9145
|
+
if (!existsSync7(resolvedPath)) {
|
|
8171
9146
|
issues.push({
|
|
8172
9147
|
severity: "error",
|
|
8173
9148
|
file: "CLAUDE.md",
|
|
@@ -8303,7 +9278,7 @@ function verifyDocs(cwd) {
|
|
|
8303
9278
|
}
|
|
8304
9279
|
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
8305
9280
|
}
|
|
8306
|
-
const archMd = readText(
|
|
9281
|
+
const archMd = readText(join8(cwd, "docs", "architecture.md"));
|
|
8307
9282
|
if (archMd) {
|
|
8308
9283
|
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
8309
9284
|
}
|
|
@@ -8350,7 +9325,7 @@ function checkBacktickPaths(content, file, cwd, issues) {
|
|
|
8350
9325
|
const resolvedRef = resolve2(root, refPath);
|
|
8351
9326
|
if (resolvedRef !== root && !resolvedRef.startsWith(root + sep2))
|
|
8352
9327
|
continue;
|
|
8353
|
-
if (!
|
|
9328
|
+
if (!existsSync7(resolvedRef)) {
|
|
8354
9329
|
issues.push({
|
|
8355
9330
|
severity: "warning",
|
|
8356
9331
|
file,
|
|
@@ -8373,18 +9348,18 @@ async function runDocsStep(cwd) {
|
|
|
8373
9348
|
}
|
|
8374
9349
|
const files = [];
|
|
8375
9350
|
files.push({
|
|
8376
|
-
path:
|
|
9351
|
+
path: join8(cwd, "AGENTS.md"),
|
|
8377
9352
|
content: generateAgentsMd(info, cwd),
|
|
8378
9353
|
type: "text"
|
|
8379
9354
|
});
|
|
8380
9355
|
files.push({
|
|
8381
|
-
path:
|
|
9356
|
+
path: join8(cwd, "CLAUDE.md"),
|
|
8382
9357
|
content: generateClaudeMd(info),
|
|
8383
9358
|
type: "text"
|
|
8384
9359
|
});
|
|
8385
9360
|
if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
|
|
8386
9361
|
files.push({
|
|
8387
|
-
path:
|
|
9362
|
+
path: join8(cwd, "docs", "architecture.md"),
|
|
8388
9363
|
content: generateArchitectureMd(info, cwd),
|
|
8389
9364
|
type: "text"
|
|
8390
9365
|
});
|
|
@@ -8422,21 +9397,21 @@ async function runDocsStep(cwd) {
|
|
|
8422
9397
|
// src/tui/writer.ts
|
|
8423
9398
|
import {
|
|
8424
9399
|
chmodSync,
|
|
8425
|
-
existsSync as
|
|
8426
|
-
mkdirSync as
|
|
8427
|
-
readFileSync as
|
|
8428
|
-
writeFileSync as
|
|
9400
|
+
existsSync as existsSync8,
|
|
9401
|
+
mkdirSync as mkdirSync5,
|
|
9402
|
+
readFileSync as readFileSync7,
|
|
9403
|
+
writeFileSync as writeFileSync5
|
|
8429
9404
|
} from "node:fs";
|
|
8430
|
-
import { homedir as
|
|
9405
|
+
import { homedir as homedir6 } from "node:os";
|
|
8431
9406
|
import { dirname as dirname3 } from "node:path";
|
|
8432
9407
|
import * as p3 from "@clack/prompts";
|
|
8433
9408
|
function ensureDir(dirPath) {
|
|
8434
|
-
if (!
|
|
8435
|
-
|
|
9409
|
+
if (!existsSync8(dirPath)) {
|
|
9410
|
+
mkdirSync5(dirPath, { recursive: true, mode: 493 });
|
|
8436
9411
|
}
|
|
8437
9412
|
}
|
|
8438
9413
|
function writeFile(filePath, content, options = {}) {
|
|
8439
|
-
const exists =
|
|
9414
|
+
const exists = existsSync8(filePath);
|
|
8440
9415
|
if (exists && !options.force) {
|
|
8441
9416
|
return { path: filePath, action: "skip" };
|
|
8442
9417
|
}
|
|
@@ -8444,7 +9419,7 @@ function writeFile(filePath, content, options = {}) {
|
|
|
8444
9419
|
ensureDir(dirname3(filePath));
|
|
8445
9420
|
const defaultMode = filePath.includes(".harmony-mcp") ? 384 : 420;
|
|
8446
9421
|
const mode = options.mode ?? defaultMode;
|
|
8447
|
-
|
|
9422
|
+
writeFileSync5(filePath, content, { mode });
|
|
8448
9423
|
if (options.mode !== undefined) {
|
|
8449
9424
|
chmodSync(filePath, options.mode);
|
|
8450
9425
|
}
|
|
@@ -8458,11 +9433,11 @@ function writeFile(filePath, content, options = {}) {
|
|
|
8458
9433
|
}
|
|
8459
9434
|
}
|
|
8460
9435
|
function mergeJsonFile(filePath, updates, options = {}) {
|
|
8461
|
-
const exists =
|
|
9436
|
+
const exists = existsSync8(filePath);
|
|
8462
9437
|
if (!exists) {
|
|
8463
9438
|
try {
|
|
8464
9439
|
ensureDir(dirname3(filePath));
|
|
8465
|
-
|
|
9440
|
+
writeFileSync5(filePath, JSON.stringify(updates, null, 2), {
|
|
8466
9441
|
mode: 420
|
|
8467
9442
|
});
|
|
8468
9443
|
return { path: filePath, action: "create" };
|
|
@@ -8475,7 +9450,7 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
8475
9450
|
}
|
|
8476
9451
|
}
|
|
8477
9452
|
try {
|
|
8478
|
-
const existing = JSON.parse(
|
|
9453
|
+
const existing = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
8479
9454
|
if (updates.mcpServers && existing.mcpServers) {
|
|
8480
9455
|
const existingServers = existing.mcpServers;
|
|
8481
9456
|
const updateServers = updates.mcpServers;
|
|
@@ -8483,12 +9458,12 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
8483
9458
|
} else {
|
|
8484
9459
|
Object.assign(existing, updates);
|
|
8485
9460
|
}
|
|
8486
|
-
|
|
9461
|
+
writeFileSync5(filePath, JSON.stringify(existing, null, 2), { mode: 420 });
|
|
8487
9462
|
return { path: filePath, action: "merge" };
|
|
8488
9463
|
} catch {
|
|
8489
9464
|
if (options.force) {
|
|
8490
9465
|
try {
|
|
8491
|
-
|
|
9466
|
+
writeFileSync5(filePath, JSON.stringify(updates, null, 2), {
|
|
8492
9467
|
mode: 420
|
|
8493
9468
|
});
|
|
8494
9469
|
return { path: filePath, action: "update" };
|
|
@@ -8508,11 +9483,11 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
8508
9483
|
}
|
|
8509
9484
|
}
|
|
8510
9485
|
function appendToToml(filePath, section, content, options = {}) {
|
|
8511
|
-
const exists =
|
|
9486
|
+
const exists = existsSync8(filePath);
|
|
8512
9487
|
if (!exists) {
|
|
8513
9488
|
try {
|
|
8514
9489
|
ensureDir(dirname3(filePath));
|
|
8515
|
-
|
|
9490
|
+
writeFileSync5(filePath, content, { mode: 420 });
|
|
8516
9491
|
return { path: filePath, action: "create" };
|
|
8517
9492
|
} catch (error) {
|
|
8518
9493
|
return {
|
|
@@ -8523,19 +9498,19 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
8523
9498
|
}
|
|
8524
9499
|
}
|
|
8525
9500
|
try {
|
|
8526
|
-
const existing =
|
|
9501
|
+
const existing = readFileSync7(filePath, "utf-8");
|
|
8527
9502
|
if (existing.includes(`[${section}]`)) {
|
|
8528
9503
|
if (options.force) {
|
|
8529
9504
|
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8530
9505
|
const updated = existing.replace(new RegExp(`(?:#[^\\n]*\\n)*\\[${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`), content.trim() + `
|
|
8531
9506
|
|
|
8532
9507
|
`);
|
|
8533
|
-
|
|
9508
|
+
writeFileSync5(filePath, updated, { mode: 420 });
|
|
8534
9509
|
return { path: filePath, action: "update" };
|
|
8535
9510
|
}
|
|
8536
9511
|
return { path: filePath, action: "skip" };
|
|
8537
9512
|
}
|
|
8538
|
-
|
|
9513
|
+
writeFileSync5(filePath, existing + `
|
|
8539
9514
|
` + content, { mode: 420 });
|
|
8540
9515
|
return { path: filePath, action: "merge" };
|
|
8541
9516
|
} catch (error) {
|
|
@@ -8548,7 +9523,7 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
8548
9523
|
}
|
|
8549
9524
|
async function writeFilesWithProgress(files, options = {}) {
|
|
8550
9525
|
const results = [];
|
|
8551
|
-
const home =
|
|
9526
|
+
const home = homedir6();
|
|
8552
9527
|
const spinner2 = p3.spinner();
|
|
8553
9528
|
spinner2.start("Writing configuration files...");
|
|
8554
9529
|
for (const file of files) {
|
|
@@ -8585,10 +9560,10 @@ function getWriteSummary(files, options = {}) {
|
|
|
8585
9560
|
const toCreate = [];
|
|
8586
9561
|
const toUpdate = [];
|
|
8587
9562
|
const toSkip = [];
|
|
8588
|
-
const home =
|
|
9563
|
+
const home = homedir6();
|
|
8589
9564
|
for (const file of files) {
|
|
8590
9565
|
const displayPath = formatPath(file.path, home);
|
|
8591
|
-
const exists =
|
|
9566
|
+
const exists = existsSync8(file.path);
|
|
8592
9567
|
if (exists && !options.force) {
|
|
8593
9568
|
toSkip.push(displayPath);
|
|
8594
9569
|
} else if (exists) {
|
|
@@ -8643,13 +9618,14 @@ var SAFE_HARMONY_TOOLS = [
|
|
|
8643
9618
|
"harmony_create_plan",
|
|
8644
9619
|
"harmony_update_plan",
|
|
8645
9620
|
"harmony_advance_plan",
|
|
9621
|
+
"harmony_link_plan_task",
|
|
8646
9622
|
"harmony_remember",
|
|
8647
9623
|
"harmony_relate",
|
|
8648
9624
|
"harmony_update_memory",
|
|
8649
9625
|
"harmony_process_command",
|
|
8650
9626
|
"harmony_sync"
|
|
8651
9627
|
];
|
|
8652
|
-
var GLOBAL_SKILLS_DIR =
|
|
9628
|
+
var GLOBAL_SKILLS_DIR = join9(homedir7(), ".agents", "skills");
|
|
8653
9629
|
var API_URL = "https://app.gethmy.com/api";
|
|
8654
9630
|
async function registerMcpServer() {
|
|
8655
9631
|
try {
|
|
@@ -8663,15 +9639,15 @@ async function registerMcpServer() {
|
|
|
8663
9639
|
}
|
|
8664
9640
|
}
|
|
8665
9641
|
async function writeMcpConfigFallback(home) {
|
|
8666
|
-
const { readFileSync:
|
|
8667
|
-
const settingsPath =
|
|
9642
|
+
const { readFileSync: readFileSync8, writeFileSync: writeFileSync6, mkdirSync: mkdirSync7, existsSync: existsSync10 } = await import("node:fs");
|
|
9643
|
+
const settingsPath = join9(home, ".claude", "settings.json");
|
|
8668
9644
|
const settingsDir = dirname4(settingsPath);
|
|
8669
|
-
if (!
|
|
8670
|
-
|
|
9645
|
+
if (!existsSync10(settingsDir)) {
|
|
9646
|
+
mkdirSync7(settingsDir, { recursive: true });
|
|
8671
9647
|
}
|
|
8672
9648
|
let settings = {};
|
|
8673
|
-
if (
|
|
8674
|
-
settings = JSON.parse(
|
|
9649
|
+
if (existsSync10(settingsPath)) {
|
|
9650
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
8675
9651
|
}
|
|
8676
9652
|
const mcpServers = settings.mcpServers || {};
|
|
8677
9653
|
mcpServers.harmony = {
|
|
@@ -8679,18 +9655,18 @@ async function writeMcpConfigFallback(home) {
|
|
|
8679
9655
|
args: ["-y", "@gethmy/mcp@latest", "serve"]
|
|
8680
9656
|
};
|
|
8681
9657
|
settings.mcpServers = mcpServers;
|
|
8682
|
-
|
|
9658
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2));
|
|
8683
9659
|
}
|
|
8684
9660
|
async function allowlistHarmonyTools(home, allowAll) {
|
|
8685
|
-
const { readFileSync:
|
|
8686
|
-
const settingsPath =
|
|
9661
|
+
const { readFileSync: readFileSync8, writeFileSync: writeFileSync6, mkdirSync: mkdirSync7, existsSync: existsSync10 } = await import("node:fs");
|
|
9662
|
+
const settingsPath = join9(home, ".claude", "settings.json");
|
|
8687
9663
|
const settingsDir = dirname4(settingsPath);
|
|
8688
|
-
if (!
|
|
8689
|
-
|
|
9664
|
+
if (!existsSync10(settingsDir)) {
|
|
9665
|
+
mkdirSync7(settingsDir, { recursive: true });
|
|
8690
9666
|
}
|
|
8691
9667
|
let settings = {};
|
|
8692
|
-
if (
|
|
8693
|
-
settings = JSON.parse(
|
|
9668
|
+
if (existsSync10(settingsPath)) {
|
|
9669
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
8694
9670
|
}
|
|
8695
9671
|
const permissions = settings.permissions || {};
|
|
8696
9672
|
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
@@ -8703,7 +9679,7 @@ async function allowlistHarmonyTools(home, allowAll) {
|
|
|
8703
9679
|
allow.push(...missing);
|
|
8704
9680
|
permissions.allow = allow;
|
|
8705
9681
|
settings.permissions = permissions;
|
|
8706
|
-
|
|
9682
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2));
|
|
8707
9683
|
return "added";
|
|
8708
9684
|
}
|
|
8709
9685
|
async function validateApiKey(apiKey, apiUrl = API_URL) {
|
|
@@ -8797,7 +9773,7 @@ async function resolveProjectSlug(apiKey, slug) {
|
|
|
8797
9773
|
};
|
|
8798
9774
|
}
|
|
8799
9775
|
async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
8800
|
-
const home =
|
|
9776
|
+
const home = homedir7();
|
|
8801
9777
|
const files = [];
|
|
8802
9778
|
const symlinks = [];
|
|
8803
9779
|
switch (agentId) {
|
|
@@ -8812,17 +9788,17 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
|
8812
9788
|
const content = buildSkillFile(fetched);
|
|
8813
9789
|
if (installMode === "global") {
|
|
8814
9790
|
files.push({
|
|
8815
|
-
path:
|
|
9791
|
+
path: join9(GLOBAL_SKILLS_DIR, name, "SKILL.md"),
|
|
8816
9792
|
content,
|
|
8817
9793
|
type: "text"
|
|
8818
9794
|
});
|
|
8819
9795
|
symlinks.push({
|
|
8820
|
-
target:
|
|
8821
|
-
link:
|
|
9796
|
+
target: join9(GLOBAL_SKILLS_DIR, name),
|
|
9797
|
+
link: join9(home, ".claude", "skills", name)
|
|
8822
9798
|
});
|
|
8823
9799
|
} else {
|
|
8824
9800
|
files.push({
|
|
8825
|
-
path:
|
|
9801
|
+
path: join9(cwd, ".claude", "skills", name, "SKILL.md"),
|
|
8826
9802
|
content,
|
|
8827
9803
|
type: "text"
|
|
8828
9804
|
});
|
|
@@ -8845,13 +9821,13 @@ ${summary}`);
|
|
|
8845
9821
|
throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
|
|
8846
9822
|
}
|
|
8847
9823
|
files.push({
|
|
8848
|
-
path:
|
|
9824
|
+
path: join9(home, ".hmy", "bin", "hmy-update-check"),
|
|
8849
9825
|
content: updateCheckFetched.content,
|
|
8850
9826
|
type: "text",
|
|
8851
9827
|
mode: 493
|
|
8852
9828
|
});
|
|
8853
9829
|
files.push({
|
|
8854
|
-
path:
|
|
9830
|
+
path: join9(home, ".hmy", "VERSION"),
|
|
8855
9831
|
content: versionInfo.version,
|
|
8856
9832
|
type: "text"
|
|
8857
9833
|
});
|
|
@@ -8913,7 +9889,7 @@ Skip if: work was already started with a card reference, or no matching card exi
|
|
|
8913
9889
|
- \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
|
|
8914
9890
|
`;
|
|
8915
9891
|
files.push({
|
|
8916
|
-
path:
|
|
9892
|
+
path: join9(cwd, "AGENTS.md"),
|
|
8917
9893
|
content: agentsContent,
|
|
8918
9894
|
type: "text"
|
|
8919
9895
|
});
|
|
@@ -8930,17 +9906,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent
|
|
|
8930
9906
|
`;
|
|
8931
9907
|
if (installMode === "global") {
|
|
8932
9908
|
files.push({
|
|
8933
|
-
path:
|
|
9909
|
+
path: join9(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
8934
9910
|
content: promptContent,
|
|
8935
9911
|
type: "text"
|
|
8936
9912
|
});
|
|
8937
9913
|
symlinks.push({
|
|
8938
|
-
target:
|
|
8939
|
-
link:
|
|
9914
|
+
target: join9(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
9915
|
+
link: join9(home, ".codex", "prompts", "hmy.md")
|
|
8940
9916
|
});
|
|
8941
9917
|
} else {
|
|
8942
9918
|
files.push({
|
|
8943
|
-
path:
|
|
9919
|
+
path: join9(home, ".codex", "prompts", "hmy.md"),
|
|
8944
9920
|
content: promptContent,
|
|
8945
9921
|
type: "text"
|
|
8946
9922
|
});
|
|
@@ -8952,7 +9928,7 @@ command = "npx"
|
|
|
8952
9928
|
args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
8953
9929
|
`;
|
|
8954
9930
|
files.push({
|
|
8955
|
-
path:
|
|
9931
|
+
path: join9(home, ".codex", "config.toml"),
|
|
8956
9932
|
content: tomlContent,
|
|
8957
9933
|
type: "toml",
|
|
8958
9934
|
tomlSection: "mcp_servers.harmony"
|
|
@@ -8961,7 +9937,7 @@ args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
|
8961
9937
|
}
|
|
8962
9938
|
case "cursor": {
|
|
8963
9939
|
files.push({
|
|
8964
|
-
path:
|
|
9940
|
+
path: join9(cwd, ".cursor", "mcp.json"),
|
|
8965
9941
|
content: JSON.stringify({
|
|
8966
9942
|
mcpServers: {
|
|
8967
9943
|
harmony: {
|
|
@@ -8987,17 +9963,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
8987
9963
|
`;
|
|
8988
9964
|
if (installMode === "global") {
|
|
8989
9965
|
files.push({
|
|
8990
|
-
path:
|
|
9966
|
+
path: join9(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
8991
9967
|
content: ruleContent,
|
|
8992
9968
|
type: "text"
|
|
8993
9969
|
});
|
|
8994
9970
|
symlinks.push({
|
|
8995
|
-
target:
|
|
8996
|
-
link:
|
|
9971
|
+
target: join9(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
9972
|
+
link: join9(home, ".cursor", "rules", "harmony.mdc")
|
|
8997
9973
|
});
|
|
8998
9974
|
} else {
|
|
8999
9975
|
files.push({
|
|
9000
|
-
path:
|
|
9976
|
+
path: join9(cwd, ".cursor", "rules", "harmony.mdc"),
|
|
9001
9977
|
content: ruleContent,
|
|
9002
9978
|
type: "text"
|
|
9003
9979
|
});
|
|
@@ -9006,7 +9982,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
9006
9982
|
}
|
|
9007
9983
|
case "windsurf": {
|
|
9008
9984
|
files.push({
|
|
9009
|
-
path:
|
|
9985
|
+
path: join9(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
9010
9986
|
content: JSON.stringify({
|
|
9011
9987
|
mcpServers: {
|
|
9012
9988
|
harmony: {
|
|
@@ -9032,17 +10008,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
9032
10008
|
`;
|
|
9033
10009
|
if (installMode === "global") {
|
|
9034
10010
|
files.push({
|
|
9035
|
-
path:
|
|
10011
|
+
path: join9(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
9036
10012
|
content: ruleContent,
|
|
9037
10013
|
type: "text"
|
|
9038
10014
|
});
|
|
9039
10015
|
symlinks.push({
|
|
9040
|
-
target:
|
|
9041
|
-
link:
|
|
10016
|
+
target: join9(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
10017
|
+
link: join9(home, ".codeium", "windsurf", "rules", "harmony.md")
|
|
9042
10018
|
});
|
|
9043
10019
|
} else {
|
|
9044
10020
|
files.push({
|
|
9045
|
-
path:
|
|
10021
|
+
path: join9(cwd, ".windsurf", "rules", "harmony.md"),
|
|
9046
10022
|
content: ruleContent,
|
|
9047
10023
|
type: "text"
|
|
9048
10024
|
});
|
|
@@ -9054,7 +10030,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
9054
10030
|
}
|
|
9055
10031
|
async function runSetup(options = {}) {
|
|
9056
10032
|
const cwd = process.cwd();
|
|
9057
|
-
const home =
|
|
10033
|
+
const home = homedir7();
|
|
9058
10034
|
console.clear();
|
|
9059
10035
|
console.log(messages.header());
|
|
9060
10036
|
const assumeYes = shouldAssumeYes(options.yes, process.stdin.isTTY);
|
|
@@ -9552,8 +10528,8 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9552
10528
|
for (const symlink of allSymlinks) {
|
|
9553
10529
|
try {
|
|
9554
10530
|
const linkDir = dirname4(symlink.link);
|
|
9555
|
-
if (!
|
|
9556
|
-
|
|
10531
|
+
if (!existsSync9(linkDir)) {
|
|
10532
|
+
mkdirSync6(linkDir, { recursive: true });
|
|
9557
10533
|
}
|
|
9558
10534
|
let linkExists = false;
|
|
9559
10535
|
try {
|
|
@@ -9562,7 +10538,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9562
10538
|
} catch {}
|
|
9563
10539
|
if (linkExists) {
|
|
9564
10540
|
if (options.force) {
|
|
9565
|
-
|
|
10541
|
+
unlinkSync2(symlink.link);
|
|
9566
10542
|
} else {
|
|
9567
10543
|
continue;
|
|
9568
10544
|
}
|
|
@@ -9581,7 +10557,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9581
10557
|
} else {
|
|
9582
10558
|
try {
|
|
9583
10559
|
await writeMcpConfigFallback(home);
|
|
9584
|
-
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(
|
|
10560
|
+
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join9(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
|
|
9585
10561
|
} catch {
|
|
9586
10562
|
p4.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
|
|
9587
10563
|
}
|
|
@@ -9602,7 +10578,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
9602
10578
|
try {
|
|
9603
10579
|
const result = await allowlistHarmonyTools(home, allowAll);
|
|
9604
10580
|
const scope = allowAll ? "all tools" : "safe tools";
|
|
9605
|
-
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)`)}`);
|
|
9606
10582
|
} catch {
|
|
9607
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.");
|
|
9608
10584
|
}
|
|
@@ -9783,4 +10759,59 @@ program.command("setup").description("Setup wizard for Harmony MCP (recommended)
|
|
|
9783
10759
|
yes: options.yes
|
|
9784
10760
|
});
|
|
9785
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
|
+
});
|
|
9786
10817
|
program.parse();
|