@indigoai-us/hq-cli 5.327.0 → 5.329.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/assets/scaffold/core/scripts/hq-dm-bind.sh +1 -1
  3. package/assets/scaffold/core/scripts/hq-job-run.sh +8 -8
  4. package/assets/scaffold/core/scripts/ontology-audience-grant.sh +1 -1
  5. package/assets/scaffold/core/scripts/ontology-candidate.sh +1 -1
  6. package/assets/scaffold/core/scripts/resolve-company.sh +1 -1
  7. package/dist/command-catalog.generated.d.ts +3699 -3
  8. package/dist/command-catalog.generated.js +5462 -1759
  9. package/dist/command-help.d.ts +2 -0
  10. package/dist/command-help.js +13 -0
  11. package/dist/commands/install-global.d.ts +21 -0
  12. package/dist/commands/install-global.js +267 -32
  13. package/dist/commands/mcp-serve.js +1 -1
  14. package/dist/commands/mcp-status.js +58 -1
  15. package/dist/commands/pack-install.d.ts +3 -1
  16. package/dist/commands/pack-install.js +17 -4
  17. package/dist/commands/packs.js +17 -0
  18. package/dist/commands/pkg-install.js +13 -1
  19. package/dist/fast-scaffold.d.ts +2 -0
  20. package/dist/fast-scaffold.js +16 -0
  21. package/dist/index.js +19 -2
  22. package/dist/lazy-commands.d.ts +1 -0
  23. package/dist/lazy-commands.js +64 -4
  24. package/dist/lib/cli-flag-keys.d.ts +3 -0
  25. package/dist/lib/cli-flag-keys.js +3 -0
  26. package/dist/lib/daemon/ops/mcp.js +11 -10
  27. package/dist/lib/daemon/ops/policy.d.ts +3 -2
  28. package/dist/lib/daemon/ops/policy.js +12 -6
  29. package/dist/lib/daemon/server.d.ts +3 -1
  30. package/dist/lib/daemon/server.js +63 -7
  31. package/dist/lib/doctor/checks/anywhere.js +58 -4
  32. package/dist/lib/install/chatgpt-targets.d.ts +8 -0
  33. package/dist/lib/install/chatgpt-targets.js +15 -0
  34. package/dist/lib/install/codex-pack-contributions.d.ts +8 -0
  35. package/dist/lib/install/codex-pack-contributions.js +205 -0
  36. package/dist/lib/install/codex-targets.d.ts +1 -1
  37. package/dist/lib/install/codex-targets.js +2 -1
  38. package/dist/lib/install/grok-targets.d.ts +8 -0
  39. package/dist/lib/install/grok-targets.js +15 -0
  40. package/dist/lib/lanes/keep-alive-audit.d.ts +3 -2
  41. package/dist/lib/lanes/keep-alive-audit.js +36 -8
  42. package/dist/main.d.ts +2 -6
  43. package/dist/main.js +3 -41
  44. package/dist/node-network-compat-fast.js +4 -0
  45. package/dist/stream-error-boundary.d.ts +7 -0
  46. package/dist/stream-error-boundary.js +33 -0
  47. package/dist/utils/vault-api.js +16 -1
  48. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export declare function runCommandHelp(argv: readonly string[]): Promise<void>;
2
+ //# sourceMappingURL=command-help.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { Command } from "commander";
2
+ import { CLI_VERSION } from "./cli-version.js";
3
+ import { registerCommandCatalog } from "./lazy-commands.js";
4
+ export async function runCommandHelp(argv) {
5
+ const program = new Command()
6
+ .name("hq")
7
+ .description("HQ management CLI — modules, packages, and cloud sync")
8
+ .version(CLI_VERSION)
9
+ .option("--verbose", "Show verbose diagnostics, including SDK runtime warnings");
10
+ registerCommandCatalog(program);
11
+ await program.parseAsync([...argv]);
12
+ }
13
+ //# sourceMappingURL=command-help.js.map
@@ -17,6 +17,12 @@ export interface GlobalInstallOptions {
17
17
  log?: (line: string) => void;
18
18
  /** Is the hq daemon installed? Default: look for its launchd/systemd unit under `home`. */
19
19
  daemonInstalled?: boolean;
20
+ /** Install Claude Code through a local plugin marketplace instead of direct hooks/MCP. */
21
+ via?: 'direct' | 'plugin';
22
+ /** Local Claude Code marketplace directory (or HQ_CLAUDE_PLUGIN_MARKETPLACE). */
23
+ marketplaceDir?: string;
24
+ /** Test seam for the Claude Code CLI; production uses `claude`. */
25
+ runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
20
26
  }
21
27
  /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
22
28
  export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
@@ -25,6 +31,14 @@ export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platfo
25
31
  * the hqd shim; otherwise to master-hook.sh / the Codex adapter.
26
32
  */
27
33
  export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
34
+ export interface ClaudePluginSource {
35
+ sourceDir: string;
36
+ marketplaceName: string;
37
+ pluginName: string;
38
+ pluginId: string;
39
+ }
40
+ /** Resolve the local marketplace source; published hosting remains an operator decision. */
41
+ export declare function resolveClaudePluginSource(explicitDir?: string, env?: NodeJS.ProcessEnv): ClaudePluginSource;
28
42
  export { AnywhereRuntimeDisabledError as AnywhereDisabledError };
29
43
  export declare const isAnywhereEnabled: (_home: string, env?: NodeJS.ProcessEnv) => boolean;
30
44
  /** Install HQ into Claude Code user scope. Returns the files touched (or that would be). */
@@ -37,6 +51,8 @@ export interface GlobalUninstallOptions {
37
51
  env?: NodeJS.ProcessEnv;
38
52
  registryFile?: string;
39
53
  log?: (line: string) => void;
54
+ /** Test seam for the Claude Code CLI; production uses `claude`. */
55
+ runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
40
56
  }
41
57
  /** Reverse every install step; non-HQ entries are left untouched. */
42
58
  export declare function uninstallGlobalClaude(opts?: GlobalUninstallOptions): string[];
@@ -49,6 +65,11 @@ export interface GlobalCommandOpts {
49
65
  runtime?: string;
50
66
  dryRun?: boolean;
51
67
  hqRoot?: string;
68
+ via?: 'direct' | 'plugin';
69
+ marketplaceDir?: string;
70
+ /** Dependency injection for tests. */
71
+ env?: NodeJS.ProcessEnv;
72
+ log?: (line: string) => void;
52
73
  }
53
74
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
54
75
  export declare function runInstallGlobal(opts: GlobalCommandOpts): void;
@@ -15,6 +15,9 @@ import { buildCodexServerDef, claudeConfigFormat, codexConfigFormat, isCodexInst
15
15
  import { seedRegistryFromHqRoot } from './link.js';
16
16
  import { resolveHqRoot } from './doctor.js';
17
17
  import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
18
+ import { resolveHqMcpEndpoint } from '../lib/daemon/mcp-endpoint.js';
19
+ import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js';
20
+ import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
18
21
  import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/registry/registry.js';
19
22
  import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
20
23
  import { readDaemonConfig } from '../lib/daemon/config.js';
@@ -42,6 +45,64 @@ export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env)
42
45
  const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
43
46
  return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
44
47
  }
48
+ /** Resolve the local marketplace source; published hosting remains an operator decision. */
49
+ export function resolveClaudePluginSource(explicitDir, env = process.env) {
50
+ const configured = explicitDir?.trim() || env.HQ_CLAUDE_PLUGIN_MARKETPLACE?.trim();
51
+ if (!configured) {
52
+ throw Object.assign(new Error('Published HQ Claude plugin source is not configured; its repository/host awaits an operator decision. Pass --marketplace-dir <dir> or set HQ_CLAUDE_PLUGIN_MARKETPLACE.'), { expected: true });
53
+ }
54
+ const sourceDir = path.resolve(configured);
55
+ const manifestPath = path.join(sourceDir, '.claude-plugin', 'marketplace.json');
56
+ let manifest;
57
+ try {
58
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
59
+ }
60
+ catch (error) {
61
+ throw Object.assign(new Error(`Cannot read Claude marketplace manifest at ${manifestPath}: ${error.message}`), { expected: true });
62
+ }
63
+ if (typeof manifest.name !== 'string' || !Array.isArray(manifest.plugins)) {
64
+ throw Object.assign(new Error(`Invalid Claude marketplace manifest at ${manifestPath}: expected string name and plugins array.`), { expected: true });
65
+ }
66
+ const hqPlugin = manifest.plugins.find((p) => !!p && typeof p === 'object' && p.name === 'hq');
67
+ if (!hqPlugin) {
68
+ throw Object.assign(new Error(`Claude marketplace ${manifest.name} does not contain the HQ plugin named "hq".`), { expected: true });
69
+ }
70
+ return { sourceDir, marketplaceName: manifest.name, pluginName: hqPlugin.name, pluginId: `${hqPlugin.name}@${manifest.name}` };
71
+ }
72
+ function runClaudeCli(args, home, env) {
73
+ const childEnv = { ...process.env, ...env, HOME: home, CLAUDE_CONFIG_DIR: path.join(home, '.claude') };
74
+ const result = spawnSync('claude', ['plugin', ...args], { encoding: 'utf8', env: childEnv, timeout: 120_000, windowsHide: true });
75
+ if (result.error || result.status !== 0) {
76
+ const cause = result.error?.message ?? `exit status ${String(result.status)}`;
77
+ throw Object.assign(new Error(`Claude Code plugin command failed (${cause}). Ensure the Claude Code CLI is installed and try again.`), { expected: true });
78
+ }
79
+ }
80
+ function removePluginSettings(doc, plugin, removePlugin, removeMarketplace, pluginSettingBefore) {
81
+ const next = { ...doc };
82
+ const enabled = next.enabledPlugins;
83
+ if (removePlugin && enabled && typeof enabled === 'object' && !Array.isArray(enabled)) {
84
+ const entries = { ...enabled };
85
+ delete entries[plugin.pluginId];
86
+ if (pluginSettingBefore !== undefined)
87
+ entries[plugin.pluginId] = pluginSettingBefore;
88
+ if (Object.keys(entries).length === 0)
89
+ delete next.enabledPlugins;
90
+ else
91
+ next.enabledPlugins = entries;
92
+ }
93
+ const marketplaces = next.extraKnownMarketplaces;
94
+ if (marketplaces && typeof marketplaces === 'object' && !Array.isArray(marketplaces)) {
95
+ const entries = { ...marketplaces };
96
+ const registration = entries[plugin.marketplaceName];
97
+ if (removeMarketplace && registration?.source?.source === 'directory' && registration.source.path === plugin.sourceDir)
98
+ delete entries[plugin.marketplaceName];
99
+ if (Object.keys(entries).length === 0)
100
+ delete next.extraKnownMarketplaces;
101
+ else
102
+ next.extraKnownMarketplaces = entries;
103
+ }
104
+ return canonical(next) === canonical(doc) ? doc : next;
105
+ }
45
106
  export { AnywhereRuntimeDisabledError as AnywhereDisabledError };
46
107
  export const isAnywhereEnabled = (_home, env = process.env) => anywhereRuntimeEnabled(env);
47
108
  function readOrNull(file) {
@@ -80,8 +141,9 @@ function settingsInstallFormat(hqRoot, target) {
80
141
  * Removal format: verifies no HQ hook remains, and serializes a doc that equals
81
142
  * the pre-install document back to the ORIGINAL bytes (byte-identical uninstall).
82
143
  */
83
- function settingsRemovalFormat(hqRoot, original) {
84
- return restoringRemovalFormat(original, (doc) => removeHqHooks(doc, hqRoot) !== doc);
144
+ function settingsRemovalFormat(hqRoots, original) {
145
+ const roots = Array.isArray(hqRoots) ? hqRoots : [hqRoots];
146
+ return restoringRemovalFormat(original, (doc) => roots.reduce((current, root) => removeHqHooks(current, root), doc) !== doc);
85
147
  }
86
148
  /** Shared removal format for a JSON hooks file (see settingsRemovalFormat). */
87
149
  function restoringRemovalFormat(original, stillPresent) {
@@ -153,6 +215,30 @@ function planSkills(hqRoot, t) {
153
215
  }
154
216
  return plan;
155
217
  }
218
+ function hqSkillLinksForRoot(skillsDir, hqRoot) {
219
+ let names;
220
+ try {
221
+ names = fs.readdirSync(skillsDir).filter((name) => name.startsWith(SKILL_LINK_PREFIX));
222
+ }
223
+ catch (error) {
224
+ if (error.code === 'ENOENT')
225
+ return [];
226
+ throw error;
227
+ }
228
+ return names.filter((name) => {
229
+ const link = path.join(skillsDir, name);
230
+ try {
231
+ if (!fs.lstatSync(link).isSymbolicLink())
232
+ return false;
233
+ return path.dirname(path.resolve(skillsDir, fs.readlinkSync(link))) === hqSkillsSource(hqRoot);
234
+ }
235
+ catch (error) {
236
+ if (error.code === 'ENOENT')
237
+ return false;
238
+ throw error;
239
+ }
240
+ }).map((name) => path.join(skillsDir, name));
241
+ }
156
242
  function removeSeeded(seeded, regFile) {
157
243
  if (seeded.length === 0 || !fs.existsSync(regFile))
158
244
  return;
@@ -192,21 +278,58 @@ export function installGlobalClaude(opts) {
192
278
  throw new AnywhereRuntimeDisabledError();
193
279
  const t = claudeTargets(home);
194
280
  const prior = readState(t);
281
+ const pluginSource = opts.via === 'plugin' ? resolveClaudePluginSource(opts.marketplaceDir, env) : undefined;
282
+ if (prior?.plugin && !pluginSource) {
283
+ throw Object.assign(new Error('HQ is installed through a Claude plugin marketplace. Run `hq uninstall --global --runtime claude` before switching back to direct installation.'), { expected: true });
284
+ }
285
+ if (prior?.plugin && pluginSource && (prior.plugin.pluginId !== pluginSource.pluginId || prior.plugin.sourceDir !== pluginSource.sourceDir)) {
286
+ throw Object.assign(new Error('A different HQ Claude plugin marketplace is already recorded for this install. Uninstall it before changing marketplace source.'), { expected: true });
287
+ }
288
+ const runClaude = opts.runClaude ?? runClaudeCli;
195
289
  const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
196
- const manifest = mcpManifest(opts);
290
+ const manifest = pluginSource ? undefined : mcpManifest(opts);
197
291
  const touched = [];
198
292
  const pointerOld = readOrNull(t.hqPointer);
199
293
  const pointerNew = `${hqRoot}\n`;
200
294
  const mdOld = readOrNull(t.claudeMd);
201
- const mdNew = addClaudeMdBlock(mdOld, hqRoot);
202
- const skills = planSkills(hqRoot, t);
295
+ const legacyRoot = prior?.hqRoot ?? (pluginSource ? pointerOld?.trim() : undefined);
296
+ const cleanupRoots = [...new Set([hqRoot, ...(legacyRoot ? [legacyRoot] : [])])];
297
+ const mdOriginal = pluginSource
298
+ ? (prior && mdOld === prior.claudeMd.installed
299
+ ? prior.claudeMd.original
300
+ : mdOld === null ? null : (removeClaudeMdBlock(mdOld) || null))
301
+ : prior ? prior.claudeMd.original : mdOld;
302
+ const mdNew = pluginSource ? mdOriginal : addClaudeMdBlock(mdOld, hqRoot);
303
+ const skills = pluginSource ? { link: [], already: [], skipped: [] } : planSkills(hqRoot, t);
203
304
  const settingsOld = readOrNull(t.settingsJson);
204
305
  const settingsDoc = readConfigDoc(t.settingsJson, jsonFormat).doc;
306
+ const directHooksRemoved = pluginSource
307
+ ? cleanupRoots.reduce((doc, root) => removeHqHooks(doc, root), settingsDoc)
308
+ : settingsDoc;
309
+ const settingsOriginal = prior
310
+ ? prior.settings.original
311
+ : pluginSource && directHooksRemoved !== settingsDoc ? jsonFormat.serialize(directHooksRemoved) : settingsOld;
312
+ const enabledPlugins = settingsDoc.enabledPlugins;
313
+ const knownMarketplaces = settingsDoc.extraKnownMarketplaces;
314
+ const existingMarketplace = pluginSource ? knownMarketplaces?.[pluginSource.marketplaceName] : undefined;
315
+ if (pluginSource && existingMarketplace && (existingMarketplace.source?.source !== 'directory' || existingMarketplace.source.path !== pluginSource.sourceDir)) {
316
+ throw Object.assign(new Error(`Claude marketplace "${pluginSource.marketplaceName}" is already registered from a different source. Remove or rename that marketplace before installing HQ.`), { expected: true });
317
+ }
318
+ const hadPluginSetting = !!pluginSource && !!enabledPlugins && Object.prototype.hasOwnProperty.call(enabledPlugins, pluginSource.pluginId);
319
+ const currentPluginSetting = pluginSource && hadPluginSetting ? enabledPlugins[pluginSource.pluginId] : undefined;
320
+ const pluginSettingBefore = prior?.plugin ? prior.plugin.pluginSettingBefore : currentPluginSetting;
321
+ const pluginNeedsInstall = !!pluginSource && currentPluginSetting !== true;
322
+ const marketplaceNeedsAdd = !!pluginSource && existingMarketplace === undefined;
323
+ const pluginAdded = !!pluginSource && (prior?.plugin?.pluginAdded === true || pluginNeedsInstall);
324
+ const marketplaceAdded = !!pluginSource && (prior?.plugin?.marketplaceAdded === true || marketplaceNeedsAdd);
205
325
  const hookTarget = hookTargetFor(hqRoot, home, opts.daemonInstalled, env);
206
- const settingsNext = addHqHooks(settingsDoc, hqRoot, hookTarget);
326
+ const settingsNext = pluginSource
327
+ ? cleanupRoots.reduce((doc, root) => removeHqHooks(doc, root), settingsDoc)
328
+ : addHqHooks(settingsDoc, hqRoot, hookTarget);
329
+ const skillLinksToRemove = pluginSource ? cleanupRoots.flatMap((root) => hqSkillLinksForRoot(t.skillsDir, root)) : [];
207
330
  const claudeDoc = readConfigDoc(t.claudeJson, claudeConfigFormat).doc;
208
331
  // Collision check runs here too, so a dry run surfaces a conflicting "hq" server.
209
- const claudeNext = mergeClaudeServer(HQ_MCP_SERVER_NAME, {
332
+ const claudeNext = pluginSource ? claudeDoc : mergeClaudeServer(HQ_MCP_SERVER_NAME, {
210
333
  ...manifest,
211
334
  _hqPack: HQ_ANYWHERE_PACK,
212
335
  })(claudeDoc);
@@ -216,10 +339,13 @@ export function installGlobalClaude(opts) {
216
339
  touched.push(t.claudeMd);
217
340
  for (const s of skills.link)
218
341
  touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
342
+ touched.push(...skillLinksToRemove);
219
343
  if (settingsNext !== settingsDoc)
220
344
  touched.push(t.settingsJson);
221
345
  if (claudeNext !== claudeDoc)
222
346
  touched.push(t.claudeJson);
347
+ if (pluginSource)
348
+ touched.push(t.settingsJson);
223
349
  touched.push(regFile, t.stateFile);
224
350
  if (opts.dryRun) {
225
351
  log(`hq install --global --runtime claude --dry-run (HQ root ${hqRoot})`);
@@ -233,23 +359,86 @@ export function installGlobalClaude(opts) {
233
359
  log(lineDiff(jsonFormat.serialize(settingsDoc), jsonFormat.serialize(settingsNext)));
234
360
  log(`\n--- ${t.claudeJson}`);
235
361
  log(lineDiff(jsonFormat.serialize(claudeDoc), jsonFormat.serialize(claudeNext)));
362
+ if (pluginSource)
363
+ log(`Would run: claude plugin marketplace add --scope user ${pluginSource.sourceDir}; claude plugin install --scope user -y ${pluginSource.pluginId}`);
236
364
  return touched;
237
365
  }
366
+ if (pluginSource && (marketplaceNeedsAdd || pluginNeedsInstall)) {
367
+ let marketplaceMayNeedRollback = false;
368
+ try {
369
+ if (marketplaceNeedsAdd) {
370
+ marketplaceMayNeedRollback = true;
371
+ runClaude(['marketplace', 'add', '--scope', 'user', '--json', pluginSource.sourceDir], home, env);
372
+ }
373
+ if (pluginNeedsInstall)
374
+ runClaude(['install', '--scope', 'user', '--json', '-y', pluginSource.pluginId], home, env);
375
+ }
376
+ catch (error) {
377
+ const rollbackErrors = [];
378
+ if (pluginNeedsInstall && pluginAdded) {
379
+ try {
380
+ runClaude(['uninstall', '--scope', 'user', '--json', pluginSource.pluginId], home, env);
381
+ }
382
+ catch (rollbackError) {
383
+ rollbackErrors.push(`plugin uninstall: ${rollbackError.message}`);
384
+ }
385
+ }
386
+ if (marketplaceMayNeedRollback) {
387
+ try {
388
+ runClaude(['marketplace', 'remove', '--scope', 'user', '--json', pluginSource.marketplaceName], home, env);
389
+ }
390
+ catch (rollbackError) {
391
+ rollbackErrors.push(`marketplace removal: ${rollbackError.message}`);
392
+ }
393
+ }
394
+ if (rollbackErrors.length > 0)
395
+ throw Object.assign(new Error(`Claude plugin installation failed and rollback was incomplete (${rollbackErrors.join('; ')})`), { cause: error, expected: true });
396
+ throw error;
397
+ }
398
+ }
238
399
  writeFileAtomic(t.hqPointer, pointerNew);
239
- writeFileAtomic(t.claudeMd, mdNew);
240
- fs.mkdirSync(t.skillsDir, { recursive: true });
400
+ if (mdOld !== mdNew) {
401
+ if (mdNew === null) {
402
+ if (fs.existsSync(t.claudeMd))
403
+ fs.unlinkSync(t.claudeMd);
404
+ }
405
+ else
406
+ writeFileAtomic(t.claudeMd, mdNew);
407
+ }
408
+ if (!pluginSource)
409
+ fs.mkdirSync(t.skillsDir, { recursive: true });
241
410
  for (const name of skills.link) {
242
411
  fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
243
412
  }
244
- writeConfigAtomic({
245
- target: t.settingsJson,
246
- pack: HQ_ANYWHERE_PACK,
247
- format: settingsInstallFormat(hqRoot, hookTarget),
248
- merge: (doc) => addHqHooks(doc, hqRoot, hookTarget),
249
- assertName: 'hooks',
250
- env: { home },
251
- });
252
- registerClaudeServer({ name: HQ_MCP_SERVER_NAME, manifest, pack: HQ_ANYWHERE_PACK, env: { home } });
413
+ if (pluginSource)
414
+ for (const link of skillLinksToRemove)
415
+ fs.unlinkSync(link);
416
+ if (pluginSource) {
417
+ // Migrate a prior direct install: remove only our old hook/MCP entries while
418
+ // retaining the original user settings for a later uninstall restore.
419
+ if (fs.existsSync(t.settingsJson)) {
420
+ writeConfigAtomic({
421
+ target: t.settingsJson,
422
+ pack: HQ_ANYWHERE_PACK,
423
+ format: settingsRemovalFormat(cleanupRoots, settingsOriginal),
424
+ merge: (doc) => cleanupRoots.reduce((current, root) => removeHqHooks(current, root), doc),
425
+ assertName: 'hooks',
426
+ env: { home },
427
+ });
428
+ }
429
+ unregisterClaudeServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
430
+ }
431
+ else {
432
+ writeConfigAtomic({
433
+ target: t.settingsJson,
434
+ pack: HQ_ANYWHERE_PACK,
435
+ format: settingsInstallFormat(hqRoot, hookTarget),
436
+ merge: (doc) => addHqHooks(doc, hqRoot, hookTarget),
437
+ assertName: 'hooks',
438
+ env: { home },
439
+ });
440
+ registerClaudeServer({ name: HQ_MCP_SERVER_NAME, manifest: manifest, pack: HQ_ANYWHERE_PACK, env: { home } });
441
+ }
253
442
  // Seeding is best-effort: the runtime wiring above is already in place, so a
254
443
  // missing or unreadable manifest is reported rather than aborting the install.
255
444
  let seeded = [];
@@ -262,25 +451,38 @@ export function installGlobalClaude(opts) {
262
451
  catch (e) {
263
452
  log(`Registry not seeded from companies/manifest.yaml: ${e.message}`);
264
453
  }
454
+ const recoveredDirectPointer = !!pluginSource && !prior && legacyRoot === pointerOld?.trim() && (directHooksRemoved !== settingsDoc || (mdOld !== null && removeClaudeMdBlock(mdOld) !== mdOld) || skillLinksToRemove.length > 0);
265
455
  const state = {
266
456
  version: 1,
267
457
  hqRoot,
268
458
  // A re-install keeps the FIRST install's originals so uninstall restores pre-HQ bytes.
269
- hqPointer: { original: prior ? prior.hqPointer.original : pointerOld },
270
- claudeMd: { original: prior ? prior.claudeMd.original : mdOld, installed: readOrNull(t.claudeMd) ?? '' },
271
- settings: { original: prior ? prior.settings.original : settingsOld, installed: readOrNull(t.settingsJson) ?? '' },
459
+ hqPointer: { original: prior ? prior.hqPointer.original : recoveredDirectPointer ? null : pointerOld },
460
+ claudeMd: { original: mdOriginal, installed: readOrNull(t.claudeMd) ?? '' },
461
+ settings: { original: settingsOriginal, installed: readOrNull(t.settingsJson) ?? '' },
272
462
  skills: {
273
- linked: [...new Set([...(prior?.skills.linked ?? []), ...skills.link, ...skills.already])].sort(),
463
+ linked: pluginSource ? [] : [...new Set([...(prior?.skills.linked ?? []), ...skills.link, ...skills.already])].sort(),
274
464
  skipped: skills.skipped,
275
465
  },
276
466
  mcp: { name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK },
467
+ ...(pluginSource ? { plugin: {
468
+ pluginId: pluginSource.pluginId,
469
+ marketplaceName: pluginSource.marketplaceName,
470
+ sourceDir: pluginSource.sourceDir,
471
+ pluginAdded,
472
+ marketplaceAdded,
473
+ ...(hadPluginSetting ? { pluginSettingBefore } : {}),
474
+ } } : {}),
277
475
  seeded: [...new Set([...(prior?.seeded ?? []), ...ownedSeeded])].sort(),
278
476
  };
279
477
  writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
280
478
  log(`HQ installed for Claude Code from ${hqRoot}.`);
281
479
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
282
- log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
283
- log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
480
+ if (pluginSource)
481
+ log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
482
+ else {
483
+ log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
484
+ log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
485
+ }
284
486
  log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
285
487
  return touched;
286
488
  }
@@ -294,6 +496,13 @@ export function uninstallGlobalClaude(opts = {}) {
294
496
  const hqRoot = state?.hqRoot ?? opts.hqRoot ?? readOrNull(t.hqPointer)?.trim();
295
497
  if (!hqRoot)
296
498
  throw new Error('No HQ global install found (no install state and no ~/.hq/root).');
499
+ const pluginSource = state?.plugin ? {
500
+ sourceDir: state.plugin.sourceDir,
501
+ marketplaceName: state.plugin.marketplaceName,
502
+ pluginName: state.plugin.pluginId.slice(0, state.plugin.pluginId.lastIndexOf('@')),
503
+ pluginId: state.plugin.pluginId,
504
+ } : undefined;
505
+ const runClaude = opts.runClaude ?? runClaudeCli;
297
506
  const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
298
507
  const touched = [];
299
508
  // MCP server (provenance-scoped: a foreign "hq" server is never removed).
@@ -304,7 +513,9 @@ export function uninstallGlobalClaude(opts = {}) {
304
513
  // settings.json hooks.
305
514
  const settingsOriginal = state ? state.settings.original : null;
306
515
  const settingsDoc = readConfigDoc(t.settingsJson, jsonFormat).doc;
307
- const settingsNext = removeHqHooks(settingsDoc, hqRoot);
516
+ const settingsNext = pluginSource
517
+ ? removePluginSettings(removeHqHooks(settingsDoc, hqRoot), pluginSource, state?.plugin?.pluginAdded ?? true, state?.plugin?.marketplaceAdded ?? true, state?.plugin?.pluginSettingBefore)
518
+ : removeHqHooks(settingsDoc, hqRoot);
308
519
  if (settingsNext !== settingsDoc)
309
520
  touched.push(t.settingsJson);
310
521
  // Skills: only symlinks that point into this HQ checkout's skills dir.
@@ -349,15 +560,27 @@ export function uninstallGlobalClaude(opts = {}) {
349
560
  log(` ${f}`);
350
561
  log(`\n--- ${t.settingsJson}`);
351
562
  log(lineDiff(jsonFormat.serialize(settingsDoc), jsonFormat.serialize(settingsNext)));
563
+ if (pluginSource)
564
+ log(`Would run: ${state?.plugin?.pluginAdded ? `claude plugin uninstall --scope user ${pluginSource.pluginId}` : '(plugin was pre-existing; leave it installed)'}${state?.plugin?.marketplaceAdded ? `; claude plugin marketplace remove --scope user ${pluginSource.marketplaceName}` : ''}`);
352
565
  return touched;
353
566
  }
567
+ if (pluginSource && state?.plugin?.pluginAdded) {
568
+ runClaude(['uninstall', '--scope', 'user', '--json', pluginSource.pluginId], home, env);
569
+ }
570
+ if (pluginSource && state?.plugin?.marketplaceAdded) {
571
+ runClaude(['marketplace', 'remove', '--scope', 'user', '--json', pluginSource.marketplaceName], home, env);
572
+ }
354
573
  unregisterClaudeServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
355
574
  if (fs.existsSync(t.settingsJson)) {
356
575
  writeConfigAtomic({
357
576
  target: t.settingsJson,
358
577
  pack: HQ_ANYWHERE_PACK,
359
- format: settingsRemovalFormat(hqRoot, settingsOriginal),
360
- merge: (doc) => removeHqHooks(doc, hqRoot),
578
+ format: restoringRemovalFormat(settingsOriginal, (doc) => (pluginSource
579
+ ? removePluginSettings(removeHqHooks(doc, hqRoot), pluginSource, state?.plugin?.pluginAdded ?? true, state?.plugin?.marketplaceAdded ?? true, state?.plugin?.pluginSettingBefore) !== doc
580
+ : removeHqHooks(doc, hqRoot) !== doc)),
581
+ merge: (doc) => pluginSource
582
+ ? removePluginSettings(removeHqHooks(doc, hqRoot), pluginSource, state?.plugin?.pluginAdded ?? true, state?.plugin?.marketplaceAdded ?? true, state?.plugin?.pluginSettingBefore)
583
+ : removeHqHooks(doc, hqRoot),
361
584
  assertName: 'hooks',
362
585
  env: { home },
363
586
  });
@@ -393,7 +616,7 @@ export function uninstallGlobalClaude(opts = {}) {
393
616
  throw e;
394
617
  }
395
618
  }
396
- log(`HQ removed from Claude Code user scope (${links.length} skill link(s), hooks, MCP server, CLAUDE.md block).`);
619
+ log(`HQ removed from Claude Code user scope (${links.length} skill link(s), ${pluginSource ? 'plugin' : 'hooks and MCP server'}, CLAUDE.md block).`);
397
620
  return touched;
398
621
  }
399
622
  function readCodexState(t) {
@@ -686,8 +909,8 @@ export function uninstallGlobalCodex(opts = {}) {
686
909
  }
687
910
  function assertRuntime(runtime) {
688
911
  const r = runtime ?? 'claude';
689
- if (r !== 'claude' && r !== 'codex') {
690
- throw Object.assign(new Error(`--runtime ${r} is not supported. Use --runtime claude or --runtime codex.`), {
912
+ if (r !== 'claude' && r !== 'codex' && r !== 'chatgpt' && r !== 'grok') {
913
+ throw Object.assign(new Error(`--runtime ${r} is not supported. Use --runtime claude, codex, chatgpt, or grok.`), {
691
914
  expected: true,
692
915
  });
693
916
  }
@@ -704,8 +927,17 @@ function resolveInstallRoot(explicit) {
704
927
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
705
928
  export function runInstallGlobal(opts) {
706
929
  const runtime = assertRuntime(opts.runtime);
930
+ if (runtime === 'chatgpt' || runtime === 'grok') {
931
+ if (!anywhereRuntimeEnabled(opts.env ?? process.env))
932
+ throw new AnywhereRuntimeDisabledError();
933
+ const endpoint = resolveHqMcpEndpoint(opts.env ?? process.env).toString();
934
+ const lines = runtime === 'chatgpt' ? chatgptConnectorInstructions(endpoint) : grokConnectorInstructions(endpoint);
935
+ for (const line of lines)
936
+ (opts.log ?? console.log)(line);
937
+ return;
938
+ }
707
939
  const install = runtime === 'codex' ? installGlobalCodex : installGlobalClaude;
708
- install({ hqRoot: resolveInstallRoot(opts.hqRoot), dryRun: opts.dryRun });
940
+ install({ hqRoot: resolveInstallRoot(opts.hqRoot), dryRun: opts.dryRun, via: opts.via, marketplaceDir: opts.marketplaceDir });
709
941
  }
710
942
  export function registerUninstallCommand(program) {
711
943
  program
@@ -721,7 +953,10 @@ export function registerUninstallCommand(program) {
721
953
  process.exitCode = 1;
722
954
  return;
723
955
  }
724
- const runtime = assertRuntime(opts.runtime);
956
+ const runtime = opts.runtime ?? 'claude';
957
+ if (runtime !== 'claude' && runtime !== 'codex') {
958
+ throw Object.assign(new Error(`--runtime ${runtime} has no local configuration to uninstall; remove its connector in the host application.`), { expected: true });
959
+ }
725
960
  const uninstall = runtime === 'codex' ? uninstallGlobalCodex : uninstallGlobalClaude;
726
961
  uninstall({ hqRoot: opts.hqRoot, dryRun: opts.dryRun });
727
962
  });
@@ -15,7 +15,7 @@ import * as fs from "node:fs";
15
15
  import * as os from "node:os";
16
16
  import * as path from "node:path";
17
17
  import { CLI_VERSION } from "../cli-version.js";
18
- import { HQ_ANYWHERE_RUNTIME_FLAG } from "./anywhere-runtime-flag.js";
18
+ import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
19
19
  import { anywhereRuntimeEnabledFromSnapshot } from "../lib/anywhere-runtime-fast-flag.js";
20
20
  import { createLineSplitter, HqdClientError, hqdRequest, hqdSocketPath } from "../lib/daemon/protocol.js";
21
21
  export const MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
@@ -24,6 +24,7 @@
24
24
  import * as fs from 'fs';
25
25
  import * as path from 'path';
26
26
  import { anywhereRuntimeEnabled } from './anywhere-runtime-flag.js';
27
+ import { claudeTargets } from '../lib/install/claude-targets.js';
27
28
  import { HQ_PACK_PROVENANCE_KEY, SECRET_REDACTION, backupRoot, claudeConfigFormat, claudeConfigPath, codexConfigFormat, codexConfigPath, CODEX_MCP_SERVERS_KEY, ConfigParseError, isCodexInstalled, readConfigDoc, resolveEnv, } from './mcp-registration.js';
28
29
  // ---------------------------------------------------------------------------
29
30
  // Pure status computation.
@@ -84,6 +85,49 @@ function summarizeDef(def) {
84
85
  function summariesDiffer(a, b) {
85
86
  return JSON.stringify(a) !== JSON.stringify(b);
86
87
  }
88
+ function findAnywherePluginMcp(home) {
89
+ const claude = path.join(home, '.claude');
90
+ try {
91
+ const targets = claudeTargets(home);
92
+ const state = JSON.parse(fs.readFileSync(targets.stateFile, 'utf8'));
93
+ const pluginId = state.plugin?.pluginId;
94
+ if (typeof pluginId !== 'string' || pluginId.length === 0)
95
+ return undefined;
96
+ const settings = JSON.parse(fs.readFileSync(targets.settingsJson, 'utf8'));
97
+ if (settings.enabledPlugins?.[pluginId] !== true)
98
+ return undefined;
99
+ const installedFile = path.join(claude, 'plugins', 'installed_plugins.json');
100
+ const installed = JSON.parse(fs.readFileSync(installedFile, 'utf8'));
101
+ const install = installed.plugins?.[pluginId]?.find((entry) => typeof entry.installPath === 'string');
102
+ if (!install || typeof install.installPath !== 'string' || !path.isAbsolute(install.installPath))
103
+ return undefined;
104
+ const installDir = path.resolve(install.installPath);
105
+ const cacheRoot = path.resolve(claude, 'plugins', 'cache');
106
+ const cacheRelative = path.relative(cacheRoot, installDir);
107
+ if (cacheRelative.startsWith('..') || path.isAbsolute(cacheRelative) || !cacheRelative.split(path.sep).includes('hq'))
108
+ return undefined;
109
+ const file = path.join(installDir, '.mcp.json');
110
+ const stat = fs.lstatSync(file);
111
+ if (!stat.isFile() || stat.isSymbolicLink())
112
+ return undefined;
113
+ const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
114
+ const server = doc.mcpServers?.hq;
115
+ return server && typeof server === 'object' && !Array.isArray(server) ? server : undefined;
116
+ }
117
+ catch (error) {
118
+ if (error.code === 'ENOENT')
119
+ return undefined;
120
+ console.warn(`Skipping unavailable hq-anywhere plugin MCP metadata: ${error.message}`);
121
+ return undefined;
122
+ }
123
+ }
124
+ function pluginAndCodexMcpEquivalent(plugin, codex) {
125
+ const pArgs = Array.isArray(plugin.args) ? plugin.args : [];
126
+ const cArgs = Array.isArray(codex.args) ? codex.args : [];
127
+ const pluginTarget = plugin.type === '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';
128
+ const codexTarget = codex.type === 'stdio' && typeof codex.command === 'string' && /(^|\/)hq$/.test(codex.command) && cArgs.length === 2 && cArgs[0] === 'mcp' && cArgs[1] === 'serve';
129
+ return pluginTarget && codexTarget && JSON.stringify(headerKeysOf(plugin)) === JSON.stringify(headerKeysOf(codex)) && JSON.stringify(envKeysOf(plugin)) === JSON.stringify(envKeysOf(codex));
130
+ }
87
131
  /** Scan the Claude runtime: read `~/.claude.json`, collect HQ-owned servers. */
88
132
  function scanClaude(env) {
89
133
  const configPath = claudeConfigPath(env);
@@ -201,6 +245,17 @@ export function computeMcpStatus(env) {
201
245
  const checkHealth = anywhereRuntimeEnabled();
202
246
  const claudeScan = scanClaude(env);
203
247
  const codexScan = scanCodex(env);
248
+ // Claude plugins keep their MCP manifest beside the installed plugin rather
249
+ // than in ~/.claude.json. Pair the HQ plugin with the HQ pack's Codex entry
250
+ // so equivalent launchers do not show as absent or drifted.
251
+ const pluginMcp = checkHealth ? findAnywherePluginMcp(env.home) : undefined;
252
+ if (pluginMcp) {
253
+ for (const [name, found] of codexScan.hqServers) {
254
+ if (found.pack === 'hq-anywhere' || found.pack.endsWith('/hq-anywhere')) {
255
+ claudeScan.hqServers.set(name, { def: pluginMcp, pack: found.pack });
256
+ }
257
+ }
258
+ }
204
259
  // Union of every HQ-owned server name across both runtimes.
205
260
  const names = new Set([...claudeScan.hqServers.keys(), ...codexScan.hqServers.keys()]);
206
261
  const servers = [];
@@ -232,7 +287,9 @@ export function computeMcpStatus(env) {
232
287
  // DRIFT = present in both but transport-relevant content differs (no header VALUES).
233
288
  let drifted = false;
234
289
  if (onClaude && onCodex && claudeFound && codexFound) {
235
- drifted = summariesDiffer(summarizeDef(claudeFound.def), summarizeDef(codexFound.def));
290
+ drifted = pluginMcp && claudeFound.def === pluginMcp && pluginAndCodexMcpEquivalent(pluginMcp, codexFound.def)
291
+ ? false
292
+ : summariesDiffer(summarizeDef(claudeFound.def), summarizeDef(codexFound.def));
236
293
  }
237
294
  servers.push({
238
295
  server: name,
@@ -278,7 +278,9 @@ export declare function verifyArtifact(input: VerifyArtifactInput): void;
278
278
  * @param item the bare server name declared under `contributes.mcp`
279
279
  */
280
280
  export declare function validateMcpManifest(payloadDir: string, item: string): void;
281
- export declare function validateManifest(payloadDir: string, hqVersion: string | null): PackManifest;
281
+ export declare function validateManifest(payloadDir: string, hqVersion: string | null, options?: {
282
+ allowReservedName?: boolean;
283
+ }): PackManifest;
282
284
  /**
283
285
  * Enforce a pack's `requires.packs`: every named dependency MUST already be
284
286
  * installed (and satisfy its optional semver RANGE) before we write anything.