@indigoai-us/hq-cli 5.345.155 → 5.345.159
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 +289 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +536 -7
- package/dist/command-catalog.generated.js +676 -24
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +13 -1
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/integrations-core.d.ts +13 -1
- package/dist/commands/integrations-core.js +49 -4
- package/dist/commands/integrations.js +4 -2
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +185 -16
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
- package/dist/lib/agent-kit/inbox-state.js +5 -1
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
- package/dist/lib/doctor/checks/sync-health.js +66 -1
- package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
- package/dist/lib/doctor/company-resolution-state.js +89 -0
- package/dist/lib/doctor/fix/apply.d.ts +36 -6
- package/dist/lib/doctor/fix/apply.js +128 -12
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -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/hook-engine/resolve.js +6 -2
- package/dist/lib/index-render/company-knowledge.d.ts +1 -1
- package/dist/lib/index-render/company-knowledge.js +4 -5
- package/dist/lib/index-render/index.js +44 -3
- package/dist/lib/index-render/projects.d.ts +1 -1
- package/dist/lib/index-render/projects.js +5 -5
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/access-denied-hint.js +6 -0
- package/dist/utils/client-health.d.ts +34 -0
- package/dist/utils/client-health.js +96 -3
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
3
|
+
import { ensureCognitoToken, loadCachedTokens } from "../utils/cognito-session.js";
|
|
4
4
|
import { assertVaultApiRequestSupported, vaultApiFetch, } from "../utils/vault-api.js";
|
|
5
5
|
import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
|
|
6
6
|
import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
|
|
@@ -8,6 +8,7 @@ import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectF
|
|
|
8
8
|
import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
|
|
9
9
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
10
10
|
import { cachedLatestVersion } from "../utils/version-check.js";
|
|
11
|
+
import { registerFeedbackShowCommand } from "./feedback-show.js";
|
|
11
12
|
export const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
|
|
12
13
|
export const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
|
|
13
14
|
export const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
|
|
@@ -199,7 +200,151 @@ export async function submitFeedback(opts) {
|
|
|
199
200
|
throw new Error(`Failed to submit feedback: ${errMsg}`);
|
|
200
201
|
}
|
|
201
202
|
const data = (await res.json());
|
|
202
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
id: data.id,
|
|
205
|
+
...(typeof data.publicUrl === "string" && data.publicUrl.length > 0
|
|
206
|
+
? { publicUrl: data.publicUrl }
|
|
207
|
+
: {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Public ticket pages: `hq feedback link <id>` and `hq feedback update <id>`
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
/** Customer-visible states accepted by `hq feedback update --state`. */
|
|
214
|
+
export const PUBLIC_TICKET_STATES = [
|
|
215
|
+
"triaged",
|
|
216
|
+
"in-progress",
|
|
217
|
+
"shipped",
|
|
218
|
+
"verified",
|
|
219
|
+
"resolved",
|
|
220
|
+
];
|
|
221
|
+
const FEEDBACK_ID_RE = /^feedback_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
222
|
+
export function assertFeedbackId(id) {
|
|
223
|
+
if (!FEEDBACK_ID_RE.test(id)) {
|
|
224
|
+
throw new Error(`Invalid feedback id '${id}': expected the full feedback_<uuid> id printed by hq feedback.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The staff feedback routes gate on the id token's email claim, so send the
|
|
229
|
+
* cached id token when a person session has one. Machine identities already
|
|
230
|
+
* get their id token back from ensureCognitoToken.
|
|
231
|
+
*/
|
|
232
|
+
async function resolveStaffToken() {
|
|
233
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
234
|
+
const cached = loadCachedTokens();
|
|
235
|
+
return typeof cached?.idToken === "string" && cached.idToken.length > 0
|
|
236
|
+
? cached.idToken
|
|
237
|
+
: token;
|
|
238
|
+
}
|
|
239
|
+
async function readFeedbackError(res) {
|
|
240
|
+
const data = await res.json().catch(() => ({}));
|
|
241
|
+
return data &&
|
|
242
|
+
typeof data === "object" &&
|
|
243
|
+
!Array.isArray(data) &&
|
|
244
|
+
typeof data.error === "string"
|
|
245
|
+
? data.error
|
|
246
|
+
: res.statusText;
|
|
247
|
+
}
|
|
248
|
+
function publicUrlFrom(data) {
|
|
249
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
250
|
+
return null;
|
|
251
|
+
const url = data.publicUrl;
|
|
252
|
+
return typeof url === "string" && url.length > 0 ? url : null;
|
|
253
|
+
}
|
|
254
|
+
/** Resolve (and lazily mint) the public tracking link for a ticket. */
|
|
255
|
+
export async function fetchFeedbackLink(opts) {
|
|
256
|
+
const res = await vaultApiFetch({
|
|
257
|
+
token: opts.token,
|
|
258
|
+
path: `/v1/feedback/${opts.id}`,
|
|
259
|
+
method: "PATCH",
|
|
260
|
+
body: { ensurePublicToken: true },
|
|
261
|
+
});
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
throw new Error(`Failed to get the public link for ${opts.id}: ${await readFeedbackError(res)}`);
|
|
264
|
+
}
|
|
265
|
+
const url = publicUrlFrom(await res.json());
|
|
266
|
+
if (!url) {
|
|
267
|
+
throw new Error(`The server did not return a public link for ${opts.id}. Public ticket pages may not be deployed yet.`);
|
|
268
|
+
}
|
|
269
|
+
return url;
|
|
270
|
+
}
|
|
271
|
+
/** Post a customer-visible update; the server also mirrors it into #hq-feedback. */
|
|
272
|
+
export async function postFeedbackUpdate(opts) {
|
|
273
|
+
const res = await vaultApiFetch({
|
|
274
|
+
token: opts.token,
|
|
275
|
+
path: `/v1/feedback/${opts.id}`,
|
|
276
|
+
method: "PATCH",
|
|
277
|
+
body: { publicUpdate: { state: opts.state, note: opts.note } },
|
|
278
|
+
});
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
throw new Error(`Failed to update ${opts.id}: ${await readFeedbackError(res)}`);
|
|
281
|
+
}
|
|
282
|
+
const data = (await res.json());
|
|
283
|
+
return {
|
|
284
|
+
publicUrl: publicUrlFrom(data),
|
|
285
|
+
slack: typeof data.publicUpdateSlack === "string" ? data.publicUpdateSlack : null,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function registerLinkSubcommand(feedbackCmd) {
|
|
289
|
+
feedbackCmd
|
|
290
|
+
.command("link")
|
|
291
|
+
.description("Print the public tracking link for a ticket (status.hqforwork.com)")
|
|
292
|
+
.argument("<id>", "Feedback id (feedback_<uuid>)")
|
|
293
|
+
.action(async (id) => {
|
|
294
|
+
try {
|
|
295
|
+
assertFeedbackId(id);
|
|
296
|
+
const token = await resolveStaffToken();
|
|
297
|
+
assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
|
|
298
|
+
const url = await fetchFeedbackLink({ id, token });
|
|
299
|
+
console.log(`Track: ${url}`);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function registerUpdateSubcommand(feedbackCmd) {
|
|
308
|
+
feedbackCmd
|
|
309
|
+
.command("update")
|
|
310
|
+
.description("Post a customer-visible status update on a ticket's public page and its #hq-feedback thread")
|
|
311
|
+
.argument("<id>", "Feedback id (feedback_<uuid>)")
|
|
312
|
+
.requiredOption("--state <state>", `One of: ${PUBLIC_TICKET_STATES.join(", ")}`)
|
|
313
|
+
.requiredOption("--note <text>", "What the customer should read (plain text, up to 2000 characters)")
|
|
314
|
+
.action(async (id, opts) => {
|
|
315
|
+
try {
|
|
316
|
+
assertFeedbackId(id);
|
|
317
|
+
if (!PUBLIC_TICKET_STATES.includes(opts.state)) {
|
|
318
|
+
throw new Error(`Invalid --state '${opts.state}': expected one of ${PUBLIC_TICKET_STATES.join(", ")}.`);
|
|
319
|
+
}
|
|
320
|
+
const note = opts.note.trim();
|
|
321
|
+
if (note.length === 0)
|
|
322
|
+
throw new Error("--note must not be empty.");
|
|
323
|
+
if (note.length > 2000)
|
|
324
|
+
throw new Error("--note must be at most 2000 characters.");
|
|
325
|
+
const token = await resolveStaffToken();
|
|
326
|
+
assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
|
|
327
|
+
const result = await postFeedbackUpdate({
|
|
328
|
+
id,
|
|
329
|
+
token,
|
|
330
|
+
state: opts.state,
|
|
331
|
+
note,
|
|
332
|
+
});
|
|
333
|
+
console.log(`Updated: ${id} -> ${opts.state}`);
|
|
334
|
+
if (result.publicUrl)
|
|
335
|
+
console.log(`Track: ${result.publicUrl}`);
|
|
336
|
+
if (result.slack === "failed") {
|
|
337
|
+
console.error(chalk.yellow("Warning:"), "the update was saved but the #hq-feedback thread post failed.");
|
|
338
|
+
}
|
|
339
|
+
else if (result.slack === "skipped") {
|
|
340
|
+
console.error(chalk.yellow("Note:"), "the ticket has no #hq-feedback thread, so nothing was posted to Slack.");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
203
348
|
}
|
|
204
349
|
function registerSubcommand(feedbackCmd, type) {
|
|
205
350
|
feedbackCmd
|
|
@@ -284,6 +429,8 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
284
429
|
logsEnabled,
|
|
285
430
|
});
|
|
286
431
|
console.log(`Submitted: ${result.id}`);
|
|
432
|
+
if (result.publicUrl)
|
|
433
|
+
console.log(`Track: ${result.publicUrl}`);
|
|
287
434
|
}
|
|
288
435
|
catch (err) {
|
|
289
436
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -297,5 +444,8 @@ export function registerFeedbackCommand(program) {
|
|
|
297
444
|
.description("Submit a bug report or feature request to HQ");
|
|
298
445
|
registerSubcommand(feedbackCmd, "bug");
|
|
299
446
|
registerSubcommand(feedbackCmd, "feature");
|
|
447
|
+
registerLinkSubcommand(feedbackCmd);
|
|
448
|
+
registerUpdateSubcommand(feedbackCmd);
|
|
449
|
+
registerFeedbackShowCommand(feedbackCmd);
|
|
300
450
|
}
|
|
301
451
|
//# sourceMappingURL=feedback.js.map
|
|
@@ -443,6 +443,22 @@ export declare const PRESIGN_BUFFER_MAX_BYTES: number;
|
|
|
443
443
|
* access token + companyUid are captured here; the orchestrator just calls
|
|
444
444
|
* `send(...)` as if it held an S3 client.
|
|
445
445
|
*/
|
|
446
|
+
/** Error code for a presigned storage download that failed after the API authorized it. */
|
|
447
|
+
export declare const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAILED";
|
|
448
|
+
/**
|
|
449
|
+
* Build the error for a failed presigned download. The API already checked the
|
|
450
|
+
* caller's ACL before it signed the URL, so a failure here is a storage or
|
|
451
|
+
* network outcome, not a missing grant. The error carries no `status: 403` and
|
|
452
|
+
* a code that `isAccessDeniedError` treats as not an access denial, so the CLI
|
|
453
|
+
* does not send the caller to `hq access` (which only re-checks the ACL that
|
|
454
|
+
* already passed).
|
|
455
|
+
*
|
|
456
|
+
* A response that really came from S3 carries `x-amz-request-id`; we report
|
|
457
|
+
* S3's error code from the XML body. A response without it came from something
|
|
458
|
+
* between this machine and the storage host, usually a sandbox egress proxy or
|
|
459
|
+
* firewall that does not allow the bucket host, and we say so.
|
|
460
|
+
*/
|
|
461
|
+
export declare function presignedDownloadError(key: string, url: string, dl: Response): Promise<Error>;
|
|
446
462
|
export declare function createCompanyPresignClient(input: {
|
|
447
463
|
token: string;
|
|
448
464
|
companyUid: string;
|
|
@@ -709,6 +709,57 @@ export const PRESIGN_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
|
|
|
709
709
|
* access token + companyUid are captured here; the orchestrator just calls
|
|
710
710
|
* `send(...)` as if it held an S3 client.
|
|
711
711
|
*/
|
|
712
|
+
/** Error code for a presigned storage download that failed after the API authorized it. */
|
|
713
|
+
export const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAILED";
|
|
714
|
+
/**
|
|
715
|
+
* Build the error for a failed presigned download. The API already checked the
|
|
716
|
+
* caller's ACL before it signed the URL, so a failure here is a storage or
|
|
717
|
+
* network outcome, not a missing grant. The error carries no `status: 403` and
|
|
718
|
+
* a code that `isAccessDeniedError` treats as not an access denial, so the CLI
|
|
719
|
+
* does not send the caller to `hq access` (which only re-checks the ACL that
|
|
720
|
+
* already passed).
|
|
721
|
+
*
|
|
722
|
+
* A response that really came from S3 carries `x-amz-request-id`; we report
|
|
723
|
+
* S3's error code from the XML body. A response without it came from something
|
|
724
|
+
* between this machine and the storage host, usually a sandbox egress proxy or
|
|
725
|
+
* firewall that does not allow the bucket host, and we say so.
|
|
726
|
+
*/
|
|
727
|
+
export async function presignedDownloadError(key, url, dl) {
|
|
728
|
+
let host = "the storage host";
|
|
729
|
+
try {
|
|
730
|
+
host = new URL(url).host;
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
// keep the generic label
|
|
734
|
+
}
|
|
735
|
+
const requestId = dl.headers.get("x-amz-request-id");
|
|
736
|
+
const bodyText = (await dl.text().catch(() => "")).slice(0, 4096);
|
|
737
|
+
const fromStorage = requestId !== null && requestId.length > 0;
|
|
738
|
+
let message;
|
|
739
|
+
let storageCode;
|
|
740
|
+
if (fromStorage) {
|
|
741
|
+
storageCode = /<Code>([^<]{1,100})<\/Code>/.exec(bodyText)?.[1];
|
|
742
|
+
message =
|
|
743
|
+
`Failed to download '${key}' (HTTP ${dl.status}` +
|
|
744
|
+
(storageCode ? `, storage error ${storageCode}` : "") +
|
|
745
|
+
`, request id ${requestId}). Your file access was granted; the storage ` +
|
|
746
|
+
`service refused the signed download. Report this with: hq feedback bug`;
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
message =
|
|
750
|
+
`Failed to download '${key}' (HTTP ${dl.status}). Your file access was ` +
|
|
751
|
+
`granted, but the response did not come from ${host}. A proxy or network ` +
|
|
752
|
+
`egress rule on this machine likely blocked it. Allow outbound HTTPS to ` +
|
|
753
|
+
`${host} (HQ vault files are served from *.s3.us-east-1.amazonaws.com).`;
|
|
754
|
+
}
|
|
755
|
+
return Object.assign(new Error(message), {
|
|
756
|
+
code: STORAGE_DOWNLOAD_FAILED_CODE,
|
|
757
|
+
key,
|
|
758
|
+
httpStatus: dl.status,
|
|
759
|
+
fromStorage,
|
|
760
|
+
...(storageCode ? { storageCode } : {}),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
712
763
|
export function createCompanyPresignClient(input) {
|
|
713
764
|
const { token, companyUid, companySlug, baseUrl } = input;
|
|
714
765
|
const denied = (message, key, extra) => Object.assign(new Error(message), {
|
|
@@ -780,7 +831,7 @@ export function createCompanyPresignClient(input) {
|
|
|
780
831
|
}
|
|
781
832
|
const dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
|
|
782
833
|
if (!dl.ok) {
|
|
783
|
-
throw
|
|
834
|
+
throw await presignedDownloadError(key, first.url, dl);
|
|
784
835
|
}
|
|
785
836
|
// HQ-CLI-5: when the response declares a length at or below the ceiling,
|
|
786
837
|
// DRAIN it here — read the whole body before send() resolves and replay it
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -9,6 +9,30 @@ export declare const FILES_SHARE_EMPTY_PREFIX_WARNING_FLAG = "files.share-empty-
|
|
|
9
9
|
export declare function parseDuration(input: string): number | null;
|
|
10
10
|
/** PRD upper bound on browser-launch share-session expiry. */
|
|
11
11
|
export declare const MAX_SHARE_SESSION_EXPIRY_MS: number;
|
|
12
|
+
/**
|
|
13
|
+
* A more specific ACL row under a prefix, as hq-pro reports it. File ACL
|
|
14
|
+
* resolution picks the single most specific row that covers a key, so each of
|
|
15
|
+
* these rows replaces the broader prefix's grants for the keys it covers
|
|
16
|
+
* (feedback_466fe8b8).
|
|
17
|
+
*/
|
|
18
|
+
export interface AclMaskingRow {
|
|
19
|
+
path: string;
|
|
20
|
+
open: boolean;
|
|
21
|
+
creatorUid: string;
|
|
22
|
+
grantees: Array<{
|
|
23
|
+
granteeType: string;
|
|
24
|
+
granteeId: string;
|
|
25
|
+
permission: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/** The warning `hq files share` prints when a grant is shadowed by more specific rows. */
|
|
29
|
+
export declare function formatShadowedGrantWarning(input: {
|
|
30
|
+
prefix: string;
|
|
31
|
+
principal: string;
|
|
32
|
+
permission: string;
|
|
33
|
+
companySlug?: string;
|
|
34
|
+
maskedBy: AclMaskingRow[];
|
|
35
|
+
}): string;
|
|
12
36
|
export interface ShareSessionResponse {
|
|
13
37
|
url: string;
|
|
14
38
|
token?: string;
|
package/dist/commands/files.js
CHANGED
|
@@ -9,6 +9,7 @@ import { looksLikeCompanyUid } from "../utils/vault-api.js";
|
|
|
9
9
|
import { AuthError } from "../utils/auth-error.js";
|
|
10
10
|
import { formatAccessDenied, offerAccessLadder, } from "../utils/access-denied-hint.js";
|
|
11
11
|
import { fetchChannels, matchChannelsByName } from "./dm.js";
|
|
12
|
+
import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
|
|
12
13
|
import { resolveCompanySlug, resolvePersonWithRosterFallback, } from "./people.js";
|
|
13
14
|
import { findHqRoot } from "../utils/manifest.js";
|
|
14
15
|
import { maxLength } from "../utils/table-width.js";
|
|
@@ -55,6 +56,39 @@ function privateFolderExplanation(prefix) {
|
|
|
55
56
|
return (`Private folder '${prefix}' — write can create only a new direct child. ` +
|
|
56
57
|
"Children are creator-only; this grant does not read, list, overwrite, or delete them.");
|
|
57
58
|
}
|
|
59
|
+
function describeMaskingGrantees(row) {
|
|
60
|
+
if (row.open)
|
|
61
|
+
return "open (all members read)";
|
|
62
|
+
if (row.grantees.length === 0)
|
|
63
|
+
return "none (creator only)";
|
|
64
|
+
return row.grantees
|
|
65
|
+
.map((g) => g.granteeType === "company-wide"
|
|
66
|
+
? `everyone:${g.permission}`
|
|
67
|
+
: `${g.granteeId}:${g.permission}`)
|
|
68
|
+
.join(", ");
|
|
69
|
+
}
|
|
70
|
+
function printMaskingTable(rows) {
|
|
71
|
+
const PATH_W = Math.max(4, ...rows.map((r) => displayAclPattern(r.path).length));
|
|
72
|
+
const CREATOR_W = Math.max(7, ...rows.map((r) => r.creatorUid.length));
|
|
73
|
+
console.log(chalk.bold(["PATH".padEnd(PATH_W), "CREATOR".padEnd(CREATOR_W), "GRANTS"].join(" ")));
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
console.log([
|
|
76
|
+
displayAclPattern(row.path).padEnd(PATH_W),
|
|
77
|
+
row.creatorUid.padEnd(CREATOR_W),
|
|
78
|
+
describeMaskingGrantees(row),
|
|
79
|
+
].join(" "));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The warning `hq files share` prints when a grant is shadowed by more specific rows. */
|
|
83
|
+
export function formatShadowedGrantWarning(input) {
|
|
84
|
+
const paths = input.maskedBy.map((row) => `'${row.path}'`).join(", ");
|
|
85
|
+
const company = input.companySlug ? ` --company ${input.companySlug}` : "";
|
|
86
|
+
return (`Warning: this grant takes no effect under ${input.maskedBy.length} more specific rule(s) ` +
|
|
87
|
+
`that do not include ${input.principal}: ${paths}. ` +
|
|
88
|
+
"Access under those paths is unchanged (the most specific rule wins). " +
|
|
89
|
+
`Share them too, for example: hq files share '${input.maskedBy[0]?.path ?? "<path>"}' --with ${input.principal} --permission ${input.permission}${company}` +
|
|
90
|
+
" — or remove the rule with hq files unshare.");
|
|
91
|
+
}
|
|
58
92
|
function formatAclPatternConflict(prefix, serverMessage) {
|
|
59
93
|
const privatePattern = prefix.endsWith("/*")
|
|
60
94
|
? prefix.slice(0, -1)
|
|
@@ -544,6 +578,14 @@ export function registerFilesCommand(program) {
|
|
|
544
578
|
console.log("\nGranted on descendant prefixes (do not affect this prefix's access):");
|
|
545
579
|
printEntryTable(tree.children, true);
|
|
546
580
|
}
|
|
581
|
+
if (Array.isArray(tree.masking) && tree.masking.length > 0) {
|
|
582
|
+
console.log(`\nMore specific rules under ${displayAclPattern(tree.prefix)} (each replaces this prefix's grants for its own paths):`);
|
|
583
|
+
printMaskingTable(tree.masking);
|
|
584
|
+
if (tree.maskingTruncated) {
|
|
585
|
+
console.log(chalk.gray(" ...list truncated; use --json for the first page."));
|
|
586
|
+
}
|
|
587
|
+
console.log(chalk.gray(`A grant on ${displayAclPattern(tree.prefix)} does not reach these paths unless they grant the same principal. Share each path too, or remove its rule with hq files unshare.`));
|
|
588
|
+
}
|
|
547
589
|
const allEntries = [...tree.direct, ...tree.inherited, ...tree.children];
|
|
548
590
|
if (allEntries.some((entry) => entry.granteeType === "company-wide")) {
|
|
549
591
|
console.log(chalk.gray("\nCompany-wide grants apply to every active member, including guests. Demoting someone to guest does not exclude them. Use hq members remove to end membership, or hq access <path> --as <email> to see effective access."));
|
|
@@ -704,7 +746,7 @@ async function resolveShareChannelTarget(params) {
|
|
|
704
746
|
const channels = await fetchChannels(token);
|
|
705
747
|
const matches = matchChannelsByName(channels, params.channelName);
|
|
706
748
|
if (matches.length === 0) {
|
|
707
|
-
console.error(chalk.red(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels
|
|
749
|
+
console.error(chalk.red(withChannelSuggestions(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`, suggestChannels(channels, params.channelName))));
|
|
708
750
|
process.exit(1);
|
|
709
751
|
}
|
|
710
752
|
if (matches.length > 1) {
|
|
@@ -894,6 +936,15 @@ async function runDirectGrant(params) {
|
|
|
894
936
|
if (isPrivateFolderPattern(printedPrefix)) {
|
|
895
937
|
console.log(chalk.gray(privateFolderExplanation(printedPrefix)));
|
|
896
938
|
}
|
|
939
|
+
if (Array.isArray(data.maskedBy) && data.maskedBy.length > 0) {
|
|
940
|
+
console.warn(chalk.yellow(formatShadowedGrantWarning({
|
|
941
|
+
prefix: printedPrefix,
|
|
942
|
+
principal: isAll ? "@all" : principal,
|
|
943
|
+
permission: params.permission,
|
|
944
|
+
...(params.companySlug ? { companySlug: params.companySlug } : {}),
|
|
945
|
+
maskedBy: data.maskedBy,
|
|
946
|
+
})));
|
|
947
|
+
}
|
|
897
948
|
if (typeof data.record?.error === "string" &&
|
|
898
949
|
typeof data.record.status === "number" &&
|
|
899
950
|
data.record.status >= 400) {
|
|
@@ -548,12 +548,16 @@ async function connectViaOAuth(token, companyUid, target, opts) {
|
|
|
548
548
|
...target.ref,
|
|
549
549
|
...(requestedScopes ? { scopes: requestedScopes } : {}),
|
|
550
550
|
};
|
|
551
|
-
|
|
552
|
-
|
|
551
|
+
const userClient = await resolveUserOAuthClient(opts, target.label);
|
|
552
|
+
if (opts.linkOnly) {
|
|
553
|
+
return await linkOnlySignIn(token, companyUid, target, opts, {
|
|
554
|
+
...startInput,
|
|
555
|
+
...(userClient ?? {}),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
553
558
|
// A user-registered app pins the console callback and skips the loopback
|
|
554
559
|
// entirely — see `userClientSignIn` for why an ephemeral port cannot be the
|
|
555
560
|
// redirect URI here.
|
|
556
|
-
const userClient = await resolveUserOAuthClient(opts, target.label);
|
|
557
561
|
if (userClient)
|
|
558
562
|
return await userClientSignIn(token, companyUid, target, opts, userClient, requestedScopes);
|
|
559
563
|
// A loopback listener only works if the browser runs on THIS machine. Over
|
|
@@ -120,6 +120,16 @@ interface GatewayMessage {
|
|
|
120
120
|
}
|
|
121
121
|
export type GatewayErrorShape = "string_error" | "object_no_code" | "object_code" | "string_code" | "nested_code" | "empty" | "redacted_empty";
|
|
122
122
|
export type GatewayFailureCode = "invalid_envelope";
|
|
123
|
+
/** Default client wait for a background (202) integration job. */
|
|
124
|
+
export declare const ASYNC_GATEWAY_DEADLINE_MS: number;
|
|
125
|
+
/** Upper bound for `hq integrations call --timeout`, in seconds. */
|
|
126
|
+
export declare const MAX_CALL_TIMEOUT_SECONDS: number;
|
|
127
|
+
/**
|
|
128
|
+
* Parse `--timeout <seconds>` for `hq integrations call` into milliseconds.
|
|
129
|
+
* Bounds how long the CLI waits for a background job; it never shortens the
|
|
130
|
+
* gateway's own limits.
|
|
131
|
+
*/
|
|
132
|
+
export declare function parseCallTimeoutMs(raw: string | undefined): number;
|
|
123
133
|
export declare class IntegrationsCliError extends Error {
|
|
124
134
|
/**
|
|
125
135
|
* True when the error is the caller's request/state/permission (a client 4xx
|
|
@@ -338,7 +348,9 @@ export declare function resolveConnection(token: string, companyUid: string, app
|
|
|
338
348
|
}, recoveryOpts?: {
|
|
339
349
|
allowSingleRevoked?: boolean;
|
|
340
350
|
}): Promise<AdminConnection>;
|
|
341
|
-
export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string, method?: "tools/call" | "tools/list"
|
|
351
|
+
export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string, method?: "tools/call" | "tools/list", opts?: {
|
|
352
|
+
waitMs?: number;
|
|
353
|
+
}): Promise<GatewayMessage>;
|
|
342
354
|
/**
|
|
343
355
|
* Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
|
|
344
356
|
* where `text` is the provider's JSON. Unwrap to the inner payload; fall back
|
|
@@ -19,7 +19,43 @@ import { connectionHealthNotice } from "../lib/integrations/health.js";
|
|
|
19
19
|
import { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
20
20
|
export { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
21
21
|
const ASYNC_GATEWAY_POLL_INTERVAL_MS = 1_000;
|
|
22
|
-
|
|
22
|
+
/** Default client wait for a background (202) integration job. */
|
|
23
|
+
export const ASYNC_GATEWAY_DEADLINE_MS = 4 * 60_000 + 30_000;
|
|
24
|
+
/** Upper bound for `hq integrations call --timeout`, in seconds. */
|
|
25
|
+
export const MAX_CALL_TIMEOUT_SECONDS = 30 * 60;
|
|
26
|
+
/**
|
|
27
|
+
* Parse `--timeout <seconds>` for `hq integrations call` into milliseconds.
|
|
28
|
+
* Bounds how long the CLI waits for a background job; it never shortens the
|
|
29
|
+
* gateway's own limits.
|
|
30
|
+
*/
|
|
31
|
+
export function parseCallTimeoutMs(raw) {
|
|
32
|
+
if (raw === undefined)
|
|
33
|
+
return ASYNC_GATEWAY_DEADLINE_MS;
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
36
|
+
throw new IntegrationsCliError(`--timeout must be a whole number of seconds (got "${raw}").`, { expected: true });
|
|
37
|
+
}
|
|
38
|
+
const seconds = Number(trimmed);
|
|
39
|
+
if (seconds < 1 || seconds > MAX_CALL_TIMEOUT_SECONDS) {
|
|
40
|
+
throw new IntegrationsCliError(`--timeout must be between 1 and ${MAX_CALL_TIMEOUT_SECONDS} seconds (got ${seconds}).`, { expected: true });
|
|
41
|
+
}
|
|
42
|
+
return seconds * 1_000;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Statuses the gateway returns when an inline (non-background) tools/call ran
|
|
46
|
+
* into the IntegrationMcpFunction 30s wall or its provider budget.
|
|
47
|
+
*/
|
|
48
|
+
function isInlineTimeoutStatus(status) {
|
|
49
|
+
return status === 500 || status === 502 || status === 504;
|
|
50
|
+
}
|
|
51
|
+
function inlineTimeoutHint(status) {
|
|
52
|
+
return (`${upstreamUnavailableMessage(status)} ` +
|
|
53
|
+
"If this was a long-running call (for example a large analytical read), it may have hit the " +
|
|
54
|
+
"gateway's 30-second limit: the gateway ran it inline, not as a background job. Long calls " +
|
|
55
|
+
"run as background jobs only when async integration jobs are enabled for your company; ask " +
|
|
56
|
+
"HQ support to enable them. Once enabled, hq integrations call waits for the result " +
|
|
57
|
+
"automatically (see --timeout).");
|
|
58
|
+
}
|
|
23
59
|
export class IntegrationsCliError extends Error {
|
|
24
60
|
/**
|
|
25
61
|
* True when the error is the caller's request/state/permission (a client 4xx
|
|
@@ -627,9 +663,9 @@ function gatewaySubcommandFor(params, method) {
|
|
|
627
663
|
return "tools_call";
|
|
628
664
|
return "other";
|
|
629
665
|
}
|
|
630
|
-
export async function callGateway(token, params, idempotencyKey, method = "tools/call") {
|
|
666
|
+
export async function callGateway(token, params, idempotencyKey, method = "tools/call", opts = {}) {
|
|
631
667
|
const gatewaySubcommand = gatewaySubcommandFor(params, method);
|
|
632
|
-
const deadline = Date.now() + ASYNC_GATEWAY_DEADLINE_MS;
|
|
668
|
+
const deadline = Date.now() + (opts.waitMs ?? ASYNC_GATEWAY_DEADLINE_MS);
|
|
633
669
|
const res = await vaultApiFetch({
|
|
634
670
|
token,
|
|
635
671
|
path: "/v1/integrations/mcp",
|
|
@@ -663,6 +699,15 @@ export async function callGateway(token, params, idempotencyKey, method = "tools
|
|
|
663
699
|
}
|
|
664
700
|
if (res.status !== 202 && (!res.ok || !terminalMessage)) {
|
|
665
701
|
raiseIfUnauthorized(res);
|
|
702
|
+
if (method === "tools/call" &&
|
|
703
|
+
params.acceptAsync === true &&
|
|
704
|
+
isInlineTimeoutStatus(res.status)) {
|
|
705
|
+
throw new IntegrationsCliError(inlineTimeoutHint(res.status), {
|
|
706
|
+
expected: true,
|
|
707
|
+
status: res.status,
|
|
708
|
+
gatewaySubcommand,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
666
711
|
raiseIfUpstreamUnavailable(res);
|
|
667
712
|
// Carry the HTTP status so a reportable transport failure (e.g. a 501 that
|
|
668
713
|
// is neither 401 nor an upstream-availability status, so it is captured)
|
|
@@ -773,7 +818,7 @@ async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, dea
|
|
|
773
818
|
break;
|
|
774
819
|
await new Promise((resolve) => setTimeout(resolve, Math.min(ASYNC_GATEWAY_POLL_INTERVAL_MS, remainingMs)));
|
|
775
820
|
}
|
|
776
|
-
throw new IntegrationsCliError(`Timed out waiting for integration job ${jobId}; the server job may still finish. To safely retry, rerun with --idempotency-key ${idempotencyKey}.`, { expected: true });
|
|
821
|
+
throw new IntegrationsCliError(`Timed out waiting for integration job ${jobId}; the server job may still finish. To safely retry, rerun with --idempotency-key ${idempotencyKey} (add --timeout <seconds> to wait longer).`, { expected: true });
|
|
777
822
|
}
|
|
778
823
|
/**
|
|
779
824
|
* Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
|
|
@@ -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, 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, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } 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";
|
|
@@ -483,8 +483,10 @@ export function registerIntegrationsCommand(program) {
|
|
|
483
483
|
.option("--args <json>", "Tool arguments as a JSON object (default {}); use - to read the JSON from stdin")
|
|
484
484
|
.option("--args-file <path>", "Read tool arguments from a JSON file (use - for stdin); avoids shell quoting of apostrophes and parentheses")
|
|
485
485
|
.option("--idempotency-key <key>", "Stable key so retries execute at most once (default: generated)")
|
|
486
|
+
.option("--timeout <seconds>", "How long to wait for a long call that runs as a background job (default 270, max 1800)")
|
|
486
487
|
.option("--json", "Machine-readable output")
|
|
487
488
|
.action(async (tool, opts) => {
|
|
489
|
+
const waitMs = parseCallTimeoutMs(opts.timeout);
|
|
488
490
|
const parsedArgs = await resolveCallArgs(opts);
|
|
489
491
|
const token = await ensureCognitoIdToken();
|
|
490
492
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
@@ -522,7 +524,7 @@ export function registerIntegrationsCommand(program) {
|
|
|
522
524
|
},
|
|
523
525
|
};
|
|
524
526
|
}
|
|
525
|
-
const message = await callGateway(token, gatewayParams, idempotencyKey);
|
|
527
|
+
const message = await callGateway(token, gatewayParams, idempotencyKey, "tools/call", { waitMs });
|
|
526
528
|
const payload = unwrapGatewayResult(message.result);
|
|
527
529
|
const queued = queuedOutcome(payload);
|
|
528
530
|
if (queued) {
|
|
@@ -9,9 +9,11 @@ import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/c
|
|
|
9
9
|
import { resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
10
10
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
11
11
|
import { CAPACITY_ONLY_REASON } from "../lib/capacity/accounts.js";
|
|
12
|
+
import { claudeOpenCommand } from "../lib/capacity/ledger.js";
|
|
12
13
|
import { accountQuotaView, clearAccountQuota, loadQuotaState } from "../lib/lanes/quota.js";
|
|
13
14
|
import { resolveLaneModel } from "../lib/lanes/spawn.js";
|
|
14
15
|
import { leadLaneIds } from "../lib/lanes/admission.js";
|
|
16
|
+
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
15
17
|
export function createLaneAccountsHandlers(deps) {
|
|
16
18
|
const { admitQueuedFromCtx, automaticBalanceMoveHistoryReason, ensureDir, laneWithRecoveredProviderSession, loadClaudeBalanceHostSnapshots, parseRemoteJson, prepareAccountMove, printJson, reconcileHostFromCtx, resolveCallerPath } = deps;
|
|
17
19
|
const VENDOR_LOGIN = {
|
|
@@ -124,6 +126,7 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
124
126
|
hostname: ctx.hostname(),
|
|
125
127
|
homedir: ctx.homedir,
|
|
126
128
|
probe: ctx.probeProvider,
|
|
129
|
+
readClaudeCredential: ctx.readClaudeCredential,
|
|
127
130
|
env: ctx.env,
|
|
128
131
|
}).map((status) => {
|
|
129
132
|
const accountCfg = config.providers[provider].accounts[status.id];
|
|
@@ -146,6 +149,10 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
146
149
|
error: "home_missing",
|
|
147
150
|
fix_command: `hq lanes accounts set-home ${provider} ${status.id} <host-home>`,
|
|
148
151
|
} : {}),
|
|
152
|
+
...(provider === "claude" && status.sign_in_state === "expired" ? {
|
|
153
|
+
error: "sign_in_expired",
|
|
154
|
+
fix_command: `${claudeOpenCommand(status.home, ctx.homedir())} then run /login`,
|
|
155
|
+
} : {}),
|
|
149
156
|
...(provider === "claude" ? {
|
|
150
157
|
identity: status.identity ?? null,
|
|
151
158
|
credential_matches_interactive: differs === null ? null : !differs,
|
|
@@ -192,7 +199,11 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
192
199
|
const balanceGateEnabled = true;
|
|
193
200
|
const recentUsageEnabled = await deps.readClaudeAccountBalanceSignalsEnabled(ctx);
|
|
194
201
|
const loads = accountLoadsForReport({ hosts: loaded.snapshots, config, quotaState });
|
|
195
|
-
const suggestions = planClaudeBalanceMoves({
|
|
202
|
+
const suggestions = planClaudeBalanceMoves({
|
|
203
|
+
hosts: loaded.snapshots,
|
|
204
|
+
config,
|
|
205
|
+
laneCompany: (lane) => resolveLaneCompany(hqRoot, lane).company,
|
|
206
|
+
});
|
|
196
207
|
const selections = loaded.snapshots.map((host) => {
|
|
197
208
|
try {
|
|
198
209
|
const selection = selectClaudeAccount({
|
|
@@ -428,6 +439,7 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
428
439
|
existsSync: ctx.existsSync,
|
|
429
440
|
hostname,
|
|
430
441
|
probe: ctx.probeProvider,
|
|
442
|
+
readClaudeCredential: ctx.readClaudeCredential,
|
|
431
443
|
env: ctx.env,
|
|
432
444
|
});
|
|
433
445
|
if (status.logged_in !== true) {
|
|
@@ -567,6 +579,7 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
567
579
|
existsSync: ctx.existsSync,
|
|
568
580
|
hostname: ctx.hostname(),
|
|
569
581
|
probe: ctx.probeProvider,
|
|
582
|
+
readClaudeCredential: ctx.readClaudeCredential,
|
|
570
583
|
env: ctx.env,
|
|
571
584
|
});
|
|
572
585
|
if (status.logged_in !== true) {
|
|
@@ -584,6 +597,7 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
584
597
|
existsSync: ctx.existsSync,
|
|
585
598
|
hostname: ctx.hostname(),
|
|
586
599
|
probe: ctx.probeProvider,
|
|
600
|
+
readClaudeCredential: ctx.readClaudeCredential,
|
|
587
601
|
env: ctx.env,
|
|
588
602
|
}),
|
|
589
603
|
});
|
|
@@ -613,6 +627,7 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
613
627
|
existsSync: ctx.existsSync,
|
|
614
628
|
hostname: ctx.hostname(),
|
|
615
629
|
probe: ctx.probeProvider,
|
|
630
|
+
readClaudeCredential: ctx.readClaudeCredential,
|
|
616
631
|
env: ctx.env,
|
|
617
632
|
}),
|
|
618
633
|
});
|