@indigoai-us/hq-cli 5.345.103 → 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 +97 -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 +19 -7
- package/dist/lib/doctor/json-output.js +16 -6
- 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
package/dist/lib/bot/run.js
CHANGED
|
@@ -63,10 +63,13 @@ import { containsNoReplySentinel, ProgressPoster, } from "./progress.js";
|
|
|
63
63
|
import { splitBody } from "./split.js";
|
|
64
64
|
import { createBotLogger } from "./log.js";
|
|
65
65
|
import { buildSystemPrompt, introDmText, readPromptSources } from "./prompt.js";
|
|
66
|
+
import { resolvePackSetupPrompt } from "./pack-setup.js";
|
|
67
|
+
import { writeLeanClaudeSettings } from "./lean-settings.js";
|
|
68
|
+
import { companyDisplayName, firstQuestionNote, questionDefault, questionMessage, readPackSetupState, writePackSetupState, } from "./pack-setup-driver.js";
|
|
66
69
|
import { loadSetupExplainer, patchSetupIntroState, readSetupIntroState, readSetupProgressIntro, replyOffersExplain, SETUP_EXPLAINED_NOTE, setupExplainDue, } from "./setup-explain.js";
|
|
67
70
|
import { detectCodingToolActivity, SIGNIFICANT_CODING_TOOL_ACTIVITY, } from "./coding-tool-activity.js";
|
|
68
71
|
import { setupKeptHereNote, setupOfferAnsweredInWordsNote, setupToolOfferAnswer, setupToolOfferText, setupWelcomeOpening, } from "./setup-tool-offer.js";
|
|
69
|
-
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled } from "./runtime/claude.js";
|
|
72
|
+
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled, onPath } from "./runtime/claude.js";
|
|
70
73
|
import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
|
|
71
74
|
import { botAttachmentsDir, dmSessionScope, isDmSessionScope } from "./paths.js";
|
|
72
75
|
import { appendDmHistory, dmHistoryHasModelThread, markDmHistoryModelTurn, readDmHistory, withRecentDmHistory } from "./dm-history.js";
|
|
@@ -78,7 +81,7 @@ import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOut
|
|
|
78
81
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
79
82
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
80
83
|
import { SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
|
|
81
|
-
import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, } from "./runtime-failure.js";
|
|
84
|
+
import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, withRetrySuggestion, } from "./runtime-failure.js";
|
|
82
85
|
import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
|
|
83
86
|
import { captureHeartbeatIdentityMismatch, identityMismatchMessage, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
|
|
84
87
|
import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
|
|
@@ -183,6 +186,24 @@ export function turnEnv(base, kind, agentUid, companySlug) {
|
|
|
183
186
|
}
|
|
184
187
|
/** The core worker HQ's setup bot runs. */
|
|
185
188
|
export const SETUP_WORKER_ID = SETUP_BOT_WORKER_ID;
|
|
189
|
+
/**
|
|
190
|
+
* The executable a bot's turns will spawn, for the start-up log: an absolute
|
|
191
|
+
* path when the runtime found one (the HQ app toolchain, a system prefix, the
|
|
192
|
+
* Claude desktop app), the bare name when it relies on PATH, or "not found"
|
|
193
|
+
* when the bare name is on no PATH entry either.
|
|
194
|
+
*/
|
|
195
|
+
export function describeRuntimeBinary(runtime, env, cwd) {
|
|
196
|
+
let binary;
|
|
197
|
+
try {
|
|
198
|
+
binary = runtime.resolveBinary?.(env, cwd, process.platform) ?? runtime.binary;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
binary = runtime.binary;
|
|
202
|
+
}
|
|
203
|
+
if (path.isAbsolute(binary))
|
|
204
|
+
return binary;
|
|
205
|
+
return onPath(binary, env) ? `${binary} (on PATH)` : `${binary} (not found on PATH; turns will fail as not-installed)`;
|
|
206
|
+
}
|
|
186
207
|
/** The one message a person gets when a turn on their message did not finish. */
|
|
187
208
|
export function didNotFinishText(reason, progressPosted) {
|
|
188
209
|
const r = reason.trim().replace(/[.\s]+$/, "");
|
|
@@ -194,10 +215,16 @@ export { containsNoReplySentinel };
|
|
|
194
215
|
export function isReasoningOnlyError(excerpt) {
|
|
195
216
|
return excerpt.includes("reasoning_only");
|
|
196
217
|
}
|
|
197
|
-
/**
|
|
198
|
-
|
|
218
|
+
/**
|
|
219
|
+
* The one note a person gets when the bot pauses after repeated failures. With
|
|
220
|
+
* `reason` (the plain failure text of the last turn) it is the summary that
|
|
221
|
+
* replaces the fifth per-turn line: one message for the run of failures, with
|
|
222
|
+
* what went wrong and the one next step.
|
|
223
|
+
*/
|
|
224
|
+
export function holdNoticeText(holdMs, messagePending, reason) {
|
|
199
225
|
const minutes = Math.max(1, Math.round(holdMs / 60_000));
|
|
200
|
-
|
|
226
|
+
const base = `I'm pausing for ${minutes} minute${minutes === 1 ? "" : "s"} after ${reason ? `${FAILURE_LIMIT} failed tries` : "a few failed tries"}, then I'll ${messagePending ? "pick your message up" : "pick up from your next message"}.`;
|
|
227
|
+
return reason ? `${base} ${reason}` : base;
|
|
201
228
|
}
|
|
202
229
|
/** Owner stop, `hq bot stop` or `hq bot restart`: all arrive as the same signal. */
|
|
203
230
|
export const STOPPED_BY_OWNER_REASON = "I was stopped before I was done";
|
|
@@ -459,6 +486,7 @@ export async function runBot(deps) {
|
|
|
459
486
|
// A restart is the owner's "try again" after signing in: start clean.
|
|
460
487
|
setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null, cliVersion });
|
|
461
488
|
log("info", `starting ${config.name} on hq-cli ${cliVersion} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
|
|
489
|
+
log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, config.companySlug), turnCwd)}`);
|
|
462
490
|
deps.cliUpdate?.startup(log);
|
|
463
491
|
try {
|
|
464
492
|
const record = await api.getAgent(config.agentUid);
|
|
@@ -479,8 +507,19 @@ export async function runBot(deps) {
|
|
|
479
507
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
480
508
|
}
|
|
481
509
|
// ── System prompt ──────────────────────────────────────────────────────────
|
|
510
|
+
// A pack worker's bot gets the pack's "Pack setup" section (and the HQ setup
|
|
511
|
+
// takeover when the pack asks for it and setup is unfinished).
|
|
512
|
+
// Resolved even under a prompt override: the instant pack setup reads it too.
|
|
513
|
+
let packSetup = null;
|
|
514
|
+
try {
|
|
515
|
+
packSetup = resolvePackSetupPrompt({ hqRoot: config.hqRoot, workerDir: config.workerDir, workerSource: config.workerSource, log });
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
log("warn", `pack setup section skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
519
|
+
}
|
|
482
520
|
const systemPrompt = deps.systemPromptOverride ??
|
|
483
521
|
buildSystemPrompt({
|
|
522
|
+
packSetup,
|
|
484
523
|
botName: config.name,
|
|
485
524
|
...(config.displayName ? { displayName: config.displayName } : {}),
|
|
486
525
|
agentUid: config.agentUid,
|
|
@@ -502,6 +541,16 @@ export async function runBot(deps) {
|
|
|
502
541
|
const systemPromptFile = path.join(dir, "system-prompt.md");
|
|
503
542
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
504
543
|
fs.writeFileSync(systemPromptFile, systemPrompt, { mode: 0o600 });
|
|
544
|
+
// A lean bot's Claude Code turns load only HQ's tool hooks (lean-settings.ts).
|
|
545
|
+
let leanSettingsFile = null;
|
|
546
|
+
if (config.lean && config.runtime === "claude") {
|
|
547
|
+
try {
|
|
548
|
+
leanSettingsFile = writeLeanClaudeSettings(dir, config.hqRoot);
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
log("warn", `lean settings not written; turns load the full settings: ${err instanceof Error ? err.message : String(err)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
505
554
|
if (hasPromotionHold(dir)) {
|
|
506
555
|
await finish("stopped", 0, "local bot held for cloud promotion");
|
|
507
556
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
@@ -580,7 +629,7 @@ export async function runBot(deps) {
|
|
|
580
629
|
*/
|
|
581
630
|
const modelHoldMs = Math.max(0, deps.modelHoldMs ?? FAILURE_WINDOW_MS);
|
|
582
631
|
let modelHoldUntil = 0;
|
|
583
|
-
const holdAfterFailures = async (excerpt, tell, messagePending = false) => {
|
|
632
|
+
const holdAfterFailures = async (excerpt, tell, messagePending = false, cls) => {
|
|
584
633
|
const until = now().getTime() + modelHoldMs;
|
|
585
634
|
modelHoldUntil = until;
|
|
586
635
|
failures.clear();
|
|
@@ -592,7 +641,7 @@ export async function runBot(deps) {
|
|
|
592
641
|
// nothing while their next messages wait out the hold.
|
|
593
642
|
if (tell) {
|
|
594
643
|
try {
|
|
595
|
-
await tell(holdNoticeText(modelHoldMs, messagePending));
|
|
644
|
+
await tell(holdNoticeText(modelHoldMs, messagePending, cls ? runtimeFailureText(cls, runtime.id) : undefined));
|
|
596
645
|
}
|
|
597
646
|
catch (err) {
|
|
598
647
|
log("warn", `could not say that turns are on hold: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -863,6 +912,7 @@ export async function runBot(deps) {
|
|
|
863
912
|
sessionId: session?.sessionId,
|
|
864
913
|
...(live.model ? { model: live.model } : {}),
|
|
865
914
|
...(live.effort ? { effort: live.effort } : {}),
|
|
915
|
+
...(leanSettingsFile ? { leanSettingsFile } : {}),
|
|
866
916
|
...bind,
|
|
867
917
|
...stream,
|
|
868
918
|
});
|
|
@@ -1046,6 +1096,12 @@ export async function runBot(deps) {
|
|
|
1046
1096
|
* A server that refuses the block (400) still gets the plain body.
|
|
1047
1097
|
*/
|
|
1048
1098
|
const postRepair = async (target, notice, rootEventId) => {
|
|
1099
|
+
if (target.kind === "dm" && !notice.richContent) {
|
|
1100
|
+
// No repair card for this class: a tap-to-answer Retry so the failure is
|
|
1101
|
+
// never a dead end in the person's DM (rooms get the plain line).
|
|
1102
|
+
await api.sendDm({ toPersonUid: target.peerUid, body: withRetrySuggestion(notice.text), audience: "human", ...(rootEventId ? { rootEventId } : {}) });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1049
1105
|
if (target.kind === "dm" && notice.richContent) {
|
|
1050
1106
|
const base = { toPersonUid: target.peerUid, body: notice.text, audience: "human", ...(rootEventId ? { rootEventId } : {}) };
|
|
1051
1107
|
try {
|
|
@@ -1400,12 +1456,56 @@ export async function runBot(deps) {
|
|
|
1400
1456
|
recordToolOffer(tool);
|
|
1401
1457
|
return true;
|
|
1402
1458
|
};
|
|
1459
|
+
// ── Instant first question (pack-setup-driver.ts) ─────────────────────────
|
|
1460
|
+
/** The one company this pack is set up in: the owner's only company with a folder here. */
|
|
1461
|
+
const resolvePackFirm = async () => {
|
|
1462
|
+
if (!deps.ownerContext)
|
|
1463
|
+
return null;
|
|
1464
|
+
const owner = await deps.ownerContext();
|
|
1465
|
+
if (owner.status !== "ok")
|
|
1466
|
+
return null;
|
|
1467
|
+
const local = owner.companies
|
|
1468
|
+
.map((c) => (c.companySlug ?? "").trim())
|
|
1469
|
+
.filter((slug, i, all) => slug && all.indexOf(slug) === i && fs.existsSync(path.join(config.hqRoot, "companies", slug)));
|
|
1470
|
+
return local.length === 1 ? local[0] : null;
|
|
1471
|
+
};
|
|
1472
|
+
/**
|
|
1473
|
+
* First start of a pack bot: send the first setup question from code. False
|
|
1474
|
+
* (run the kickoff model turn) when there is no pack setup, it already
|
|
1475
|
+
* started, or the owner has no single company here to set the pack up in.
|
|
1476
|
+
*/
|
|
1477
|
+
const startPackSetup = async () => {
|
|
1478
|
+
if (!packSetup || readPackSetupState(dir))
|
|
1479
|
+
return false;
|
|
1480
|
+
try {
|
|
1481
|
+
const firm = await resolvePackFirm();
|
|
1482
|
+
if (!firm) {
|
|
1483
|
+
log("info", "pack setup: no single company on this computer to set up in; the kickoff turn handles it");
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
const q = packSetup.setup.questions[0];
|
|
1487
|
+
const firmName = companyDisplayName(config.hqRoot, firm);
|
|
1488
|
+
await sendToTarget({ kind: "dm", peerUid: config.ownerUid }, questionMessage(q, questionDefault(q, { firmName })), undefined, "human");
|
|
1489
|
+
writePackSetupState(dir, { v: 1, pack: packSetup.packName, firm, askedFirst: q.id, modelNotePending: true, updatedAt: now().toISOString() });
|
|
1490
|
+
log("info", `pack setup for ${firm}: asked ${q.id} from code (no kickoff turn)`);
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
catch (err) {
|
|
1494
|
+
log("warn", `first pack setup question not sent from code (${err instanceof Error ? err.message : String(err)}); running the kickoff`);
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1403
1498
|
const runKickoff = async (introEventId) => {
|
|
1404
1499
|
const prompt = (config.kickoff ?? "").trim();
|
|
1405
1500
|
if (!prompt || stopping || state !== "running")
|
|
1406
1501
|
return;
|
|
1407
1502
|
if (isSetupBot && (await offerCodingTool()))
|
|
1408
1503
|
return;
|
|
1504
|
+
if (packSetup && (await startPackSetup())) {
|
|
1505
|
+
if (readBotConfig(dir)?.kickoffPendingSignIn)
|
|
1506
|
+
patchBotConfig(dir, { kickoffPendingSignIn: false });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1409
1509
|
const peerUid = config.ownerUid;
|
|
1410
1510
|
const tReceived = now().getTime();
|
|
1411
1511
|
log("info", `kickoff turn starting (${prompt.length} chars)`);
|
|
@@ -1462,6 +1562,13 @@ export async function runBot(deps) {
|
|
|
1462
1562
|
// The person sees one plain sentence and one next step; the raw runtime
|
|
1463
1563
|
// text is in the log (recordTurnFailure) and status.json only.
|
|
1464
1564
|
const cls = failureClass(outcome);
|
|
1565
|
+
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
1566
|
+
// The failure that hit the limit: one summary carries the reason and the pause.
|
|
1567
|
+
clearInflight(dir, KICKOFF_MESSAGE_ID);
|
|
1568
|
+
log("error", `kickoff turn failed (${count}/${FAILURE_LIMIT} in window); not retried`);
|
|
1569
|
+
await holdAfterFailures(outcome.excerpt, sendToPerson, false, cls);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1465
1572
|
const notice = outcome.aborted
|
|
1466
1573
|
? { cls, text: `${didNotFinishText(failureReason(outcome), poster.postedCount > 0)} Send me any message and I'll pick up from there.` }
|
|
1467
1574
|
: runtimeRepairNotice({
|
|
@@ -1479,9 +1586,6 @@ export async function runBot(deps) {
|
|
|
1479
1586
|
/* the marker stays: the next start says it did not finish */
|
|
1480
1587
|
}
|
|
1481
1588
|
log("error", `kickoff turn ${outcome.aborted ? "stopped by the owner" : `failed (${count}/${FAILURE_LIMIT} in window)`}; not retried`);
|
|
1482
|
-
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
1483
|
-
await holdAfterFailures(outcome.excerpt, sendToPerson);
|
|
1484
|
-
}
|
|
1485
1589
|
};
|
|
1486
1590
|
/**
|
|
1487
1591
|
* One fired job: a model turn on the job's own prompt, in the job's own
|
|
@@ -1629,12 +1733,14 @@ export async function runBot(deps) {
|
|
|
1629
1733
|
// The model never ran, so nothing happened outside this Mac: retry.
|
|
1630
1734
|
clearInflight(dir, id);
|
|
1631
1735
|
const { excerpt } = recordTurnFailure(outcome.error, target.sessionScope);
|
|
1736
|
+
const cls = classifyRuntimeError(outcome.error);
|
|
1632
1737
|
if (failures.exceeded(now().getTime())) {
|
|
1633
|
-
await holdAfterFailures(excerpt, sendToPerson, true);
|
|
1738
|
+
await holdAfterFailures(excerpt, sendToPerson, true, cls);
|
|
1634
1739
|
return;
|
|
1635
1740
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1741
|
+
// A missing tool does not come back between retries: say so now.
|
|
1742
|
+
if (attempt >= MAX_ATTEMPTS_PER_MESSAGE || cls === "not-installed") {
|
|
1743
|
+
const plain = runtimeFailureText(cls, runtime.id);
|
|
1638
1744
|
await sendToPerson(`I couldn't run this one. ${plain}`).catch(() => undefined);
|
|
1639
1745
|
await completeJobRun(jobId, id, { status: "failed", output: jobRunOutput(plain), disposition: `${runtime.id} could not be started` });
|
|
1640
1746
|
markReported("failed");
|
|
@@ -1674,7 +1780,7 @@ export async function runBot(deps) {
|
|
|
1674
1780
|
attempts.delete(id);
|
|
1675
1781
|
log("error", `scheduled run ${id} of job ${jobId} ${outcome.aborted ? "stopped by the owner" : "failed"} after it started; not re-run`);
|
|
1676
1782
|
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
1677
|
-
await holdAfterFailures(outcome.excerpt, sendToPerson);
|
|
1783
|
+
await holdAfterFailures(outcome.excerpt, sendToPerson, false, failureClass(outcome));
|
|
1678
1784
|
}
|
|
1679
1785
|
};
|
|
1680
1786
|
/**
|
|
@@ -1865,14 +1971,24 @@ export async function runBot(deps) {
|
|
|
1865
1971
|
const toolOffer = isSetupBot && ownerDm ? pendingToolOffer() : null;
|
|
1866
1972
|
const toolAnswer = toolOffer ? setupToolOfferAnswer(messageText, attachmentRefs.length) : null;
|
|
1867
1973
|
const kickoffPrompt = (config.kickoff ?? "").trim();
|
|
1974
|
+
// While (or right after) the app runs the pack setup, the model is told where it stands.
|
|
1975
|
+
const packState = packSetup && ownerDm ? readPackSetupState(dir) : null;
|
|
1976
|
+
const packTurnNote = packSetup && packState?.modelNotePending ? firstQuestionNote(packSetup.setup, packState, companyDisplayName(config.hqRoot, packState.firm)) : null;
|
|
1868
1977
|
const ownerText = toolOffer && toolAnswer === "kept_here" && kickoffPrompt
|
|
1869
1978
|
? `${setupKeptHereNote(toolOffer)}\n\n${kickoffPrompt}`
|
|
1870
1979
|
: toolOffer
|
|
1871
1980
|
? `${setupOfferAnsweredInWordsNote(toolOffer)}\n\n${text}`
|
|
1872
1981
|
: setupNote
|
|
1873
1982
|
? `${SETUP_EXPLAINED_NOTE}\n\n${text}`
|
|
1874
|
-
:
|
|
1875
|
-
|
|
1983
|
+
: packTurnNote
|
|
1984
|
+
? `${packTurnNote}\n\n${text}`
|
|
1985
|
+
: text;
|
|
1986
|
+
// A note the desktop app sends the setup bot as it finishes visual setup
|
|
1987
|
+
// steps. It often lands while the bot's previous question is still open;
|
|
1988
|
+
// answering every note re-posted that question (three times in a row on
|
|
1989
|
+
// 2026-10-12). Such a turn may stay quiet.
|
|
1990
|
+
const appSetupNote = isSetupBot && ownerDm && isAppSetupNote(messageText);
|
|
1991
|
+
const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? (appSetupNote ? `${APP_SETUP_NOTE_TURN_NOTE}\n\n${ownerText}` : ownerText) :
|
|
1876
1992
|
`[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
|
|
1877
1993
|
const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
|
|
1878
1994
|
const { sendProgress, sendToPerson, saved } = trackingSend(markerTarget, item.rootEventId);
|
|
@@ -1888,7 +2004,7 @@ export async function runBot(deps) {
|
|
|
1888
2004
|
tReceived,
|
|
1889
2005
|
sendProgress,
|
|
1890
2006
|
sendToPerson,
|
|
1891
|
-
silenceable: target.kind === "room" && !target.mentioned,
|
|
2007
|
+
silenceable: (target.kind === "room" && !target.mentioned) || appSetupNote,
|
|
1892
2008
|
...(readDirs.length ? { readDirs } : {}),
|
|
1893
2009
|
...(target.kind === "dm" ? { historyEventId: item.eventId ?? id } : {}),
|
|
1894
2010
|
...(target.kind === "room"
|
|
@@ -1917,6 +2033,11 @@ export async function runBot(deps) {
|
|
|
1917
2033
|
}
|
|
1918
2034
|
if (outcome.ok && ownerDm)
|
|
1919
2035
|
noteSetupReply(outcome.reply);
|
|
2036
|
+
if (outcome.ok && packState?.modelNotePending) {
|
|
2037
|
+
const latest = readPackSetupState(dir);
|
|
2038
|
+
if (latest)
|
|
2039
|
+
writePackSetupState(dir, { ...latest, modelNotePending: false, updatedAt: now().toISOString() });
|
|
2040
|
+
}
|
|
1920
2041
|
if (outcome.ok) {
|
|
1921
2042
|
// A no_reply sentinel or intentionally empty reply: ack silently, post nothing.
|
|
1922
2043
|
if (outcome.silent) {
|
|
@@ -1966,14 +2087,17 @@ export async function runBot(deps) {
|
|
|
1966
2087
|
// outside this Mac, so retrying is safe.
|
|
1967
2088
|
clearInflight(dir, id);
|
|
1968
2089
|
const { excerpt } = recordTurnFailure(outcome.error, sessionScope);
|
|
2090
|
+
const cls = classifyRuntimeError(outcome.error);
|
|
1969
2091
|
if (failures.exceeded(now().getTime())) {
|
|
1970
|
-
await holdAfterFailures(excerpt, sendToPerson, true);
|
|
2092
|
+
await holdAfterFailures(excerpt, sendToPerson, true, cls);
|
|
1971
2093
|
return;
|
|
1972
2094
|
}
|
|
1973
|
-
|
|
2095
|
+
// A missing tool does not come back between retries: tell the person on
|
|
2096
|
+
// the first attempt (with Retry) instead of after a silent minute.
|
|
2097
|
+
if (attempt >= MAX_ATTEMPTS_PER_MESSAGE || cls === "not-installed") {
|
|
1974
2098
|
try {
|
|
1975
2099
|
await postRepair(markerTarget, runtimeRepairNotice({
|
|
1976
|
-
cls
|
|
2100
|
+
cls,
|
|
1977
2101
|
runtime: runtime.id,
|
|
1978
2102
|
botName: config.name,
|
|
1979
2103
|
context: "message",
|
|
@@ -2010,15 +2134,21 @@ export async function runBot(deps) {
|
|
|
2010
2134
|
log("info", `unmentioned room turn ${id} failed with no post (${where}); acked silently`);
|
|
2011
2135
|
// Nobody asked the bot here, so the hold note goes to the owner's DM, not the room.
|
|
2012
2136
|
if (failures.exceeded(now().getTime())) {
|
|
2013
|
-
await holdAfterFailures(outcome.excerpt, (body) => sendToTarget({ kind: "dm", peerUid: config.ownerUid }, body, undefined, "human"));
|
|
2137
|
+
await holdAfterFailures(outcome.excerpt, (body) => sendToTarget({ kind: "dm", peerUid: config.ownerUid }, body, undefined, "human"), false, failureClass(outcome));
|
|
2014
2138
|
}
|
|
2015
2139
|
return;
|
|
2016
2140
|
}
|
|
2141
|
+
const holdNow = !outcome.aborted && failures.exceeded(now().getTime());
|
|
2017
2142
|
try {
|
|
2018
2143
|
// DM or @-mentioned room reasoning_only: the person asked directly, so
|
|
2019
2144
|
// still say we did not finish — but with a plain reason, never the raw
|
|
2020
2145
|
// internal error string.
|
|
2021
|
-
if (
|
|
2146
|
+
if (holdNow) {
|
|
2147
|
+
// The failure that hit the limit: one summary (reason, next step, the
|
|
2148
|
+
// pause) instead of a per-turn line followed by a pause note.
|
|
2149
|
+
await holdAfterFailures(outcome.excerpt, (body) => sendToPerson(body), false, failureClass(outcome));
|
|
2150
|
+
}
|
|
2151
|
+
else if (reasoningOnly || outcome.aborted) {
|
|
2022
2152
|
const reason = reasoningOnly ? "the model returned no answer" : failureReason(outcome);
|
|
2023
2153
|
await sendToPerson(didNotFinishText(reason, poster.postedCount > 0));
|
|
2024
2154
|
}
|
|
@@ -2044,9 +2174,6 @@ export async function runBot(deps) {
|
|
|
2044
2174
|
clearInflight(dir, id);
|
|
2045
2175
|
attempts.delete(id);
|
|
2046
2176
|
log("error", `turn on ${id} ${outcome.aborted ? "stopped by the owner" : "failed"} after it started; told the owner, not re-run`);
|
|
2047
|
-
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
2048
|
-
await holdAfterFailures(outcome.excerpt, sendToPerson);
|
|
2049
|
-
}
|
|
2050
2177
|
};
|
|
2051
2178
|
// The kickoff runs alongside the heartbeat, before the first inbox poll, so
|
|
2052
2179
|
// an owner message sent meanwhile is answered after it in the same session.
|
|
@@ -2259,4 +2386,15 @@ export async function runBot(deps) {
|
|
|
2259
2386
|
});
|
|
2260
2387
|
return { stop, done, status: readStatus, idle: () => pending.size === 0 };
|
|
2261
2388
|
}
|
|
2389
|
+
/** The prefix the HQ desktop app puts on the setup notes it sends the setup bot. */
|
|
2390
|
+
const APP_SETUP_NOTE_PREFIX = "Setup note from the HQ desktop app:";
|
|
2391
|
+
/** True for a setup note the desktop app sent, not words the person typed. */
|
|
2392
|
+
export function isAppSetupNote(text) {
|
|
2393
|
+
return text.trimStart().startsWith(APP_SETUP_NOTE_PREFIX);
|
|
2394
|
+
}
|
|
2395
|
+
/** Put ahead of an app setup note: fold it in, and stay quiet unless it changes what to say. */
|
|
2396
|
+
export const APP_SETUP_NOTE_TURN_NOTE = "[This message is a setup note from the HQ desktop app, not words from the person. Record what it settles. " +
|
|
2397
|
+
"If you already asked the person something that is still unanswered and this note does not change it, reply with exactly " +
|
|
2398
|
+
'{"action":"no_reply"} and nothing else, so nothing is posted. Never repeat a question you already asked. ' +
|
|
2399
|
+
"Write to the person only when the note changes what they need to know or answer next, in one or two sentences.]";
|
|
2262
2400
|
//# sourceMappingURL=run.js.map
|
|
@@ -45,6 +45,31 @@ export declare function onPath(name: string, env: NodeJS.ProcessEnv): boolean;
|
|
|
45
45
|
* skipped), matching the HQ desktop app's own lookup.
|
|
46
46
|
*/
|
|
47
47
|
export declare function bundledClaudeBin(homeDir?: string): string | null;
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Where a bot looks for `claude`, in order, outside the PATH walk. The HQ
|
|
50
|
+
* desktop app's toolchain comes first: on a clean Mac it is the only copy, and
|
|
51
|
+
* it is the one the app keeps updated. Then the usual install prefixes, so a
|
|
52
|
+
* bot under launchd (no shell profile) still finds a Homebrew or npm global
|
|
53
|
+
* install even when its PATH is thin.
|
|
54
|
+
*/
|
|
55
|
+
export declare const CLAUDE_SYSTEM_BIN_DIRS: readonly ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"];
|
|
56
|
+
/**
|
|
57
|
+
* The `claude` a bot spawns. Order (contract "Claude resolution for bots"):
|
|
58
|
+
*
|
|
59
|
+
* 1. `~/Library/Application Support/Indigo HQ/toolchain/npm-global/bin/claude`
|
|
60
|
+
* (the HQ app's own install; macOS only)
|
|
61
|
+
* 2. /usr/local/bin, /opt/homebrew/bin, /usr/bin
|
|
62
|
+
* 3. PATH (returned as the bare name, so the spawn uses the same lookup)
|
|
63
|
+
* 4. the Claude desktop app's bundled Claude Code
|
|
64
|
+
* 5. the bare name, so the failure reads "claude is not installed".
|
|
65
|
+
*
|
|
66
|
+
* Absolute paths come back for 1, 2 and 4 so the bot log can say which one
|
|
67
|
+
* ran.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveClaudeBinary(env: NodeJS.ProcessEnv, homeDir?: string, platform?: NodeJS.Platform, cwd?: string, systemBinDirs?: readonly string[]): string;
|
|
70
|
+
export interface ClaudeRuntimeOptions {
|
|
71
|
+
/** Install prefixes checked after the HQ app toolchain (test seam; default CLAUDE_SYSTEM_BIN_DIRS). */
|
|
72
|
+
systemBinDirs?: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
export declare function createClaudeRuntime(env?: NodeJS.ProcessEnv, opts?: ClaudeRuntimeOptions): BotRuntime;
|
|
50
75
|
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -19,6 +19,7 @@ import * as fs from "node:fs";
|
|
|
19
19
|
import * as os from "node:os";
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import { messagesStreamEvents, messagesStreamFailure, messagesStreamResult } from "./messages-stream.js";
|
|
22
|
+
import { hqAppToolchainBin } from "../toolchain.js";
|
|
22
23
|
import { resolveRuntimeBinary } from "./windows.js";
|
|
23
24
|
export const CLAUDE_PERMISSION_MODE_ENV = "HQ_BOT_CLAUDE_PERMISSION_MODE";
|
|
24
25
|
export const DEFAULT_CLAUDE_PERMISSION_MODE = "bypassPermissions";
|
|
@@ -104,18 +105,59 @@ export function bundledClaudeBin(homeDir = os.homedir()) {
|
|
|
104
105
|
});
|
|
105
106
|
return candidates.at(-1)?.bin ?? null;
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Where a bot looks for `claude`, in order, outside the PATH walk. The HQ
|
|
110
|
+
* desktop app's toolchain comes first: on a clean Mac it is the only copy, and
|
|
111
|
+
* it is the one the app keeps updated. Then the usual install prefixes, so a
|
|
112
|
+
* bot under launchd (no shell profile) still finds a Homebrew or npm global
|
|
113
|
+
* install even when its PATH is thin.
|
|
114
|
+
*/
|
|
115
|
+
export const CLAUDE_SYSTEM_BIN_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"];
|
|
116
|
+
function executableAt(file) {
|
|
117
|
+
try {
|
|
118
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
119
|
+
return fs.statSync(file).isFile();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The `claude` a bot spawns. Order (contract "Claude resolution for bots"):
|
|
127
|
+
*
|
|
128
|
+
* 1. `~/Library/Application Support/Indigo HQ/toolchain/npm-global/bin/claude`
|
|
129
|
+
* (the HQ app's own install; macOS only)
|
|
130
|
+
* 2. /usr/local/bin, /opt/homebrew/bin, /usr/bin
|
|
131
|
+
* 3. PATH (returned as the bare name, so the spawn uses the same lookup)
|
|
132
|
+
* 4. the Claude desktop app's bundled Claude Code
|
|
133
|
+
* 5. the bare name, so the failure reads "claude is not installed".
|
|
134
|
+
*
|
|
135
|
+
* Absolute paths come back for 1, 2 and 4 so the bot log can say which one
|
|
136
|
+
* ran.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveClaudeBinary(env, homeDir, platform = process.platform, cwd = process.cwd(), systemBinDirs = CLAUDE_SYSTEM_BIN_DIRS) {
|
|
108
139
|
if (platform === "win32")
|
|
109
140
|
return resolveRuntimeBinary("claude", env, platform, fs.existsSync, cwd);
|
|
141
|
+
const home = homeDir ?? env.HOME ?? os.homedir();
|
|
142
|
+
if (platform === "darwin") {
|
|
143
|
+
const toolchain = hqAppToolchainBin("claude", home);
|
|
144
|
+
if (executableAt(toolchain))
|
|
145
|
+
return toolchain;
|
|
146
|
+
}
|
|
147
|
+
for (const dir of systemBinDirs) {
|
|
148
|
+
const candidate = path.join(dir, "claude");
|
|
149
|
+
if (executableAt(candidate))
|
|
150
|
+
return candidate;
|
|
151
|
+
}
|
|
110
152
|
if (onPath("claude", env))
|
|
111
153
|
return "claude";
|
|
112
|
-
return bundledClaudeBin(
|
|
154
|
+
return bundledClaudeBin(home) ?? "claude";
|
|
113
155
|
}
|
|
114
|
-
export function createClaudeRuntime(env = process.env) {
|
|
156
|
+
export function createClaudeRuntime(env = process.env, opts = {}) {
|
|
115
157
|
return {
|
|
116
158
|
id: "claude",
|
|
117
159
|
binary: "claude",
|
|
118
|
-
resolveBinary: (turnEnv, cwd, platform) => resolveClaudeBinary(turnEnv, undefined, platform ?? process.platform, cwd),
|
|
160
|
+
resolveBinary: (turnEnv, cwd, platform) => resolveClaudeBinary(turnEnv, undefined, platform ?? process.platform, cwd, opts.systemBinDirs ?? CLAUDE_SYSTEM_BIN_DIRS),
|
|
119
161
|
buildArgs(input, scratch) {
|
|
120
162
|
const usesWindowsStdin = input.platform === "win32";
|
|
121
163
|
const args = [
|
|
@@ -142,6 +184,9 @@ export function createClaudeRuntime(env = process.env) {
|
|
|
142
184
|
}
|
|
143
185
|
if (input.model)
|
|
144
186
|
args.push("--model", input.model);
|
|
187
|
+
if (input.leanSettingsFile) {
|
|
188
|
+
args.push("--strict-mcp-config", "--setting-sources", "", "--settings", input.leanSettingsFile);
|
|
189
|
+
}
|
|
145
190
|
if (input.sessionId)
|
|
146
191
|
args.push("--resume", input.sessionId);
|
|
147
192
|
else
|
|
@@ -33,6 +33,11 @@ export interface RuntimeTurnInput {
|
|
|
33
33
|
sessionId?: string;
|
|
34
34
|
/** Optional model override. */
|
|
35
35
|
model?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Claude only: a lean turn. The settings file holds HQ's tool hooks; the turn
|
|
38
|
+
* loads no other settings, plugins or MCP servers (BotConfig.lean).
|
|
39
|
+
*/
|
|
40
|
+
leanSettingsFile?: string;
|
|
36
41
|
/** Thinking level for the CLI (runtimes fall back to medium). */
|
|
37
42
|
effort?: string;
|
|
38
43
|
/**
|
|
@@ -89,7 +89,7 @@ export interface RuntimeRepairNotice {
|
|
|
89
89
|
cls: RuntimeFailureClass;
|
|
90
90
|
/** The plain DM body. */
|
|
91
91
|
text: string;
|
|
92
|
-
/** Absent for `not-installed` and `unknown`: those get the plain text
|
|
92
|
+
/** Absent for `not-installed` and `unknown`: those get the plain text (plus a Retry suggestion in a DM). */
|
|
93
93
|
richContent?: RuntimeRichContent;
|
|
94
94
|
}
|
|
95
95
|
export declare function runtimeRepairBlock(args: {
|
|
@@ -99,7 +99,23 @@ export declare function runtimeRepairBlock(args: {
|
|
|
99
99
|
retryOf?: string;
|
|
100
100
|
suggestedModel?: string;
|
|
101
101
|
}): RuntimeRepairBlock;
|
|
102
|
-
/** The
|
|
102
|
+
/** The tap-to-answer button under a failure that carries no repair card. */
|
|
103
|
+
export declare const RETRY_SUGGESTION = "Retry";
|
|
104
|
+
/**
|
|
105
|
+
* The `hq-block` suggestions envelope the desktop draws as buttons (the same
|
|
106
|
+
* one the setup bot uses for its questions). Tapping sends the text back as
|
|
107
|
+
* the person's next message, so a failed turn is never a dead end.
|
|
108
|
+
*/
|
|
109
|
+
export declare function suggestionsBlock(items: string[]): string;
|
|
110
|
+
/** `text` followed by a one-button Retry envelope. */
|
|
111
|
+
export declare function withRetrySuggestion(text: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* The plain body plus, for a repairable class, the repair block. Classes
|
|
114
|
+
* without a card (`not-installed`, `unknown`) get the plain text; the DM post
|
|
115
|
+
* path adds a Retry suggestion under it (see run.ts postRepair): on 2026-10-11
|
|
116
|
+
* a fresh bot whose Claude Code was missing left the person with nothing to
|
|
117
|
+
* press.
|
|
118
|
+
*/
|
|
103
119
|
export declare function runtimeRepairNotice(args: {
|
|
104
120
|
cls: RuntimeFailureClass;
|
|
105
121
|
runtime: BotRuntimeId | string;
|
|
@@ -155,7 +155,27 @@ export function runtimeRepairBlock(args) {
|
|
|
155
155
|
...(args.suggestedModel ? { suggestedModel: args.suggestedModel.slice(0, 128) } : {}),
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
|
-
/** The
|
|
158
|
+
/** The tap-to-answer button under a failure that carries no repair card. */
|
|
159
|
+
export const RETRY_SUGGESTION = "Retry";
|
|
160
|
+
/**
|
|
161
|
+
* The `hq-block` suggestions envelope the desktop draws as buttons (the same
|
|
162
|
+
* one the setup bot uses for its questions). Tapping sends the text back as
|
|
163
|
+
* the person's next message, so a failed turn is never a dead end.
|
|
164
|
+
*/
|
|
165
|
+
export function suggestionsBlock(items) {
|
|
166
|
+
return "```hq-block\n" + JSON.stringify({ v: 1, blocks: [{ kind: "suggestions", items }] }) + "\n```";
|
|
167
|
+
}
|
|
168
|
+
/** `text` followed by a one-button Retry envelope. */
|
|
169
|
+
export function withRetrySuggestion(text) {
|
|
170
|
+
return `${text}\n\n${suggestionsBlock([RETRY_SUGGESTION])}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The plain body plus, for a repairable class, the repair block. Classes
|
|
174
|
+
* without a card (`not-installed`, `unknown`) get the plain text; the DM post
|
|
175
|
+
* path adds a Retry suggestion under it (see run.ts postRepair): on 2026-10-11
|
|
176
|
+
* a fresh bot whose Claude Code was missing left the person with nothing to
|
|
177
|
+
* press.
|
|
178
|
+
*/
|
|
159
179
|
export function runtimeRepairNotice(args) {
|
|
160
180
|
const text = args.text ?? runtimeFailureText(args.cls, args.runtime, args.context);
|
|
161
181
|
if (!isRuntimeRepairClass(args.cls))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HQ desktop app's managed toolchain.
|
|
3
|
+
*
|
|
4
|
+
* The app installs node, hq-cli and @anthropic-ai/claude-code under one
|
|
5
|
+
* prefix so a clean Mac can run bots without the person installing anything:
|
|
6
|
+
*
|
|
7
|
+
* ~/Library/Application Support/Indigo HQ/toolchain/
|
|
8
|
+
* node/bin/node
|
|
9
|
+
* npm-global/bin/hq
|
|
10
|
+
* npm-global/bin/claude
|
|
11
|
+
*
|
|
12
|
+
* The bot daemon's PATH, the hq-cli update watcher and the Claude Code
|
|
13
|
+
* resolver all key on the same root, so it lives here once.
|
|
14
|
+
*/
|
|
15
|
+
/** Path segments under the home directory. macOS only; other platforms have no app toolchain. */
|
|
16
|
+
export declare const HQ_APP_TOOLCHAIN_SEGMENTS: readonly ["Library", "Application Support", "Indigo HQ", "toolchain"];
|
|
17
|
+
/** The npm prefix the app installs CLIs into, relative to the toolchain root. */
|
|
18
|
+
export declare const HQ_APP_TOOLCHAIN_NPM_GLOBAL_BIN: string;
|
|
19
|
+
/** `~/Library/Application Support/Indigo HQ/toolchain`. */
|
|
20
|
+
export declare function hqAppToolchainRoot(homeDir?: string): string;
|
|
21
|
+
/** `<toolchain>/npm-global/bin`, where `hq` and `claude` live. */
|
|
22
|
+
export declare function hqAppToolchainBinDir(homeDir?: string): string;
|
|
23
|
+
/** `<toolchain>/npm-global/bin/<name>`: where the app puts a CLI it manages. */
|
|
24
|
+
export declare function hqAppToolchainBin(name: string, homeDir?: string): string;
|
|
25
|
+
//# sourceMappingURL=toolchain.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HQ desktop app's managed toolchain.
|
|
3
|
+
*
|
|
4
|
+
* The app installs node, hq-cli and @anthropic-ai/claude-code under one
|
|
5
|
+
* prefix so a clean Mac can run bots without the person installing anything:
|
|
6
|
+
*
|
|
7
|
+
* ~/Library/Application Support/Indigo HQ/toolchain/
|
|
8
|
+
* node/bin/node
|
|
9
|
+
* npm-global/bin/hq
|
|
10
|
+
* npm-global/bin/claude
|
|
11
|
+
*
|
|
12
|
+
* The bot daemon's PATH, the hq-cli update watcher and the Claude Code
|
|
13
|
+
* resolver all key on the same root, so it lives here once.
|
|
14
|
+
*/
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
/** Path segments under the home directory. macOS only; other platforms have no app toolchain. */
|
|
18
|
+
export const HQ_APP_TOOLCHAIN_SEGMENTS = ["Library", "Application Support", "Indigo HQ", "toolchain"];
|
|
19
|
+
/** The npm prefix the app installs CLIs into, relative to the toolchain root. */
|
|
20
|
+
export const HQ_APP_TOOLCHAIN_NPM_GLOBAL_BIN = path.join("npm-global", "bin");
|
|
21
|
+
/** `~/Library/Application Support/Indigo HQ/toolchain`. */
|
|
22
|
+
export function hqAppToolchainRoot(homeDir = os.homedir()) {
|
|
23
|
+
return path.join(homeDir, ...HQ_APP_TOOLCHAIN_SEGMENTS);
|
|
24
|
+
}
|
|
25
|
+
/** `<toolchain>/npm-global/bin`, where `hq` and `claude` live. */
|
|
26
|
+
export function hqAppToolchainBinDir(homeDir = os.homedir()) {
|
|
27
|
+
return path.join(hqAppToolchainRoot(homeDir), HQ_APP_TOOLCHAIN_NPM_GLOBAL_BIN);
|
|
28
|
+
}
|
|
29
|
+
/** `<toolchain>/npm-global/bin/<name>`: where the app puts a CLI it manages. */
|
|
30
|
+
export function hqAppToolchainBin(name, homeDir = os.homedir()) {
|
|
31
|
+
return path.join(hqAppToolchainBinDir(homeDir), name);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=toolchain.js.map
|