@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 +24 -0
- package/dist/command-catalog.generated.d.ts +18 -3
- package/dist/command-catalog.generated.js +24 -3
- package/dist/commands/agents.d.ts +3 -0
- package/dist/commands/agents.js +103 -4
- package/dist/commands/report.js +44 -1
- package/dist/lib/agents/model-catalog.d.ts +61 -0
- package/dist/lib/agents/model-catalog.js +275 -0
- package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +4 -0
- package/dist/lib/core-utils/qmd-reindex-after-sync.js +19 -1
- package/dist/lib/report-publish.d.ts +3 -2
- package/dist/lib/report-publish.js +4 -2
- package/package.json +1 -1
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
|
|
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: "
|
|
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
|
|
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": "
|
|
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 {
|
package/dist/commands/agents.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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>", "
|
|
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");
|
package/dist/commands/report.js
CHANGED
|
@@ -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
|
}
|