@indigoai-us/hq-cli 5.190.0 → 5.192.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.192.0] — 2026-09-25
6
+
7
+ ### Added
8
+
9
+ - `hq agents models` lists cloud-agent catalog entries by provider and supports JSON output.
10
+ Provision and update validate model IDs against the catalog. They use the cached
11
+ catalog when the endpoint is unavailable, and rely on server validation if no cache exists.
12
+
13
+ ### Fixed
14
+
15
+ - `hq report adopt` checks whether scheduled jobs are enabled for the company before creating a cloud job, and explains when the server rejects a disabled job.
16
+
17
+ ## [5.191.0] — 2026-09-25
18
+
19
+ ### Fixed
20
+
21
+ - `hq agents provision` now rejects an invalid `--slug` before sending the agent create request.
22
+
5
23
  ## [5.190.0] — 2026-09-25
6
24
 
7
25
  ### Added
@@ -19,6 +37,12 @@
19
37
  - `hq bot promote` accepts server-signed upload URLs without SSE headers when the bucket supplies encryption. SSE header names are checked case-insensitively, and any supplied encryption must be a complete SSE-KMS pair.
20
38
  - Company slug lookup errors now point to the supported `hq sources entities` command.
21
39
 
40
+ ### Added
41
+
42
+ - QMD native-binding startup failures after sync can emit an actionable warning
43
+ when the `qmd.reindex-startup-warning` hq-flags canary is enabled. The flag
44
+ defaults off, and sync still succeeds when indexing cannot run.
45
+
22
46
  ## [5.189.0] — 2026-09-25
23
47
 
24
48
  ### Added
@@ -3845,6 +3845,21 @@ export declare const COMMAND_CATALOG: readonly [{
3845
3845
  readonly description: "Company slug (resolves to companyUid)";
3846
3846
  }];
3847
3847
  readonly subcommands: readonly [{
3848
+ readonly name: "models";
3849
+ readonly description: "List the model catalog available to cloud agents";
3850
+ readonly aliases: readonly [];
3851
+ readonly hidden: false;
3852
+ readonly usage: "[options]";
3853
+ readonly arguments: readonly [];
3854
+ readonly options: readonly [{
3855
+ readonly flags: "--provider <provider>";
3856
+ readonly description: "Filter to one provider: codex, grok, or claude";
3857
+ }, {
3858
+ readonly flags: "--json";
3859
+ readonly description: "Print the stable catalog JSON shape";
3860
+ }];
3861
+ readonly subcommands: readonly [];
3862
+ }, {
3848
3863
  readonly name: "message";
3849
3864
  readonly description: "Send a message to an agent and wait for its reply";
3850
3865
  readonly aliases: readonly ["msg"];
@@ -3939,7 +3954,7 @@ export declare const COMMAND_CATALOG: readonly [{
3939
3954
  readonly description: "Runtime: agents-v2 (recommended) | codex | grok | claude (accepted for script compatibility; use agents-v2 for new Claude agents)";
3940
3955
  }, {
3941
3956
  readonly flags: "--model <model>";
3942
- readonly description: "Brain model, for example claude-opus-5-5, gpt-5.5, or grok-4.6";
3957
+ readonly description: "Brain model from `hq agents models`";
3943
3958
  }, {
3944
3959
  readonly flags: "--auth-mode <mode>";
3945
3960
  readonly description: "Auth: subscription | apiKey (default subscription)";
@@ -4074,7 +4089,7 @@ export declare const COMMAND_CATALOG: readonly [{
4074
4089
  }, {
4075
4090
  readonly name: "config";
4076
4091
  readonly description: "Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)";
4077
- readonly aliases: readonly [];
4092
+ readonly aliases: readonly ["update"];
4078
4093
  readonly hidden: false;
4079
4094
  readonly usage: "[options] <agentUid>";
4080
4095
  readonly arguments: readonly [{
@@ -4087,7 +4102,7 @@ export declare const COMMAND_CATALOG: readonly [{
4087
4102
  readonly description: "Company slug (resolves to companyUid)";
4088
4103
  }, {
4089
4104
  readonly flags: "--model <model>";
4090
- readonly description: "Codex model id";
4105
+ readonly description: "Model id from `hq agents models`";
4091
4106
  }, {
4092
4107
  readonly flags: "--effort <effort>";
4093
4108
  readonly description: "Reasoning effort: minimal | low | medium | high | xhigh";
@@ -4961,6 +4961,25 @@ export const COMMAND_CATALOG = [
4961
4961
  }
4962
4962
  ],
4963
4963
  "subcommands": [
4964
+ {
4965
+ "name": "models",
4966
+ "description": "List the model catalog available to cloud agents",
4967
+ "aliases": [],
4968
+ "hidden": false,
4969
+ "usage": "[options]",
4970
+ "arguments": [],
4971
+ "options": [
4972
+ {
4973
+ "flags": "--provider <provider>",
4974
+ "description": "Filter to one provider: codex, grok, or claude"
4975
+ },
4976
+ {
4977
+ "flags": "--json",
4978
+ "description": "Print the stable catalog JSON shape"
4979
+ }
4980
+ ],
4981
+ "subcommands": []
4982
+ },
4964
4983
  {
4965
4984
  "name": "message",
4966
4985
  "description": "Send a message to an agent and wait for its reply",
@@ -5092,7 +5111,7 @@ export const COMMAND_CATALOG = [
5092
5111
  },
5093
5112
  {
5094
5113
  "flags": "--model <model>",
5095
- "description": "Brain model, for example claude-opus-5-5, gpt-5.5, or grok-4.6"
5114
+ "description": "Brain model from `hq agents models`"
5096
5115
  },
5097
5116
  {
5098
5117
  "flags": "--auth-mode <mode>",
@@ -5268,7 +5287,9 @@ export const COMMAND_CATALOG = [
5268
5287
  {
5269
5288
  "name": "config",
5270
5289
  "description": "Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)",
5271
- "aliases": [],
5290
+ "aliases": [
5291
+ "update"
5292
+ ],
5272
5293
  "hidden": false,
5273
5294
  "usage": "[options] <agentUid>",
5274
5295
  "arguments": [
@@ -5285,7 +5306,7 @@ export const COMMAND_CATALOG = [
5285
5306
  },
5286
5307
  {
5287
5308
  "flags": "--model <model>",
5288
- "description": "Codex model id"
5309
+ "description": "Model id from `hq agents models`"
5289
5310
  },
5290
5311
  {
5291
5312
  "flags": "--effort <effort>",
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { Command } from "commander";
30
30
  import { type TerminalSessionPayload } from "@indigoai-us/hq-cloud/outposts/node";
31
+ import { type AgentModelProvider, type AgentModelCatalog } from "../lib/agents/model-catalog.js";
31
32
  import { type BillingErrorPayload } from "../utils/billing-gate.js";
32
33
  /** Reasoning-effort values hq-pro accepts on `runtime-config`. */
33
34
  export declare const VALID_EFFORTS: Set<string>;
@@ -341,6 +342,8 @@ export declare function formatJobsTable(jobs: AgentJobView[]): string;
341
342
  export declare function formatPauseResult(result: PauseJobResult): string;
342
343
  /** Human-readable "hot-applied to the running box" vs "saved for next launch". */
343
344
  export declare function appliedHint(applied: boolean): string;
345
+ /** Stable human table shared by the all-provider and provider-filtered forms. */
346
+ export declare function formatAgentModelsTable(catalog: AgentModelCatalog, selectedProvider?: AgentModelProvider): string;
344
347
  /** Agent uid shape (`agt_…`). */
345
348
  /** One message in a DM thread, as returned by `GET /v1/notify/thread`. */
346
349
  export interface DmThreadMessage {
@@ -38,6 +38,7 @@ import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSe
38
38
  import { peekIdToken } from "../utils/id-token.js";
39
39
  import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, } from "../utils/plan-gate-error.js";
40
40
  import { AGENT_UID_PATTERN, isAgentUid } from "../lib/notify-recipient.js";
41
+ import { AGENT_MODEL_PROVIDERS, modelSelectionErrorMessage, readAgentModelCatalog, validateAgentModelSelection, } from "../lib/agents/model-catalog.js";
41
42
  import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
42
43
  /** Reasoning-effort values hq-pro accepts on `runtime-config`. */
43
44
  export const VALID_EFFORTS = new Set([
@@ -55,11 +56,14 @@ export const VALID_PROVIDERS = new Set(["codex", "grok", "claude", "agents-v2"])
55
56
  export const VALID_AUTH_MODES = new Set(["subscription", "apiKey"]);
56
57
  /** Customer-facing agent size keys served by hq-pro's authoritative catalog. */
57
58
  export const VALID_AGENT_SIZE_KEYS = new Set(["basic", "power", "dev"]);
58
- const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model claude-opus-5-5";
59
+ // Keep cloud agent create slugs aligned with hq-pro-agents POST /v1/agents.
60
+ const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
61
+ const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model <model-id>";
59
62
  const CLAUDE_API_KEY_ARGS = "--auth-mode apiKey --api-key-env <VAR>";
60
63
  function claudeProvisionGuidance() {
61
64
  return [
62
65
  "The account must be enabled for Claude. Subscription auth is the default.",
66
+ "Use `hq agents models` to see the current model ids.",
63
67
  `Create an agent with: ${CLAUDE_PROVISION_COMMAND}`,
64
68
  `For API-key auth, add: ${CLAUDE_API_KEY_ARGS}`,
65
69
  "The CLI accepts --provider claude for script compatibility, but the server rejects new agents that use it.",
@@ -861,6 +865,50 @@ export function appliedHint(applied) {
861
865
  ? "applied to the running box now"
862
866
  : "saved — takes effect on the agent's next launch";
863
867
  }
868
+ async function validateAgentModelOption(token, chosenProvider, modelId) {
869
+ const result = await readAgentModelCatalog(token);
870
+ if (!result.ok) {
871
+ if (result.reason === "unavailable") {
872
+ console.error(chalk.yellow("model validation skipped: catalog unavailable (the server still validates)."));
873
+ return;
874
+ }
875
+ throw new Error(`Could not read the model catalog (HTTP ${result.status}). Check your hq login and try again.`);
876
+ }
877
+ if (result.source === "cache") {
878
+ console.error(chalk.yellow(`Model catalog unavailable; validating against the cached catalog from ${new Date(result.fetchedAt).toISOString()}.`));
879
+ }
880
+ const selection = validateAgentModelSelection(result.catalog, modelId, chosenProvider);
881
+ if (!selection.ok) {
882
+ throw new Error(modelSelectionErrorMessage(modelId, selection));
883
+ }
884
+ }
885
+ /** Stable human table shared by the all-provider and provider-filtered forms. */
886
+ export function formatAgentModelsTable(catalog, selectedProvider) {
887
+ const providers = selectedProvider ? [selectedProvider] : AGENT_MODEL_PROVIDERS;
888
+ const sections = providers.map((provider) => {
889
+ const view = catalog.providers[provider];
890
+ const columns = ["ID", "LABEL", "DEFAULT", "NEW", "AVAILABILITY"];
891
+ const rows = view.models.map((model) => [
892
+ model.id,
893
+ model.label,
894
+ model.id === view.defaultModel ? "yes" : "",
895
+ model.isNew ? "yes" : "",
896
+ model.selectable ? "available" : "not yet on agents",
897
+ ]);
898
+ const widths = columns.map((column, index) => Math.max(column.length, ...rows.map((row) => row[index].length)));
899
+ const render = (cells) => cells.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd();
900
+ return [
901
+ chalk.bold(provider.toUpperCase()),
902
+ render(columns),
903
+ ...rows.map(render),
904
+ ].join("\n");
905
+ });
906
+ return sections.join("\n\n");
907
+ }
908
+ function catalogUnavailableMessage() {
909
+ return ("Model catalog is unavailable and no cached copy exists. " +
910
+ "Check your connection and try again after model-catalog.serve is enabled.");
911
+ }
864
912
  /**
865
913
  * Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
866
914
  * directly; anything else is treated as a slug or display name and resolved
@@ -1095,6 +1143,8 @@ export function registerAgentsCommand(program) {
1095
1143
  bySubcommand: {
1096
1144
  message: { capability: "agents:use", routeAvailable: false },
1097
1145
  thread: { capability: "agents:use", routeAvailable: false },
1146
+ // The catalog read endpoint is JWT-only; hq-pro has no keyed parallel.
1147
+ models: { capability: "agents:use", routeAvailable: false },
1098
1148
  // Interactive terminals are for humans: hq-pro serves no keyed
1099
1149
  // `/v1/keys/agents/{uid}/terminal` route, so machine keys fail closed.
1100
1150
  terminal: { capability: "agents:use", routeAvailable: false },
@@ -1110,6 +1160,43 @@ export function registerAgentsCommand(program) {
1110
1160
  // `hq agents --company acme list`.
1111
1161
  const companyOf = (sub) => sub.opts().company ??
1112
1162
  agents.opts().company;
1163
+ agents
1164
+ .command("models")
1165
+ .description("List the model catalog available to cloud agents")
1166
+ .option("--provider <provider>", "Filter to one provider: codex, grok, or claude")
1167
+ .option("--json", "Print the stable catalog JSON shape")
1168
+ .action(async (opts) => {
1169
+ try {
1170
+ const token = (await resolveVaultCredential()).token;
1171
+ const result = await readAgentModelCatalog(token);
1172
+ if (!result.ok) {
1173
+ if (result.reason === "request_failed") {
1174
+ throw new Error(`Could not read the model catalog (HTTP ${result.status}). Check your hq login and try again.`);
1175
+ }
1176
+ console.error(chalk.red(catalogUnavailableMessage()));
1177
+ process.exit(1);
1178
+ }
1179
+ if (result.source === "cache") {
1180
+ console.error(chalk.yellow(`Model catalog unavailable; showing cached catalog from ${new Date(result.fetchedAt).toISOString()}.`));
1181
+ }
1182
+ const providers = opts.provider
1183
+ ? { [opts.provider]: result.catalog.providers[opts.provider] }
1184
+ : result.catalog.providers;
1185
+ if (opts.json) {
1186
+ // Stable shape: { generatedAt, providers }. A provider filter selects
1187
+ // one key but never changes or flattens the response envelope.
1188
+ process.stdout.write(JSON.stringify({ generatedAt: result.catalog.generatedAt, providers }, null, 2) + "\n");
1189
+ return;
1190
+ }
1191
+ console.log(formatAgentModelsTable(result.catalog, opts.provider));
1192
+ }
1193
+ catch (err) {
1194
+ fail(err);
1195
+ }
1196
+ });
1197
+ agents.commands.find((command) => command.name() === "models")
1198
+ ?.options.find((option) => option.long === "--provider")
1199
+ ?.choices(AGENT_MODEL_PROVIDERS);
1113
1200
  agents
1114
1201
  .command("message <agent> <message...>")
1115
1202
  .alias("msg")
@@ -1267,7 +1354,7 @@ export function registerAgentsCommand(program) {
1267
1354
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1268
1355
  .option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
1269
1356
  .option("--provider <provider>", "Runtime: agents-v2 (recommended) | codex | grok | claude (accepted for script compatibility; use agents-v2 for new Claude agents)")
1270
- .option("--model <model>", "Brain model, for example claude-opus-5-5, gpt-5.5, or grok-4.6")
1357
+ .option("--model <model>", "Brain model from `hq agents models`")
1271
1358
  .option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
1272
1359
  .option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
1273
1360
  .option("--slack-bot-token <xoxb-token>", "Reuse an existing Slack bot token; HQ_SLACK_BOT_TOKEN or --slack-tokens-stdin avoid shell history")
@@ -1281,6 +1368,12 @@ export function registerAgentsCommand(program) {
1281
1368
  .addHelpText("after", `\n${claudeProvisionGuidance()}\n`)
1282
1369
  .action(async function (name, opts) {
1283
1370
  try {
1371
+ const slug = opts.slug ?? slugifyAgentName(name);
1372
+ if (!AGENT_SLUG_PATTERN.test(slug)) {
1373
+ console.error(chalk.red("Agent slug must start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens, and be at most 63 characters. " +
1374
+ "Update --slug or the agent name."));
1375
+ process.exit(1);
1376
+ }
1284
1377
  // Both of these previously fell back to their default on an
1285
1378
  // unrecognised value, silently provisioning a billable box the operator
1286
1379
  // did not ask for. They now exit(1) instead — see parseEnumOption.
@@ -1326,6 +1419,9 @@ export function registerAgentsCommand(program) {
1326
1419
  }
1327
1420
  const token = (await resolveVaultCredential()).token;
1328
1421
  const companyUid = await getCompanyUid(token, companyOf(this));
1422
+ if (opts.model !== undefined) {
1423
+ await validateAgentModelOption(token, provider, opts.model);
1424
+ }
1329
1425
  const idempotencyKey = `hq-cli-${randomUUID()}`;
1330
1426
  const sizeKey = parseEnumOption(opts.size, VALID_AGENT_SIZE_KEYS, "--size");
1331
1427
  const shouldChooseInteractively = !sizeKey && process.stdin.isTTY === true;
@@ -1341,7 +1437,6 @@ export function registerAgentsCommand(program) {
1341
1437
  // Every path confirms hq-pro's company-specific quote. Omission still
1342
1438
  // leaves the POST size implicit, preserving the server-side default.
1343
1439
  confirmAgentCreateQuoteOrExit(quotedSize, opts.yes, opts.json);
1344
- const slug = opts.slug ?? slugifyAgentName(name);
1345
1440
  try {
1346
1441
  const result = await provisionAgent(token, {
1347
1442
  companyUid,
@@ -1629,9 +1724,10 @@ export function registerAgentsCommand(program) {
1629
1724
  });
1630
1725
  agents
1631
1726
  .command("config <agentUid>")
1727
+ .alias("update")
1632
1728
  .description("Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)")
1633
1729
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1634
- .option("--model <model>", "Codex model id")
1730
+ .option("--model <model>", "Model id from `hq agents models`")
1635
1731
  .option("--effort <effort>", "Reasoning effort: minimal | low | medium | high | xhigh")
1636
1732
  .option("--tier <tier>", "Service tier (speed): default | priority")
1637
1733
  .option("--provider <provider>", "Migrate brain/runtime provider (codex | grok | claude | agents-v2). DESTRUCTIVE: terminates and reprovisions the box; requires --model and --yes.")
@@ -1700,6 +1796,9 @@ export function registerAgentsCommand(program) {
1700
1796
  process.exit(1);
1701
1797
  }
1702
1798
  const token = (await resolveVaultCredential()).token;
1799
+ if (opts.model !== undefined) {
1800
+ await validateAgentModelOption(token, opts.provider?.trim().toLowerCase(), opts.model);
1801
+ }
1703
1802
  const result = await patchAgentRuntimeConfig(token, agentUid, patch);
1704
1803
  if (opts.json) {
1705
1804
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
@@ -9,6 +9,7 @@ import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext,
9
9
  const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL ?? "https://api.indigo-hq.com";
10
10
  const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
11
11
  const MAX_REPORT_DISCOVERY_CHANNELS = 50;
12
+ const JOB_EXECUTION_DISABLED_MESSAGE = "Scheduled jobs are turned off for this company. A company owner can turn them back on.";
12
13
  function reportPath(channelId, reportId) {
13
14
  return `/v1/notify/channels/${encodeURIComponent(channelId)}/reports/${encodeURIComponent(reportId)}`;
14
15
  }
@@ -414,10 +415,39 @@ async function cancelOwnedJob(token, agentUid, jobId) {
414
415
  if (!res.ok && res.status !== 404)
415
416
  throw new ReportPublishError("Could not remove the report job.");
416
417
  }
418
+ async function requireScheduledJobsEnabled(token, agentUid) {
419
+ let executionEnabled;
420
+ try {
421
+ const res = await vaultApiFetch({
422
+ token,
423
+ path: `/v1/agents/${encodeURIComponent(agentUid)}/jobs`,
424
+ method: "GET",
425
+ });
426
+ if (!res.ok) {
427
+ console.debug("Scheduled job preflight was unavailable; proceeding with job creation.", { status: res.status });
428
+ return;
429
+ }
430
+ const body = await res.json();
431
+ if (body && typeof body === "object" && !Array.isArray(body)) {
432
+ executionEnabled = body.executionEnabled;
433
+ }
434
+ }
435
+ catch (error) {
436
+ console.debug("Scheduled job preflight failed; proceeding with job creation.", error);
437
+ return;
438
+ }
439
+ if (executionEnabled === false) {
440
+ throw new ReportPublishError(JOB_EXECUTION_DISABLED_MESSAGE, "job-execution-disabled");
441
+ }
442
+ if (executionEnabled !== true) {
443
+ console.debug("Scheduled job preflight did not return a boolean executionEnabled; proceeding with job creation.");
444
+ }
445
+ }
417
446
  async function adoptAction(reportId, options) {
418
447
  const token = await ensureCognitoToken();
419
448
  const report = await resolveReport(token, reportId, options.channel);
420
449
  const updated = await adoptReport(report, {
450
+ preflightJobCreation: () => requireScheduledJobsEnabled(token, report.ownerAgentUid),
421
451
  cancelJob: (jobId) => cancelOwnedJob(token, report.ownerAgentUid, jobId),
422
452
  createJob: async (input) => {
423
453
  const res = await vaultApiFetch({
@@ -426,8 +456,21 @@ async function adoptAction(reportId, options) {
426
456
  method: "POST",
427
457
  body: input,
428
458
  });
429
- if (!res.ok)
459
+ if (!res.ok) {
460
+ if (res.status === 409) {
461
+ let rejection;
462
+ try {
463
+ rejection = await res.json();
464
+ }
465
+ catch {
466
+ // A non-JSON conflict response is still the generic install failure.
467
+ }
468
+ if (rejection?.code === "JOB_EXECUTION_DISABLED") {
469
+ throw new ReportPublishError(JOB_EXECUTION_DISABLED_MESSAGE, "job-execution-disabled");
470
+ }
471
+ }
430
472
  throw new ReportPublishError("Could not install the report job.");
473
+ }
431
474
  const body = await res.json();
432
475
  const jobId = body.job?.jobId ?? body.jobId;
433
476
  if (!jobId)
@@ -0,0 +1,61 @@
1
+ export declare const AGENT_MODEL_PROVIDERS: readonly ["codex", "grok", "claude"];
2
+ export type AgentModelProvider = (typeof AGENT_MODEL_PROVIDERS)[number];
3
+ export interface AgentCatalogModel {
4
+ id: string;
5
+ label: string;
6
+ visibility: string;
7
+ isNew: boolean;
8
+ authModes: string[];
9
+ efforts: string[];
10
+ selectable: boolean;
11
+ unavailableReason?: string | null;
12
+ }
13
+ export interface AgentCatalogProvider {
14
+ models: AgentCatalogModel[];
15
+ defaultModel: string;
16
+ }
17
+ export interface AgentModelCatalog {
18
+ providers: Record<AgentModelProvider, AgentCatalogProvider>;
19
+ generatedAt: string;
20
+ }
21
+ export type AgentModelCatalogResult = {
22
+ ok: true;
23
+ catalog: AgentModelCatalog;
24
+ etag: string;
25
+ fetchedAt: number;
26
+ source: "network" | "cache";
27
+ } | {
28
+ ok: false;
29
+ reason: "unavailable";
30
+ } | {
31
+ ok: false;
32
+ reason: "request_failed";
33
+ status: number;
34
+ };
35
+ export interface ReadAgentModelCatalogOptions {
36
+ baseUrl?: string;
37
+ cacheFile?: string;
38
+ now?: () => number;
39
+ request?: (headers: Record<string, string>, signal: AbortSignal) => Promise<Response>;
40
+ timeoutMs?: number;
41
+ }
42
+ /** Use the same effective state directory as the CLI's person-token cache. */
43
+ export declare function agentModelCatalogCacheFile(baseUrl?: string, home?: string, env?: NodeJS.ProcessEnv): string;
44
+ /** Parse the documented response without constraining forward-compatible string values. */
45
+ export declare function parseAgentModelCatalog(value: unknown): AgentModelCatalog | null;
46
+ /** Read the live endpoint, conditionally refresh it, and fall back to the last good cache. */
47
+ export declare function readAgentModelCatalog(token: string, options?: ReadAgentModelCatalogOptions): Promise<AgentModelCatalogResult>;
48
+ export type ModelSelectionResult = {
49
+ ok: true;
50
+ } | {
51
+ ok: false;
52
+ reason: "unknown" | "not_selectable";
53
+ provider?: AgentModelProvider;
54
+ suggestions: string[];
55
+ };
56
+ /** Check an explicit model against the selected provider or inferred agents-v2 brain. */
57
+ export declare function validateAgentModelSelection(catalog: AgentModelCatalog, modelId: string, chosenProvider?: string): ModelSelectionResult;
58
+ export declare function modelSelectionErrorMessage(modelId: string, result: Extract<ModelSelectionResult, {
59
+ ok: false;
60
+ }>): string;
61
+ //# sourceMappingURL=model-catalog.d.ts.map
@@ -0,0 +1,275 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { DEFAULT_VAULT_API_URL, personTokenCacheFile } from "../../utils/cognito-session.js";
6
+ import { vaultApiFetch } from "../../utils/vault-api.js";
7
+ export const AGENT_MODEL_PROVIDERS = ["codex", "grok", "claude"];
8
+ const MODEL_CATALOG_REQUEST_TIMEOUT_MS = 15_000;
9
+ /** Use the same effective state directory as the CLI's person-token cache. */
10
+ export function agentModelCatalogCacheFile(baseUrl = DEFAULT_VAULT_API_URL, home = os.homedir(), env = process.env) {
11
+ const scope = createHash("sha256").update(baseUrl).digest("hex").slice(0, 16);
12
+ return path.join(path.dirname(personTokenCacheFile(home, env)), `agent-model-catalog-${scope}.json`);
13
+ }
14
+ /** Parse the documented response without constraining forward-compatible string values. */
15
+ export function parseAgentModelCatalog(value) {
16
+ if (!isRecord(value) || !isRecord(value.providers))
17
+ return null;
18
+ if (typeof value.generatedAt !== "string" || !Number.isFinite(Date.parse(value.generatedAt))) {
19
+ return null;
20
+ }
21
+ const providers = {};
22
+ for (const provider of AGENT_MODEL_PROVIDERS) {
23
+ const rawProvider = value.providers[provider];
24
+ if (!isRecord(rawProvider) ||
25
+ !Array.isArray(rawProvider.models) ||
26
+ typeof rawProvider.defaultModel !== "string") {
27
+ return null;
28
+ }
29
+ const models = [];
30
+ for (const rawModel of rawProvider.models) {
31
+ if (!isRecord(rawModel) ||
32
+ typeof rawModel.id !== "string" ||
33
+ !/^[A-Za-z0-9._-]{1,128}$/.test(rawModel.id) ||
34
+ typeof rawModel.label !== "string" ||
35
+ typeof rawModel.visibility !== "string" ||
36
+ typeof rawModel.isNew !== "boolean" ||
37
+ !isStringArray(rawModel.authModes) ||
38
+ !isStringArray(rawModel.efforts) ||
39
+ typeof rawModel.selectable !== "boolean" ||
40
+ (rawModel.unavailableReason !== undefined &&
41
+ rawModel.unavailableReason !== null &&
42
+ typeof rawModel.unavailableReason !== "string")) {
43
+ return null;
44
+ }
45
+ models.push({
46
+ id: rawModel.id,
47
+ label: rawModel.label,
48
+ visibility: rawModel.visibility,
49
+ isNew: rawModel.isNew,
50
+ authModes: rawModel.authModes,
51
+ efforts: rawModel.efforts,
52
+ selectable: rawModel.selectable,
53
+ ...(rawModel.unavailableReason !== undefined
54
+ ? { unavailableReason: rawModel.unavailableReason }
55
+ : {}),
56
+ });
57
+ }
58
+ if (!models.some((model) => model.id === rawProvider.defaultModel && model.selectable)) {
59
+ return null;
60
+ }
61
+ providers[provider] = { models, defaultModel: rawProvider.defaultModel };
62
+ }
63
+ return { providers, generatedAt: value.generatedAt };
64
+ }
65
+ /** Read the live endpoint, conditionally refresh it, and fall back to the last good cache. */
66
+ export async function readAgentModelCatalog(token, options = {}) {
67
+ const baseUrl = options.baseUrl ?? DEFAULT_VAULT_API_URL;
68
+ const cacheFile = options.cacheFile ?? agentModelCatalogCacheFile(baseUrl);
69
+ const now = options.now ?? Date.now;
70
+ const signal = AbortSignal.timeout(options.timeoutMs ?? MODEL_CATALOG_REQUEST_TIMEOUT_MS);
71
+ const cached = readCache(cacheFile);
72
+ const headers = cached
73
+ ? { "If-None-Match": cached.etag }
74
+ : {};
75
+ let response;
76
+ try {
77
+ response = options.request
78
+ ? await options.request(headers, signal)
79
+ : await vaultApiFetch({ token, baseUrl, path: "/v1/agents/models", headers, signal });
80
+ }
81
+ catch {
82
+ return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
83
+ }
84
+ if (response.status === 304) {
85
+ if (!cached)
86
+ return { ok: false, reason: "unavailable" };
87
+ const refreshed = {
88
+ ...cached,
89
+ etag: response.headers.get("etag") ?? cached.etag,
90
+ fetchedAt: now(),
91
+ };
92
+ if (isStrongEtag(refreshed.etag))
93
+ writeCache(cacheFile, refreshed);
94
+ return fromCache(refreshed, "network");
95
+ }
96
+ if (response.status === 404 || response.status === 429 || response.status >= 500) {
97
+ return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
98
+ }
99
+ if (!response.ok) {
100
+ return { ok: false, reason: "request_failed", status: response.status };
101
+ }
102
+ try {
103
+ const catalog = parseAgentModelCatalog(await response.json());
104
+ const etag = response.headers.get("etag");
105
+ if (!catalog || !etag || !isStrongEtag(etag)) {
106
+ return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
107
+ }
108
+ const entry = {
109
+ version: 1,
110
+ catalog,
111
+ etag,
112
+ fetchedAt: now(),
113
+ };
114
+ writeCache(cacheFile, entry);
115
+ return { ok: true, ...entry, source: "network" };
116
+ }
117
+ catch {
118
+ return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
119
+ }
120
+ }
121
+ /** Check an explicit model against the selected provider or inferred agents-v2 brain. */
122
+ export function validateAgentModelSelection(catalog, modelId, chosenProvider) {
123
+ const provider = isCatalogProvider(chosenProvider) ? chosenProvider : undefined;
124
+ const inferred = providerFromModelId(modelId);
125
+ const providers = provider
126
+ ? [provider]
127
+ : inferred
128
+ ? [inferred]
129
+ : [...AGENT_MODEL_PROVIDERS];
130
+ const rows = providers.flatMap((name) => catalog.providers[name].models.map((model) => ({ provider: name, model })));
131
+ const exact = rows.find(({ model }) => model.id.toLowerCase() === modelId.toLowerCase());
132
+ if (exact?.model.selectable)
133
+ return { ok: true };
134
+ const suggestions = closestSelectableModels(modelId, rows.filter(({ model }) => model.selectable).map(({ model }) => model.id));
135
+ return {
136
+ ok: false,
137
+ reason: exact ? "not_selectable" : "unknown",
138
+ ...(providers.length === 1 ? { provider: providers[0] } : {}),
139
+ suggestions,
140
+ };
141
+ }
142
+ export function modelSelectionErrorMessage(modelId, result) {
143
+ const scope = result.provider ? `the ${result.provider} catalog` : "the model catalog";
144
+ const problem = result.reason === "not_selectable" ? "not selectable in" : "not listed in";
145
+ const suggestion = result.suggestions.length
146
+ ? ` Close matches: ${result.suggestions.join(", ")}.`
147
+ : "";
148
+ const command = result.provider
149
+ ? ` Run \`hq agents models --provider ${result.provider}\` to see available models.`
150
+ : " Run `hq agents models` to see available models.";
151
+ return `Model '${modelId}' is ${problem} ${scope}.${suggestion}${command}`;
152
+ }
153
+ function providerFromModelId(modelId) {
154
+ const normalized = modelId.toLowerCase();
155
+ if (normalized.startsWith("gpt-"))
156
+ return "codex";
157
+ if (normalized.startsWith("grok-"))
158
+ return "grok";
159
+ if (normalized.startsWith("claude-"))
160
+ return "claude";
161
+ return undefined;
162
+ }
163
+ function closestSelectableModels(modelId, candidates) {
164
+ const needle = modelId.toLowerCase();
165
+ return candidates
166
+ .map((id) => ({
167
+ id,
168
+ distance: editDistance(needle, id.toLowerCase()),
169
+ prefix: commonPrefixLength(needle, id.toLowerCase()),
170
+ }))
171
+ .filter(({ distance, id }) => distance <= Math.max(2, Math.floor(needle.length / 4)) ||
172
+ id.toLowerCase().startsWith(needle))
173
+ .sort((a, b) => a.distance - b.distance || b.prefix - a.prefix || a.id.localeCompare(b.id))
174
+ .slice(0, 3)
175
+ .map(({ id }) => id);
176
+ }
177
+ function editDistance(left, right) {
178
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
179
+ for (let i = 1; i <= left.length; i += 1) {
180
+ const current = [i];
181
+ for (let j = 1; j <= right.length; j += 1) {
182
+ current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1));
183
+ }
184
+ previous = current;
185
+ }
186
+ return previous[right.length];
187
+ }
188
+ function commonPrefixLength(left, right) {
189
+ let index = 0;
190
+ while (index < left.length && index < right.length && left[index] === right[index])
191
+ index += 1;
192
+ return index;
193
+ }
194
+ function readCache(file) {
195
+ let serialized;
196
+ try {
197
+ serialized = fs.readFileSync(file, "utf8");
198
+ }
199
+ catch (error) {
200
+ if (isMissingFile(error))
201
+ return null;
202
+ console.warn("Unable to read the model catalog cache; continuing without cached data.");
203
+ return null;
204
+ }
205
+ let value;
206
+ try {
207
+ value = JSON.parse(serialized);
208
+ }
209
+ catch {
210
+ console.warn("Ignoring an invalid model catalog cache file.");
211
+ return null;
212
+ }
213
+ if (!isRecord(value) || value.version !== 1 || !isStrongEtag(value.etag)) {
214
+ console.warn("Ignoring an invalid model catalog cache file.");
215
+ return null;
216
+ }
217
+ if (typeof value.fetchedAt !== "number" || !Number.isFinite(value.fetchedAt)) {
218
+ console.warn("Ignoring an invalid model catalog cache file.");
219
+ return null;
220
+ }
221
+ const catalog = parseAgentModelCatalog(value.catalog);
222
+ if (!catalog) {
223
+ console.warn("Ignoring an invalid model catalog cache file.");
224
+ return null;
225
+ }
226
+ return { version: 1, catalog, etag: value.etag, fetchedAt: value.fetchedAt };
227
+ }
228
+ function writeCache(file, cache) {
229
+ const temporary = `${file}.${process.pid}.${cache.fetchedAt}.tmp`;
230
+ try {
231
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
232
+ fs.writeFileSync(temporary, JSON.stringify(cache), { encoding: "utf8", mode: 0o600 });
233
+ fs.renameSync(temporary, file);
234
+ }
235
+ catch {
236
+ console.warn("Unable to save the model catalog cache; offline use may be unavailable.");
237
+ try {
238
+ fs.rmSync(temporary, { force: true });
239
+ }
240
+ catch (error) {
241
+ if (!isMissingFile(error)) {
242
+ console.warn("Unable to remove a temporary model catalog cache file.");
243
+ }
244
+ }
245
+ }
246
+ }
247
+ function fromCache(cache, source = "cache") {
248
+ return {
249
+ ok: true,
250
+ catalog: cache.catalog,
251
+ etag: cache.etag,
252
+ fetchedAt: cache.fetchedAt,
253
+ source,
254
+ };
255
+ }
256
+ function isStrongEtag(value) {
257
+ return typeof value === "string" &&
258
+ value.length >= 2 &&
259
+ value.startsWith('"') &&
260
+ value.endsWith('"') &&
261
+ !/[\r\n]/.test(value);
262
+ }
263
+ function isMissingFile(error) {
264
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
265
+ }
266
+ function isCatalogProvider(value) {
267
+ return value !== undefined && AGENT_MODEL_PROVIDERS.includes(value);
268
+ }
269
+ function isStringArray(value) {
270
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
271
+ }
272
+ function isRecord(value) {
273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
274
+ }
275
+ //# sourceMappingURL=model-catalog.js.map
@@ -1,6 +1,8 @@
1
1
  import { type EmbedLockDependencies } from "../search-index/embed-lock.js";
2
2
  import { type LoadGateDependencies } from "../search-index/load-gate.js";
3
3
  import { type UtilityIo } from "./common.js";
4
+ export declare const QMD_REINDEX_STARTUP_WARNING_FLAG = "qmd.reindex-startup-warning";
5
+ export declare const QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK = false;
4
6
  export type QmdReindexOptions = UtilityIo & {
5
7
  cwd?: string;
6
8
  /** Test seams; default to the real search-index implementations. */
@@ -17,6 +19,8 @@ export type QmdReindexOptions = UtilityIo & {
17
19
  embedLockDependencies?: EmbedLockDependencies;
18
20
  /** Test seams for the host load gate consulted while the embed lock is held. */
19
21
  loadGateDependencies?: LoadGateDependencies;
22
+ /** Test seam for the hq-flags-gated warning on qmd native startup failures. */
23
+ startupFailureWarningsEnabled?: () => boolean;
20
24
  };
21
25
  export declare function qmdReindexAfterSync(args?: string[], options?: QmdReindexOptions): number;
22
26
  //# sourceMappingURL=qmd-reindex-after-sync.d.ts.map
@@ -15,7 +15,14 @@ import { reconcileCollections, resolveQmdBin, resolveQmdHome, runQmd } from "../
15
15
  import { applyIndexSizeLimit } from "../search-index/max-doc-bytes.js";
16
16
  import { defaultEmbedLockDependencies, runWithEmbedLock, } from "../search-index/embed-lock.js";
17
17
  import { LoadGateConfigurationError, checkEmbedLoad, defaultLoadGateDependencies, recordSuccessfulEmbed, } from "../search-index/load-gate.js";
18
+ import { resolveProcessFlagGate } from "../flag-registry.js";
19
+ import { isQmdNativeBindingError, QMD_NATIVE_BINDING_REMEDY, } from "../../utils/qmd-native-binding-error.js";
18
20
  import { ioFor, line } from "./common.js";
21
+ export const QMD_REINDEX_STARTUP_WARNING_FLAG = "qmd.reindex-startup-warning";
22
+ export const QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK = false;
23
+ function qmdStartupFailureWarningsEnabled() {
24
+ return resolveProcessFlagGate(QMD_REINDEX_STARTUP_WARNING_FLAG, { fallback: QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK }, () => QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK);
25
+ }
19
26
  export function qmdReindexAfterSync(args = [], options = {}) {
20
27
  let hqRoot = "";
21
28
  let embed = false;
@@ -42,6 +49,15 @@ export function qmdReindexAfterSync(args = [], options = {}) {
42
49
  const run = options.run ?? ((argv, opts) => runQmd(argv, opts));
43
50
  const sizeLimit = options.sizeLimit ?? ((root) => applyIndexSizeLimit(root));
44
51
  const { stderr } = ioFor(options);
52
+ let startupFailureWarningShown = false;
53
+ const warnOnNativeStartupFailure = (error) => {
54
+ if (startupFailureWarningShown ||
55
+ !isQmdNativeBindingError(error) ||
56
+ !(options.startupFailureWarningsEnabled ?? qmdStartupFailureWarningsEnabled)())
57
+ return;
58
+ startupFailureWarningShown = true;
59
+ line(stderr, `hq: search index was not updated after sync; ${QMD_NATIVE_BINDING_REMEDY}`);
60
+ };
45
61
  // Every qmd interaction is best-effort: the shell suffixed each with `|| true`.
46
62
  try {
47
63
  reconcile(hqRoot, { bin });
@@ -62,7 +78,8 @@ export function qmdReindexAfterSync(args = [], options = {}) {
62
78
  try {
63
79
  run(["update"], { bin, cwd: hqRoot });
64
80
  }
65
- catch {
81
+ catch (error) {
82
+ warnOnNativeStartupFailure(error);
66
83
  /* never fail a sync on index freshness */
67
84
  }
68
85
  if (embed) {
@@ -91,6 +108,7 @@ export function qmdReindexAfterSync(args = [], options = {}) {
91
108
  catch (error) {
92
109
  if (error instanceof LoadGateConfigurationError)
93
110
  line(stderr, `hq: ${error.message}`);
111
+ warnOnNativeStartupFailure(error);
94
112
  /* embeddings are deferred-cost and optional */
95
113
  }
96
114
  }
@@ -106,6 +106,7 @@ export declare function ownerFailureNotice(input: {
106
106
  body: string;
107
107
  } | null;
108
108
  export interface ReportAdoptDependencies {
109
+ preflightJobCreation(): Promise<void>;
109
110
  createJob(input: {
110
111
  prompt: string;
111
112
  schedule: {
@@ -124,8 +125,8 @@ export interface ReportAdoptDependencies {
124
125
  patch(report: ChannelReport, patch: ReportPatch): Promise<ChannelReport>;
125
126
  }
126
127
  export declare class ReportPublishError extends Error {
127
- readonly code: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "revision-conflict";
128
- constructor(message: string, code?: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "revision-conflict");
128
+ readonly code: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "job-execution-disabled" | "revision-conflict";
129
+ constructor(message: string, code?: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "job-execution-disabled" | "revision-conflict");
129
130
  }
130
131
  /** A deliberately boring, token-safe failure reason stored on the report. */
131
132
  export declare function plainFailureReason(error: unknown): string;
@@ -41,7 +41,7 @@ export class ReportPublishError extends Error {
41
41
  /** A deliberately boring, token-safe failure reason stored on the report. */
42
42
  export function plainFailureReason(error) {
43
43
  if (error instanceof ReportPublishError) {
44
- if (error.code === "source-invalid" || error.code === "job-failed" || error.code === "revision-conflict")
44
+ if (error.code === "source-invalid" || error.code === "job-failed" || error.code === "job-execution-disabled" || error.code === "revision-conflict")
45
45
  return error.message.slice(0, 240);
46
46
  if (error.code === "already-running")
47
47
  return "already running";
@@ -231,9 +231,11 @@ export async function adoptReport(report, deps) {
231
231
  throw new ReportPublishError("Report schedule is missing.", "source-invalid");
232
232
  }
233
233
  try {
234
+ const cron = reportJobCron(schedule);
235
+ if (cron)
236
+ await deps.preflightJobCreation();
234
237
  if (report.jobId)
235
238
  await deps.cancelJob(report.jobId);
236
- const cron = reportJobCron(schedule);
237
239
  if (!cron) {
238
240
  return deps.patch(report, { jobId: null, jobState: "adopted", jobStateReason: null });
239
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.190.0",
3
+ "version": "5.192.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {