sidekick-shared 0.27.6 → 0.28.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 +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getConfigDir } from './paths';
|
|
4
|
+
import { atomicWriteJsonSync, withFileLockSync } from './writers/atomic';
|
|
5
|
+
import { _emitAccountsChanged } from './accountChangeSignal';
|
|
6
|
+
export { ACCOUNT_PROVIDER_IDS } from './providerIds';
|
|
7
|
+
export function getAccountsDir() {
|
|
8
|
+
return path.join(getConfigDir(), 'accounts');
|
|
9
|
+
}
|
|
10
|
+
function getRegistryPath() {
|
|
11
|
+
return path.join(getAccountsDir(), 'accounts.json');
|
|
12
|
+
}
|
|
13
|
+
function getRegistryLockPath() {
|
|
14
|
+
return getRegistryPath() + '.lock';
|
|
15
|
+
}
|
|
16
|
+
function ensureAccountsDir() {
|
|
17
|
+
fs.mkdirSync(getAccountsDir(), { recursive: true, mode: 0o700 });
|
|
18
|
+
}
|
|
19
|
+
function writeRegistryUnlocked(registry, options = {}) {
|
|
20
|
+
atomicWriteJsonSync(getRegistryPath(), normalizeRegistry(registry));
|
|
21
|
+
if (!options.silent)
|
|
22
|
+
_emitAccountsChanged('local');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run `operation` while holding the registry lock. For callers that need a
|
|
26
|
+
* read-modify-write across several steps (the legacy Claude facade's
|
|
27
|
+
* add/remove); inside it use the `*Unlocked` writers, never the locking ones:
|
|
28
|
+
* the lock is not re-entrant.
|
|
29
|
+
*/
|
|
30
|
+
export function withSavedAccountRegistryLock(operation) {
|
|
31
|
+
ensureAccountsDir();
|
|
32
|
+
return withFileLockSync(getRegistryLockPath(), operation);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Serializes registry read-modify-write cycles across processes. Without the
|
|
36
|
+
* lock, two concurrent mutators (CLI, editor extension, external consumers)
|
|
37
|
+
* read the same base state and the second write silently drops the first
|
|
38
|
+
* mutation. Lock ordering: callers that also hold a provider auth-swap lock
|
|
39
|
+
* take that lock first and this one inside it, never the reverse.
|
|
40
|
+
*/
|
|
41
|
+
function mutateSavedAccountRegistry(mutator, options = {}) {
|
|
42
|
+
return withSavedAccountRegistryLock(() => mutateSavedAccountRegistryUnlocked(mutator, options));
|
|
43
|
+
}
|
|
44
|
+
function mutateSavedAccountRegistryUnlocked(mutator, options = {}) {
|
|
45
|
+
const next = mutator(readSavedAccountRegistry() ?? createEmptyRegistry());
|
|
46
|
+
writeRegistryUnlocked(next, options);
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
function createEmptyRegistry() {
|
|
50
|
+
return {
|
|
51
|
+
version: 2,
|
|
52
|
+
activeByProvider: {
|
|
53
|
+
'claude-code': null,
|
|
54
|
+
codex: null,
|
|
55
|
+
},
|
|
56
|
+
accounts: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function normalizeRegistry(registry) {
|
|
60
|
+
const normalized = createEmptyRegistry();
|
|
61
|
+
normalized.activeByProvider['claude-code'] = registry.activeByProvider?.['claude-code'] ?? null;
|
|
62
|
+
normalized.activeByProvider.codex = registry.activeByProvider?.codex ?? null;
|
|
63
|
+
normalized.accounts = Array.isArray(registry.accounts)
|
|
64
|
+
? registry.accounts
|
|
65
|
+
.filter((account) => !!account &&
|
|
66
|
+
(account.providerId === 'claude-code' || account.providerId === 'codex') &&
|
|
67
|
+
typeof account.id === 'string' &&
|
|
68
|
+
typeof account.addedAt === 'string')
|
|
69
|
+
.map((account) => ({
|
|
70
|
+
...account,
|
|
71
|
+
label: account.label || undefined,
|
|
72
|
+
email: account.email || undefined,
|
|
73
|
+
providerAccountId: account.providerAccountId || undefined,
|
|
74
|
+
metadata: account.metadata
|
|
75
|
+
? {
|
|
76
|
+
email: account.metadata.email || undefined,
|
|
77
|
+
workspaceId: account.metadata.workspaceId || undefined,
|
|
78
|
+
planType: account.metadata.planType || undefined,
|
|
79
|
+
authMode: account.metadata.authMode || undefined,
|
|
80
|
+
origin: account.metadata.origin || undefined,
|
|
81
|
+
}
|
|
82
|
+
: undefined,
|
|
83
|
+
}))
|
|
84
|
+
: [];
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
function migrateLegacyRegistry(legacy) {
|
|
88
|
+
return {
|
|
89
|
+
version: 2,
|
|
90
|
+
activeByProvider: {
|
|
91
|
+
'claude-code': legacy.activeAccountUuid,
|
|
92
|
+
codex: null,
|
|
93
|
+
},
|
|
94
|
+
accounts: legacy.accounts.map((account) => ({
|
|
95
|
+
id: account.uuid,
|
|
96
|
+
providerId: 'claude-code',
|
|
97
|
+
providerAccountId: account.uuid,
|
|
98
|
+
email: account.email,
|
|
99
|
+
label: account.label,
|
|
100
|
+
addedAt: account.addedAt,
|
|
101
|
+
metadata: {
|
|
102
|
+
email: account.email,
|
|
103
|
+
},
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function readSavedAccountRegistry() {
|
|
108
|
+
try {
|
|
109
|
+
const content = fs.readFileSync(getRegistryPath(), 'utf8');
|
|
110
|
+
const parsed = JSON.parse(content);
|
|
111
|
+
if (parsed?.version === 2) {
|
|
112
|
+
return normalizeRegistry(parsed);
|
|
113
|
+
}
|
|
114
|
+
if (parsed?.version === 1 && Array.isArray(parsed.accounts)) {
|
|
115
|
+
return migrateLegacyRegistry(parsed);
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function writeSavedAccountRegistry(registry) {
|
|
124
|
+
ensureAccountsDir();
|
|
125
|
+
withFileLockSync(getRegistryLockPath(), () => writeRegistryUnlocked(registry));
|
|
126
|
+
}
|
|
127
|
+
export function listSavedAccountProfiles(providerId) {
|
|
128
|
+
const registry = readSavedAccountRegistry();
|
|
129
|
+
if (!registry)
|
|
130
|
+
return [];
|
|
131
|
+
return providerId
|
|
132
|
+
? registry.accounts.filter((account) => account.providerId === providerId)
|
|
133
|
+
: registry.accounts;
|
|
134
|
+
}
|
|
135
|
+
export function getActiveSavedAccount(providerId) {
|
|
136
|
+
const registry = readSavedAccountRegistry();
|
|
137
|
+
if (!registry)
|
|
138
|
+
return null;
|
|
139
|
+
const activeId = registry.activeByProvider[providerId];
|
|
140
|
+
if (!activeId)
|
|
141
|
+
return null;
|
|
142
|
+
return (registry.accounts.find((account) => account.providerId === providerId && account.id === activeId) ?? null);
|
|
143
|
+
}
|
|
144
|
+
export function upsertSavedAccountProfile(profile) {
|
|
145
|
+
return mutateSavedAccountRegistry(upsertProfile(profile));
|
|
146
|
+
}
|
|
147
|
+
export function setActiveSavedAccount(providerId, accountId, options = {}) {
|
|
148
|
+
return mutateSavedAccountRegistry(setActive(providerId, accountId), options);
|
|
149
|
+
}
|
|
150
|
+
function setActive(providerId, accountId) {
|
|
151
|
+
return (registry) => {
|
|
152
|
+
registry.activeByProvider[providerId] = accountId;
|
|
153
|
+
return registry;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function upsertProfile(profile) {
|
|
157
|
+
return (registry) => {
|
|
158
|
+
const index = registry.accounts.findIndex((account) => account.providerId === profile.providerId && account.id === profile.id);
|
|
159
|
+
if (index >= 0) {
|
|
160
|
+
registry.accounts[index] = profile;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
registry.accounts.push(profile);
|
|
164
|
+
}
|
|
165
|
+
return registry;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** {@link upsertSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
169
|
+
export function upsertSavedAccountProfileUnlocked(profile) {
|
|
170
|
+
return mutateSavedAccountRegistryUnlocked(upsertProfile(profile));
|
|
171
|
+
}
|
|
172
|
+
/** {@link setActiveSavedAccount} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
173
|
+
export function setActiveSavedAccountUnlocked(providerId, accountId, options = {}) {
|
|
174
|
+
return mutateSavedAccountRegistryUnlocked(setActive(providerId, accountId), options);
|
|
175
|
+
}
|
|
176
|
+
function replaceProfiles(providerId, accounts, activeId) {
|
|
177
|
+
return (registry) => {
|
|
178
|
+
registry.accounts = registry.accounts
|
|
179
|
+
.filter((account) => account.providerId !== providerId)
|
|
180
|
+
.concat(accounts);
|
|
181
|
+
registry.activeByProvider[providerId] = activeId;
|
|
182
|
+
return registry;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function replaceSavedAccountProfiles(providerId, accounts, activeId) {
|
|
186
|
+
return mutateSavedAccountRegistry(replaceProfiles(providerId, accounts, activeId));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Same as {@link replaceSavedAccountProfiles} without taking the registry
|
|
190
|
+
* lock: for callers already inside {@link withSavedAccountRegistryLock}.
|
|
191
|
+
*/
|
|
192
|
+
export function replaceSavedAccountProfilesUnlocked(providerId, accounts, activeId) {
|
|
193
|
+
return mutateSavedAccountRegistryUnlocked(replaceProfiles(providerId, accounts, activeId));
|
|
194
|
+
}
|
|
195
|
+
export function removeSavedAccountProfile(providerId, accountId) {
|
|
196
|
+
ensureAccountsDir();
|
|
197
|
+
return withFileLockSync(getRegistryLockPath(), () => removeSavedAccountProfileUnlocked(providerId, accountId));
|
|
198
|
+
}
|
|
199
|
+
/** {@link removeSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
|
|
200
|
+
export function removeSavedAccountProfileUnlocked(providerId, accountId) {
|
|
201
|
+
const registry = readSavedAccountRegistry();
|
|
202
|
+
if (!registry)
|
|
203
|
+
return null;
|
|
204
|
+
const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
|
|
205
|
+
if (index === -1)
|
|
206
|
+
return null;
|
|
207
|
+
const [removed] = registry.accounts.splice(index, 1);
|
|
208
|
+
if (registry.activeByProvider[providerId] === accountId) {
|
|
209
|
+
registry.activeByProvider[providerId] =
|
|
210
|
+
registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
|
|
211
|
+
}
|
|
212
|
+
writeRegistryUnlocked(registry);
|
|
213
|
+
return removed;
|
|
214
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { resolveActiveClaudeAccount } from './accounts';
|
|
2
|
+
import { resolveActiveCodexAccount } from './codexProfiles';
|
|
3
|
+
function errorMessage(error) {
|
|
4
|
+
return error instanceof Error ? error.message : String(error);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Reads active Claude Code and Codex account status in one filesystem pass.
|
|
8
|
+
*
|
|
9
|
+
* If reading either provider throws, the result still has provider-shaped
|
|
10
|
+
* fields so startup flows can render a consistent "not configured" state.
|
|
11
|
+
*/
|
|
12
|
+
export function getActiveAccountStatus(error, options = {}) {
|
|
13
|
+
try {
|
|
14
|
+
const claudeAccount = resolveActiveClaudeAccount(options);
|
|
15
|
+
const codexAccount = resolveActiveCodexAccount(options);
|
|
16
|
+
const claude = claudeAccount.source !== 'none'
|
|
17
|
+
? {
|
|
18
|
+
present: true,
|
|
19
|
+
...(claudeAccount.registryAccountId
|
|
20
|
+
? { accountId: claudeAccount.registryAccountId }
|
|
21
|
+
: {}),
|
|
22
|
+
email: claudeAccount.email,
|
|
23
|
+
label: claudeAccount.label ?? claudeAccount.email,
|
|
24
|
+
}
|
|
25
|
+
: { present: false };
|
|
26
|
+
const codex = codexAccount.source !== 'none'
|
|
27
|
+
? {
|
|
28
|
+
present: true,
|
|
29
|
+
...(codexAccount.registryAccountId
|
|
30
|
+
? { accountId: codexAccount.registryAccountId }
|
|
31
|
+
: {}),
|
|
32
|
+
email: codexAccount.email,
|
|
33
|
+
label: codexAccount.label ?? codexAccount.email ?? codexAccount.providerAccountId,
|
|
34
|
+
}
|
|
35
|
+
: { present: false };
|
|
36
|
+
return {
|
|
37
|
+
ok: claude.present || codex.present,
|
|
38
|
+
claude,
|
|
39
|
+
codex,
|
|
40
|
+
error,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (caught) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
claude: { present: false },
|
|
47
|
+
codex: { present: false },
|
|
48
|
+
error: error ?? errorMessage(caught),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shape of a verified account switch plus the undo record. The
|
|
3
|
+
* provider-specific switch cores live in accounts.ts (Claude) and
|
|
4
|
+
* codexProfiles.ts (Codex); accountManager.ts exposes the provider-agnostic
|
|
5
|
+
* `switchAccount*` and `undoLastSwitch` entry points.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import { getAccountsDir } from './accountRegistry';
|
|
11
|
+
import { atomicWriteJsonSync } from './writers/atomic';
|
|
12
|
+
function providerDirName(provider) {
|
|
13
|
+
return provider === 'codex' ? 'codex' : 'claude';
|
|
14
|
+
}
|
|
15
|
+
export function getLastSwitchPath(provider) {
|
|
16
|
+
return path.join(getAccountsDir(), providerDirName(provider), 'last-switch.json');
|
|
17
|
+
}
|
|
18
|
+
export function readLastSwitch(provider) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(fs.readFileSync(getLastSwitchPath(provider), 'utf8'));
|
|
21
|
+
return parsed && typeof parsed.token === 'string' && typeof parsed.to === 'string'
|
|
22
|
+
? parsed
|
|
23
|
+
: null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function writeLastSwitch(provider, from, to) {
|
|
30
|
+
const record = {
|
|
31
|
+
token: randomUUID(),
|
|
32
|
+
provider,
|
|
33
|
+
from,
|
|
34
|
+
to,
|
|
35
|
+
at: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
atomicWriteJsonSync(getLastSwitchPath(provider), record);
|
|
38
|
+
return record;
|
|
39
|
+
}
|
|
40
|
+
export function clearLastSwitch(provider) {
|
|
41
|
+
try {
|
|
42
|
+
fs.rmSync(getLastSwitchPath(provider), { force: true });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* best effort */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function switchFailure(provider, accountId, error, extra = {}) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error,
|
|
52
|
+
provider,
|
|
53
|
+
accountId,
|
|
54
|
+
previousAccountId: null,
|
|
55
|
+
verified: false,
|
|
56
|
+
verification: 'none',
|
|
57
|
+
warnings: [],
|
|
58
|
+
hints: [],
|
|
59
|
+
runningConsumers: [],
|
|
60
|
+
...extra,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Fold per-consumer warnings into a result; keeps `warning` (legacy) in sync. */
|
|
64
|
+
export function finishSwitchResult(result) {
|
|
65
|
+
const warnings = [
|
|
66
|
+
...result.warnings,
|
|
67
|
+
...result.runningConsumers.map((consumer) => consumer.reachability),
|
|
68
|
+
];
|
|
69
|
+
return {
|
|
70
|
+
...result,
|
|
71
|
+
warnings,
|
|
72
|
+
warning: warnings.length ? warnings.join(' ') : undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuous reconciliation between the live provider logins and sidekick's
|
|
3
|
+
* saved profiles. One engine replaces the old bootstrap/reconcile/backup
|
|
4
|
+
* paths: it registers logins sidekick has never seen, folds rotated tokens
|
|
5
|
+
* back into their profile, merges duplicate Codex seats, and re-points the
|
|
6
|
+
* active pointer at whatever is really logged in.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { cleanupAbandonedClaudeLogins, syncClaudeLiveStateUnlocked, withClaudeAuthSwapLock, } from './accounts';
|
|
11
|
+
import { getLiveClaudeHome, readLiveClaudeIdentity } from './claudeProfiles';
|
|
12
|
+
import { cleanupAbandonedCodexLogins, cleanupAbandonedCodexLoginsAsync, hasPendingCodexRepair, syncCodexLiveStateUnlocked, withCodexAuthSwapLock, } from './codexProfiles';
|
|
13
|
+
import { getSystemCodexHome } from './codexPaths';
|
|
14
|
+
import { inspectRunningAccountConsumers, inspectRunningAccountConsumersSync, } from './processDetection';
|
|
15
|
+
import { emptyProviderSyncReport, } from './accountSyncTypes';
|
|
16
|
+
const DEFAULT_KEYCHAIN_FOLD_INTERVAL_MS = 10 * 60 * 1000;
|
|
17
|
+
const fingerprints = {
|
|
18
|
+
'claude-code': {},
|
|
19
|
+
codex: {},
|
|
20
|
+
};
|
|
21
|
+
let syncInProgress = false;
|
|
22
|
+
function statStamp(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
const stat = fs.statSync(filePath);
|
|
25
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return 'absent';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function claudeFingerprint() {
|
|
32
|
+
const home = getLiveClaudeHome();
|
|
33
|
+
const identity = readLiveClaudeIdentity();
|
|
34
|
+
// Fingerprint the identity, not the config file: Claude Code rewrites
|
|
35
|
+
// `~/.claude.json` on nearly every prompt, which would defeat the
|
|
36
|
+
// `onlyIfChanged` short-circuit and the macOS keychain fold interval.
|
|
37
|
+
return [
|
|
38
|
+
identity?.uuid ?? 'none',
|
|
39
|
+
identity?.email ?? '',
|
|
40
|
+
statStamp(path.join(home, '.credentials.json')),
|
|
41
|
+
].join('|');
|
|
42
|
+
}
|
|
43
|
+
function codexFingerprint() {
|
|
44
|
+
const home = getSystemCodexHome();
|
|
45
|
+
return [
|
|
46
|
+
statStamp(path.join(home, 'auth.json')),
|
|
47
|
+
statStamp(path.join(home, '.credentials.json')),
|
|
48
|
+
].join('|');
|
|
49
|
+
}
|
|
50
|
+
function sourceForReason(reason) {
|
|
51
|
+
return reason === 'pre-switch' ? 'switch' : 'live-sync';
|
|
52
|
+
}
|
|
53
|
+
function shouldSync(provider, fingerprint, options) {
|
|
54
|
+
if (!options.onlyIfChanged || (provider === 'codex' && hasPendingCodexRepair()))
|
|
55
|
+
return true;
|
|
56
|
+
const state = fingerprints[provider];
|
|
57
|
+
if (state.fingerprint !== fingerprint)
|
|
58
|
+
return true;
|
|
59
|
+
if (provider === 'claude-code' && process.platform === 'darwin') {
|
|
60
|
+
// The Keychain item can change without touching any watched file.
|
|
61
|
+
return (state.lastFoldAt ?? 0) + options.keychainFoldIntervalMs <= options.now;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** Sync one provider under its swap lock; never throws. */
|
|
66
|
+
function syncProvider(provider, reason, options, scan) {
|
|
67
|
+
const fingerprint = provider === 'codex' ? codexFingerprint() : claudeFingerprint();
|
|
68
|
+
if (!shouldSync(provider, fingerprint, options)) {
|
|
69
|
+
return { ...emptyProviderSyncReport(), skipped: 'unchanged' };
|
|
70
|
+
}
|
|
71
|
+
const source = sourceForReason(reason);
|
|
72
|
+
let report;
|
|
73
|
+
try {
|
|
74
|
+
if (provider === 'codex')
|
|
75
|
+
scan ??= inspectRunningAccountConsumersSync('codex');
|
|
76
|
+
report =
|
|
77
|
+
provider === 'codex'
|
|
78
|
+
? withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }))
|
|
79
|
+
: withClaudeAuthSwapLock(() => syncClaudeLiveStateUnlocked(source));
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return { warnings: [`Could not acquire the ${provider} account-switch lock: ${err}`] };
|
|
83
|
+
}
|
|
84
|
+
fingerprints[provider] = {
|
|
85
|
+
// Re-fingerprint after the sync: folding rewrites nothing in the live home,
|
|
86
|
+
// but registering touches accounts.json, which is also watched.
|
|
87
|
+
fingerprint: provider === 'codex' ? codexFingerprint() : claudeFingerprint(),
|
|
88
|
+
lastFoldAt: options.now,
|
|
89
|
+
};
|
|
90
|
+
return report;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Reconcile the live logins with the saved profiles for both providers.
|
|
94
|
+
* Safe to call from watchers: re-entrant calls return an empty report, and
|
|
95
|
+
* `onlyIfChanged` short-circuits when the live files are unchanged.
|
|
96
|
+
*/
|
|
97
|
+
function syncLiveAccountStateCore(options = {}, scan) {
|
|
98
|
+
const reason = options.reason ?? 'manual';
|
|
99
|
+
const now = options.now ?? Date.now();
|
|
100
|
+
const resolved = {
|
|
101
|
+
onlyIfChanged: options.onlyIfChanged ?? (reason === 'watch' || reason === 'poll'),
|
|
102
|
+
keychainFoldIntervalMs: options.keychainFoldIntervalMs ?? DEFAULT_KEYCHAIN_FOLD_INTERVAL_MS,
|
|
103
|
+
now,
|
|
104
|
+
};
|
|
105
|
+
const providers = options.providers ?? ['claude-code', 'codex'];
|
|
106
|
+
const report = {
|
|
107
|
+
claude: { ...emptyProviderSyncReport(), skipped: 'not-requested' },
|
|
108
|
+
codex: { ...emptyProviderSyncReport(), skipped: 'not-requested' },
|
|
109
|
+
ranAt: now,
|
|
110
|
+
reason,
|
|
111
|
+
};
|
|
112
|
+
if (syncInProgress) {
|
|
113
|
+
report.claude.skipped = 'in-progress';
|
|
114
|
+
report.codex.skipped = 'in-progress';
|
|
115
|
+
return report;
|
|
116
|
+
}
|
|
117
|
+
syncInProgress = true;
|
|
118
|
+
try {
|
|
119
|
+
if (providers.includes('claude-code'))
|
|
120
|
+
report.claude = syncProvider('claude-code', reason, resolved);
|
|
121
|
+
if (providers.includes('codex'))
|
|
122
|
+
report.codex = syncProvider('codex', reason, resolved, scan);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
syncInProgress = false;
|
|
126
|
+
}
|
|
127
|
+
return report;
|
|
128
|
+
}
|
|
129
|
+
export function syncLiveAccountStateSync(options = {}) {
|
|
130
|
+
return syncLiveAccountStateCore(options);
|
|
131
|
+
}
|
|
132
|
+
/** Promise-returning twin of {@link syncLiveAccountStateSync} for async hosts. */
|
|
133
|
+
export async function syncLiveAccountState(options = {}) {
|
|
134
|
+
const scan = !options.providers || options.providers.includes('codex')
|
|
135
|
+
? await inspectRunningAccountConsumers('codex')
|
|
136
|
+
: undefined;
|
|
137
|
+
return syncLiveAccountStateCore(options, scan);
|
|
138
|
+
}
|
|
139
|
+
/** Forget the change fingerprints (tests, or after an external reset). */
|
|
140
|
+
export function _resetSyncFingerprints() {
|
|
141
|
+
fingerprints['claude-code'] = {};
|
|
142
|
+
fingerprints.codex = {};
|
|
143
|
+
}
|
|
144
|
+
/** Remove isolated login homes that never authenticated (both providers). */
|
|
145
|
+
export function cleanupAbandonedLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Date.now()) {
|
|
146
|
+
return {
|
|
147
|
+
claude: cleanupAbandonedClaudeLogins(olderThanMs, now),
|
|
148
|
+
codex: cleanupAbandonedCodexLogins(olderThanMs, now),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function cleanupAbandonedLoginsAsync(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
152
|
+
return {
|
|
153
|
+
claude: cleanupAbandonedClaudeLogins(olderThanMs, now),
|
|
154
|
+
codex: await cleanupAbandonedCodexLoginsAsync(olderThanMs, now),
|
|
155
|
+
};
|
|
156
|
+
}
|