@indigoai-us/hq-cli 5.191.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,18 @@
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
+
5
17
  ## [5.191.0] — 2026-09-25
6
18
 
7
19
  ### Fixed
@@ -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([
@@ -57,11 +58,12 @@ export const VALID_AUTH_MODES = new Set(["subscription", "apiKey"]);
57
58
  export const VALID_AGENT_SIZE_KEYS = new Set(["basic", "power", "dev"]);
58
59
  // Keep cloud agent create slugs aligned with hq-pro-agents POST /v1/agents.
59
60
  const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
60
- const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model claude-opus-5-5";
61
+ const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model <model-id>";
61
62
  const CLAUDE_API_KEY_ARGS = "--auth-mode apiKey --api-key-env <VAR>";
62
63
  function claudeProvisionGuidance() {
63
64
  return [
64
65
  "The account must be enabled for Claude. Subscription auth is the default.",
66
+ "Use `hq agents models` to see the current model ids.",
65
67
  `Create an agent with: ${CLAUDE_PROVISION_COMMAND}`,
66
68
  `For API-key auth, add: ${CLAUDE_API_KEY_ARGS}`,
67
69
  "The CLI accepts --provider claude for script compatibility, but the server rejects new agents that use it.",
@@ -863,6 +865,50 @@ export function appliedHint(applied) {
863
865
  ? "applied to the running box now"
864
866
  : "saved — takes effect on the agent's next launch";
865
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
+ }
866
912
  /**
867
913
  * Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
868
914
  * directly; anything else is treated as a slug or display name and resolved
@@ -1097,6 +1143,8 @@ export function registerAgentsCommand(program) {
1097
1143
  bySubcommand: {
1098
1144
  message: { capability: "agents:use", routeAvailable: false },
1099
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 },
1100
1148
  // Interactive terminals are for humans: hq-pro serves no keyed
1101
1149
  // `/v1/keys/agents/{uid}/terminal` route, so machine keys fail closed.
1102
1150
  terminal: { capability: "agents:use", routeAvailable: false },
@@ -1112,6 +1160,43 @@ export function registerAgentsCommand(program) {
1112
1160
  // `hq agents --company acme list`.
1113
1161
  const companyOf = (sub) => sub.opts().company ??
1114
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);
1115
1200
  agents
1116
1201
  .command("message <agent> <message...>")
1117
1202
  .alias("msg")
@@ -1269,7 +1354,7 @@ export function registerAgentsCommand(program) {
1269
1354
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1270
1355
  .option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
1271
1356
  .option("--provider <provider>", "Runtime: agents-v2 (recommended) | codex | grok | claude (accepted for script compatibility; use agents-v2 for new Claude agents)")
1272
- .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`")
1273
1358
  .option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
1274
1359
  .option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
1275
1360
  .option("--slack-bot-token <xoxb-token>", "Reuse an existing Slack bot token; HQ_SLACK_BOT_TOKEN or --slack-tokens-stdin avoid shell history")
@@ -1334,6 +1419,9 @@ export function registerAgentsCommand(program) {
1334
1419
  }
1335
1420
  const token = (await resolveVaultCredential()).token;
1336
1421
  const companyUid = await getCompanyUid(token, companyOf(this));
1422
+ if (opts.model !== undefined) {
1423
+ await validateAgentModelOption(token, provider, opts.model);
1424
+ }
1337
1425
  const idempotencyKey = `hq-cli-${randomUUID()}`;
1338
1426
  const sizeKey = parseEnumOption(opts.size, VALID_AGENT_SIZE_KEYS, "--size");
1339
1427
  const shouldChooseInteractively = !sizeKey && process.stdin.isTTY === true;
@@ -1636,9 +1724,10 @@ export function registerAgentsCommand(program) {
1636
1724
  });
1637
1725
  agents
1638
1726
  .command("config <agentUid>")
1727
+ .alias("update")
1639
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)")
1640
1729
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1641
- .option("--model <model>", "Codex model id")
1730
+ .option("--model <model>", "Model id from `hq agents models`")
1642
1731
  .option("--effort <effort>", "Reasoning effort: minimal | low | medium | high | xhigh")
1643
1732
  .option("--tier <tier>", "Service tier (speed): default | priority")
1644
1733
  .option("--provider <provider>", "Migrate brain/runtime provider (codex | grok | claude | agents-v2). DESTRUCTIVE: terminates and reprovisions the box; requires --model and --yes.")
@@ -1707,6 +1796,9 @@ export function registerAgentsCommand(program) {
1707
1796
  process.exit(1);
1708
1797
  }
1709
1798
  const token = (await resolveVaultCredential()).token;
1799
+ if (opts.model !== undefined) {
1800
+ await validateAgentModelOption(token, opts.provider?.trim().toLowerCase(), opts.model);
1801
+ }
1710
1802
  const result = await patchAgentRuntimeConfig(token, agentUid, patch);
1711
1803
  if (opts.json) {
1712
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
@@ -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.191.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": {