@cleocode/adapters 2026.10.3 → 2026.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/heavy-command-hook.d.ts +12 -0
- package/dist/heavy-command-hook.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +647 -49
- package/dist/index.js.map +3 -3
- package/dist/providers/codex/install.d.ts.map +1 -1
- package/dist/providers/shared/heavy-command-hook-delivery.d.ts +97 -0
- package/dist/providers/shared/heavy-command-hook-delivery.d.ts.map +1 -0
- package/dist/providers/shared/heavy-command-hook-install.d.ts +153 -7
- package/dist/providers/shared/heavy-command-hook-install.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/heavy-command-hook.ts +21 -0
- package/src/index.ts +12 -0
- package/src/providers/codex/install.ts +3 -4
- package/src/providers/shared/__tests__/heavy-command-hook-delivery.test.ts +580 -0
- package/src/providers/shared/heavy-command-hook-delivery.ts +620 -0
- package/src/providers/shared/heavy-command-hook-install.ts +395 -31
package/dist/index.js
CHANGED
|
@@ -18088,8 +18088,15 @@ var init_nexus_cloud = __esm({
|
|
|
18088
18088
|
nexusCloudProjectListItemSchema = z26.object({
|
|
18089
18089
|
...projectFields,
|
|
18090
18090
|
role: z26.string(),
|
|
18091
|
+
/**
|
|
18092
|
+
* The project name sealed with the project data key (`Project.encryptedName`), base64;
|
|
18093
|
+
* only a holder of the account key can open it (T13102).
|
|
18094
|
+
*/
|
|
18095
|
+
encryptedName: z26.string().nullable().optional(),
|
|
18091
18096
|
streamId: z26.string().optional(),
|
|
18092
18097
|
headSeq: z26.number().int().nonnegative().nullable().optional(),
|
|
18098
|
+
/** The stream's head snapshot; null when nothing was ever pushed (T13102: restorable or not). */
|
|
18099
|
+
headCheckpointId: z26.string().nullable().optional(),
|
|
18093
18100
|
openConflicts: z26.number().int().nonnegative().optional(),
|
|
18094
18101
|
replicas: z26.array(nexusCloudReplicaSchema).default([]),
|
|
18095
18102
|
devices: nexusCloudDeviceCountsSchema.optional(),
|
|
@@ -18235,6 +18242,13 @@ var init_nexus_cloud = __esm({
|
|
|
18235
18242
|
}
|
|
18236
18243
|
});
|
|
18237
18244
|
|
|
18245
|
+
// packages/contracts/src/nexus-first-run.ts
|
|
18246
|
+
var init_nexus_first_run = __esm({
|
|
18247
|
+
"packages/contracts/src/nexus-first-run.ts"() {
|
|
18248
|
+
"use strict";
|
|
18249
|
+
}
|
|
18250
|
+
});
|
|
18251
|
+
|
|
18238
18252
|
// packages/contracts/src/nexus-vault.ts
|
|
18239
18253
|
import { z as z27 } from "zod";
|
|
18240
18254
|
var isoTime2, uuid, nexusUserKeysSchema, nexusProjectKeysSchema, nexusLeaseSchema, nexusLeaseAcquireSchema, nexusStreamHeadSchema, nexusActivityEventSchema, nexusActivityPageSchema;
|
|
@@ -18380,6 +18394,7 @@ var init_src = __esm({
|
|
|
18380
18394
|
init_decide_sites();
|
|
18381
18395
|
init_nexus_account();
|
|
18382
18396
|
init_nexus_cloud();
|
|
18397
|
+
init_nexus_first_run();
|
|
18383
18398
|
init_nexus_vault();
|
|
18384
18399
|
init_sync_schema();
|
|
18385
18400
|
init_task_id();
|
|
@@ -44960,6 +44975,7 @@ import {
|
|
|
44960
44975
|
readFileSync as readFileSync4,
|
|
44961
44976
|
realpathSync,
|
|
44962
44977
|
rmSync,
|
|
44978
|
+
statSync,
|
|
44963
44979
|
writeFileSync as writeFileSync2
|
|
44964
44980
|
} from "node:fs";
|
|
44965
44981
|
import { homedir as homedir4, tmpdir } from "node:os";
|
|
@@ -44972,6 +44988,25 @@ function canonical(path) {
|
|
|
44972
44988
|
return resolve2(path);
|
|
44973
44989
|
}
|
|
44974
44990
|
}
|
|
44991
|
+
function nonDirectoryAncestor(dir) {
|
|
44992
|
+
let cur = resolve2(dir);
|
|
44993
|
+
for (; ; ) {
|
|
44994
|
+
try {
|
|
44995
|
+
return statSync(cur).isDirectory() ? null : cur;
|
|
44996
|
+
} catch (err) {
|
|
44997
|
+
const code = err.code;
|
|
44998
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") return null;
|
|
44999
|
+
}
|
|
45000
|
+
const parent = dirname2(cur);
|
|
45001
|
+
if (parent === cur) return null;
|
|
45002
|
+
cur = parent;
|
|
45003
|
+
}
|
|
45004
|
+
}
|
|
45005
|
+
function ensureParentDir(target) {
|
|
45006
|
+
const blocked = nonDirectoryAncestor(dirname2(target));
|
|
45007
|
+
if (blocked !== null) throw new HeavyHookPathBlockedError(blocked, target);
|
|
45008
|
+
mkdirSync(dirname2(target), { recursive: true });
|
|
45009
|
+
}
|
|
44975
45010
|
function isUserHomeDir(projectDir, home = homedir4()) {
|
|
44976
45011
|
return canonical(projectDir) === canonical(home);
|
|
44977
45012
|
}
|
|
@@ -45083,7 +45118,7 @@ async function syncJsonHeavyCommandHook(configPath, provider, mode) {
|
|
|
45083
45118
|
});
|
|
45084
45119
|
return removed ? "removed" : "unchanged";
|
|
45085
45120
|
}
|
|
45086
|
-
|
|
45121
|
+
ensureParentDir(configPath);
|
|
45087
45122
|
clearOlderCleoMarkers();
|
|
45088
45123
|
let result = "unchanged";
|
|
45089
45124
|
await updateJsonConfigFile2(configPath, (config2) => {
|
|
@@ -45120,18 +45155,19 @@ function excludeFile(projectDir) {
|
|
|
45120
45155
|
if (path === null || path === "") return null;
|
|
45121
45156
|
return isAbsolute(path) ? path : join6(projectDir, path);
|
|
45122
45157
|
}
|
|
45123
|
-
function excludeLine(projectDir) {
|
|
45158
|
+
function excludeLine(projectDir, relPath) {
|
|
45124
45159
|
const prefix = gitOutput(projectDir, ["rev-parse", "--show-prefix"]) ?? "";
|
|
45125
|
-
return `/${prefix}${
|
|
45160
|
+
return `/${prefix}${relPath}`;
|
|
45126
45161
|
}
|
|
45127
|
-
function
|
|
45162
|
+
function excludeHookFileFromGit(projectDir, relPath, marker16 = HOOK_FILE_EXCLUDE_MARKER) {
|
|
45128
45163
|
const exclude = excludeFile(projectDir);
|
|
45129
45164
|
if (exclude === null) return false;
|
|
45130
|
-
if (gitOutput(projectDir, ["check-ignore",
|
|
45131
|
-
|
|
45165
|
+
if (gitOutput(projectDir, ["check-ignore", relPath]) !== null) return false;
|
|
45166
|
+
if (gitOutput(projectDir, ["ls-files", "--error-unmatch", relPath]) !== null) return false;
|
|
45167
|
+
const line = excludeLine(projectDir, relPath);
|
|
45132
45168
|
const text = existsSync5(exclude) ? readFileSync4(exclude, "utf-8") : "";
|
|
45133
45169
|
if (text.split("\n").includes(line)) return false;
|
|
45134
|
-
const block = `${
|
|
45170
|
+
const block = `${marker16}
|
|
45135
45171
|
${line}
|
|
45136
45172
|
`;
|
|
45137
45173
|
mkdirSync(dirname2(exclude), { recursive: true });
|
|
@@ -45139,16 +45175,150 @@ ${line}
|
|
|
45139
45175
|
${block}`);
|
|
45140
45176
|
return true;
|
|
45141
45177
|
}
|
|
45142
|
-
function
|
|
45178
|
+
function unexcludeHookFileFromGit(projectDir, relPath) {
|
|
45143
45179
|
const exclude = excludeFile(projectDir);
|
|
45144
45180
|
if (exclude === null || !existsSync5(exclude)) return false;
|
|
45145
45181
|
const lines = readFileSync4(exclude, "utf-8").split("\n");
|
|
45146
|
-
const at = lines.indexOf(excludeLine(projectDir));
|
|
45147
|
-
|
|
45182
|
+
const at = lines.indexOf(excludeLine(projectDir, relPath));
|
|
45183
|
+
const above = lines[at - 1];
|
|
45184
|
+
if (at < 1 || above !== LOCAL_SETTINGS_EXCLUDE_MARKER && above !== HOOK_FILE_EXCLUDE_MARKER) {
|
|
45185
|
+
return false;
|
|
45186
|
+
}
|
|
45148
45187
|
lines.splice(at - 1, 2);
|
|
45149
45188
|
writeFileSync2(exclude, lines.join("\n"));
|
|
45150
45189
|
return true;
|
|
45151
45190
|
}
|
|
45191
|
+
function hookFileVisibleToGit(projectDir, relPath) {
|
|
45192
|
+
if (gitOutput(projectDir, ["rev-parse", "--is-inside-work-tree"]) !== "true") return false;
|
|
45193
|
+
if (gitOutput(projectDir, ["check-ignore", relPath]) !== null) return false;
|
|
45194
|
+
return gitOutput(projectDir, ["ls-files", "--error-unmatch", relPath]) === null;
|
|
45195
|
+
}
|
|
45196
|
+
function excludeLocalSettingsFromGit(projectDir) {
|
|
45197
|
+
return excludeHookFileFromGit(projectDir, CLAUDE_LOCAL_SETTINGS, LOCAL_SETTINGS_EXCLUDE_MARKER);
|
|
45198
|
+
}
|
|
45199
|
+
function unexcludeLocalSettingsFromGit(projectDir) {
|
|
45200
|
+
return unexcludeHookFileFromGit(projectDir, CLAUDE_LOCAL_SETTINGS);
|
|
45201
|
+
}
|
|
45202
|
+
function cleoCreatedConfig(config2) {
|
|
45203
|
+
if (!isPlainObject2(config2) || Object.keys(config2).some((k) => k !== "hooks")) return false;
|
|
45204
|
+
if (!isPlainObject2(config2.hooks)) return false;
|
|
45205
|
+
let mine = 0;
|
|
45206
|
+
for (const groups of Object.values(config2.hooks)) {
|
|
45207
|
+
if (!Array.isArray(groups)) return false;
|
|
45208
|
+
for (const group of groups) {
|
|
45209
|
+
if (!isPlainObject2(group) || !Array.isArray(group.hooks)) return false;
|
|
45210
|
+
for (const hook of group.hooks) {
|
|
45211
|
+
if (!isPlainObject2(hook) || typeof hook.command !== "string" || !hook.command.includes(CLEO_HOOK_MARKER)) {
|
|
45212
|
+
return false;
|
|
45213
|
+
}
|
|
45214
|
+
mine++;
|
|
45215
|
+
}
|
|
45216
|
+
}
|
|
45217
|
+
}
|
|
45218
|
+
return mine > 0;
|
|
45219
|
+
}
|
|
45220
|
+
function codexHooksSharedReason(projectDir, checkGit = true) {
|
|
45221
|
+
const file2 = join6(projectDir, CODEX_HOOKS_FILE);
|
|
45222
|
+
if (checkGit && gitOutput(projectDir, ["ls-files", "--error-unmatch", CODEX_HOOKS_FILE]) !== null) {
|
|
45223
|
+
return "is tracked by git (a shared team config)";
|
|
45224
|
+
}
|
|
45225
|
+
if (!existsSync5(file2)) return null;
|
|
45226
|
+
let config2;
|
|
45227
|
+
try {
|
|
45228
|
+
config2 = JSON.parse(readFileSync4(file2, "utf-8"));
|
|
45229
|
+
} catch {
|
|
45230
|
+
return "is the project's own Codex hook config (empty or not valid JSON)";
|
|
45231
|
+
}
|
|
45232
|
+
return cleoCreatedConfig(config2) ? null : "is the project's own Codex hook config";
|
|
45233
|
+
}
|
|
45234
|
+
function committedCodexHookHoldsCleo(projectDir) {
|
|
45235
|
+
const committed = gitOutput(projectDir, ["show", `HEAD:./${CODEX_HOOKS_FILE}`]);
|
|
45236
|
+
if (committed === null || committed === "") return false;
|
|
45237
|
+
try {
|
|
45238
|
+
const config2 = JSON.parse(committed);
|
|
45239
|
+
if (!isPlainObject2(config2)) return false;
|
|
45240
|
+
const groups = isPlainObject2(config2.hooks) ? config2.hooks.PreToolUse : void 0;
|
|
45241
|
+
return Array.isArray(groups) && groups.some(
|
|
45242
|
+
(g) => isPlainObject2(g) && Array.isArray(g.hooks) && g.hooks.some(isHeavyHookObject)
|
|
45243
|
+
);
|
|
45244
|
+
} catch {
|
|
45245
|
+
return false;
|
|
45246
|
+
}
|
|
45247
|
+
}
|
|
45248
|
+
function jsonEqual(a, b) {
|
|
45249
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
45250
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
|
|
45251
|
+
}
|
|
45252
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
45253
|
+
const keys = Object.keys(a);
|
|
45254
|
+
return keys.length === Object.keys(b).length && keys.every((k) => Object.hasOwn(b, k) && jsonEqual(a[k], b[k]));
|
|
45255
|
+
}
|
|
45256
|
+
return a === b;
|
|
45257
|
+
}
|
|
45258
|
+
function restoreCommittedCodexHooks(projectDir) {
|
|
45259
|
+
if (gitOutput(projectDir, ["ls-files", "--error-unmatch", CODEX_HOOKS_FILE]) === null)
|
|
45260
|
+
return false;
|
|
45261
|
+
const file2 = join6(projectDir, CODEX_HOOKS_FILE);
|
|
45262
|
+
let committed;
|
|
45263
|
+
let working;
|
|
45264
|
+
try {
|
|
45265
|
+
committed = execFileSync("git", ["-C", projectDir, "show", `HEAD:./${CODEX_HOOKS_FILE}`], {
|
|
45266
|
+
encoding: "utf-8",
|
|
45267
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
45268
|
+
timeout: 5e3
|
|
45269
|
+
});
|
|
45270
|
+
working = JSON.parse(readFileSync4(file2, "utf-8"));
|
|
45271
|
+
} catch {
|
|
45272
|
+
return false;
|
|
45273
|
+
}
|
|
45274
|
+
if (!isPlainObject2(working)) return false;
|
|
45275
|
+
const hooks = hookMap(working);
|
|
45276
|
+
if (!placeHeavyHook(hooks, null).changed) return false;
|
|
45277
|
+
try {
|
|
45278
|
+
if (!jsonEqual(working, JSON.parse(committed))) return false;
|
|
45279
|
+
} catch {
|
|
45280
|
+
return false;
|
|
45281
|
+
}
|
|
45282
|
+
writeFileSync2(file2, committed);
|
|
45283
|
+
return true;
|
|
45284
|
+
}
|
|
45285
|
+
function excludedByCleo(projectDir, relPath) {
|
|
45286
|
+
const exclude = excludeFile(projectDir);
|
|
45287
|
+
if (exclude === null || !existsSync5(exclude)) return false;
|
|
45288
|
+
const lines = readFileSync4(exclude, "utf-8").split("\n");
|
|
45289
|
+
const at = lines.indexOf(excludeLine(projectDir, relPath));
|
|
45290
|
+
const above = lines[at - 1];
|
|
45291
|
+
return at >= 1 && (above === LOCAL_SETTINGS_EXCLUDE_MARKER || above === HOOK_FILE_EXCLUDE_MARKER);
|
|
45292
|
+
}
|
|
45293
|
+
function codexHookSnippet() {
|
|
45294
|
+
return JSON.stringify(heavyCommandHookEntry("codex"));
|
|
45295
|
+
}
|
|
45296
|
+
function trackedFileModified(projectDir, relPath) {
|
|
45297
|
+
if (gitOutput(projectDir, ["ls-files", "--error-unmatch", relPath]) === null) return false;
|
|
45298
|
+
return (gitOutput(projectDir, ["diff", "--name-only", "HEAD", "--", relPath]) ?? "") !== "";
|
|
45299
|
+
}
|
|
45300
|
+
async function syncCodexHeavyCommandHook(projectDir, mode) {
|
|
45301
|
+
const file2 = join6(projectDir, CODEX_HOOKS_FILE);
|
|
45302
|
+
if (mode !== "off") {
|
|
45303
|
+
const shared = codexHooksSharedReason(projectDir);
|
|
45304
|
+
if (shared !== null) {
|
|
45305
|
+
unexcludeHookFileFromGit(projectDir, CODEX_HOOKS_FILE);
|
|
45306
|
+
throw new HeavyHookSharedConfigError(file2, shared);
|
|
45307
|
+
}
|
|
45308
|
+
} else if (committedCodexHookHoldsCleo(projectDir)) {
|
|
45309
|
+
throw new HeavyHookSharedConfigError(
|
|
45310
|
+
file2,
|
|
45311
|
+
"holds CLEO's hook as the team committed it (removing it is a team change)"
|
|
45312
|
+
);
|
|
45313
|
+
} else if (restoreCommittedCodexHooks(projectDir)) {
|
|
45314
|
+
unexcludeHookFileFromGit(projectDir, CODEX_HOOKS_FILE);
|
|
45315
|
+
return "removed";
|
|
45316
|
+
}
|
|
45317
|
+
const result = await syncJsonHeavyCommandHook(file2, "codex", mode);
|
|
45318
|
+
if (mode === "off") unexcludeHookFileFromGit(projectDir, CODEX_HOOKS_FILE);
|
|
45319
|
+
else excludeHookFileFromGit(projectDir, CODEX_HOOKS_FILE);
|
|
45320
|
+
return result;
|
|
45321
|
+
}
|
|
45152
45322
|
async function syncClaudeCodeHeavyCommandHook(projectDir, mode) {
|
|
45153
45323
|
const result = await syncJsonHeavyCommandHook(
|
|
45154
45324
|
join6(projectDir, CLAUDE_LOCAL_SETTINGS),
|
|
@@ -45157,11 +45327,14 @@ async function syncClaudeCodeHeavyCommandHook(projectDir, mode) {
|
|
|
45157
45327
|
);
|
|
45158
45328
|
if (mode === "off") unexcludeLocalSettingsFromGit(projectDir);
|
|
45159
45329
|
else excludeLocalSettingsFromGit(projectDir);
|
|
45160
|
-
|
|
45161
|
-
|
|
45162
|
-
|
|
45163
|
-
|
|
45164
|
-
|
|
45330
|
+
try {
|
|
45331
|
+
await syncJsonHeavyCommandHook(
|
|
45332
|
+
join6(projectDir, ".claude", "settings.json"),
|
|
45333
|
+
"claude-code",
|
|
45334
|
+
"off"
|
|
45335
|
+
);
|
|
45336
|
+
} catch {
|
|
45337
|
+
}
|
|
45165
45338
|
return result;
|
|
45166
45339
|
}
|
|
45167
45340
|
function opencodeHeavyCommandPluginSource() {
|
|
@@ -45217,20 +45390,24 @@ function opencodeHeavyCommandPluginSource() {
|
|
|
45217
45390
|
].join("\n");
|
|
45218
45391
|
}
|
|
45219
45392
|
function syncOpencodeHeavyCommandPlugin(projectDir, mode) {
|
|
45220
|
-
const pluginPath = join6(projectDir,
|
|
45393
|
+
const pluginPath = join6(projectDir, OPENCODE_PLUGIN_FILE);
|
|
45221
45394
|
const exists = existsSync5(pluginPath);
|
|
45222
45395
|
if (mode === "off") {
|
|
45396
|
+
unexcludeHookFileFromGit(projectDir, OPENCODE_PLUGIN_FILE);
|
|
45223
45397
|
if (!exists) return "unchanged";
|
|
45224
45398
|
rmSync(pluginPath, { force: true });
|
|
45225
45399
|
return "removed";
|
|
45226
45400
|
}
|
|
45227
45401
|
const source = opencodeHeavyCommandPluginSource();
|
|
45228
|
-
|
|
45229
|
-
|
|
45230
|
-
|
|
45231
|
-
|
|
45402
|
+
const current = exists && readFileSync4(pluginPath, "utf-8") === source;
|
|
45403
|
+
if (!current) {
|
|
45404
|
+
ensureParentDir(pluginPath);
|
|
45405
|
+
writeFileSync2(pluginPath, source, "utf-8");
|
|
45406
|
+
}
|
|
45407
|
+
excludeHookFileFromGit(projectDir, OPENCODE_PLUGIN_FILE);
|
|
45408
|
+
return current ? "unchanged" : exists ? "updated" : "installed";
|
|
45232
45409
|
}
|
|
45233
|
-
var HEAVY_COMMAND_HOOK_ID, HEAVY_COMMAND_HOOK_TIMEOUT_SEC, OPENCODE_HEAVY_COMMAND_PLUGIN, HEAVY_TOOL_WORDS, OLDER_CLEO_HOOK_ANSWER, OLDER_CLEO_MARKER_PREFIX, CLAUDE_LOCAL_SETTINGS, LOCAL_SETTINGS_EXCLUDE_MARKER;
|
|
45410
|
+
var HEAVY_COMMAND_HOOK_ID, HEAVY_COMMAND_HOOK_TIMEOUT_SEC, OPENCODE_HEAVY_COMMAND_PLUGIN, HEAVY_TOOL_WORDS, HeavyHookPathBlockedError, OLDER_CLEO_HOOK_ANSWER, OLDER_CLEO_MARKER_PREFIX, CLAUDE_LOCAL_SETTINGS, LOCAL_SETTINGS_EXCLUDE_MARKER, HOOK_FILE_EXCLUDE_MARKER, CODEX_HOOKS_FILE, OPENCODE_PLUGIN_FILE, HeavyHookSharedConfigError;
|
|
45234
45411
|
var init_heavy_command_hook_install = __esm({
|
|
45235
45412
|
"packages/adapters/src/providers/shared/heavy-command-hook-install.ts"() {
|
|
45236
45413
|
"use strict";
|
|
@@ -45270,6 +45447,19 @@ var init_heavy_command_hook_install = __esm({
|
|
|
45270
45447
|
"cargo",
|
|
45271
45448
|
"go"
|
|
45272
45449
|
]);
|
|
45450
|
+
HeavyHookPathBlockedError = class extends Error {
|
|
45451
|
+
/** The existing path that is not a directory. */
|
|
45452
|
+
path;
|
|
45453
|
+
/**
|
|
45454
|
+
* @param path - the existing non-directory path.
|
|
45455
|
+
* @param target - the config file that would have lived under it.
|
|
45456
|
+
*/
|
|
45457
|
+
constructor(path, target) {
|
|
45458
|
+
super(`${path} exists but is not a directory, so ${target} cannot be created under it`);
|
|
45459
|
+
this.name = "HeavyHookPathBlockedError";
|
|
45460
|
+
this.path = path;
|
|
45461
|
+
}
|
|
45462
|
+
};
|
|
45273
45463
|
OLDER_CLEO_HOOK_ANSWER = JSON.stringify({
|
|
45274
45464
|
hookSpecificOutput: {
|
|
45275
45465
|
hookEventName: "PreToolUse",
|
|
@@ -45279,11 +45469,27 @@ var init_heavy_command_hook_install = __esm({
|
|
|
45279
45469
|
OLDER_CLEO_MARKER_PREFIX = "cleo-hook-unsupported-";
|
|
45280
45470
|
CLAUDE_LOCAL_SETTINGS = ".claude/settings.local.json";
|
|
45281
45471
|
LOCAL_SETTINGS_EXCLUDE_MARKER = "# cleo-hook: keep the per-machine heavy-command hook settings out of git (T12983)";
|
|
45472
|
+
HOOK_FILE_EXCLUDE_MARKER = "# cleo-hook: keep a per-machine heavy-command hook file out of git (T13124)";
|
|
45473
|
+
CODEX_HOOKS_FILE = ".codex/hooks.json";
|
|
45474
|
+
OPENCODE_PLUGIN_FILE = `.opencode/plugins/${OPENCODE_HEAVY_COMMAND_PLUGIN}`;
|
|
45475
|
+
HeavyHookSharedConfigError = class extends Error {
|
|
45476
|
+
/** The project's own config file. */
|
|
45477
|
+
path;
|
|
45478
|
+
/**
|
|
45479
|
+
* @param path - the config file.
|
|
45480
|
+
* @param why - what makes it the project's own.
|
|
45481
|
+
*/
|
|
45482
|
+
constructor(path, why) {
|
|
45483
|
+
super(`${path} ${why}, so CLEO does not write or hide it`);
|
|
45484
|
+
this.name = "HeavyHookSharedConfigError";
|
|
45485
|
+
this.path = path;
|
|
45486
|
+
}
|
|
45487
|
+
};
|
|
45282
45488
|
}
|
|
45283
45489
|
});
|
|
45284
45490
|
|
|
45285
45491
|
// packages/adapters/src/providers/shared/hook-template-installer.ts
|
|
45286
|
-
import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, statSync } from "node:fs";
|
|
45492
|
+
import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, statSync as statSync2 } from "node:fs";
|
|
45287
45493
|
import { dirname as dirname3, join as join7 } from "node:path";
|
|
45288
45494
|
import { fileURLToPath } from "node:url";
|
|
45289
45495
|
function resolveProviderCandidates() {
|
|
@@ -45311,8 +45517,8 @@ function copyTemplate(src, dest, force) {
|
|
|
45311
45517
|
throw new Error(`CLEO hook template missing at source: ${src}`);
|
|
45312
45518
|
}
|
|
45313
45519
|
if (!force && existsSync6(dest)) {
|
|
45314
|
-
const srcStat =
|
|
45315
|
-
const destStat =
|
|
45520
|
+
const srcStat = statSync2(src);
|
|
45521
|
+
const destStat = statSync2(dest);
|
|
45316
45522
|
if (srcStat.size === destStat.size && srcStat.mtimeMs <= destStat.mtimeMs) {
|
|
45317
45523
|
return { wrote: false };
|
|
45318
45524
|
}
|
|
@@ -47781,7 +47987,7 @@ var init_hooks4 = __esm({
|
|
|
47781
47987
|
});
|
|
47782
47988
|
|
|
47783
47989
|
// packages/adapters/src/providers/pi/install.ts
|
|
47784
|
-
import { join as
|
|
47990
|
+
import { join as join29 } from "node:path";
|
|
47785
47991
|
import { ensureProviderInstructionFile as ensureProviderInstructionFile7 } from "@cleocode/caamp";
|
|
47786
47992
|
function getCleoReferences() {
|
|
47787
47993
|
return [`@${getCleoTemplatesTildePath()}/CLEO-INJECTION.md`, "@.cleo/memory-bridge.md"];
|
|
@@ -47808,7 +48014,7 @@ var init_install4 = __esm({
|
|
|
47808
48014
|
});
|
|
47809
48015
|
const projectUpdated = projectResult.action !== "intact";
|
|
47810
48016
|
if (projectUpdated) {
|
|
47811
|
-
details.instructionFile =
|
|
48017
|
+
details.instructionFile = join29(projectDir, projectResult.instructFile);
|
|
47812
48018
|
}
|
|
47813
48019
|
let globalUpdated = false;
|
|
47814
48020
|
try {
|
|
@@ -47879,7 +48085,7 @@ var init_install4 = __esm({
|
|
|
47879
48085
|
});
|
|
47880
48086
|
|
|
47881
48087
|
// packages/adapters/src/providers/pi/spawn.ts
|
|
47882
|
-
import { findOnPath as
|
|
48088
|
+
import { findOnPath as findOnPath12 } from "@cleocode/paths";
|
|
47883
48089
|
function getPiCliPath() {
|
|
47884
48090
|
return process.env["PI_CLI_PATH"] ?? "pi";
|
|
47885
48091
|
}
|
|
@@ -47902,7 +48108,7 @@ var init_spawn4 = __esm({
|
|
|
47902
48108
|
*/
|
|
47903
48109
|
async canSpawn() {
|
|
47904
48110
|
const cliPath = getPiCliPath();
|
|
47905
|
-
return
|
|
48111
|
+
return findOnPath12(cliPath) !== null;
|
|
47906
48112
|
}
|
|
47907
48113
|
/**
|
|
47908
48114
|
* Spawn a subagent via Pi CLI.
|
|
@@ -48013,22 +48219,22 @@ var init_spawn4 = __esm({
|
|
|
48013
48219
|
});
|
|
48014
48220
|
|
|
48015
48221
|
// packages/adapters/src/providers/pi/adapter.ts
|
|
48016
|
-
import { existsSync as
|
|
48017
|
-
import { homedir as
|
|
48018
|
-
import { join as
|
|
48019
|
-
import { findOnPath as
|
|
48222
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
48223
|
+
import { homedir as homedir14 } from "node:os";
|
|
48224
|
+
import { join as join30 } from "node:path";
|
|
48225
|
+
import { findOnPath as findOnPath13 } from "@cleocode/paths";
|
|
48020
48226
|
function getPiAgentDir() {
|
|
48021
48227
|
const env = process.env["PI_CODING_AGENT_DIR"];
|
|
48022
48228
|
if (env !== void 0 && env.length > 0) {
|
|
48023
|
-
if (env === "~") return
|
|
48024
|
-
if (env.startsWith("~/")) return
|
|
48229
|
+
if (env === "~") return homedir14();
|
|
48230
|
+
if (env.startsWith("~/")) return join30(homedir14(), env.slice(2));
|
|
48025
48231
|
return env;
|
|
48026
48232
|
}
|
|
48027
48233
|
const piHome = process.env["PI_HOME"];
|
|
48028
48234
|
if (piHome !== void 0 && piHome.length > 0) {
|
|
48029
|
-
return
|
|
48235
|
+
return join30(piHome, "agent");
|
|
48030
48236
|
}
|
|
48031
|
-
return
|
|
48237
|
+
return join30(homedir14(), ".pi", "agent");
|
|
48032
48238
|
}
|
|
48033
48239
|
var PiAdapter;
|
|
48034
48240
|
var init_adapter4 = __esm({
|
|
@@ -48131,17 +48337,17 @@ var init_adapter4 = __esm({
|
|
|
48131
48337
|
}
|
|
48132
48338
|
let cliAvailable = false;
|
|
48133
48339
|
const cliPath = process.env["PI_CLI_PATH"] ?? "pi";
|
|
48134
|
-
const resolvedCli =
|
|
48340
|
+
const resolvedCli = findOnPath13(cliPath);
|
|
48135
48341
|
cliAvailable = resolvedCli !== null;
|
|
48136
48342
|
if (resolvedCli) details.cliPath = cliPath === "pi" ? resolvedCli : cliPath;
|
|
48137
48343
|
else details.cliAvailable = false;
|
|
48138
48344
|
const agentDir = getPiAgentDir();
|
|
48139
|
-
const agentDirExists =
|
|
48345
|
+
const agentDirExists = existsSync21(agentDir);
|
|
48140
48346
|
details.agentDirExists = agentDirExists;
|
|
48141
48347
|
details.agentDir = agentDir;
|
|
48142
48348
|
if (this.projectDir) {
|
|
48143
|
-
const projectPiDir =
|
|
48144
|
-
details.projectPiDirExists =
|
|
48349
|
+
const projectPiDir = join30(this.projectDir, ".pi");
|
|
48350
|
+
details.projectPiDirExists = existsSync21(projectPiDir);
|
|
48145
48351
|
}
|
|
48146
48352
|
details.piCodingAgentDirSet = process.env["PI_CODING_AGENT_DIR"] !== void 0;
|
|
48147
48353
|
details.piHomeSet = process.env["PI_HOME"] !== void 0;
|
|
@@ -48512,9 +48718,8 @@ var CodexInstallProvider = class {
|
|
|
48512
48718
|
details.heavyCommandHook = "skipped";
|
|
48513
48719
|
} else if (options.heavyCommandHook !== void 0) {
|
|
48514
48720
|
try {
|
|
48515
|
-
details.heavyCommandHook = await
|
|
48516
|
-
|
|
48517
|
-
"codex",
|
|
48721
|
+
details.heavyCommandHook = await syncCodexHeavyCommandHook(
|
|
48722
|
+
projectDir,
|
|
48518
48723
|
options.heavyCommandHook
|
|
48519
48724
|
);
|
|
48520
48725
|
} catch (err) {
|
|
@@ -49244,20 +49449,405 @@ function createAdapter6() {
|
|
|
49244
49449
|
|
|
49245
49450
|
// packages/adapters/src/index.ts
|
|
49246
49451
|
init_opencode();
|
|
49452
|
+
|
|
49453
|
+
// packages/adapters/src/providers/shared/heavy-command-hook-delivery.ts
|
|
49454
|
+
init_heavy_command_hook_install();
|
|
49455
|
+
init_hook_config();
|
|
49456
|
+
import { execFileSync as execFileSync2, spawnSync as spawnSync3 } from "node:child_process";
|
|
49457
|
+
import { existsSync as existsSync20, lstatSync, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
|
|
49458
|
+
import { homedir as homedir13 } from "node:os";
|
|
49459
|
+
import { dirname as dirname6, join as join28 } from "node:path";
|
|
49460
|
+
import { JsonConfigParseError } from "@cleocode/caamp";
|
|
49461
|
+
import { findOnPath as findOnPath11 } from "@cleocode/paths";
|
|
49462
|
+
var HEAVY_HOOK_DELIVERY_PROVIDERS = Object.freeze([
|
|
49463
|
+
"claude-code",
|
|
49464
|
+
"codex",
|
|
49465
|
+
"opencode",
|
|
49466
|
+
"kimi"
|
|
49467
|
+
]);
|
|
49468
|
+
var HEAVY_HOOK_FIX_COMMAND = "cleo doctor heavy-command-hook --fix";
|
|
49469
|
+
var CODEX_SHARED_REMEDY = "if the team wants Codex governed here, add CLEO's PreToolUse entry (the snippet) to .codex/hooks.json yourself and commit it deliberately; CLEO never writes or hides a project's own hook config";
|
|
49470
|
+
var KIMI_REASON = "Kimi reads hooks only from its global ~/.kimi/config.toml, and CLEO never writes user-global configs";
|
|
49471
|
+
var KIMI_REMEDY = "add a [[hooks]] entry for PreToolUse that runs `cleo hook heavy-command --provider kimi` to ~/.kimi/config.toml yourself";
|
|
49472
|
+
function homeOf(env) {
|
|
49473
|
+
return env.HOME || homedir13();
|
|
49474
|
+
}
|
|
49475
|
+
function heavyHookTarget(provider, projectDir, env = process.env) {
|
|
49476
|
+
switch (provider) {
|
|
49477
|
+
case "claude-code":
|
|
49478
|
+
return join28(projectDir, CLAUDE_LOCAL_SETTINGS);
|
|
49479
|
+
case "codex":
|
|
49480
|
+
return join28(projectDir, CODEX_HOOKS_FILE);
|
|
49481
|
+
case "opencode":
|
|
49482
|
+
return join28(projectDir, OPENCODE_PLUGIN_FILE);
|
|
49483
|
+
case "kimi":
|
|
49484
|
+
return join28(homeOf(env), ".kimi", "config.toml");
|
|
49485
|
+
}
|
|
49486
|
+
}
|
|
49487
|
+
function detectHeavyHookProvider(provider, projectDir, env = process.env) {
|
|
49488
|
+
const home = homeOf(env);
|
|
49489
|
+
const signals = provider === "claude-code" ? [
|
|
49490
|
+
[
|
|
49491
|
+
env.CLAUDECODE === "1" || Boolean(env.CLAUDE_CODE_ENTRYPOINT),
|
|
49492
|
+
"running inside Claude Code"
|
|
49493
|
+
],
|
|
49494
|
+
[() => existsSync20(join28(projectDir, ".claude")), "the project has .claude/"],
|
|
49495
|
+
[() => findOnPath11("claude", { env }) !== null, "`claude` is on PATH"],
|
|
49496
|
+
[() => existsSync20(env.CLAUDE_CONFIG_DIR || join28(home, ".claude")), "~/.claude exists"]
|
|
49497
|
+
] : provider === "codex" ? [
|
|
49498
|
+
[() => existsSync20(join28(projectDir, ".codex")), "the project has .codex"],
|
|
49499
|
+
[() => findOnPath11("codex", { env }) !== null, "`codex` is on PATH"],
|
|
49500
|
+
[() => existsSync20(env.CODEX_HOME || join28(home, ".codex")), "~/.codex exists"]
|
|
49501
|
+
] : provider === "opencode" ? [
|
|
49502
|
+
[() => existsSync20(join28(projectDir, ".opencode")), "the project has .opencode/"],
|
|
49503
|
+
[
|
|
49504
|
+
() => existsSync20(join28(projectDir, "opencode.json")) || existsSync20(join28(projectDir, "opencode.jsonc")),
|
|
49505
|
+
"the project has opencode.json"
|
|
49506
|
+
],
|
|
49507
|
+
[() => findOnPath11("opencode", { env }) !== null, "`opencode` is on PATH"],
|
|
49508
|
+
[
|
|
49509
|
+
() => existsSync20(join28(env.XDG_CONFIG_HOME || join28(home, ".config"), "opencode")),
|
|
49510
|
+
"~/.config/opencode exists"
|
|
49511
|
+
]
|
|
49512
|
+
] : [
|
|
49513
|
+
[() => findOnPath11("kimi", { env }) !== null, "`kimi` is on PATH"],
|
|
49514
|
+
[() => existsSync20(join28(home, ".kimi")), "~/.kimi exists"]
|
|
49515
|
+
];
|
|
49516
|
+
for (const [test, why] of signals) {
|
|
49517
|
+
if (typeof test === "boolean" ? test : test()) return { detected: true, why };
|
|
49518
|
+
}
|
|
49519
|
+
if (provider !== "kimi" && existsSync20(heavyHookTarget(provider, projectDir, env))) {
|
|
49520
|
+
return { detected: true, why: "its hook config exists" };
|
|
49521
|
+
}
|
|
49522
|
+
return {
|
|
49523
|
+
detected: false,
|
|
49524
|
+
why: `${provider} is not in use here (no config directory or CLI found)`
|
|
49525
|
+
};
|
|
49526
|
+
}
|
|
49527
|
+
function errorText(err) {
|
|
49528
|
+
return err instanceof Error ? err.message : String(err);
|
|
49529
|
+
}
|
|
49530
|
+
async function syncProvider(provider, projectDir, mode, env) {
|
|
49531
|
+
const target = heavyHookTarget(provider, projectDir, env);
|
|
49532
|
+
if (provider === "kimi") {
|
|
49533
|
+
return mode === "off" ? { provider, status: "unchanged", target } : { provider, status: "unsupported", target, reason: KIMI_REASON, remedy: KIMI_REMEDY };
|
|
49534
|
+
}
|
|
49535
|
+
try {
|
|
49536
|
+
const result = provider === "claude-code" ? await syncClaudeCodeHeavyCommandHook(projectDir, mode) : provider === "codex" ? await syncCodexHeavyCommandHook(projectDir, mode) : syncOpencodeHeavyCommandPlugin(projectDir, mode);
|
|
49537
|
+
return { provider, status: result, target };
|
|
49538
|
+
} catch (err) {
|
|
49539
|
+
if (err instanceof HeavyHookSharedConfigError) {
|
|
49540
|
+
return {
|
|
49541
|
+
provider,
|
|
49542
|
+
status: "needs-consent",
|
|
49543
|
+
target,
|
|
49544
|
+
reason: err.message,
|
|
49545
|
+
...mode === "off" ? {
|
|
49546
|
+
remedy: "the team committed CLEO's hook in .codex/hooks.json; remove it there in a team change if the team agrees"
|
|
49547
|
+
} : { remedy: CODEX_SHARED_REMEDY, snippet: codexHookSnippet() }
|
|
49548
|
+
};
|
|
49549
|
+
}
|
|
49550
|
+
if (err instanceof HeavyHookPathBlockedError) {
|
|
49551
|
+
return {
|
|
49552
|
+
provider,
|
|
49553
|
+
status: "blocked",
|
|
49554
|
+
target,
|
|
49555
|
+
reason: err.message,
|
|
49556
|
+
remedy: `remove or rename ${err.path} (it is not a directory), then run: ${HEAVY_HOOK_FIX_COMMAND}`
|
|
49557
|
+
};
|
|
49558
|
+
}
|
|
49559
|
+
return {
|
|
49560
|
+
provider,
|
|
49561
|
+
status: "failed",
|
|
49562
|
+
target,
|
|
49563
|
+
reason: errorText(err),
|
|
49564
|
+
remedy: err instanceof JsonConfigParseError ? `fix the JSON in ${err.filePath} (it was left untouched), then run: ${HEAVY_HOOK_FIX_COMMAND}` : `run: ${HEAVY_HOOK_FIX_COMMAND}`
|
|
49565
|
+
};
|
|
49566
|
+
}
|
|
49567
|
+
}
|
|
49568
|
+
async function syncProjectHeavyCommandHooks(projectDir, mode, options = {}) {
|
|
49569
|
+
const env = options.env ?? process.env;
|
|
49570
|
+
const providers = options.providers ?? HEAVY_HOOK_DELIVERY_PROVIDERS;
|
|
49571
|
+
const home = isUserHomeDir(projectDir, homeOf(env));
|
|
49572
|
+
const outcomes = [];
|
|
49573
|
+
for (const provider of providers) {
|
|
49574
|
+
const target = heavyHookTarget(provider, projectDir, env);
|
|
49575
|
+
if (home) {
|
|
49576
|
+
outcomes.push({
|
|
49577
|
+
provider,
|
|
49578
|
+
status: "skipped",
|
|
49579
|
+
target,
|
|
49580
|
+
reason: "the project is your home directory, whose provider configs are user-global"
|
|
49581
|
+
});
|
|
49582
|
+
continue;
|
|
49583
|
+
}
|
|
49584
|
+
const detection = detectHeavyHookProvider(provider, projectDir, env);
|
|
49585
|
+
if (mode !== "off" && !detection.detected) {
|
|
49586
|
+
outcomes.push({ provider, status: "skipped", target, reason: detection.why });
|
|
49587
|
+
continue;
|
|
49588
|
+
}
|
|
49589
|
+
outcomes.push(await syncProvider(provider, projectDir, mode, env));
|
|
49590
|
+
}
|
|
49591
|
+
return outcomes;
|
|
49592
|
+
}
|
|
49593
|
+
function jsonHookPresence(file2, provider) {
|
|
49594
|
+
let raw;
|
|
49595
|
+
try {
|
|
49596
|
+
raw = readFileSync12(file2, "utf-8");
|
|
49597
|
+
} catch (err) {
|
|
49598
|
+
const code = err.code;
|
|
49599
|
+
return code === "ENOENT" || code === "ENOTDIR" ? { kind: "absent" } : { kind: "unreadable", error: errorText(err) };
|
|
49600
|
+
}
|
|
49601
|
+
if (raw.trim() === "") return { kind: "absent" };
|
|
49602
|
+
let config2;
|
|
49603
|
+
try {
|
|
49604
|
+
config2 = JSON.parse(raw);
|
|
49605
|
+
} catch (err) {
|
|
49606
|
+
return { kind: "unreadable", error: errorText(err) };
|
|
49607
|
+
}
|
|
49608
|
+
if (!isPlainObject2(config2))
|
|
49609
|
+
return { kind: "unreadable", error: "top-level value is not an object" };
|
|
49610
|
+
const hooks = config2.hooks;
|
|
49611
|
+
const groups = isPlainObject2(hooks) ? hooks.PreToolUse : void 0;
|
|
49612
|
+
if (!Array.isArray(groups)) return { kind: "absent" };
|
|
49613
|
+
const expected = JSON.stringify(heavyCommandHookObject(provider));
|
|
49614
|
+
let found = { kind: "absent" };
|
|
49615
|
+
for (const group of groups) {
|
|
49616
|
+
if (!isPlainObject2(group) || !Array.isArray(group.hooks)) continue;
|
|
49617
|
+
for (const hook of group.hooks) {
|
|
49618
|
+
if (!isHeavyHookObject(hook)) continue;
|
|
49619
|
+
if (JSON.stringify(hook) === expected) return { kind: "present", current: true };
|
|
49620
|
+
found = { kind: "present", current: false };
|
|
49621
|
+
}
|
|
49622
|
+
}
|
|
49623
|
+
return found;
|
|
49624
|
+
}
|
|
49625
|
+
function presenceOf(provider, projectDir, target) {
|
|
49626
|
+
if (provider !== "opencode") {
|
|
49627
|
+
const local = jsonHookPresence(target, provider);
|
|
49628
|
+
if (provider !== "claude-code" || local.kind !== "absent") return local;
|
|
49629
|
+
const legacy = jsonHookPresence(join28(projectDir, ".claude", "settings.json"), provider);
|
|
49630
|
+
return legacy.kind === "present" ? { kind: "present", current: false } : { kind: "absent" };
|
|
49631
|
+
}
|
|
49632
|
+
if (!existsSync20(target)) return { kind: "absent" };
|
|
49633
|
+
try {
|
|
49634
|
+
return {
|
|
49635
|
+
kind: "present",
|
|
49636
|
+
current: readFileSync12(target, "utf-8") === opencodeHeavyCommandPluginSource()
|
|
49637
|
+
};
|
|
49638
|
+
} catch (err) {
|
|
49639
|
+
return { kind: "unreadable", error: errorText(err) };
|
|
49640
|
+
}
|
|
49641
|
+
}
|
|
49642
|
+
var HOOK_FILES = {
|
|
49643
|
+
"claude-code": CLAUDE_LOCAL_SETTINGS,
|
|
49644
|
+
codex: CODEX_HOOKS_FILE,
|
|
49645
|
+
opencode: OPENCODE_PLUGIN_FILE
|
|
49646
|
+
};
|
|
49647
|
+
function inspectProvider(provider, projectDir, mode, env, gitChecks) {
|
|
49648
|
+
const target = heavyHookTarget(provider, projectDir, env);
|
|
49649
|
+
const { detected, why } = detectHeavyHookProvider(provider, projectDir, env);
|
|
49650
|
+
const base = { provider, detected, target };
|
|
49651
|
+
if (provider === "kimi") {
|
|
49652
|
+
if (!detected || mode === "off") return { ...base, state: "not-detected", detail: why };
|
|
49653
|
+
let manual = false;
|
|
49654
|
+
try {
|
|
49655
|
+
manual = readFileSync12(target, "utf-8").includes(HEAVY_COMMAND_HOOK_ID);
|
|
49656
|
+
} catch {
|
|
49657
|
+
}
|
|
49658
|
+
return manual ? { ...base, state: "installed", detail: `a manual entry in ${target} runs the hook` } : { ...base, state: "unsupported", detail: `${why}; ${KIMI_REASON}`, remedy: KIMI_REMEDY };
|
|
49659
|
+
}
|
|
49660
|
+
const presence = presenceOf(provider, projectDir, target);
|
|
49661
|
+
if (presence.kind === "unreadable") {
|
|
49662
|
+
return {
|
|
49663
|
+
...base,
|
|
49664
|
+
state: "unreadable",
|
|
49665
|
+
detail: `${target} cannot be read or parsed (${presence.error})`,
|
|
49666
|
+
remedy: `fix ${target}, then run: ${HEAVY_HOOK_FIX_COMMAND}`
|
|
49667
|
+
};
|
|
49668
|
+
}
|
|
49669
|
+
if (mode === "off") {
|
|
49670
|
+
return presence.kind === "present" ? {
|
|
49671
|
+
...base,
|
|
49672
|
+
state: "outdated",
|
|
49673
|
+
detail: "resources.heavyCommandHook is off, but the hook is still installed",
|
|
49674
|
+
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND} (removes it)`
|
|
49675
|
+
} : { ...base, state: "disabled", detail: "resources.heavyCommandHook is off" };
|
|
49676
|
+
}
|
|
49677
|
+
if (presence.kind === "present") {
|
|
49678
|
+
if (!presence.current) {
|
|
49679
|
+
return {
|
|
49680
|
+
...base,
|
|
49681
|
+
state: "outdated",
|
|
49682
|
+
detail: `the hook in ${target} (or a legacy location) was written by another CLEO build`,
|
|
49683
|
+
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND}`
|
|
49684
|
+
};
|
|
49685
|
+
}
|
|
49686
|
+
if (!gitChecks) return { ...base, state: "installed", detail: `installed in ${target}` };
|
|
49687
|
+
const shared2 = provider === "codex" ? codexHooksSharedReason(projectDir, true) : null;
|
|
49688
|
+
if (shared2 !== null && excludedByCleo(projectDir, CODEX_HOOKS_FILE)) {
|
|
49689
|
+
return {
|
|
49690
|
+
...base,
|
|
49691
|
+
state: "outdated",
|
|
49692
|
+
detail: `CLEO's info/exclude line hides ${target}, which ${shared2}`,
|
|
49693
|
+
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND} (removes CLEO's exclude line)`
|
|
49694
|
+
};
|
|
49695
|
+
}
|
|
49696
|
+
if (shared2 !== null) {
|
|
49697
|
+
const modified = trackedFileModified(projectDir, CODEX_HOOKS_FILE);
|
|
49698
|
+
if (modified || hookFileVisibleToGit(projectDir, CODEX_HOOKS_FILE)) {
|
|
49699
|
+
return {
|
|
49700
|
+
...base,
|
|
49701
|
+
state: "outdated",
|
|
49702
|
+
detail: `CLEO's per-machine hook sits in ${target}, which ${shared2}, as ${modified ? "an uncommitted change" : "an untracked file"}: one \`git commit -a\` ships it to the team, and it widens evidence scope`,
|
|
49703
|
+
remedy: "remove CLEO's entry from .codex/hooks.json, or commit it deliberately if the team wants Codex governed"
|
|
49704
|
+
};
|
|
49705
|
+
}
|
|
49706
|
+
return { ...base, state: "installed", detail: `installed in ${target}, which ${shared2}` };
|
|
49707
|
+
}
|
|
49708
|
+
if (hookFileVisibleToGit(projectDir, HOOK_FILES[provider])) {
|
|
49709
|
+
return {
|
|
49710
|
+
...base,
|
|
49711
|
+
state: "outdated",
|
|
49712
|
+
detail: `${target} is installed but git sees it as an untracked file (it widens evidence scope)`,
|
|
49713
|
+
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND} (adds it to the repository's info/exclude)`
|
|
49714
|
+
};
|
|
49715
|
+
}
|
|
49716
|
+
return { ...base, state: "installed", detail: `installed in ${target}` };
|
|
49717
|
+
}
|
|
49718
|
+
if (!detected) return { ...base, state: "not-detected", detail: why };
|
|
49719
|
+
const shared = provider === "codex" ? codexHooksSharedReason(projectDir, gitChecks) : null;
|
|
49720
|
+
if (shared !== null && gitChecks && excludedByCleo(projectDir, CODEX_HOOKS_FILE)) {
|
|
49721
|
+
return {
|
|
49722
|
+
...base,
|
|
49723
|
+
state: "outdated",
|
|
49724
|
+
detail: `CLEO's info/exclude line hides ${target}, which ${shared}`,
|
|
49725
|
+
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND} (removes CLEO's exclude line)`
|
|
49726
|
+
};
|
|
49727
|
+
}
|
|
49728
|
+
if (shared !== null) {
|
|
49729
|
+
return {
|
|
49730
|
+
...base,
|
|
49731
|
+
state: "needs-consent",
|
|
49732
|
+
detail: `${target} ${shared}, so CLEO does not write or hide it; Codex runs ungoverned here until the team adds the entry (${why})`,
|
|
49733
|
+
remedy: CODEX_SHARED_REMEDY,
|
|
49734
|
+
snippet: codexHookSnippet()
|
|
49735
|
+
};
|
|
49736
|
+
}
|
|
49737
|
+
const blocked = nonDirectoryAncestor(dirname6(target));
|
|
49738
|
+
if (blocked !== null) {
|
|
49739
|
+
return {
|
|
49740
|
+
...base,
|
|
49741
|
+
state: "blocked",
|
|
49742
|
+
detail: `${blocked} exists but is not a directory, so the hook cannot be installed (${why})`,
|
|
49743
|
+
remedy: `remove or rename ${blocked}, then run: ${HEAVY_HOOK_FIX_COMMAND}`
|
|
49744
|
+
};
|
|
49745
|
+
}
|
|
49746
|
+
return {
|
|
49747
|
+
...base,
|
|
49748
|
+
state: "missing",
|
|
49749
|
+
detail: `${why}, but ${target} has no heavy-command hook: agent-run tests and builds bypass the machine-wide budget`,
|
|
49750
|
+
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND}`
|
|
49751
|
+
};
|
|
49752
|
+
}
|
|
49753
|
+
function inspectProjectHeavyCommandHooks(projectDir, mode, options = {}) {
|
|
49754
|
+
const env = options.env ?? process.env;
|
|
49755
|
+
const providers = options.providers ?? HEAVY_HOOK_DELIVERY_PROVIDERS;
|
|
49756
|
+
if (isUserHomeDir(projectDir, homeOf(env))) {
|
|
49757
|
+
return providers.map((provider) => ({
|
|
49758
|
+
provider,
|
|
49759
|
+
detected: false,
|
|
49760
|
+
state: "not-detected",
|
|
49761
|
+
target: heavyHookTarget(provider, projectDir, env),
|
|
49762
|
+
detail: "the project is your home directory; CLEO installs this hook per project only"
|
|
49763
|
+
}));
|
|
49764
|
+
}
|
|
49765
|
+
const gitChecks = options.gitChecks ?? true;
|
|
49766
|
+
return providers.map((provider) => inspectProvider(provider, projectDir, mode, env, gitChecks));
|
|
49767
|
+
}
|
|
49768
|
+
var OLDER_CLEO_MARKER_TTL_MS = 60 * 60 * 1e3;
|
|
49769
|
+
function olderCleoMarkerPath(cleoPath, dir, env) {
|
|
49770
|
+
try {
|
|
49771
|
+
const key = execFileSync2(
|
|
49772
|
+
"/bin/sh",
|
|
49773
|
+
["-c", `printf '%s|%s' "$1" "$2" | cksum | tr -c '0-9' '-'`, "sh", cleoPath, dir],
|
|
49774
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }
|
|
49775
|
+
);
|
|
49776
|
+
const base = env.XDG_RUNTIME_DIR || env.TMPDIR || "/tmp";
|
|
49777
|
+
return join28(base, `${OLDER_CLEO_MARKER_PREFIX}${key}`);
|
|
49778
|
+
} catch {
|
|
49779
|
+
return null;
|
|
49780
|
+
}
|
|
49781
|
+
}
|
|
49782
|
+
function hasOlderCleoMarker(cleoPath, dir, env) {
|
|
49783
|
+
const marker16 = olderCleoMarkerPath(cleoPath, dir, env);
|
|
49784
|
+
if (marker16 === null) return false;
|
|
49785
|
+
try {
|
|
49786
|
+
const st = lstatSync(marker16);
|
|
49787
|
+
if (!st.isFile()) return false;
|
|
49788
|
+
if (Date.now() - st.mtimeMs > OLDER_CLEO_MARKER_TTL_MS) return false;
|
|
49789
|
+
return statSync3(cleoPath).mtimeMs <= st.mtimeMs;
|
|
49790
|
+
} catch {
|
|
49791
|
+
return false;
|
|
49792
|
+
}
|
|
49793
|
+
}
|
|
49794
|
+
function probeHeavyHookCli(projectDir, options = {}) {
|
|
49795
|
+
const env = options.env ?? process.env;
|
|
49796
|
+
const path = findOnPath11("cleo", { env });
|
|
49797
|
+
if (path === null) {
|
|
49798
|
+
return {
|
|
49799
|
+
state: "missing",
|
|
49800
|
+
path: null,
|
|
49801
|
+
detail: "no `cleo` on PATH, so the hook lets every heavy command run ungoverned",
|
|
49802
|
+
remedy: "install CLEO so `cleo` is on PATH: npm i -g @cleocode/cleo"
|
|
49803
|
+
};
|
|
49804
|
+
}
|
|
49805
|
+
const older = {
|
|
49806
|
+
state: "older",
|
|
49807
|
+
path,
|
|
49808
|
+
detail: `${path} predates \`cleo hook\` and \`cleo run\`, so the hook lets every heavy command run ungoverned`,
|
|
49809
|
+
remedy: `upgrade the cleo at ${path} (npm i -g @cleocode/cleo, or your version manager), then run: ${HEAVY_HOOK_FIX_COMMAND}`
|
|
49810
|
+
};
|
|
49811
|
+
if (hasOlderCleoMarker(path, projectDir, env)) return older;
|
|
49812
|
+
const probe = spawnSync3(path, ["hook", "heavy-command", "--provider", "claude-code"], {
|
|
49813
|
+
cwd: projectDir,
|
|
49814
|
+
env: { ...env },
|
|
49815
|
+
input: "{}",
|
|
49816
|
+
encoding: "utf-8",
|
|
49817
|
+
timeout: 2e4
|
|
49818
|
+
});
|
|
49819
|
+
if (probe.status === 0) {
|
|
49820
|
+
return { state: "current", path, detail: `${path} answers \`cleo hook heavy-command\`` };
|
|
49821
|
+
}
|
|
49822
|
+
if (probe.status === 127 && /Unknown command/.test(probe.stderr ?? "")) return older;
|
|
49823
|
+
return {
|
|
49824
|
+
state: "unknown",
|
|
49825
|
+
path,
|
|
49826
|
+
detail: `${path} did not answer \`cleo hook heavy-command\` (${probe.error?.message ?? `exit ${probe.status ?? "none"}`}); the hook fails open, so heavy commands run ungoverned while it does not`,
|
|
49827
|
+
remedy: `run \`${path} hook heavy-command < /dev/null\` in ${projectDir} to see why it fails (a broken version-manager shim, say)`
|
|
49828
|
+
};
|
|
49829
|
+
}
|
|
49830
|
+
var heavyHookDeliveryApi = Object.freeze({
|
|
49831
|
+
syncProjectHeavyCommandHooks,
|
|
49832
|
+
inspectProjectHeavyCommandHooks,
|
|
49833
|
+
probeHeavyHookCli
|
|
49834
|
+
});
|
|
49835
|
+
|
|
49836
|
+
// packages/adapters/src/index.ts
|
|
49247
49837
|
init_hook_template_installer();
|
|
49248
49838
|
|
|
49249
49839
|
// packages/adapters/src/registry.ts
|
|
49250
|
-
import { readFileSync as
|
|
49251
|
-
import { dirname as
|
|
49840
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
49841
|
+
import { dirname as dirname7, join as join31, resolve as resolve4 } from "node:path";
|
|
49252
49842
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
49253
49843
|
var PROVIDER_IDS = ["claude-code", "opencode", "cursor", "pi"];
|
|
49254
49844
|
function getProviderManifests() {
|
|
49255
49845
|
const manifests = [];
|
|
49256
|
-
const baseDir = resolve4(
|
|
49846
|
+
const baseDir = resolve4(dirname7(fileURLToPath3(import.meta.url)), "providers");
|
|
49257
49847
|
for (const providerId of PROVIDER_IDS) {
|
|
49258
49848
|
try {
|
|
49259
|
-
const manifestPath =
|
|
49260
|
-
const raw =
|
|
49849
|
+
const manifestPath = join31(baseDir, providerId, "manifest.json");
|
|
49850
|
+
const raw = readFileSync13(manifestPath, "utf-8");
|
|
49261
49851
|
manifests.push(JSON.parse(raw));
|
|
49262
49852
|
} catch {
|
|
49263
49853
|
}
|
|
@@ -49305,6 +49895,8 @@ export {
|
|
|
49305
49895
|
GeminiCliAdapter,
|
|
49306
49896
|
GeminiCliHookProvider,
|
|
49307
49897
|
GeminiCliInstallProvider,
|
|
49898
|
+
HEAVY_HOOK_DELIVERY_PROVIDERS,
|
|
49899
|
+
HEAVY_HOOK_FIX_COMMAND,
|
|
49308
49900
|
KimiAdapter,
|
|
49309
49901
|
KimiHookProvider,
|
|
49310
49902
|
KimiInstallProvider,
|
|
@@ -49325,6 +49917,7 @@ export {
|
|
|
49325
49917
|
createAdapter5 as createGeminiCliAdapter,
|
|
49326
49918
|
createAdapter6 as createKimiAdapter,
|
|
49327
49919
|
createAdapter7 as createOpenCodeAdapter,
|
|
49920
|
+
detectHeavyHookProvider,
|
|
49328
49921
|
discoverProviders,
|
|
49329
49922
|
extractSymbolsFromText,
|
|
49330
49923
|
getProviderHookTemplatePath,
|
|
@@ -49333,8 +49926,13 @@ export {
|
|
|
49333
49926
|
getSetupInstructions,
|
|
49334
49927
|
getSharedHookCorePath,
|
|
49335
49928
|
getStatuslineConfig,
|
|
49929
|
+
heavyHookDeliveryApi,
|
|
49930
|
+
heavyHookTarget,
|
|
49931
|
+
inspectProjectHeavyCommandHooks,
|
|
49336
49932
|
installProviderHookTemplates,
|
|
49933
|
+
probeHeavyHookCli,
|
|
49337
49934
|
resolveTools,
|
|
49338
|
-
runNexusPostModificationCheck
|
|
49935
|
+
runNexusPostModificationCheck,
|
|
49936
|
+
syncProjectHeavyCommandHooks
|
|
49339
49937
|
};
|
|
49340
49938
|
//# sourceMappingURL=index.js.map
|