@indigoai-us/hq-cli 5.203.0 → 5.204.0
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 +8 -0
- package/dist/command-catalog.generated.d.ts +27 -0
- package/dist/command-catalog.generated.js +35 -0
- package/dist/commands/lanes.d.ts +3 -0
- package/dist/commands/lanes.js +200 -25
- package/dist/lib/lanes/account-home.d.ts +8 -0
- package/dist/lib/lanes/account-home.js +10 -0
- package/dist/lib/lanes/accounts.d.ts +4 -1
- package/dist/lib/lanes/accounts.js +15 -10
- package/dist/lib/lanes/claude-account-balance.d.ts +3 -0
- package/dist/lib/lanes/claude-account-balance.js +19 -4
- package/dist/lib/lanes/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +55 -13
- package/dist/lib/lanes/host-capacity.d.ts +4 -0
- package/dist/lib/lanes/host-capacity.js +8 -1
- package/dist/lib/lanes/index.d.ts +2 -0
- package/dist/lib/lanes/index.js +1 -0
- package/dist/lib/lanes/liveness.d.ts +1 -0
- package/dist/lib/lanes/liveness.js +3 -1
- package/dist/lib/lanes/reconcile.js +20 -9
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +30 -14
- package/dist/lib/lanes/types.d.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.204.0] — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq lanes accounts set-home` adds a host-specific home to the synced lane
|
|
10
|
+
account registry. Account status and Claude balancing now distinguish a
|
|
11
|
+
missing home from a signed-out account.
|
|
12
|
+
|
|
5
13
|
## [5.203.0] — 2026-09-26
|
|
6
14
|
|
|
7
15
|
### Added
|
|
@@ -8135,6 +8135,33 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8135
8135
|
readonly description: "JSON on stdout (always)";
|
|
8136
8136
|
}];
|
|
8137
8137
|
readonly subcommands: readonly [];
|
|
8138
|
+
}, {
|
|
8139
|
+
readonly name: "set-home";
|
|
8140
|
+
readonly description: "Set the host-specific home for an existing provider account";
|
|
8141
|
+
readonly aliases: readonly [];
|
|
8142
|
+
readonly hidden: false;
|
|
8143
|
+
readonly usage: "[options] <provider> <account-id> <path>";
|
|
8144
|
+
readonly arguments: readonly [{
|
|
8145
|
+
readonly name: "provider";
|
|
8146
|
+
readonly required: true;
|
|
8147
|
+
readonly variadic: false;
|
|
8148
|
+
}, {
|
|
8149
|
+
readonly name: "account-id";
|
|
8150
|
+
readonly required: true;
|
|
8151
|
+
readonly variadic: false;
|
|
8152
|
+
}, {
|
|
8153
|
+
readonly name: "path";
|
|
8154
|
+
readonly required: true;
|
|
8155
|
+
readonly variadic: false;
|
|
8156
|
+
}];
|
|
8157
|
+
readonly options: readonly [{
|
|
8158
|
+
readonly flags: "--host <hostname>";
|
|
8159
|
+
readonly description: "Host key (defaults to this host's hostname)";
|
|
8160
|
+
}, {
|
|
8161
|
+
readonly flags: "--json";
|
|
8162
|
+
readonly description: "JSON on stdout (always)";
|
|
8163
|
+
}];
|
|
8164
|
+
readonly subcommands: readonly [];
|
|
8138
8165
|
}, {
|
|
8139
8166
|
readonly name: "login";
|
|
8140
8167
|
readonly description: "Create a home, run the vendor login, and register the account";
|
|
@@ -10523,6 +10523,41 @@ export const COMMAND_CATALOG = [
|
|
|
10523
10523
|
],
|
|
10524
10524
|
"subcommands": []
|
|
10525
10525
|
},
|
|
10526
|
+
{
|
|
10527
|
+
"name": "set-home",
|
|
10528
|
+
"description": "Set the host-specific home for an existing provider account",
|
|
10529
|
+
"aliases": [],
|
|
10530
|
+
"hidden": false,
|
|
10531
|
+
"usage": "[options] <provider> <account-id> <path>",
|
|
10532
|
+
"arguments": [
|
|
10533
|
+
{
|
|
10534
|
+
"name": "provider",
|
|
10535
|
+
"required": true,
|
|
10536
|
+
"variadic": false
|
|
10537
|
+
},
|
|
10538
|
+
{
|
|
10539
|
+
"name": "account-id",
|
|
10540
|
+
"required": true,
|
|
10541
|
+
"variadic": false
|
|
10542
|
+
},
|
|
10543
|
+
{
|
|
10544
|
+
"name": "path",
|
|
10545
|
+
"required": true,
|
|
10546
|
+
"variadic": false
|
|
10547
|
+
}
|
|
10548
|
+
],
|
|
10549
|
+
"options": [
|
|
10550
|
+
{
|
|
10551
|
+
"flags": "--host <hostname>",
|
|
10552
|
+
"description": "Host key (defaults to this host's hostname)"
|
|
10553
|
+
},
|
|
10554
|
+
{
|
|
10555
|
+
"flags": "--json",
|
|
10556
|
+
"description": "JSON on stdout (always)"
|
|
10557
|
+
}
|
|
10558
|
+
],
|
|
10559
|
+
"subcommands": []
|
|
10560
|
+
},
|
|
10526
10561
|
{
|
|
10527
10562
|
"name": "login",
|
|
10528
10563
|
"description": "Create a home, run the vendor login, and register the account",
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -95,11 +95,14 @@ export interface LanesCommandDeps {
|
|
|
95
95
|
laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
96
96
|
/** The local-only PR 2 implementation refuses if mesh storage is enabled. */
|
|
97
97
|
laneTrackerMeshEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
98
|
+
/** Test seam for the default-off host-scoped account-home writer gate. */
|
|
99
|
+
laneHostScopedAccountHomesEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
98
100
|
/** Test seam for recording approvals against the active identity. */
|
|
99
101
|
trackerActor?: () => string | null;
|
|
100
102
|
}
|
|
101
103
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
102
104
|
export declare const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
|
|
105
|
+
export declare const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
|
|
103
106
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
104
107
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
105
108
|
/**
|
package/dist/commands/lanes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
13
14
|
import { Option } from "commander";
|
|
14
15
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
15
16
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -32,11 +33,12 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
32
33
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
33
34
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
34
35
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
36
|
+
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
35
37
|
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
36
38
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
37
39
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
38
40
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
39
|
-
import { providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
41
|
+
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
40
42
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
41
43
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
42
44
|
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
@@ -59,6 +61,7 @@ const VENDOR_LOGIN = {
|
|
|
59
61
|
export { emitLaneDecisionEvents };
|
|
60
62
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
61
63
|
export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
|
|
64
|
+
export const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
|
|
62
65
|
function defaultLanePlacementEnabled() {
|
|
63
66
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
64
67
|
}
|
|
@@ -82,6 +85,15 @@ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
|
|
|
82
85
|
env,
|
|
83
86
|
});
|
|
84
87
|
}
|
|
88
|
+
function defaultLaneHostScopedAccountHomesEnabled(company, env) {
|
|
89
|
+
return readLanesRegistryFlag(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, { company, env });
|
|
90
|
+
}
|
|
91
|
+
async function hostScopedAccountHomesEnabled(ctx) {
|
|
92
|
+
const localOverride = readLocalOverride(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, ctx.env);
|
|
93
|
+
if (localOverride !== null)
|
|
94
|
+
return localOverride;
|
|
95
|
+
return ctx.laneHostScopedAccountHomesEnabled(await ctx.resolveCompany());
|
|
96
|
+
}
|
|
85
97
|
function withoutProviderHome(value) {
|
|
86
98
|
if (Array.isArray(value))
|
|
87
99
|
return value.map(withoutProviderHome);
|
|
@@ -286,6 +298,8 @@ function resolveDeps(deps = {}) {
|
|
|
286
298
|
((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
|
|
287
299
|
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
288
300
|
laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
|
|
301
|
+
laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
|
|
302
|
+
((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
|
|
289
303
|
trackerActor: deps.trackerActor ?? defaultTrackerActor,
|
|
290
304
|
};
|
|
291
305
|
}
|
|
@@ -298,6 +312,7 @@ function livenessDepsFromCtx(ctx) {
|
|
|
298
312
|
env: ctx.env,
|
|
299
313
|
existsSync: ctx.existsSync,
|
|
300
314
|
readFileSync: ctx.readFileSync,
|
|
315
|
+
hostname: ctx.hostname,
|
|
301
316
|
};
|
|
302
317
|
}
|
|
303
318
|
function reconcileHostFromCtx(ctx) {
|
|
@@ -1004,6 +1019,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
|
|
|
1004
1019
|
lanes: loadAllLanes(hqRoot),
|
|
1005
1020
|
config,
|
|
1006
1021
|
existsSync: ctx.existsSync,
|
|
1022
|
+
hostname: ctx.hostname(),
|
|
1007
1023
|
now: ctx.now(),
|
|
1008
1024
|
});
|
|
1009
1025
|
const remote = await Promise.all(remoteIds.map(async (id) => {
|
|
@@ -1118,6 +1134,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1118
1134
|
existsSync: ctx.existsSync,
|
|
1119
1135
|
probe: ctx.probeProvider,
|
|
1120
1136
|
env: ctx.env,
|
|
1137
|
+
hostname: ctx.hostname(),
|
|
1121
1138
|
now: ctx.now(),
|
|
1122
1139
|
});
|
|
1123
1140
|
const snapshots = [{
|
|
@@ -1140,6 +1157,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1140
1157
|
existsSync: ctx.existsSync,
|
|
1141
1158
|
probe: ctx.probeProvider,
|
|
1142
1159
|
env: ctx.env,
|
|
1160
|
+
hostname: ctx.hostname(),
|
|
1143
1161
|
now: ctx.now(),
|
|
1144
1162
|
}),
|
|
1145
1163
|
lanes: actualLocalLanes,
|
|
@@ -2196,6 +2214,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
2196
2214
|
try {
|
|
2197
2215
|
const resolved = resolveAccount(lane.provider, lane.account, config, {
|
|
2198
2216
|
hqRoot,
|
|
2217
|
+
hostname: os.hostname(),
|
|
2199
2218
|
accountDeps: {
|
|
2200
2219
|
probe: modelDeps.probe,
|
|
2201
2220
|
env: modelDeps.env,
|
|
@@ -2203,6 +2222,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
2203
2222
|
});
|
|
2204
2223
|
const status = accountStatus(lane.provider, resolved.id, config, {
|
|
2205
2224
|
existsSync: (p) => fs.existsSync(p),
|
|
2225
|
+
hostname: os.hostname(),
|
|
2206
2226
|
probe: modelDeps.probe,
|
|
2207
2227
|
env: modelDeps.env,
|
|
2208
2228
|
});
|
|
@@ -2589,6 +2609,7 @@ async function runCreate(ctx, opts) {
|
|
|
2589
2609
|
}
|
|
2590
2610
|
const resolved = resolveAccount(provider, requested, config, {
|
|
2591
2611
|
hqRoot,
|
|
2612
|
+
hostname: ctx.hostname(),
|
|
2592
2613
|
cliModel,
|
|
2593
2614
|
model: cliModel ?? profileModel,
|
|
2594
2615
|
});
|
|
@@ -2597,6 +2618,7 @@ async function runCreate(ctx, opts) {
|
|
|
2597
2618
|
else if (profileAccount !== undefined && profileAccounts === undefined) {
|
|
2598
2619
|
const resolved = resolveAccount(provider, profileAccount, config, {
|
|
2599
2620
|
hqRoot,
|
|
2621
|
+
hostname: ctx.hostname(),
|
|
2600
2622
|
cliModel,
|
|
2601
2623
|
model: cliModel ?? profileModel,
|
|
2602
2624
|
});
|
|
@@ -2650,6 +2672,7 @@ async function runCreate(ctx, opts) {
|
|
|
2650
2672
|
const candidate = pinnedAccount ??
|
|
2651
2673
|
resolveAccount(provider, undefined, config, {
|
|
2652
2674
|
hqRoot,
|
|
2675
|
+
hostname: ctx.hostname(),
|
|
2653
2676
|
existsSync: ctx.existsSync,
|
|
2654
2677
|
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
2655
2678
|
cliModel,
|
|
@@ -2669,6 +2692,7 @@ async function runCreate(ctx, opts) {
|
|
|
2669
2692
|
(profileAccounts === undefined || profileAccounts.includes(id)) &&
|
|
2670
2693
|
accountStatus(provider, id, config, {
|
|
2671
2694
|
existsSync: ctx.existsSync,
|
|
2695
|
+
hostname: ctx.hostname(),
|
|
2672
2696
|
probe: ctx.probeProvider,
|
|
2673
2697
|
env: ctx.env,
|
|
2674
2698
|
}).logged_in === true)
|
|
@@ -2679,7 +2703,10 @@ async function runCreate(ctx, opts) {
|
|
|
2679
2703
|
const quotaBlocked = [];
|
|
2680
2704
|
const quota = loadQuotaState(hqRoot);
|
|
2681
2705
|
for (const id of toValidate) {
|
|
2682
|
-
const
|
|
2706
|
+
const account = accounts[id];
|
|
2707
|
+
const home = account === undefined
|
|
2708
|
+
? undefined
|
|
2709
|
+
: effectiveAccountHome(account, ctx.hostname()).home;
|
|
2683
2710
|
if (home === undefined)
|
|
2684
2711
|
continue;
|
|
2685
2712
|
checkedAccounts.push(id);
|
|
@@ -2782,6 +2809,7 @@ async function runCreate(ctx, opts) {
|
|
|
2782
2809
|
existsSync: ctx.existsSync,
|
|
2783
2810
|
probe: ctx.probeProvider,
|
|
2784
2811
|
env: ctx.env,
|
|
2812
|
+
hostname: ctx.hostname(),
|
|
2785
2813
|
now: ctx.now(),
|
|
2786
2814
|
}),
|
|
2787
2815
|
lanes: localLanes,
|
|
@@ -2805,6 +2833,7 @@ async function runCreate(ctx, opts) {
|
|
|
2805
2833
|
pinnedAccount = resolveAccount(provider, undefined, config, {
|
|
2806
2834
|
hqRoot,
|
|
2807
2835
|
existsSync: ctx.existsSync,
|
|
2836
|
+
hostname: ctx.hostname(),
|
|
2808
2837
|
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
2809
2838
|
cliModel,
|
|
2810
2839
|
model: cliModel ?? profileModel,
|
|
@@ -4029,6 +4058,7 @@ async function runCapacity(ctx) {
|
|
|
4029
4058
|
existsSync: ctx.existsSync,
|
|
4030
4059
|
probe: ctx.probeProvider,
|
|
4031
4060
|
env: ctx.env,
|
|
4061
|
+
hostname: ctx.hostname(),
|
|
4032
4062
|
now: ctx.now(),
|
|
4033
4063
|
});
|
|
4034
4064
|
printJson(snapshot);
|
|
@@ -4873,6 +4903,7 @@ function validateResumeProfileModel(ctx, lane, profile) {
|
|
|
4873
4903
|
const account = resolveAccount(lane.provider, lane.account, config, {
|
|
4874
4904
|
hqRoot,
|
|
4875
4905
|
existsSync: ctx.existsSync,
|
|
4906
|
+
hostname: ctx.hostname(),
|
|
4876
4907
|
model: profile.meta.model,
|
|
4877
4908
|
});
|
|
4878
4909
|
validateProfileLaneModel(lane.provider, profile.meta.model, account.home, profile.id, {
|
|
@@ -4919,7 +4950,7 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4919
4950
|
if (account === undefined) {
|
|
4920
4951
|
throw new AccountResolutionError("unknown_account", `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
4921
4952
|
}
|
|
4922
|
-
validateLaneModel(lane.provider, model, account.home, {
|
|
4953
|
+
validateLaneModel(lane.provider, model, effectiveAccountHome(account, ctx.hostname()).home, {
|
|
4923
4954
|
readFileSync: ctx.readFileSync,
|
|
4924
4955
|
probe: ctx.probeProvider,
|
|
4925
4956
|
env: ctx.env,
|
|
@@ -5039,11 +5070,13 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5039
5070
|
if (targetCfg === undefined) {
|
|
5040
5071
|
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(target)} for ${lane.provider}. Configured: ${Object.keys(providerCfg.accounts).join(", ") || "(none)"}`, { provider: lane.provider, account: target, enabled: Object.keys(providerCfg.accounts).filter((id) => providerCfg.accounts[id]?.enabled) });
|
|
5041
5072
|
}
|
|
5073
|
+
const targetHome = effectiveAccountHome(targetCfg, ctx.hostname()).home;
|
|
5042
5074
|
if (!targetCfg.enabled) {
|
|
5043
5075
|
throw new AccountResolutionError("account_disabled", `Account ${JSON.stringify(target)} is disabled${targetCfg.disabled_reason ? `: ${targetCfg.disabled_reason}` : ""}`, { provider: lane.provider, account: target, reason: targetCfg.disabled_reason ?? "" });
|
|
5044
5076
|
}
|
|
5045
5077
|
if (accountStatus(lane.provider, target, config, {
|
|
5046
5078
|
existsSync: ctx.existsSync,
|
|
5079
|
+
hostname: ctx.hostname(),
|
|
5047
5080
|
probe: ctx.probeProvider,
|
|
5048
5081
|
env: ctx.env,
|
|
5049
5082
|
}).logged_in !== true) {
|
|
@@ -5060,7 +5093,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5060
5093
|
: undefined;
|
|
5061
5094
|
const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
|
|
5062
5095
|
if (input.cliModel === undefined && profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
|
|
5063
|
-
validateProfileLaneModel(lane.provider, profileModel,
|
|
5096
|
+
validateProfileLaneModel(lane.provider, profileModel, targetHome, profile.id, {
|
|
5064
5097
|
readFileSync: ctx.readFileSync,
|
|
5065
5098
|
probe: ctx.probeProvider,
|
|
5066
5099
|
env: ctx.env,
|
|
@@ -5086,7 +5119,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5086
5119
|
throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
|
|
5087
5120
|
}
|
|
5088
5121
|
if (model !== undefined) {
|
|
5089
|
-
validateLaneModel(lane.provider, model,
|
|
5122
|
+
validateLaneModel(lane.provider, model, targetHome, {
|
|
5090
5123
|
readFileSync: ctx.readFileSync,
|
|
5091
5124
|
probe: ctx.probeProvider,
|
|
5092
5125
|
env: ctx.env,
|
|
@@ -5096,14 +5129,15 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5096
5129
|
if (sourceCfg === undefined) {
|
|
5097
5130
|
throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
5098
5131
|
}
|
|
5132
|
+
const sourceHome = effectiveAccountHome(sourceCfg, ctx.hostname()).home;
|
|
5099
5133
|
// Checks done. Copy the session files, then write the record.
|
|
5100
5134
|
const copy = ensureSessionHome({
|
|
5101
5135
|
provider: lane.provider,
|
|
5102
5136
|
sessionId,
|
|
5103
5137
|
cwd: hqRoot,
|
|
5104
|
-
targetHome
|
|
5105
|
-
registeredHomes: Object.values(providerCfg.accounts).map((account) => account.home),
|
|
5106
|
-
providerHomeAtSpawn: lane.provider_home_at_spawn ??
|
|
5138
|
+
targetHome,
|
|
5139
|
+
registeredHomes: Object.values(providerCfg.accounts).map((account) => effectiveAccountHome(account, ctx.hostname()).home),
|
|
5140
|
+
providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceHome,
|
|
5107
5141
|
});
|
|
5108
5142
|
const at = now.toISOString();
|
|
5109
5143
|
const from = lane.account ?? null;
|
|
@@ -5186,6 +5220,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
|
5186
5220
|
}
|
|
5187
5221
|
if (accountStatus("claude", lane.account, config, {
|
|
5188
5222
|
existsSync: ctx.existsSync,
|
|
5223
|
+
hostname: ctx.hostname(),
|
|
5189
5224
|
probe: ctx.probeProvider,
|
|
5190
5225
|
env: ctx.env,
|
|
5191
5226
|
}).logged_in === false) {
|
|
@@ -5212,6 +5247,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
|
5212
5247
|
? false
|
|
5213
5248
|
: accountStatus("claude", lane.account, config, {
|
|
5214
5249
|
existsSync: ctx.existsSync,
|
|
5250
|
+
hostname: ctx.hostname(),
|
|
5215
5251
|
probe: ctx.probeProvider,
|
|
5216
5252
|
env: ctx.env,
|
|
5217
5253
|
}).logged_in;
|
|
@@ -5274,6 +5310,7 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
|
5274
5310
|
}
|
|
5275
5311
|
if (accountStatus(lane.provider, lane.account, config, {
|
|
5276
5312
|
existsSync: ctx.existsSync,
|
|
5313
|
+
hostname: ctx.hostname(),
|
|
5277
5314
|
probe: ctx.probeProvider,
|
|
5278
5315
|
env: ctx.env,
|
|
5279
5316
|
}).logged_in !== true) {
|
|
@@ -6419,7 +6456,7 @@ function drainHookStatusFor(ctx, lane) {
|
|
|
6419
6456
|
const account = config.providers[lane.provider].accounts[lane.account];
|
|
6420
6457
|
if (account === undefined)
|
|
6421
6458
|
return null;
|
|
6422
|
-
const home = canonicalizeHome(account.home, {
|
|
6459
|
+
const home = canonicalizeHome(effectiveAccountHome(account, ctx.hostname()).home, {
|
|
6423
6460
|
homedir: ctx.homedir,
|
|
6424
6461
|
realpathSync: (p) => fs.realpathSync(p),
|
|
6425
6462
|
existsSync: (p) => fs.existsSync(p),
|
|
@@ -7671,6 +7708,7 @@ async function runAccountsList(ctx, opts) {
|
|
|
7671
7708
|
const quota = loadQuotaState(hqRoot);
|
|
7672
7709
|
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
|
|
7673
7710
|
existsSync: ctx.existsSync,
|
|
7711
|
+
hostname: ctx.hostname(),
|
|
7674
7712
|
probe: ctx.probeProvider,
|
|
7675
7713
|
env: ctx.env,
|
|
7676
7714
|
}).map((status) => {
|
|
@@ -7686,6 +7724,7 @@ async function runAccountsList(ctx, opts) {
|
|
|
7686
7724
|
});
|
|
7687
7725
|
return {
|
|
7688
7726
|
...status,
|
|
7727
|
+
effective_home: status.home,
|
|
7689
7728
|
model: accountCfg?.model ?? null,
|
|
7690
7729
|
effort: accountCfg?.effort ?? null,
|
|
7691
7730
|
effective_model: model ?? null,
|
|
@@ -7849,9 +7888,23 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7849
7888
|
throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
|
|
7850
7889
|
}
|
|
7851
7890
|
const registeredAccount = config.providers[provider].accounts[id];
|
|
7852
|
-
const
|
|
7853
|
-
|
|
7854
|
-
|
|
7891
|
+
const hostname = ctx.hostname().trim();
|
|
7892
|
+
if (!hostname) {
|
|
7893
|
+
throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
|
|
7894
|
+
}
|
|
7895
|
+
const requestedHome = opts.home?.trim();
|
|
7896
|
+
if (opts.home !== undefined && !requestedHome) {
|
|
7897
|
+
throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
|
|
7898
|
+
}
|
|
7899
|
+
const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
|
|
7900
|
+
? await hostScopedAccountHomesEnabled(ctx)
|
|
7901
|
+
: false;
|
|
7902
|
+
const homeRaw = requestedHome ??
|
|
7903
|
+
(registeredAccount !== undefined
|
|
7904
|
+
? effectiveAccountHome(registeredAccount, hostname).home
|
|
7905
|
+
: id === "a"
|
|
7906
|
+
? DEFAULT_ACCOUNT_HOMES[provider]
|
|
7907
|
+
: `~/.${provider}-${id}`);
|
|
7855
7908
|
const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
|
|
7856
7909
|
? homeRaw
|
|
7857
7910
|
: resolveCallerPath(callerCwd, homeRaw);
|
|
@@ -7860,20 +7913,43 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7860
7913
|
realpathSync: (p) => fs.realpathSync(p),
|
|
7861
7914
|
existsSync: (p) => fs.existsSync(p),
|
|
7862
7915
|
});
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7916
|
+
const nextAccount = registeredAccount === undefined
|
|
7917
|
+
? { home, enabled: true }
|
|
7918
|
+
: { ...registeredAccount, enabled: true };
|
|
7919
|
+
if (registeredAccount !== undefined) {
|
|
7920
|
+
if (hostScopedWriterEnabled && requestedHome !== undefined) {
|
|
7921
|
+
if (home === registeredAccount.home) {
|
|
7922
|
+
if (nextAccount.hosts !== undefined) {
|
|
7923
|
+
delete nextAccount.hosts[hostname];
|
|
7924
|
+
if (Object.keys(nextAccount.hosts).length === 0)
|
|
7925
|
+
delete nextAccount.hosts;
|
|
7926
|
+
}
|
|
7927
|
+
}
|
|
7928
|
+
else {
|
|
7929
|
+
nextAccount.hosts = {
|
|
7930
|
+
...(registeredAccount.hosts ?? {}),
|
|
7931
|
+
[hostname]: { home },
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7934
|
+
}
|
|
7935
|
+
else if (requestedHome !== undefined) {
|
|
7936
|
+
// With the rollout off, retain the legacy shared-home write shape.
|
|
7937
|
+
nextAccount.home = home;
|
|
7868
7938
|
}
|
|
7869
7939
|
}
|
|
7940
|
+
delete nextAccount.disabled_reason;
|
|
7941
|
+
assertUniqueAccountHomes(provider, {
|
|
7942
|
+
...config.providers[provider].accounts,
|
|
7943
|
+
[id]: nextAccount,
|
|
7944
|
+
});
|
|
7870
7945
|
ensureDir(home);
|
|
7871
7946
|
const defaultId = config.providers[provider].default_account;
|
|
7872
|
-
const
|
|
7873
|
-
? config.providers[provider].accounts[defaultId]
|
|
7947
|
+
const defaultAccount = defaultId !== undefined
|
|
7948
|
+
? config.providers[provider].accounts[defaultId]
|
|
7949
|
+
: Object.values(config.providers[provider].accounts).find((account) => account.enabled);
|
|
7950
|
+
const defaultHome = (defaultAccount !== undefined
|
|
7951
|
+
? effectiveAccountHome(defaultAccount, hostname).home
|
|
7874
7952
|
: undefined) ??
|
|
7875
|
-
Object.values(config.providers[provider].accounts).find((account) => account.enabled)
|
|
7876
|
-
?.home ??
|
|
7877
7953
|
canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
|
|
7878
7954
|
homedir: ctx.homedir,
|
|
7879
7955
|
realpathSync: (p) => fs.realpathSync(p),
|
|
@@ -7899,12 +7975,10 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7899
7975
|
if ((grokNeedsRails || copied) && !railsPresent(home)) {
|
|
7900
7976
|
throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
|
|
7901
7977
|
}
|
|
7902
|
-
config.providers[provider].accounts[id] =
|
|
7903
|
-
home,
|
|
7904
|
-
enabled: true,
|
|
7905
|
-
};
|
|
7978
|
+
config.providers[provider].accounts[id] = nextAccount;
|
|
7906
7979
|
const status = accountStatus(provider, id, config, {
|
|
7907
7980
|
existsSync: ctx.existsSync,
|
|
7981
|
+
hostname,
|
|
7908
7982
|
probe: ctx.probeProvider,
|
|
7909
7983
|
env: ctx.env,
|
|
7910
7984
|
});
|
|
@@ -7916,6 +7990,93 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7916
7990
|
writeLanesConfig(hqRoot, config);
|
|
7917
7991
|
printJson({ ok: true, action: "accounts.login", account: status });
|
|
7918
7992
|
}
|
|
7993
|
+
async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
|
|
7994
|
+
if (!(await hostScopedAccountHomesEnabled(ctx))) {
|
|
7995
|
+
throw new LanesError("host_scoped_account_homes_disabled", "Setting a host-specific account home requires the lanes.host-scoped-account-homes flag", { flag: LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG });
|
|
7996
|
+
}
|
|
7997
|
+
const callerCwd = ctx.callerCwd();
|
|
7998
|
+
const hqRoot = ctx.hqRoot();
|
|
7999
|
+
const provider = parseLaneProvider(providerRaw);
|
|
8000
|
+
const id = accountIdRaw.trim();
|
|
8001
|
+
if (!id) {
|
|
8002
|
+
throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
|
|
8003
|
+
provider,
|
|
8004
|
+
account: accountIdRaw,
|
|
8005
|
+
enabled: [],
|
|
8006
|
+
});
|
|
8007
|
+
}
|
|
8008
|
+
const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
|
|
8009
|
+
if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
|
|
8010
|
+
throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
|
|
8011
|
+
host: opts.host ?? "",
|
|
8012
|
+
});
|
|
8013
|
+
}
|
|
8014
|
+
if (!homeRaw.trim()) {
|
|
8015
|
+
throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
|
|
8016
|
+
provider,
|
|
8017
|
+
account: id,
|
|
8018
|
+
});
|
|
8019
|
+
}
|
|
8020
|
+
const config = loadLanesConfig(hqRoot);
|
|
8021
|
+
const account = config.providers[provider].accounts[id];
|
|
8022
|
+
if (account === undefined) {
|
|
8023
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
|
|
8024
|
+
provider,
|
|
8025
|
+
account: id,
|
|
8026
|
+
enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
|
|
8027
|
+
});
|
|
8028
|
+
}
|
|
8029
|
+
const homePath = homeRaw.trim();
|
|
8030
|
+
const isCurrentHost = hostname === ctx.hostname().trim();
|
|
8031
|
+
let home;
|
|
8032
|
+
if (isCurrentHost) {
|
|
8033
|
+
const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
|
|
8034
|
+
? homePath
|
|
8035
|
+
: resolveCallerPath(callerCwd, homePath);
|
|
8036
|
+
home = canonicalizeHome(homeResolved, {
|
|
8037
|
+
homedir: ctx.homedir,
|
|
8038
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
8039
|
+
existsSync: (p) => fs.existsSync(p),
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
else if (homePath === "~" || homePath.startsWith("~/")) {
|
|
8043
|
+
home = homePath;
|
|
8044
|
+
}
|
|
8045
|
+
else if (path.isAbsolute(homePath)) {
|
|
8046
|
+
home = path.normalize(homePath);
|
|
8047
|
+
}
|
|
8048
|
+
else {
|
|
8049
|
+
throw new LanesError("invalid_remote_account_home", "A home for another host must be an absolute path or start with ~/", { provider, account: id, hostname, home: homePath });
|
|
8050
|
+
}
|
|
8051
|
+
for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
|
|
8052
|
+
if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
|
|
8053
|
+
throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
|
|
8054
|
+
}
|
|
8055
|
+
}
|
|
8056
|
+
if (home === account.home) {
|
|
8057
|
+
if (account.hosts !== undefined) {
|
|
8058
|
+
delete account.hosts[hostname];
|
|
8059
|
+
if (Object.keys(account.hosts).length === 0)
|
|
8060
|
+
delete account.hosts;
|
|
8061
|
+
}
|
|
8062
|
+
}
|
|
8063
|
+
else {
|
|
8064
|
+
const hosts = Object.assign(Object.create(null), account.hosts ?? {});
|
|
8065
|
+
hosts[hostname] = { home };
|
|
8066
|
+
account.hosts = hosts;
|
|
8067
|
+
}
|
|
8068
|
+
assertUniqueAccountHomes(provider, config.providers[provider].accounts);
|
|
8069
|
+
writeLanesConfig(hqRoot, config);
|
|
8070
|
+
printJson({
|
|
8071
|
+
ok: true,
|
|
8072
|
+
action: "accounts.set-home",
|
|
8073
|
+
provider,
|
|
8074
|
+
account: id,
|
|
8075
|
+
hostname,
|
|
8076
|
+
home,
|
|
8077
|
+
home_source: home === account.home ? "default" : "host",
|
|
8078
|
+
});
|
|
8079
|
+
}
|
|
7919
8080
|
async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
7920
8081
|
const hqRoot = ctx.hqRoot();
|
|
7921
8082
|
const provider = parseLaneProvider(providerRaw);
|
|
@@ -7932,6 +8093,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
7932
8093
|
}
|
|
7933
8094
|
const status = accountStatus(provider, accountId, config, {
|
|
7934
8095
|
existsSync: ctx.existsSync,
|
|
8096
|
+
hostname: ctx.hostname(),
|
|
7935
8097
|
probe: ctx.probeProvider,
|
|
7936
8098
|
env: ctx.env,
|
|
7937
8099
|
});
|
|
@@ -7948,6 +8110,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
7948
8110
|
action: "accounts.enable",
|
|
7949
8111
|
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
7950
8112
|
existsSync: ctx.existsSync,
|
|
8113
|
+
hostname: ctx.hostname(),
|
|
7951
8114
|
probe: ctx.probeProvider,
|
|
7952
8115
|
env: ctx.env,
|
|
7953
8116
|
}),
|
|
@@ -7976,6 +8139,7 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
|
7976
8139
|
action: "accounts.disable",
|
|
7977
8140
|
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
7978
8141
|
existsSync: ctx.existsSync,
|
|
8142
|
+
hostname: ctx.hostname(),
|
|
7979
8143
|
probe: ctx.probeProvider,
|
|
7980
8144
|
env: ctx.env,
|
|
7981
8145
|
}),
|
|
@@ -8490,6 +8654,17 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8490
8654
|
.action(wrap(async (opts) => {
|
|
8491
8655
|
await runAccountsBalance(ctx, opts);
|
|
8492
8656
|
}));
|
|
8657
|
+
accounts
|
|
8658
|
+
.command("set-home")
|
|
8659
|
+
.description("Set the host-specific home for an existing provider account")
|
|
8660
|
+
.argument("<provider>", "claude | codex | grok")
|
|
8661
|
+
.argument("<account-id>", "Existing account id")
|
|
8662
|
+
.argument("<path>", "Home directory for this host")
|
|
8663
|
+
.option("--host <hostname>", "Host key (defaults to this host's hostname)")
|
|
8664
|
+
.option("--json", "JSON on stdout (always)")
|
|
8665
|
+
.action(wrap(async (provider, accountId, home, opts) => {
|
|
8666
|
+
await runAccountsSetHome(ctx, provider, accountId, home, opts);
|
|
8667
|
+
}));
|
|
8493
8668
|
accounts
|
|
8494
8669
|
.command("login")
|
|
8495
8670
|
.description("Create a home, run the vendor login, and register the account")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AccountConfig } from "./types.js";
|
|
2
|
+
export type EffectiveAccountHome = {
|
|
3
|
+
home: string;
|
|
4
|
+
source: "host" | "default";
|
|
5
|
+
};
|
|
6
|
+
/** Resolve the account home for this host; an empty host identity fails back to the default. */
|
|
7
|
+
export declare function effectiveAccountHome(account: AccountConfig, hostname?: string): EffectiveAccountHome;
|
|
8
|
+
//# sourceMappingURL=account-home.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
/** Resolve the account home for this host; an empty host identity fails back to the default. */
|
|
3
|
+
export function effectiveAccountHome(account, hostname = os.hostname()) {
|
|
4
|
+
const host = hostname.trim();
|
|
5
|
+
const hostHome = host ? account.hosts?.[host]?.home : undefined;
|
|
6
|
+
return hostHome === undefined
|
|
7
|
+
? { home: account.home, source: "default" }
|
|
8
|
+
: { home: hostHome, source: "host" };
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=account-home.js.map
|