@indigoai-us/hq-cli 5.130.0 → 5.132.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 +155 -1
- package/contracts/preflight/v1/fixtures.json +87 -0
- package/contracts/preflight/v1/schema.json +58 -0
- package/dist/command-catalog.generated.d.ts +39 -0
- package/dist/command-catalog.generated.js +50 -0
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +1 -1
- package/dist/commands/files.js +137 -9
- package/dist/commands/lanes.d.ts +13 -2
- package/dist/commands/lanes.js +394 -42
- package/dist/commands/mesh.js +66 -13
- package/dist/commands/update.js +1 -1
- package/dist/lib/bot/paths.d.ts +7 -5
- package/dist/lib/bot/paths.js +8 -1
- package/dist/lib/bot/prompt.d.ts +0 -2
- package/dist/lib/bot/prompt.js +5 -11
- package/dist/lib/bot/room-policy.d.ts +1 -24
- package/dist/lib/bot/room-policy.js +2 -35
- package/dist/lib/bot/run.d.ts +7 -8
- package/dist/lib/bot/run.js +21 -33
- package/dist/lib/bot/session.d.ts +1 -1
- package/dist/lib/bot/session.js +1 -1
- package/dist/lib/lanes/accounts.d.ts +6 -0
- package/dist/lib/lanes/accounts.js +2 -1
- package/dist/lib/lanes/config.d.ts +4 -0
- package/dist/lib/lanes/config.js +9 -0
- package/dist/lib/lanes/index.d.ts +10 -3
- package/dist/lib/lanes/index.js +5 -1
- package/dist/lib/lanes/mesh-events.js +4 -0
- package/dist/lib/lanes/models.d.ts +78 -0
- package/dist/lib/lanes/models.js +205 -0
- package/dist/lib/lanes/move-account.d.ts +56 -0
- package/dist/lib/lanes/move-account.js +193 -0
- package/dist/lib/lanes/provider-rejection.d.ts +86 -0
- package/dist/lib/lanes/provider-rejection.js +258 -0
- package/dist/lib/lanes/quota.d.ts +17 -0
- package/dist/lib/lanes/quota.js +24 -0
- package/dist/lib/lanes/reconcile-install.d.ts +24 -3
- package/dist/lib/lanes/reconcile-install.js +48 -12
- package/dist/lib/lanes/reconcile.js +41 -11
- package/dist/lib/lanes/spawn.js +26 -5
- package/dist/lib/lanes/store.js +10 -0
- package/dist/lib/lanes/types.d.ts +49 -2
- package/dist/lib/lanes/types.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +27 -0
- package/dist/lib/lanes/watcher.js +68 -5
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/api.js +15 -2
- package/dist/lib/mesh/live/backfill-held.d.ts +4 -3
- package/dist/lib/mesh/live/backfill-held.js +37 -14
- package/dist/lib/mesh/live/daemon/doctor.d.ts +4 -0
- package/dist/lib/mesh/live/daemon/doctor.js +15 -4
- package/dist/lib/mesh/live/daemon/install.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/install.js +84 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/run.js +17 -4
- package/dist/lib/mesh/live/emit-client.d.ts +2 -0
- package/dist/lib/mesh/live/emit-client.js +15 -8
- package/dist/lib/mesh/live/emit-evidence.d.ts +19 -0
- package/dist/lib/mesh/live/emit-evidence.js +20 -2
- package/dist/lib/mesh/live/emit.d.ts +9 -0
- package/dist/lib/mesh/live/emit.js +28 -2
- package/dist/lib/mesh/live/flush.js +9 -3
- package/dist/lib/search-index/index.js +22 -3
- package/dist/lib/work-context/company.d.ts +16 -0
- package/dist/lib/work-context/company.js +46 -14
- package/dist/lib/work-context/preflight-contract.d.ts +28 -0
- package/dist/lib/work-context/preflight-contract.js +85 -0
- package/dist/main.js +11 -1
- package/dist/utils/cognito-session.d.ts +39 -0
- package/dist/utils/cognito-session.js +119 -3
- package/dist/utils/environmental-error.d.ts +18 -0
- package/dist/utils/environmental-error.js +240 -7
- package/dist/utils/expected-cli-error.d.ts +2 -0
- package/dist/utils/expected-cli-error.js +4 -0
- package/dist/utils/self-update.js +4 -1
- package/dist/utils/vault-api.d.ts +1 -0
- package/dist/utils/vault-api.js +11 -3
- package/package.json +3 -2
package/dist/lib/lanes/config.js
CHANGED
|
@@ -28,6 +28,8 @@ export const DEFAULT_COMPACT_AFTER_MS = 60 * 60 * 1000;
|
|
|
28
28
|
* with `progress_event_min_interval_ms`.
|
|
29
29
|
*/
|
|
30
30
|
export const DEFAULT_PROGRESS_EVENT_MIN_INTERVAL_MS = 5 * 60 * 1000;
|
|
31
|
+
/** Account hold after a 429 that names no reset time (`quota_default_hold_ms`). */
|
|
32
|
+
export const DEFAULT_QUOTA_HOLD_MS = 15 * 60 * 1000;
|
|
31
33
|
/** Spec §9: last event within 10 minutes → live. */
|
|
32
34
|
export const DEFAULT_LIVE_WITHIN_MS = 10 * 60 * 1000;
|
|
33
35
|
/** Spec §9: turn_start with no turn_end after 2 hours → lost. */
|
|
@@ -231,6 +233,9 @@ function parseProvider(provider, raw, deps) {
|
|
|
231
233
|
if (raw.progress_event_min_interval_ms !== undefined) {
|
|
232
234
|
parsed.progress_event_min_interval_ms = positiveInt(raw.progress_event_min_interval_ms, `providers.${provider}.progress_event_min_interval_ms`);
|
|
233
235
|
}
|
|
236
|
+
if (raw.quota_default_hold_ms !== undefined) {
|
|
237
|
+
parsed.quota_default_hold_ms = positiveInt(raw.quota_default_hold_ms, `providers.${provider}.quota_default_hold_ms`);
|
|
238
|
+
}
|
|
234
239
|
return parsed;
|
|
235
240
|
}
|
|
236
241
|
function parseThrottle(raw) {
|
|
@@ -398,6 +403,10 @@ export function providerCompactAfterMs(provider) {
|
|
|
398
403
|
export function providerProgressEventMinIntervalMs(provider) {
|
|
399
404
|
return provider.progress_event_min_interval_ms ?? DEFAULT_PROGRESS_EVENT_MIN_INTERVAL_MS;
|
|
400
405
|
}
|
|
406
|
+
/** Hold for a reset-less 429 on this provider's accounts. */
|
|
407
|
+
export function providerQuotaDefaultHoldMs(provider) {
|
|
408
|
+
return provider.quota_default_hold_ms ?? DEFAULT_QUOTA_HOLD_MS;
|
|
409
|
+
}
|
|
401
410
|
/** Account running cap, defaulting to the provider ceiling when unset. */
|
|
402
411
|
export function accountMaxRunning(provider, accountId) {
|
|
403
412
|
const account = provider.accounts[accountId];
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
6
|
-
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LaneRecord, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
7
|
-
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, writeLanesConfig, } from "./config.js";
|
|
6
|
+
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LaneRecord, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
7
|
+
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
8
8
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
9
9
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
10
10
|
export type { LanesHqRootOpts } from "./hq-root.js";
|
|
@@ -31,7 +31,14 @@ export type { LivenessDeps, LivenessEventKind, LivenessIndex, SessionIndexEntry,
|
|
|
31
31
|
export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
|
|
32
32
|
export type { ReconcileChange, ReconcileDeps, ReconcileHost, ReconcileSkip, ReconcileSummary, } from "./reconcile.js";
|
|
33
33
|
export { RECONCILE_TIMER_INTERVAL, defaultSystemdUserDir, installReconcileTimer, providerPathEnv, renderReconcileService, renderReconcileTimer, uninstallReconcileTimer, } from "./reconcile-install.js";
|
|
34
|
-
export type { ReconcileInstallOpts, ReconcileInstallResult, RunCommand, RunCommandResult, } from "./reconcile-install.js";
|
|
34
|
+
export type { ProviderPathEnv, ReconcileInstallOpts, ReconcileInstallResult, RunCommand, RunCommandResult, } from "./reconcile-install.js";
|
|
35
|
+
export { unitPathEnv } from "./reconcile-install.js";
|
|
36
|
+
export { PROVIDER_REJECTED_REASON, PROVIDER_REJECTION_MESSAGE_CAP, parseClaudeResultRejection, parseCodexTurnFailed, parseGrokErrorOutput, parseProviderRejection, readProviderRejection, retryableFromStatus, } from "./provider-rejection.js";
|
|
37
|
+
export type { ProviderRejection } from "./provider-rejection.js";
|
|
38
|
+
export { CODEX_MODELS_CACHE_NAME, MODEL_PROBE_TIMEOUT_MS, claudeKnownModels, codexKnownModels, defaultProbe, grokKnownModels, knownModels, modelIsKnown, parseClaudeHelpModels, parseGrokModelsOutput, validateLaneModel, } from "./models.js";
|
|
39
|
+
export type { KnownModels, KnownModelsDeps, ProbeCommand, ProbeResult } from "./models.js";
|
|
40
|
+
export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
|
|
41
|
+
export type { SessionCopyPlan, SessionCopyResult } from "./move-account.js";
|
|
35
42
|
export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
|
|
36
43
|
export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
|
|
37
44
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
6
|
-
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, writeLanesConfig, } from "./config.js";
|
|
6
|
+
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
7
7
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
8
8
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
9
9
|
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
@@ -17,6 +17,10 @@ export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PRO
|
|
|
17
17
|
export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
|
|
18
18
|
export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
|
|
19
19
|
export { RECONCILE_TIMER_INTERVAL, defaultSystemdUserDir, installReconcileTimer, providerPathEnv, renderReconcileService, renderReconcileTimer, uninstallReconcileTimer, } from "./reconcile-install.js";
|
|
20
|
+
export { unitPathEnv } from "./reconcile-install.js";
|
|
21
|
+
export { PROVIDER_REJECTED_REASON, PROVIDER_REJECTION_MESSAGE_CAP, parseClaudeResultRejection, parseCodexTurnFailed, parseGrokErrorOutput, parseProviderRejection, readProviderRejection, retryableFromStatus, } from "./provider-rejection.js";
|
|
22
|
+
export { CODEX_MODELS_CACHE_NAME, MODEL_PROBE_TIMEOUT_MS, claudeKnownModels, codexKnownModels, defaultProbe, grokKnownModels, knownModels, modelIsKnown, parseClaudeHelpModels, parseGrokModelsOutput, validateLaneModel, } from "./models.js";
|
|
23
|
+
export { SESSION_MOVE_REASON, copySessionFiles, encodeClaudeProjectDir, findCodexRollout, planClaudeSessionCopy, planCodexSessionCopy, planSessionCopy, } from "./move-account.js";
|
|
20
24
|
export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorkerRegistryFile, registryPath, resolveContextBasePaths, resolveLaneWorker, resolveSkillFiles, tenancyFromRegistryPath, } from "./registry.js";
|
|
21
25
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
22
26
|
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
@@ -191,10 +191,14 @@ export function laneCanEmit(lane) {
|
|
|
191
191
|
return lane.company !== null && lane.company !== undefined && !!lane.thread_id;
|
|
192
192
|
}
|
|
193
193
|
function baseIdentity(lane) {
|
|
194
|
+
const lastMove = lane.account_history?.[lane.account_history.length - 1];
|
|
194
195
|
return {
|
|
195
196
|
lane_id: lane.lane_id,
|
|
196
197
|
project_id: lane.project_id,
|
|
197
198
|
story_id: lane.story_id,
|
|
199
|
+
...(lane.account !== undefined ? { account: lane.account } : {}),
|
|
200
|
+
// A moved lane names the move on every event its run emits (US-023 §8).
|
|
201
|
+
...(lastMove !== undefined ? { account_move: { from: lastMove.from, to: lastMove.to, at: lastMove.at } } : {}),
|
|
198
202
|
};
|
|
199
203
|
}
|
|
200
204
|
/**
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-provider known model ids, for validating `hq lanes create --model`.
|
|
3
|
+
*
|
|
4
|
+
* No provider adapter in this package carries a model list, so each id set
|
|
5
|
+
* is read from where the provider itself keeps it, the way the accounts
|
|
6
|
+
* probes read provider state: from the account home or the provider CLI,
|
|
7
|
+
* never from a list written into this package.
|
|
8
|
+
*
|
|
9
|
+
* - codex: `<CODEX_HOME>/models_cache.json`, the model catalog the codex CLI
|
|
10
|
+
* fetches and caches per home. Every `models[].slug` is accepted.
|
|
11
|
+
* - grok: `grok models` (run with `GROK_HOME` set to the account home),
|
|
12
|
+
* which prints `Available models:` followed by one id per line. It lists
|
|
13
|
+
* models even when the home is not logged in.
|
|
14
|
+
* - claude: `claude --help`, whose `--model` text names the accepted aliases
|
|
15
|
+
* (`'fable', 'opus', or 'sonnet'`) and says a full name is accepted too.
|
|
16
|
+
* An alias from that text or any `claude-` full name passes.
|
|
17
|
+
*
|
|
18
|
+
* A source that cannot be read (no cache yet, CLI missing, probe failed)
|
|
19
|
+
* returns null and the caller skips validation: the provider's own
|
|
20
|
+
* rejection at run time is still classified `provider_rejected`.
|
|
21
|
+
*/
|
|
22
|
+
import type { LaneProvider } from "./types.js";
|
|
23
|
+
export declare const CODEX_MODELS_CACHE_NAME = "models_cache.json";
|
|
24
|
+
export declare const MODEL_PROBE_TIMEOUT_MS = 10000;
|
|
25
|
+
export interface ProbeResult {
|
|
26
|
+
status: number | null;
|
|
27
|
+
stdout: string;
|
|
28
|
+
stderr: string;
|
|
29
|
+
}
|
|
30
|
+
export type ProbeCommand = (command: string, args: string[], env: NodeJS.ProcessEnv) => ProbeResult;
|
|
31
|
+
export interface KnownModelsDeps {
|
|
32
|
+
readFileSync?: (p: string, encoding: "utf8") => string;
|
|
33
|
+
probe?: ProbeCommand;
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
}
|
|
36
|
+
export interface KnownModels {
|
|
37
|
+
provider: LaneProvider;
|
|
38
|
+
/** Exact ids the provider accepts. */
|
|
39
|
+
ids: string[];
|
|
40
|
+
/** Prefixes accepted in addition to `ids` (claude full names). */
|
|
41
|
+
prefixes: string[];
|
|
42
|
+
/** Where the list came from, for the refusal and the report. */
|
|
43
|
+
source: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function defaultProbe(command: string, args: string[], env: NodeJS.ProcessEnv): ProbeResult;
|
|
46
|
+
/** Read `models[].slug` from the codex catalog cache in `home`. Null when absent or unreadable. */
|
|
47
|
+
export declare function codexKnownModels(home: string, readFileSync?: NonNullable<KnownModelsDeps["readFileSync"]>): KnownModels | null;
|
|
48
|
+
/**
|
|
49
|
+
* Parse `grok models` output:
|
|
50
|
+
*
|
|
51
|
+
* Available models:
|
|
52
|
+
* * grok-4.7 (default)
|
|
53
|
+
* - grok-4.7-build-fast
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseGrokModelsOutput(text: string): string[];
|
|
56
|
+
export declare function grokKnownModels(home: string, probe?: ProbeCommand, env?: NodeJS.ProcessEnv): KnownModels | null;
|
|
57
|
+
/**
|
|
58
|
+
* Parse the `--model` option text of `claude --help`:
|
|
59
|
+
*
|
|
60
|
+
* --model <model> Model for the current session. Provide an alias for
|
|
61
|
+
* the latest model (e.g. 'fable', 'opus', or 'sonnet')
|
|
62
|
+
* or a model's full name (e.g. 'claude-fable-5').
|
|
63
|
+
*
|
|
64
|
+
* Returns the quoted aliases and the prefix of the quoted full-name example.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseClaudeHelpModels(text: string): {
|
|
67
|
+
aliases: string[];
|
|
68
|
+
prefixes: string[];
|
|
69
|
+
};
|
|
70
|
+
export declare function claudeKnownModels(probe?: ProbeCommand, env?: NodeJS.ProcessEnv): KnownModels | null;
|
|
71
|
+
export declare function knownModels(provider: LaneProvider, home: string, deps?: KnownModelsDeps): KnownModels | null;
|
|
72
|
+
export declare function modelIsKnown(known: KnownModels, model: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Refuse a `--model` the provider does not know. Returns the source used, or
|
|
75
|
+
* null when no source was readable and the value passed unvalidated.
|
|
76
|
+
*/
|
|
77
|
+
export declare function validateLaneModel(provider: LaneProvider, model: string, home: string, deps?: KnownModelsDeps): string | null;
|
|
78
|
+
//# sourceMappingURL=models.d.ts.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-provider known model ids, for validating `hq lanes create --model`.
|
|
3
|
+
*
|
|
4
|
+
* No provider adapter in this package carries a model list, so each id set
|
|
5
|
+
* is read from where the provider itself keeps it, the way the accounts
|
|
6
|
+
* probes read provider state: from the account home or the provider CLI,
|
|
7
|
+
* never from a list written into this package.
|
|
8
|
+
*
|
|
9
|
+
* - codex: `<CODEX_HOME>/models_cache.json`, the model catalog the codex CLI
|
|
10
|
+
* fetches and caches per home. Every `models[].slug` is accepted.
|
|
11
|
+
* - grok: `grok models` (run with `GROK_HOME` set to the account home),
|
|
12
|
+
* which prints `Available models:` followed by one id per line. It lists
|
|
13
|
+
* models even when the home is not logged in.
|
|
14
|
+
* - claude: `claude --help`, whose `--model` text names the accepted aliases
|
|
15
|
+
* (`'fable', 'opus', or 'sonnet'`) and says a full name is accepted too.
|
|
16
|
+
* An alias from that text or any `claude-` full name passes.
|
|
17
|
+
*
|
|
18
|
+
* A source that cannot be read (no cache yet, CLI missing, probe failed)
|
|
19
|
+
* returns null and the caller skips validation: the provider's own
|
|
20
|
+
* rejection at run time is still classified `provider_rejected`.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { LanesError } from "./errors.js";
|
|
26
|
+
import { PROVIDER_HOME_ENV } from "./accounts.js";
|
|
27
|
+
export const CODEX_MODELS_CACHE_NAME = "models_cache.json";
|
|
28
|
+
export const MODEL_PROBE_TIMEOUT_MS = 10_000;
|
|
29
|
+
export function defaultProbe(command, args, env) {
|
|
30
|
+
const result = spawnSync(command, args, {
|
|
31
|
+
env,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
timeout: MODEL_PROBE_TIMEOUT_MS,
|
|
34
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
});
|
|
36
|
+
if (result.error) {
|
|
37
|
+
return { status: null, stdout: "", stderr: result.error.message };
|
|
38
|
+
}
|
|
39
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
40
|
+
}
|
|
41
|
+
function isPlainObject(value) {
|
|
42
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
/** Read `models[].slug` from the codex catalog cache in `home`. Null when absent or unreadable. */
|
|
45
|
+
export function codexKnownModels(home, readFileSync = (p, enc) => fs.readFileSync(p, enc)) {
|
|
46
|
+
const source = path.join(home, CODEX_MODELS_CACHE_NAME);
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = readFileSync(source, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const code = err.code;
|
|
53
|
+
if (code === "ENOENT")
|
|
54
|
+
return null;
|
|
55
|
+
// EACCES, EISDIR, EIO: the list cannot be read, which is the same
|
|
56
|
+
// contract as a missing or corrupt cache. Reported, never an internal failure.
|
|
57
|
+
process.stderr.write(`hq lanes: cannot read ${source} (${err instanceof Error ? err.message : String(err)}); --model is not validated for codex\n`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(text);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
process.stderr.write(`hq lanes: ${source} is not JSON (${err instanceof Error ? err.message : String(err)}); --model is not validated for codex\n`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.models))
|
|
69
|
+
return null;
|
|
70
|
+
const ids = [];
|
|
71
|
+
for (const entry of parsed.models) {
|
|
72
|
+
if (isPlainObject(entry) && typeof entry.slug === "string" && entry.slug.trim().length > 0) {
|
|
73
|
+
ids.push(entry.slug.trim());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (ids.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
return { provider: "codex", ids, prefixes: [], source };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parse `grok models` output:
|
|
82
|
+
*
|
|
83
|
+
* Available models:
|
|
84
|
+
* * grok-4.7 (default)
|
|
85
|
+
* - grok-4.7-build-fast
|
|
86
|
+
*/
|
|
87
|
+
export function parseGrokModelsOutput(text) {
|
|
88
|
+
const ids = [];
|
|
89
|
+
let inList = false;
|
|
90
|
+
for (const raw of text.split("\n")) {
|
|
91
|
+
const line = raw.trim();
|
|
92
|
+
if (/^available models:/i.test(line)) {
|
|
93
|
+
inList = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!inList)
|
|
97
|
+
continue;
|
|
98
|
+
const match = /^[*-]\s+(\S+)/.exec(line);
|
|
99
|
+
if (match) {
|
|
100
|
+
ids.push(match[1]);
|
|
101
|
+
}
|
|
102
|
+
else if (line.length === 0 && ids.length > 0) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return ids;
|
|
107
|
+
}
|
|
108
|
+
export function grokKnownModels(home, probe = defaultProbe, env = process.env) {
|
|
109
|
+
const source = "grok models";
|
|
110
|
+
const result = probe("grok", ["models"], { ...env, [PROVIDER_HOME_ENV.grok]: home });
|
|
111
|
+
// Only a probe that exited 0 is a catalog. A wrapper or auth check can
|
|
112
|
+
// print a partial list and then fail; an id missing from that output must
|
|
113
|
+
// not refuse the model, so a failed probe skips validation.
|
|
114
|
+
if (result.status !== 0) {
|
|
115
|
+
process.stderr.write(`hq lanes: \`${source}\` failed (exit ${result.status ?? "spawn error"}${result.stderr.trim() ? `: ${result.stderr.trim().split("\n")[0]}` : ""}); --model is not validated for grok\n`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const ids = parseGrokModelsOutput(result.stdout);
|
|
119
|
+
if (ids.length === 0) {
|
|
120
|
+
process.stderr.write(`hq lanes: \`${source}\` listed no models (exit ${result.status ?? "spawn error"}${result.stderr.trim() ? `: ${result.stderr.trim().split("\n")[0]}` : ""}); --model is not validated for grok\n`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return { provider: "grok", ids, prefixes: [], source };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parse the `--model` option text of `claude --help`:
|
|
127
|
+
*
|
|
128
|
+
* --model <model> Model for the current session. Provide an alias for
|
|
129
|
+
* the latest model (e.g. 'fable', 'opus', or 'sonnet')
|
|
130
|
+
* or a model's full name (e.g. 'claude-fable-5').
|
|
131
|
+
*
|
|
132
|
+
* Returns the quoted aliases and the prefix of the quoted full-name example.
|
|
133
|
+
*/
|
|
134
|
+
export function parseClaudeHelpModels(text) {
|
|
135
|
+
const start = text.indexOf("--model");
|
|
136
|
+
if (start === -1)
|
|
137
|
+
return { aliases: [], prefixes: [] };
|
|
138
|
+
const rest = text.slice(start);
|
|
139
|
+
const end = rest.search(/\n\s*-{1,2}[A-Za-z]/);
|
|
140
|
+
const block = end === -1 ? rest : rest.slice(0, end);
|
|
141
|
+
const aliases = [];
|
|
142
|
+
const prefixes = new Set();
|
|
143
|
+
// Only quoted tokens shaped like an id: the possessive in "model's" also
|
|
144
|
+
// opens a quote, and must not be read as one.
|
|
145
|
+
for (const match of block.matchAll(/'([A-Za-z0-9][A-Za-z0-9._[\]-]*)'/g)) {
|
|
146
|
+
const value = match[1];
|
|
147
|
+
const dash = value.indexOf("-");
|
|
148
|
+
if (dash > 0) {
|
|
149
|
+
prefixes.add(value.slice(0, dash + 1));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
aliases.push(value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { aliases, prefixes: [...prefixes] };
|
|
156
|
+
}
|
|
157
|
+
export function claudeKnownModels(probe = defaultProbe, env = process.env) {
|
|
158
|
+
const source = "claude --help";
|
|
159
|
+
const result = probe("claude", ["--help"], env);
|
|
160
|
+
if (result.status !== 0) {
|
|
161
|
+
process.stderr.write(`hq lanes: \`${source}\` failed (exit ${result.status ?? "spawn error"}${result.stderr.trim() ? `: ${result.stderr.trim().split("\n")[0]}` : ""}); --model is not validated for claude\n`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const parsed = parseClaudeHelpModels(result.stdout);
|
|
165
|
+
if (parsed.aliases.length === 0 && parsed.prefixes.length === 0) {
|
|
166
|
+
process.stderr.write(`hq lanes: \`${source}\` named no --model aliases (exit ${result.status ?? "spawn error"}); --model is not validated for claude\n`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return { provider: "claude", ids: parsed.aliases, prefixes: parsed.prefixes, source };
|
|
170
|
+
}
|
|
171
|
+
export function knownModels(provider, home, deps = {}) {
|
|
172
|
+
switch (provider) {
|
|
173
|
+
case "codex":
|
|
174
|
+
return codexKnownModels(home, deps.readFileSync);
|
|
175
|
+
case "grok":
|
|
176
|
+
return grokKnownModels(home, deps.probe, deps.env);
|
|
177
|
+
case "claude":
|
|
178
|
+
return claudeKnownModels(deps.probe, deps.env);
|
|
179
|
+
default: {
|
|
180
|
+
const _exhaustive = provider;
|
|
181
|
+
return _exhaustive;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export function modelIsKnown(known, model) {
|
|
186
|
+
if (known.ids.includes(model))
|
|
187
|
+
return true;
|
|
188
|
+
return known.prefixes.some((prefix) => model.startsWith(prefix) && model.length > prefix.length);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Refuse a `--model` the provider does not know. Returns the source used, or
|
|
192
|
+
* null when no source was readable and the value passed unvalidated.
|
|
193
|
+
*/
|
|
194
|
+
export function validateLaneModel(provider, model, home, deps = {}) {
|
|
195
|
+
const known = knownModels(provider, home, deps);
|
|
196
|
+
if (known === null)
|
|
197
|
+
return null;
|
|
198
|
+
if (modelIsKnown(known, model))
|
|
199
|
+
return known.source;
|
|
200
|
+
const accepted = [...known.ids, ...known.prefixes.map((prefix) => `${prefix}*`)];
|
|
201
|
+
const message = `Provider ${provider} does not know model ${JSON.stringify(model)} (source: ${known.source}). Accepted: ${accepted.join(", ")}`;
|
|
202
|
+
// `message` is in details too: LanesError.toJSON prints details only.
|
|
203
|
+
throw new LanesError("model_unsupported", message, { provider, model, accepted, source: known.source, message });
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-023: move a lane's provider session to another account of the same
|
|
3
|
+
* provider. A provider CLI only resumes sessions stored under its own home
|
|
4
|
+
* (`CLAUDE_CONFIG_DIR/projects/<encoded cwd>/<id>.jsonl`,
|
|
5
|
+
* `CODEX_HOME/sessions/YYYY/MM/DD/rollout-*-<id>.jsonl`), so the session
|
|
6
|
+
* files are copied into the target home first. The source copy is never
|
|
7
|
+
* moved or deleted, so the lane can be moved back. Only session transcripts
|
|
8
|
+
* are copied: a credential file name is refused by name, whatever path it
|
|
9
|
+
* arrives on.
|
|
10
|
+
*/
|
|
11
|
+
import type { LaneProvider } from "./types.js";
|
|
12
|
+
export declare const SESSION_MOVE_REASON: "operator_move";
|
|
13
|
+
export interface SessionCopyPlan {
|
|
14
|
+
provider: LaneProvider;
|
|
15
|
+
/** Source files, absolute. */
|
|
16
|
+
sources: string[];
|
|
17
|
+
/** Target files, absolute, in the same order as `sources`. */
|
|
18
|
+
targets: string[];
|
|
19
|
+
/** Source directory copied as a tree (claude subagent transcripts), or null. */
|
|
20
|
+
source_dir: string | null;
|
|
21
|
+
target_dir: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface SessionCopyResult extends SessionCopyPlan {
|
|
24
|
+
/** Target paths that were written by this call (already-present ones are not listed). */
|
|
25
|
+
copied: string[];
|
|
26
|
+
}
|
|
27
|
+
/** `claude` keys a project directory by its cwd with every non-alphanumeric character as `-`. */
|
|
28
|
+
export declare function encodeClaudeProjectDir(cwd: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Locate the claude transcript `<sessionId>.jsonl` and its optional sibling
|
|
31
|
+
* `<sessionId>/` directory under `<home>/projects/<encoded cwd>/`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function planClaudeSessionCopy(sourceHome: string, targetHome: string, cwd: string, sessionId: string): SessionCopyPlan;
|
|
34
|
+
/** Walk `<home>/sessions/YYYY/MM/DD/` for `rollout-*-<sessionId>.jsonl`. */
|
|
35
|
+
export declare function findCodexRollout(home: string, sessionId: string): string | null;
|
|
36
|
+
export declare function planCodexSessionCopy(sourceHome: string, targetHome: string, sessionId: string): SessionCopyPlan;
|
|
37
|
+
export declare function planSessionCopy(provider: LaneProvider, input: {
|
|
38
|
+
sourceHome: string;
|
|
39
|
+
targetHome: string;
|
|
40
|
+
cwd: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
}): SessionCopyPlan;
|
|
43
|
+
/** Every file a plan would write, transcript(s) first, then the subagent tree. */
|
|
44
|
+
export declare function planFiles(plan: SessionCopyPlan): Array<{
|
|
45
|
+
from: string;
|
|
46
|
+
to: string;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Execute a plan: copy the transcript(s) and the subagent tree into the
|
|
50
|
+
* target home. Every path is checked against the credential file names
|
|
51
|
+
* before the first byte is copied, so a refusal writes nothing. A file
|
|
52
|
+
* already present in the target with the same content is left alone; one
|
|
53
|
+
* that differs is replaced by the source copy. The source is never touched.
|
|
54
|
+
*/
|
|
55
|
+
export declare function copySessionFiles(plan: SessionCopyPlan): SessionCopyResult;
|
|
56
|
+
//# sourceMappingURL=move-account.d.ts.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-023: move a lane's provider session to another account of the same
|
|
3
|
+
* provider. A provider CLI only resumes sessions stored under its own home
|
|
4
|
+
* (`CLAUDE_CONFIG_DIR/projects/<encoded cwd>/<id>.jsonl`,
|
|
5
|
+
* `CODEX_HOME/sessions/YYYY/MM/DD/rollout-*-<id>.jsonl`), so the session
|
|
6
|
+
* files are copied into the target home first. The source copy is never
|
|
7
|
+
* moved or deleted, so the lane can be moved back. Only session transcripts
|
|
8
|
+
* are copied: a credential file name is refused by name, whatever path it
|
|
9
|
+
* arrives on.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { PROVIDER_CREDENTIAL_FILES } from "./accounts.js";
|
|
14
|
+
import { LanesError } from "./errors.js";
|
|
15
|
+
export const SESSION_MOVE_REASON = "operator_move";
|
|
16
|
+
/** `claude` keys a project directory by its cwd with every non-alphanumeric character as `-`. */
|
|
17
|
+
export function encodeClaudeProjectDir(cwd) {
|
|
18
|
+
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
19
|
+
}
|
|
20
|
+
function credentialName(provider, file) {
|
|
21
|
+
const base = path.basename(file);
|
|
22
|
+
return Object.values(PROVIDER_CREDENTIAL_FILES).some((names) => names.includes(base)) ||
|
|
23
|
+
PROVIDER_CREDENTIAL_FILES[provider].includes(base);
|
|
24
|
+
}
|
|
25
|
+
function isDirectory(p) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.statSync(p).isDirectory();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isFile(p) {
|
|
34
|
+
try {
|
|
35
|
+
return fs.statSync(p).isFile();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function sessionMissing(provider, sessionId, searched) {
|
|
42
|
+
return new LanesError("provider_session_missing", `No ${provider} session ${sessionId} under the source account home. Searched: ${searched.join(", ")}`, { provider, session_id: sessionId, searched });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Locate the claude transcript `<sessionId>.jsonl` and its optional sibling
|
|
46
|
+
* `<sessionId>/` directory under `<home>/projects/<encoded cwd>/`.
|
|
47
|
+
*/
|
|
48
|
+
export function planClaudeSessionCopy(sourceHome, targetHome, cwd, sessionId) {
|
|
49
|
+
const rel = path.join("projects", encodeClaudeProjectDir(cwd));
|
|
50
|
+
const transcript = path.join(sourceHome, rel, `${sessionId}.jsonl`);
|
|
51
|
+
if (!isFile(transcript))
|
|
52
|
+
throw sessionMissing("claude", sessionId, [transcript]);
|
|
53
|
+
const subagents = path.join(sourceHome, rel, sessionId);
|
|
54
|
+
const hasSubagents = isDirectory(subagents);
|
|
55
|
+
return {
|
|
56
|
+
provider: "claude",
|
|
57
|
+
sources: [transcript],
|
|
58
|
+
targets: [path.join(targetHome, rel, `${sessionId}.jsonl`)],
|
|
59
|
+
source_dir: hasSubagents ? subagents : null,
|
|
60
|
+
target_dir: hasSubagents ? path.join(targetHome, rel, sessionId) : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Walk `<home>/sessions/YYYY/MM/DD/` for `rollout-*-<sessionId>.jsonl`. */
|
|
64
|
+
export function findCodexRollout(home, sessionId) {
|
|
65
|
+
const root = path.join(home, "sessions");
|
|
66
|
+
if (!isDirectory(root))
|
|
67
|
+
return null;
|
|
68
|
+
const suffix = `-${sessionId}.jsonl`;
|
|
69
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
70
|
+
while (stack.length > 0) {
|
|
71
|
+
const { dir, depth } = stack.pop();
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const full = path.join(dir, entry.name);
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
if (depth < 3)
|
|
83
|
+
stack.push({ dir: full, depth: depth + 1 });
|
|
84
|
+
}
|
|
85
|
+
else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(suffix)) {
|
|
86
|
+
return full;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
export function planCodexSessionCopy(sourceHome, targetHome, sessionId) {
|
|
93
|
+
const rollout = findCodexRollout(sourceHome, sessionId);
|
|
94
|
+
if (rollout === null) {
|
|
95
|
+
throw sessionMissing("codex", sessionId, [path.join(sourceHome, "sessions", "**", `rollout-*-${sessionId}.jsonl`)]);
|
|
96
|
+
}
|
|
97
|
+
const rel = path.relative(sourceHome, rollout);
|
|
98
|
+
return {
|
|
99
|
+
provider: "codex",
|
|
100
|
+
sources: [rollout],
|
|
101
|
+
targets: [path.join(targetHome, rel)],
|
|
102
|
+
source_dir: null,
|
|
103
|
+
target_dir: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function planSessionCopy(provider, input) {
|
|
107
|
+
switch (provider) {
|
|
108
|
+
case "claude":
|
|
109
|
+
return planClaudeSessionCopy(input.sourceHome, input.targetHome, input.cwd, input.sessionId);
|
|
110
|
+
case "codex":
|
|
111
|
+
return planCodexSessionCopy(input.sourceHome, input.targetHome, input.sessionId);
|
|
112
|
+
case "grok":
|
|
113
|
+
throw new LanesError("provider_session_not_portable", "Provider grok has no session resume, so a lane cannot move between grok accounts", { provider });
|
|
114
|
+
default: {
|
|
115
|
+
const _exhaustive = provider;
|
|
116
|
+
return _exhaustive;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function refuseCredential(provider, file) {
|
|
121
|
+
if (credentialName(provider, file)) {
|
|
122
|
+
throw new LanesError("credential_copy_refused", `Refusing to copy ${path.basename(file)}: only session transcripts move between accounts`, { provider, file: path.basename(file) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function sameContent(a, b) {
|
|
126
|
+
try {
|
|
127
|
+
const sa = fs.statSync(a);
|
|
128
|
+
const sb = fs.statSync(b);
|
|
129
|
+
if (sa.size !== sb.size)
|
|
130
|
+
return false;
|
|
131
|
+
return fs.readFileSync(a).equals(fs.readFileSync(b));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Copy one file via a temp name and rename, so a partial copy is never a
|
|
139
|
+
* visible transcript. A target that already exists is refreshed when its
|
|
140
|
+
* content differs: the lane last ran on the source account, so the source
|
|
141
|
+
* copy is the newer one (a lane moved A→B and back to A must not resume A's
|
|
142
|
+
* pre-move transcript). Returns false when the target already matched.
|
|
143
|
+
*/
|
|
144
|
+
function copyFileAtomic(source, target) {
|
|
145
|
+
if (fs.existsSync(target) && sameContent(source, target))
|
|
146
|
+
return false;
|
|
147
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
148
|
+
const tmp = `${target}.partial.${process.pid}`;
|
|
149
|
+
fs.copyFileSync(source, tmp);
|
|
150
|
+
fs.renameSync(tmp, target);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
function listTree(sourceDir, targetDir, out) {
|
|
154
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
155
|
+
const from = path.join(sourceDir, entry.name);
|
|
156
|
+
const to = path.join(targetDir, entry.name);
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
listTree(from, to, out);
|
|
159
|
+
}
|
|
160
|
+
else if (entry.isFile()) {
|
|
161
|
+
out.push({ from, to });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Every file a plan would write, transcript(s) first, then the subagent tree. */
|
|
166
|
+
export function planFiles(plan) {
|
|
167
|
+
const files = plan.sources.map((from, i) => ({ from, to: plan.targets[i] }));
|
|
168
|
+
if (plan.source_dir !== null && plan.target_dir !== null) {
|
|
169
|
+
listTree(plan.source_dir, plan.target_dir, files);
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Execute a plan: copy the transcript(s) and the subagent tree into the
|
|
175
|
+
* target home. Every path is checked against the credential file names
|
|
176
|
+
* before the first byte is copied, so a refusal writes nothing. A file
|
|
177
|
+
* already present in the target with the same content is left alone; one
|
|
178
|
+
* that differs is replaced by the source copy. The source is never touched.
|
|
179
|
+
*/
|
|
180
|
+
export function copySessionFiles(plan) {
|
|
181
|
+
const files = planFiles(plan);
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
refuseCredential(plan.provider, file.from);
|
|
184
|
+
refuseCredential(plan.provider, file.to);
|
|
185
|
+
}
|
|
186
|
+
const copied = [];
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
if (copyFileAtomic(file.from, file.to))
|
|
189
|
+
copied.push(file.to);
|
|
190
|
+
}
|
|
191
|
+
return { ...plan, copied };
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=move-account.js.map
|