@indigoai-us/hq-cli 5.331.0 → 5.332.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/CHANGELOG.md +4 -0
- package/assets/scaffold/core/scripts/policy-age-report.sh +27 -0
- package/dist/commands/core-derive-trigger-facts-native.js +2 -2
- package/dist/commands/core-hq-session-native.js +2 -1
- package/dist/commands/core-migrate-policy-triggers-native.js +8 -5
- package/dist/commands/core-policy.js +13 -12
- package/dist/commands/core-resolve-company-native.js +4 -3
- package/dist/commands/core-share-suggestion-state-native.d.ts +1 -0
- package/dist/commands/core-share-suggestion-state-native.js +15 -3
- package/dist/commands/install-global.js +20 -9
- package/dist/commands/mcp-registration.d.ts +7 -0
- package/dist/commands/mcp-registration.js +8 -2
- package/dist/commands/mcp-status.js +21 -7
- package/dist/commands/pack-install.d.ts +18 -0
- package/dist/commands/pack-install.js +22 -6
- package/dist/commands/packs.js +9 -3
- package/dist/lib/core-utils/awk-diagnostics.d.ts +2 -0
- package/dist/lib/core-utils/awk-diagnostics.js +39 -0
- package/dist/lib/core-utils/byte-oriented-locale.d.ts +5 -0
- package/dist/lib/core-utils/byte-oriented-locale.js +23 -0
- package/dist/lib/install/codex-pack-contributions.d.ts +1 -1
- package/dist/lib/install/codex-pack-contributions.js +24 -1
- package/dist/lib/install/codex-targets.js +3 -1
- package/dist/utils/pack-contributions.d.ts +4 -0
- package/dist/utils/pack-contributions.js +23 -7
- package/dist/utils/resolve-executable.d.ts +3 -0
- package/dist/utils/resolve-executable.js +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.332.0] — 2026-10-02
|
|
6
|
+
- Match macOS shell behavior in native core commands for text, diagnostics, and file modes.
|
|
7
|
+
- `hq mcp status` now recognizes the real Claude plugin MCP definition and Codex PATH commands, while global and `hq-anywhere` pack installs share one Codex hook per event and one `hq` MCP server.
|
|
8
|
+
|
|
5
9
|
## [5.331.0] — 2026-10-02
|
|
6
10
|
- Prevent duplicate Claude hook dispatch inside the HQ root when global hooks are installed.
|
|
7
11
|
|
|
@@ -51,6 +51,33 @@ done
|
|
|
51
51
|
[ "${#DIRS[@]}" -gt 0 ] || DIRS=("$HQ_ROOT/personal/policies" "$HQ_ROOT/core/policies")
|
|
52
52
|
command -v jq >/dev/null 2>&1 || { echo "policy-age-report: jq required" >&2; exit 2; }
|
|
53
53
|
NOW_EPOCH="${HQ_POLICY_REPORT_NOW_EPOCH:-$(date +%s)}"
|
|
54
|
+
if [ -n "${HQ_POLICY_REPORT_NOW_EPOCH:-}" ]; then
|
|
55
|
+
CLOCK_INPUT="$HQ_POLICY_REPORT_NOW_EPOCH"
|
|
56
|
+
if [[ ! "$CLOCK_INPUT" =~ ^[+-]?[0-9]+$ ]]; then
|
|
57
|
+
printf 'policy-age-report: HQ_POLICY_REPORT_NOW_EPOCH must be an integer: %s\n' "$CLOCK_INPUT" >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
CLOCK_NEGATIVE=0
|
|
61
|
+
CLOCK_DIGITS="$CLOCK_INPUT"
|
|
62
|
+
case "$CLOCK_DIGITS" in
|
|
63
|
+
-*) CLOCK_NEGATIVE=1; CLOCK_DIGITS="${CLOCK_DIGITS#-}" ;;
|
|
64
|
+
+*) CLOCK_DIGITS="${CLOCK_DIGITS#+}" ;;
|
|
65
|
+
esac
|
|
66
|
+
while [[ "${#CLOCK_DIGITS}" -gt 1 && "${CLOCK_DIGITS:0:1}" = 0 ]]; do
|
|
67
|
+
CLOCK_DIGITS="${CLOCK_DIGITS:1}"
|
|
68
|
+
done
|
|
69
|
+
CLOCK_LIMIT=9223372036854775807
|
|
70
|
+
[ "$CLOCK_NEGATIVE" = 1 ] && CLOCK_LIMIT=9223372036854775808
|
|
71
|
+
if [[ "${#CLOCK_DIGITS}" -gt "${#CLOCK_LIMIT}" ]] || { [[ "${#CLOCK_DIGITS}" -eq "${#CLOCK_LIMIT}" ]] && (LC_ALL=C; [[ "$CLOCK_DIGITS" > "$CLOCK_LIMIT" ]]); }; then
|
|
72
|
+
printf 'policy-age-report: HQ_POLICY_REPORT_NOW_EPOCH must be an integer: %s\n' "$CLOCK_INPUT" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
if [ "$CLOCK_NEGATIVE" = 1 ]; then
|
|
76
|
+
NOW_EPOCH=$((0 - 10#$CLOCK_DIGITS))
|
|
77
|
+
else
|
|
78
|
+
NOW_EPOCH=$((10#$CLOCK_DIGITS))
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
54
81
|
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
55
82
|
|
|
56
83
|
# One pass over every ledger: slug<TAB>mtime, then reduce to count + max mtime.
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
5
|
+
import { isByteOrientedLocale } from "../lib/core-utils/byte-oriented-locale.js";
|
|
5
6
|
export const DERIVE_TRIGGER_FACTS_ASSET = "core/scripts/derive-trigger-facts.sh";
|
|
6
7
|
const TEXT_FACT_AWK = "core/scripts/lib/trigger-fact-text.awk";
|
|
7
8
|
const TRANSCRIPT_TAIL_MAX_BYTES = 1_048_576;
|
|
@@ -19,8 +20,7 @@ function asciiLower(value) {
|
|
|
19
20
|
return value.replace(/[A-Z]/g, (letter) => String.fromCharCode(letter.charCodeAt(0) + 32));
|
|
20
21
|
}
|
|
21
22
|
function localeLower(value, env) {
|
|
22
|
-
|
|
23
|
-
if (/^(?:C|POSIX)(?:$|\.(?!utf-?8$))/i.test(locale))
|
|
23
|
+
if (isByteOrientedLocale(env, process.platform))
|
|
24
24
|
return asciiLower(value);
|
|
25
25
|
// glibc applies towlower independently to each character. Lowercase code
|
|
26
26
|
// points separately so JavaScript does not apply context-sensitive sigma.
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
6
7
|
export const HQ_SESSION_ASSET = "core/scripts/hq-session.sh";
|
|
7
8
|
export const HQ_SESSION_HELP = [
|
|
8
9
|
"hq-core: public",
|
|
@@ -483,7 +484,7 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
483
484
|
}
|
|
484
485
|
catch (error) {
|
|
485
486
|
if (error?.code === "EACCES" || error?.code === "EPERM") {
|
|
486
|
-
return { code: 2, stdout: "", stderr:
|
|
487
|
+
return { code: 2, stdout: "", stderr: awkCannotOpenFileDiagnostic(metaPath, "Permission denied", 6, process.platform, deps.env.PATH) };
|
|
487
488
|
}
|
|
488
489
|
if (isExpectedFsMiss(error))
|
|
489
490
|
return { code: 1, stdout: "", stderr: `awk: cannot open ${metaPath} (Permission denied)\n` };
|
|
@@ -5,6 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { evaluateTrigger } from "../utils/core-eval-trigger.js";
|
|
8
|
+
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
8
9
|
export const MIGRATE_POLICY_TRIGGERS_ASSET = "core/scripts/migrate-policy-triggers.sh";
|
|
9
10
|
const ON_LIVE = "[PreToolUse, PostToolUse, UserPromptSubmit, AssistantIntent]";
|
|
10
11
|
const ON_START = "[SessionStart]";
|
|
@@ -151,9 +152,11 @@ function deriveWhen(trigger, rawTags, enforcementRaw) {
|
|
|
151
152
|
}
|
|
152
153
|
function writeAtomic(fileSystem, target, bytes) {
|
|
153
154
|
const temporary = path.join(path.dirname(target), `.migrate-policy-triggers.${process.pid}.${Math.random().toString(16).slice(2)}`);
|
|
155
|
+
// The shell writes through mktemp, whose private mode survives the rename on both platforms.
|
|
156
|
+
const mode = 0o600;
|
|
154
157
|
try {
|
|
155
|
-
fileSystem.writeFileSync(temporary, bytes, { encoding: "utf8", mode
|
|
156
|
-
fileSystem.chmodSync(temporary,
|
|
158
|
+
fileSystem.writeFileSync(temporary, bytes, { encoding: "utf8", mode, flag: "wx" });
|
|
159
|
+
fileSystem.chmodSync(temporary, mode);
|
|
157
160
|
fileSystem.renameSync(temporary, target);
|
|
158
161
|
return true;
|
|
159
162
|
}
|
|
@@ -165,11 +168,11 @@ function writeAtomic(fileSystem, target, bytes) {
|
|
|
165
168
|
return false;
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
|
-
function unreadablePolicyDiagnostic(filename, error) {
|
|
171
|
+
function unreadablePolicyDiagnostic(filename, error, pathValue) {
|
|
169
172
|
const code = error.code;
|
|
170
173
|
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : "No such file or directory";
|
|
171
174
|
const script = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/scaffold/core/scripts/migrate-policy-triggers.sh");
|
|
172
|
-
const awk =
|
|
175
|
+
const awk = awkCannotOpenFileDiagnostic(filename, message, 1, process.platform, pathValue);
|
|
173
176
|
return `${script}: line 314: ${filename}: ${message}\n${awk.repeat(3)}`;
|
|
174
177
|
}
|
|
175
178
|
function shellGlobMd(fileSystem, directory) {
|
|
@@ -293,7 +296,7 @@ export function runMigratePolicyTriggers(args, deps) {
|
|
|
293
296
|
original = fileSystem.readFileSync(filename, "utf8");
|
|
294
297
|
}
|
|
295
298
|
catch (error) {
|
|
296
|
-
stderr += unreadablePolicyDiagnostic(filename, error);
|
|
299
|
+
stderr += unreadablePolicyDiagnostic(filename, error, deps.env.PATH);
|
|
297
300
|
untriggered += 1;
|
|
298
301
|
continue;
|
|
299
302
|
}
|
|
@@ -8,6 +8,7 @@ import { resolveLiveRoot, resolveBundledAsset } from '../utils/hq-roots.js';
|
|
|
8
8
|
import { runBundledScript } from '../utils/run-bundled-script.js';
|
|
9
9
|
import { describeCoreCommand } from './core-command-catalog.js';
|
|
10
10
|
import { runPolicyFrontmatter as runPolicyFrontmatterImpl } from './core-policy-frontmatter.js';
|
|
11
|
+
import { awkCannotOpenFileDiagnostic } from '../lib/core-utils/awk-diagnostics.js';
|
|
11
12
|
const POLICY_AGE_REPORT_ASSET = 'core/scripts/policy-age-report.sh';
|
|
12
13
|
const POLICY_RETIRE_ASSET = 'core/scripts/policy-retire.sh';
|
|
13
14
|
const POLICY_FRONTMATTER_ASSET = 'core/scripts/read-policy-frontmatter.sh';
|
|
@@ -266,7 +267,7 @@ function epochFromDate(value) {
|
|
|
266
267
|
return 0;
|
|
267
268
|
return Math.floor(parsed / 1000);
|
|
268
269
|
}
|
|
269
|
-
function countLedgerRows(directory, withMtime, recursive) {
|
|
270
|
+
function countLedgerRows(directory, withMtime, recursive, awkPath) {
|
|
270
271
|
const counts = new Map();
|
|
271
272
|
const newest = new Map();
|
|
272
273
|
let anyFiles = false;
|
|
@@ -299,7 +300,7 @@ function countLedgerRows(directory, withMtime, recursive) {
|
|
|
299
300
|
catch (error) {
|
|
300
301
|
const code = error.code;
|
|
301
302
|
if (withMtime && (code === 'EACCES' || code === 'EPERM')) {
|
|
302
|
-
stderr +=
|
|
303
|
+
stderr += awkCannotOpenFileDiagnostic(full, 'Permission denied', 1, process.platform, awkPath);
|
|
303
304
|
continue;
|
|
304
305
|
}
|
|
305
306
|
if (expectedFsMiss(error))
|
|
@@ -391,12 +392,11 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
391
392
|
const epoch = bashInteger(env.HQ_POLICY_REPORT_NOW_EPOCH);
|
|
392
393
|
if (epoch === undefined) {
|
|
393
394
|
const value = env.HQ_POLICY_REPORT_NOW_EPOCH;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
395
|
+
throw new Error(`policy-age-report: HQ_POLICY_REPORT_NOW_EPOCH must be an integer: ${value}`);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
now = Number(epoch);
|
|
398
399
|
}
|
|
399
|
-
now = Number(epoch);
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
catch (error) {
|
|
@@ -404,8 +404,9 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
404
404
|
}
|
|
405
405
|
const ledgerDir = env.HQ_POLICY_LEDGER_DIR || path.join(root, 'workspace/orchestrator/policy-trigger-state');
|
|
406
406
|
const retrievalDir = env.HQ_POLICY_RETRIEVAL_LEDGER_DIR || path.join(root, 'workspace/orchestrator/policy-retrieval-state');
|
|
407
|
-
const
|
|
408
|
-
const
|
|
407
|
+
const awkPath = env.PATH ?? '/usr/bin:/bin';
|
|
408
|
+
const usage = countLedgerRows(ledgerDir, true, true, awkPath);
|
|
409
|
+
const retrieval = countLedgerRows(retrievalDir, false, false, awkPath);
|
|
409
410
|
const dirs = parsed.dirs.length > 0 ? parsed.dirs : [path.join(root, 'personal/policies'), path.join(root, 'core/policies')];
|
|
410
411
|
const rows = [];
|
|
411
412
|
let stderr = usage.stderr + retrieval.stderr;
|
|
@@ -439,13 +440,13 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
439
440
|
const singleIncident = ['user-correction', 'session-learning', 'back-pressure-failure'].includes(source) && (version || '1') === '1';
|
|
440
441
|
const classes = [];
|
|
441
442
|
if (fired === 0) {
|
|
442
|
-
const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, resolveBundledAsset(POLICY_AGE_REPORT_ASSET),
|
|
443
|
+
const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 157);
|
|
443
444
|
stderr += comparison.stderr;
|
|
444
445
|
if (comparison.value && usage.anyFiles)
|
|
445
446
|
classes.push('never-fired');
|
|
446
447
|
}
|
|
447
448
|
if (fired > 0) {
|
|
448
|
-
const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET),
|
|
449
|
+
const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 158);
|
|
449
450
|
stderr += comparison.stderr;
|
|
450
451
|
if (comparison.value)
|
|
451
452
|
classes.push('dormant');
|
|
@@ -455,7 +456,7 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
455
456
|
if (refs.renamed.length > 0)
|
|
456
457
|
classes.push('renamed-refs');
|
|
457
458
|
if (singleIncident) {
|
|
458
|
-
const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET),
|
|
459
|
+
const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 162);
|
|
459
460
|
stderr += comparison.stderr;
|
|
460
461
|
if (comparison.value && fired <= 1)
|
|
461
462
|
classes.push('single-incident-old');
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
5
6
|
export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
|
|
6
7
|
export const RESOLVE_COMPANY_HELP = [
|
|
7
8
|
"hq-core: public",
|
|
@@ -36,7 +37,7 @@ function readFile(filePath, fileSystem) {
|
|
|
36
37
|
throw error;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
function readManifest(filePath, fileSystem) {
|
|
40
|
+
function readManifest(filePath, fileSystem, pathValue) {
|
|
40
41
|
try {
|
|
41
42
|
return fileSystem.readFileSync(filePath, "utf8");
|
|
42
43
|
}
|
|
@@ -46,7 +47,7 @@ function readManifest(filePath, fileSystem) {
|
|
|
46
47
|
return {
|
|
47
48
|
code: 0,
|
|
48
49
|
stdout: Buffer.from('{"company":"","source":"none"}\n'),
|
|
49
|
-
stderr: Buffer.from(
|
|
50
|
+
stderr: Buffer.from(awkCannotOpenFileDiagnostic(filePath, "Permission denied", 1, process.platform, pathValue)),
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP")
|
|
@@ -340,7 +341,7 @@ export async function resolveCompany(args, dependencies) {
|
|
|
340
341
|
const manifest = path.join(root, "companies/manifest.yaml");
|
|
341
342
|
if (!isFile(manifest, fileSystem))
|
|
342
343
|
return output("", "none");
|
|
343
|
-
const manifestResult = readManifest(manifest, fileSystem);
|
|
344
|
+
const manifestResult = readManifest(manifest, fileSystem, env.PATH ?? "/usr/bin:/bin");
|
|
344
345
|
if (typeof manifestResult !== "string")
|
|
345
346
|
return manifestResult;
|
|
346
347
|
const manifestText = manifestResult;
|
|
@@ -13,6 +13,7 @@ export type ShareSuggestionDependencies = {
|
|
|
13
13
|
fs?: Pick<typeof fs, "existsSync" | "statSync" | "readFileSync" | "writeFileSync" | "appendFileSync" | "renameSync" | "unlinkSync" | "mkdirSync">;
|
|
14
14
|
now?: () => Date;
|
|
15
15
|
stdinIsTty?: boolean;
|
|
16
|
+
platform?: NodeJS.Platform;
|
|
16
17
|
};
|
|
17
18
|
export declare function runShareSuggestionState(args: readonly string[], stdin: string, deps: ShareSuggestionDependencies): HookHelperOutput;
|
|
18
19
|
//# sourceMappingURL=core-share-suggestion-state-native.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Native port of share-suggestion-state.sh; kept dependency-light for hooks. */
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { isByteOrientedLocale } from "../lib/core-utils/byte-oriented-locale.js";
|
|
4
5
|
export const SHARE_SUGGESTION_STATE_ASSET = "core/scripts/share-suggestion-state.sh";
|
|
5
6
|
const isObj = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
7
|
const sortKeys = (value) => Array.isArray(value) ? value.map(sortKeys)
|
|
@@ -148,6 +149,7 @@ export function runShareSuggestionState(args, stdin, deps) {
|
|
|
148
149
|
if (!root)
|
|
149
150
|
return failFromShellMain("share-suggestion-state: unable to resolve HQ root\n");
|
|
150
151
|
const { fileSystem: f, env, isoNow } = makeContext(deps);
|
|
152
|
+
const platform = deps.platform ?? process.platform;
|
|
151
153
|
const stateHome = path.join(root, "workspace/orchestrator/share-suggestions");
|
|
152
154
|
const historyPath = path.join(stateHome, "history.jsonl");
|
|
153
155
|
const suppressionsPath = path.join(stateHome, "suppressions.jsonl");
|
|
@@ -169,10 +171,20 @@ export function runShareSuggestionState(args, stdin, deps) {
|
|
|
169
171
|
catch { /* locate the first existing non-directory parent when present */ }
|
|
170
172
|
}
|
|
171
173
|
const reason = code === "ENOTDIR" ? "Not a directory" : code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "EEXIST" ? "File exists" : (error instanceof Error ? error.message : String(error));
|
|
172
|
-
|
|
174
|
+
const mkdirError = platform === "darwin"
|
|
175
|
+
? `mkdir: ${blockedPath}: ${reason}\n`
|
|
176
|
+
: `mkdir: cannot create directory ‘${blockedPath}’: ${reason}\n`;
|
|
177
|
+
return failFromShellMain(`${mkdirError}share-suggestion-state: unable to create state directory\n`);
|
|
173
178
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
179
|
+
const sessionUnits = platform === "darwin" && !isByteOrientedLocale(env, platform)
|
|
180
|
+
? Array.from(rawSession)
|
|
181
|
+
: Buffer.from(rawSession, "utf8");
|
|
182
|
+
const sessionId = Array.from(sessionUnits, (unit) => {
|
|
183
|
+
const code = typeof unit === "number" ? unit : unit.codePointAt(0);
|
|
184
|
+
return code === 45 || code === 95 || (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122)
|
|
185
|
+
? String.fromCharCode(code)
|
|
186
|
+
: "_";
|
|
187
|
+
}).join("") || "unknown";
|
|
176
188
|
const pendingPath = path.join(stateHome, `${sessionId}.json`);
|
|
177
189
|
// Bash command substitution removes every trailing LF before exporting the payload.
|
|
178
190
|
const payloadRaw = deps.stdinIsTty ? "" : stdin.replace(/\n+$/, "");
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import { buildCodexServerDef, claudeConfigFormat, codexConfigFormat, isCodexInstalled, jsonFormat, mergeCodexServer, registerCodexServer, unregisterCodexServer, mergeClaudeServer, readConfigDoc, realpathOrSelf, registerClaudeServer, resolveEnv, unregisterClaudeServer, writeConfigAtomic, } from './mcp-registration.js';
|
|
14
|
+
import { buildCodexServerDef, claudeConfigFormat, codexConfigFormat, codexRemovalFormat, isCodexInstalled, jsonFormat, mergeCodexServer, removeCodexServer, registerCodexServer, unregisterCodexServer, mergeClaudeServer, readConfigDoc, realpathOrSelf, registerClaudeServer, resolveEnv, unregisterClaudeServer, writeConfigAtomic, } from './mcp-registration.js';
|
|
15
15
|
import { seedRegistryFromHqRoot } from './link.js';
|
|
16
16
|
import { resolveHqRoot } from './doctor.js';
|
|
17
17
|
import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
|
|
@@ -685,8 +685,14 @@ export function installGlobalCodex(opts) {
|
|
|
685
685
|
const hooksNext = addCodexHooks(hooksDoc, hqRoot, hookTarget);
|
|
686
686
|
const tomlOld = readOrNull(t.configToml);
|
|
687
687
|
const tomlDoc = readConfigDoc(t.configToml, codexConfigFormat).doc;
|
|
688
|
+
const codexServers = tomlDoc.value.mcp_servers;
|
|
689
|
+
const legacyPackServerPresent = codexServers?.['hq-anywhere']?._hqPack === HQ_ANYWHERE_PACK;
|
|
688
690
|
// Collision check runs here too, so a dry run surfaces a conflicting "hq" server.
|
|
689
|
-
const
|
|
691
|
+
const codexDef = buildCodexServerDef(manifest, HQ_ANYWHERE_PACK, () => null, new Set());
|
|
692
|
+
const migrationBase = legacyPackServerPresent
|
|
693
|
+
? codexConfigFormat.parse(codexRemovalFormat.serialize(removeCodexServer('hq-anywhere', HQ_ANYWHERE_PACK)(tomlDoc)))
|
|
694
|
+
: tomlDoc;
|
|
695
|
+
const tomlNext = mergeCodexServer(HQ_MCP_SERVER_NAME, codexDef)(migrationBase);
|
|
690
696
|
if (pointerOld !== pointerNew)
|
|
691
697
|
touched.push(hqPointer);
|
|
692
698
|
if (mdOld !== mdNew)
|
|
@@ -697,7 +703,7 @@ export function installGlobalCodex(opts) {
|
|
|
697
703
|
touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
|
|
698
704
|
if (hooksNext !== hooksDoc)
|
|
699
705
|
touched.push(t.hooksJson);
|
|
700
|
-
if (tomlNext !== tomlDoc)
|
|
706
|
+
if (tomlNext !== tomlDoc || legacyPackServerPresent)
|
|
701
707
|
touched.push(t.configToml);
|
|
702
708
|
touched.push(regFile, t.stateFile);
|
|
703
709
|
if (opts.dryRun) {
|
|
@@ -746,6 +752,9 @@ export function installGlobalCodex(opts) {
|
|
|
746
752
|
assertName: 'hooks',
|
|
747
753
|
env: { home },
|
|
748
754
|
});
|
|
755
|
+
if (legacyPackServerPresent) {
|
|
756
|
+
unregisterCodexServer({ name: 'hq-anywhere', pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
757
|
+
}
|
|
749
758
|
registerCodexServer({ name: HQ_MCP_SERVER_NAME, manifest, pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
750
759
|
let seeded = [];
|
|
751
760
|
let ownedSeeded = [];
|
|
@@ -792,14 +801,15 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
792
801
|
throw new Error('No HQ global install found (no install state and no ~/.hq/root).');
|
|
793
802
|
const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
|
|
794
803
|
const claudeStillInstalled = fs.existsSync(claudeTargets(home).stateFile);
|
|
804
|
+
const packStillInstalled = fs.existsSync(path.join(hqRoot, 'core', 'packages', 'hq-anywhere', 'package.yaml'));
|
|
795
805
|
const touched = [];
|
|
796
806
|
const tomlDoc = readConfigDoc(t.configToml, codexConfigFormat).doc;
|
|
797
807
|
const servers = tomlDoc.value.mcp_servers;
|
|
798
|
-
if (servers?.[HQ_MCP_SERVER_NAME]?._hqPack === HQ_ANYWHERE_PACK)
|
|
808
|
+
if (!packStillInstalled && servers?.[HQ_MCP_SERVER_NAME]?._hqPack === HQ_ANYWHERE_PACK)
|
|
799
809
|
touched.push(t.configToml);
|
|
800
810
|
const hooksOriginal = state ? state.hooks.original : null;
|
|
801
811
|
const hooksDoc = readConfigDoc(t.hooksJson, jsonFormat).doc;
|
|
802
|
-
const hooksNext = removeCodexHooks(hooksDoc, hqRoot);
|
|
812
|
+
const hooksNext = packStillInstalled ? hooksDoc : removeCodexHooks(hooksDoc, hqRoot);
|
|
803
813
|
if (hooksNext !== hooksDoc)
|
|
804
814
|
touched.push(t.hooksJson);
|
|
805
815
|
const source = hqSkillsSource(hqRoot);
|
|
@@ -848,7 +858,8 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
848
858
|
log(lineDiff(jsonFormat.serialize(hooksDoc), jsonFormat.serialize(hooksNext)));
|
|
849
859
|
return touched;
|
|
850
860
|
}
|
|
851
|
-
|
|
861
|
+
if (!packStillInstalled)
|
|
862
|
+
unregisterCodexServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
852
863
|
// Removal re-serializes the TOML (dropping comments). When what is left equals
|
|
853
864
|
// the pre-install document, put the original bytes back.
|
|
854
865
|
const tomlOriginal = state?.configToml.original ?? null;
|
|
@@ -858,12 +869,12 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
858
869
|
writeFileAtomic(realpathOrSelf(t.configToml), tomlOriginal);
|
|
859
870
|
}
|
|
860
871
|
}
|
|
861
|
-
if (fs.existsSync(t.hooksJson)) {
|
|
872
|
+
if (!packStillInstalled && fs.existsSync(t.hooksJson)) {
|
|
862
873
|
writeConfigAtomic({
|
|
863
874
|
target: t.hooksJson,
|
|
864
875
|
pack: HQ_ANYWHERE_PACK,
|
|
865
|
-
format: restoringRemovalFormat(hooksOriginal, (doc) => removeCodexHooks(doc, hqRoot) !== doc),
|
|
866
|
-
merge: (doc) => removeCodexHooks(doc, hqRoot),
|
|
876
|
+
format: restoringRemovalFormat(hooksOriginal, (doc) => (packStillInstalled ? doc : removeCodexHooks(doc, hqRoot)) !== doc),
|
|
877
|
+
merge: (doc) => packStillInstalled ? doc : removeCodexHooks(doc, hqRoot),
|
|
867
878
|
assertName: 'hooks',
|
|
868
879
|
env: { home },
|
|
869
880
|
});
|
|
@@ -529,6 +529,8 @@ export interface RegisterServerOptions {
|
|
|
529
529
|
* injected `env.home`, so in tests it lands in the tmpdir — never the real ~/.
|
|
530
530
|
*/
|
|
531
531
|
audit?: boolean;
|
|
532
|
+
/** Preserve the globally installed Codex HQ endpoint while still registering Claude. */
|
|
533
|
+
preserveCodex?: boolean;
|
|
532
534
|
}
|
|
533
535
|
/** Outcome of registering one server into one surface. */
|
|
534
536
|
export interface RegisterSurfaceResult {
|
|
@@ -645,6 +647,8 @@ export declare function registerMcpServers(pkg: string, names: string[], options
|
|
|
645
647
|
* Omitted → read `HQ_DISABLE_MCP_REGISTRATION` directly.
|
|
646
648
|
*/
|
|
647
649
|
registrationEnabled?: boolean;
|
|
650
|
+
/** Preserve the global Codex HQ endpoint when the same pack also wires Claude. */
|
|
651
|
+
preserveCodex?: boolean;
|
|
648
652
|
}): RegisterServerResult[];
|
|
649
653
|
/** smol-toml's value-table type (its `parse` return + `stringify` input). */
|
|
650
654
|
type TomlTable = Record<string, unknown>;
|
|
@@ -825,6 +829,8 @@ export interface UnregisterServerOptions {
|
|
|
825
829
|
* that call {@link unregisterClaudeServer}/{@link unregisterCodexServer} directly.
|
|
826
830
|
*/
|
|
827
831
|
audit?: boolean;
|
|
832
|
+
/** Keep the shared Codex hq endpoint while the global installer still owns it. */
|
|
833
|
+
preserveCodex?: boolean;
|
|
828
834
|
}
|
|
829
835
|
/**
|
|
830
836
|
* Un-register ONE server from the PINNED single Claude surface (top-level
|
|
@@ -924,6 +930,7 @@ export declare function unregisterMcpServers(pkg: string, names: string[], optio
|
|
|
924
930
|
/** Lock tuning + backup stamp passthrough (tests). */
|
|
925
931
|
lock?: AcquireLockOptions;
|
|
926
932
|
stamp?: string;
|
|
933
|
+
preserveCodex?: boolean;
|
|
927
934
|
}): UnregisterServerResult[];
|
|
928
935
|
export {};
|
|
929
936
|
//# sourceMappingURL=mcp-registration.d.ts.map
|
|
@@ -1177,7 +1177,9 @@ export function registerServer(opts) {
|
|
|
1177
1177
|
const codexPrevHash = audit ? hashFileContents(codexTarget) : '';
|
|
1178
1178
|
let codex;
|
|
1179
1179
|
try {
|
|
1180
|
-
codex =
|
|
1180
|
+
codex = opts.preserveCodex
|
|
1181
|
+
? { skipped: true, reason: 'shared global install owns Codex hq endpoint' }
|
|
1182
|
+
: registerCodexServer(opts);
|
|
1181
1183
|
}
|
|
1182
1184
|
catch (e) {
|
|
1183
1185
|
if (audit) {
|
|
@@ -1302,6 +1304,7 @@ export function registerMcpServers(pkg, names, options) {
|
|
|
1302
1304
|
env: options?.env,
|
|
1303
1305
|
lock: options?.lock,
|
|
1304
1306
|
stamp: options?.stamp,
|
|
1307
|
+
preserveCodex: options?.preserveCodex && pkg === 'hq-anywhere' && name === 'hq',
|
|
1305
1308
|
});
|
|
1306
1309
|
});
|
|
1307
1310
|
}
|
|
@@ -2113,7 +2116,9 @@ export function unregisterServer(opts) {
|
|
|
2113
2116
|
const codexPrevHash = audit ? hashFileContents(codexTarget) : '';
|
|
2114
2117
|
let codex;
|
|
2115
2118
|
try {
|
|
2116
|
-
codex =
|
|
2119
|
+
codex = opts.preserveCodex
|
|
2120
|
+
? { skipped: true, reason: 'Codex hq endpoint remains owned by the global install' }
|
|
2121
|
+
: unregisterCodexServer(opts);
|
|
2117
2122
|
}
|
|
2118
2123
|
catch (e) {
|
|
2119
2124
|
if (audit) {
|
|
@@ -2242,6 +2247,7 @@ export function unregisterMcpServers(pkg, names, options) {
|
|
|
2242
2247
|
env: options?.env,
|
|
2243
2248
|
lock: options?.lock,
|
|
2244
2249
|
stamp: options?.stamp,
|
|
2250
|
+
preserveCodex: options?.preserveCodex,
|
|
2245
2251
|
});
|
|
2246
2252
|
});
|
|
2247
2253
|
}
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
|
+
import { isExecutableOnPath } from '../utils/resolve-executable.js';
|
|
26
27
|
import { anywhereRuntimeEnabled } from './anywhere-runtime-flag.js';
|
|
27
|
-
import { claudeTargets } from '../lib/install/claude-targets.js';
|
|
28
|
+
import { claudeTargets, MCP_SUBPROCESS_PATH } from '../lib/install/claude-targets.js';
|
|
28
29
|
import { HQ_PACK_PROVENANCE_KEY, SECRET_REDACTION, backupRoot, claudeConfigFormat, claudeConfigPath, codexConfigFormat, codexConfigPath, CODEX_MCP_SERVERS_KEY, ConfigParseError, isCodexInstalled, readConfigDoc, resolveEnv, } from './mcp-registration.js';
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
30
31
|
// Pure status computation.
|
|
@@ -40,7 +41,11 @@ function provenanceOf(def) {
|
|
|
40
41
|
}
|
|
41
42
|
/** The transport string off a def (`type`), or '' when not a string. */
|
|
42
43
|
function transportOf(def) {
|
|
43
|
-
|
|
44
|
+
if (typeof def.type === 'string')
|
|
45
|
+
return def.type;
|
|
46
|
+
// Claude Code's plugin .mcp.json omits `type` for command-backed servers;
|
|
47
|
+
// both Claude and Codex interpret that shape as stdio.
|
|
48
|
+
return typeof def.command === 'string' ? 'stdio' : '';
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
46
51
|
* The target off a def — url (http/sse) or command(+args) (stdio). NEVER a
|
|
@@ -124,9 +129,15 @@ function findAnywherePluginMcp(home) {
|
|
|
124
129
|
function pluginAndCodexMcpEquivalent(plugin, codex) {
|
|
125
130
|
const pArgs = Array.isArray(plugin.args) ? plugin.args : [];
|
|
126
131
|
const cArgs = Array.isArray(codex.args) ? codex.args : [];
|
|
127
|
-
const pluginTarget = plugin
|
|
128
|
-
const
|
|
129
|
-
|
|
132
|
+
const pluginTarget = transportOf(plugin) === 'stdio' && plugin.command === '/bin/sh' && pArgs.length === 3 && pArgs[0] === '${CLAUDE_PLUGIN_ROOT}/scripts/hq-claude-plugin-launch.sh' && pArgs[1] === 'mcp' && pArgs[2] === 'serve';
|
|
133
|
+
const bareOrAbsoluteHq = typeof codex.command === 'string' && /(^|[\\/])hq(?:\.exe)?$/.test(codex.command) && cArgs.length === 2 && cArgs[0] === 'mcp' && cArgs[1] === 'serve';
|
|
134
|
+
const globalNodeEntry = typeof codex.command === 'string' && path.isAbsolute(codex.command) && /(^|[\\/])node(?:\.exe)?$/.test(codex.command) && cArgs.length === 3 && typeof cArgs[0] === 'string' && path.isAbsolute(cArgs[0]) && cArgs[1] === 'mcp' && cArgs[2] === 'serve';
|
|
135
|
+
const codexTarget = transportOf(codex) === 'stdio' && (bareOrAbsoluteHq || globalNodeEntry);
|
|
136
|
+
const pluginEnv = plugin.env && typeof plugin.env === 'object' ? plugin.env : {};
|
|
137
|
+
const codexEnv = codex.env && typeof codex.env === 'object' ? codex.env : {};
|
|
138
|
+
const envMatch = JSON.stringify(envKeysOf(plugin)) === JSON.stringify(envKeysOf(codex)) ||
|
|
139
|
+
(globalNodeEntry && Object.keys(pluginEnv).length === 0 && Object.keys(codexEnv).length === 1 && codexEnv.PATH === MCP_SUBPROCESS_PATH);
|
|
140
|
+
return pluginTarget && codexTarget && JSON.stringify(headerKeysOf(plugin)) === JSON.stringify(headerKeysOf(codex)) && envMatch;
|
|
130
141
|
}
|
|
131
142
|
/** Scan the Claude runtime: read `~/.claude.json`, collect HQ-owned servers. */
|
|
132
143
|
function scanClaude(env) {
|
|
@@ -208,12 +219,15 @@ function isLaunchable(def) {
|
|
|
208
219
|
if (transportOf(def) !== 'stdio')
|
|
209
220
|
return true;
|
|
210
221
|
const command = def.command;
|
|
211
|
-
if (typeof command !== 'string' || !
|
|
222
|
+
if (typeof command !== 'string' || !commandIsLaunchable(command))
|
|
212
223
|
return false;
|
|
213
|
-
}
|
|
214
224
|
const args = Array.isArray(def.args) ? def.args : [];
|
|
215
225
|
return args.every((a) => typeof a !== 'string' || !path.isAbsolute(a) || fs.existsSync(a));
|
|
216
226
|
}
|
|
227
|
+
/** Codex resolves bare MCP commands through the host process PATH. */
|
|
228
|
+
function commandIsLaunchable(command) {
|
|
229
|
+
return isExecutableOnPath(command);
|
|
230
|
+
}
|
|
217
231
|
/** List the most-recent backup dir names under `backupRoot` (newest first, capped). */
|
|
218
232
|
function recentBackups(env) {
|
|
219
233
|
const root = backupRoot(env);
|
|
@@ -336,6 +336,24 @@ export declare function confirmMcp(pkg: PackManifest, payloadDir: string, allowM
|
|
|
336
336
|
* authorization response. No cached value is a fallback.
|
|
337
337
|
*/
|
|
338
338
|
export declare function makeInstallSecretResolver(secretNames: string[], company?: string): Promise<SecretResolver>;
|
|
339
|
+
/**
|
|
340
|
+
* Register one pack's `contributes.mcp` servers into the shared Claude/Codex
|
|
341
|
+
* agent configs, PER SERVER, so a single unresolvable `${secret:NAME}` defers
|
|
342
|
+
* ONLY that server instead of aborting the whole install. Returns the registered
|
|
343
|
+
* and skipped (deferred) server-name lists for the one-line summary.
|
|
344
|
+
*
|
|
345
|
+
* Per-server policy:
|
|
346
|
+
* - SUCCESS → push to `registered`.
|
|
347
|
+
* - McpManifestError matching
|
|
348
|
+
* `/cannot resolve \$\{secret:/` → SKIP (push to `skipped`), warn on stderr,
|
|
349
|
+
* install still succeeds (the key gets minted later, then a re-install wires it).
|
|
350
|
+
* - ANY OTHER error → RE-THROW (abort install). Only the
|
|
351
|
+
* unresolvable-secret case is swallowed.
|
|
352
|
+
*/
|
|
353
|
+
export declare function wireMcpServers(pkg: PackManifest, destDir: string, company?: string): Promise<{
|
|
354
|
+
registered: string[];
|
|
355
|
+
skipped: string[];
|
|
356
|
+
}>;
|
|
339
357
|
/**
|
|
340
358
|
* Install the fetched payload to `<hqRoot>/core/packages/<pkg.name>/` (HQ
|
|
341
359
|
* v12+ layout). The HQ template (`hq-core` / `hq-core-staging`) ships
|
|
@@ -46,13 +46,14 @@ import semverValid from 'semver/functions/valid.js';
|
|
|
46
46
|
import semverValidRange from 'semver/ranges/valid.js';
|
|
47
47
|
import semverGt from 'semver/functions/gt.js';
|
|
48
48
|
import { findHqRoot } from '../utils/manifest.js';
|
|
49
|
-
import {
|
|
49
|
+
import { codexTargets } from '../lib/install/codex-targets.js';
|
|
50
|
+
import { readHqVersion, routeContribution, listInstalledPacks, mcpServerNameForContribution } from '../utils/pack-contributions.js';
|
|
50
51
|
import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
|
|
51
52
|
import { safeExtractTarball } from './safe-extract.js';
|
|
52
53
|
import { getCompanyUid, vaultApiFetch, vaultApiFetchPublic, } from '../utils/vault-api.js';
|
|
53
54
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
54
55
|
import { formatApiKeyCapabilityDenial, peekHqApiKey, } from '../utils/resolve-vault-credential.js';
|
|
55
|
-
import { redactSecrets, SECRET_REDACTION, isMcpRegistrationEnabled, registerMcpServers, McpManifestError, } from './mcp-registration.js';
|
|
56
|
+
import { redactSecrets, SECRET_REDACTION, codexConfigFormat, codexConfigPath, isMcpRegistrationEnabled, registerMcpServers, readConfigDoc, unregisterMcpServers, McpManifestError, } from './mcp-registration.js';
|
|
56
57
|
import { listSecretCacheScopes } from '../utils/secrets-cache.js';
|
|
57
58
|
import { loadRevealedSecrets } from './secrets.js';
|
|
58
59
|
import { syncCodexPackHooks, syncCodexPackSkills } from '../lib/install/codex-pack-contributions.js';
|
|
@@ -1492,7 +1493,7 @@ function extractDeferredSecretName(message) {
|
|
|
1492
1493
|
* - ANY OTHER error → RE-THROW (abort install). Only the
|
|
1493
1494
|
* unresolvable-secret case is swallowed.
|
|
1494
1495
|
*/
|
|
1495
|
-
async function wireMcpServers(pkg, destDir, company) {
|
|
1496
|
+
export async function wireMcpServers(pkg, destDir, company) {
|
|
1496
1497
|
const loadManifest = (name) => loadMcpManifestFrom(destDir, name);
|
|
1497
1498
|
const registered = [];
|
|
1498
1499
|
const skipped = [];
|
|
@@ -1501,14 +1502,29 @@ async function wireMcpServers(pkg, destDir, company) {
|
|
|
1501
1502
|
const registrationEnabled = isMcpRegistrationEnabled();
|
|
1502
1503
|
for (const name of pkg.contributes.mcp ?? []) {
|
|
1503
1504
|
try {
|
|
1505
|
+
// The reserved hq-anywhere pack shares the global `hq` endpoint. Remove
|
|
1506
|
+
// the pre-pairing alias left by older installs before registering `hq`.
|
|
1507
|
+
const registrationName = mcpServerNameForContribution(pkg.name, name);
|
|
1508
|
+
const codexHome = process.env.HOME ?? os.homedir();
|
|
1509
|
+
const globalStatePresent = pkg.name === 'hq-anywhere' && fs.existsSync(codexTargets(codexHome).stateFile);
|
|
1510
|
+
const codexDoc = globalStatePresent ? readConfigDoc(codexConfigPath({ home: codexHome }), codexConfigFormat).doc : undefined;
|
|
1511
|
+
const codexServers = codexDoc?.value.mcp_servers;
|
|
1512
|
+
const globalCodexOwnsHq = globalStatePresent && codexServers?.hq?._hqPack === pkg.name;
|
|
1504
1513
|
const resolveSecret = await makeInstallSecretResolver(collectManifestSecretNames(loadManifest(name)), company);
|
|
1505
1514
|
// Per-server call: registerMcpServers throws on the FIRST unresolvable
|
|
1506
1515
|
// secret, so calling it one name at a time lets us catch + continue.
|
|
1507
|
-
const results = registerMcpServers(pkg.name, [
|
|
1508
|
-
loadManifest,
|
|
1516
|
+
const results = registerMcpServers(pkg.name, [registrationName], {
|
|
1517
|
+
loadManifest: (registeredName) => loadManifest(registeredName === registrationName ? name : registeredName),
|
|
1509
1518
|
resolveSecret,
|
|
1510
1519
|
registrationEnabled,
|
|
1520
|
+
preserveCodex: globalCodexOwnsHq,
|
|
1511
1521
|
});
|
|
1522
|
+
// Only retire the pre-pairing alias after the canonical registration has
|
|
1523
|
+
// been written to Codex. A kill switch, absent Codex runtime, or deferred
|
|
1524
|
+
// secret must leave the prior working entry untouched.
|
|
1525
|
+
if (registrationName !== name && results.some((result) => !('skipped' in result.codex) || (globalCodexOwnsHq && result.codex.reason === 'shared global install owns Codex hq endpoint'))) {
|
|
1526
|
+
unregisterMcpServers(pkg.name, [name]);
|
|
1527
|
+
}
|
|
1512
1528
|
if (results.length > 0)
|
|
1513
1529
|
registered.push(name);
|
|
1514
1530
|
}
|
|
@@ -1763,7 +1779,7 @@ export async function installPack(source, opts = {}) {
|
|
|
1763
1779
|
const codexHome = process.env.HOME ?? os.homedir();
|
|
1764
1780
|
const packHooks = pkg.contributes.hooks ?? [];
|
|
1765
1781
|
const skillWarnings = syncCodexPackSkills(destDir, pkg.name, pkg.contributes.skills ?? [], false, codexHome);
|
|
1766
|
-
syncCodexPackHooks(destDir, packHooks, false, codexHome);
|
|
1782
|
+
syncCodexPackHooks(destDir, packHooks, false, codexHome, hqRoot, pkg.name);
|
|
1767
1783
|
for (const warning of skillWarnings)
|
|
1768
1784
|
say(chalk.yellow(` ! ${warning}`));
|
|
1769
1785
|
// US-007 — register `contributes.mcp` servers into the shared Claude/Codex
|
package/dist/commands/packs.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import * as fs from 'fs';
|
|
20
20
|
import * as os from 'os';
|
|
21
|
+
import { codexTargets } from '../lib/install/codex-targets.js';
|
|
21
22
|
import * as path from 'path';
|
|
22
23
|
import * as readline from 'readline';
|
|
23
24
|
import { spawnSync } from 'child_process';
|
|
@@ -252,7 +253,7 @@ async function runUpdate(name, opts) {
|
|
|
252
253
|
// Remove Codex registrations and skills from the old manifest before the
|
|
253
254
|
// pack directory is replaced. The replacement may rename its commands or
|
|
254
255
|
// drop contributions entirely, so the new manifest cannot clean those.
|
|
255
|
-
syncCodexPackHooks(pack.dir, m.contributes?.hooks ?? [], true);
|
|
256
|
+
syncCodexPackHooks(pack.dir, m.contributes?.hooks ?? [], true, process.env.HOME ?? os.homedir(), hqRoot, pname);
|
|
256
257
|
syncCodexPackSkills(pack.dir, pname, m.contributes?.skills ?? [], true);
|
|
257
258
|
// Un-wire old contributions first so a contribution dropped by the new
|
|
258
259
|
// version doesn't leave a dangling host symlink. installPack re-wires.
|
|
@@ -314,7 +315,7 @@ async function runUninstall(name, opts) {
|
|
|
314
315
|
// marker/absolute command still points at this pack.
|
|
315
316
|
try {
|
|
316
317
|
const home = process.env.HOME ?? os.homedir();
|
|
317
|
-
syncCodexPackHooks(packDir, contributes.hooks ?? [], true, home);
|
|
318
|
+
syncCodexPackHooks(packDir, contributes.hooks ?? [], true, home, hqRoot, name);
|
|
318
319
|
warnings.push(...syncCodexPackSkills(packDir, name, contributes.skills ?? [], true, home));
|
|
319
320
|
}
|
|
320
321
|
catch (e) {
|
|
@@ -325,7 +326,12 @@ async function runUninstall(name, opts) {
|
|
|
325
326
|
// pack's `_hqPack`, and skip-and-warns on any foreign/unstamped same-named entry.
|
|
326
327
|
// Tolerant of a Codex-less host / absent config; idempotent on re-run.
|
|
327
328
|
try {
|
|
328
|
-
const
|
|
329
|
+
const home = process.env.HOME ?? os.homedir();
|
|
330
|
+
const globalCodexState = codexTargets(home).stateFile;
|
|
331
|
+
const mcp = unwirePackMcp(name, contributes, {
|
|
332
|
+
env: { home },
|
|
333
|
+
preserveCodex: name === 'hq-anywhere' && fs.existsSync(globalCodexState),
|
|
334
|
+
});
|
|
329
335
|
for (const server of mcp.servers) {
|
|
330
336
|
if ('skipped' in server.claude)
|
|
331
337
|
continue; // (claude is always inspected; never skipped)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The bundled shell companions emit awk's open-file diagnostic. GNU awk and
|
|
5
|
+
* the BSD awk shipped by macOS format that diagnostic differently. Prefer
|
|
6
|
+
* the executable selected by PATH; macOS runners can put GNU awk ahead of the
|
|
7
|
+
* system awk.
|
|
8
|
+
*/
|
|
9
|
+
function usesGnuAwk(pathValue, platform) {
|
|
10
|
+
for (const directory of (pathValue ?? "/usr/bin:/bin").split(path.delimiter)) {
|
|
11
|
+
if (!directory)
|
|
12
|
+
continue;
|
|
13
|
+
const candidate = path.join(directory, "awk");
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.statSync(candidate).isFile())
|
|
16
|
+
continue;
|
|
17
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
18
|
+
const resolved = fs.realpathSync(candidate);
|
|
19
|
+
if (/^gawk(?:[-.]|$)/i.test(path.basename(resolved)))
|
|
20
|
+
return true;
|
|
21
|
+
if (resolved.split(path.sep).some((segment) => /^gawk(?:[-.]|$)/i.test(segment)))
|
|
22
|
+
return true;
|
|
23
|
+
// Some runners expose GNU awk under the generic name `awk`.
|
|
24
|
+
if (fs.readFileSync(resolved).includes(Buffer.from("GNU Awk")))
|
|
25
|
+
return true;
|
|
26
|
+
return platform !== "darwin";
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// execvp continues through PATH entries that lack an executable awk.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return platform !== "darwin";
|
|
33
|
+
}
|
|
34
|
+
export function awkCannotOpenFileDiagnostic(filename, reason, sourceLine = 1, platform = process.platform, pathValue = process.env.PATH) {
|
|
35
|
+
if (!usesGnuAwk(pathValue, platform))
|
|
36
|
+
return `awk: can't open file ${filename}\n source line number ${sourceLine}\n`;
|
|
37
|
+
return `awk: fatal: cannot open file \`${filename}' for reading: ${reason}\n`;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=awk-diagnostics.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type LocaleEnvironment = Partial<Pick<NodeJS.ProcessEnv, "LC_ALL" | "LC_CTYPE" | "LANG">>;
|
|
2
|
+
/** Whether the effective locale makes the shell's character classes byte-oriented. */
|
|
3
|
+
export declare function isByteOrientedLocale(env: LocaleEnvironment, platform?: NodeJS.Platform, localeDirectory?: string): boolean;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=byte-oriented-locale.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
function macLocaleIsRecognized(locale, localeDirectory) {
|
|
4
|
+
// Locale names accepted by macOS are backed by LC_CTYPE data in this directory.
|
|
5
|
+
// Keep the complete libc locale name intact; Intl's locale database is unrelated.
|
|
6
|
+
if (locale.includes("/"))
|
|
7
|
+
return false;
|
|
8
|
+
return fs.existsSync(path.join(localeDirectory, locale, "LC_CTYPE"));
|
|
9
|
+
}
|
|
10
|
+
/** Whether the effective locale makes the shell's character classes byte-oriented. */
|
|
11
|
+
export function isByteOrientedLocale(env, platform = process.platform, localeDirectory = "/usr/share/locale") {
|
|
12
|
+
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG;
|
|
13
|
+
if (!locale)
|
|
14
|
+
return true;
|
|
15
|
+
if (/^(?:C|POSIX)$/i.test(locale) || /^C@/i.test(locale))
|
|
16
|
+
return true;
|
|
17
|
+
if (platform === "darwin")
|
|
18
|
+
return !macLocaleIsRecognized(locale, localeDirectory);
|
|
19
|
+
if (/^(?:C|POSIX)\./i.test(locale))
|
|
20
|
+
return !/^\w+\.utf-?8$/i.test(locale);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=byte-oriented-locale.js.map
|
|
@@ -2,7 +2,7 @@ type Doc = Record<string, unknown>;
|
|
|
2
2
|
export declare function syncCodexPackSkills(packDir: string, packName: string, names: string[], remove?: boolean, home?: string): string[];
|
|
3
3
|
export declare function mergeCodexPackHooks(doc: Doc, packDir: string, items: string[]): Doc;
|
|
4
4
|
export declare function removeCodexPackHooks(doc: Doc, packDir: string, items: string[]): Doc;
|
|
5
|
-
export declare function syncCodexPackHooks(packDir: string, items: string[], remove?: boolean, home?: string): void;
|
|
5
|
+
export declare function syncCodexPackHooks(packDir: string, items: string[], remove?: boolean, home?: string, hqRoot?: string, packName?: string): void;
|
|
6
6
|
export declare function readCodexPackHooks(target: string): Doc;
|
|
7
7
|
export {};
|
|
8
8
|
//# sourceMappingURL=codex-pack-contributions.d.ts.map
|
|
@@ -3,6 +3,8 @@ import * as os from 'node:os';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { anywhereRuntimeEnabled } from '../../commands/anywhere-runtime-flag.js';
|
|
5
5
|
import { jsonFormat, readConfigDoc, writeConfigAtomic } from '../../commands/mcp-registration.js';
|
|
6
|
+
import { hookTargetFor } from '../../commands/install-global.js';
|
|
7
|
+
import { addCodexHooks, codexTargets, removeCodexHooks } from './codex-targets.js';
|
|
6
8
|
const configFormat = {
|
|
7
9
|
parse: jsonFormat.parse,
|
|
8
10
|
serialize: jsonFormat.serialize,
|
|
@@ -186,14 +188,35 @@ function removeCommands(hooks, commands) {
|
|
|
186
188
|
delete hooks[event];
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
|
-
export function syncCodexPackHooks(packDir, items, remove = false, home = os.homedir()) {
|
|
191
|
+
export function syncCodexPackHooks(packDir, items, remove = false, home = os.homedir(), hqRoot, packName = 'hq-anywhere') {
|
|
190
192
|
assertContributionNames(items);
|
|
191
193
|
if ((!remove && !anywhereRuntimeEnabled()) || !fs.existsSync(path.join(home, '.codex')))
|
|
192
194
|
return;
|
|
193
195
|
const codexItems = items.filter((item) => fs.existsSync(path.join(packDir, 'hooks', `${item}-hooks.json`)));
|
|
194
196
|
if (codexItems.length === 0)
|
|
195
197
|
return;
|
|
198
|
+
const sharedAnywherePack = packName === 'hq-anywhere';
|
|
199
|
+
const sharedGlobalInstall = sharedAnywherePack && fs.existsSync(codexTargets(home).stateFile);
|
|
200
|
+
// The global Codex installer owns the canonical command while its state is
|
|
201
|
+
// present. The pack still contributes skills/MCP, but must not register a
|
|
202
|
+
// second hook or remove the shared hook during pack uninstall/update.
|
|
203
|
+
if (sharedGlobalInstall)
|
|
204
|
+
return;
|
|
196
205
|
const target = hooksPath(home);
|
|
206
|
+
if (hqRoot && sharedAnywherePack) {
|
|
207
|
+
const hookTarget = hookTargetFor(hqRoot, home);
|
|
208
|
+
writeConfigAtomic({
|
|
209
|
+
target,
|
|
210
|
+
pack: 'hq-anywhere',
|
|
211
|
+
format: configFormat,
|
|
212
|
+
merge: (doc) => remove
|
|
213
|
+
? removeCodexHooks(removeCodexPackHooks(doc, packDir, codexItems), hqRoot)
|
|
214
|
+
: addCodexHooks(removeCodexPackHooks(doc, packDir, codexItems), hqRoot, hookTarget),
|
|
215
|
+
assertName: remove ? `hooks-remove:${path.resolve(hqRoot)}${path.sep}` : codexItems[0],
|
|
216
|
+
env: { home },
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
197
220
|
writeConfigAtomic({
|
|
198
221
|
target, pack: 'hq-anywhere', format: configFormat,
|
|
199
222
|
merge: (doc) => remove ? removeCodexPackHooks(doc, packDir, codexItems) : mergeCodexPackHooks(doc, packDir, codexItems),
|
|
@@ -93,7 +93,9 @@ function isAdapterEntry(entry, hqRoot) {
|
|
|
93
93
|
return false;
|
|
94
94
|
const cmd = entry.command;
|
|
95
95
|
return (typeof cmd === 'string' &&
|
|
96
|
-
(cmd.includes(JSON.stringify(codexAdapterPath(hqRoot))) ||
|
|
96
|
+
(cmd.includes(JSON.stringify(codexAdapterPath(hqRoot))) ||
|
|
97
|
+
cmd.includes(JSON.stringify(shimPath(hqRoot))) ||
|
|
98
|
+
cmd.includes(JSON.stringify(path.join(hqRoot, 'core', 'packages', 'hq-anywhere', 'hooks', 'codex.sh')))));
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
99
101
|
* Add one HQ hook group per event; idempotent. An HQ entry for the other
|
|
@@ -54,6 +54,8 @@ export type { WireMode };
|
|
|
54
54
|
* rather than relying on `linkFor` happening to return `null`.
|
|
55
55
|
*/
|
|
56
56
|
export declare function routeContribution(key: PackContributeKey): WireMode;
|
|
57
|
+
/** Keep the reserved hq-anywhere pack on the same user-visible MCP name as global HQ. */
|
|
58
|
+
export declare function mcpServerNameForContribution(packName: string, contributionName: string): string;
|
|
57
59
|
/**
|
|
58
60
|
* The subset of a pack's declared `contributes` keys that are `wire: 'merge'`
|
|
59
61
|
* (e.g. `mcp`) — the keys that MUST be routed to `registerMcpServers` (US-006)
|
|
@@ -142,6 +144,8 @@ export interface UnwirePackMcpOptions {
|
|
|
142
144
|
lock?: AcquireLockOptions;
|
|
143
145
|
/** Backup timestamp override (deterministic tests). */
|
|
144
146
|
stamp?: string;
|
|
147
|
+
/** Keep the shared Codex `hq` endpoint when global Codex install state remains. */
|
|
148
|
+
preserveCodex?: boolean;
|
|
145
149
|
}
|
|
146
150
|
/**
|
|
147
151
|
* Un-register the `wire: 'merge'` (MCP) servers a pack declares — the parallel to
|
|
@@ -60,6 +60,10 @@ export { CONTRIBUTION_TABLE };
|
|
|
60
60
|
export function routeContribution(key) {
|
|
61
61
|
return CONTRIBUTION_TABLE[key].wire;
|
|
62
62
|
}
|
|
63
|
+
/** Keep the reserved hq-anywhere pack on the same user-visible MCP name as global HQ. */
|
|
64
|
+
export function mcpServerNameForContribution(packName, contributionName) {
|
|
65
|
+
return packName === 'hq-anywhere' && contributionName === 'hq-anywhere' ? 'hq' : contributionName;
|
|
66
|
+
}
|
|
63
67
|
/**
|
|
64
68
|
* The subset of a pack's declared `contributes` keys that are `wire: 'merge'`
|
|
65
69
|
* (e.g. `mcp`) — the keys that MUST be routed to `registerMcpServers` (US-006)
|
|
@@ -333,14 +337,26 @@ export function unwirePack(hqRoot, packDir, contributes) {
|
|
|
333
337
|
export function unwirePackMcp(packName, contributes, options) {
|
|
334
338
|
const servers = [];
|
|
335
339
|
for (const key of mergeKeys(contributes)) {
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
340
|
+
const declaredNames = (contributes[key] ?? []).filter((n) => typeof n === 'string' && n.length > 0);
|
|
341
|
+
if (declaredNames.length === 0)
|
|
338
342
|
continue;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
for (const declaredName of declaredNames) {
|
|
344
|
+
const registeredName = mcpServerNameForContribution(packName, declaredName);
|
|
345
|
+
if (registeredName !== declaredName) {
|
|
346
|
+
// Remove the prior pack-only alias from older installs during upgrade/uninstall.
|
|
347
|
+
servers.push(...unregisterMcpServers(packName, [declaredName], {
|
|
348
|
+
env: options?.env,
|
|
349
|
+
lock: options?.lock,
|
|
350
|
+
stamp: options?.stamp,
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
servers.push(...unregisterMcpServers(packName, [registeredName], {
|
|
354
|
+
env: options?.env,
|
|
355
|
+
lock: options?.lock,
|
|
356
|
+
stamp: options?.stamp,
|
|
357
|
+
preserveCodex: options?.preserveCodex && packName === 'hq-anywhere' && registeredName === 'hq',
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
344
360
|
}
|
|
345
361
|
return { servers };
|
|
346
362
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function executableCandidates(command: string, pathValue?: string, platform?: NodeJS.Platform, pathExt?: string): string[];
|
|
2
|
+
export declare function isExecutableOnPath(command: string, pathValue?: string, platform?: NodeJS.Platform, pathExt?: string): boolean;
|
|
3
|
+
//# sourceMappingURL=resolve-executable.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function executableCandidates(command, pathValue = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM') {
|
|
4
|
+
const extensions = platform === 'win32'
|
|
5
|
+
? (pathExt || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
|
|
6
|
+
: [''];
|
|
7
|
+
const candidates = path.isAbsolute(command)
|
|
8
|
+
? [command]
|
|
9
|
+
: pathValue.split(platform === 'win32' ? ';' : path.delimiter).filter(Boolean).flatMap((directory) => extensions.map((extension) => path.join(directory, `${command}${extension}`)));
|
|
10
|
+
return candidates;
|
|
11
|
+
}
|
|
12
|
+
export function isExecutableOnPath(command, pathValue = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM') {
|
|
13
|
+
return executableCandidates(command, pathValue, platform, pathExt).some((candidate) => {
|
|
14
|
+
try {
|
|
15
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
16
|
+
return fs.statSync(candidate).isFile();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=resolve-executable.js.map
|