@indigoai-us/hq-cli 5.345.203 → 5.345.204
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 +9 -0
- package/dist/commands/bot.js +7 -2
- package/dist/commands/cloud.d.ts +14 -2
- package/dist/commands/cloud.js +25 -16
- package/dist/commands/sync-core.js +2 -0
- package/dist/lib/bot/claude-login.d.ts +7 -1
- package/dist/lib/bot/claude-login.js +56 -16
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +53 -5
- package/dist/lib/capacity/collectors/claude-oauth.js +251 -71
- package/dist/lib/daemon/catalog.js +2 -2
- package/dist/lib/daemon/run.js +2 -2
- package/dist/lib/lanes/config.js +13 -0
- package/dist/lib/lanes/throttle.d.ts +12 -1
- package/dist/lib/lanes/throttle.js +27 -8
- package/dist/lib/lanes/types.d.ts +5 -0
- package/dist/lib/signals/steps/extract.d.ts +110 -4
- package/dist/lib/signals/steps/extract.js +520 -39
- package/dist/lib/sync-core/engine-home.d.ts +2 -1
- package/dist/lib/sync-core/engine-home.js +25 -8
- package/dist/lib/sync-core/reconcile.d.ts +3 -1
- package/dist/lib/sync-core/reconcile.js +21 -8
- package/dist/lib/sync-core/release.d.ts +14 -5
- package/dist/lib/sync-core/release.js +58 -14
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.204] — 2026-11-05
|
|
6
|
+
- `hq bot create`, `hq doctor` and the Claude bot sign-in check now read the macOS Keychain item that Claude Code keeps under your login account name first. A Mac can also hold an older item for the same service under another account (often "unknown", with no Claude login in it). The check used to read that one and said Claude was not signed in right after you signed in from HQ Desktop, so Create bot failed.
|
|
7
|
+
- `hq bot create --json` now includes `brainSignIn` (the same shape `hq bot list`, `status` and `start` report) when it refuses a Claude bot because Claude Code is not signed in, so HQ Desktop can offer its own sign-in instead of "Could not create".
|
|
8
|
+
- `hq capacity` renews expired Claude tokens on a busy Mac again. The refresh check reads the lane list once per collect instead of once per account, runs its two reads together without blocking the mesh daemon, skips the Outpost mirror refresh it does not need, and allows each read 30 seconds instead of 15. On macOS it now reads each running `claude` process's environment, so an open session on the default home no longer blocks the refresh of every other account home. A process whose environment cannot be read still blocks every home, and if the lane read fails no token is refreshed. A session started with `HOME` set to another directory and no `CLAUDE_CONFIG_DIR` now blocks the refresh of that directory's `.claude` home.
|
|
9
|
+
- `hq signals run` now runs the extract step on the Grok CLI when the main local bot runs on Grok, instead of failing every source item with "grok has no restricted tool mode". Each turn runs grok 1.0.70 or newer in `dontAsk` mode with only the file reader allowed, grok's own read-only shell list denied, the MCP bridge, web tools and subagents off, a turn cap, and a per-turn `strict` sandbox profile that can read only the item's channel folder (plus the scripts the candidate command needs) and grants no extra writes. Reads of the Grok login are denied for the literal `~/.grok` spelling, the isolated home and the real home, because Grok matches a `~` path literally before expanding it. An older grok, or a main bot on grok with no grok installed, is never run unrestricted: the step uses Claude Code, then Codex, when that tool is installed and signed in, records the fallback in the run's step detail, and otherwise fails the run naming the grok version it needs. On macOS and Windows the step does not select Grok, because Grok only blocks child-process network on Linux, and falls through the same way. Grok has no terminal tool: HQ validates its returned candidate commands, checks the pinned writer still matches the script it generated, and then invokes that writer with fixed arguments, rejecting shell chaining, substitutions and redirections. The host writes candidates after the turn. The resolved Grok executable is pinned for the probe and turn, and probe caching distinguishes changes to PATH. Its isolated temporary home carries only a private login-file copy, excludes user configuration and hooks, and is removed when the turn ends, and its environment is an allowlist.
|
|
10
|
+
- `hq lanes` admission on macOS no longer holds every new lane while `kern.memorystatus_vm_pressure_level` is warn. A warn level now holds only when the free percentage (`kern.memorystatus_level`) is at or below `throttle.memory_warn_min_free_percent` (default 20, a number from 0 to 100); critical still always holds, and `throttle.min_available_mem_mb` still applies. The hold message states this rule.
|
|
11
|
+
- The daemon's new-sync-engine units now plan on Windows. A Windows machine downloads the `windows-x64` or `windows-arm64` zip, installs `hq-syncd.exe` (moving a running copy aside to `hq-syncd.exe.old`), and writes an engine home with no symlinks: the config names the token file and the engine reads `menubar.json` and `work-mesh/` from the daemon's state folder. Windows vaults stay on the current sync, and no engine is downloaded, until `SYNC_CORE_WINDOWS_FULL_RELEASE` names an engine release whose Windows build runs full sync; it is unset in this release. One machine can test a build first by setting `HQ_SYNC_CORE_WINDOWS_FULL_RELEASE` to that release.
|
|
12
|
+
- Show the upload-hold explanation and recovery command in sync status.
|
|
13
|
+
|
|
5
14
|
## [5.345.203] — 2026-11-05
|
|
6
15
|
- Fix Codex capacity routing after plan exhaustion: available credits keep accounts usable at lowest priority, and superseded snapshots or passed resets no longer retain exhausted usage.
|
|
7
16
|
- Keep company Outpost integration commands on their delegated machine session and allow sharing connections with an `out_*` principal.
|
package/dist/commands/bot.js
CHANGED
|
@@ -935,7 +935,11 @@ function requireClaudeCodeLoginForCreate(name) {
|
|
|
935
935
|
const check = botCreateSeams.claudeLogin();
|
|
936
936
|
if (check.signedIn)
|
|
937
937
|
return;
|
|
938
|
-
|
|
938
|
+
const reason = `Claude Code is not signed in on this computer, so ${name} could not answer.`;
|
|
939
|
+
// The same `brainSignIn` shape list, status and start report, so the
|
|
940
|
+
// desktop app offers its own sign-in instead of showing a terminal command.
|
|
941
|
+
const brainSignIn = { provider: "claude", needed: true, reason, command: "claude /login" };
|
|
942
|
+
throw Object.assign(new Error(`${reason} ${describeClaudeLoginSources(check)} ${claudeLoginFix()}`.replace(/\s+/g, " ").trim()), { expected: true, brainSignIn });
|
|
939
943
|
}
|
|
940
944
|
export const botStopSeams = {
|
|
941
945
|
/** True when this process runs inside the bot process `botPid` (one of its turns). */
|
|
@@ -1099,7 +1103,8 @@ export async function runBotCreate(nameArg, opts) {
|
|
|
1099
1103
|
// there too, not only a line on stderr.
|
|
1100
1104
|
if (opts.json && !err?.jsonReported) {
|
|
1101
1105
|
const message = err instanceof Error ? err.message : String(err);
|
|
1102
|
-
|
|
1106
|
+
const brainSignIn = err?.brainSignIn ?? null;
|
|
1107
|
+
console.log(JSON.stringify({ ok: false, name: nameArg, reason: "create-failed", message, ...(brainSignIn ? { brainSignIn } : {}) }, null, 2));
|
|
1103
1108
|
}
|
|
1104
1109
|
throw err;
|
|
1105
1110
|
}
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -210,15 +210,27 @@ export declare function scopeExcludedWarning(count: number): string | null;
|
|
|
210
210
|
* largest count it sees instead of a sum: a repeated emit for the same hold
|
|
211
211
|
* cannot inflate the total. A missing, non-numeric or negative count reads as 0.
|
|
212
212
|
*/
|
|
213
|
+
export interface IdentityHeldNotice {
|
|
214
|
+
count: number;
|
|
215
|
+
/** Plain-language hold from hq-cloud, when that build sends one. */
|
|
216
|
+
message?: string;
|
|
217
|
+
}
|
|
213
218
|
export declare function createIdentityHeldTracker(): {
|
|
214
219
|
observe: (event: {
|
|
215
220
|
type: string;
|
|
216
221
|
count?: unknown;
|
|
222
|
+
message?: unknown;
|
|
223
|
+
reason?: unknown;
|
|
217
224
|
}) => void;
|
|
218
225
|
count: () => number;
|
|
226
|
+
notice: () => IdentityHeldNotice;
|
|
219
227
|
};
|
|
220
|
-
/**
|
|
221
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Recovery lines for files held by the account-switch push fence.
|
|
230
|
+
* A newer hq-cloud sends `message` (file names and `hq sync release-held`).
|
|
231
|
+
* Older builds send a count only, and this keeps the previous sentence.
|
|
232
|
+
*/
|
|
233
|
+
export declare function identityHeldLines(count: number, notice?: Pick<IdentityHeldNotice, "message">): string[];
|
|
222
234
|
/** Actionable recovery for files refused because the vault baseline is stale. */
|
|
223
235
|
export declare function staleUploadRefusalLines(count: number, paths?: readonly string[]): string[];
|
|
224
236
|
/**
|
package/dist/commands/cloud.js
CHANGED
|
@@ -500,19 +500,9 @@ export function scopeExcludedWarning(count) {
|
|
|
500
500
|
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
501
501
|
`ask an admin to grant you write on those paths.`);
|
|
502
502
|
}
|
|
503
|
-
/**
|
|
504
|
-
* Counts the files share() held back because this sync scope is fenced after an account
|
|
505
|
-
* switch on this computer (feedback_a69e12ba). share() reports the hold only as
|
|
506
|
-
* an `identity-switch-push-held` event and leaves the files out of every
|
|
507
|
-
* result counter, so without this the push printed "Pushed 0 file(s) (0 B,
|
|
508
|
-
* 0 skipped)" as a success while the vault kept the old copy.
|
|
509
|
-
*
|
|
510
|
-
* Each event carries the size of one fenced hold, so the tracker keeps the
|
|
511
|
-
* largest count it sees instead of a sum: a repeated emit for the same hold
|
|
512
|
-
* cannot inflate the total. A missing, non-numeric or negative count reads as 0.
|
|
513
|
-
*/
|
|
514
503
|
export function createIdentityHeldTracker() {
|
|
515
504
|
let held = 0;
|
|
505
|
+
let message;
|
|
516
506
|
return {
|
|
517
507
|
observe(event) {
|
|
518
508
|
if (event.type !== "identity-switch-push-held")
|
|
@@ -521,12 +511,24 @@ export function createIdentityHeldTracker() {
|
|
|
521
511
|
? Math.max(0, Math.trunc(event.count))
|
|
522
512
|
: 0;
|
|
523
513
|
held = Math.max(held, n);
|
|
514
|
+
// A company rebind uses the same event type and keeps its own warning.
|
|
515
|
+
if (event.reason === "company-rebind")
|
|
516
|
+
return;
|
|
517
|
+
if (typeof event.message === "string" && event.message.trim())
|
|
518
|
+
message = event.message.trim();
|
|
524
519
|
},
|
|
525
520
|
count: () => held,
|
|
521
|
+
notice: () => ({ count: held, ...(message ? { message } : {}) }),
|
|
526
522
|
};
|
|
527
523
|
}
|
|
528
|
-
/**
|
|
529
|
-
|
|
524
|
+
/**
|
|
525
|
+
* Recovery lines for files held by the account-switch push fence.
|
|
526
|
+
* A newer hq-cloud sends `message` (file names and `hq sync release-held`).
|
|
527
|
+
* Older builds send a count only, and this keeps the previous sentence.
|
|
528
|
+
*/
|
|
529
|
+
export function identityHeldLines(count, notice) {
|
|
530
|
+
if (notice?.message)
|
|
531
|
+
return [`⚠ ${notice.message}`];
|
|
530
532
|
if (count <= 0)
|
|
531
533
|
return [];
|
|
532
534
|
return [
|
|
@@ -1566,7 +1568,7 @@ export function registerCloudCommands(program) {
|
|
|
1566
1568
|
for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
|
|
1567
1569
|
log(chalk.yellow(line));
|
|
1568
1570
|
}
|
|
1569
|
-
for (const line of identityHeldLines(identityHeldCount)) {
|
|
1571
|
+
for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
|
|
1570
1572
|
log(chalk.yellow(line));
|
|
1571
1573
|
}
|
|
1572
1574
|
await syncHealth.succeeded();
|
|
@@ -1599,7 +1601,12 @@ export function registerCloudCommands(program) {
|
|
|
1599
1601
|
for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
|
|
1600
1602
|
log(chalk.yellow(line));
|
|
1601
1603
|
}
|
|
1602
|
-
for (const line of identityHeldLines(identityHeldCount)) {
|
|
1604
|
+
for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
|
|
1605
|
+
log(chalk.yellow(line));
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
else if (identityHeld.notice().message) {
|
|
1609
|
+
for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
|
|
1603
1610
|
log(chalk.yellow(line));
|
|
1604
1611
|
}
|
|
1605
1612
|
}
|
|
@@ -2469,8 +2476,10 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2469
2476
|
const pushRefused = planLimitTracker.summary();
|
|
2470
2477
|
const pushUploadFailed = readUploadFailures(pushResult);
|
|
2471
2478
|
const pushHeldCount = pushIdentityHeld.count();
|
|
2479
|
+
const pushHeldNotice = pushIdentityHeld.notice();
|
|
2472
2480
|
const pushStatus = pushResult.aborted ||
|
|
2473
2481
|
pushHeldCount > 0 ||
|
|
2482
|
+
Boolean(pushHeldNotice.message) ||
|
|
2474
2483
|
pushResult.filesExcludedByScope > 0 ||
|
|
2475
2484
|
(pushResult.filesRefusedStale ?? 0) > 0 ||
|
|
2476
2485
|
pushUploadFailed.count > 0 ||
|
|
@@ -2498,7 +2507,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2498
2507
|
for (const line of uploadFailedLines(pushUploadFailed.count, pushUploadFailed.paths)) {
|
|
2499
2508
|
console.log(chalk.yellow(line));
|
|
2500
2509
|
}
|
|
2501
|
-
for (const line of identityHeldLines(pushHeldCount)) {
|
|
2510
|
+
for (const line of identityHeldLines(pushHeldCount, pushHeldNotice)) {
|
|
2502
2511
|
console.log(chalk.yellow(line));
|
|
2503
2512
|
}
|
|
2504
2513
|
if (pushResult.aborted) {
|
|
@@ -59,6 +59,8 @@ export async function runSyncCoreReconcile(opts, d = defaultDeps()) {
|
|
|
59
59
|
}),
|
|
60
60
|
readRolloutFlag: (companyUid) => readCompanyRolloutFlag(companyUid, token, { env: d.env }),
|
|
61
61
|
ensureBinary: (release) => ensureSyncCoreBinary({ home: d.home, platform: d.platform, arch: d.arch, release, env: d.env }),
|
|
62
|
+
platform: d.platform,
|
|
63
|
+
env: d.env,
|
|
62
64
|
readPin: () => readSyncCorePin({ env: d.env }),
|
|
63
65
|
log: opts.json ? () => { } : d.out,
|
|
64
66
|
});
|
|
@@ -21,7 +21,13 @@ export interface ClaudeLoginDeps {
|
|
|
21
21
|
env?: NodeJS.ProcessEnv;
|
|
22
22
|
home?: string;
|
|
23
23
|
platform?: NodeJS.Platform;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Reads the Keychain item; `account` scopes the read to one account name.
|
|
26
|
+
* Omitted account means "any item for the service" (the first one found).
|
|
27
|
+
*/
|
|
28
|
+
readKeychain?: (account?: string) => KeychainRead;
|
|
29
|
+
/** The login user's account name; null when unknown. */
|
|
30
|
+
username?: string | null;
|
|
25
31
|
now?: () => number;
|
|
26
32
|
}
|
|
27
33
|
export declare function checkClaudeCodeLogin(deps?: ClaudeLoginDeps): ClaudeLoginCheck;
|
|
@@ -17,9 +17,20 @@ import * as path from "node:path";
|
|
|
17
17
|
import { fleetHome } from "./fleet-local.js";
|
|
18
18
|
export const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
19
19
|
export const CLAUDE_LOGIN_ENV_VARS = ["ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"];
|
|
20
|
-
function
|
|
20
|
+
function safeUsername() {
|
|
21
21
|
try {
|
|
22
|
-
|
|
22
|
+
return os.userInfo().username || null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readKeychainWithSecurity(account) {
|
|
29
|
+
const args = account
|
|
30
|
+
? ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-a", account, "-w"]
|
|
31
|
+
: ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"];
|
|
32
|
+
try {
|
|
33
|
+
const stdout = execFileSync("security", args, {
|
|
23
34
|
encoding: "utf8",
|
|
24
35
|
stdio: ["ignore", "pipe", "pipe"],
|
|
25
36
|
timeout: 5000,
|
|
@@ -113,30 +124,59 @@ export function checkClaudeCodeLogin(deps = {}) {
|
|
|
113
124
|
sources.push({ source: "macos_keychain", status: "skipped", reason: "not macOS" });
|
|
114
125
|
}
|
|
115
126
|
else {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
// Claude Code (and the HQ desktop sign-in, which runs `claude auth login`)
|
|
128
|
+
// writes its login under the login user's account name. Another item with
|
|
129
|
+
// the same service can exist under a different account (an old one written
|
|
130
|
+
// as "unknown", or one holding only MCP OAuth), and an unscoped read
|
|
131
|
+
// returns whichever the Keychain finds first. Ask for the user's item
|
|
132
|
+
// first, then fall back to any item for the service.
|
|
133
|
+
const readKeychain = deps.readKeychain ?? readKeychainWithSecurity;
|
|
134
|
+
const username = deps.username === undefined ? safeUsername() : deps.username;
|
|
135
|
+
const recordOf = (result) => {
|
|
136
|
+
if (result.code !== 0)
|
|
137
|
+
return null;
|
|
122
138
|
let payload;
|
|
123
139
|
try {
|
|
124
|
-
payload = JSON.parse(
|
|
140
|
+
payload = JSON.parse(result.stdout.trim());
|
|
125
141
|
}
|
|
126
142
|
catch {
|
|
127
143
|
payload = null;
|
|
128
144
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
return payload && typeof payload === "object" ? oauthRecord(payload.claudeAiOauth) : null;
|
|
146
|
+
};
|
|
147
|
+
const scoped = username ? readKeychain(username) : null;
|
|
148
|
+
const scopedRecord = scoped ? recordOf(scoped) : null;
|
|
149
|
+
let read;
|
|
150
|
+
let record;
|
|
151
|
+
if (scoped && scopedRecord) {
|
|
152
|
+
read = scoped;
|
|
153
|
+
record = scopedRecord;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const any = readKeychain();
|
|
157
|
+
const anyRecord = recordOf(any);
|
|
158
|
+
if (anyRecord || !scoped || scoped.code !== 0) {
|
|
159
|
+
read = any;
|
|
160
|
+
record = anyRecord;
|
|
135
161
|
}
|
|
136
162
|
else {
|
|
137
|
-
|
|
163
|
+
read = scoped;
|
|
164
|
+
record = null;
|
|
138
165
|
}
|
|
139
166
|
}
|
|
167
|
+
if (read.code !== 0) {
|
|
168
|
+
const detail = read.stderr.replace(/\s+/g, " ").trim().slice(0, 200) || `exit ${read.code}`;
|
|
169
|
+
sources.push({ source: "macos_keychain", status: "missing", reason: `no readable '${CLAUDE_KEYCHAIN_SERVICE}' item (${detail})` });
|
|
170
|
+
}
|
|
171
|
+
else if (record) {
|
|
172
|
+
const described = describeRecord(record, now);
|
|
173
|
+
sources.push({ source: "macos_keychain", status: described.status, reason: described.reason });
|
|
174
|
+
if (described.usable)
|
|
175
|
+
found("macos_keychain");
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
sources.push({ source: "macos_keychain", status: "missing", reason: "keychain item has no claudeAiOauth access token" });
|
|
179
|
+
}
|
|
140
180
|
}
|
|
141
181
|
return { signedIn: via !== null, via, sources };
|
|
142
182
|
}
|
|
@@ -140,8 +140,10 @@ export type WriteClaudeCredentials = (request: WriteClaudeCredentialsRequest) =>
|
|
|
140
140
|
* way). The gate treats this as proof the account is already signed out and
|
|
141
141
|
* records revoked without hitting the token endpoint.
|
|
142
142
|
* - `home` is the resolved per-host provider home for the account; the gate
|
|
143
|
-
* compares it to the home being refreshed with `
|
|
144
|
-
*
|
|
143
|
+
* compares it to the home being refreshed with `canonicalHomePath` (realpath,
|
|
144
|
+
* lower case on darwin) so symlinks and case differences do not slip through;
|
|
145
|
+
* a home that cannot be resolved blocks. `null` is accepted only when the row
|
|
146
|
+
* is not live.
|
|
145
147
|
*/
|
|
146
148
|
export interface LiveLane {
|
|
147
149
|
home: string | null;
|
|
@@ -164,8 +166,13 @@ export type ListLiveLanes = () => Promise<readonly LiveLane[]>;
|
|
|
164
166
|
*/
|
|
165
167
|
export interface ClaudeProcess {
|
|
166
168
|
pid: number;
|
|
169
|
+
/**
|
|
170
|
+
* `HOME` is set when the probe read it. Claude Code resolves its default config dir as
|
|
171
|
+
* HOME/.claude, so a process with HOME set and no CLAUDE_CONFIG_DIR holds that dir.
|
|
172
|
+
*/
|
|
167
173
|
env: {
|
|
168
174
|
CLAUDE_CONFIG_DIR?: string;
|
|
175
|
+
HOME?: string;
|
|
169
176
|
};
|
|
170
177
|
probe: "env" | "command-only";
|
|
171
178
|
}
|
|
@@ -245,7 +252,7 @@ export declare function mapClaudeOAuthUsage(body: unknown, options: MapOptions):
|
|
|
245
252
|
readings: ReadingInput[];
|
|
246
253
|
failures: ParserFailureEvent[];
|
|
247
254
|
};
|
|
248
|
-
export declare function collectClaudeOAuth(
|
|
255
|
+
export declare function collectClaudeOAuth(inputOptions?: CollectClaudeOAuthOptions): Promise<ClaudeOAuthCollectResult>;
|
|
249
256
|
/**
|
|
250
257
|
* Ledger account id → hashed profile account id. Rows with no captured
|
|
251
258
|
* identity are omitted. The raw uuid is never returned.
|
|
@@ -349,11 +356,16 @@ export declare function resolveHqInvocation(argv?: readonly string[], execPath?:
|
|
|
349
356
|
command: string;
|
|
350
357
|
args: string[];
|
|
351
358
|
};
|
|
352
|
-
/**
|
|
359
|
+
/**
|
|
360
|
+
* Run `hq ... --json` and parse stdout. Async on purpose: the collector runs inside the mesh
|
|
361
|
+
* daemon, and a synchronous read that takes 15-30 s on a loaded machine froze the daemon's
|
|
362
|
+
* event loop (MQTT keepalive, owner heartbeat) for the whole read.
|
|
363
|
+
* Exported for tests; `invocation` defaults to {@link resolveHqInvocation}.
|
|
364
|
+
*/
|
|
353
365
|
export declare function execHqJson(args: readonly string[], invocation?: {
|
|
354
366
|
command: string;
|
|
355
367
|
args: string[];
|
|
356
|
-
}, env?: NodeJS.ProcessEnv): unknown
|
|
368
|
+
}, env?: NodeJS.ProcessEnv): Promise<unknown>;
|
|
357
369
|
/**
|
|
358
370
|
* Parse `hq lanes list --json` payload into typed rows.
|
|
359
371
|
* Exported for tests; fail-closed semantics live in defaultListLiveLanes.
|
|
@@ -369,12 +381,48 @@ export declare function parseAccountsListPayload(payload: unknown): readonly Raw
|
|
|
369
381
|
* Exported for tests.
|
|
370
382
|
*/
|
|
371
383
|
export declare function buildLiveLanesFromPayloads(lanes: readonly RawLaneRow[], accounts: readonly RawAccountRow[]): readonly LiveLane[];
|
|
384
|
+
/**
|
|
385
|
+
* Share one lane read across every home in a collect. Without this each expired home ran its
|
|
386
|
+
* own pair of `hq lanes ... --json` reads, so one slow machine turned into N timeouts per
|
|
387
|
+
* collect. A failure is shared too: the next home fails closed without waiting again.
|
|
388
|
+
* Exported for tests.
|
|
389
|
+
*/
|
|
390
|
+
export declare function memoizeListLiveLanes(list: ListLiveLanes): ListLiveLanes;
|
|
372
391
|
/**
|
|
373
392
|
* Parse `ps -axo pid=,command=` into rows that mention the `claude` executable.
|
|
374
393
|
* Exported for tests. Returned rows carry `probe: "command-only"` so the gate
|
|
375
394
|
* knows it cannot read per-process CLAUDE_CONFIG_DIR on darwin.
|
|
376
395
|
*/
|
|
377
396
|
export declare function parsePsClaudeOutput(stdout: string): readonly ClaudeProcess[];
|
|
397
|
+
/** Pid to full command line, from `ps -ww -o pid=,command= -p <pids>`. Exported for tests. */
|
|
398
|
+
export declare function parsePsCommandLines(stdout: string): ReadonlyMap<number, string>;
|
|
399
|
+
export interface PsEnvProbeOptions {
|
|
400
|
+
isDirectory?: (dir: string) => boolean;
|
|
401
|
+
/** False only when the pid is known to be gone (ESRCH). Unknown counts as alive. */
|
|
402
|
+
isAlive?: (pid: number) => boolean;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Upgrade darwin command-only rows with each process's environment. `ps -wwE` prints a
|
|
406
|
+
* same-user process's command line followed by its environment, unquoted and space separated.
|
|
407
|
+
* Only the text after the exact command line (read separately with `ps -ww`) is treated as the
|
|
408
|
+
* environment, so a prompt argument that mentions CLAUDE_CONFIG_DIR= or HOME= is never read as
|
|
409
|
+
* env. A row becomes `probe: "env"` only when:
|
|
410
|
+
* - the `-E` line starts with the command line plus a space, and the rest starts with NAME=;
|
|
411
|
+
* - CLAUDE_CONFIG_DIR= appears at most once in that rest, and its value is absolute and
|
|
412
|
+
* names an existing directory (the value runs to the next ` NAME=` or the end of line).
|
|
413
|
+
* Anything else stays command-only, which blocks every home. A pid missing from the output
|
|
414
|
+
* is dropped only when it is known to have exited; otherwise it stays command-only.
|
|
415
|
+
* Exported for tests.
|
|
416
|
+
*/
|
|
417
|
+
export declare function parsePsEnvOutput(envStdout: string, commandLines: ReadonlyMap<number, string>, rows: readonly ClaudeProcess[], options?: PsEnvProbeOptions): readonly ClaudeProcess[];
|
|
418
|
+
/** Default config dir of an env-probed process with CLAUDE_CONFIG_DIR unset. Exported for tests. */
|
|
419
|
+
export declare function processDefaultConfigDir(proc: ClaudeProcess): string;
|
|
420
|
+
/**
|
|
421
|
+
* Canonical form of a home directory for comparison: symlinks resolved by the OS, and lower
|
|
422
|
+
* case on darwin, where APFS is case-insensitive by default. Null when the path cannot be
|
|
423
|
+
* resolved. Exported for tests.
|
|
424
|
+
*/
|
|
425
|
+
export declare function canonicalHomePath(dir: string, platform?: NodeJS.Platform): string | null;
|
|
378
426
|
export interface ClaudeTokenRefreshResult {
|
|
379
427
|
outcome: "refreshed" | "revoked" | "error" | "skipped-race" | "skipped-live" | "skipped-live-check-failed" | "skipped-process" | "skipped-process-check-failed" | "skipped-backoff" | "skipped-revoked";
|
|
380
428
|
newAccessToken?: string;
|