@indigoai-us/hq-cli 5.345.111 → 5.345.113
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 +20 -0
- package/dist/command-catalog.generated.d.ts +4 -0
- package/dist/command-catalog.generated.js +5 -0
- package/dist/commands/capacity.js +1 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +70 -1
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +180 -180
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +16 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +57 -5
- package/dist/lib/capacity/route.js +11 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +2 -0
- package/dist/lib/daemon/supervisor.d.ts +2 -0
- package/dist/lib/daemon/supervisor.js +30 -0
- 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.71","bundleSha256":"809fe7fd214f197c14df0586b2bf4e4536002cd122eb61a6817af4f7ba478637","formatVersion":1}
|
|
@@ -332,6 +332,22 @@ interface RawAccountRow {
|
|
|
332
332
|
auth_state?: unknown;
|
|
333
333
|
provider?: unknown;
|
|
334
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* How to re-invoke the running hq CLI without a PATH lookup. When argv[1] is
|
|
337
|
+
* the entry of an installed hq-cli package, run it with the same node binary;
|
|
338
|
+
* the collector runs under launchd and lane workers where PATH may be empty.
|
|
339
|
+
* Falls back to `hq` on PATH when this process is not the hq CLI (tests,
|
|
340
|
+
* embedders). Exported for tests.
|
|
341
|
+
*/
|
|
342
|
+
export declare function resolveHqInvocation(argv?: readonly string[], execPath?: string): {
|
|
343
|
+
command: string;
|
|
344
|
+
args: string[];
|
|
345
|
+
};
|
|
346
|
+
/** Exported for tests; `invocation` defaults to {@link resolveHqInvocation}. */
|
|
347
|
+
export declare function execHqJson(args: readonly string[], invocation?: {
|
|
348
|
+
command: string;
|
|
349
|
+
args: string[];
|
|
350
|
+
}, env?: NodeJS.ProcessEnv): unknown;
|
|
335
351
|
/**
|
|
336
352
|
* Parse `hq lanes list --json` payload into typed rows.
|
|
337
353
|
* Exported for tests; fail-closed semantics live in defaultListLiveLanes.
|
|
@@ -251,7 +251,12 @@ export async function collectClaudeOAuth(options = {}) {
|
|
|
251
251
|
});
|
|
252
252
|
if (refreshResult?.outcome === "refreshed" && refreshResult.newAccessToken) {
|
|
253
253
|
currentToken = refreshResult.newAccessToken;
|
|
254
|
-
|
|
254
|
+
// A refresh renews the token but does not grant scopes the sign-in
|
|
255
|
+
// lacked. Re-check the profile scope before clearing the block.
|
|
256
|
+
const refreshedScopes = refreshResult.newScopes && refreshResult.newScopes.length > 0
|
|
257
|
+
? refreshResult.newScopes
|
|
258
|
+
: meta.scopes;
|
|
259
|
+
currentBlocked = hasProfileScope(refreshedScopes) ? undefined : "unavailable: missing_scope";
|
|
255
260
|
}
|
|
256
261
|
else if (refreshResult?.outcome === "revoked" || refreshResult?.outcome === "skipped-revoked") {
|
|
257
262
|
currentBlocked = SIGN_IN_REVOKED_REASON;
|
|
@@ -410,13 +415,17 @@ async function collectEnumeratedClaude(options, scoped, root, clock, cliVersion)
|
|
|
410
415
|
if (refreshResult) {
|
|
411
416
|
if (refreshResult.outcome === "refreshed" && refreshResult.newAccessToken) {
|
|
412
417
|
account.token = refreshResult.newAccessToken;
|
|
413
|
-
account.blocked = null;
|
|
414
418
|
if (refreshResult.newExpiresAt !== undefined)
|
|
415
419
|
account.expiresAt = refreshResult.newExpiresAt;
|
|
416
420
|
if (refreshResult.newRefreshToken)
|
|
417
421
|
account.refreshToken = refreshResult.newRefreshToken;
|
|
418
422
|
if (refreshResult.newScopes && refreshResult.newScopes.length > 0)
|
|
419
423
|
account.scopes = refreshResult.newScopes;
|
|
424
|
+
// A refresh renews the token but does not grant scopes the sign-in
|
|
425
|
+
// lacked. Re-check the profile scope before clearing the block.
|
|
426
|
+
account.blocked = hasProfileScope(account.scopes)
|
|
427
|
+
? null
|
|
428
|
+
: credentialBlockReason("unavailable: missing_scope");
|
|
420
429
|
}
|
|
421
430
|
else if (refreshResult.outcome === "revoked" || refreshResult.outcome === "skipped-revoked") {
|
|
422
431
|
account.blocked = SIGN_IN_REVOKED_REASON;
|
|
@@ -537,7 +546,10 @@ async function readEnumeratedHome(group, options, nowMs, root) {
|
|
|
537
546
|
platform,
|
|
538
547
|
};
|
|
539
548
|
}
|
|
540
|
-
|
|
549
|
+
// A fresh poll normally skips the credential read. When a revoked marker
|
|
550
|
+
// exists, read anyway: the marker check needs the current refresh token to
|
|
551
|
+
// tell a re-sign-in (fingerprint changed) from the same revoked token.
|
|
552
|
+
if (pollIsFresh(root, accountId, nowMs) && revokedMarkerForCurrentCredential(root, accountId, null) === "none") {
|
|
541
553
|
return {
|
|
542
554
|
accountId,
|
|
543
555
|
labels,
|
|
@@ -1623,6 +1635,10 @@ const defaultWriteClaudeCredentials = async (request) => {
|
|
|
1623
1635
|
return { ok: false, raceDetected: true };
|
|
1624
1636
|
}
|
|
1625
1637
|
const body = JSON.stringify(request.document);
|
|
1638
|
+
// `security add-generic-password` only takes the secret as `-w <value>`
|
|
1639
|
+
// on argv, so the credential JSON is visible to `ps` for the duration of
|
|
1640
|
+
// this call. Claude Code writes its own Keychain item the same way; this
|
|
1641
|
+
// matches that exposure rather than adding a new one.
|
|
1626
1642
|
const writeArgs = selector === "s"
|
|
1627
1643
|
? ["add-generic-password", "-U", "-s", request.service, "-w", body]
|
|
1628
1644
|
: ["add-generic-password", "-U", "-s", request.service, "-a", username, "-w", body];
|
|
@@ -1670,8 +1686,44 @@ const defaultWriteClaudeCredentials = async (request) => {
|
|
|
1670
1686
|
return { ok: false, error: message };
|
|
1671
1687
|
}
|
|
1672
1688
|
};
|
|
1673
|
-
|
|
1674
|
-
|
|
1689
|
+
const HQ_CLI_PACKAGE_NAME = "@indigoai-us/hq-cli";
|
|
1690
|
+
/**
|
|
1691
|
+
* How to re-invoke the running hq CLI without a PATH lookup. When argv[1] is
|
|
1692
|
+
* the entry of an installed hq-cli package, run it with the same node binary;
|
|
1693
|
+
* the collector runs under launchd and lane workers where PATH may be empty.
|
|
1694
|
+
* Falls back to `hq` on PATH when this process is not the hq CLI (tests,
|
|
1695
|
+
* embedders). Exported for tests.
|
|
1696
|
+
*/
|
|
1697
|
+
export function resolveHqInvocation(argv = process.argv, execPath = process.execPath) {
|
|
1698
|
+
const entry = argv[1];
|
|
1699
|
+
if (entry) {
|
|
1700
|
+
try {
|
|
1701
|
+
const real = fs.realpathSync(entry);
|
|
1702
|
+
let dir = path.dirname(real);
|
|
1703
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
1704
|
+
const manifest = path.join(dir, "package.json");
|
|
1705
|
+
if (fs.existsSync(manifest)) {
|
|
1706
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
1707
|
+
if (parsed.name === HQ_CLI_PACKAGE_NAME)
|
|
1708
|
+
return { command: execPath, args: [real] };
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
const parent = path.dirname(dir);
|
|
1712
|
+
if (parent === dir)
|
|
1713
|
+
break;
|
|
1714
|
+
dir = parent;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
catch {
|
|
1718
|
+
// Unresolvable entry: fall through to the PATH lookup below.
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return { command: "hq", args: [] };
|
|
1722
|
+
}
|
|
1723
|
+
/** Exported for tests; `invocation` defaults to {@link resolveHqInvocation}. */
|
|
1724
|
+
export function execHqJson(args, invocation = resolveHqInvocation(), env = process.env) {
|
|
1725
|
+
const result = spawnSync(invocation.command, [...invocation.args, ...args], {
|
|
1726
|
+
env,
|
|
1675
1727
|
encoding: "utf8",
|
|
1676
1728
|
timeout: 5_000,
|
|
1677
1729
|
windowsHide: true,
|
|
@@ -250,6 +250,17 @@ export function pickAccount(input) {
|
|
|
250
250
|
});
|
|
251
251
|
continue;
|
|
252
252
|
}
|
|
253
|
+
// The ledger sets usageStale when the sign-in expired and no reading has
|
|
254
|
+
// landed since the expiry. The limits on file describe the old token, so
|
|
255
|
+
// the account cannot be ranked on them.
|
|
256
|
+
if (account.usageStale === true) {
|
|
257
|
+
skipped.push({
|
|
258
|
+
...base,
|
|
259
|
+
reason: "no fresh reading (sign-in expired)",
|
|
260
|
+
fix: account.fixCommand ?? null,
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
253
264
|
const applicable = (account.limits ?? []).filter((l) => limitApplies(providerId, l, model));
|
|
254
265
|
const viewRows = applicable.map((limit) => {
|
|
255
266
|
const resetMs = limit.resetsAt ? Date.parse(limit.resetsAt) : NaN;
|
|
@@ -87,6 +87,8 @@ export interface PlanContext {
|
|
|
87
87
|
platform: NodeJS.Platform;
|
|
88
88
|
home: string;
|
|
89
89
|
hqRoot: string | undefined;
|
|
90
|
+
/** Daemon-owned request directory consumed by the hq-cloud watch runner. */
|
|
91
|
+
syncPassRequestsDir: string;
|
|
90
92
|
/** hq-cloud's sync-runner.js, or null when it cannot be found. */
|
|
91
93
|
syncRunnerPath: string | null;
|
|
92
94
|
/** hq-cloud's `hq-outpost-heartbeat` bin, when the hq-cloud in use has one. */
|
|
@@ -142,7 +142,7 @@ function planService(service, ctx) {
|
|
|
142
142
|
SYNC_RUNNER_OWNER,
|
|
143
143
|
],
|
|
144
144
|
},
|
|
145
|
-
env: { ...ctx.legacySyncEnv },
|
|
145
|
+
env: { ...ctx.legacySyncEnv, HQ_SYNC_PASS_REQUEST_DIR: ctx.syncPassRequestsDir },
|
|
146
146
|
heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
|
|
147
147
|
});
|
|
148
148
|
let ownerBlock;
|
|
@@ -11,6 +11,8 @@ export interface DaemonPaths {
|
|
|
11
11
|
lockPath: string;
|
|
12
12
|
logsDir: string;
|
|
13
13
|
requestsDir: string;
|
|
14
|
+
/** One-shot sync passes queued for the running hq-cloud watch runner. */
|
|
15
|
+
syncPassRequestsDir: string;
|
|
14
16
|
supervisorLogPath: string;
|
|
15
17
|
unitLogPath: (unitId: string) => string;
|
|
16
18
|
/** The FIFO a unit that speaks the sync protocol writes its stdout to (see protocol-tail.ts); kept out of logs/ so nothing reading logs drains it. */
|
package/dist/lib/daemon/paths.js
CHANGED
|
@@ -17,6 +17,7 @@ export function daemonPaths(home = os.homedir(), env = process.env) {
|
|
|
17
17
|
lockPath: path.join(root, "daemon.pid"),
|
|
18
18
|
logsDir,
|
|
19
19
|
requestsDir: path.join(root, "requests"),
|
|
20
|
+
syncPassRequestsDir: path.resolve(root, "sync-pass-requests"),
|
|
20
21
|
supervisorLogPath: path.join(logsDir, "daemon.log"),
|
|
21
22
|
unitLogPath: (unitId) => path.join(logsDir, `${unitId.replace(/[^A-Za-z0-9_.-]/g, "-")}.log`),
|
|
22
23
|
unitProtocolPath: (unitId) => path.join(root, "protocol", `${unitId.replace(/[^A-Za-z0-9_.-]/g, "-")}.fifo`),
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -451,6 +451,7 @@ export async function runDaemon(opts) {
|
|
|
451
451
|
const home = opts.home ?? os.homedir();
|
|
452
452
|
const platform = process.platform;
|
|
453
453
|
const paths = daemonPaths(home, env);
|
|
454
|
+
ensurePrivateDir(paths.syncPassRequestsDir);
|
|
454
455
|
const log = makeLogger(paths, opts.foreground === true);
|
|
455
456
|
const syncRunnerBundleDirectory = opts.syncRunnerBundleDirectory ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../hq-cloud-sync-runner");
|
|
456
457
|
const cliPackageRoot = opts.cliPackageRoot === undefined ? resolveRunningInstall().packageRoot : opts.cliPackageRoot;
|
|
@@ -835,6 +836,7 @@ export async function runDaemon(opts) {
|
|
|
835
836
|
platform,
|
|
836
837
|
home,
|
|
837
838
|
hqRoot,
|
|
839
|
+
syncPassRequestsDir: paths.syncPassRequestsDir,
|
|
838
840
|
syncRunnerPath: runners.syncRunner,
|
|
839
841
|
heartbeatRunnerPath: runners.heartbeat,
|
|
840
842
|
sessionHostRunnerPath: runners.sessionHost,
|
|
@@ -63,6 +63,8 @@ interface UnitState {
|
|
|
63
63
|
lastResult?: "ok" | "failed";
|
|
64
64
|
lastHeartbeatAt?: string;
|
|
65
65
|
lastPassResult?: ReturnType<typeof syncPassResult>;
|
|
66
|
+
/** The pass-request folder the running child was started with; absent when it was not. */
|
|
67
|
+
passRequestDir?: string;
|
|
66
68
|
}
|
|
67
69
|
/** An app that runs the daemon as its own child process instead of an OS unit. */
|
|
68
70
|
export type HostApp = "desktop";
|
|
@@ -37,6 +37,33 @@ const MESH_OWNER_LOG_INTERVAL_MS = 10 * 60_000;
|
|
|
37
37
|
export const HANDOFF_STAGGER_MS = 15_000;
|
|
38
38
|
/** How long a draining unit may take to finish its work before it gets SIGTERM. */
|
|
39
39
|
export const HANDOFF_DRAIN_MAX_MS = 15 * 60_000;
|
|
40
|
+
const PASS_REQUEST_DIR_ENV = "HQ_SYNC_PASS_REQUEST_DIR";
|
|
41
|
+
function passRequestDirOf(env) {
|
|
42
|
+
const dir = env?.[PASS_REQUEST_DIR_ENV];
|
|
43
|
+
return dir ? dir : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** An adopted child carries no env, only the key it was started with (see planKey). */
|
|
46
|
+
function passRequestDirFromKey(key) {
|
|
47
|
+
try {
|
|
48
|
+
const fields = JSON.parse(key);
|
|
49
|
+
if (!Array.isArray(fields))
|
|
50
|
+
return undefined;
|
|
51
|
+
const env = fields[6];
|
|
52
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
53
|
+
return undefined;
|
|
54
|
+
const dir = env[PASS_REQUEST_DIR_ENV];
|
|
55
|
+
return typeof dir === "string" && dir ? dir : undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function recordPassRequestDir(state, dir) {
|
|
62
|
+
if (dir)
|
|
63
|
+
state.passRequestDir = dir;
|
|
64
|
+
else
|
|
65
|
+
delete state.passRequestDir;
|
|
66
|
+
}
|
|
40
67
|
/** Plan reads in a row that must find a running remote-control host blocked before it is paused. */
|
|
41
68
|
const BLOCKED_READS_BEFORE_PAUSE = 2;
|
|
42
69
|
function backoffMs(attempt) {
|
|
@@ -560,6 +587,7 @@ export function createSupervisor(deps) {
|
|
|
560
587
|
}
|
|
561
588
|
slot.child = child;
|
|
562
589
|
slot.childKey = slot.key;
|
|
590
|
+
recordPassRequestDir(slot.state, passRequestDirOf(slot.plan.env));
|
|
563
591
|
slot.childStartedAt = now();
|
|
564
592
|
slot.state.status = "running";
|
|
565
593
|
slot.state.startedAt = slot.childStartedAt;
|
|
@@ -651,6 +679,7 @@ export function createSupervisor(deps) {
|
|
|
651
679
|
}
|
|
652
680
|
slot.child = child;
|
|
653
681
|
slot.childKey = slot.key;
|
|
682
|
+
recordPassRequestDir(slot.state, passRequestDirOf(slot.plan.env));
|
|
654
683
|
slot.childStartedAt = now();
|
|
655
684
|
slot.state.status = "running";
|
|
656
685
|
delete slot.state.reason;
|
|
@@ -880,6 +909,7 @@ export function createSupervisor(deps) {
|
|
|
880
909
|
slot.adopted = true;
|
|
881
910
|
slot.child = unit.child;
|
|
882
911
|
slot.childKey = unit.key;
|
|
912
|
+
recordPassRequestDir(slot.state, passRequestDirFromKey(unit.key));
|
|
883
913
|
slot.childStartedAt = now();
|
|
884
914
|
slot.state.status = "running";
|
|
885
915
|
slot.state.startedAt = slot.childStartedAt;
|
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.113",
|
|
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.71",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|