@indigoai-us/hq-cli 5.345.145 → 5.345.147
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 +18 -0
- package/README.md +14 -0
- package/dist/command-catalog.generated.d.ts +37 -1
- package/dist/command-catalog.generated.js +45 -1
- package/dist/commands/auth.js +4 -2
- package/dist/commands/bot.js +1 -1
- package/dist/commands/capacity.js +2 -0
- package/dist/commands/files.js +10 -6
- package/dist/commands/lanes.js +2 -0
- package/dist/commands/login.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +185 -190
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/daemon.js +3 -0
- package/dist/lib/bot/runtime/index.js +3 -1
- package/dist/lib/capacity/route.d.ts +21 -1
- package/dist/lib/capacity/route.js +68 -1
- package/dist/lib/doctor/checks/release-links.d.ts +23 -0
- package/dist/lib/doctor/checks/release-links.js +152 -0
- package/dist/lib/doctor/registry.js +3 -0
- package/dist/lib/workers-registry/index.d.ts +6 -0
- package/dist/lib/workers-registry/index.js +8 -0
- package/dist/utils/headless.d.ts +2 -0
- package/dist/utils/headless.js +8 -0
- package/dist/utils/login-browser.d.ts +4 -0
- package/dist/utils/login-browser.js +122 -0
- package/dist/utils/team-upgrade.js +1 -7
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.87","bundleSha256":"916fb28a230f0f0dd83cfaa87366670512898636a137c74a4c989a261301cf5e","formatVersion":1}
|
package/dist/lib/bot/daemon.js
CHANGED
|
@@ -77,6 +77,8 @@ export function renderBotLaunchdPlist(p) {
|
|
|
77
77
|
["HOME", p.home],
|
|
78
78
|
["PATH", p.pathEntries.join(":")],
|
|
79
79
|
["HQ_NO_UPDATE_CHECK", "1"],
|
|
80
|
+
// Bots must never replace/relaunch the interactive desktop, including during setup.
|
|
81
|
+
["HQ_NO_ENSURE_HQ_DESKTOP", "1"],
|
|
80
82
|
...(p.hqRoot ? [["HQ_ROOT", p.hqRoot]] : []),
|
|
81
83
|
]
|
|
82
84
|
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
@@ -135,6 +137,7 @@ RestartSec=5
|
|
|
135
137
|
Environment=HOME=${p.home}
|
|
136
138
|
Environment=PATH=${p.pathEntries.join(":")}
|
|
137
139
|
Environment=HQ_NO_UPDATE_CHECK=1
|
|
140
|
+
Environment=HQ_NO_ENSURE_HQ_DESKTOP=1
|
|
138
141
|
${p.hqRoot ? `Environment=HQ_ROOT=${p.hqRoot}\n` : ""}StandardOutput=append:${p.logPath}
|
|
139
142
|
StandardError=append:${p.logPath}
|
|
140
143
|
|
|
@@ -345,7 +345,9 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
|
345
345
|
const recheck = memo !== undefined && (nowMs() - memo.at >= MODEL_FALLBACK_RECHECK_MS || memo.turns >= MODEL_FALLBACK_RECHECK_TURNS);
|
|
346
346
|
const remembered = memo && !recheck ? memo : undefined;
|
|
347
347
|
const args = runtime.buildArgs(remembered ? { ...turnInput, model: remembered.to } : turnInput, { outputFile, newSessionId });
|
|
348
|
-
|
|
348
|
+
// Enforce at the model boundary too: old plists and direct `bot run` callers
|
|
349
|
+
// may lack the guard. A bot prompt must never quit the user's desktop app.
|
|
350
|
+
const env = { ...(input.env ?? deps.env ?? process.env), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
349
351
|
const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
|
|
350
352
|
const stdin = runtime.stdinInput?.(turnInput);
|
|
351
353
|
const protocol = runtime.spawnProtocol?.(turnInput);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LaneRecord, LaneState, LanesConfig } from "../lanes/types.js";
|
|
2
2
|
import { type LaneProvider } from "../lanes/types.js";
|
|
3
|
-
import { type NowLimit, type NowView } from "./ledger.js";
|
|
3
|
+
import { type NowAccount, type NowLimit, type NowView } from "./ledger.js";
|
|
4
4
|
/** All lane providers, in the stable order boss-route uses for ranking ties. */
|
|
5
5
|
export declare const ROUTE_PROVIDERS: readonly LaneProvider[];
|
|
6
6
|
/** Lane states that count against an account's live load. */
|
|
@@ -28,6 +28,12 @@ export declare const OLD_READING_PENALTY = 10;
|
|
|
28
28
|
* view stale). Sits below any account with fresh known headroom.
|
|
29
29
|
*/
|
|
30
30
|
export declare const UNKNOWN_USAGE_SCORE = -100;
|
|
31
|
+
/**
|
|
32
|
+
* Score for an account whose plan limit is used up but which still runs on
|
|
33
|
+
* paid extra usage. Ranked only with --allow-extra-usage, below every account
|
|
34
|
+
* with plan room or unknown usage.
|
|
35
|
+
*/
|
|
36
|
+
export declare const EXTRA_USAGE_SCORE = -200;
|
|
31
37
|
/** Default --floor applied by the CLI (percent headroom to clear). */
|
|
32
38
|
export declare const DEFAULT_FLOOR = 15;
|
|
33
39
|
/** Default --prefer order applied by the CLI. */
|
|
@@ -85,6 +91,8 @@ export interface RouteRanked {
|
|
|
85
91
|
lane_flags: string;
|
|
86
92
|
/** True when no usable reading exists and the account is ranked as unknown. */
|
|
87
93
|
usage_unknown: boolean;
|
|
94
|
+
/** True when a plan limit is used up and the lane would spend paid extra usage. */
|
|
95
|
+
extra_usage?: boolean;
|
|
88
96
|
}
|
|
89
97
|
/** Live lane on an account the router would avoid; command names a replacement. */
|
|
90
98
|
export interface RouteMove {
|
|
@@ -130,6 +138,12 @@ export interface PickAccountInput {
|
|
|
130
138
|
capacityStale?: boolean;
|
|
131
139
|
/** Error summary when the lanes list could not be read. */
|
|
132
140
|
lanesError?: string | null;
|
|
141
|
+
/**
|
|
142
|
+
* Owner opt-in (2026-10-17): rank an account whose plan limit is used up
|
|
143
|
+
* but whose Claude extra usage is enabled and not capped. Off by default so
|
|
144
|
+
* lanes never spend paid usage without being asked to.
|
|
145
|
+
*/
|
|
146
|
+
allowExtraUsage?: boolean;
|
|
133
147
|
}
|
|
134
148
|
export declare class RouteUsageError extends Error {
|
|
135
149
|
readonly code = "route_usage";
|
|
@@ -141,6 +155,12 @@ export declare function resolveRequestedProviders(raw: readonly string[] | undef
|
|
|
141
155
|
export declare function resolvePreferOrder(raw: readonly string[] | undefined): LaneProvider[];
|
|
142
156
|
/** Parse `--floor <n>`; 0..100 inclusive, default DEFAULT_FLOOR when missing. */
|
|
143
157
|
export declare function resolveFloor(raw: string | number | undefined): number;
|
|
158
|
+
/**
|
|
159
|
+
* True when the account's Claude extra usage reading says the provider will
|
|
160
|
+
* keep serving it after a plan limit runs out: enabled, not at its monthly
|
|
161
|
+
* cap, and not stopped by a spend control.
|
|
162
|
+
*/
|
|
163
|
+
export declare function hasExtraUsageRoom(account: NowAccount): boolean;
|
|
144
164
|
/**
|
|
145
165
|
* Pure routing decision over a snapshot: scores every signed-in lane account,
|
|
146
166
|
* applies the --prefer order under the --floor headroom, returns the pick (or
|
|
@@ -47,6 +47,12 @@ export const OLD_READING_PENALTY = 10;
|
|
|
47
47
|
* view stale). Sits below any account with fresh known headroom.
|
|
48
48
|
*/
|
|
49
49
|
export const UNKNOWN_USAGE_SCORE = -100;
|
|
50
|
+
/**
|
|
51
|
+
* Score for an account whose plan limit is used up but which still runs on
|
|
52
|
+
* paid extra usage. Ranked only with --allow-extra-usage, below every account
|
|
53
|
+
* with plan room or unknown usage.
|
|
54
|
+
*/
|
|
55
|
+
export const EXTRA_USAGE_SCORE = -200;
|
|
50
56
|
/** Default --floor applied by the CLI (percent headroom to clear). */
|
|
51
57
|
export const DEFAULT_FLOOR = 15;
|
|
52
58
|
/** Default --prefer order applied by the CLI. */
|
|
@@ -178,6 +184,27 @@ function liveLaneCount(lanes, provider, account) {
|
|
|
178
184
|
}
|
|
179
185
|
return count;
|
|
180
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* True when the account's Claude extra usage reading says the provider will
|
|
189
|
+
* keep serving it after a plan limit runs out: enabled, not at its monthly
|
|
190
|
+
* cap, and not stopped by a spend control.
|
|
191
|
+
*/
|
|
192
|
+
export function hasExtraUsageRoom(account) {
|
|
193
|
+
const limits = account.limits ?? [];
|
|
194
|
+
const extra = limits.find((l) => l.kind === "credits" && l.limitId === "extra_usage");
|
|
195
|
+
if (!extra || extra.hasCredits !== true)
|
|
196
|
+
return false;
|
|
197
|
+
if (extra.spendControlReached === true)
|
|
198
|
+
return false;
|
|
199
|
+
if (typeof extra.usedPercent === "number" && extra.usedPercent >= 100)
|
|
200
|
+
return false;
|
|
201
|
+
const spend = limits.find((l) => l.kind === "credits" && l.limitId === "spend");
|
|
202
|
+
if (spend?.spendControlReached === true)
|
|
203
|
+
return false;
|
|
204
|
+
if (spend && typeof spend.usedPercent === "number" && spend.usedPercent >= 100)
|
|
205
|
+
return false;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
181
208
|
function limitAgeMs(view, nowMs) {
|
|
182
209
|
if (!view.resetPassed || !view.limit.resetsAt)
|
|
183
210
|
return view.readAge;
|
|
@@ -368,9 +395,49 @@ export function pickAccount(input) {
|
|
|
368
395
|
&& row.readAge <= LIMIT_STALE_MS
|
|
369
396
|
&& (row.limit.state === "exhausted" || (typeof row.used === "number" && row.used >= 100)));
|
|
370
397
|
if (exhausted) {
|
|
398
|
+
const extraRoom = providerId === "claude" && hasExtraUsageRoom(account);
|
|
399
|
+
if (extraRoom && input.allowExtraUsage === true) {
|
|
400
|
+
const live = liveLaneCount(lanes, providerId, laneMatch.id);
|
|
401
|
+
const weekly = applicable.find((l) => l.kind === "weekly_all");
|
|
402
|
+
const whyParts = [
|
|
403
|
+
`${labelLimit(exhausted.limit)} used up; runs on paid extra usage (--allow-extra-usage)`,
|
|
404
|
+
];
|
|
405
|
+
if (live)
|
|
406
|
+
whyParts.push(`${live} live lane${live === 1 ? "" : "s"} already on it`);
|
|
407
|
+
if (tokenExpired)
|
|
408
|
+
whyParts.push("sign-in renews when the lane starts");
|
|
409
|
+
ranked.push({
|
|
410
|
+
provider: providerId,
|
|
411
|
+
account: laneMatch.id,
|
|
412
|
+
label,
|
|
413
|
+
organization: account.organization ?? null,
|
|
414
|
+
headroom: 0,
|
|
415
|
+
score: EXTRA_USAGE_SCORE - live * LANE_PENALTY,
|
|
416
|
+
live_lanes: live,
|
|
417
|
+
reset_soon: false,
|
|
418
|
+
token_expired: tokenExpired,
|
|
419
|
+
reading_age_h: Math.round(limitAgeMs(exhausted, nowMs) / 36e5),
|
|
420
|
+
binding: {
|
|
421
|
+
limit: labelLimit(exhausted.limit),
|
|
422
|
+
used_percent: typeof exhausted.used === "number" ? exhausted.used : 100,
|
|
423
|
+
resets_at: exhausted.limit.resetsAt ?? null,
|
|
424
|
+
},
|
|
425
|
+
weekly_resets_at: weekly?.resetsAt ?? null,
|
|
426
|
+
why: whyParts.join("; "),
|
|
427
|
+
lane_flags: `--provider ${providerId} --account ${laneMatch.id}`,
|
|
428
|
+
usage_unknown: false,
|
|
429
|
+
extra_usage: true,
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
371
433
|
skipped.push({
|
|
372
434
|
...base,
|
|
373
|
-
reason:
|
|
435
|
+
reason: extraRoom
|
|
436
|
+
? `${labelLimit(exhausted.limit)} used up; extra usage is on`
|
|
437
|
+
: `${labelLimit(exhausted.limit)} used up`,
|
|
438
|
+
...(extraRoom
|
|
439
|
+
? { fix: "pass --allow-extra-usage to run on paid extra usage" }
|
|
440
|
+
: {}),
|
|
374
441
|
resets_at: exhausted.limit.resetsAt ?? null,
|
|
375
442
|
});
|
|
376
443
|
continue;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-shipped links (hq-core mode 120000). An update that junctions a
|
|
3
|
+
* directory at the staging clone, then deletes that clone, leaves a link that
|
|
4
|
+
* does not resolve (feedback_3e1a7445). `hq doctor` reports those and the
|
|
5
|
+
* repair step. A link that resolves somewhere else inside the HQ root is a
|
|
6
|
+
* user customization and is left alone.
|
|
7
|
+
*/
|
|
8
|
+
import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
9
|
+
export declare const RELEASE_LINKS_FAMILY_ID = "release-links";
|
|
10
|
+
export declare const RELEASE_LINKS_FAMILY_TITLE = "Release links";
|
|
11
|
+
export declare const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
|
|
12
|
+
export declare const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y` again. On Windows without Developer Mode, a directory link is written as a junction and a file link as a copy.";
|
|
13
|
+
interface ReleaseLink {
|
|
14
|
+
rel: string;
|
|
15
|
+
linkText: string;
|
|
16
|
+
kind: "dir" | "file";
|
|
17
|
+
}
|
|
18
|
+
/** Paths git records as symlinks in the hq-core release. */
|
|
19
|
+
export declare const RELEASE_LINKS: readonly ReleaseLink[];
|
|
20
|
+
export declare const releaseLinksFamily: CheckFamily;
|
|
21
|
+
export declare function checkReleaseLinks(context: CheckContext): CheckResult[];
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=release-links.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-shipped links (hq-core mode 120000). An update that junctions a
|
|
3
|
+
* directory at the staging clone, then deletes that clone, leaves a link that
|
|
4
|
+
* does not resolve (feedback_3e1a7445). `hq doctor` reports those and the
|
|
5
|
+
* repair step. A link that resolves somewhere else inside the HQ root is a
|
|
6
|
+
* user customization and is left alone.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
export const RELEASE_LINKS_FAMILY_ID = "release-links";
|
|
11
|
+
export const RELEASE_LINKS_FAMILY_TITLE = "Release links";
|
|
12
|
+
export const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
|
|
13
|
+
export const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y` again. On Windows without Developer Mode, a directory link is written as a junction and a file link as a copy.";
|
|
14
|
+
const CYGWIN_SYMLINK_COOKIE = Buffer.from("!<symlink>", "latin1");
|
|
15
|
+
/** Paths git records as symlinks in the hq-core release. */
|
|
16
|
+
export const RELEASE_LINKS = [
|
|
17
|
+
{ rel: ".agents/skills", linkText: "../.claude/skills", kind: "dir" },
|
|
18
|
+
{ rel: ".codex/claude", linkText: "../.claude", kind: "dir" },
|
|
19
|
+
{
|
|
20
|
+
rel: ".codex/output-style.md",
|
|
21
|
+
linkText: "../.claude/output-styles/hq.md",
|
|
22
|
+
kind: "file",
|
|
23
|
+
},
|
|
24
|
+
{ rel: "AGENTS.md", linkText: ".claude/CLAUDE.md", kind: "file" },
|
|
25
|
+
{
|
|
26
|
+
rel: "companies/_template/.obsidian",
|
|
27
|
+
linkText: "../../.obsidian",
|
|
28
|
+
kind: "dir",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
export const releaseLinksFamily = {
|
|
32
|
+
id: RELEASE_LINKS_FAMILY_ID,
|
|
33
|
+
title: RELEASE_LINKS_FAMILY_TITLE,
|
|
34
|
+
run: (context) => Promise.resolve(checkReleaseLinks(context)),
|
|
35
|
+
};
|
|
36
|
+
export function checkReleaseLinks(context) {
|
|
37
|
+
const present = RELEASE_LINKS.filter((entry) => lstatOrNull(path.join(context.hqRoot, entry.rel)) !== null);
|
|
38
|
+
if (present.length === 0) {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
status: "NA",
|
|
42
|
+
checkId: RELEASE_LINKS_CHECK_ID,
|
|
43
|
+
target: context.hqRoot,
|
|
44
|
+
message: "Skipped: this HQ root has none of the release-shipped links.",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
const failures = [];
|
|
49
|
+
for (const entry of present) {
|
|
50
|
+
const problem = releaseLinkProblem(context.hqRoot, entry);
|
|
51
|
+
if (!problem)
|
|
52
|
+
continue;
|
|
53
|
+
failures.push({
|
|
54
|
+
status: "FAIL",
|
|
55
|
+
checkId: RELEASE_LINKS_CHECK_ID,
|
|
56
|
+
target: entry.rel,
|
|
57
|
+
message: `${entry.rel} ${problem}`,
|
|
58
|
+
remediation: RELEASE_LINK_REMEDIATION,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (failures.length > 0)
|
|
62
|
+
return failures;
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
status: "PASS",
|
|
66
|
+
checkId: RELEASE_LINKS_CHECK_ID,
|
|
67
|
+
target: context.hqRoot,
|
|
68
|
+
message: "Release links resolve inside this HQ root.",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
function releaseLinkProblem(hqRoot, entry) {
|
|
73
|
+
const destination = path.join(hqRoot, entry.rel);
|
|
74
|
+
const lst = lstatOrNull(destination);
|
|
75
|
+
if (!lst)
|
|
76
|
+
return null;
|
|
77
|
+
if (lst.isSymbolicLink()) {
|
|
78
|
+
let resolved;
|
|
79
|
+
try {
|
|
80
|
+
resolved = fs.realpathSync.native(destination);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return "does not resolve. Its target is missing, often because an update linked it at a staging folder that was then deleted.";
|
|
84
|
+
}
|
|
85
|
+
if (!pathIsInside(hqRoot, resolved)) {
|
|
86
|
+
return "points outside this HQ root, so it does not resolve to the shipped target.";
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (lst.isFile()) {
|
|
91
|
+
const bytes = readFileOrNull(destination);
|
|
92
|
+
if (bytes && isSymlinkStub(bytes, entry.linkText)) {
|
|
93
|
+
return "is still a Git symlink stub, not a working link.";
|
|
94
|
+
}
|
|
95
|
+
if (entry.kind === "dir") {
|
|
96
|
+
return "is a file, not a directory link.";
|
|
97
|
+
}
|
|
98
|
+
const expected = containedTarget(hqRoot, entry);
|
|
99
|
+
if (expected) {
|
|
100
|
+
const target = readFileOrNull(expected);
|
|
101
|
+
if (target && bytes && bytes.equals(target))
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// A regular file that is not the stub is a user edit or a Windows copy
|
|
105
|
+
// of the target. Either way the path is usable.
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// A real directory in place of a directory link is a user replacement.
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function containedTarget(hqRoot, entry) {
|
|
112
|
+
const destination = path.join(hqRoot, entry.rel);
|
|
113
|
+
const resolved = path.resolve(path.dirname(destination), entry.linkText);
|
|
114
|
+
return pathIsInside(hqRoot, resolved) ? resolved : null;
|
|
115
|
+
}
|
|
116
|
+
function isSymlinkStub(bytes, linkText) {
|
|
117
|
+
if (bytes.equals(Buffer.from(linkText, "utf8")))
|
|
118
|
+
return true;
|
|
119
|
+
return (bytes.length >= CYGWIN_SYMLINK_COOKIE.length &&
|
|
120
|
+
bytes.subarray(0, CYGWIN_SYMLINK_COOKIE.length).equals(CYGWIN_SYMLINK_COOKIE));
|
|
121
|
+
}
|
|
122
|
+
function pathIsInside(parent, child) {
|
|
123
|
+
const parentAbs = path.resolve(parent);
|
|
124
|
+
const childAbs = path.resolve(child);
|
|
125
|
+
if (childAbs === parentAbs || childAbs.startsWith(parentAbs + path.sep))
|
|
126
|
+
return true;
|
|
127
|
+
try {
|
|
128
|
+
const parentReal = fs.realpathSync.native(parentAbs);
|
|
129
|
+
const childReal = fs.realpathSync.native(childAbs);
|
|
130
|
+
return childReal === parentReal || childReal.startsWith(parentReal + path.sep);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function lstatOrNull(target) {
|
|
137
|
+
try {
|
|
138
|
+
return fs.lstatSync(target);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function readFileOrNull(target) {
|
|
145
|
+
try {
|
|
146
|
+
return fs.readFileSync(target);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=release-links.js.map
|
|
@@ -26,6 +26,7 @@ import { botsFamily } from "./checks/bots.js";
|
|
|
26
26
|
import { anywhereFamily } from "./checks/anywhere.js";
|
|
27
27
|
import { scaffoldCliFloorFamily } from "./checks/scaffold-cli-floor.js";
|
|
28
28
|
import { strandedLanesFamily } from "./checks/stranded-lanes.js";
|
|
29
|
+
import { releaseLinksFamily } from "./checks/release-links.js";
|
|
29
30
|
import { fixtureCoverageFamily } from "./fixtures/discover.js";
|
|
30
31
|
import { checkClaudeWiring } from "./checks/claude-wiring.js";
|
|
31
32
|
/**
|
|
@@ -270,6 +271,8 @@ export function createDefaultRegistry() {
|
|
|
270
271
|
// Failed keep-alive lead lanes cannot recover themselves; make them visible
|
|
271
272
|
// in the standard local doctor report with the senior recovery command.
|
|
272
273
|
registry.register(strandedLanesFamily);
|
|
274
|
+
// Release symlinks left pointing at a deleted update staging folder.
|
|
275
|
+
registry.register(releaseLinksFamily);
|
|
273
276
|
return registry;
|
|
274
277
|
}
|
|
275
278
|
//# sourceMappingURL=registry.js.map
|
|
@@ -23,6 +23,12 @@ export interface GenerateWorkersRegistryResult {
|
|
|
23
23
|
* own, and because duplicates resolve by path order — a tenant path sorts before
|
|
24
24
|
* both `core` and `personal` — the checkout wins. The operator's actual worker
|
|
25
25
|
* then silently vanishes from the registry every skill reads.
|
|
26
|
+
*
|
|
27
|
+
* Claude Code worktrees (`.claude/worktrees/<name>/`) and Codex worktrees
|
|
28
|
+
* (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
|
|
29
|
+
* nested HQ root under a company project. Those copies land in the registry as
|
|
30
|
+
* `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
|
|
31
|
+
* `hq lanes create` then fails with `worker_tenancy_unresolved`.
|
|
26
32
|
*/
|
|
27
33
|
export declare function isExcludedWorkerDirectory(relativeDirectory: string): boolean;
|
|
28
34
|
/**
|
|
@@ -157,9 +157,17 @@ function withoutTimestamp(content) {
|
|
|
157
157
|
* own, and because duplicates resolve by path order — a tenant path sorts before
|
|
158
158
|
* both `core` and `personal` — the checkout wins. The operator's actual worker
|
|
159
159
|
* then silently vanishes from the registry every skill reads.
|
|
160
|
+
*
|
|
161
|
+
* Claude Code worktrees (`.claude/worktrees/<name>/`) and Codex worktrees
|
|
162
|
+
* (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
|
|
163
|
+
* nested HQ root under a company project. Those copies land in the registry as
|
|
164
|
+
* `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
|
|
165
|
+
* `hq lanes create` then fails with `worker_tenancy_unresolved`.
|
|
160
166
|
*/
|
|
161
167
|
export function isExcludedWorkerDirectory(relativeDirectory) {
|
|
162
168
|
const normalized = relativeDirectory.replaceAll('\\', '/');
|
|
169
|
+
if (/(?:^|\/)\.(?:claude|codex)\/worktrees(?:\/|$)/.test(normalized))
|
|
170
|
+
return true;
|
|
163
171
|
const [root, tenant, nested] = normalized.split('/');
|
|
164
172
|
if (root !== 'companies')
|
|
165
173
|
return /(?:^|\/)_overrides(?:\/|$)/.test(normalized);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CognitoAuthConfig, CognitoTokens } from "@indigoai-us/hq-cloud";
|
|
2
|
+
/** Keep desktop login unchanged; manual login uses the same PKCE loopback flow. */
|
|
3
|
+
export declare function loginWithBrowserPreference(config: CognitoAuthConfig, browser?: boolean): Promise<CognitoTokens>;
|
|
4
|
+
//# sourceMappingURL=login-browser.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { browserLogin, saveCachedTokens, CognitoAuthError } from "@indigoai-us/hq-cloud";
|
|
4
|
+
import { isHeadless } from "./headless.js";
|
|
5
|
+
function assertHumanLoginAllowed() {
|
|
6
|
+
// Preserve hq-cloud's company-machine boundary on the manual URL path.
|
|
7
|
+
if (["1", "true", "yes"].includes(process.env.HQ_MACHINE_AUTH_REQUIRED?.trim().toLowerCase() ?? "")) {
|
|
8
|
+
throw new CognitoAuthError("Human authentication is unavailable in required company machine mode");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Keep desktop login unchanged; manual login uses the same PKCE loopback flow. */
|
|
12
|
+
export async function loginWithBrowserPreference(config, browser = true) {
|
|
13
|
+
if (browser && !isHeadless())
|
|
14
|
+
return browserLogin(config);
|
|
15
|
+
assertHumanLoginAllowed();
|
|
16
|
+
const port = config.port ?? 3000;
|
|
17
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
18
|
+
const base = `https://${config.userPoolDomain}.auth.${config.region}.amazoncognito.com`;
|
|
19
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
20
|
+
const state = randomBytes(16).toString("base64url");
|
|
21
|
+
const url = new URL(`${base}/oauth2/authorize`);
|
|
22
|
+
url.search = new URLSearchParams({
|
|
23
|
+
client_id: config.clientId,
|
|
24
|
+
response_type: "code",
|
|
25
|
+
scope: (config.scopes ?? ["openid", "email", "profile"]).join(" "),
|
|
26
|
+
redirect_uri: redirectUri,
|
|
27
|
+
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
28
|
+
code_challenge_method: "S256",
|
|
29
|
+
state,
|
|
30
|
+
}).toString();
|
|
31
|
+
if (config.identityProvider)
|
|
32
|
+
url.searchParams.set("identity_provider", config.identityProvider);
|
|
33
|
+
if (config.prompt)
|
|
34
|
+
url.searchParams.set("prompt", config.prompt);
|
|
35
|
+
const code = await new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false;
|
|
37
|
+
const handler = (req, res) => {
|
|
38
|
+
if (settled) {
|
|
39
|
+
res.writeHead(410).end("Sign-in already finished");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const callback = new URL(req.url ?? "/", redirectUri);
|
|
43
|
+
if (callback.pathname !== "/callback") {
|
|
44
|
+
res.writeHead(404).end("Not found");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const receivedState = Buffer.from(callback.searchParams.get("state") ?? "");
|
|
48
|
+
const expectedState = Buffer.from(state);
|
|
49
|
+
const validState = receivedState.length === expectedState.length
|
|
50
|
+
&& timingSafeEqual(receivedState, expectedState);
|
|
51
|
+
const code = callback.searchParams.get("code");
|
|
52
|
+
if (!validState || !code || callback.searchParams.has("error")) {
|
|
53
|
+
res.writeHead(400).end("Sign-in failed. Return to your terminal and try again.");
|
|
54
|
+
finish(new CognitoAuthError(validState ? "Sign-in callback was denied or missing a code" : "Cognito state parameter mismatch"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
res.writeHead(200, { "Content-Type": "text/plain" }).end("Sign-in response received. Return to your terminal to finish.");
|
|
58
|
+
finish();
|
|
59
|
+
resolve(code);
|
|
60
|
+
};
|
|
61
|
+
const servers = [createServer(handler), createServer(handler)];
|
|
62
|
+
const timer = setTimeout(() => finish(new CognitoAuthError("Login timed out after 15 minutes")), 15 * 60 * 1000);
|
|
63
|
+
function finish(error) {
|
|
64
|
+
settled = true;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
for (const server of servers)
|
|
67
|
+
server.close();
|
|
68
|
+
if (error)
|
|
69
|
+
reject(error);
|
|
70
|
+
}
|
|
71
|
+
let attempts = 0;
|
|
72
|
+
let listening = 0;
|
|
73
|
+
let lastError;
|
|
74
|
+
const ready = () => {
|
|
75
|
+
if (++attempts !== servers.length)
|
|
76
|
+
return;
|
|
77
|
+
if (!listening) {
|
|
78
|
+
finish(lastError ?? new Error("Cannot bind login callback port"));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
assertHumanLoginAllowed();
|
|
83
|
+
console.log(`Open this URL to sign in:\n${url.toString()}`);
|
|
84
|
+
console.log(`Waiting for sign-in. For a remote host, forward local port ${port} to this host's port ${port} before opening the URL.`);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
finish(error instanceof Error ? error : new Error("Sign-in unavailable"));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
servers.forEach((server, index) => {
|
|
91
|
+
server.once("error", (error) => { lastError = error; ready(); });
|
|
92
|
+
server.listen(port, index === 0 ? "127.0.0.1" : "::1", () => { listening++; ready(); });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
assertHumanLoginAllowed();
|
|
96
|
+
const response = await fetch(`${base}/oauth2/token`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
99
|
+
body: new URLSearchParams({ grant_type: "authorization_code", client_id: config.clientId, code, code_verifier: verifier, redirect_uri: redirectUri }),
|
|
100
|
+
signal: AbortSignal.timeout(30_000),
|
|
101
|
+
});
|
|
102
|
+
// Never echo an OAuth response body, which can contain credentials.
|
|
103
|
+
if (!response.ok)
|
|
104
|
+
throw new CognitoAuthError(`Token exchange failed (${response.status})`);
|
|
105
|
+
const data = await response.json().catch(() => {
|
|
106
|
+
throw new CognitoAuthError("Cognito returned an invalid token response");
|
|
107
|
+
});
|
|
108
|
+
if (!data || typeof data.access_token !== "string" || !data.access_token
|
|
109
|
+
|| typeof data.id_token !== "string" || !data.id_token
|
|
110
|
+
|| typeof data.refresh_token !== "string" || !data.refresh_token
|
|
111
|
+
|| typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
112
|
+
throw new CognitoAuthError("Cognito returned an incomplete token response");
|
|
113
|
+
}
|
|
114
|
+
const tokens = {
|
|
115
|
+
accessToken: data.access_token, idToken: data.id_token, refreshToken: data.refresh_token,
|
|
116
|
+
expiresAt: Date.now() + data.expires_in * 1000, tokenType: "Bearer",
|
|
117
|
+
};
|
|
118
|
+
assertHumanLoginAllowed();
|
|
119
|
+
saveCachedTokens(tokens);
|
|
120
|
+
return tokens;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=login-browser.js.map
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import { isHeadless } from "./headless.js";
|
|
1
2
|
import open from "open";
|
|
2
3
|
import { ensureCognitoToken } from "./cognito-session.js";
|
|
3
4
|
import { getCompanyUid, vaultApiFetch } from "./vault-api.js";
|
|
4
5
|
const CHECKOUT_SUCCESS_URL = "https://hq.computer/billing?checkout=success";
|
|
5
6
|
const CHECKOUT_CANCEL_URL = "https://hq.computer/billing?checkout=cancel";
|
|
6
|
-
function isHeadless() {
|
|
7
|
-
if (process.env.CI)
|
|
8
|
-
return true;
|
|
9
|
-
if (process.platform !== "linux")
|
|
10
|
-
return false;
|
|
11
|
-
return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
12
|
-
}
|
|
13
7
|
function ownerUpgradeError() {
|
|
14
8
|
return Object.assign(new Error("Only a company owner can upgrade. Ask a company owner to upgrade."), { expected: true });
|
|
15
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.147",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
50
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
51
|
-
"@indigoai-us/hq-cloud": "~6.18.
|
|
51
|
+
"@indigoai-us/hq-cloud": "~6.18.87",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|