@indigoai-us/hq-cli 5.345.104 → 5.345.105

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 (74) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/assets/bot-workers/setup/skills/add-a-pack.md +130 -0
  3. package/assets/bot-workers/setup/worker.yaml +3 -0
  4. package/dist/command-catalog.generated.d.ts +206 -0
  5. package/dist/command-catalog.generated.js +246 -0
  6. package/dist/commands/bot.d.ts +10 -0
  7. package/dist/commands/bot.js +20 -12
  8. package/dist/commands/daemon.js +385 -1
  9. package/dist/commands/integrations-connect.d.ts +6 -0
  10. package/dist/commands/integrations-connect.js +36 -0
  11. package/dist/commands/integrations-slack.d.ts +30 -0
  12. package/dist/commands/integrations-slack.js +81 -0
  13. package/dist/commands/integrations.js +2 -0
  14. package/dist/commands/pack-install.d.ts +20 -1
  15. package/dist/commands/pack-install.js +80 -5
  16. package/dist/commands/pack-start-bot.d.ts +24 -0
  17. package/dist/commands/pack-start-bot.js +76 -0
  18. package/dist/commands/packs.d.ts +9 -0
  19. package/dist/commands/packs.js +26 -3
  20. package/dist/commands/pkg-install.js +11 -2
  21. package/dist/commands/secrets.d.ts +1 -5
  22. package/dist/commands/secrets.js +2 -92
  23. package/dist/lib/bot/config.d.ts +7 -0
  24. package/dist/lib/bot/daemon.js +2 -1
  25. package/dist/lib/bot/index.d.ts +2 -0
  26. package/dist/lib/bot/index.js +2 -0
  27. package/dist/lib/bot/lean-settings.d.ts +16 -0
  28. package/dist/lib/bot/lean-settings.js +34 -0
  29. package/dist/lib/bot/pack-link.d.ts +66 -0
  30. package/dist/lib/bot/pack-link.js +153 -0
  31. package/dist/lib/bot/pack-setup-driver.d.ts +37 -0
  32. package/dist/lib/bot/pack-setup-driver.js +83 -0
  33. package/dist/lib/bot/pack-setup.d.ts +62 -0
  34. package/dist/lib/bot/pack-setup.js +103 -0
  35. package/dist/lib/bot/prompt.d.ts +8 -0
  36. package/dist/lib/bot/prompt.js +5 -0
  37. package/dist/lib/bot/run.d.ts +18 -2
  38. package/dist/lib/bot/run.js +163 -25
  39. package/dist/lib/bot/runtime/claude.d.ts +27 -2
  40. package/dist/lib/bot/runtime/claude.js +49 -4
  41. package/dist/lib/bot/runtime/types.d.ts +5 -0
  42. package/dist/lib/bot/runtime-failure.d.ts +18 -2
  43. package/dist/lib/bot/runtime-failure.js +21 -1
  44. package/dist/lib/bot/toolchain.d.ts +25 -0
  45. package/dist/lib/bot/toolchain.js +33 -0
  46. package/dist/lib/bot/worker-source.d.ts +1 -0
  47. package/dist/lib/bot/worker-source.js +7 -1
  48. package/dist/lib/daemon/catalog.d.ts +15 -0
  49. package/dist/lib/daemon/catalog.js +49 -0
  50. package/dist/lib/daemon/paths.d.ts +1 -0
  51. package/dist/lib/daemon/paths.js +1 -0
  52. package/dist/lib/daemon/remote-control-forwarder.d.ts +14 -0
  53. package/dist/lib/daemon/remote-control-forwarder.js +104 -0
  54. package/dist/lib/daemon/remote-control-pools.d.ts +44 -0
  55. package/dist/lib/daemon/remote-control-pools.js +245 -0
  56. package/dist/lib/daemon/remote-control.d.ts +4 -0
  57. package/dist/lib/daemon/remote-control.js +17 -2
  58. package/dist/lib/daemon/run.js +126 -0
  59. package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +5 -4
  60. package/dist/lib/doctor/fix/refresh-sync-progress.js +5 -4
  61. package/dist/lib/doctor/json-output.d.ts +5 -5
  62. package/dist/lib/doctor/json-output.js +2 -5
  63. package/dist/lib/packages/setup-block.d.ts +43 -0
  64. package/dist/lib/packages/setup-block.js +236 -0
  65. package/dist/lib/packages/setup-prompt.d.ts +34 -0
  66. package/dist/lib/packages/setup-prompt.js +128 -0
  67. package/dist/lib/packages/setup-takeover.d.ts +50 -0
  68. package/dist/lib/packages/setup-takeover.js +75 -0
  69. package/dist/lib/packages/start-bot.d.ts +47 -0
  70. package/dist/lib/packages/start-bot.js +66 -0
  71. package/dist/lib/session-company-lock.d.ts +7 -0
  72. package/dist/lib/session-company-lock.js +97 -0
  73. package/dist/types.d.ts +82 -0
  74. package/package.json +1 -1
@@ -170,6 +170,16 @@ interface RawListingDetail extends RawListingDetailFields {
170
170
  }
171
171
  /** Map a raw `GET /v1/listings/{id}` body into a MarketplaceListing. */
172
172
  export declare function toMarketplaceListing(raw: RawListingDetail): MarketplaceListing;
173
+ /**
174
+ * Run a marketplace request again when the server answers 5xx or the network
175
+ * fails. A brief 502 from the API gateway ended a pack install from the app
176
+ * ("Failed to resolve listing … (HTTP 502)") although the next request
177
+ * succeeded. 4xx answers and the final attempt are returned as they are.
178
+ */
179
+ export declare function withTransientRetry(request: () => Promise<Response>, opts?: {
180
+ delaysMs?: number[];
181
+ sleep?: (ms: number) => Promise<void>;
182
+ }): Promise<Response>;
173
183
  /** Default production marketplace deps — public listings API + presigned S3. */
174
184
  export declare function defaultMarketplaceDeps(): MarketplaceDeps;
175
185
  /**
@@ -449,8 +459,17 @@ export interface InstallPackOptions {
449
459
  * for marketplace sources, which always require a platform signature.
450
460
  */
451
461
  allowUnsigned?: boolean;
462
+ /** Packs whose install led to this one, for dependency installs; guards cycles. */
463
+ dependencyChain?: string[];
452
464
  }
453
465
  export declare function runPackInstallWithLease<T>(run: () => Promise<T>, leaseOptions?: ResidentCommandLeaseOptions): Promise<T>;
454
- export declare function installPack(source: string, opts?: InstallPackOptions): Promise<void>;
466
+ /** What `installPack` installed; undefined when the install was aborted or skipped. */
467
+ export interface InstallPackResult {
468
+ pkg: PackManifest;
469
+ /** Absolute pack root under `<hqRoot>/core/packages/<name>`. */
470
+ destDir: string;
471
+ hqRoot: string;
472
+ }
473
+ export declare function installPack(source: string, opts?: InstallPackOptions): Promise<InstallPackResult | undefined>;
455
474
  export {};
456
475
  //# sourceMappingURL=pack-install.d.ts.map
@@ -58,6 +58,7 @@ import { formatApiKeyCapabilityDenial, peekHqApiKey, } from '../utils/resolve-va
58
58
  import { redactSecrets, SECRET_REDACTION, codexConfigFormat, codexConfigPath, isMcpRegistrationEnabled, registerMcpServers, readConfigDoc, unregisterMcpServers, McpManifestError, } from './mcp-registration.js';
59
59
  import { listSecretCacheScopes } from '../utils/secrets-cache.js';
60
60
  import { loadRevealedSecrets } from './secrets.js';
61
+ import { parseSetupBlock } from '../lib/packages/setup-block.js';
61
62
  import { syncCodexPackHooks, syncCodexPackSkills } from '../lib/install/codex-pack-contributions.js';
62
63
  const PACK_UPDATE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
63
64
  const PACK_UPDATE_FETCH_TIMEOUT_MS = 3_000;
@@ -311,12 +312,35 @@ export function toMarketplaceListing(raw) {
311
312
  publicKey: detail.publicKey,
312
313
  };
313
314
  }
315
+ /**
316
+ * Run a marketplace request again when the server answers 5xx or the network
317
+ * fails. A brief 502 from the API gateway ended a pack install from the app
318
+ * ("Failed to resolve listing … (HTTP 502)") although the next request
319
+ * succeeded. 4xx answers and the final attempt are returned as they are.
320
+ */
321
+ export async function withTransientRetry(request, opts = {}) {
322
+ const delays = opts.delaysMs ?? [500, 1500];
323
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
324
+ for (let attempt = 0;; attempt += 1) {
325
+ const last = attempt >= delays.length;
326
+ try {
327
+ const res = await request();
328
+ if (res.status < 500 || last)
329
+ return res;
330
+ }
331
+ catch (err) {
332
+ if (last)
333
+ throw err;
334
+ }
335
+ await sleep(delays[attempt]);
336
+ }
337
+ }
314
338
  /** Default production marketplace deps — public listings API + presigned S3. */
315
339
  export function defaultMarketplaceDeps() {
316
340
  const fetchDetail = async (listingId) => {
317
- const res = await vaultApiFetchPublic({
341
+ const res = await withTransientRetry(() => vaultApiFetchPublic({
318
342
  path: `/v1/listings/${encodeURIComponent(listingId)}`,
319
- });
343
+ }));
320
344
  if (!res.ok) {
321
345
  throw new Error(`Failed to resolve listing ${listingId} (HTTP ${res.status}). The pack may be unavailable or unapproved.`);
322
346
  }
@@ -340,10 +364,10 @@ export function defaultMarketplaceDeps() {
340
364
  const query = { slug, q: slug };
341
365
  if (version)
342
366
  query.version = version;
343
- const res = await vaultApiFetchPublic({
367
+ const res = await withTransientRetry(() => vaultApiFetchPublic({
344
368
  path: '/v1/listings',
345
369
  query,
346
- });
370
+ }));
347
371
  if (!res.ok) {
348
372
  throw new Error(`Failed to search marketplace for "${slug}" (HTTP ${res.status}).`);
349
373
  }
@@ -1149,6 +1173,17 @@ export function validateManifest(payloadDir, hqVersion, options = {}) {
1149
1173
  }
1150
1174
  }
1151
1175
  }
1176
+ // setup — OPTIONAL and backwards-compatible.
1177
+ // Absent → the pack installs exactly as before. Present → the block must be
1178
+ // well-shaped, its bot must be a contributed worker, and every script it
1179
+ // names must exist in the payload. The error names the failing field
1180
+ // (lib/packages/setup-block.ts), e.g. `setup.run.script "x" does not exist`.
1181
+ if (m.setup !== undefined) {
1182
+ m.setup = parseSetupBlock(m.setup, {
1183
+ packDir: payloadDir,
1184
+ workers: contributes.workers ?? [],
1185
+ });
1186
+ }
1152
1187
  return m;
1153
1188
  }
1154
1189
  // ---------------------------------------------------------------------------
@@ -1180,7 +1215,7 @@ export function assertPackDependencies(hqRoot, pkg) {
1180
1215
  }
1181
1216
  if (!installed.has(dep.name)) {
1182
1217
  throw new Error(`Pack ${pkg.name} requires ${dep.name}, which is not installed. ` +
1183
- `Install it first, e.g.: hq install marketplace:${dep.name}`);
1218
+ `Install it first, e.g.: hq install ${packSlugFromName(dep.name)}`);
1184
1219
  }
1185
1220
  if (dep.version) {
1186
1221
  const have = installed.get(dep.name);
@@ -1191,6 +1226,42 @@ export function assertPackDependencies(hqRoot, pkg) {
1191
1226
  }
1192
1227
  }
1193
1228
  }
1229
+ /** Marketplace slug for a pack name: `hq-pack-client-service` -> `client-service`. */
1230
+ function packSlugFromName(name) {
1231
+ return name.startsWith('hq-pack-') ? name.slice('hq-pack-'.length) : name;
1232
+ }
1233
+ /**
1234
+ * Install a marketplace pack's missing `requires.packs` from the marketplace
1235
+ * first, so one install from a pack page or a pasted link brings the packs it
1236
+ * needs. Only marketplace installs do this: the dependency comes from the same
1237
+ * signed channel. A dependency that is installed at a version outside the
1238
+ * range is left alone, and assertPackDependencies reports it.
1239
+ */
1240
+ async function installMissingMarketplaceDependencies(hqRoot, pkg, opts, say) {
1241
+ const deps = pkg.requires?.packs ?? [];
1242
+ if (deps.length === 0)
1243
+ return;
1244
+ const chain = [...(opts.dependencyChain ?? []), pkg.name];
1245
+ for (const dep of deps) {
1246
+ if (dep.name === pkg.name)
1247
+ continue; // assertPackDependencies refuses this
1248
+ const installed = new Set(listInstalledPacks(hqRoot).map((p) => p.manifest?.name ?? p.name));
1249
+ if (installed.has(dep.name))
1250
+ continue;
1251
+ if (chain.includes(dep.name)) {
1252
+ throw new Error(`Pack dependency cycle: ${[...chain, dep.name].join(' -> ')}`);
1253
+ }
1254
+ say(chalk.dim(`-> ${pkg.name} requires ${dep.name}; installing it from the marketplace first`));
1255
+ const result = await installPackWithoutLease(`${MARKETPLACE_PREFIX}${packSlugFromName(dep.name)}`, {
1256
+ ...opts,
1257
+ integrity: undefined,
1258
+ dependencyChain: chain,
1259
+ });
1260
+ if (!result) {
1261
+ throw new Error(`Pack ${pkg.name} requires ${dep.name}, and its install did not finish.`);
1262
+ }
1263
+ }
1264
+ }
1194
1265
  // ---------------------------------------------------------------------------
1195
1266
  // Post-install get-started line (US-005)
1196
1267
  // ---------------------------------------------------------------------------
@@ -1743,6 +1814,9 @@ async function installPackWithoutLease(source, opts) {
1743
1814
  const pkg = validateManifest(fetched.payloadDir, hqVersion, {
1744
1815
  allowReservedName: transport === 'local' || allowPinnedAliasName,
1745
1816
  });
1817
+ if (transport === 'marketplace') {
1818
+ await installMissingMarketplaceDependencies(hqRoot, pkg, opts, say);
1819
+ }
1746
1820
  // M0 — pack-to-pack dependency pre-flight. Runs BEFORE any prompts or writes
1747
1821
  // so a missing/unsatisfied `requires.packs` aborts with no partial state, and
1748
1822
  // before we bother the operator with hook/MCP trust prompts for a pack that
@@ -1826,6 +1900,7 @@ async function installPackWithoutLease(source, opts) {
1826
1900
  const getStarted = getStartedLine(pkg.initialization);
1827
1901
  if (getStarted)
1828
1902
  say(chalk.cyan(getStarted));
1903
+ return { pkg, destDir, hqRoot };
1829
1904
  }
1830
1905
  finally {
1831
1906
  fs.rmSync(tmpDir, { recursive: true, force: true });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Production wiring for `hq packages install --start-bot`: the bot lifecycle
3
+ * seams of lib/packages/start-bot.ts bound to the real `hq bot` internals
4
+ * (create, restart, intro) and the bot DM API. Loaded on demand by
5
+ * pkg-install.ts so an ordinary install never imports the bot machinery.
6
+ */
7
+ import type { InstallPackResult } from "./pack-install.js";
8
+ import { type StartPackBotDeps, type StartPackBotResult } from "../lib/packages/start-bot.js";
9
+ /**
10
+ * The model and thinking level a pack's setup bot runs on. Left unset, Claude
11
+ * Code used the account's default model (Claude Fable 5.1 in a 2026-10-12
12
+ * walkthrough), and one setup turn took nine minutes. Pack setup is guided
13
+ * reading and writing: Opus 5.5 at medium thinking does it well and faster.
14
+ * The person can still change either with `hq bot set`.
15
+ */
16
+ export declare const PACK_BOT_MODEL = "claude-opus-5-5";
17
+ export declare const PACK_BOT_EFFORT = "medium";
18
+ export declare function defaultStartPackBotDeps(): StartPackBotDeps;
19
+ /**
20
+ * Start the pack's setup bot after a successful install. Never throws: a bot
21
+ * that cannot start leaves the install successful (contract section 3).
22
+ */
23
+ export declare function startBotForInstalledPack(result: InstallPackResult | undefined, deps?: StartPackBotDeps): Promise<StartPackBotResult | null>;
24
+ //# sourceMappingURL=pack-start-bot.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Production wiring for `hq packages install --start-bot`: the bot lifecycle
3
+ * seams of lib/packages/start-bot.ts bound to the real `hq bot` internals
4
+ * (create, restart, intro) and the bot DM API. Loaded on demand by
5
+ * pkg-install.ts so an ordinary install never imports the bot machinery.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import chalk from "chalk";
9
+ import { botExists, botTokenSupplier, runBotCreate, runBotIntro, runBotRestart } from "./bot.js";
10
+ import { BotApi } from "../lib/bot/api.js";
11
+ import { readBotConfig } from "../lib/bot/config.js";
12
+ import { botCredsPath, botDir } from "../lib/bot/paths.js";
13
+ import { startPackSetupBot } from "../lib/packages/start-bot.js";
14
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
15
+ import { resolveCallerPersonUid } from "../utils/vault-api.js";
16
+ /**
17
+ * One DM to the owner. As the bot when its identity exists on this computer
18
+ * (the same path `hq bot intro` uses); otherwise as the signed-in person, to
19
+ * themselves, with the session the install already relied on.
20
+ */
21
+ async function dmOwner(name, body) {
22
+ const dir = botDir(name);
23
+ const config = readBotConfig(dir);
24
+ if (config && fs.existsSync(botCredsPath(dir))) {
25
+ await new BotApi({ token: botTokenSupplier(dir) }).sendDm({ toPersonUid: config.ownerUid, body });
26
+ return;
27
+ }
28
+ const token = await ensureCognitoToken({ tokenSource: "person", interactive: false });
29
+ const owner = await resolveCallerPersonUid(token, undefined, { command: "hq packages install --start-bot" });
30
+ if (!owner)
31
+ throw new Error("your HQ login has no person entity yet");
32
+ await new BotApi({ token: async () => token }).sendDm({ toPersonUid: owner, body });
33
+ }
34
+ /**
35
+ * The model and thinking level a pack's setup bot runs on. Left unset, Claude
36
+ * Code used the account's default model (Claude Fable 5.1 in a 2026-10-12
37
+ * walkthrough), and one setup turn took nine minutes. Pack setup is guided
38
+ * reading and writing: Opus 5.5 at medium thinking does it well and faster.
39
+ * The person can still change either with `hq bot set`.
40
+ */
41
+ export const PACK_BOT_MODEL = "claude-opus-5-5";
42
+ export const PACK_BOT_EFFORT = "medium";
43
+ export function defaultStartPackBotDeps() {
44
+ return {
45
+ botExists,
46
+ createBot: (name, opts) => runBotCreate(name, {
47
+ worker: opts.worker,
48
+ displayName: opts.displayName,
49
+ intro: opts.intro,
50
+ kickoff: opts.kickoff,
51
+ lean: true,
52
+ model: PACK_BOT_MODEL,
53
+ effort: PACK_BOT_EFFORT,
54
+ }),
55
+ restartBot: (name) => runBotRestart(name, { output: "text" }),
56
+ sendIntro: (name) => runBotIntro(name),
57
+ dmOwner,
58
+ say: (line) => console.log(line),
59
+ };
60
+ }
61
+ /**
62
+ * Start the pack's setup bot after a successful install. Never throws: a bot
63
+ * that cannot start leaves the install successful (contract section 3).
64
+ */
65
+ export async function startBotForInstalledPack(result, deps = defaultStartPackBotDeps()) {
66
+ if (!result)
67
+ return null;
68
+ const setup = result.pkg.setup;
69
+ if (!setup) {
70
+ deps.say(chalk.dim(`--start-bot: ${result.pkg.name} has no setup: block, so there is no bot to start.`));
71
+ return null;
72
+ }
73
+ deps.say(chalk.dim(`-> starting the pack's setup bot ${setup.bot.name} (${setup.bot.display_name})`));
74
+ return startPackSetupBot(result.pkg.name, setup, deps);
75
+ }
76
+ //# sourceMappingURL=pack-start-bot.js.map
@@ -41,6 +41,8 @@ interface InstalledPackView {
41
41
  }>;
42
42
  inCatalog: boolean;
43
43
  updateAvailable: boolean | null;
44
+ /** True when the pack declares a valid `setup:` block (chat-driven setup bot). */
45
+ setup?: boolean;
44
46
  /**
45
47
  * Post-install initialization (US-005). Present only when the pack's
46
48
  * package.yaml declares an `initialization` block — drives the HQ Sync
@@ -77,5 +79,12 @@ export interface UpdateDependencies {
77
79
  export declare function runUpdate(name: string | undefined, opts: UpdateOpts, deps?: UpdateDependencies): Promise<UpdateCheck[]>;
78
80
  export declare function registerPacksListCommand(parent: Command): void;
79
81
  export declare function registerPacksCommand(parent: Command): void;
82
+ /**
83
+ * The source to reinstall from. A marketplace install stamps the exact version
84
+ * it got (`marketplace:<slug>@<version>`), so reinstalling that source fetched
85
+ * the same version again: `hq packs update` reported 0.4.15 as latest and put
86
+ * 0.4.14 back. With a newer listing known, install that version instead.
87
+ */
88
+ export declare function sourceForUpdate(source: string, probe: Pick<LatestResult, "transport" | "latest" | "updateAvailable">): string;
80
89
  export {};
81
90
  //# sourceMappingURL=packs.d.ts.map
@@ -27,8 +27,9 @@ import semverSatisfies from 'semver/functions/satisfies.js';
27
27
  import { findHqRoot } from '../utils/manifest.js';
28
28
  import { resolveHqRoot as resolveDetectedHqRoot } from './doctor.js';
29
29
  import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
30
- import { classify, PINNED_HQ_ANYWHERE_SOURCE, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
30
+ import { classify, PINNED_HQ_ANYWHERE_SOURCE, resolveLatest, parseMarketplaceSource, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
31
31
  import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
32
+ import { readInstalledPackSetup } from '../lib/packages/setup-block.js';
32
33
  import { syncCodexPackHooks, syncCodexPackSkills } from '../lib/install/codex-pack-contributions.js';
33
34
  function resolveRoot(opts) {
34
35
  return opts.hqRoot ? path.resolve(opts.hqRoot) : findHqRoot();
@@ -124,6 +125,10 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
124
125
  initialization.prompt = initObj.prompt;
125
126
  }
126
127
  }
128
+ // pack setup bots — `setup: yes` only for a block that is
129
+ // valid against the installed pack (its bot is a contributed worker and the
130
+ // scripts it names exist); `no` for packs without one or with a broken one.
131
+ const setup = readInstalledPackSetup(m, pack.dir) !== undefined;
127
132
  return {
128
133
  name: m.name ?? pack.name,
129
134
  version: m.version,
@@ -137,6 +142,7 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
137
142
  brokenLinks,
138
143
  inCatalog: m.source ? installedSources.has(m.source) : false,
139
144
  updateAvailable,
145
+ setup,
140
146
  ...(initialization ? { initialization } : {}),
141
147
  };
142
148
  }
@@ -189,7 +195,7 @@ function printListHuman(view) {
189
195
  const broken = p.links.broken > 0 ? chalk.red(` ! ${p.links.broken} broken link(s)`) : '';
190
196
  const upd = p.updateAvailable ? chalk.yellow(' ^ update available') : '';
191
197
  const hq = p.hqCoreSatisfied === false ? chalk.red(' x hqCore mismatch') : '';
192
- console.log(` ${chalk.green(p.name)}@${p.version ?? '?'} ${chalk.dim(p.transport ?? '')}${upd}${hq}${broken}`);
198
+ console.log(` ${chalk.green(p.name)}@${p.version ?? '?'} ${chalk.dim(p.transport ?? '')} setup: ${p.setup ? 'yes' : 'no'}${upd}${hq}${broken}`);
193
199
  }
194
200
  console.log();
195
201
  }
@@ -273,7 +279,7 @@ export async function runUpdate(name, opts, deps = {}) {
273
279
  if (opts.hqRoot)
274
280
  process.chdir(hqRoot); // installPack resolves via findHqRoot()
275
281
  try {
276
- await installPack(source, {
282
+ await installPack(sourceForUpdate(source, probe), {
277
283
  allowHooks: opts.yes || opts.allowHooks,
278
284
  allowMcp: opts.yes || opts.allowMcp,
279
285
  followBranch: opts.branch,
@@ -514,4 +520,21 @@ export function registerPacksCommand(parent) {
514
520
  }
515
521
  });
516
522
  }
523
+ /**
524
+ * The source to reinstall from. A marketplace install stamps the exact version
525
+ * it got (`marketplace:<slug>@<version>`), so reinstalling that source fetched
526
+ * the same version again: `hq packs update` reported 0.4.15 as latest and put
527
+ * 0.4.14 back. With a newer listing known, install that version instead.
528
+ */
529
+ export function sourceForUpdate(source, probe) {
530
+ if (probe.transport !== "marketplace" || !probe.latest || probe.updateAvailable !== true)
531
+ return source;
532
+ try {
533
+ const { slug } = parseMarketplaceSource(source);
534
+ return `marketplace:${slug}@${probe.latest}`;
535
+ }
536
+ catch {
537
+ return source;
538
+ }
539
+ }
517
540
  //# sourceMappingURL=packs.js.map
@@ -44,6 +44,7 @@ export function registerPackageInstallCommand(parent) {
44
44
  .option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
45
45
  .option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
46
46
  .option('--allow-unsigned', 'Acknowledge an unsigned local directory source; marketplace installs always require a signature')
47
+ .option('--start-bot', "After install, create and start the pack's setup bot (packs with a setup: block); prints `bot: <name>`")
47
48
  .option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
48
49
  .option('--global', 'Install HQ into an agent runtime at user scope (requires the hq-anywhere-runtime flag)')
49
50
  .option('--runtime <runtime>', 'Runtime for --global (claude | codex | chatgpt | grok)', 'claude')
@@ -128,8 +129,9 @@ export function registerPackageInstallCommand(parent) {
128
129
  });
129
130
  return;
130
131
  }
132
+ let installed;
131
133
  if (sourceMatchesPackPattern(source)) {
132
- await installPack(source, {
134
+ installed = await installPack(source, {
133
135
  company: opts.company,
134
136
  allowHooks: opts.allowHooks,
135
137
  allowMcp: opts.allowMcp,
@@ -142,7 +144,7 @@ export function registerPackageInstallCommand(parent) {
142
144
  // (/packages, /entitlements) was never deployed, so a bare slug
143
145
  // would fail at registry setup. Resolve it through the marketplace
144
146
  // listings transport — the live install path — instead.
145
- await installPack(`${MARKETPLACE_PREFIX}${source}`, {
147
+ installed = await installPack(`${MARKETPLACE_PREFIX}${source}`, {
146
148
  company: opts.company,
147
149
  allowHooks: opts.allowHooks,
148
150
  allowMcp: opts.allowMcp,
@@ -150,6 +152,13 @@ export function registerPackageInstallCommand(parent) {
150
152
  followBranch: opts.branch,
151
153
  });
152
154
  }
155
+ // pack setup bots — a pack with a valid `setup:` block
156
+ // gets its bot created and started after wiring. A bot that cannot
157
+ // start never fails the install (the reason is printed and DMed).
158
+ if (opts.startBot) {
159
+ const { startBotForInstalledPack } = await import('./pack-start-bot.js');
160
+ await startBotForInstalledPack(installed);
161
+ }
153
162
  }
154
163
  catch (error) {
155
164
  console.error(chalk.red('Install failed:'), error instanceof Error ? error.message : 'Unknown error');
@@ -1,12 +1,8 @@
1
1
  import type { Command } from "commander";
2
+ export { readSessionCompanyLock } from "../lib/session-company-lock.js";
2
3
  export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
3
4
  export type { SecretLoadResponse, SecretScriptLockMode, SecretTier, SecretUsage, SecretUsageChannel } from "../secrets-env-action.js";
4
5
  export type { SecretScriptUsage, SecretMetadata, SecretLoadSuccessRow } from "../secrets-env-action.js";
5
- type SessionCompanyLock = {
6
- sessionId: string;
7
- companies: string[];
8
- } | undefined;
9
- export declare function readSessionCompanyLock(env?: NodeJS.ProcessEnv): SessionCompanyLock;
10
6
  import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
11
7
  export type { VaultApiOptions } from "../utils/vault-api.js";
12
8
  export { vaultApiFetch, getCompanyUid, getEntityUid };
@@ -2,98 +2,9 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as yaml from "js-yaml";
4
4
  import { assertApiKeyCompany, buildSecretUsage, extractApiMessage, highSecuritySandboxOnlyMessage, loadRevealedSecrets, normalizeScriptLockMode, normalizeSecretTier, parseSecretNameList, requestedApiKeyCompany, runSecretsEnvCommand, scopeOpts, secretsCredentialOptions, } from "../secrets-env-action.js";
5
+ import { readSessionCompanyLock } from "../lib/session-company-lock.js";
6
+ export { readSessionCompanyLock } from "../lib/session-company-lock.js";
5
7
  export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
6
- export function readSessionCompanyLock(env = process.env) {
7
- const root = resolveDefaultHqRoot({ env });
8
- const sessionId = resolveSessionId(root, env);
9
- if (!sessionId)
10
- return undefined;
11
- const sessionDir = path.join(root, "workspace/sessions", sessionId);
12
- const capabilityPath = path.join(sessionDir, "scope-capability.json");
13
- const metaPath = path.join(sessionDir, "meta.yaml");
14
- let primary;
15
- let companies;
16
- let hasCapability = false;
17
- try {
18
- fs.lstatSync(capabilityPath);
19
- hasCapability = true;
20
- }
21
- catch (error) {
22
- const code = error.code;
23
- if (code !== "ENOENT" && code !== "ENOTDIR")
24
- throw new Error("Cannot verify this session's company lock because its scope capability is unreadable.");
25
- }
26
- if (hasCapability) {
27
- let value;
28
- try {
29
- value = JSON.parse(fs.readFileSync(capabilityPath, "utf8"));
30
- }
31
- catch {
32
- throw new Error("Cannot verify this session's company lock because its scope capability is unreadable.");
33
- }
34
- if (!value || typeof value !== "object" || Array.isArray(value))
35
- throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
36
- const cap = value;
37
- if (cap.session_id !== sessionId)
38
- throw new Error("Cannot verify this session's company lock because its scope capability does not match the active session.");
39
- if (typeof cap.company_slug !== "string" || !/^[A-Za-z0-9_-]+$/.test(cap.company_slug))
40
- throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
41
- primary = cap.company_slug;
42
- if (cap.company_slugs === undefined)
43
- companies = [primary];
44
- else if (Array.isArray(cap.company_slugs)
45
- && cap.company_slugs.length > 0
46
- && cap.company_slugs.every((slug) => typeof slug === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(slug))
47
- && cap.company_slugs[0] === primary
48
- && new Set(cap.company_slugs).size === cap.company_slugs.length
49
- && ((primary === "personal" && cap.company_slugs.length === 1)
50
- || (primary !== "personal" && !cap.company_slugs.includes("personal"))))
51
- companies = [...cap.company_slugs];
52
- else
53
- throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
54
- }
55
- else {
56
- try {
57
- const meta = fs.readFileSync(metaPath, "utf8");
58
- const read = (key) => meta.split(/\r?\n/).find((line) => new RegExp(`^\\s*${key}:`).test(line))?.replace(/^\s*[^:]+:\s*/, "").replace(/^"|"$/g, "") ?? "";
59
- primary = read("company_slug");
60
- const hasList = meta.split(/\r?\n/).some((line) => /^\s*company_slugs:/.test(line));
61
- const listed = hasList ? read("company_slugs") : "";
62
- if (hasList && (!listed || !primary))
63
- throw new Error("invalid company lock");
64
- companies = hasList ? listed.split(",") : primary ? [primary] : [];
65
- if (companies.some((slug) => !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(slug)))
66
- throw new Error("invalid company lock");
67
- if (new Set(companies).size !== companies.length)
68
- throw new Error("invalid company lock");
69
- if (primary && companies[0] !== primary)
70
- throw new Error("invalid company lock");
71
- if (companies.length > 1 && companies.includes("personal"))
72
- throw new Error("invalid company lock");
73
- }
74
- catch (error) {
75
- const code = error.code;
76
- if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES")
77
- return { sessionId, companies: [] };
78
- throw new Error("Cannot verify this session's company lock because its metadata is invalid.");
79
- }
80
- }
81
- const real = companies.filter((slug) => {
82
- if (slug === "personal")
83
- return true;
84
- try {
85
- const companyPath = path.join(root, "companies", slug);
86
- const stat = fs.lstatSync(companyPath);
87
- return stat.isDirectory() && !stat.isSymbolicLink();
88
- }
89
- catch {
90
- return false;
91
- }
92
- });
93
- if (primary && real[0] !== primary)
94
- return { sessionId, companies: [] };
95
- return { sessionId, companies: real };
96
- }
97
8
  function companyUidForSlug(root, slug) {
98
9
  try {
99
10
  const doc = yaml.load(fs.readFileSync(path.join(root, "companies", "manifest.yaml"), "utf8"));
@@ -147,7 +58,6 @@ import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from "../u
147
58
  import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
148
59
  import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
149
60
  import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
150
- import { resolveSessionId } from "../lib/core-utils/session-id.js";
151
61
  import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
152
62
  import { isPlanGateError } from "../utils/plan-gate-error.js";
153
63
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
@@ -148,6 +148,13 @@ export interface BotConfig {
148
148
  introSentAt?: string;
149
149
  /** The kickoff could not run because the coding tool needed a sign-in; it runs once that works. */
150
150
  kickoffPendingSignIn?: boolean;
151
+ /**
152
+ * Run Claude Code turns lean: without the computer's plugins, MCP servers and
153
+ * HQ's session/prompt/stop hooks, keeping only HQ's tool hooks (PreToolUse,
154
+ * PostToolUse). A plain reply then takes seconds instead of ~20. Set for pack
155
+ * setup bots, which only need files and shell commands.
156
+ */
157
+ lean?: boolean;
151
158
  }
152
159
  export declare function readBotConfig(dir: string): BotConfig | null;
153
160
  export declare function writeBotConfig(dir: string, config: BotConfig): void;
@@ -15,6 +15,7 @@ import * as path from "node:path";
15
15
  import { spawn } from "node:child_process";
16
16
  import { resolveHqBinary, resolveNodeBinary, preferStableHqBinary } from "../mesh/live/daemon/install.js";
17
17
  import { botLogPath, launchdLabel } from "./paths.js";
18
+ import { hqAppToolchainRoot } from "./toolchain.js";
18
19
  import { discoverLaunchdIdentity, installLaunchdPlist, launchdProgramArguments, launchdPlistProgramArguments as parsePlistArguments, parseLaunchdCommand, reloadLaunchd } from "../service-manager/launchd.js";
19
20
  import { nodeBinaryProblem, preferStableNodeBinary } from "../node-binary.js";
20
21
  export function detectBotPlatform(platform = process.platform) {
@@ -25,7 +26,7 @@ export function detectBotPlatform(platform = process.platform) {
25
26
  return "other";
26
27
  }
27
28
  export function defaultPathEntries(home) {
28
- const toolchain = path.join(home, "Library", "Application Support", "Indigo HQ", "toolchain");
29
+ const toolchain = hqAppToolchainRoot(home);
29
30
  return [
30
31
  // launchd does not load shell profiles; npm CLIs also need env node.
31
32
  path.join(toolchain, "node", "bin"),
@@ -20,6 +20,8 @@ export * from "./local-name.js";
20
20
  export * from "./job-run.js";
21
21
  export * from "./job-completions.js";
22
22
  export * from "./inflight.js";
23
+ export * from "./toolchain.js";
24
+ export * from "./pack-link.js";
23
25
  export * from "./lock.js";
24
26
  export * from "./display-name.js";
25
27
  //# sourceMappingURL=index.d.ts.map
@@ -20,6 +20,8 @@ export * from "./local-name.js";
20
20
  export * from "./job-run.js";
21
21
  export * from "./job-completions.js";
22
22
  export * from "./inflight.js";
23
+ export * from "./toolchain.js";
24
+ export * from "./pack-link.js";
23
25
  export * from "./lock.js";
24
26
  export * from "./display-name.js";
25
27
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Claude Code settings for a lean bot turn (BotConfig.lean). The turn runs
3
+ * with `--setting-sources ""` and `--strict-mcp-config`, so no user or project
4
+ * settings, plugins or MCP servers load; this file brings back only HQ's tool
5
+ * hooks (PreToolUse, PostToolUse) from `<hqRoot>/.claude/settings.json`, with
6
+ * `$CLAUDE_PROJECT_DIR` spelled out as the HQ root.
7
+ */
8
+ export declare const LEAN_SETTINGS_FILE = "lean-claude-settings.json";
9
+ export declare const LEAN_HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse"];
10
+ /** The lean settings object for an HQ root (empty hooks when HQ has none). */
11
+ export declare function leanClaudeSettings(hqRoot: string): {
12
+ hooks: Record<string, unknown>;
13
+ };
14
+ /** Write the lean settings into the bot folder; returns the file path. */
15
+ export declare function writeLeanClaudeSettings(botDir: string, hqRoot: string): string;
16
+ //# sourceMappingURL=lean-settings.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Claude Code settings for a lean bot turn (BotConfig.lean). The turn runs
3
+ * with `--setting-sources ""` and `--strict-mcp-config`, so no user or project
4
+ * settings, plugins or MCP servers load; this file brings back only HQ's tool
5
+ * hooks (PreToolUse, PostToolUse) from `<hqRoot>/.claude/settings.json`, with
6
+ * `$CLAUDE_PROJECT_DIR` spelled out as the HQ root.
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ export const LEAN_SETTINGS_FILE = "lean-claude-settings.json";
11
+ export const LEAN_HOOK_EVENTS = ["PreToolUse", "PostToolUse"];
12
+ /** The lean settings object for an HQ root (empty hooks when HQ has none). */
13
+ export function leanClaudeSettings(hqRoot) {
14
+ let hooks = {};
15
+ try {
16
+ const raw = JSON.parse(fs.readFileSync(path.join(hqRoot, ".claude", "settings.json"), "utf8"));
17
+ for (const event of LEAN_HOOK_EVENTS) {
18
+ if (raw.hooks?.[event] !== undefined)
19
+ hooks[event] = raw.hooks[event];
20
+ }
21
+ hooks = JSON.parse(JSON.stringify(hooks).replace(/\$\{?CLAUDE_PROJECT_DIR\}?/g, hqRoot.replace(/\\/g, "\\\\").replace(/"/g, '\\"')));
22
+ }
23
+ catch {
24
+ hooks = {};
25
+ }
26
+ return { hooks };
27
+ }
28
+ /** Write the lean settings into the bot folder; returns the file path. */
29
+ export function writeLeanClaudeSettings(botDir, hqRoot) {
30
+ const file = path.join(botDir, LEAN_SETTINGS_FILE);
31
+ fs.writeFileSync(file, `${JSON.stringify(leanClaudeSettings(hqRoot), null, 2)}\n`, { mode: 0o600 });
32
+ return file;
33
+ }
34
+ //# sourceMappingURL=lean-settings.js.map