@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +97 -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 +19 -7
  62. package/dist/lib/doctor/json-output.js +16 -6
  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
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The auto-generated "Pack setup" section of a pack bot's system prompt
3
+ * (build contract section 3). Rendered from the pack's validated `setup:`
4
+ * block: the questions in order with their options, the one-at-a-time rule
5
+ * with the `hq-block` suggestions envelope (contract section 2), the exact
6
+ * command to run with the collected answers, and the done message.
7
+ */
8
+ import * as path from "node:path";
9
+ export const SKIP_SUGGESTION = "Skip";
10
+ export const HQ_BLOCK_FENCE = "hq-block";
11
+ /** The fenced JSON envelope the app draws as buttons (contract section 2). */
12
+ export function suggestionsEnvelope(items) {
13
+ const json = JSON.stringify({ v: 1, blocks: [{ kind: "suggestions", items }] });
14
+ return `\`\`\`${HQ_BLOCK_FENCE}\n${json}\n\`\`\``;
15
+ }
16
+ /** The buttons for one question: its options, plus Skip when it is skippable. */
17
+ export function questionSuggestions(q) {
18
+ const items = [...q.options];
19
+ if (q.skippable && !items.includes(SKIP_SUGGESTION))
20
+ items.push(SKIP_SUGGESTION);
21
+ return items;
22
+ }
23
+ function shellQuote(value) {
24
+ return `'${value.replace(/'/g, `'\\''`)}'`;
25
+ }
26
+ /** `bash <packRoot>/<script> --firm <company slug> --flag '<answer>' ...` */
27
+ export function renderRunCommand(input) {
28
+ const { setup, packRoot } = input;
29
+ const flags = setup.questions
30
+ .filter((q) => setup.run.args[q.id] !== undefined)
31
+ .map((q) => `${setup.run.args[q.id]} ${shellQuote(`<${q.id}>`)}`);
32
+ return ["bash", path.join(packRoot, setup.run.script), "--firm", "<company slug>", ...flags].join(" ");
33
+ }
34
+ /** The `then` commands, each run from the pack root with the same --firm. */
35
+ export function renderThenCommands(input) {
36
+ return input.setup.run.then.map((entry) => {
37
+ const [first, ...rest] = entry.trim().split(/\s+/);
38
+ return ["bash", path.join(input.packRoot, first), "--firm", "<company slug>", ...rest].join(" ");
39
+ });
40
+ }
41
+ /** How to run a pack tool script: node for .mjs/.js, bash otherwise. */
42
+ function toolCommand(packRoot, tool, args) {
43
+ const runner = /\.(m?js|cjs)$/.test(tool) ? "node" : "bash";
44
+ return `${runner} ${path.join(packRoot, tool)} ${args}`.trim();
45
+ }
46
+ /** One tool option, rendered with exactly how it connects and how it is read. */
47
+ function renderSourceOption(packRoot, o) {
48
+ if (o.available === false)
49
+ return ` - ${o.name}: no direct connection yet${o.note ? ` (${o.note})` : ""}. Offer a workaround if one helps; otherwise say so once, plainly, and move on.`;
50
+ if (o.tool) {
51
+ return (` - ${o.name}: connect with \`${toolCommand(packRoot, o.tool, o.connect ?? "connect")}\`, check with \`${toolCommand(packRoot, o.tool, o.status ?? "status")}\`` +
52
+ (o.usage ? `, read with \`${toolCommand(packRoot, o.tool, o.usage)}\`` : "") +
53
+ ` (read-only).`);
54
+ }
55
+ return (` - ${o.name}: connect with \`hq integrations connect ${o.domain} --company <company slug> --link-only\`, read with \`hq integrations tools\` / \`hq integrations call\` (read-only tools only)` +
56
+ (o.note ? `; ${o.note}` : "") +
57
+ `.`);
58
+ }
59
+ /**
60
+ * "Their tools": what the bot needs to know to learn from the person's tools
61
+ * before asking. Facts (how each tool connects and is read) and principles;
62
+ * the bot decides how to run the conversation.
63
+ */
64
+ export function renderLearnFirst(setup, packRoot) {
65
+ const sources = setup.sources ?? [];
66
+ const lines = [
67
+ `### Their tools`,
68
+ ``,
69
+ `Start here, before asking about the topics below: most of them should come from their tools, not from questions. \`hq integrations list --usable --json --company <company slug>\` shows what this company already has connected. ` +
70
+ `Find out which tools they use in whatever way fits the conversation, connect the ones that help, and read them (with a yes first) to fill in as much of the setup as you can; then ask only what is still missing. ` +
71
+ `Talk about tools by what they are for until the person has named one.`,
72
+ ];
73
+ if (sources.length) {
74
+ lines.push(``, `What each kind of tool is good for, and the routes known to work for common tools. These are examples, not a limit: any tool they use is worth trying. Never tell the person a tool cannot be connected before you have tried the routes below:`, ...sources.flatMap((s) => [`- ${s.kind} (to read ${s.read}; helps with ${s.informs.join(", ")}):`, ...s.options.map((o) => renderSourceOption(path.resolve(packRoot), o))]), ``, `Any other tool: work out how to connect it yourself, trying these in order and stopping at the first that works. ` +
75
+ `(1) HQ's catalog: \`hq integrations catalog <name> --company <company slug>\`, then \`hq integrations connect <domain> --company <company slug> --link-only\` for a sign-in link. ` +
76
+ `(2) An access key: if the sign-in needs a key, or the tool only offers API keys or tokens, look up where the tool issues one, tell them the exact steps, and send \`hq secrets --company <company slug> generate-link <TOOL>_API_KEY --expires 1h\` so they paste it into HQ, never into the chat. Then connect with \`hq secrets --company <company slug> get --reveal <TOOL>_API_KEY | hq integrations connect <domain> --company <company slug> --token-stdin\`, or call the tool's own read API with the key through \`hq secrets exec --company <company slug> --only <TOOL>_API_KEY -- <command>\`, never printing it. ` +
77
+ `(3) A docs page or MCP server: \`hq integrations discover <docs url>\`, or \`hq integrations connect --mcp-url <url> --company <company slug> --link-only\`. ` +
78
+ `(4) A workaround: an export (CSV, PDF) or a few shared files dropped into the chat, a public page you can read, or the same data in a tool they already connected. ` +
79
+ `If none works, say so once and move on; the setup does not depend on any one tool.`, ``, `Mechanics: a connect command prints JSON. A \`url\` (or \`authorizationUrl\`) is a link to send them; \`started: true\` means a sign-in page already opened in their browser. ` +
80
+ `Never run \`hq integrations connect\` without \`--link-only\`: it waits for a sign-in the person has not been sent. ` +
81
+ `After they say they are done, confirm with the tool's check command (or \`hq integrations list\`).`);
82
+ }
83
+ lines.push(``, `Reading: ask before you read, read only what you said you would, and use read-only commands; never send, post or change anything. ` +
84
+ `Propose what you found with where it came from and let them correct it, rather than asking questions the data already answers. ` +
85
+ `Describe what you found in your own words; never paste private messages back.`);
86
+ return lines.join("\n");
87
+ }
88
+ function renderQuestion(q) {
89
+ const lines = [`- \`${q.id}\`: ${q.goal ?? q.text}`];
90
+ if (q.goal)
91
+ lines.push(` The pack phrases it: "${q.text}"`);
92
+ if (q.options.length)
93
+ lines.push(` Typical answers (to help you understand, not to show as buttons): ${q.options.join(", ")}`);
94
+ if (q.default_from)
95
+ lines.push(` Start from the value of \`${q.default_from}\` and let them confirm or change it.`);
96
+ lines.push(q.skippable ? ` Optional: if they would rather not say, pass \`skipped\`.` : ` Required.`);
97
+ return lines.join("\n");
98
+ }
99
+ /** The whole "## Pack setup" section: a brief, not a script. */
100
+ export function renderPackSetupSection(input) {
101
+ const { setup, packName, packRoot } = input;
102
+ const parts = [];
103
+ parts.push(`## Pack setup (${packName})\n\n` +
104
+ `You are the setup bot of the \`${packName}\` pack, installed at \`${packRoot}\`. Set the pack up for this person from this chat. ` +
105
+ `This section is a brief, not a script: it says what you need to learn, what you can use, and what done looks like. How you run the conversation is your call. Adapt to the person: follow their lead, skip what you already know, ask follow-ups when an answer is vague, and keep messages short. ` +
106
+ `You run every command yourself; never ask the person to open a terminal or Claude Code. ` +
107
+ `Everything you write in a turn is posted to the person, including text between commands, so never narrate your steps or write notes to yourself. ` +
108
+ `Keep your progress in \`pack-setup-${packName}.md\` in your memory folder so a later turn carries on instead of starting over.`);
109
+ parts.push(renderLearnFirst(setup, packRoot));
110
+ parts.push(`### What you need to learn\n\n${setup.questions.map(renderQuestion).join("\n")}`);
111
+ if (setup.documents?.length) {
112
+ parts.push(`### Documents\n\nThese are worth asking for if their tools do not have them: ${setup.documents.join(", ")}. They can drop files into the chat; read what arrives and use it.`);
113
+ }
114
+ parts.push(`### Buttons\n\n` +
115
+ `You can offer tap-to-answer buttons by ending a message with an \`${HQ_BLOCK_FENCE}\` fenced JSON envelope, for example ${suggestionsEnvelope(["Done", "Skip"])} The app draws each item as a button and sends its text back as the person's reply. ` +
116
+ `Use them only when there is a real, short set of choices: at most four, all of the same kind (never mix answers with actions like connecting a tool). Leave them off questions answered by typing, and never put a guessed or example answer in one. Typed replies always work too.`);
117
+ const thenCommands = renderThenCommands(input);
118
+ parts.push(`### Setting it up\n\n` +
119
+ `When you know enough, run this from the HQ folder, filling each \`<question id>\` with what you learned (\`skipped\` when they skipped) and \`<company slug>\` with their company's folder under \`companies/\`:\n\n` +
120
+ `\`\`\`bash\n${renderRunCommand(input)}\n\`\`\`` +
121
+ (thenCommands.length ? `\n\nThen, with the same \`--firm\`:\n\n\`\`\`bash\n${thenCommands.join("\n")}\n\`\`\`` : "") +
122
+ `\n\nIf a command fails, say plainly what did not work and offer to try again; never paste a stack trace.`);
123
+ parts.push(`### When it is done\n\n` +
124
+ `Tell them what you set up: what you created and where to find it in Files, what you connected, and anything left undone. The pack's own summary of the moment is: "${setup.done.message}"` +
125
+ (setup.done.suggestion ? ` A natural next step to offer: "${setup.done.suggestion}".` : ""));
126
+ return parts.join("\n\n");
127
+ }
128
+ //# sourceMappingURL=setup-prompt.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Takeover of HQ's generic setup by a pack bot (build contract section 3).
3
+ *
4
+ * When a pack's `setup.takeover_setup` is true and HQ's setup bot has not
5
+ * recorded that setup is finished, the pack bot's system prompt also carries
6
+ * the setup worker's skills and a note to finish HQ setup first, then run the
7
+ * pack setup. "Finished" follows the setup worker's own rule: its progress
8
+ * note (`setup-progress.md` in the setup bot's memory folder) says
9
+ * `Status: finished`. No note, or any other status, means not finished.
10
+ *
11
+ * Where the note lives: the setup bot's config at `~/.hq/bots/setup/bot.json`
12
+ * names its memory folder (`personal/workers/setup/memory` under the HQ root
13
+ * for synced memory, or `~/.hq/bots/setup/memory` for local memory).
14
+ */
15
+ import type { WorkerSkillSource } from "../bot/prompt.js";
16
+ export declare const HQ_SETUP_BOT_NAME = "setup";
17
+ export declare const SETUP_PROGRESS_NOTE = "setup-progress.md";
18
+ /** Same rule as the setup worker: the note says `Status: finished`. */
19
+ export declare function progressNoteSaysFinished(note: string | null | undefined): boolean;
20
+ /**
21
+ * The HQ setup bot's progress note on this computer, or null when the setup
22
+ * bot was never created here or has not written one.
23
+ */
24
+ export declare function readHqSetupProgressNote(opts?: {
25
+ botsRoot?: string;
26
+ hqRootFallback?: string;
27
+ }): {
28
+ path: string | null;
29
+ note: string | null;
30
+ };
31
+ /**
32
+ * Whether the pack bot takes over HQ setup: the pack asked for it and the
33
+ * progress note does not say setup is finished.
34
+ */
35
+ export declare function decideTakeover(input: {
36
+ takeoverSetup: boolean;
37
+ progressNote: string | null | undefined;
38
+ }): boolean;
39
+ export interface TakeoverPromptInput {
40
+ packName: string;
41
+ /** hqRoot-relative folder of HQ's setup worker. */
42
+ setupWorkerDir: string;
43
+ /** The setup worker's skills (name, description, hqRoot-relative file). */
44
+ skills: readonly WorkerSkillSource[];
45
+ /** Where HQ's setup bot keeps (or would keep) its progress note; null when unknown. */
46
+ progressNotePath: string | null;
47
+ }
48
+ /** The "## Finish HQ setup first" section, placed before the pack setup section. */
49
+ export declare function renderTakeoverSection(input: TakeoverPromptInput): string;
50
+ //# sourceMappingURL=setup-takeover.d.ts.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Takeover of HQ's generic setup by a pack bot (build contract section 3).
3
+ *
4
+ * When a pack's `setup.takeover_setup` is true and HQ's setup bot has not
5
+ * recorded that setup is finished, the pack bot's system prompt also carries
6
+ * the setup worker's skills and a note to finish HQ setup first, then run the
7
+ * pack setup. "Finished" follows the setup worker's own rule: its progress
8
+ * note (`setup-progress.md` in the setup bot's memory folder) says
9
+ * `Status: finished`. No note, or any other status, means not finished.
10
+ *
11
+ * Where the note lives: the setup bot's config at `~/.hq/bots/setup/bot.json`
12
+ * names its memory folder (`personal/workers/setup/memory` under the HQ root
13
+ * for synced memory, or `~/.hq/bots/setup/memory` for local memory).
14
+ */
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { readBotConfig, resolveBotMemoryDir } from "../bot/config.js";
18
+ import { botDir } from "../bot/paths.js";
19
+ export const HQ_SETUP_BOT_NAME = "setup";
20
+ export const SETUP_PROGRESS_NOTE = "setup-progress.md";
21
+ /** Same rule as the setup worker: the note says `Status: finished`. */
22
+ export function progressNoteSaysFinished(note) {
23
+ if (typeof note !== "string")
24
+ return false;
25
+ return /^Status:[ \t]*finished/im.test(note);
26
+ }
27
+ /**
28
+ * The HQ setup bot's progress note on this computer, or null when the setup
29
+ * bot was never created here or has not written one.
30
+ */
31
+ export function readHqSetupProgressNote(opts = {}) {
32
+ const config = readBotConfig(botDir(HQ_SETUP_BOT_NAME, opts.botsRoot));
33
+ if (!config)
34
+ return { path: null, note: null };
35
+ const notePath = path.join(resolveBotMemoryDir(config), SETUP_PROGRESS_NOTE);
36
+ return { path: notePath, note: readNote(notePath) };
37
+ }
38
+ function readNote(p) {
39
+ try {
40
+ return fs.readFileSync(p, "utf8");
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Whether the pack bot takes over HQ setup: the pack asked for it and the
48
+ * progress note does not say setup is finished.
49
+ */
50
+ export function decideTakeover(input) {
51
+ if (!input.takeoverSetup)
52
+ return false;
53
+ return !progressNoteSaysFinished(input.progressNote);
54
+ }
55
+ /** The "## Finish HQ setup first" section, placed before the pack setup section. */
56
+ export function renderTakeoverSection(input) {
57
+ const skillLines = input.skills.map((s) => {
58
+ const desc = s.description ? ` — ${s.description}` : "";
59
+ const file = s.file ? ` — ${s.file}` : " — (no skill file on disk)";
60
+ return `- ${s.name}${desc}${file}`;
61
+ });
62
+ return (`## Finish HQ setup first\n\n` +
63
+ `HQ's own setup bot has not recorded that this HQ is set up` +
64
+ (input.progressNotePath ? ` (its progress note at \`${input.progressNotePath}\` does not say \`Status: finished\`)` : " (it has no progress note on this computer)") +
65
+ `. Before the \`${input.packName}\` pack setup, finish the generic HQ setup yourself, using HQ's setup worker skills below: ` +
66
+ `read \`${path.join(input.setupWorkerDir, "worker.yaml")}\` for the walkthrough and its progress-note rules, do only the steps that are not done, and ` +
67
+ (input.progressNotePath
68
+ ? `keep HQ's setup progress note at \`${input.progressNotePath}\` up to date as you go (not a copy in your own memory folder), ending with \`Status: finished\`, so HQ's setup bot and later packs see that setup is done. `
69
+ : `write the same \`setup-progress.md\` note in your memory folder as you go, ending with \`Status: finished\`. `) +
70
+ `Skip any step the HQ plainly shows is done (signed in, a company they belong to). Then carry on with the Pack setup section. ` +
71
+ `Tell the person in one line that you are finishing HQ setup first, and ask one question at a time, as the Pack setup section says.\n\n` +
72
+ `### HQ setup worker skills (${path.join(input.setupWorkerDir, "skills")}/)\n\n` +
73
+ (skillLines.length ? skillLines.join("\n") : "- (none found on disk)"));
74
+ }
75
+ //# sourceMappingURL=setup-takeover.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `hq packages install <source> --start-bot` (sample-pack-in-app-setup US-003,
3
+ * build contract section 3).
4
+ *
5
+ * After a pack with a valid `setup:` block is wired, create the personal bot
6
+ * `setup.bot.name` running the pack worker of the same id, with the display
7
+ * name and intro from the block, through the same code path as
8
+ * `hq bot create` (never a child `hq` process), and start it the way
9
+ * `hq bot create` does (launchd when installed). A bot of that name that
10
+ * already exists is restarted and re-sends its intro. If the bot cannot start,
11
+ * the install still succeeds: the reason is printed and the owner gets one
12
+ * plain DM with one next step. On success the install prints `bot: <name>`,
13
+ * which the desktop app reads to open the right DM.
14
+ */
15
+ import type { PackSetup } from "../../types.js";
16
+ export interface StartPackBotDeps {
17
+ /** A bot of this name exists on this computer. */
18
+ botExists: (name: string) => boolean;
19
+ /** `hq bot create <name> --worker <id> --display-name ... --intro ... --kickoff ...` */
20
+ createBot: (name: string, opts: {
21
+ worker: string;
22
+ displayName: string;
23
+ intro: string;
24
+ kickoff: string;
25
+ }) => Promise<void>;
26
+ /** `hq bot restart <name>` */
27
+ restartBot: (name: string) => Promise<void>;
28
+ /** `hq bot intro <name>` (re-send the hello) */
29
+ sendIntro: (name: string) => Promise<void>;
30
+ /** One DM to the owner, best effort; failures are reported, never thrown. */
31
+ dmOwner: (name: string, body: string) => Promise<void>;
32
+ /** Human output (stdout). */
33
+ say: (line: string) => void;
34
+ }
35
+ export interface StartPackBotResult {
36
+ ok: boolean;
37
+ bot: string;
38
+ /** "created" | "restarted" */
39
+ action: "created" | "restarted";
40
+ reason?: string;
41
+ }
42
+ /** The kickoff turn that makes the bot begin the pack setup as soon as it is online. */
43
+ export declare function packSetupKickoff(packName: string, setup: PackSetup): string;
44
+ /** One plain sentence plus one next step, for the owner's DM when the start failed. */
45
+ export declare function startFailureDm(setup: PackSetup, reason: string): string;
46
+ export declare function startPackSetupBot(packName: string, setup: PackSetup, deps: StartPackBotDeps): Promise<StartPackBotResult>;
47
+ //# sourceMappingURL=start-bot.d.ts.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `hq packages install <source> --start-bot` (sample-pack-in-app-setup US-003,
3
+ * build contract section 3).
4
+ *
5
+ * After a pack with a valid `setup:` block is wired, create the personal bot
6
+ * `setup.bot.name` running the pack worker of the same id, with the display
7
+ * name and intro from the block, through the same code path as
8
+ * `hq bot create` (never a child `hq` process), and start it the way
9
+ * `hq bot create` does (launchd when installed). A bot of that name that
10
+ * already exists is restarted and re-sends its intro. If the bot cannot start,
11
+ * the install still succeeds: the reason is printed and the owner gets one
12
+ * plain DM with one next step. On success the install prints `bot: <name>`,
13
+ * which the desktop app reads to open the right DM.
14
+ */
15
+ /** The kickoff turn that makes the bot begin the pack setup as soon as it is online. */
16
+ export function packSetupKickoff(packName, setup) {
17
+ return (`Kickoff: I just added the ${packName} pack and you are its setup bot. Begin now. ` +
18
+ `Follow the Pack setup section of your instructions: if a "Finish HQ setup first" section is present, start there; ` +
19
+ `otherwise ask the first setup question (\`${setup.questions[0].id}\`) with its suggestions. One question per message.`);
20
+ }
21
+ /** One plain sentence plus one next step, for the owner's DM when the start failed. */
22
+ export function startFailureDm(setup, reason) {
23
+ const next = `hq bot start ${setup.bot.name}`;
24
+ return `${setup.bot.display_name} could not start (${plain(reason)}). Next step: open Terminal and run \`${next}\`, or ask your setup bot to start it.`;
25
+ }
26
+ function plain(reason) {
27
+ return reason.replace(/\s+/g, " ").trim().replace(/[.:]+$/, "");
28
+ }
29
+ function errorMessage(err) {
30
+ return err instanceof Error ? err.message : String(err);
31
+ }
32
+ export async function startPackSetupBot(packName, setup, deps) {
33
+ const name = setup.bot.name;
34
+ const exists = deps.botExists(name);
35
+ const action = exists ? "restarted" : "created";
36
+ try {
37
+ if (exists) {
38
+ deps.say(`bot ${name} already exists; restarting it and sending its intro again`);
39
+ await deps.restartBot(name);
40
+ await deps.sendIntro(name);
41
+ }
42
+ else {
43
+ await deps.createBot(name, {
44
+ worker: name,
45
+ displayName: setup.bot.display_name,
46
+ intro: setup.bot.intro,
47
+ kickoff: packSetupKickoff(packName, setup),
48
+ });
49
+ }
50
+ deps.say(`bot: ${name}`);
51
+ return { ok: true, bot: name, action };
52
+ }
53
+ catch (err) {
54
+ const reason = errorMessage(err);
55
+ deps.say(`bot ${name} could not be started: ${reason}`);
56
+ deps.say(`The pack is installed. Start the bot later with: hq bot start ${name}`);
57
+ try {
58
+ await deps.dmOwner(name, startFailureDm(setup, reason));
59
+ }
60
+ catch (dmErr) {
61
+ deps.say(`could not DM you about it: ${errorMessage(dmErr)}`);
62
+ }
63
+ return { ok: false, bot: name, action, reason };
64
+ }
65
+ }
66
+ //# sourceMappingURL=start-bot.js.map
@@ -0,0 +1,7 @@
1
+ export type SessionCompanyLock = {
2
+ sessionId: string;
3
+ companies: string[];
4
+ } | undefined;
5
+ /** Read the session's validated company scope capability or legacy metadata. */
6
+ export declare function readSessionCompanyLock(env?: NodeJS.ProcessEnv): SessionCompanyLock;
7
+ //# sourceMappingURL=session-company-lock.d.ts.map
@@ -0,0 +1,97 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { resolveSessionId } from "./core-utils/session-id.js";
4
+ import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
5
+ /** Read the session's validated company scope capability or legacy metadata. */
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
+ //# sourceMappingURL=session-company-lock.js.map
package/dist/types.d.ts CHANGED
@@ -127,6 +127,88 @@ export interface PackManifest {
127
127
  entrypoint: string;
128
128
  prompt?: string;
129
129
  };
130
+ /**
131
+ * Guided in-app setup. Optional — absent on
132
+ * legacy packs, which install unchanged. Validated by
133
+ * `lib/packages/setup-block.ts`; `hq packages install --start-bot` creates the
134
+ * bot it names and the bot's system prompt renders the questions and the run
135
+ * command from it.
136
+ */
137
+ setup?: PackSetup;
138
+ }
139
+ /** One question the pack bot asks, in order. */
140
+ export interface PackSetupQuestion {
141
+ id: string;
142
+ text: string;
143
+ /** Suggested answers shown as buttons; may be empty (free text only). */
144
+ options: string[];
145
+ /** The bot proposes this value and asks to confirm (e.g. company_display_name). */
146
+ default_from?: string;
147
+ skippable: boolean;
148
+ /** What the bot needs to learn from this question; it follows up until it has that. */
149
+ goal?: string;
150
+ }
151
+ export interface PackSetup {
152
+ bot: {
153
+ /** Bot handle; also the worker id under contributes.workers. ^[a-z0-9-]{2,32}$ */
154
+ name: string;
155
+ display_name: string;
156
+ intro: string;
157
+ };
158
+ /** When true and HQ's setup bot has not finished, the pack bot finishes HQ setup first. */
159
+ takeover_setup: boolean;
160
+ questions: PackSetupQuestion[];
161
+ run: {
162
+ /** Relative to the pack root; must exist in the pack. */
163
+ script: string;
164
+ /** question id -> flag (the bot passes --firm itself). */
165
+ args: Record<string, string>;
166
+ /** Commands run after `script`, in order; each first token exists in the pack. */
167
+ then: string[];
168
+ };
169
+ done: {
170
+ message: string;
171
+ suggestion?: string;
172
+ };
173
+ /** Documents worth asking for at the start (a rate card, a past proposal); the bot reads them and asks less. */
174
+ documents?: string[];
175
+ /**
176
+ * Kinds of tools the bot asks about, connects and reads (with the owner's
177
+ * yes) to answer questions itself before asking them: sent client emails for
178
+ * voice, CRM deals for pricing. `informs` names question ids.
179
+ */
180
+ sources?: PackSetupSource[];
181
+ }
182
+ /**
183
+ * One tool a source can be. It connects in one of two ways: through HQ
184
+ * integrations (`domain`, a sign-in link from `hq integrations connect`), or
185
+ * through a script the pack ships (`tool`, run with `connect` / `status` and
186
+ * read with `usage`). `available: false` means the pack cannot read it yet.
187
+ */
188
+ export interface PackSetupSourceOption {
189
+ name: string;
190
+ domain?: string;
191
+ /** Pack-relative script (.mjs/.js run with node, .sh with bash). */
192
+ tool?: string;
193
+ /** Arguments that start the connection (default "connect"). It prints JSON: a `url` to send, or `started: true` when it opened a sign-in page itself. */
194
+ connect?: string;
195
+ /** Arguments that report the connection (default "status"); JSON with `connected`. */
196
+ status?: string;
197
+ /** How to read with the tool, e.g. "search <terms> | recent | read <id>". */
198
+ usage?: string;
199
+ available?: boolean;
200
+ /** Shown to the bot when `available` is false, e.g. "coming in a later update". */
201
+ note?: string;
202
+ }
203
+ export interface PackSetupSource {
204
+ /** The kind of tool, as the person would say it: "email", "CRM", "team chat", "docs". */
205
+ kind: string;
206
+ /** What to read from it, in words the bot repeats when it asks: "the last 30 emails you sent to clients". */
207
+ read: string;
208
+ /** Question ids this source can answer. */
209
+ informs: string[];
210
+ /** Likely tools for this kind and how each one connects. */
211
+ options: PackSetupSourceOption[];
130
212
  }
131
213
  export {};
132
214
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.103",
3
+ "version": "5.345.105",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {