@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.
- package/CHANGELOG.md +93 -0
- package/assets/bot-workers/setup/skills/add-a-pack.md +130 -0
- package/assets/bot-workers/setup/worker.yaml +3 -0
- package/dist/command-catalog.generated.d.ts +206 -0
- package/dist/command-catalog.generated.js +246 -0
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +20 -12
- package/dist/commands/daemon.js +385 -1
- package/dist/commands/integrations-connect.d.ts +6 -0
- package/dist/commands/integrations-connect.js +36 -0
- package/dist/commands/integrations-slack.d.ts +30 -0
- package/dist/commands/integrations-slack.js +81 -0
- package/dist/commands/integrations.js +2 -0
- package/dist/commands/pack-install.d.ts +20 -1
- package/dist/commands/pack-install.js +80 -5
- package/dist/commands/pack-start-bot.d.ts +24 -0
- package/dist/commands/pack-start-bot.js +76 -0
- package/dist/commands/packs.d.ts +9 -0
- package/dist/commands/packs.js +26 -3
- package/dist/commands/pkg-install.js +11 -2
- package/dist/commands/secrets.d.ts +1 -5
- package/dist/commands/secrets.js +2 -92
- package/dist/lib/bot/config.d.ts +7 -0
- package/dist/lib/bot/daemon.js +2 -1
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/lean-settings.d.ts +16 -0
- package/dist/lib/bot/lean-settings.js +34 -0
- package/dist/lib/bot/pack-link.d.ts +66 -0
- package/dist/lib/bot/pack-link.js +153 -0
- package/dist/lib/bot/pack-setup-driver.d.ts +37 -0
- package/dist/lib/bot/pack-setup-driver.js +83 -0
- package/dist/lib/bot/pack-setup.d.ts +62 -0
- package/dist/lib/bot/pack-setup.js +103 -0
- package/dist/lib/bot/prompt.d.ts +8 -0
- package/dist/lib/bot/prompt.js +5 -0
- package/dist/lib/bot/run.d.ts +18 -2
- package/dist/lib/bot/run.js +163 -25
- package/dist/lib/bot/runtime/claude.d.ts +27 -2
- package/dist/lib/bot/runtime/claude.js +49 -4
- package/dist/lib/bot/runtime/types.d.ts +5 -0
- package/dist/lib/bot/runtime-failure.d.ts +18 -2
- package/dist/lib/bot/runtime-failure.js +21 -1
- package/dist/lib/bot/toolchain.d.ts +25 -0
- package/dist/lib/bot/toolchain.js +33 -0
- package/dist/lib/bot/worker-source.d.ts +1 -0
- package/dist/lib/bot/worker-source.js +7 -1
- package/dist/lib/daemon/catalog.d.ts +15 -0
- package/dist/lib/daemon/catalog.js +49 -0
- package/dist/lib/daemon/paths.d.ts +1 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/remote-control-forwarder.d.ts +14 -0
- package/dist/lib/daemon/remote-control-forwarder.js +104 -0
- package/dist/lib/daemon/remote-control-pools.d.ts +44 -0
- package/dist/lib/daemon/remote-control-pools.js +245 -0
- package/dist/lib/daemon/remote-control.d.ts +4 -0
- package/dist/lib/daemon/remote-control.js +17 -2
- package/dist/lib/daemon/run.js +126 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +5 -4
- package/dist/lib/doctor/fix/refresh-sync-progress.js +5 -4
- package/dist/lib/doctor/json-output.d.ts +5 -5
- package/dist/lib/doctor/json-output.js +2 -5
- package/dist/lib/packages/setup-block.d.ts +43 -0
- package/dist/lib/packages/setup-block.js +236 -0
- package/dist/lib/packages/setup-prompt.d.ts +34 -0
- package/dist/lib/packages/setup-prompt.js +128 -0
- package/dist/lib/packages/setup-takeover.d.ts +50 -0
- package/dist/lib/packages/setup-takeover.js +75 -0
- package/dist/lib/packages/start-bot.d.ts +47 -0
- package/dist/lib/packages/start-bot.js +66 -0
- package/dist/lib/session-company-lock.d.ts +7 -0
- package/dist/lib/session-company-lock.js +97 -0
- package/dist/types.d.ts +82 -0
- 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
|
-
|
|
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
|
|
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
|
package/dist/commands/packs.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/packs.js
CHANGED
|
@@ -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 };
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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";
|
package/dist/lib/bot/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/bot/daemon.js
CHANGED
|
@@ -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 =
|
|
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"),
|
package/dist/lib/bot/index.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/bot/index.js
CHANGED
|
@@ -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
|