@indigoai-us/hq-cli 5.323.0 → 5.325.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 +11 -1
- package/dist/bin/hq-auth-refresh.d.ts +5 -1
- package/dist/bin/hq-auth-refresh.js +33 -1
- package/dist/command-catalog.generated.d.ts +125 -6
- package/dist/command-catalog.generated.js +162 -6
- package/dist/command-registration-plan.d.ts +24 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
- package/dist/commands/anywhere-runtime-flag.js +22 -0
- package/dist/commands/core-derive-trigger-facts-native.js +37 -3
- package/dist/commands/daemon.js +39 -2
- package/dist/commands/db-provision.js +6 -1
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +10 -0
- package/dist/commands/install-global.d.ts +46 -0
- package/dist/commands/install-global.js +674 -0
- package/dist/commands/link.d.ts +30 -0
- package/dist/commands/link.js +184 -0
- package/dist/commands/mcp-status.d.ts +6 -0
- package/dist/commands/mcp-status.js +21 -4
- package/dist/commands/pkg-install.js +20 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +52 -6
- package/dist/fast-core.d.ts +2 -1
- package/dist/fast-core.js +12 -8
- package/dist/index.js +9 -7
- package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
- package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
- package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
- package/dist/lib/core-utils/timeout-guard-native.js +4 -30
- package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
- package/dist/lib/core-utils/timeout-guard.js +2 -1
- package/dist/lib/daemon/convergence.d.ts +42 -0
- package/dist/lib/daemon/convergence.js +66 -0
- package/dist/lib/daemon/heartbeat.d.ts +43 -0
- package/dist/lib/daemon/heartbeat.js +70 -0
- package/dist/lib/daemon/ops/journal.d.ts +29 -0
- package/dist/lib/daemon/ops/journal.js +141 -0
- package/dist/lib/daemon/ops/memory.d.ts +20 -0
- package/dist/lib/daemon/ops/memory.js +65 -0
- package/dist/lib/daemon/ops/mesh.d.ts +38 -0
- package/dist/lib/daemon/ops/mesh.js +70 -0
- package/dist/lib/daemon/ops/policy.d.ts +78 -0
- package/dist/lib/daemon/ops/policy.js +283 -0
- package/dist/lib/daemon/protocol.d.ts +68 -0
- package/dist/lib/daemon/protocol.js +154 -0
- package/dist/lib/daemon/run.d.ts +10 -0
- package/dist/lib/daemon/run.js +85 -3
- package/dist/lib/daemon/server.d.ts +36 -0
- package/dist/lib/daemon/server.js +306 -0
- package/dist/lib/daemon/session-registry.d.ts +30 -0
- package/dist/lib/daemon/session-registry.js +42 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
- package/dist/lib/doctor/checks/anywhere.js +182 -0
- package/dist/lib/doctor/registry.js +6 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/install/claude-targets.d.ts +47 -0
- package/dist/lib/install/claude-targets.js +203 -0
- package/dist/lib/install/codex-targets.d.ts +57 -0
- package/dist/lib/install/codex-targets.js +187 -0
- package/dist/lib/mesh/live/session-seq.js +62 -9
- package/dist/lib/registry/normalize-remote.d.ts +21 -0
- package/dist/lib/registry/normalize-remote.js +68 -0
- package/dist/lib/registry/registry.d.ts +75 -0
- package/dist/lib/registry/registry.js +261 -0
- package/dist/utils/cognito-session.d.ts +5 -1
- package/dist/utils/cognito-session.js +8 -12
- package/dist/utils/feedback-diagnostics.d.ts +2 -0
- package/dist/utils/version-check.d.ts +2 -0
- package/dist/utils/version-check.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Events master-hook.sh is registered for at user scope. */
|
|
2
|
+
export declare const GLOBAL_HOOK_EVENTS: readonly ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
|
|
3
|
+
/** Name of the HQ MCP server entry in ~/.claude.json. */
|
|
4
|
+
export declare const HQ_MCP_SERVER_NAME = "hq";
|
|
5
|
+
/** `_hqPack` provenance stamped on the HQ MCP server entry. */
|
|
6
|
+
export declare const HQ_ANYWHERE_PACK = "hq-anywhere";
|
|
7
|
+
/** PATH for the stdio MCP subprocess (policy hq-mcp-absolute-paths). */
|
|
8
|
+
export declare const MCP_SUBPROCESS_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
9
|
+
/** Prefix for skill symlinks under ~/.claude/skills. */
|
|
10
|
+
export declare const SKILL_LINK_PREFIX = "hq-";
|
|
11
|
+
export declare const CLAUDE_MD_BEGIN = "<!-- hq-anywhere:begin (managed by `hq install --global`; remove with `hq uninstall --global`) -->";
|
|
12
|
+
export declare const CLAUDE_MD_END = "<!-- hq-anywhere:end -->";
|
|
13
|
+
export interface ClaudeTargets {
|
|
14
|
+
hqPointer: string;
|
|
15
|
+
claudeMd: string;
|
|
16
|
+
skillsDir: string;
|
|
17
|
+
settingsJson: string;
|
|
18
|
+
claudeJson: string;
|
|
19
|
+
stateFile: string;
|
|
20
|
+
hqConfigFile: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function claudeTargets(home: string): ClaudeTargets;
|
|
23
|
+
export declare function masterHookPath(hqRoot: string): string;
|
|
24
|
+
export declare function charterPath(hqRoot: string): string;
|
|
25
|
+
export declare function hqSkillsSource(hqRoot: string): string;
|
|
26
|
+
/** The exact hook command registered for one event (absolute path, quoted). */
|
|
27
|
+
export declare function hookCommand(hqRoot: string, event: string): string;
|
|
28
|
+
export declare function claudeMdBlock(hqRoot: string): string;
|
|
29
|
+
/** Append the block (replacing any prior HQ block). `null` input = file absent. */
|
|
30
|
+
export declare function addClaudeMdBlock(current: string | null, hqRoot: string): string;
|
|
31
|
+
/** Strip the HQ block, plus the separator `addClaudeMdBlock` put before it. */
|
|
32
|
+
export declare function removeClaudeMdBlock(current: string): string;
|
|
33
|
+
export declare function hasClaudeMdBlock(current: string | null): boolean;
|
|
34
|
+
type Doc = Record<string, unknown>;
|
|
35
|
+
/** Add one master-hook group per event; idempotent. Foreign hooks are untouched. */
|
|
36
|
+
export declare function addHqHooks(doc: Doc, hqRoot: string): Doc;
|
|
37
|
+
/** Remove only HQ master-hook entries; drop containers this left empty. */
|
|
38
|
+
export declare function removeHqHooks(doc: Doc, hqRoot: string): Doc;
|
|
39
|
+
export declare function hasAllHqHooks(doc: Doc, hqRoot: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Minimal line diff (LCS) with -/+ prefixes. Only changed lines and `context`
|
|
42
|
+
* unchanged lines around them are shown, so a dry run never echoes a whole
|
|
43
|
+
* config file (which may hold unrelated tokens). Returns '(no changes)' when equal.
|
|
44
|
+
*/
|
|
45
|
+
export declare function lineDiff(before: string, after: string, context?: number): string;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=claude-targets.d.ts.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code user-scope targets for `hq install --global --runtime claude`
|
|
3
|
+
* (hq-anywhere-runtime US-005). Paths and PURE transforms only; the command in
|
|
4
|
+
* src/commands/install-global.ts does the IO through the mcp-registration
|
|
5
|
+
* safe-write substrate.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
/** Events master-hook.sh is registered for at user scope. */
|
|
9
|
+
export const GLOBAL_HOOK_EVENTS = [
|
|
10
|
+
'SessionStart',
|
|
11
|
+
'UserPromptSubmit',
|
|
12
|
+
'PreToolUse',
|
|
13
|
+
'PostToolUse',
|
|
14
|
+
'Stop',
|
|
15
|
+
];
|
|
16
|
+
/** Name of the HQ MCP server entry in ~/.claude.json. */
|
|
17
|
+
export const HQ_MCP_SERVER_NAME = 'hq';
|
|
18
|
+
/** `_hqPack` provenance stamped on the HQ MCP server entry. */
|
|
19
|
+
export const HQ_ANYWHERE_PACK = 'hq-anywhere';
|
|
20
|
+
/** PATH for the stdio MCP subprocess (policy hq-mcp-absolute-paths). */
|
|
21
|
+
export const MCP_SUBPROCESS_PATH = '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin';
|
|
22
|
+
/** Prefix for skill symlinks under ~/.claude/skills. */
|
|
23
|
+
export const SKILL_LINK_PREFIX = 'hq-';
|
|
24
|
+
export const CLAUDE_MD_BEGIN = '<!-- hq-anywhere:begin (managed by `hq install --global`; remove with `hq uninstall --global`) -->';
|
|
25
|
+
export const CLAUDE_MD_END = '<!-- hq-anywhere:end -->';
|
|
26
|
+
export function claudeTargets(home) {
|
|
27
|
+
return {
|
|
28
|
+
hqPointer: path.join(home, '.hq', 'root'),
|
|
29
|
+
claudeMd: path.join(home, '.claude', 'CLAUDE.md'),
|
|
30
|
+
skillsDir: path.join(home, '.claude', 'skills'),
|
|
31
|
+
settingsJson: path.join(home, '.claude', 'settings.json'),
|
|
32
|
+
claudeJson: path.join(home, '.claude.json'),
|
|
33
|
+
stateFile: path.join(home, '.hq', 'anywhere', 'claude-install.json'),
|
|
34
|
+
hqConfigFile: path.join(home, '.hq', 'config.json'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function masterHookPath(hqRoot) {
|
|
38
|
+
return path.join(hqRoot, '.claude', 'hooks', 'master-hook.sh');
|
|
39
|
+
}
|
|
40
|
+
export function charterPath(hqRoot) {
|
|
41
|
+
return path.join(hqRoot, '.claude', 'CLAUDE.md');
|
|
42
|
+
}
|
|
43
|
+
export function hqSkillsSource(hqRoot) {
|
|
44
|
+
return path.join(hqRoot, '.claude', 'skills');
|
|
45
|
+
}
|
|
46
|
+
/** The exact hook command registered for one event (absolute path, quoted). */
|
|
47
|
+
export function hookCommand(hqRoot, event) {
|
|
48
|
+
return `bash ${JSON.stringify(masterHookPath(hqRoot))} ${event}`;
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// CLAUDE.md block.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
export function claudeMdBlock(hqRoot) {
|
|
54
|
+
return `${CLAUDE_MD_BEGIN}\n@${charterPath(hqRoot)}\n${CLAUDE_MD_END}\n`;
|
|
55
|
+
}
|
|
56
|
+
/** Append the block (replacing any prior HQ block). `null` input = file absent. */
|
|
57
|
+
export function addClaudeMdBlock(current, hqRoot) {
|
|
58
|
+
const base = current === null ? '' : removeClaudeMdBlock(current);
|
|
59
|
+
const block = claudeMdBlock(hqRoot);
|
|
60
|
+
if (base === '')
|
|
61
|
+
return block;
|
|
62
|
+
return base.endsWith('\n') ? `${base}\n${block}` : `${base}\n\n${block}`;
|
|
63
|
+
}
|
|
64
|
+
/** Strip the HQ block, plus the separator `addClaudeMdBlock` put before it. */
|
|
65
|
+
export function removeClaudeMdBlock(current) {
|
|
66
|
+
const begin = current.indexOf(CLAUDE_MD_BEGIN);
|
|
67
|
+
if (begin === -1)
|
|
68
|
+
return current;
|
|
69
|
+
const endIdx = current.indexOf(CLAUDE_MD_END, begin);
|
|
70
|
+
if (endIdx === -1)
|
|
71
|
+
return current;
|
|
72
|
+
let end = endIdx + CLAUDE_MD_END.length;
|
|
73
|
+
if (current[end] === '\n')
|
|
74
|
+
end += 1;
|
|
75
|
+
let start = begin;
|
|
76
|
+
// Undo the blank-line separator. (Exact bytes are restored by the caller from
|
|
77
|
+
// install state when the file is otherwise unchanged.)
|
|
78
|
+
if (start >= 2 && current.slice(start - 2, start) === '\n\n')
|
|
79
|
+
start -= 1;
|
|
80
|
+
return current.slice(0, start) + current.slice(end);
|
|
81
|
+
}
|
|
82
|
+
export function hasClaudeMdBlock(current) {
|
|
83
|
+
return current !== null && current.includes(CLAUDE_MD_BEGIN) && current.includes(CLAUDE_MD_END);
|
|
84
|
+
}
|
|
85
|
+
function isHqHookEntry(entry, hqRoot) {
|
|
86
|
+
if (!entry || typeof entry !== 'object')
|
|
87
|
+
return false;
|
|
88
|
+
const cmd = entry.command;
|
|
89
|
+
return typeof cmd === 'string' && cmd.includes(JSON.stringify(masterHookPath(hqRoot)));
|
|
90
|
+
}
|
|
91
|
+
/** Add one master-hook group per event; idempotent. Foreign hooks are untouched. */
|
|
92
|
+
export function addHqHooks(doc, hqRoot) {
|
|
93
|
+
const hooks = { ...(doc.hooks ?? {}) };
|
|
94
|
+
let changed = false;
|
|
95
|
+
for (const event of GLOBAL_HOOK_EVENTS) {
|
|
96
|
+
const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
97
|
+
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => isHqHookEntry(h, hqRoot)));
|
|
98
|
+
if (present)
|
|
99
|
+
continue;
|
|
100
|
+
groups.push({ hooks: [{ type: 'command', command: hookCommand(hqRoot, event), timeout: 300 }] });
|
|
101
|
+
hooks[event] = groups;
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
return changed ? { ...doc, hooks } : doc;
|
|
105
|
+
}
|
|
106
|
+
/** Remove only HQ master-hook entries; drop containers this left empty. */
|
|
107
|
+
export function removeHqHooks(doc, hqRoot) {
|
|
108
|
+
if (!doc.hooks || typeof doc.hooks !== 'object' || Array.isArray(doc.hooks))
|
|
109
|
+
return doc;
|
|
110
|
+
const hooks = { ...doc.hooks };
|
|
111
|
+
let changed = false;
|
|
112
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
113
|
+
if (!Array.isArray(value))
|
|
114
|
+
continue;
|
|
115
|
+
const groups = [];
|
|
116
|
+
for (const g of value) {
|
|
117
|
+
if (!g || !Array.isArray(g.hooks)) {
|
|
118
|
+
groups.push(g);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const kept = g.hooks.filter((h) => !isHqHookEntry(h, hqRoot));
|
|
122
|
+
if (kept.length === g.hooks.length)
|
|
123
|
+
groups.push(g);
|
|
124
|
+
else {
|
|
125
|
+
changed = true;
|
|
126
|
+
if (kept.length > 0)
|
|
127
|
+
groups.push({ ...g, hooks: kept });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (groups.length === 0)
|
|
131
|
+
delete hooks[event];
|
|
132
|
+
else
|
|
133
|
+
hooks[event] = groups;
|
|
134
|
+
}
|
|
135
|
+
if (!changed)
|
|
136
|
+
return doc;
|
|
137
|
+
const next = { ...doc };
|
|
138
|
+
if (Object.keys(hooks).length === 0)
|
|
139
|
+
delete next.hooks;
|
|
140
|
+
else
|
|
141
|
+
next.hooks = hooks;
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
export function hasAllHqHooks(doc, hqRoot) {
|
|
145
|
+
return addHqHooks(doc, hqRoot) === doc;
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Dry-run diff.
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
/**
|
|
151
|
+
* Minimal line diff (LCS) with -/+ prefixes. Only changed lines and `context`
|
|
152
|
+
* unchanged lines around them are shown, so a dry run never echoes a whole
|
|
153
|
+
* config file (which may hold unrelated tokens). Returns '(no changes)' when equal.
|
|
154
|
+
*/
|
|
155
|
+
export function lineDiff(before, after, context = 2) {
|
|
156
|
+
const a = before === '' ? [] : before.replace(/\n$/, '').split('\n');
|
|
157
|
+
const b = after === '' ? [] : after.replace(/\n$/, '').split('\n');
|
|
158
|
+
const n = a.length;
|
|
159
|
+
const m = b.length;
|
|
160
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
161
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
162
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
163
|
+
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const ops = [];
|
|
167
|
+
let i = 0;
|
|
168
|
+
let j = 0;
|
|
169
|
+
while (i < n && j < m) {
|
|
170
|
+
if (a[i] === b[j]) {
|
|
171
|
+
ops.push({ tag: ' ', line: a[i] });
|
|
172
|
+
i++;
|
|
173
|
+
j++;
|
|
174
|
+
}
|
|
175
|
+
else if (lcs[i + 1][j] >= lcs[i][j + 1])
|
|
176
|
+
ops.push({ tag: '-', line: a[i++] });
|
|
177
|
+
else
|
|
178
|
+
ops.push({ tag: '+', line: b[j++] });
|
|
179
|
+
}
|
|
180
|
+
while (i < n)
|
|
181
|
+
ops.push({ tag: '-', line: a[i++] });
|
|
182
|
+
while (j < m)
|
|
183
|
+
ops.push({ tag: '+', line: b[j++] });
|
|
184
|
+
const changed = ops.map((o, k) => (o.tag !== ' ' ? k : -1)).filter((k) => k >= 0);
|
|
185
|
+
if (changed.length === 0)
|
|
186
|
+
return '(no changes)';
|
|
187
|
+
const keep = new Set();
|
|
188
|
+
for (const k of changed)
|
|
189
|
+
for (let d = -context; d <= context; d++)
|
|
190
|
+
keep.add(k + d);
|
|
191
|
+
const out = [];
|
|
192
|
+
let last = -1;
|
|
193
|
+
ops.forEach((o, k) => {
|
|
194
|
+
if (!keep.has(k))
|
|
195
|
+
return;
|
|
196
|
+
if (last !== -1 && k !== last + 1)
|
|
197
|
+
out.push(' ...');
|
|
198
|
+
out.push(`${o.tag === ' ' ? ' ' : o.tag} ${o.line}`);
|
|
199
|
+
last = k;
|
|
200
|
+
});
|
|
201
|
+
return out.join('\n');
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=claude-targets.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Events the Codex hook adapter is registered for at user scope. */
|
|
2
|
+
export declare const CODEX_HOOK_EVENTS: readonly ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
|
|
3
|
+
export declare const AGENTS_MD_BEGIN = "<!-- hq-anywhere:begin (managed by `hq install --global --runtime codex`; remove with `hq uninstall --global --runtime codex`) -->";
|
|
4
|
+
export declare const AGENTS_MD_END = "<!-- hq-anywhere:end -->";
|
|
5
|
+
/** Name of the legacy ad-hoc skills mirror (~/.codex/skills/hq). */
|
|
6
|
+
export declare const LEGACY_SKILLS_NAME = "hq";
|
|
7
|
+
/** A directory carrying this file is HQ-owned and may be replaced. */
|
|
8
|
+
export declare const HQ_OWNED_MARKER = ".hq-anywhere-managed";
|
|
9
|
+
/** Where an HQ-owned legacy directory is moved so uninstall can put it back. */
|
|
10
|
+
export declare const LEGACY_BACKUP_NAME = ".hq-legacy-skills-backup";
|
|
11
|
+
export interface CodexTargets {
|
|
12
|
+
codexHome: string;
|
|
13
|
+
agentsMd: string;
|
|
14
|
+
skillsDir: string;
|
|
15
|
+
hooksJson: string;
|
|
16
|
+
configToml: string;
|
|
17
|
+
stateFile: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function codexTargets(home: string): CodexTargets;
|
|
20
|
+
export declare function codexAdapterPath(hqRoot: string): string;
|
|
21
|
+
/** HQ's Codex charter: AGENTS.md at the HQ root, else the Claude charter. */
|
|
22
|
+
export declare function codexCharterSource(hqRoot: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Hook command for every event. The adapter reads the event from
|
|
25
|
+
* `hook_event_name` and falls back to its own $PWD when Codex sends no `cwd`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function codexHookCommand(hqRoot: string): string;
|
|
28
|
+
export declare function agentsMdBlock(charter: string): string;
|
|
29
|
+
/** Append the block (replacing any prior HQ block). `null` input = file absent. */
|
|
30
|
+
export declare function addAgentsMdBlock(current: string | null, charter: string): string;
|
|
31
|
+
/** Strip the HQ block, plus the separator `addAgentsMdBlock` put before it. */
|
|
32
|
+
export declare function removeAgentsMdBlock(current: string): string;
|
|
33
|
+
type Doc = Record<string, unknown>;
|
|
34
|
+
/** Add one adapter group per event; idempotent. Foreign hooks are untouched. */
|
|
35
|
+
export declare function addCodexHooks(doc: Doc, hqRoot: string): Doc;
|
|
36
|
+
/** Remove only HQ adapter entries; drop containers this left empty. */
|
|
37
|
+
export declare function removeCodexHooks(doc: Doc, hqRoot: string): Doc;
|
|
38
|
+
export declare function hasAllCodexHooks(doc: Doc, hqRoot: string): boolean;
|
|
39
|
+
export type LegacySkillsMirror = {
|
|
40
|
+
kind: 'absent';
|
|
41
|
+
}
|
|
42
|
+
/** HQ-owned symlink: its target resolves inside the HQ checkout. */
|
|
43
|
+
| {
|
|
44
|
+
kind: 'hq-symlink';
|
|
45
|
+
target: string;
|
|
46
|
+
}
|
|
47
|
+
/** HQ-owned directory: carries {@link HQ_OWNED_MARKER}. */
|
|
48
|
+
| {
|
|
49
|
+
kind: 'hq-dir';
|
|
50
|
+
}
|
|
51
|
+
/** Anything else. Left in place with a warning. */
|
|
52
|
+
| {
|
|
53
|
+
kind: 'foreign';
|
|
54
|
+
};
|
|
55
|
+
export declare function classifyLegacySkills(skillsDir: string, hqRoot: string): LegacySkillsMirror;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=codex-targets.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex user-scope targets for `hq install --global --runtime codex`
|
|
3
|
+
* (hq-anywhere-runtime US-006). Paths and PURE transforms only; the command in
|
|
4
|
+
* src/commands/install-global.ts does the IO through the mcp-registration
|
|
5
|
+
* safe-write substrate.
|
|
6
|
+
*
|
|
7
|
+
* Codex has no @import, so the HQ charter is inlined into ~/.codex/AGENTS.md
|
|
8
|
+
* between markers. Hooks go to ~/.codex/hooks.json (same group shape as Claude's
|
|
9
|
+
* settings.json) and call the existing hq-core adapter by absolute path; this
|
|
10
|
+
* module never writes a second adapter.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
/** Events the Codex hook adapter is registered for at user scope. */
|
|
15
|
+
export const CODEX_HOOK_EVENTS = [
|
|
16
|
+
'SessionStart',
|
|
17
|
+
'UserPromptSubmit',
|
|
18
|
+
'PreToolUse',
|
|
19
|
+
'PostToolUse',
|
|
20
|
+
'Stop',
|
|
21
|
+
];
|
|
22
|
+
export const AGENTS_MD_BEGIN = '<!-- hq-anywhere:begin (managed by `hq install --global --runtime codex`; remove with `hq uninstall --global --runtime codex`) -->';
|
|
23
|
+
export const AGENTS_MD_END = '<!-- hq-anywhere:end -->';
|
|
24
|
+
/** Name of the legacy ad-hoc skills mirror (~/.codex/skills/hq). */
|
|
25
|
+
export const LEGACY_SKILLS_NAME = 'hq';
|
|
26
|
+
/** A directory carrying this file is HQ-owned and may be replaced. */
|
|
27
|
+
export const HQ_OWNED_MARKER = '.hq-anywhere-managed';
|
|
28
|
+
/** Where an HQ-owned legacy directory is moved so uninstall can put it back. */
|
|
29
|
+
export const LEGACY_BACKUP_NAME = '.hq-legacy-skills-backup';
|
|
30
|
+
export function codexTargets(home) {
|
|
31
|
+
const codexHome = path.join(home, '.codex');
|
|
32
|
+
return {
|
|
33
|
+
codexHome,
|
|
34
|
+
agentsMd: path.join(codexHome, 'AGENTS.md'),
|
|
35
|
+
skillsDir: path.join(codexHome, 'skills'),
|
|
36
|
+
hooksJson: path.join(codexHome, 'hooks.json'),
|
|
37
|
+
configToml: path.join(codexHome, 'config.toml'),
|
|
38
|
+
stateFile: path.join(home, '.hq', 'anywhere', 'codex-install.json'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function codexAdapterPath(hqRoot) {
|
|
42
|
+
return path.join(hqRoot, '.codex', 'hooks', 'hq-codex-hook-adapter.sh');
|
|
43
|
+
}
|
|
44
|
+
/** HQ's Codex charter: AGENTS.md at the HQ root, else the Claude charter. */
|
|
45
|
+
export function codexCharterSource(hqRoot) {
|
|
46
|
+
const agents = path.join(hqRoot, 'AGENTS.md');
|
|
47
|
+
return fs.existsSync(agents) ? agents : path.join(hqRoot, '.claude', 'CLAUDE.md');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Hook command for every event. The adapter reads the event from
|
|
51
|
+
* `hook_event_name` and falls back to its own $PWD when Codex sends no `cwd`.
|
|
52
|
+
*/
|
|
53
|
+
export function codexHookCommand(hqRoot) {
|
|
54
|
+
return `/bin/bash ${JSON.stringify(codexAdapterPath(hqRoot))}`;
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// AGENTS.md block.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
export function agentsMdBlock(charter) {
|
|
60
|
+
const body = charter.endsWith('\n') ? charter : `${charter}\n`;
|
|
61
|
+
return `${AGENTS_MD_BEGIN}\n${body}${AGENTS_MD_END}\n`;
|
|
62
|
+
}
|
|
63
|
+
/** Append the block (replacing any prior HQ block). `null` input = file absent. */
|
|
64
|
+
export function addAgentsMdBlock(current, charter) {
|
|
65
|
+
const base = current === null ? '' : removeAgentsMdBlock(current);
|
|
66
|
+
const block = agentsMdBlock(charter);
|
|
67
|
+
if (base === '')
|
|
68
|
+
return block;
|
|
69
|
+
return base.endsWith('\n') ? `${base}\n${block}` : `${base}\n\n${block}`;
|
|
70
|
+
}
|
|
71
|
+
/** Strip the HQ block, plus the separator `addAgentsMdBlock` put before it. */
|
|
72
|
+
export function removeAgentsMdBlock(current) {
|
|
73
|
+
const begin = current.indexOf(AGENTS_MD_BEGIN);
|
|
74
|
+
if (begin === -1)
|
|
75
|
+
return current;
|
|
76
|
+
const endIdx = current.indexOf(AGENTS_MD_END, begin);
|
|
77
|
+
if (endIdx === -1)
|
|
78
|
+
return current;
|
|
79
|
+
let end = endIdx + AGENTS_MD_END.length;
|
|
80
|
+
if (current[end] === '\n')
|
|
81
|
+
end += 1;
|
|
82
|
+
let start = begin;
|
|
83
|
+
if (start >= 2 && current.slice(start - 2, start) === '\n\n')
|
|
84
|
+
start -= 1;
|
|
85
|
+
return current.slice(0, start) + current.slice(end);
|
|
86
|
+
}
|
|
87
|
+
function isAdapterEntry(entry, hqRoot) {
|
|
88
|
+
if (!entry || typeof entry !== 'object')
|
|
89
|
+
return false;
|
|
90
|
+
const cmd = entry.command;
|
|
91
|
+
return typeof cmd === 'string' && cmd.includes(JSON.stringify(codexAdapterPath(hqRoot)));
|
|
92
|
+
}
|
|
93
|
+
/** Add one adapter group per event; idempotent. Foreign hooks are untouched. */
|
|
94
|
+
export function addCodexHooks(doc, hqRoot) {
|
|
95
|
+
const hooks = { ...(doc.hooks ?? {}) };
|
|
96
|
+
let changed = false;
|
|
97
|
+
for (const event of CODEX_HOOK_EVENTS) {
|
|
98
|
+
const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
99
|
+
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => isAdapterEntry(h, hqRoot)));
|
|
100
|
+
if (present)
|
|
101
|
+
continue;
|
|
102
|
+
groups.push({ hooks: [{ type: 'command', command: codexHookCommand(hqRoot), timeout: 300 }] });
|
|
103
|
+
hooks[event] = groups;
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
return changed ? { ...doc, hooks } : doc;
|
|
107
|
+
}
|
|
108
|
+
/** Remove only HQ adapter entries; drop containers this left empty. */
|
|
109
|
+
export function removeCodexHooks(doc, hqRoot) {
|
|
110
|
+
if (!doc.hooks || typeof doc.hooks !== 'object' || Array.isArray(doc.hooks))
|
|
111
|
+
return doc;
|
|
112
|
+
const hooks = { ...doc.hooks };
|
|
113
|
+
let changed = false;
|
|
114
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
115
|
+
if (!Array.isArray(value))
|
|
116
|
+
continue;
|
|
117
|
+
const groups = [];
|
|
118
|
+
for (const g of value) {
|
|
119
|
+
if (!g || !Array.isArray(g.hooks)) {
|
|
120
|
+
groups.push(g);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const kept = g.hooks.filter((h) => !isAdapterEntry(h, hqRoot));
|
|
124
|
+
if (kept.length === g.hooks.length)
|
|
125
|
+
groups.push(g);
|
|
126
|
+
else {
|
|
127
|
+
changed = true;
|
|
128
|
+
if (kept.length > 0)
|
|
129
|
+
groups.push({ ...g, hooks: kept });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (groups.length === 0)
|
|
133
|
+
delete hooks[event];
|
|
134
|
+
else
|
|
135
|
+
hooks[event] = groups;
|
|
136
|
+
}
|
|
137
|
+
if (!changed)
|
|
138
|
+
return doc;
|
|
139
|
+
const next = { ...doc };
|
|
140
|
+
if (Object.keys(hooks).length === 0)
|
|
141
|
+
delete next.hooks;
|
|
142
|
+
else
|
|
143
|
+
next.hooks = hooks;
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
export function hasAllCodexHooks(doc, hqRoot) {
|
|
147
|
+
return addCodexHooks(doc, hqRoot) === doc;
|
|
148
|
+
}
|
|
149
|
+
function within(child, parent) {
|
|
150
|
+
const rel = path.relative(parent, child);
|
|
151
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
152
|
+
}
|
|
153
|
+
export function classifyLegacySkills(skillsDir, hqRoot) {
|
|
154
|
+
const p = path.join(skillsDir, LEGACY_SKILLS_NAME);
|
|
155
|
+
let st;
|
|
156
|
+
try {
|
|
157
|
+
st = fs.lstatSync(p);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
if (e.code === 'ENOENT')
|
|
161
|
+
return { kind: 'absent' };
|
|
162
|
+
throw e;
|
|
163
|
+
}
|
|
164
|
+
if (st.isSymbolicLink()) {
|
|
165
|
+
const target = fs.readlinkSync(p);
|
|
166
|
+
const resolved = path.resolve(skillsDir, target);
|
|
167
|
+
let real = resolved;
|
|
168
|
+
try {
|
|
169
|
+
real = fs.realpathSync(resolved);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Dangling link: judge by the literal target.
|
|
173
|
+
}
|
|
174
|
+
let root = hqRoot;
|
|
175
|
+
try {
|
|
176
|
+
root = fs.realpathSync(hqRoot);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// HQ root checked by the caller; keep the literal path.
|
|
180
|
+
}
|
|
181
|
+
return within(resolved, hqRoot) || within(real, root) ? { kind: 'hq-symlink', target } : { kind: 'foreign' };
|
|
182
|
+
}
|
|
183
|
+
if (st.isDirectory() && fs.existsSync(path.join(p, HQ_OWNED_MARKER)))
|
|
184
|
+
return { kind: 'hq-dir' };
|
|
185
|
+
return { kind: 'foreign' };
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=codex-targets.js.map
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
+
const LOCK_STALE_MS = 30_000;
|
|
11
|
+
const LOCK_WAIT_MS = 10_000;
|
|
12
|
+
const LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
10
13
|
export function sessionSeqDir(env = process.env) {
|
|
11
14
|
return env.WORK_MESH_SEQ_DIR?.trim() || path.join(os.homedir(), ".hq", "work-mesh", "seq");
|
|
12
15
|
}
|
|
@@ -17,17 +20,67 @@ export function reserveSessionSeq(sessionId, count, env = process.env) {
|
|
|
17
20
|
const dir = sessionSeqDir(env);
|
|
18
21
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
19
22
|
const file = path.join(dir, sessionId);
|
|
20
|
-
|
|
23
|
+
const lockFile = `${file}.lock`;
|
|
24
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
25
|
+
let lockFd;
|
|
26
|
+
while (lockFd === undefined) {
|
|
27
|
+
try {
|
|
28
|
+
lockFd = fs.openSync(lockFile, "wx", 0o600);
|
|
29
|
+
fs.writeSync(lockFd, `${process.pid}\n`);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err.code !== "EEXIST")
|
|
33
|
+
throw err;
|
|
34
|
+
let reclaim = false;
|
|
35
|
+
try {
|
|
36
|
+
const stat = fs.statSync(lockFile);
|
|
37
|
+
const rawPid = fs.readFileSync(lockFile, "utf8").trim();
|
|
38
|
+
const pid = /^\d+$/.test(rawPid) ? Number(rawPid) : null;
|
|
39
|
+
const age = Date.now() - stat.mtimeMs;
|
|
40
|
+
if (age > LOCK_STALE_MS) {
|
|
41
|
+
if (pid === null)
|
|
42
|
+
reclaim = true;
|
|
43
|
+
else {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
}
|
|
47
|
+
catch (probeErr) {
|
|
48
|
+
reclaim = probeErr.code === "ESRCH";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (reclaim)
|
|
53
|
+
fs.unlinkSync(lockFile);
|
|
54
|
+
}
|
|
55
|
+
catch (lockErr) {
|
|
56
|
+
if (lockErr.code !== "ENOENT")
|
|
57
|
+
throw lockErr;
|
|
58
|
+
}
|
|
59
|
+
if (Date.now() >= deadline)
|
|
60
|
+
throw new Error(`timed out reserving mesh sequence for ${sessionId}`);
|
|
61
|
+
Atomics.wait(LOCK_SLEEP, 0, 0, 5);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
21
64
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
65
|
+
let current = 0;
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(file, "utf8").trim();
|
|
68
|
+
if (/^\d+$/.test(raw))
|
|
69
|
+
current = Number(raw);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code !== "ENOENT")
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
const first = current + 1;
|
|
76
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
77
|
+
fs.writeFileSync(temp, `${current + Math.max(0, count)}\n`, { mode: 0o600 });
|
|
78
|
+
fs.renameSync(temp, file);
|
|
79
|
+
return first;
|
|
25
80
|
}
|
|
26
|
-
|
|
27
|
-
|
|
81
|
+
finally {
|
|
82
|
+
fs.closeSync(lockFd);
|
|
83
|
+
fs.unlinkSync(lockFile);
|
|
28
84
|
}
|
|
29
|
-
const first = current + 1;
|
|
30
|
-
fs.writeFileSync(file, `${current + Math.max(0, count)}\n`, { mode: 0o600 });
|
|
31
|
-
return first;
|
|
32
85
|
}
|
|
33
86
|
//# sourceMappingURL=session-seq.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git remote URL normalisation for the repo-to-company registry (hq-anywhere US-004).
|
|
3
|
+
*
|
|
4
|
+
* Two clones of the same repo must produce the same key whether they were
|
|
5
|
+
* cloned over ssh or https, with or without a trailing `.git`, and in any
|
|
6
|
+
* letter case. The key drops scheme, user, and port, keeps host + path, and
|
|
7
|
+
* case-folds the result: `github.com/indigoai-us/hq-cli`.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Normalise a git remote URL to `host/path`. Accepts scp-style
|
|
11
|
+
* (`git@host:owner/name.git`), `ssh://`, `git://`, `http(s)://`, and bare
|
|
12
|
+
* `host/owner/name`. Returns null when the input has no host or path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeRemoteUrl(remoteUrl: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Normalise a `companies/manifest.yaml` repos[] entry. URLs go through
|
|
17
|
+
* normalizeRemoteUrl; a bare `owner/name` is assumed to live on github.com,
|
|
18
|
+
* the only host HQ manifests list today.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeManifestRepo(entry: string): string | null;
|
|
21
|
+
//# sourceMappingURL=normalize-remote.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git remote URL normalisation for the repo-to-company registry (hq-anywhere US-004).
|
|
3
|
+
*
|
|
4
|
+
* Two clones of the same repo must produce the same key whether they were
|
|
5
|
+
* cloned over ssh or https, with or without a trailing `.git`, and in any
|
|
6
|
+
* letter case. The key drops scheme, user, and port, keeps host + path, and
|
|
7
|
+
* case-folds the result: `github.com/indigoai-us/hq-cli`.
|
|
8
|
+
*/
|
|
9
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
10
|
+
const SCP = /^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/;
|
|
11
|
+
/**
|
|
12
|
+
* Normalise a git remote URL to `host/path`. Accepts scp-style
|
|
13
|
+
* (`git@host:owner/name.git`), `ssh://`, `git://`, `http(s)://`, and bare
|
|
14
|
+
* `host/owner/name`. Returns null when the input has no host or path.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeRemoteUrl(remoteUrl) {
|
|
17
|
+
const raw = remoteUrl.trim();
|
|
18
|
+
if (!raw)
|
|
19
|
+
return null;
|
|
20
|
+
let host;
|
|
21
|
+
let pathname;
|
|
22
|
+
const scp = SCHEME.test(raw) ? null : SCP.exec(raw);
|
|
23
|
+
if (scp) {
|
|
24
|
+
host = scp[1];
|
|
25
|
+
pathname = scp[2];
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
try {
|
|
29
|
+
const u = new URL(SCHEME.test(raw) ? raw : `https://${raw}`);
|
|
30
|
+
host = u.hostname;
|
|
31
|
+
pathname = decodeURIComponent(u.pathname);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const cleanPath = pathname
|
|
38
|
+
.replace(/\\/g, "/")
|
|
39
|
+
.replace(/\/+$/, "")
|
|
40
|
+
.replace(/\.git$/i, "")
|
|
41
|
+
.split("/")
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join("/");
|
|
44
|
+
const cleanHost = host.trim().replace(/\.$/, "");
|
|
45
|
+
if (!cleanHost || !cleanPath)
|
|
46
|
+
return null;
|
|
47
|
+
return `${cleanHost}/${cleanPath}`.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Normalise a `companies/manifest.yaml` repos[] entry. URLs go through
|
|
51
|
+
* normalizeRemoteUrl; a bare `owner/name` is assumed to live on github.com,
|
|
52
|
+
* the only host HQ manifests list today.
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeManifestRepo(entry) {
|
|
55
|
+
const trimmed = entry.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return null;
|
|
58
|
+
if (SCHEME.test(trimmed) || /^[^@/\s]+@[^:/\s]+:/.test(trimmed)) {
|
|
59
|
+
return normalizeRemoteUrl(trimmed);
|
|
60
|
+
}
|
|
61
|
+
const parts = trimmed.replace(/\.git$/i, "").split("/").filter(Boolean);
|
|
62
|
+
if (parts.length === 2)
|
|
63
|
+
return `github.com/${parts.join("/")}`.toLowerCase();
|
|
64
|
+
if (parts.length > 2 && parts[0].includes("."))
|
|
65
|
+
return normalizeRemoteUrl(trimmed);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=normalize-remote.js.map
|