@indigoai-us/hq-cli 5.115.1 → 5.115.2

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 (32) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +10 -87
  3. package/dist/commands/bot.js +1 -1
  4. package/dist/commands/index-cmd.d.ts +8 -1
  5. package/dist/commands/index-cmd.js +15 -5
  6. package/dist/commands/onboard-identity-guard.d.ts +1 -1
  7. package/dist/commands/onboard.js +1 -1
  8. package/dist/lib/bot/prompt.d.ts +8 -1
  9. package/dist/lib/bot/prompt.js +10 -2
  10. package/dist/lib/bot/run.js +1 -1
  11. package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +4 -1
  12. package/dist/lib/core-utils/qmd-reindex-after-sync.js +11 -2
  13. package/dist/lib/onboarding/checkpoint.d.ts +13 -0
  14. package/dist/lib/onboarding/checkpoint.js +44 -0
  15. package/dist/lib/onboarding/cli/onboard.d.ts +53 -0
  16. package/dist/lib/onboarding/cli/onboard.js +135 -0
  17. package/dist/lib/onboarding/cli/prompts.d.ts +28 -0
  18. package/dist/lib/onboarding/cli/prompts.js +83 -0
  19. package/dist/lib/onboarding/errors.d.ts +33 -0
  20. package/dist/lib/onboarding/errors.js +59 -0
  21. package/dist/lib/onboarding/index.d.ts +17 -0
  22. package/dist/lib/onboarding/index.js +16 -0
  23. package/dist/lib/onboarding/orchestrator.d.ts +47 -0
  24. package/dist/lib/onboarding/orchestrator.js +569 -0
  25. package/dist/lib/onboarding/types.d.ts +79 -0
  26. package/dist/lib/onboarding/types.js +8 -0
  27. package/dist/lib/search-index/background.js +2 -200
  28. package/dist/lib/search-index/embed-lock.d.ts +38 -0
  29. package/dist/lib/search-index/embed-lock.js +286 -0
  30. package/dist/lib/search-index/index.d.ts +1 -0
  31. package/dist/lib/search-index/index.js +4 -1
  32. package/package.json +1 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.115.2] — 2026-09-15
6
+
7
+ ### Fixed
8
+
9
+ - Search indexing no longer starts a second embedding rebuild when one is
10
+ already running. Two rebuilds could use much of a machine while writing to
11
+ the same local index, so hq now skips the duplicate and lets a later pass
12
+ finish any remaining work.
13
+ - Creating a company from the command line (`hq onboard create-company`), and from a bot's conversation, works again; it had been refused with "ownerUid must match the authenticated caller". A company you already created but never finished setting up is now picked up and completed instead of being refused as "already taken".
14
+
5
15
  ## [5.115.1] — 2026-09-15
6
16
 
7
17
  ### Fixed
@@ -1,93 +1,16 @@
1
1
  #!/usr/bin/env bash
2
- # qmd-reindex-after-sync.shincremental qmd reindex after an HQ sync.
2
+ # FORWARDERthe implementation of this script lives in the hq CLI.
3
3
  #
4
- # Why this exists: the qmd index is a per-machine local SQLite store derived
5
- # from local folder paths. Nothing re-indexed it after `hq-sync` pulled new
6
- # files in, so search results diverged between teammates depending on who ran
7
- # `qmd update` most recently. This script makes post-sync indexing automatic
8
- # and deterministic, so every machine's personal index converges to the same
9
- # content. The index itself stays personal (it is large, binary, and embeds
10
- # absolute local paths); only the *freshness* is made automatic.
11
- #
12
- # Behavior:
13
- # 1. Auto-registers any company knowledge dir that isn't yet a qmd
14
- # collection (fixes the "newly-synced knowledge isn't searchable until I
15
- # manually map it" gap).
16
- # 2. Runs an incremental lexical reindex (`qmd update` — fast, skips
17
- # unchanged files by mtime).
18
- # 3. Rebuilds embeddings only when called with --embed (slow on a
19
- # multi-GB index; meant for an idle/interval pass, not every sync).
20
- #
21
- # Idempotent and safe: no-op (exit 0) when qmd is absent or the path isn't an
22
- # HQ root. Never blocks or fails a sync.
23
- #
24
- # Usage: qmd-reindex-after-sync.sh [hq_root] [--embed]
25
- # hq_root Local HQ directory (default: $PWD). Must contain core/core.yaml.
26
- # --embed Also rebuild embeddings (deferred-cost; omit for fast lexical-only).
27
-
28
- set -uo pipefail
29
-
30
- hq_root=""
31
- embed=0
32
- for arg in "$@"; do
33
- case "$arg" in
34
- --embed) embed=1 ;;
35
- --*) : ;; # ignore unknown flags
36
- *) [ -z "$hq_root" ] && hq_root="$arg" ;;
37
- esac
38
- done
39
- [ -z "$hq_root" ] && hq_root="$PWD"
40
-
41
- command -v qmd >/dev/null 2>&1 || exit 0
42
- [ -f "$hq_root/core/core.yaml" ] || exit 0
43
-
44
- cd "$hq_root" || exit 0
45
-
46
- # 1. Auto-register company knowledge collections that don't exist yet.
47
- # Convention matches /newcompany: --name <slug> --mask "**/*.md".
48
- existing="$(qmd collection list 2>/dev/null || true)"
49
- for kdir in companies/*/knowledge; do
50
- [ -d "$kdir" ] || continue
51
- [ -n "$(find "$kdir" -name '*.md' -not -name 'INDEX.md' -print -quit 2>/dev/null)" ] || continue
52
- slug="$(basename "$(dirname "$kdir")")"
53
- printf '%s\n' "$existing" | grep -Fq "qmd://$slug/" && continue
54
- qmd collection add "$hq_root/$kdir" --name "$slug" --mask "**/*.md" >/dev/null 2>&1 || true
55
- qmd context add "qmd://$slug" "Knowledge base for $slug." >/dev/null 2>&1 || true
56
- done
57
-
58
- # 1b. Auto-register company *projects* collections that don't exist yet.
59
- # Convention matches the HQ-level `hq-projects` collection: --name <slug>-projects
60
- # --mask "**/*.{md,json}" (so prd.json + project docs are searchable). Without
61
- # this, company projects/ dirs are indexed by nothing and /startwork's global
62
- # `qmd search "prd.json"` and /brainstorm's project discovery silently miss them.
63
- for pdir in companies/*/projects; do
64
- [ -d "$pdir" ] || continue
65
- [ -n "$(find "$pdir" -type f \( -name '*.md' -o -name '*.json' \) -print -quit 2>/dev/null)" ] || continue
66
- slug="$(basename "$(dirname "$pdir")")"
67
- name="${slug}-projects"
68
- printf '%s\n' "$existing" | grep -Fq "qmd://$name/" && continue
69
- qmd collection add "$hq_root/$pdir" --name "$name" --mask "**/*.{md,json}" >/dev/null 2>&1 || true
70
- qmd context add "qmd://$name" "Project PRDs and documentation for $slug." >/dev/null 2>&1 || true
71
- done
72
-
73
- # 1c. Auto-register the personal knowledge collection if it isn't one yet.
74
- # personal/knowledge is now read DIRECTLY (the reindex symlink mirror into
75
- # core/knowledge is retired), so it needs its own qmd collection to stay
76
- # searchable rather than riding in under a core collection.
77
- if [ -d "personal/knowledge" ] && \
78
- [ -n "$(find "personal/knowledge" -name '*.md' -not -name 'INDEX.md' -print -quit 2>/dev/null)" ]; then
79
- printf '%s\n' "$existing" | grep -Fq "qmd://personal-knowledge/" || {
80
- qmd collection add "$hq_root/personal/knowledge" --name "personal-knowledge" --mask "**/*.md" >/dev/null 2>&1 || true
81
- qmd context add "qmd://personal-knowledge" "Personal knowledge base (owner overlay)." >/dev/null 2>&1 || true
82
- }
83
- fi
4
+ # This shipped compatibility path must delegate rather than invoke qmd itself:
5
+ # `hq core qmd-reindex-after-sync` participates in hq-cli's shared embed lock.
6
+ # Arguments, stdio, exit status, and signal disposition are preserved.
84
7
 
85
- # 2. Incremental lexical reindex (cheap; mtime-incremental inside qmd).
86
- qmd update >/dev/null 2>&1 || true
8
+ set -euo pipefail
87
9
 
88
- # 3. Embeddings only on explicit request.
89
- if [ "$embed" -eq 1 ]; then
90
- qmd embed >/dev/null 2>&1 || true
10
+ if ! command -v hq >/dev/null 2>&1; then
11
+ echo "qmd-reindex-after-sync.sh: requires the hq CLI — this script's implementation now ships with it." >&2
12
+ echo "Install it with: npm install -g @indigoai-us/hq-cli" >&2
13
+ exit 127
91
14
  fi
92
15
 
93
- exit 0
16
+ exec hq core qmd-reindex-after-sync "$@"
@@ -800,7 +800,7 @@ async function runBotIntro(nameArg) {
800
800
  if (hasPromotionHold(dir))
801
801
  throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
802
802
  const api = new BotApi({ token: botTokenSupplier(dir) });
803
- await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(name, config.runtime, config.intro) });
803
+ await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(name, config.runtime, config.intro, { kind: effectiveBotKind(config), companies: effectiveBotCompanies(config) }) });
804
804
  patchBotConfig(dir, { introSentAt: new Date().toISOString() });
805
805
  console.log(chalk.green(`${name} introduced itself to you.`));
806
806
  }
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { type RunQmdOptions, type SearchCollection, type QmdProcessResult } from '../lib/search-index/index.js';
3
3
  import { type BackgroundDependencies, type BackgroundResult, type BackgroundStatus } from '../lib/search-index/background.js';
4
+ import { type EmbedLockDependencies } from '../lib/search-index/embed-lock.js';
4
5
  export type SearchIndexDependencies = {
5
6
  reconcileCollections: (hqRoot: string) => unknown;
6
7
  /** Apply the per-document size cap to qmd's config before the update reads anything. */
@@ -13,9 +14,15 @@ export type SearchIndexDependencies = {
13
14
  runBackgroundLauncher?: (dependencies: BackgroundDependencies) => BackgroundResult;
14
15
  runBackgroundWorker?: (dependencies: BackgroundDependencies) => BackgroundResult | Promise<BackgroundResult>;
15
16
  backgroundStatus?: (dependencies: BackgroundDependencies) => BackgroundStatus;
17
+ /** Test seams for the lock shared by every hq-cli embed entry point. */
18
+ embedLockDependencies?: EmbedLockDependencies;
19
+ writeStderr?: (text: string) => void;
20
+ };
21
+ export type SyncSearchIndexResult = {
22
+ embedded: boolean;
16
23
  };
17
24
  /** Incrementally update qmd, embedding only when an operator explicitly asks. */
18
- export declare function syncSearchIndex(hqRoot: string, embed: boolean, dependencies?: SearchIndexDependencies): void;
25
+ export declare function syncSearchIndex(hqRoot: string, embed: boolean, dependencies?: SearchIndexDependencies): SyncSearchIndexResult;
19
26
  export declare function collectionStatusLines(expected: SearchCollection[], registered: ReadonlySet<string>): string[];
20
27
  export declare function collectionSummary(expected: SearchCollection[], registered: ReadonlySet<string>): string;
21
28
  export declare function registerIndexCommand(program: Command, dependencies?: SearchIndexDependencies): void;
@@ -1,6 +1,7 @@
1
1
  import { Option } from 'commander';
2
- import { deriveCollections, listRegisteredCollections, packageLocalBin, reconcileCollections, resolveQmdBin, resolveQmdVersion, runQmd, } from '../lib/search-index/index.js';
2
+ import { deriveCollections, listRegisteredCollections, packageLocalBin, reconcileCollections, resolveQmdBin, resolveQmdHome, resolveQmdVersion, runQmd, } from '../lib/search-index/index.js';
3
3
  import { backgroundStatus, defaultBackgroundDependencies, runBackgroundLauncher, runBackgroundWorker, } from '../lib/search-index/background.js';
4
+ import { defaultEmbedLockDependencies, runWithEmbedLock, } from '../lib/search-index/embed-lock.js';
4
5
  import { applyIndexSizeLimit } from '../lib/search-index/max-doc-bytes.js';
5
6
  import { findHqRoot } from '../utils/manifest.js';
6
7
  import { QMD_NATIVE_BINDING_REMEDY, isQmdNativeBindingError, } from '../utils/qmd-native-binding-error.js';
@@ -35,8 +36,17 @@ export function syncSearchIndex(hqRoot, embed, dependencies = defaults) {
35
36
  /* capping is an optimisation; indexing is the job */
36
37
  }
37
38
  dependencies.runQmd(['update'], { cwd: hqRoot });
38
- if (embed)
39
- dependencies.runQmd(['embed'], { cwd: hqRoot });
39
+ if (!embed)
40
+ return { embedded: false };
41
+ const runEmbed = () => dependencies.runQmd(['embed'], { cwd: hqRoot });
42
+ const lockDependencies = dependencies.embedLockDependencies ?? defaultEmbedLockDependencies();
43
+ const outcome = runWithEmbedLock(resolveQmdHome(lockDependencies.env), lockDependencies, runEmbed);
44
+ const writeStderr = dependencies.writeStderr ?? ((text) => process.stderr.write(text));
45
+ if (outcome === 'busy')
46
+ writeStderr('hq: qmd embed is already running; skipping this pass.\n');
47
+ if (outcome === 'unavailable')
48
+ writeStderr('hq: cannot access the qmd embed lock; skipping this pass.\n');
49
+ return { embedded: outcome === 'ran' };
40
50
  }
41
51
  function resolveRoot(hqRoot) {
42
52
  return hqRoot ?? findHqRoot();
@@ -73,8 +83,8 @@ export function registerIndexCommand(program, dependencies = defaults) {
73
83
  .option('--hq-root <path>', 'HQ root to index (defaults to auto-detected root)')
74
84
  .action((options) => {
75
85
  const hqRoot = resolveRoot(options.hqRoot);
76
- syncSearchIndex(hqRoot, options.embed === true);
77
- console.log(`Updated search index for ${hqRoot}${options.embed ? ' (including embeddings)' : ''}.`);
86
+ const result = syncSearchIndex(hqRoot, options.embed === true, dependencies);
87
+ console.log(`Updated search index for ${hqRoot}${result.embedded ? ' (including embeddings)' : ''}.`);
78
88
  });
79
89
  index
80
90
  .command('collections')
@@ -24,7 +24,7 @@
24
24
  * the checkpoint adopted a person the caller no longer owns. The resume command
25
25
  * uses it to surface a recoverable, actionable error instead of looping.
26
26
  */
27
- import type { OnboardingCheckpoint } from "@indigoai-us/hq-onboarding";
27
+ import type { OnboardingCheckpoint } from "../lib/onboarding/index.js";
28
28
  export type OnboardingIdentityCheck = {
29
29
  kind: "ok";
30
30
  } | {
@@ -19,7 +19,7 @@
19
19
  * browser-OAuth flow opens automatically.
20
20
  */
21
21
  import chalk from "chalk";
22
- import { runOnboardCli, readCheckpoint } from "@indigoai-us/hq-onboarding";
22
+ import { runOnboardCli, readCheckpoint } from "../lib/onboarding/index.js";
23
23
  import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
24
24
  import { createDefaultVaultClient } from "./cloud-provision.js";
25
25
  import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
@@ -110,6 +110,13 @@ export declare function nonOwnerRefusalText(botName: string, kind?: BotKind): st
110
110
  /**
111
111
  * The one-time introduction the bot DMs its owner when it first comes online.
112
112
  * A bot created with `--intro` sends that text verbatim instead.
113
+ *
114
+ * The first sentence depends on the bot's kind: a personal bot runs under its
115
+ * owner's login and permissions; a company bot runs with its own identity as
116
+ * a member of its companies and can only work inside those.
113
117
  */
114
- export declare function introDmText(botName: string, runtime: string, intro?: string): string;
118
+ export declare function introDmText(botName: string, runtime: string, intro?: string, identity?: {
119
+ kind?: BotKind;
120
+ companies?: string[];
121
+ }): string;
115
122
  //# sourceMappingURL=prompt.d.ts.map
@@ -342,12 +342,20 @@ export function nonOwnerRefusalText(botName, kind = "personal") {
342
342
  /**
343
343
  * The one-time introduction the bot DMs its owner when it first comes online.
344
344
  * A bot created with `--intro` sends that text verbatim instead.
345
+ *
346
+ * The first sentence depends on the bot's kind: a personal bot runs under its
347
+ * owner's login and permissions; a company bot runs with its own identity as
348
+ * a member of its companies and can only work inside those.
345
349
  */
346
- export function introDmText(botName, runtime, intro) {
350
+ export function introDmText(botName, runtime, intro, identity = {}) {
347
351
  if (intro?.trim())
348
352
  return intro.trim();
349
353
  const runtimeLabel = runtime === "claude" ? "Claude Code" : runtime === "codex" ? "Codex" : runtime === "grok" ? "Grok" : runtime;
350
- return (`Hi, I'm ${botName} your HQ bot. I run locally on this computer using your ${runtimeLabel} login, so I can work inside your HQ with your permissions.\n\n` +
354
+ const companies = (identity.companies ?? []).filter((c) => c.trim());
355
+ const opening = (identity.kind ?? "personal") === "company"
356
+ ? `Hi, I'm ${botName} — your HQ company bot. I run locally on this computer with my own identity, as a member of ${companies.length > 0 ? companies.join(", ") : "my companies"}, so I can only work inside ${companies.length === 1 ? "that company" : "those companies"}.`
357
+ : `Hi, I'm ${botName} — your HQ bot. I run locally on this computer using your ${runtimeLabel} login, so I can work inside your HQ with your permissions.`;
358
+ return (`${opening}\n\n` +
351
359
  `You can message me here from the desktop app or your phone whenever this computer is on, and I'll reply in this thread. ` +
352
360
  `You can also open me in Claude Code or Codex from your HQ folder.\n\n` +
353
361
  `What would you like me to do first?`);
@@ -298,7 +298,7 @@ export async function runBot(deps) {
298
298
  }
299
299
  else if (!config.introSentAt) {
300
300
  try {
301
- const sent = await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(config.name, config.runtime, config.intro) });
301
+ const sent = await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(config.name, config.runtime, config.intro, { kind: effectiveBotKind(config), companies: effectiveBotCompanies(config) }) });
302
302
  patchBotConfig(dir, { introSentAt: now().toISOString() });
303
303
  log("info", "intro DM sent to owner");
304
304
  if (config.kickoff?.trim())
@@ -1,4 +1,5 @@
1
- import type { UtilityIo } from "./common.js";
1
+ import { type EmbedLockDependencies } from "../search-index/embed-lock.js";
2
+ import { type UtilityIo } from "./common.js";
2
3
  export type QmdReindexOptions = UtilityIo & {
3
4
  cwd?: string;
4
5
  /** Test seams; default to the real search-index implementations. */
@@ -11,6 +12,8 @@ export type QmdReindexOptions = UtilityIo & {
11
12
  cwd: string;
12
13
  }) => unknown;
13
14
  sizeLimit?: (hqRoot: string) => unknown;
15
+ /** Test seams for the lock shared by every hq-cli embed entry point. */
16
+ embedLockDependencies?: EmbedLockDependencies;
14
17
  };
15
18
  export declare function qmdReindexAfterSync(args?: string[], options?: QmdReindexOptions): number;
16
19
  //# sourceMappingURL=qmd-reindex-after-sync.d.ts.map
@@ -11,8 +11,10 @@
11
11
  */
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
- import { reconcileCollections, resolveQmdBin, runQmd } from "../search-index/index.js";
14
+ import { reconcileCollections, resolveQmdBin, resolveQmdHome, runQmd } from "../search-index/index.js";
15
15
  import { applyIndexSizeLimit } from "../search-index/max-doc-bytes.js";
16
+ import { defaultEmbedLockDependencies, runWithEmbedLock, } from "../search-index/embed-lock.js";
17
+ import { ioFor, line } from "./common.js";
16
18
  export function qmdReindexAfterSync(args = [], options = {}) {
17
19
  let hqRoot = "";
18
20
  let embed = false;
@@ -38,6 +40,7 @@ export function qmdReindexAfterSync(args = [], options = {}) {
38
40
  const reconcile = options.reconcile ?? ((root, opts) => reconcileCollections(root, opts));
39
41
  const run = options.run ?? ((argv, opts) => runQmd(argv, opts));
40
42
  const sizeLimit = options.sizeLimit ?? ((root) => applyIndexSizeLimit(root));
43
+ const { stderr } = ioFor(options);
41
44
  // Every qmd interaction is best-effort: the shell suffixed each with `|| true`.
42
45
  try {
43
46
  reconcile(hqRoot, { bin });
@@ -62,8 +65,14 @@ export function qmdReindexAfterSync(args = [], options = {}) {
62
65
  /* never fail a sync on index freshness */
63
66
  }
64
67
  if (embed) {
68
+ const lockDependencies = options.embedLockDependencies ?? defaultEmbedLockDependencies();
65
69
  try {
66
- run(["embed"], { bin, cwd: hqRoot });
70
+ const outcome = runWithEmbedLock(resolveQmdHome(lockDependencies.env), lockDependencies, () => { run(["embed"], { bin, cwd: hqRoot }); });
71
+ if (outcome === "busy") {
72
+ line(stderr, "hq: qmd embed is already running; skipping this pass.");
73
+ }
74
+ if (outcome === "unavailable")
75
+ line(stderr, "hq: cannot access the qmd embed lock; skipping this pass.");
67
76
  }
68
77
  catch {
69
78
  /* embeddings are deferred-cost and optional */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Onboarding checkpoint persistence (VLT-9 US-001; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Reads/writes .hq/onboarding-state.json for idempotent resume.
5
+ * If a flow fails partway, re-running picks up from the last checkpoint.
6
+ */
7
+ import type { OnboardingCheckpoint, OnboardingStep } from "./types.js";
8
+ export declare function getCheckpointPath(hqRoot: string): string;
9
+ export declare function readCheckpoint(hqRoot: string): Promise<OnboardingCheckpoint | null>;
10
+ export declare function writeCheckpoint(hqRoot: string, checkpoint: OnboardingCheckpoint): Promise<void>;
11
+ export declare function isStepComplete(checkpoint: OnboardingCheckpoint | null, step: OnboardingStep): boolean;
12
+ export declare function deleteCheckpoint(hqRoot: string): Promise<void>;
13
+ //# sourceMappingURL=checkpoint.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Onboarding checkpoint persistence (VLT-9 US-001; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Reads/writes .hq/onboarding-state.json for idempotent resume.
5
+ * If a flow fails partway, re-running picks up from the last checkpoint.
6
+ */
7
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
8
+ import { join, dirname } from "node:path";
9
+ const CHECKPOINT_FILE = "onboarding-state.json";
10
+ export function getCheckpointPath(hqRoot) {
11
+ return join(hqRoot, ".hq", CHECKPOINT_FILE);
12
+ }
13
+ export async function readCheckpoint(hqRoot) {
14
+ const path = getCheckpointPath(hqRoot);
15
+ try {
16
+ const raw = await readFile(path, "utf-8");
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export async function writeCheckpoint(hqRoot, checkpoint) {
24
+ const path = getCheckpointPath(hqRoot);
25
+ await mkdir(dirname(path), { recursive: true });
26
+ checkpoint.updatedAt = new Date().toISOString();
27
+ await writeFile(path, JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
28
+ }
29
+ export function isStepComplete(checkpoint, step) {
30
+ if (!checkpoint)
31
+ return false;
32
+ return checkpoint.completedSteps.includes(step);
33
+ }
34
+ export async function deleteCheckpoint(hqRoot) {
35
+ const { unlink } = await import("node:fs/promises");
36
+ const path = getCheckpointPath(hqRoot);
37
+ try {
38
+ await unlink(path);
39
+ }
40
+ catch {
41
+ // File may not exist — that's fine
42
+ }
43
+ }
44
+ //# sourceMappingURL=checkpoint.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * CLI onboard entry point (VLT-9 US-002; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Programmatic API for the /onboard command. The slash command calls
5
+ * these functions; they can also be consumed by integration tests.
6
+ */
7
+ import type { VaultServiceConfig } from "@indigoai-us/hq-cloud";
8
+ import { VaultClient } from "@indigoai-us/hq-cloud";
9
+ import type { OnboardingResult } from "../types.js";
10
+ export interface OnboardCliOptions {
11
+ mode: "create-company" | "join-company" | "resume" | "dry-run";
12
+ personName?: string;
13
+ personEmail?: string;
14
+ companyName?: string;
15
+ companySlug?: string;
16
+ inviteToken?: string;
17
+ vaultConfig: VaultServiceConfig;
18
+ hqRoot: string;
19
+ log?: (msg: string) => void;
20
+ }
21
+ export interface OnboardCliResult {
22
+ success: boolean;
23
+ result?: OnboardingResult;
24
+ error?: string;
25
+ }
26
+ export type CompanySlugAvailability = {
27
+ kind: "available";
28
+ }
29
+ /** The caller already owns (or is a member of) a company with this slug. */
30
+ | {
31
+ kind: "mine";
32
+ uid: string;
33
+ }
34
+ /** Another account holds the slug. */
35
+ | {
36
+ kind: "taken";
37
+ };
38
+ /**
39
+ * Decide whether `create-company` may proceed for a slug.
40
+ *
41
+ * A company that already exists in the CALLER's namespace is not "taken": it
42
+ * is theirs — typically left behind by an earlier run that created the
43
+ * entity and then died before the bucket, membership, or config were done
44
+ * (there may be no local checkpoint or company folder at all). The flow
45
+ * resumes against it instead of refusing. Only a slug held by a different
46
+ * account is refused.
47
+ */
48
+ export declare function checkCompanySlugAvailability(client: Pick<VaultClient, "entity">, slug: string): Promise<CompanySlugAvailability>;
49
+ /**
50
+ * Run the /onboard CLI flow.
51
+ */
52
+ export declare function runOnboardCli(options: OnboardCliOptions): Promise<OnboardCliResult>;
53
+ //# sourceMappingURL=onboard.d.ts.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * CLI onboard entry point (VLT-9 US-002; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Programmatic API for the /onboard command. The slash command calls
5
+ * these functions; they can also be consumed by integration tests.
6
+ */
7
+ import { VaultClient, VaultConflictError, VaultNotFoundError } from "@indigoai-us/hq-cloud";
8
+ import { createCompanyFlow, joinCompanyFlow, resumeOnboarding, } from "../orchestrator.js";
9
+ import { readCheckpoint, getCheckpointPath } from "../checkpoint.js";
10
+ import { formatProgress, formatSummary, formatError } from "./prompts.js";
11
+ const CREATE_STEPS = 6;
12
+ const JOIN_STEPS = 6;
13
+ /**
14
+ * Decide whether `create-company` may proceed for a slug.
15
+ *
16
+ * A company that already exists in the CALLER's namespace is not "taken": it
17
+ * is theirs — typically left behind by an earlier run that created the
18
+ * entity and then died before the bucket, membership, or config were done
19
+ * (there may be no local checkpoint or company folder at all). The flow
20
+ * resumes against it instead of refusing. Only a slug held by a different
21
+ * account is refused.
22
+ */
23
+ export async function checkCompanySlugAvailability(client, slug) {
24
+ const mine = await client.entity.findInMyNamespace("company", slug);
25
+ if (mine)
26
+ return { kind: "mine", uid: mine.uid };
27
+ try {
28
+ await client.entity.findBySlug("company", slug);
29
+ return { kind: "taken" };
30
+ }
31
+ catch (err) {
32
+ // VaultNotFoundError = slug available, continue
33
+ const isNotFound = err instanceof VaultNotFoundError ||
34
+ (err instanceof Error && err.name === "VaultNotFoundError");
35
+ if (isNotFound)
36
+ return { kind: "available" };
37
+ // Several other tenants hold the slug (server answers 409): not ours.
38
+ const isConflict = err instanceof VaultConflictError ||
39
+ (err instanceof Error && err.name === "VaultConflictError");
40
+ if (isConflict)
41
+ return { kind: "taken" };
42
+ throw err;
43
+ }
44
+ }
45
+ /**
46
+ * Run the /onboard CLI flow.
47
+ */
48
+ export async function runOnboardCli(options) {
49
+ const { mode, vaultConfig, hqRoot, log = console.log } = options;
50
+ let stepCounter = 0;
51
+ const onProgress = (event) => {
52
+ if (event.status === "running")
53
+ stepCounter++;
54
+ const total = mode === "create-company" ? CREATE_STEPS : JOIN_STEPS;
55
+ log(formatProgress(event, stepCounter, total));
56
+ };
57
+ const config = { vaultConfig, hqRoot };
58
+ try {
59
+ if (mode === "resume") {
60
+ const checkpoint = await readCheckpoint(hqRoot);
61
+ if (!checkpoint) {
62
+ return { success: false, error: "No checkpoint found. Run /onboard to start." };
63
+ }
64
+ log(`Resuming ${checkpoint.mode} flow from step ${checkpoint.completedSteps.length + 1}...`);
65
+ const result = await resumeOnboarding(config, onProgress);
66
+ log("");
67
+ log(formatSummary(result));
68
+ return { success: true, result };
69
+ }
70
+ if (mode === "dry-run") {
71
+ log("DRY RUN — simulating create-company flow:");
72
+ log(" 1. Create person entity");
73
+ log(" 2. Create company entity");
74
+ log(" 3. Provision S3 bucket + KMS key");
75
+ log(" 4. Bootstrap owner membership");
76
+ log(" 5. Verify STS credential vending");
77
+ log(" 6. Write .hq/config.json");
78
+ log("");
79
+ log("No resources will be created. Run /onboard to execute.");
80
+ return { success: true };
81
+ }
82
+ if (mode === "create-company") {
83
+ // Validate slug availability
84
+ if (options.companySlug) {
85
+ const client = new VaultClient(vaultConfig);
86
+ const availability = await checkCompanySlugAvailability(client, options.companySlug);
87
+ if (availability.kind === "taken") {
88
+ return {
89
+ success: false,
90
+ error: `Company slug "${options.companySlug}" is already taken. Choose another.`,
91
+ };
92
+ }
93
+ if (availability.kind === "mine") {
94
+ log(`Company "${options.companySlug}" already exists in your account (${availability.uid}) — finishing its setup.`);
95
+ }
96
+ }
97
+ const input = {
98
+ mode: "create-company",
99
+ personName: options.personName,
100
+ personEmail: options.personEmail,
101
+ companyName: options.companyName,
102
+ companySlug: options.companySlug,
103
+ };
104
+ log(`Creating company "${input.companyName}" (${input.companySlug})...`);
105
+ log("");
106
+ const result = await createCompanyFlow(input, config, onProgress);
107
+ log("");
108
+ log(formatSummary(result));
109
+ return { success: true, result };
110
+ }
111
+ if (mode === "join-company") {
112
+ const input = {
113
+ mode: "join-company",
114
+ personName: options.personName,
115
+ personEmail: options.personEmail,
116
+ inviteToken: options.inviteToken,
117
+ };
118
+ log("Joining company via invite...");
119
+ log("");
120
+ const result = await joinCompanyFlow(input, config, onProgress);
121
+ log("");
122
+ log(formatSummary(result));
123
+ return { success: true, result };
124
+ }
125
+ return { success: false, error: `Unknown mode: ${mode}` };
126
+ }
127
+ catch (err) {
128
+ const checkpointPath = getCheckpointPath(hqRoot);
129
+ const errorMsg = err instanceof Error ? err.message : String(err);
130
+ log("");
131
+ log(formatError(err instanceof Error ? err : new Error(errorMsg), checkpointPath));
132
+ return { success: false, error: errorMsg };
133
+ }
134
+ }
135
+ //# sourceMappingURL=onboard.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * CLI prompt helpers for /onboard command (VLT-9 US-002; vendored from @indigoai-us/hq-onboarding).
3
+ *
4
+ * Provides typed prompt interfaces the slash command can call.
5
+ * These are library functions — the actual UX is in onboard.md.
6
+ */
7
+ import type { OnboardingResult, OnboardingProgress } from "../types.js";
8
+ /**
9
+ * Format a progress event for CLI display.
10
+ */
11
+ export declare function formatProgress(event: OnboardingProgress, stepNumber: number, totalSteps: number): string;
12
+ /**
13
+ * Format the success summary box.
14
+ */
15
+ export declare function formatSummary(result: OnboardingResult): string;
16
+ /**
17
+ * Format an error for CLI display with recovery hints.
18
+ */
19
+ export declare function formatError(error: Error, checkpointPath: string): string;
20
+ /**
21
+ * Validate a company slug: lowercase, alphanumeric + hyphens, 3-40 chars.
22
+ */
23
+ export declare function validateSlug(slug: string): string | null;
24
+ /**
25
+ * Validate email format.
26
+ */
27
+ export declare function validateEmail(email: string): string | null;
28
+ //# sourceMappingURL=prompts.d.ts.map