@indigoai-us/hq-cli 5.121.1 → 5.122.0

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 (38) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/command-catalog.generated.d.ts +38 -4
  3. package/dist/command-catalog.generated.js +48 -4
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +26 -0
  7. package/dist/commands/agent-enroll.js +63 -6
  8. package/dist/commands/agent.js +15 -1
  9. package/dist/commands/cloud-demote.js +3 -2
  10. package/dist/commands/cloud-provision.d.ts +11 -0
  11. package/dist/commands/cloud-provision.js +25 -0
  12. package/dist/commands/cloud-retire.d.ts +51 -0
  13. package/dist/commands/cloud-retire.js +154 -0
  14. package/dist/commands/mesh.js +26 -26
  15. package/dist/lib/agent-kit/adopt-identity.d.ts +27 -0
  16. package/dist/lib/agent-kit/adopt-identity.js +51 -0
  17. package/dist/lib/agent-kit/paths.d.ts +20 -0
  18. package/dist/lib/agent-kit/paths.js +61 -1
  19. package/dist/lib/doctor/checks/work-context.js +1 -1
  20. package/dist/lib/mesh/client.d.ts +3 -2
  21. package/dist/lib/mesh/client.js +3 -2
  22. package/dist/lib/mesh/live/backfill-held.d.ts +3 -2
  23. package/dist/lib/mesh/live/backfill-held.js +3 -2
  24. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  25. package/dist/lib/mesh/live/daemon/doctor.js +12 -2
  26. package/dist/lib/mesh/live/daemon/run.d.ts +7 -0
  27. package/dist/lib/mesh/live/daemon/run.js +41 -0
  28. package/dist/lib/mesh/live/flush.js +44 -2
  29. package/dist/lib/mesh/live/spool.d.ts +13 -0
  30. package/dist/lib/mesh/live/spool.js +67 -0
  31. package/dist/lib/work-context/config.d.ts +1 -4
  32. package/dist/lib/work-context/config.js +1 -5
  33. package/dist/lib/work-context/outbox.d.ts +8 -0
  34. package/dist/lib/work-context/outbox.js +47 -0
  35. package/dist/lib/work-context/reconcile.js +12 -2
  36. package/dist/lib/work-context/state.d.ts +23 -0
  37. package/dist/lib/work-context/state.js +67 -0
  38. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Fixed
6
+
7
+ - Work Mesh now keeps the company you selected when its session context is
8
+ acknowledged or recovered after a restart. Mesh activity stays attached to
9
+ the right company instead of being held for missing attribution.
10
+
11
+ ## [5.122.0] — 2026-09-18
12
+
13
+ ### Added
14
+
15
+ - A bot can now join HQ from the computer its owner already uses. Before,
16
+ `hq agent enroll` refused on such a machine — the owner is signed in there,
17
+ and an agent identity must not share a host account with a person's login —
18
+ which left two bad options: a second user account, or `--replace`.
19
+
20
+ Enrollment now moves the bot into its own directory instead of refusing:
21
+ `~/.hq-agent/<name>/`, with its own credentials, key, state and logs. The
22
+ owner's session file is never read, moved, or changed. `hq agent probe`,
23
+ `hq agent kit`, `hq agent inbox` and `hq agent mcp` all find that directory
24
+ on their own, so the bot is itself without anyone exporting environment
25
+ variables. Use `--name <bot>` to choose the directory, or to run a second
26
+ bot on the same computer.
27
+
28
+ Two or more local bots are deliberately ambiguous: `hq agent` commands then
29
+ ask for `HQ_AGENT_DIR` rather than guessing which identity to act as.
30
+
31
+ ## [5.121.2] — 2026-09-18
32
+
33
+ ### Added
34
+
35
+ - `hq cloud retire company <slug>` soft-tombstones a cloud company (owner
36
+ only, confirm unless `--yes`) so `hq cloud demote company` can run without
37
+ `--propagate-deletes`. Vault files stay; local folders are not touched.
38
+
5
39
  ## [5.121.1] — 2026-09-18
6
40
 
7
41
  ### Fixed
@@ -722,6 +722,37 @@ export declare const COMMAND_CATALOG: readonly [{
722
722
  }];
723
723
  readonly subcommands: readonly [];
724
724
  }];
725
+ }, {
726
+ readonly name: "retire";
727
+ readonly description: "Soft-tombstone a cloud-backed company (owner only)";
728
+ readonly aliases: readonly [];
729
+ readonly hidden: false;
730
+ readonly usage: "[options] [command]";
731
+ readonly arguments: readonly [];
732
+ readonly options: readonly [];
733
+ readonly subcommands: readonly [{
734
+ readonly name: "company";
735
+ readonly description: "Soft-tombstone a cloud company so `hq cloud demote company` can run. Does not delete vault files or local folders. Prompts unless --yes.";
736
+ readonly aliases: readonly [];
737
+ readonly hidden: false;
738
+ readonly usage: "[options] <slug>";
739
+ readonly arguments: readonly [{
740
+ readonly name: "slug";
741
+ readonly required: true;
742
+ readonly variadic: false;
743
+ }];
744
+ readonly options: readonly [{
745
+ readonly flags: "--hq-root <path>";
746
+ readonly description: "Local HQ tree root (default: <runtime-path>)";
747
+ }, {
748
+ readonly flags: "--vault-api-url <url>";
749
+ readonly description: "Vault API URL (default: https://hqapi.hq.computer)";
750
+ }, {
751
+ readonly flags: "-y, --yes";
752
+ readonly description: "Skip the confirmation prompt (for scripts)";
753
+ }];
754
+ readonly subcommands: readonly [];
755
+ }];
725
756
  }, {
726
757
  readonly name: "demote";
727
758
  readonly description: "Demote a cloud-backed entity back to local-only";
@@ -732,7 +763,7 @@ export declare const COMMAND_CATALOG: readonly [{
732
763
  readonly options: readonly [];
733
764
  readonly subcommands: readonly [{
734
765
  readonly name: "company";
735
- readonly description: "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned in hq-console. Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.";
766
+ readonly description: "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned (`hq cloud retire company` or hq-console). Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.";
736
767
  readonly aliases: readonly [];
737
768
  readonly hidden: false;
738
769
  readonly usage: "[options] <slug>";
@@ -4240,6 +4271,9 @@ export declare const COMMAND_CATALOG: readonly [{
4240
4271
  }, {
4241
4272
  readonly flags: "--replace";
4242
4273
  readonly description: "Overwrite an existing machine identity on this host";
4274
+ }, {
4275
+ readonly flags: "--name <bot>";
4276
+ readonly description: "Enroll into ~/.hq-agent/<bot>/ — for a bot sharing a computer with its owner";
4243
4277
  }, {
4244
4278
  readonly flags: "--api-base-url <url>";
4245
4279
  readonly description: "hq-pro control plane (default: HQ_VAULT_API_URL or production)";
@@ -5716,7 +5750,7 @@ export declare const COMMAND_CATALOG: readonly [{
5716
5750
  readonly subcommands: readonly [];
5717
5751
  }, {
5718
5752
  readonly name: "set";
5719
- readonly description: "Set the device default company slug (unlocked when migration is true for every membership)";
5753
+ readonly description: "Set the device default company slug after verifying membership";
5720
5754
  readonly aliases: readonly [];
5721
5755
  readonly hidden: false;
5722
5756
  readonly usage: "[options] <slug>";
@@ -5727,10 +5761,10 @@ export declare const COMMAND_CATALOG: readonly [{
5727
5761
  }];
5728
5762
  readonly options: readonly [{
5729
5763
  readonly flags: "--allow-without-migration";
5730
- readonly description: "Bypass DEFAULT_COMPANY_LOCKED with a warning";
5764
+ readonly description: "Deprecated compatibility no-op; selected-company membership is always verified";
5731
5765
  }, {
5732
5766
  readonly flags: "--company <slug|uid>";
5733
- readonly description: "Deprecated no-op; unlock probes every active membership";
5767
+ readonly description: "Deprecated no-op; the selected company is verified directly";
5734
5768
  }, {
5735
5769
  readonly flags: "--json";
5736
5770
  readonly description: "Print machine-readable JSON";
@@ -924,6 +924,46 @@ export const COMMAND_CATALOG = [
924
924
  }
925
925
  ]
926
926
  },
927
+ {
928
+ "name": "retire",
929
+ "description": "Soft-tombstone a cloud-backed company (owner only)",
930
+ "aliases": [],
931
+ "hidden": false,
932
+ "usage": "[options] [command]",
933
+ "arguments": [],
934
+ "options": [],
935
+ "subcommands": [
936
+ {
937
+ "name": "company",
938
+ "description": "Soft-tombstone a cloud company so `hq cloud demote company` can run. Does not delete vault files or local folders. Prompts unless --yes.",
939
+ "aliases": [],
940
+ "hidden": false,
941
+ "usage": "[options] <slug>",
942
+ "arguments": [
943
+ {
944
+ "name": "slug",
945
+ "required": true,
946
+ "variadic": false
947
+ }
948
+ ],
949
+ "options": [
950
+ {
951
+ "flags": "--hq-root <path>",
952
+ "description": "Local HQ tree root (default: <runtime-path>)"
953
+ },
954
+ {
955
+ "flags": "--vault-api-url <url>",
956
+ "description": "Vault API URL (default: https://hqapi.hq.computer)"
957
+ },
958
+ {
959
+ "flags": "-y, --yes",
960
+ "description": "Skip the confirmation prompt (for scripts)"
961
+ }
962
+ ],
963
+ "subcommands": []
964
+ }
965
+ ]
966
+ },
927
967
  {
928
968
  "name": "demote",
929
969
  "description": "Demote a cloud-backed entity back to local-only",
@@ -935,7 +975,7 @@ export const COMMAND_CATALOG = [
935
975
  "subcommands": [
936
976
  {
937
977
  "name": "company",
938
- "description": "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned in hq-console. Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.",
978
+ "description": "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned (`hq cloud retire company` or hq-console). Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.",
939
979
  "aliases": [],
940
980
  "hidden": false,
941
981
  "usage": "[options] <slug>",
@@ -5489,6 +5529,10 @@ export const COMMAND_CATALOG = [
5489
5529
  "flags": "--replace",
5490
5530
  "description": "Overwrite an existing machine identity on this host"
5491
5531
  },
5532
+ {
5533
+ "flags": "--name <bot>",
5534
+ "description": "Enroll into ~/.hq-agent/<bot>/ — for a bot sharing a computer with its owner"
5535
+ },
5492
5536
  {
5493
5537
  "flags": "--api-base-url <url>",
5494
5538
  "description": "hq-pro control plane (default: HQ_VAULT_API_URL or production)"
@@ -7374,7 +7418,7 @@ export const COMMAND_CATALOG = [
7374
7418
  },
7375
7419
  {
7376
7420
  "name": "set",
7377
- "description": "Set the device default company slug (unlocked when migration is true for every membership)",
7421
+ "description": "Set the device default company slug after verifying membership",
7378
7422
  "aliases": [],
7379
7423
  "hidden": false,
7380
7424
  "usage": "[options] <slug>",
@@ -7388,11 +7432,11 @@ export const COMMAND_CATALOG = [
7388
7432
  "options": [
7389
7433
  {
7390
7434
  "flags": "--allow-without-migration",
7391
- "description": "Bypass DEFAULT_COMPANY_LOCKED with a warning"
7435
+ "description": "Deprecated compatibility no-op; selected-company membership is always verified"
7392
7436
  },
7393
7437
  {
7394
7438
  "flags": "--company <slug|uid>",
7395
- "description": "Deprecated no-op; unlock probes every active membership"
7439
+ "description": "Deprecated no-op; the selected company is verified directly"
7396
7440
  },
7397
7441
  {
7398
7442
  "flags": "--json",
@@ -141,6 +141,12 @@ export declare const REGISTRATION_PLAN: readonly [{
141
141
  readonly parent: "cloud";
142
142
  readonly module: "./commands/cloud-provision.js";
143
143
  readonly exportName: "registerCloudProvisionCommands";
144
+ }, {
145
+ readonly type: "registrar";
146
+ readonly root: "cloud";
147
+ readonly parent: "cloud";
148
+ readonly module: "./commands/cloud-retire.js";
149
+ readonly exportName: "registerCloudRetireCommands";
144
150
  }, {
145
151
  readonly type: "registrar";
146
152
  readonly root: "cloud";
@@ -26,6 +26,7 @@ export const REGISTRATION_PLAN = [
26
26
  { type: "registrar", root: "sync", parent: "sync", module: "./commands/sync-manifest.js", exportName: "registerSyncManifestCommand" },
27
27
  { type: "group", root: "cloud", id: "cloud", name: "cloud", description: "Cloud commands — provision entities and manage cloud-backed companies" },
28
28
  { type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-provision.js", exportName: "registerCloudProvisionCommands" },
29
+ { type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-retire.js", exportName: "registerCloudRetireCommands" },
29
30
  { type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-demote.js", exportName: "registerCloudDemoteCommands" },
30
31
  { type: "registrar", root: "team-sync", parent: "program", module: "./commands/team-sync.js", exportName: "registerTeamSyncCommand" },
31
32
  { type: "registrar", root: "login", parent: "program", module: "./commands/login.js", exportName: "registerLoginCommand" },
@@ -88,7 +88,29 @@ export interface EnrollOptions {
88
88
  company?: string;
89
89
  replace?: boolean;
90
90
  apiBaseUrl?: string;
91
+ /** Enroll into `~/.hq-agent/<name>/` — a bot sharing a host with a person. */
92
+ name?: string;
91
93
  }
94
+ /**
95
+ * Where this enrollment should write, given what already lives on the host.
96
+ *
97
+ * A bot that runs on its owner's own computer is the common case, not an
98
+ * error: the machine has the owner's HQ session, and the two identities have
99
+ * to coexist. So an unpinned enrollment onto such a host does NOT refuse and
100
+ * does NOT overwrite anything — it moves into `~/.hq-agent/<name>/`, its own
101
+ * tree with its own credentials, key, state and logs. The person's session
102
+ * file is neither read nor touched, and `hq agent …` finds the bot's tree on
103
+ * its own afterwards (see agentDir).
104
+ */
105
+ export declare function resolveEnrollTarget(opts: {
106
+ name?: string;
107
+ replace?: boolean;
108
+ }, env?: NodeJS.ProcessEnv, home?: string): {
109
+ paths: AgentKitPaths;
110
+ local: boolean;
111
+ };
112
+ /** Lowercase slug, so the directory name can never escape `~/.hq-agent`. */
113
+ export declare function validateLocalAgentName(name: string): string;
92
114
  export interface EnrollResult {
93
115
  agentUid: string;
94
116
  companySlug: string;
@@ -98,6 +120,10 @@ export interface EnrollResult {
98
120
  hostKeyPath: string;
99
121
  /** Set when --company disagreed with the server's company. */
100
122
  companyMismatch?: string;
123
+ /** True when this enrolled beside a person's session in its own directory. */
124
+ local: boolean;
125
+ /** The tree this identity lives in. */
126
+ agentDir: string;
101
127
  }
102
128
  /** Pure-ish orchestration so tests can drive it without a TTY or network. */
103
129
  export declare function enrollHost(opts: EnrollOptions, deps?: EnrollDeps): Promise<EnrollResult>;
@@ -22,7 +22,7 @@ import { DEFAULT_VAULT_API_URL, personTokenCacheFile, } from "../utils/cognito-s
22
22
  import { HQ_CLIENT_NAME } from "../utils/vault-api.js";
23
23
  import { networkTransportErrorCode } from "../utils/network-transport-error.js";
24
24
  import { generateHostKeyPair, machineCredsFileExists, readExternalMachineCreds, writeHostKeyPair, writeMachineCreds, } from "../lib/agent-kit/creds.js";
25
- import { agentKitPaths } from "../lib/agent-kit/paths.js";
25
+ import { DEFAULT_LOCAL_AGENT_NAME, agentKitPaths, localAgentDir, } from "../lib/agent-kit/paths.js";
26
26
  export const ENROLL_PATH = "/v1/agents/enroll";
27
27
  /** Canonical code: un-grouped, uppercase (server hashes exactly this). */
28
28
  export function normalizeEnrollmentCode(raw) {
@@ -63,9 +63,14 @@ export function refusalMessage(existing) {
63
63
  case "none":
64
64
  return null;
65
65
  case "human":
66
+ // Reached only when a caller pins the tree (HQ_AGENT_DIR / --dir) at a
67
+ // home that would sit on top of a person's login. The unpinned path
68
+ // enrolls into a sibling directory instead of refusing — see
69
+ // resolveEnrollTarget.
66
70
  return (`A human HQ session exists at ${existing.file}. An agent identity must not ` +
67
- `share a host account with a person's login. Run enrollment under a dedicated ` +
68
- `user (or container), or pass --replace to enroll anyway — the human session ` +
71
+ `share a host account with a person's login. Enroll into a directory of its ` +
72
+ `own (drop HQ_AGENT_DIR and this happens automatically, or pass ` +
73
+ `--name <bot>), or pass --replace to enroll here anyway — the human session ` +
69
74
  `file is left untouched and never read.`);
70
75
  case "machine":
71
76
  return (`This host already has a machine identity at ${existing.file}` +
@@ -169,10 +174,49 @@ function parseEnrollResponse(raw) {
169
174
  },
170
175
  };
171
176
  }
177
+ /**
178
+ * Where this enrollment should write, given what already lives on the host.
179
+ *
180
+ * A bot that runs on its owner's own computer is the common case, not an
181
+ * error: the machine has the owner's HQ session, and the two identities have
182
+ * to coexist. So an unpinned enrollment onto such a host does NOT refuse and
183
+ * does NOT overwrite anything — it moves into `~/.hq-agent/<name>/`, its own
184
+ * tree with its own credentials, key, state and logs. The person's session
185
+ * file is neither read nor touched, and `hq agent …` finds the bot's tree on
186
+ * its own afterwards (see agentDir).
187
+ */
188
+ export function resolveEnrollTarget(opts, env = process.env, home = os.homedir()) {
189
+ const pinned = Boolean(env.HQ_AGENT_DIR?.trim() || env.HQ_MACHINE_CREDS_FILE?.trim());
190
+ if (opts.name) {
191
+ const dir = localAgentDir(validateLocalAgentName(opts.name), home);
192
+ return { paths: agentKitPaths(home, { ...env, HQ_AGENT_DIR: dir }), local: true };
193
+ }
194
+ const base = agentKitPaths(home, env);
195
+ // A pinned tree is the caller's explicit choice; --replace is an explicit
196
+ // choice too. Neither gets silently redirected.
197
+ if (pinned || opts.replace)
198
+ return { paths: base, local: false };
199
+ if (detectExistingIdentity(base, env).kind !== "human") {
200
+ return { paths: base, local: false };
201
+ }
202
+ const dir = localAgentDir(DEFAULT_LOCAL_AGENT_NAME, home);
203
+ return { paths: agentKitPaths(home, { ...env, HQ_AGENT_DIR: dir }), local: true };
204
+ }
205
+ /** Lowercase slug, so the directory name can never escape `~/.hq-agent`. */
206
+ export function validateLocalAgentName(name) {
207
+ const trimmed = (name ?? "").trim();
208
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(trimmed) || trimmed.includes("--")) {
209
+ throw new EnrollError(`Invalid --name "${name}": use 1–40 lowercase letters, digits and single hyphens (e.g. "grokbot").`);
210
+ }
211
+ return trimmed;
212
+ }
172
213
  /** Pure-ish orchestration so tests can drive it without a TTY or network. */
173
214
  export async function enrollHost(opts, deps = {}) {
174
215
  const env = deps.env ?? process.env;
175
- const paths = deps.paths ?? agentKitPaths(os.homedir(), env);
216
+ const target = deps.paths
217
+ ? { paths: deps.paths, local: false }
218
+ : resolveEnrollTarget({ name: opts.name, replace: opts.replace }, env);
219
+ const paths = target.paths;
176
220
  const doFetch = deps.fetch ?? fetch;
177
221
  const apiBaseUrl = (opts.apiBaseUrl ?? env.HQ_VAULT_API_URL ?? DEFAULT_VAULT_API_URL).replace(/\/+$/, "");
178
222
  const code = normalizeEnrollmentCode(opts.code);
@@ -224,6 +268,8 @@ export async function enrollHost(opts, deps = {}) {
224
268
  };
225
269
  writeMachineCreds(paths, creds);
226
270
  return {
271
+ local: target.local,
272
+ agentDir: paths.agentDir,
227
273
  agentUid: parsed.agentUid,
228
274
  companySlug: parsed.companySlug,
229
275
  companyUid: parsed.companyUid,
@@ -241,6 +287,7 @@ export function registerAgentEnrollCommand(agent) {
241
287
  .description("Enroll this host as an external HQ agent using a one-time code")
242
288
  .option("--company <slug>", "Expected company slug (warns when the code belongs elsewhere)")
243
289
  .option("--replace", "Overwrite an existing machine identity on this host")
290
+ .option("--name <bot>", "Enroll into ~/.hq-agent/<bot>/ — for a bot sharing a computer with its owner")
244
291
  .option("--api-base-url <url>", "hq-pro control plane (default: HQ_VAULT_API_URL or production)")
245
292
  .action(async (code, opts) => {
246
293
  try {
@@ -249,8 +296,16 @@ export function registerAgentEnrollCommand(agent) {
249
296
  company: opts.company,
250
297
  replace: opts.replace,
251
298
  apiBaseUrl: opts.apiBaseUrl,
299
+ name: opts.name,
252
300
  });
253
301
  console.log(chalk.green(`Enrolled as ${result.agentUid} in ${result.companySlug}.`));
302
+ if (result.local) {
303
+ // Say it plainly: this host belongs to a person, and the bot is a
304
+ // guest on it. Nothing of theirs was read, moved or overwritten.
305
+ console.log(chalk.dim(` This computer already has a person signed in, so this agent lives in its own\n` +
306
+ ` directory beside them: ${result.agentDir}. Their session was not read or changed.\n` +
307
+ ` Every hq agent command finds this tree on its own — no environment to set.`));
308
+ }
254
309
  console.log(` host fingerprint: ${result.hostFingerprint}`);
255
310
  console.log(` credentials: ${result.credsPath} (0600)`);
256
311
  console.log(` host key: ${result.hostKeyPath} (0600)`);
@@ -260,8 +315,10 @@ export function registerAgentEnrollCommand(agent) {
260
315
  }
261
316
  console.log("");
262
317
  console.log("Next:");
263
- console.log(" hq whoami # should report the agent identity");
264
- console.log(" hq agent kit install # sync, work-mesh, inbox and heartbeat services");
318
+ console.log(result.local
319
+ ? " hq agent probe # its first line reports this agent, not you"
320
+ : " hq whoami # should report the agent identity");
321
+ console.log(" hq agent kit install # inbox poller and heartbeat services");
265
322
  console.log(" hq agent probe # end-to-end check, reported to the console");
266
323
  }
267
324
  catch (err) {
@@ -8,6 +8,9 @@
8
8
  * hq agent mcp stdio MCP server for the bot framework
9
9
  * hq agent inbox [done <id…>] pending HQ messages / mark handled
10
10
  */
11
+ import * as os from "node:os";
12
+ import { adoptAgentIdentityEnv } from "../lib/agent-kit/adopt-identity.js";
13
+ import { agentKitPaths } from "../lib/agent-kit/paths.js";
11
14
  import { registerAgentEnrollCommand } from "./agent-enroll.js";
12
15
  import { registerAgentInboxCommand } from "./agent-inbox.js";
13
16
  import { registerAgentKitCommand } from "./agent-kit.js";
@@ -16,7 +19,18 @@ import { registerAgentProbeCommand } from "./agent-probe.js";
16
19
  export function registerAgentCommand(program) {
17
20
  const agent = program
18
21
  .command("agent")
19
- .description("Enroll and run this host as an external HQ agent");
22
+ .description("Enroll and run this host as an external HQ agent")
23
+ // A bot that shares a computer with its owner keeps its identity in its
24
+ // own directory. Publish that tree to the environment before any
25
+ // subcommand runs, so the CLI and hq-cloud both mint as the AGENT rather
26
+ // than falling back to the one fixed path — and, on a person's computer,
27
+ // to the person. Enroll is exempt: it is the command that decides where
28
+ // the tree goes.
29
+ .hook("preAction", (_thisCommand, actionCommand) => {
30
+ if (actionCommand.name() === "enroll")
31
+ return;
32
+ adoptAgentIdentityEnv(agentKitPaths(os.homedir(), process.env), process.env);
33
+ });
20
34
  registerAgentEnrollCommand(agent);
21
35
  registerAgentKitCommand(agent);
22
36
  registerAgentProbeCommand(agent);
@@ -160,8 +160,9 @@ export function registerCloudDemoteCommands(program) {
160
160
  demoteCmd
161
161
  .command("company")
162
162
  .description("Demote a cloud-backed company to local-only after the cloud entity " +
163
- "has been soft-tombstoned in hq-console. Removes .hq/config.json, " +
164
- "flips company.yaml `cloud: false`, and strips the manifest cloud refs.")
163
+ "has been soft-tombstoned (`hq cloud retire company` or hq-console). " +
164
+ "Removes .hq/config.json, flips company.yaml `cloud: false`, and " +
165
+ "strips the manifest cloud refs.")
165
166
  .argument("<slug>", "Company slug")
166
167
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
167
168
  .option("--vault-api-url <url>", `Vault API URL (default: ${DEFAULT_VAULT_API_URL})`, DEFAULT_VAULT_API_URL)
@@ -38,6 +38,8 @@ export interface VaultEntity {
38
38
  kmsKeyId?: string | null;
39
39
  status?: string;
40
40
  ownerUid?: string;
41
+ /** Set by hq-pro after a soft tombstone. Absent on live companies. */
42
+ deleted?: boolean;
41
43
  }
42
44
  /** Per-company `.hq/config.json` schema (matches AppBar `provision.rs::CompanyConfig`). */
43
45
  export interface CompanyConfig {
@@ -142,6 +144,15 @@ export interface VaultClient {
142
144
  name: string;
143
145
  ownerUid?: string;
144
146
  }): Promise<VaultEntity>;
147
+ /**
148
+ * Soft-tombstone a company (`DELETE /entity/{uid}`). Owner-only. Leaves the
149
+ * row, S3 bucket, KMS key, and memberships intact. Idempotent on an already
150
+ * deleted company. Used by `hq cloud retire company`.
151
+ */
152
+ softDeleteCompany(uid: string): Promise<{
153
+ deleted: true;
154
+ uid: string;
155
+ }>;
145
156
  }
146
157
  /** Custom error class so the CLI runner can map to exit codes. */
147
158
  export declare class ProvisionError extends Error {
@@ -364,6 +364,31 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
364
364
  }
365
365
  return data.entity;
366
366
  },
367
+ async softDeleteCompany(uid) {
368
+ const url = `${apiUrl.replace(/\/$/, "")}/entity/${encodeURIComponent(uid)}`;
369
+ const res = await fetch(url, { method: "DELETE", headers });
370
+ if (res.status === 403) {
371
+ const body = await safeBody(res);
372
+ throw new ProvisionError(2, `Refusing to retire '${uid}': only the company owner can soft-tombstone it. ${body}`);
373
+ }
374
+ if (res.status === 404) {
375
+ const body = await safeBody(res);
376
+ throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: not found — ${body}`);
377
+ }
378
+ if (res.status === 400) {
379
+ const body = await safeBody(res);
380
+ throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
381
+ }
382
+ if (!res.ok) {
383
+ const body = await safeBody(res);
384
+ throw new ProvisionError(1, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
385
+ }
386
+ const data = (await res.json());
387
+ if (data.deleted !== true || typeof data.uid !== "string") {
388
+ throw new ProvisionError(1, `Vault DELETE /entity/${uid} returned ${res.status} with no deleted confirmation`);
389
+ }
390
+ return { deleted: true, uid: data.uid };
391
+ },
367
392
  };
368
393
  }
369
394
  async function safeBody(res) {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `hq cloud retire company <slug>` — soft-tombstone a cloud company so
3
+ * `hq cloud demote company` can run without `--propagate-deletes`.
4
+ *
5
+ * Console equivalent: Settings → Delete company (`DELETE /entity/{uid}`).
6
+ * Owner-only. The entity row, S3 bucket, KMS key, and memberships stay;
7
+ * `/membership/me` stops listing the company. Local files are not touched —
8
+ * demote is the follow-up that flips the local tree back to local-only.
9
+ *
10
+ * Confirm step is on by default (`[y/N]`). `--yes` skips it for scripts.
11
+ * A non-TTY stdin without `--yes` refuses rather than hanging.
12
+ *
13
+ * Exit codes (mirrors cloud-provision / cloud-demote):
14
+ * 0 — success, already tombstoned, or the operator cancelled.
15
+ * 1 — vault HTTP failure (auth/network/5xx).
16
+ * 2 — validation (bad slug, no entity, not owner, missing confirm).
17
+ */
18
+ import { Command } from "commander";
19
+ import { type VaultClient } from "./cloud-provision.js";
20
+ /** Final stdout JSON shape. */
21
+ export interface RetireResult {
22
+ ok: boolean;
23
+ company_slug: string;
24
+ cloud_uid: string | null;
25
+ /** True when hq-pro already had deleted=true (no DELETE sent). */
26
+ already_deleted: boolean;
27
+ cancelled?: boolean;
28
+ }
29
+ export interface RetireCompanyOptions {
30
+ slug: string;
31
+ hqRoot: string;
32
+ vaultApiUrl: string;
33
+ /** Skip the interactive confirmation prompt. */
34
+ yes?: boolean;
35
+ vaultClient?: VaultClient;
36
+ resolveAccessToken?: () => Promise<string>;
37
+ confirm?: (question: string) => Promise<boolean>;
38
+ stdinIsTTY?: boolean;
39
+ }
40
+ export declare function defaultConfirm(question: string): Promise<boolean>;
41
+ /**
42
+ * Read `companyUid` from `companies/<slug>/.hq/config.json` when the by-slug
43
+ * lookup misses (a tombstoned company is filtered from that route).
44
+ */
45
+ export declare function readLocalCompanyUid(hqRoot: string, slug: string): string | null;
46
+ export declare function retireCompany(options: RetireCompanyOptions): Promise<RetireResult>;
47
+ /**
48
+ * Register `retire company <slug>` under the `cloud` command group.
49
+ */
50
+ export declare function registerCloudRetireCommands(program: Command): void;
51
+ //# sourceMappingURL=cloud-retire.d.ts.map