@indigoai-us/hq-cli 5.345.170 → 5.345.172
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 +84 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +206 -7
- package/dist/command-catalog.generated.js +252 -8
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +179 -18
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +228 -10
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +124 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +142 -0
- package/dist/lib/bot/fleet-local.js +689 -0
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -24
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/sync-core/engine-home.d.ts +44 -0
- package/dist/lib/sync-core/engine-home.js +193 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +196 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +140 -0
- package/dist/lib/sync-core/rollout-state.d.ts +24 -0
- package/dist/lib/sync-core/rollout-state.js +94 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
- package/dist/lib/daemon/sync-core-canary.js +0 -56
|
@@ -74,7 +74,11 @@ export interface AdminConnection {
|
|
|
74
74
|
grantCount?: number;
|
|
75
75
|
};
|
|
76
76
|
installation?: FactoryInstallation | null;
|
|
77
|
+
ownerType?: ConnectionOwnerType;
|
|
78
|
+
ownerId?: string;
|
|
79
|
+
accountLabel?: string;
|
|
77
80
|
}
|
|
81
|
+
export type ConnectionOwnerType = "person" | "company" | "legacy" | "invalid";
|
|
78
82
|
/**
|
|
79
83
|
* The actionable state returned instead of attempting to use a revoked
|
|
80
84
|
* connection. Keep this machine-readable so commands and scripts get the same
|
|
@@ -111,6 +115,9 @@ export interface AdminSurface {
|
|
|
111
115
|
*/
|
|
112
116
|
canAddIntegrations?: boolean;
|
|
113
117
|
};
|
|
118
|
+
capabilities?: {
|
|
119
|
+
personalConnect?: boolean;
|
|
120
|
+
};
|
|
114
121
|
connections: AdminConnection[];
|
|
115
122
|
audit: AdminAuditEvent[];
|
|
116
123
|
}
|
|
@@ -390,4 +397,20 @@ export declare function printIntegrationCallResultJson(value: unknown): void;
|
|
|
390
397
|
* a user to fix something that is not broken.
|
|
391
398
|
*/
|
|
392
399
|
export declare function printHealthNotice(connection: AdminConnection): void;
|
|
400
|
+
export declare function connectionOwnerLabel(connection: Pick<AdminConnection, "ownerType">): string | undefined;
|
|
401
|
+
export declare function connectionAccountLabel(connection: Pick<AdminConnection, "accountLabel">): string | undefined;
|
|
402
|
+
export interface IntegrationAccountCandidate {
|
|
403
|
+
connectionId: string;
|
|
404
|
+
provider: string;
|
|
405
|
+
ownerType?: string;
|
|
406
|
+
accountLabel?: string;
|
|
407
|
+
}
|
|
408
|
+
export declare const ACCOUNT_REQUIRED_CODE = "AccountRequired";
|
|
409
|
+
export declare const ACCOUNT_NOT_FOUND_CODE = "IntegrationAccountNotFound";
|
|
410
|
+
export declare function accountSelectionMessage(code: typeof ACCOUNT_REQUIRED_CODE | typeof ACCOUNT_NOT_FOUND_CODE, candidates: readonly IntegrationAccountCandidate[], account?: string, rerun?: string): string;
|
|
411
|
+
export declare function accountCandidateFor(connection: AdminConnection): IntegrationAccountCandidate;
|
|
412
|
+
export declare function selectAccountConnections(connections: readonly AdminConnection[], opts: {
|
|
413
|
+
account: string;
|
|
414
|
+
provider?: string;
|
|
415
|
+
}): AdminConnection[];
|
|
393
416
|
//# sourceMappingURL=integrations-core.d.ts.map
|
|
@@ -397,6 +397,9 @@ export async function fetchAdminSurface(token, companyUid) {
|
|
|
397
397
|
return {
|
|
398
398
|
companyUid: data.companyUid ?? companyUid,
|
|
399
399
|
...(data.factoryEnabled !== undefined ? { factoryEnabled: data.factoryEnabled } : {}),
|
|
400
|
+
...(data.capabilities && typeof data.capabilities === "object"
|
|
401
|
+
? { capabilities: data.capabilities }
|
|
402
|
+
: {}),
|
|
400
403
|
viewer: data.viewer ?? {
|
|
401
404
|
personUid: "",
|
|
402
405
|
role: "member",
|
|
@@ -717,6 +720,9 @@ export async function callGateway(token, params, idempotencyKey, method = "tools
|
|
|
717
720
|
throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status), status: res.status, gatewaySubcommand });
|
|
718
721
|
}
|
|
719
722
|
if (terminalMessage?.error) {
|
|
723
|
+
const accountError = accountSelectionError(terminalMessage.error, typeof params.account === "string" ? params.account : undefined);
|
|
724
|
+
if (accountError)
|
|
725
|
+
throw accountError;
|
|
720
726
|
const gatewayError = gatewayErrorDetails(terminalMessage.error);
|
|
721
727
|
const invalidEnvelope = gatewayError.rpcCode === undefined && !gatewayError.message;
|
|
722
728
|
const gatewayFailureCode = invalidEnvelope
|
|
@@ -890,4 +896,98 @@ export function printHealthNotice(connection) {
|
|
|
890
896
|
if (notice.remediation)
|
|
891
897
|
console.log(chalk.yellow(` ${redactUrlsInText(notice.remediation)}`));
|
|
892
898
|
}
|
|
899
|
+
const OWNER_LABELS = {
|
|
900
|
+
person: "Personal",
|
|
901
|
+
company: "Company",
|
|
902
|
+
legacy: "Shared (legacy)",
|
|
903
|
+
invalid: "Unknown owner",
|
|
904
|
+
};
|
|
905
|
+
export function connectionOwnerLabel(connection) {
|
|
906
|
+
const ownerType = connection.ownerType;
|
|
907
|
+
return ownerType && Object.hasOwn(OWNER_LABELS, ownerType) ? OWNER_LABELS[ownerType] : undefined;
|
|
908
|
+
}
|
|
909
|
+
export function connectionAccountLabel(connection) {
|
|
910
|
+
if (typeof connection.accountLabel !== "string")
|
|
911
|
+
return undefined;
|
|
912
|
+
const cleaned = safeAppName(connection.accountLabel, "");
|
|
913
|
+
return cleaned ? redactUrlsInText(cleaned) : undefined;
|
|
914
|
+
}
|
|
915
|
+
export const ACCOUNT_REQUIRED_CODE = "AccountRequired";
|
|
916
|
+
export const ACCOUNT_NOT_FOUND_CODE = "IntegrationAccountNotFound";
|
|
917
|
+
function accountCandidatesFrom(value) {
|
|
918
|
+
if (!Array.isArray(value))
|
|
919
|
+
return [];
|
|
920
|
+
const candidates = [];
|
|
921
|
+
for (const item of value) {
|
|
922
|
+
const record = recordValue(item);
|
|
923
|
+
if (!record || typeof record.connectionId !== "string" || typeof record.provider !== "string")
|
|
924
|
+
continue;
|
|
925
|
+
candidates.push({
|
|
926
|
+
connectionId: record.connectionId,
|
|
927
|
+
provider: record.provider,
|
|
928
|
+
...(typeof record.ownerType === "string" ? { ownerType: record.ownerType } : {}),
|
|
929
|
+
...(typeof record.accountLabel === "string" ? { accountLabel: record.accountLabel } : {}),
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return candidates;
|
|
933
|
+
}
|
|
934
|
+
function formatAccountCandidate(candidate) {
|
|
935
|
+
const id = safeAppName(candidate.connectionId, "?");
|
|
936
|
+
const owner = connectionOwnerLabel({ ownerType: candidate.ownerType });
|
|
937
|
+
const label = connectionAccountLabel({ accountLabel: candidate.accountLabel });
|
|
938
|
+
const details = [safeAppName(bareProvider(candidate.provider), "?"), owner, label].filter(Boolean).join(" · ");
|
|
939
|
+
return ` ${id} ${details}`;
|
|
940
|
+
}
|
|
941
|
+
export function accountSelectionMessage(code, candidates, account, rerun = "Rerun with --account <connection id or account label>.") {
|
|
942
|
+
const headline = code === ACCOUNT_REQUIRED_CODE
|
|
943
|
+
? `More than one account can run this. ${rerun}`
|
|
944
|
+
: account
|
|
945
|
+
? `No account matches '${safeAppName(account, "?")}'. ${rerun}`
|
|
946
|
+
: "No account you can use has this tool.";
|
|
947
|
+
if (candidates.length === 0)
|
|
948
|
+
return headline;
|
|
949
|
+
return [headline, "Accounts:", ...candidates.map(formatAccountCandidate)].join("\n");
|
|
950
|
+
}
|
|
951
|
+
function accountSelectionError(error, account) {
|
|
952
|
+
const data = recordValue(recordValue(error)?.data);
|
|
953
|
+
const code = data?.code;
|
|
954
|
+
if (code !== ACCOUNT_REQUIRED_CODE && code !== ACCOUNT_NOT_FOUND_CODE)
|
|
955
|
+
return null;
|
|
956
|
+
return new IntegrationsCliError(accountSelectionMessage(code, accountCandidatesFrom(data?.candidates), account), {
|
|
957
|
+
expected: true,
|
|
958
|
+
code,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
export function accountCandidateFor(connection) {
|
|
962
|
+
return {
|
|
963
|
+
connectionId: connection.id,
|
|
964
|
+
provider: connection.provider,
|
|
965
|
+
...(connection.ownerType ? { ownerType: connection.ownerType } : {}),
|
|
966
|
+
...(connection.accountLabel ? { accountLabel: connection.accountLabel } : {}),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function matchesProviderSelector(connection, provider) {
|
|
970
|
+
const want = provider.trim().toLowerCase();
|
|
971
|
+
const wantHumanSlug = humanSlug(provider);
|
|
972
|
+
const displayName = connection.installation?.displayName ?? "";
|
|
973
|
+
return (bareProvider(connection.provider).toLowerCase() === want ||
|
|
974
|
+
connection.provider.toLowerCase() === want ||
|
|
975
|
+
displayName.trim().toLowerCase() === want ||
|
|
976
|
+
(wantHumanSlug !== "" && humanSlug(displayName) === wantHumanSlug));
|
|
977
|
+
}
|
|
978
|
+
export function selectAccountConnections(connections, opts) {
|
|
979
|
+
const pool = connections.filter((connection) => connection.status !== "revoked" &&
|
|
980
|
+
(!opts.provider || matchesProviderSelector(connection, opts.provider)));
|
|
981
|
+
const want = opts.account.trim().toLowerCase();
|
|
982
|
+
const matches = pool.filter((connection) => connection.id.toLowerCase() === want ||
|
|
983
|
+
(typeof connection.accountLabel === "string" && connection.accountLabel.trim().toLowerCase() === want));
|
|
984
|
+
if (matches.length === 0) {
|
|
985
|
+
throw new IntegrationsCliError(accountSelectionMessage(ACCOUNT_NOT_FOUND_CODE, pool.map(accountCandidateFor), opts.account), { expected: true, code: ACCOUNT_NOT_FOUND_CODE });
|
|
986
|
+
}
|
|
987
|
+
const providers = new Set(matches.map((connection) => connection.provider));
|
|
988
|
+
if (providers.size > 1) {
|
|
989
|
+
throw new IntegrationsCliError(accountSelectionMessage(ACCOUNT_REQUIRED_CODE, matches.map(accountCandidateFor), opts.account, "Rerun with --provider <app> or --account <connection id>."), { expected: true, code: ACCOUNT_REQUIRED_CODE });
|
|
990
|
+
}
|
|
991
|
+
return matches;
|
|
992
|
+
}
|
|
893
993
|
//# sourceMappingURL=integrations-core.js.map
|
|
@@ -17,7 +17,7 @@ import chalk from "chalk";
|
|
|
17
17
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
18
18
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
19
19
|
import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
|
|
20
|
-
import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, fetchAuditPage, AUDIT_PAGE_MAX, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
20
|
+
import { IntegrationsCliError, bareProvider, printHealthNotice, connectionAccountLabel, connectionOwnerLabel, fetchConnections, fetchAdminSurface, fetchAuditPage, AUDIT_PAGE_MAX, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
21
21
|
import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, terminalSafeText, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
|
|
22
22
|
const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
|
|
23
23
|
const PERMISSIONS = ["read", "write", "admin"];
|
|
@@ -219,6 +219,13 @@ export function registerManageCommands(integrations) {
|
|
|
219
219
|
if (install?.id)
|
|
220
220
|
console.log(chalk.dim(` installation: ${install.id}`));
|
|
221
221
|
console.log(chalk.dim(` status: ${connection.status}`));
|
|
222
|
+
const owner = connectionOwnerLabel(connection);
|
|
223
|
+
if (owner) {
|
|
224
|
+
console.log(chalk.dim(` owner: ${owner}`));
|
|
225
|
+
const account = connectionAccountLabel(connection);
|
|
226
|
+
if (account)
|
|
227
|
+
console.log(chalk.dim(` account: ${account}`));
|
|
228
|
+
}
|
|
222
229
|
if (connection.status === "revoked") {
|
|
223
230
|
const details = revokedConnectionDetails(connection, opts.company);
|
|
224
231
|
console.log(chalk.yellow(` Revoked — ${details.reason}`));
|
|
@@ -56,7 +56,7 @@ import chalk from "chalk";
|
|
|
56
56
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
57
57
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
58
58
|
import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
|
|
59
|
-
import { IntegrationsCliError, bareProvider, callGateway, parseCallTimeoutMs, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
59
|
+
import { IntegrationsCliError, bareProvider, callGateway, parseCallTimeoutMs, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, connectionAccountLabel, connectionOwnerLabel, selectAccountConnections, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, accountCandidateFor, accountSelectionMessage, ACCOUNT_REQUIRED_CODE, } from "./integrations-core.js";
|
|
60
60
|
import { registerConnectCommands } from "./integrations-connect.js";
|
|
61
61
|
import { registerImportCommands } from "./integrations-import.js";
|
|
62
62
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
@@ -329,6 +329,26 @@ function approvalOutcomeFailed(row) {
|
|
|
329
329
|
return ["failed", "write_unknown"].includes(row.status ?? "") ||
|
|
330
330
|
["failed", "unknown"].includes(row.outcome ?? "");
|
|
331
331
|
}
|
|
332
|
+
function selectToolConnection(connections, opts) {
|
|
333
|
+
if (opts.account === undefined) {
|
|
334
|
+
return { connection: selectConnection(connections, opts), accountMatches: [] };
|
|
335
|
+
}
|
|
336
|
+
if (opts.connection) {
|
|
337
|
+
throw new IntegrationsCliError("Use either --connection or --account, not both.", {
|
|
338
|
+
expected: true,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (!opts.account.trim()) {
|
|
342
|
+
throw new IntegrationsCliError("--account was empty — pass a connection id or account label.", {
|
|
343
|
+
expected: true,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const accountMatches = selectAccountConnections(connections, {
|
|
347
|
+
account: opts.account,
|
|
348
|
+
...(opts.provider ? { provider: opts.provider } : {}),
|
|
349
|
+
});
|
|
350
|
+
return { connection: accountMatches[0], accountMatches };
|
|
351
|
+
}
|
|
332
352
|
export function registerIntegrationsCommand(program) {
|
|
333
353
|
const integrations = program
|
|
334
354
|
.command("integrations")
|
|
@@ -375,7 +395,10 @@ export function registerIntegrationsCommand(program) {
|
|
|
375
395
|
return;
|
|
376
396
|
}
|
|
377
397
|
for (const c of connections) {
|
|
398
|
+
const owner = connectionOwnerLabel(c);
|
|
378
399
|
const flags = [
|
|
400
|
+
owner ?? null,
|
|
401
|
+
owner ? connectionAccountLabel(c) ?? null : null,
|
|
379
402
|
c.status,
|
|
380
403
|
c.writePolicy ? `writes: ${c.writePolicy}` : null,
|
|
381
404
|
c.installation?.status === "needs_credentials" ? "needs sign-in" : null,
|
|
@@ -397,13 +420,17 @@ export function registerIntegrationsCommand(program) {
|
|
|
397
420
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
398
421
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
399
422
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
423
|
+
.option("--account <id-or-label>", "Which account of the app: its connection id or account label")
|
|
400
424
|
.option("--describe <tool>", "Print the full input schema for one tool")
|
|
401
425
|
.option("--json", "Machine-readable output")
|
|
402
426
|
.action(async (opts) => {
|
|
403
427
|
const token = await ensureCognitoIdToken();
|
|
404
428
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
405
429
|
const connections = await fetchConnections(token, companyUid);
|
|
406
|
-
const connection =
|
|
430
|
+
const { connection, accountMatches } = selectToolConnection(connections, opts);
|
|
431
|
+
if (accountMatches.length > 1) {
|
|
432
|
+
throw new IntegrationsCliError(accountSelectionMessage(ACCOUNT_REQUIRED_CODE, accountMatches.map(accountCandidateFor), opts.account, "Rerun with --account <connection id>."), { expected: true, code: ACCOUNT_REQUIRED_CODE });
|
|
433
|
+
}
|
|
407
434
|
if (reportRevokedConnection(connection, opts))
|
|
408
435
|
return;
|
|
409
436
|
let payload;
|
|
@@ -479,6 +506,7 @@ export function registerIntegrationsCommand(program) {
|
|
|
479
506
|
.description("Call one of a connected app's tools (approval-gated when it changes things)")
|
|
480
507
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
481
508
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
509
|
+
.option("--account <id-or-label>", "Which account of the app: its connection id or account label")
|
|
482
510
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
483
511
|
.option("--args <json>", "Tool arguments as a JSON object (default {}); use - to read the JSON from stdin")
|
|
484
512
|
.option("--args-file <path>", "Read tool arguments from a JSON file (use - for stdin); avoids shell quoting of apostrophes and parentheses")
|
|
@@ -490,9 +518,23 @@ export function registerIntegrationsCommand(program) {
|
|
|
490
518
|
const parsedArgs = await resolveCallArgs(opts);
|
|
491
519
|
const token = await ensureCognitoIdToken();
|
|
492
520
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
493
|
-
const connection =
|
|
521
|
+
const { connection, accountMatches } = selectToolConnection(await fetchConnections(token, companyUid), opts);
|
|
494
522
|
if (reportRevokedConnection(connection, { ...opts, fail: true }))
|
|
495
523
|
return;
|
|
524
|
+
const accountSelector = opts.account !== undefined &&
|
|
525
|
+
accountMatches.length !== 1 &&
|
|
526
|
+
(connection.ownerType === "person" || connection.ownerType === "company")
|
|
527
|
+
? opts.account.trim()
|
|
528
|
+
: undefined;
|
|
529
|
+
if (accountSelector === undefined && accountMatches.length > 1) {
|
|
530
|
+
throw new IntegrationsCliError(accountSelectionMessage(ACCOUNT_REQUIRED_CODE, accountMatches.map(accountCandidateFor), opts.account, "Rerun with --account <connection id>."), { expected: true, code: ACCOUNT_REQUIRED_CODE });
|
|
531
|
+
}
|
|
532
|
+
const routing = accountSelector === undefined
|
|
533
|
+
? { connectionId: connection.id }
|
|
534
|
+
: {};
|
|
535
|
+
const accountParams = accountSelector === undefined
|
|
536
|
+
? {}
|
|
537
|
+
: { account: accountSelector, provider: connection.provider };
|
|
496
538
|
const idempotencyKey = opts.idempotencyKey ?? `hq-cli-${randomUUID()}`;
|
|
497
539
|
let gatewayParams;
|
|
498
540
|
if (isBuiltInGatewayProvider(connection.provider)) {
|
|
@@ -506,7 +548,8 @@ export function registerIntegrationsCommand(program) {
|
|
|
506
548
|
companyUid,
|
|
507
549
|
acceptAsync: true,
|
|
508
550
|
name: tool,
|
|
509
|
-
|
|
551
|
+
...accountParams,
|
|
552
|
+
arguments: { ...parsedArgs, companyUid, ...routing, idempotencyKey },
|
|
510
553
|
};
|
|
511
554
|
}
|
|
512
555
|
else {
|
|
@@ -515,9 +558,10 @@ export function registerIntegrationsCommand(program) {
|
|
|
515
558
|
companyUid,
|
|
516
559
|
acceptAsync: true,
|
|
517
560
|
name: `${prefix}.mcp.tools.call`,
|
|
561
|
+
...accountParams,
|
|
518
562
|
arguments: {
|
|
519
563
|
companyUid,
|
|
520
|
-
|
|
564
|
+
...routing,
|
|
521
565
|
toolName: tool,
|
|
522
566
|
arguments: parsedArgs,
|
|
523
567
|
idempotencyKey,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Ctx, LaneResumeOptions } from "./lanes-shared.js";
|
|
2
|
+
import { LanesError } from "../lib/lanes/errors.js";
|
|
2
3
|
import type { LaneRecord } from "../lib/lanes/types.js";
|
|
3
4
|
/**
|
|
4
5
|
* A loop lane that finished a phase and is waiting for its next envelope is
|
|
@@ -14,6 +15,25 @@ export declare function settleLoopLaneIdle(hqRoot: string, laneId: string): Lane
|
|
|
14
15
|
* worker, so it is handed to resume as the `done` lane the phase left.
|
|
15
16
|
*/
|
|
16
17
|
export declare function prepareIdleLoopLaneForResume(hqRoot: string, laneId: string): LaneRecord;
|
|
18
|
+
/**
|
|
19
|
+
* LanesError codes from the resume admission path that mean admission could
|
|
20
|
+
* not run right now and a later admission can retry: the admission or store
|
|
21
|
+
* lock timed out (store_lock_timeout), the lane changed while admission was
|
|
22
|
+
* checked (lane_admission_conflict), or a lane record CAS miss
|
|
23
|
+
* (version_conflict). Any other resume error is permanent and is rethrown.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TRANSIENT_LOOP_ADMISSION_ERRORS: ReadonlySet<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Park a loop phase whose resume was refused before an engine launched. The
|
|
28
|
+
* lane becomes `queued` with `admit_failed` and the error, and carries the
|
|
29
|
+
* phase as `pending_resume`, so admission can launch it later and the lane is
|
|
30
|
+
* never left looking active with no engine.
|
|
31
|
+
*/
|
|
32
|
+
export declare function deferRefusedLoopPhase(hqRoot: string, laneId: string, at: string, error: LanesError, phase: {
|
|
33
|
+
text: string;
|
|
34
|
+
sessionId: string | undefined;
|
|
35
|
+
workingDirectory: string;
|
|
36
|
+
}): LaneRecord;
|
|
17
37
|
export interface LoopControllerDeps {
|
|
18
38
|
resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
|
|
19
39
|
interruptLane(laneId: string, reason: string, options: {
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { readLane, retryUpdateLane, updateLane } from "../lib/lanes/store.js";
|
|
5
5
|
import { LanesError, isLaneVersionConflict } from "../lib/lanes/errors.js";
|
|
6
|
+
import { liveNodeBinary } from "../lib/lanes/node-binary.js";
|
|
6
7
|
import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
|
|
7
8
|
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
8
9
|
import { raiseLaneQuestion } from "../lib/lanes/questions.js";
|
|
@@ -33,6 +34,44 @@ export function settleLoopLaneIdle(hqRoot, laneId) {
|
|
|
33
34
|
export function prepareIdleLoopLaneForResume(hqRoot, laneId) {
|
|
34
35
|
return casLoopLaneState(hqRoot, laneId, "running", "done");
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* LanesError codes from the resume admission path that mean admission could
|
|
39
|
+
* not run right now and a later admission can retry: the admission or store
|
|
40
|
+
* lock timed out (store_lock_timeout), the lane changed while admission was
|
|
41
|
+
* checked (lane_admission_conflict), or a lane record CAS miss
|
|
42
|
+
* (version_conflict). Any other resume error is permanent and is rethrown.
|
|
43
|
+
*/
|
|
44
|
+
export const TRANSIENT_LOOP_ADMISSION_ERRORS = new Set([
|
|
45
|
+
"store_lock_timeout",
|
|
46
|
+
"lane_admission_conflict",
|
|
47
|
+
"version_conflict",
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Park a loop phase whose resume was refused before an engine launched. The
|
|
51
|
+
* lane becomes `queued` with `admit_failed` and the error, and carries the
|
|
52
|
+
* phase as `pending_resume`, so admission can launch it later and the lane is
|
|
53
|
+
* never left looking active with no engine.
|
|
54
|
+
*/
|
|
55
|
+
export function deferRefusedLoopPhase(hqRoot, laneId, at, error, phase) {
|
|
56
|
+
return retryUpdateLane(hqRoot, laneId, "command", (record) => ({
|
|
57
|
+
...record,
|
|
58
|
+
state: "queued",
|
|
59
|
+
queued_reason: "admit_failed",
|
|
60
|
+
queued_details: { reason: "admit_failed", error: error.error, message: error.message || error.error },
|
|
61
|
+
queued_at: at,
|
|
62
|
+
admission_reserved_at: undefined,
|
|
63
|
+
pending_resume: {
|
|
64
|
+
...(phase.sessionId ? { session_id: phase.sessionId } : { fresh_thread: true }),
|
|
65
|
+
trigger: "resume",
|
|
66
|
+
caller: "loop",
|
|
67
|
+
resumed_from: "done",
|
|
68
|
+
text: phase.text,
|
|
69
|
+
compact: true,
|
|
70
|
+
working_directory: phase.workingDirectory,
|
|
71
|
+
at,
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
36
75
|
function casLoopLaneState(hqRoot, laneId, from, to) {
|
|
37
76
|
for (let attempt = 0;; attempt += 1) {
|
|
38
77
|
const current = readLane(hqRoot, laneId);
|
|
@@ -67,8 +106,8 @@ export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
|
|
|
67
106
|
const args = [entry, "lanes", "_loop-worker", laneId];
|
|
68
107
|
const env = { ...ctx.env, HQ_LANE_LOOP_CONTROLLER: "1", ...(initialAdmission ? { HQ_LANE_LOOP_INITIAL: "1" } : {}) };
|
|
69
108
|
const child = ctx.spawnProcess
|
|
70
|
-
? ctx.spawnProcess(
|
|
71
|
-
: spawn(
|
|
109
|
+
? ctx.spawnProcess(liveNodeBinary(), args, { cwd: ctx.hqRoot(), detached: true, windowsHide: true, stdio: ["ignore", 1, 2], env })
|
|
110
|
+
: spawn(liveNodeBinary(), args, { cwd: ctx.hqRoot(), detached: true, stdio: "ignore", env });
|
|
72
111
|
child.unref();
|
|
73
112
|
if (child.pid === undefined)
|
|
74
113
|
throw new LanesError("loop_controller_spawn_failed", "Loop controller did not start", { lane_id: laneId });
|
|
@@ -208,15 +247,34 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
208
247
|
else {
|
|
209
248
|
if (current.state === "running")
|
|
210
249
|
current = prepareIdleLoopLaneForResume(hqRoot, laneId);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
250
|
+
const resumeText = `Read ${briefPath} and follow it as the complete instruction for this phase.`;
|
|
251
|
+
try {
|
|
252
|
+
await deps.resumeLane(laneId, {
|
|
253
|
+
text: resumeText,
|
|
254
|
+
project: current.project_id,
|
|
255
|
+
...(storyId ? { story: storyId } : {}),
|
|
256
|
+
freshThread: fresh,
|
|
257
|
+
...(sessionId ? { loopSessionId: sessionId } : {}),
|
|
258
|
+
...(repairError !== undefined ? { loopHandoffRepair: true } : {}),
|
|
259
|
+
workingDirectory: worktree,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
// Transient admission refusal before any engine launched (for
|
|
264
|
+
// example a store lock timeout). The envelope is already active, so keep it
|
|
265
|
+
// and park the lane as queued with a reason `hq lanes show`
|
|
266
|
+
// displays; the next admission launches this phase from
|
|
267
|
+
// pending_resume and the wait below picks up its result.
|
|
268
|
+
if (!(error instanceof LanesError) ||
|
|
269
|
+
!TRANSIENT_LOOP_ADMISSION_ERRORS.has(error.error) ||
|
|
270
|
+
readLane(hqRoot, laneId).state !== "done")
|
|
271
|
+
throw error;
|
|
272
|
+
deferRefusedLoopPhase(hqRoot, laneId, ctx.now().toISOString(), error, {
|
|
273
|
+
text: resumeText,
|
|
274
|
+
sessionId: fresh ? undefined : sessionId ?? current.provider_session_id,
|
|
275
|
+
workingDirectory: worktree,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
220
278
|
}
|
|
221
279
|
let interruptPromise;
|
|
222
280
|
const abortPhase = () => {
|
package/dist/commands/lanes.js
CHANGED
|
@@ -508,9 +508,17 @@ function livenessDepsFromCtx(ctx) {
|
|
|
508
508
|
* envelopes; every other lane spawns its worker directly. Every admission path
|
|
509
509
|
* (reconcile, in-process queue drain, hq lanes admit) goes through here so a
|
|
510
510
|
* loop lane never runs its brief without a controller.
|
|
511
|
+
*
|
|
512
|
+
* A queued loop lane whose controller is still alive is a deferred phase: the
|
|
513
|
+
* controller already activated the envelope and is waiting for its engine.
|
|
514
|
+
* That lane spawns the phase worker from pending_resume; starting a second
|
|
515
|
+
* controller would lose the runner lease and leave the lane running with no
|
|
516
|
+
* engine (feedback_865fe8a0).
|
|
511
517
|
*/
|
|
512
518
|
function startAdmittedLane(ctx, root, candidate) {
|
|
513
|
-
|
|
519
|
+
const deferredLoopPhase = candidate.loop_mode === true && candidate.pending_resume !== undefined &&
|
|
520
|
+
loopControllerIsAlive(runDir(root, candidate.lane_id));
|
|
521
|
+
if (candidate.loop_mode !== true || candidate.state !== "queued" || deferredLoopPhase) {
|
|
514
522
|
const admissionHost = reconcileHostFromCtx(ctx);
|
|
515
523
|
return spawnLaneWorker(root, candidate, {
|
|
516
524
|
env: ctx.env,
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { Command } from 'commander';
|
|
23
23
|
import { retrustCodexHooksAfterRescue } from '../utils/hook-trust.js';
|
|
24
|
+
import { repairDanglingReleaseLinks } from '../lib/doctor/checks/release-links.js';
|
|
24
25
|
export interface RescueTarget {
|
|
25
26
|
source: string;
|
|
26
27
|
/** undefined → let the script apply its own default ref (`main`). */
|
|
@@ -52,6 +53,14 @@ export declare function shouldAttemptSelfUpdate(opts: {
|
|
|
52
53
|
* changes the rescue's exit status, because the core update itself succeeded.
|
|
53
54
|
*/
|
|
54
55
|
export declare function retrustHooksAfterRescue(hqRoot: string, retrust?: typeof retrustCodexHooksAfterRescue): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Repair HQ-managed release links (`.agents/skills`, `.codex/claude`, ...)
|
|
58
|
+
* that do not resolve after a rescue, for example because they were laid at
|
|
59
|
+
* the rescue's temp extraction folder, which is then deleted
|
|
60
|
+
* (feedback_2ab63e50). Exported for tests. Best-effort: a failure is reported
|
|
61
|
+
* but never changes the rescue's exit status.
|
|
62
|
+
*/
|
|
63
|
+
export declare function repairReleaseLinksAfterRescue(hqRoot: string, repair?: typeof repairDanglingReleaseLinks): void;
|
|
55
64
|
/** Latest release tag for a repo, e.g. `v12.3.0`. Throws on failure. */
|
|
56
65
|
export declare function getLatestReleaseTag(repo: string, token?: string): Promise<string>;
|
|
57
66
|
/** Resolve a tag to its commit SHA, dereferencing annotated tags. */
|
package/dist/commands/rescue.js
CHANGED
|
@@ -6,6 +6,7 @@ import { rescue } from '@indigoai-us/hq-cloud';
|
|
|
6
6
|
import { findHqRoot } from '../utils/manifest.js';
|
|
7
7
|
import { selfUpdateAndReexec } from '../utils/self-update.js';
|
|
8
8
|
import { retrustCodexHooksAfterRescue } from '../utils/hook-trust.js';
|
|
9
|
+
import { repairDanglingReleaseLinks } from '../lib/doctor/checks/release-links.js';
|
|
9
10
|
import { PROD_SOURCE, STAGING_SOURCE, resolveGhToken } from '../utils/shipped-codex-hooks.js';
|
|
10
11
|
/**
|
|
11
12
|
* Resolve the source repo + ref from the user's flags. Pure + exported for
|
|
@@ -49,6 +50,35 @@ export async function retrustHooksAfterRescue(hqRoot, retrust = retrustCodexHook
|
|
|
49
50
|
'Codex will skip changed HQ hooks until you run `hq reindex`.'));
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Repair HQ-managed release links (`.agents/skills`, `.codex/claude`, ...)
|
|
55
|
+
* that do not resolve after a rescue, for example because they were laid at
|
|
56
|
+
* the rescue's temp extraction folder, which is then deleted
|
|
57
|
+
* (feedback_2ab63e50). Exported for tests. Best-effort: a failure is reported
|
|
58
|
+
* but never changes the rescue's exit status.
|
|
59
|
+
*/
|
|
60
|
+
export function repairReleaseLinksAfterRescue(hqRoot, repair = repairDanglingReleaseLinks) {
|
|
61
|
+
try {
|
|
62
|
+
for (const entry of repair(hqRoot)) {
|
|
63
|
+
if (entry.action === 'repointed') {
|
|
64
|
+
console.log(`rescue: repointed dangling link ${entry.rel} -> ${entry.linkText ?? entry.target}`);
|
|
65
|
+
}
|
|
66
|
+
else if (entry.action === 'removed') {
|
|
67
|
+
console.log(`rescue: removed dangling link ${entry.rel} (its target is missing)`);
|
|
68
|
+
}
|
|
69
|
+
else if (entry.action === 'skipped') {
|
|
70
|
+
console.log(`rescue: left dangling link ${entry.rel} alone: ${entry.reason ?? 'not an HQ-managed target'}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.warn(chalk.yellow(`rescue: could not repair ${entry.rel}: ${entry.reason ?? 'unknown error'}. Run \`hq doctor --fix\`.`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.warn(chalk.yellow(`rescue: could not repair dangling release links: ${err instanceof Error ? err.message : 'unknown error'}. ` +
|
|
79
|
+
'Run `hq doctor --fix`.'));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
52
82
|
function ghHeaders(token) {
|
|
53
83
|
const headers = {
|
|
54
84
|
Accept: 'application/vnd.github+json',
|
|
@@ -185,6 +215,7 @@ export function registerRescueCommand(program) {
|
|
|
185
215
|
ghToken: token,
|
|
186
216
|
});
|
|
187
217
|
if (status === 0 && !opts.check) {
|
|
218
|
+
repairReleaseLinksAfterRescue(hqRoot);
|
|
188
219
|
await retrustHooksAfterRescue(hqRoot);
|
|
189
220
|
}
|
|
190
221
|
process.exit(status);
|