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,265 @@
|
|
|
1
|
+
/** Inspection is read-only. Repairs touch only credential paths Sidekick owns. */
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { getAccountsDir, listSavedAccountProfiles } from './accountRegistry';
|
|
5
|
+
import { codexRefreshFingerprint, compareCodexAuthFreshness, identitiesMatch, readAuthIdentityFromRaw, } from './codexAuth';
|
|
6
|
+
import { getCodexLoginsDir, getCodexBackupPath, getCodexCredentialStoreMode, getCodexProfileHome, getCodexProfilesDir, getSystemCodexHome, } from './codexPaths';
|
|
7
|
+
import { canonicalCodexPath, codexSnapshotFingerprint, isCodexLogin, isLegacyCodexLogin, readCodexBackup, readCodexCredentialStore, removeCodexProfileCredentials, writeCodexBackup, } from './codexCredentialStore';
|
|
8
|
+
import { isAccountLeaseHeld } from './accountLease';
|
|
9
|
+
import { inspectRunningAccountConsumersSync } from './processDetection';
|
|
10
|
+
import { syncCodexLiveStateUnlocked, withCodexAuthSwapLock } from './codexProfiles';
|
|
11
|
+
function within(child, root) {
|
|
12
|
+
const resolved = canonicalCodexPath(child), base = canonicalCodexPath(root);
|
|
13
|
+
return resolved === base || resolved.startsWith(base + path.sep);
|
|
14
|
+
}
|
|
15
|
+
function ownedDirectories(root) {
|
|
16
|
+
try {
|
|
17
|
+
return fs
|
|
18
|
+
.readdirSync(root, { withFileTypes: true })
|
|
19
|
+
.filter((e) => e.isDirectory() && !e.isSymbolicLink())
|
|
20
|
+
.map((e) => path.join(root, e.name))
|
|
21
|
+
.filter((dir) => within(dir, root));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readFinding(f) {
|
|
28
|
+
if (!f.file)
|
|
29
|
+
return readCodexCredentialStore(f.configDir);
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(f.file, 'utf8');
|
|
32
|
+
const legacy = path.basename(f.file).startsWith('credentials-');
|
|
33
|
+
const snapshot = { authRaw: legacy ? null : raw, legacyRaw: legacy ? raw : null };
|
|
34
|
+
return {
|
|
35
|
+
status: (legacy ? isLegacyCodexLogin(raw) : isCodexLogin(raw)) ? 'ok' : 'corrupt',
|
|
36
|
+
snapshot,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return error.code === 'ENOENT'
|
|
41
|
+
? { status: 'absent', snapshot: { authRaw: null, legacyRaw: null } }
|
|
42
|
+
: { status: 'error' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function inspectCodexCredentialStores(options = {}) {
|
|
46
|
+
const now = options.now ?? Date.now();
|
|
47
|
+
const findings = [];
|
|
48
|
+
const live = readCodexCredentialStore();
|
|
49
|
+
if (live.status === 'corrupt' || live.status === 'error')
|
|
50
|
+
findings.push({
|
|
51
|
+
kind: live.status === 'error' ? 'unreadable' : 'corrupt',
|
|
52
|
+
configDir: getSystemCodexHome(),
|
|
53
|
+
ownership: 'live',
|
|
54
|
+
detail: 'The live Codex login could not be read; it may be mid-write.',
|
|
55
|
+
action: 'Close Codex and inspect again; live data is never deleted by doctor.',
|
|
56
|
+
});
|
|
57
|
+
const profiles = listSavedAccountProfiles('codex');
|
|
58
|
+
for (const profile of profiles) {
|
|
59
|
+
const backup = readCodexBackup(profile.id);
|
|
60
|
+
if (backup.status === 'error' || backup.status === 'corrupt')
|
|
61
|
+
findings.push({
|
|
62
|
+
kind: backup.status === 'error' ? 'unreadable' : 'corrupt',
|
|
63
|
+
ownership: 'backup',
|
|
64
|
+
accountId: profile.id,
|
|
65
|
+
email: profile.email,
|
|
66
|
+
configDir: path.dirname(getCodexBackupPath(profile.id)),
|
|
67
|
+
file: getCodexBackupPath(profile.id),
|
|
68
|
+
detail: 'The private Codex backup could not be read safely.',
|
|
69
|
+
action: 'Inspect the backup or sign in again; unreadable credentials are preserved.',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const candidates = [];
|
|
73
|
+
for (const root of [getCodexProfilesDir(), getCodexLoginsDir()]) {
|
|
74
|
+
for (const dir of ownedDirectories(root)) {
|
|
75
|
+
const pending = path.join(dir, 'profile.json');
|
|
76
|
+
if (fs.existsSync(pending)) {
|
|
77
|
+
try {
|
|
78
|
+
const at = Date.parse(JSON.parse(fs.readFileSync(pending, 'utf8')).addedAt);
|
|
79
|
+
if (!Number.isFinite(at) || now - at < 24 * 60 * 60 * 1000)
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const home = path.join(dir, 'codex-home');
|
|
87
|
+
if (within(home, root) &&
|
|
88
|
+
canonicalCodexPath(home) !== canonicalCodexPath(getSystemCodexHome())) {
|
|
89
|
+
candidates.push({
|
|
90
|
+
configDir: home,
|
|
91
|
+
ownership: root === getCodexProfilesDir() ? 'profile-home' : 'login-home',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const stash = path.join(getAccountsDir(), 'codex', 'stash');
|
|
97
|
+
for (const dir of ownedDirectories(stash)) {
|
|
98
|
+
const home = path.join(dir, 'codex-home');
|
|
99
|
+
if (within(home, stash))
|
|
100
|
+
candidates.push({ configDir: home, ownership: 'stash' });
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
for (const entry of fs.readdirSync(stash, { withFileTypes: true })) {
|
|
104
|
+
if (entry.isFile() && /^(auth|credentials)-.*\.json$/.test(entry.name))
|
|
105
|
+
candidates.push({
|
|
106
|
+
configDir: stash,
|
|
107
|
+
file: path.join(stash, entry.name),
|
|
108
|
+
ownership: 'stash',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* no old stash */
|
|
114
|
+
}
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
const store = readFinding(candidate);
|
|
117
|
+
if (store.status === 'absent')
|
|
118
|
+
continue;
|
|
119
|
+
const identity = store.status === 'ok' ? readAuthIdentityFromRaw(store.snapshot.authRaw) : null;
|
|
120
|
+
const profile = profiles.find((p) => identitiesMatch(identity, {
|
|
121
|
+
email: p.email ?? p.metadata?.email,
|
|
122
|
+
workspaceId: p.metadata?.workspaceId,
|
|
123
|
+
})) ??
|
|
124
|
+
profiles.find((p) => canonicalCodexPath(getCodexProfileHome(p.id)) === canonicalCodexPath(candidate.configDir));
|
|
125
|
+
const base = { ...candidate, accountId: profile?.id, email: identity?.email ?? profile?.email };
|
|
126
|
+
if (store.status === 'error' || store.status === 'corrupt') {
|
|
127
|
+
findings.push({
|
|
128
|
+
...base,
|
|
129
|
+
kind: store.status === 'error' ? 'unreadable' : 'corrupt',
|
|
130
|
+
detail: 'This credential store could not be read safely.',
|
|
131
|
+
action: 'Inspect or sign in again; unreadable credentials are preserved.',
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const fingerprint = codexSnapshotFingerprint(store.snapshot);
|
|
136
|
+
const liveDuplicate = live.status === 'ok' &&
|
|
137
|
+
(identitiesMatch(identity, readAuthIdentityFromRaw(live.snapshot.authRaw)) ||
|
|
138
|
+
fingerprint === codexSnapshotFingerprint(live.snapshot));
|
|
139
|
+
if (candidate.ownership === 'stash')
|
|
140
|
+
findings.push({
|
|
141
|
+
...base,
|
|
142
|
+
fingerprint,
|
|
143
|
+
kind: 'stale-stash',
|
|
144
|
+
detail: 'An old stash holds another account credential.',
|
|
145
|
+
action: 'Preserve the freshest known login privately, then remove this credential.',
|
|
146
|
+
});
|
|
147
|
+
else if (liveDuplicate)
|
|
148
|
+
findings.push({
|
|
149
|
+
...base,
|
|
150
|
+
fingerprint,
|
|
151
|
+
kind: 'live-duplicate',
|
|
152
|
+
detail: 'This home holds a second usable copy of the live Codex login.',
|
|
153
|
+
action: 'Repair a newer login first, then remove the duplicate when Codex is stopped.',
|
|
154
|
+
});
|
|
155
|
+
else if (!profile || candidate.ownership === 'login-home')
|
|
156
|
+
findings.push({
|
|
157
|
+
...base,
|
|
158
|
+
fingerprint,
|
|
159
|
+
kind: 'orphan',
|
|
160
|
+
detail: 'An abandoned home holds a login outside a saved profile.',
|
|
161
|
+
action: 'Preserve it under its known account before removing it; unknown identities need manual review.',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (fs.existsSync(path.join(getAccountsDir(), 'codex', 'switch-journal.json')))
|
|
165
|
+
findings.push({
|
|
166
|
+
kind: 'pending-switch',
|
|
167
|
+
ownership: 'journal',
|
|
168
|
+
configDir: path.join(getAccountsDir(), 'codex'),
|
|
169
|
+
detail: 'An interrupted account switch needs reconciliation.',
|
|
170
|
+
action: 'Close Codex and finish recovery from the live fingerprint.',
|
|
171
|
+
});
|
|
172
|
+
return { findings, homesChecked: candidates.length + 1 };
|
|
173
|
+
}
|
|
174
|
+
export function repairCodexCredentialStores(options = {}) {
|
|
175
|
+
const inspection = inspectCodexCredentialStores(options);
|
|
176
|
+
const result = {
|
|
177
|
+
...inspection,
|
|
178
|
+
dryRun: options.dryRun ?? true,
|
|
179
|
+
repaired: [],
|
|
180
|
+
};
|
|
181
|
+
if (result.dryRun)
|
|
182
|
+
return result;
|
|
183
|
+
const scan = inspectRunningAccountConsumersSync('codex');
|
|
184
|
+
const mode = getCodexCredentialStoreMode();
|
|
185
|
+
if (!scan.ok || scan.consumers.length || (mode !== 'file' && mode !== 'unknown')) {
|
|
186
|
+
result.repaired = inspection.findings.map((finding) => ({
|
|
187
|
+
finding,
|
|
188
|
+
ok: false,
|
|
189
|
+
error: 'Repair requires stopped Codex consumers, a successful process scan, and readable file-mode configuration.',
|
|
190
|
+
}));
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
withCodexAuthSwapLock(() => {
|
|
194
|
+
result.sync = syncCodexLiveStateUnlocked('migration', { scan });
|
|
195
|
+
for (const finding of inspection.findings) {
|
|
196
|
+
let error;
|
|
197
|
+
try {
|
|
198
|
+
if (finding.kind === 'pending-switch') {
|
|
199
|
+
if (fs.existsSync(path.join(finding.configDir, 'switch-journal.json')))
|
|
200
|
+
throw new Error();
|
|
201
|
+
}
|
|
202
|
+
else if (finding.kind === 'live-duplicate' && finding.ownership === 'profile-home') {
|
|
203
|
+
if (readFinding(finding).status !== 'absent')
|
|
204
|
+
throw new Error();
|
|
205
|
+
}
|
|
206
|
+
else if (finding.kind === 'stale-stash' ||
|
|
207
|
+
finding.kind === 'orphan' ||
|
|
208
|
+
finding.kind === 'live-duplicate') {
|
|
209
|
+
if (!within(finding.file ?? finding.configDir, path.join(getAccountsDir(), 'codex')) ||
|
|
210
|
+
canonicalCodexPath(finding.configDir) === canonicalCodexPath(getSystemCodexHome()))
|
|
211
|
+
throw new Error();
|
|
212
|
+
const current = readFinding(finding);
|
|
213
|
+
if (current.status === 'absent') {
|
|
214
|
+
result.repaired.push({ finding, ok: true });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (current.status !== 'ok' ||
|
|
218
|
+
codexSnapshotFingerprint(current.snapshot) !== finding.fingerprint ||
|
|
219
|
+
!finding.accountId ||
|
|
220
|
+
isAccountLeaseHeld('codex', finding.accountId))
|
|
221
|
+
throw new Error();
|
|
222
|
+
const backup = readCodexBackup(finding.accountId);
|
|
223
|
+
if (backup.status === 'ok' &&
|
|
224
|
+
compareCodexAuthFreshness(backup.snapshot.authRaw, current.snapshot.authRaw) === 0 &&
|
|
225
|
+
codexSnapshotFingerprint(backup.snapshot) !==
|
|
226
|
+
codexSnapshotFingerprint(current.snapshot) &&
|
|
227
|
+
(!codexRefreshFingerprint(backup.snapshot.authRaw) ||
|
|
228
|
+
codexRefreshFingerprint(backup.snapshot.authRaw) !==
|
|
229
|
+
codexRefreshFingerprint(current.snapshot.authRaw))) {
|
|
230
|
+
throw new Error(); // Two incomparable chains: deletion would guess which is usable.
|
|
231
|
+
}
|
|
232
|
+
const chosen = backup.status === 'ok' &&
|
|
233
|
+
compareCodexAuthFreshness(backup.snapshot.authRaw, current.snapshot.authRaw) >= 0
|
|
234
|
+
? backup.snapshot
|
|
235
|
+
: current.snapshot;
|
|
236
|
+
writeCodexBackup(finding.accountId, chosen);
|
|
237
|
+
const live = readCodexCredentialStore();
|
|
238
|
+
if (live.status === 'ok' &&
|
|
239
|
+
identitiesMatch(readAuthIdentityFromRaw(live.snapshot.authRaw), readAuthIdentityFromRaw(chosen.authRaw)) &&
|
|
240
|
+
compareCodexAuthFreshness(chosen.authRaw, live.snapshot.authRaw) > 0) {
|
|
241
|
+
result.sync = syncCodexLiveStateUnlocked('migration', { scan });
|
|
242
|
+
const repairedLive = readCodexCredentialStore();
|
|
243
|
+
if (repairedLive.status !== 'ok' ||
|
|
244
|
+
compareCodexAuthFreshness(repairedLive.snapshot.authRaw, chosen.authRaw) < 0)
|
|
245
|
+
throw new Error();
|
|
246
|
+
}
|
|
247
|
+
if (finding.file)
|
|
248
|
+
fs.rmSync(finding.file);
|
|
249
|
+
else
|
|
250
|
+
removeCodexProfileCredentials(finding.configDir, current.snapshot);
|
|
251
|
+
if (finding.ownership === 'login-home')
|
|
252
|
+
fs.rmSync(path.dirname(finding.configDir), { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
throw new Error();
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
error =
|
|
259
|
+
'Preserved for review: unreadable, ambiguous, changed, leased, or not safely repairable yet.';
|
|
260
|
+
}
|
|
261
|
+
result.repaired.push({ finding, ok: !error, error });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** Raw credential I/O. Backups are never Codex homes and never launchable. */
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { getAccountsDir } from './accountRegistry';
|
|
6
|
+
import { getCodexBackupPath, getSystemCodexHome } from './codexPaths';
|
|
7
|
+
import { parseAuthJson } from './codexAuth';
|
|
8
|
+
import { atomicWriteFileSync } from './writers/atomic';
|
|
9
|
+
export function isLegacyCodexLogin(raw) {
|
|
10
|
+
const value = parseAuthJson(raw);
|
|
11
|
+
return Boolean(value &&
|
|
12
|
+
['OPENAI_API_KEY', 'CODEX_API_KEY'].some((key) => typeof value[key] === 'string' && value[key].length > 0));
|
|
13
|
+
}
|
|
14
|
+
export function isCodexLogin(raw) {
|
|
15
|
+
const parsed = parseAuthJson(raw);
|
|
16
|
+
if (!parsed || Array.isArray(parsed))
|
|
17
|
+
return false;
|
|
18
|
+
if (typeof parsed.OPENAI_API_KEY === 'string' && parsed.OPENAI_API_KEY)
|
|
19
|
+
return true;
|
|
20
|
+
return Boolean(typeof parsed.tokens?.access_token === 'string' &&
|
|
21
|
+
parsed.tokens.access_token &&
|
|
22
|
+
typeof parsed.tokens.refresh_token === 'string' &&
|
|
23
|
+
parsed.tokens.refresh_token);
|
|
24
|
+
}
|
|
25
|
+
export function codexLoginOnly(snapshot) {
|
|
26
|
+
return {
|
|
27
|
+
authRaw: snapshot.authRaw,
|
|
28
|
+
legacyRaw: isLegacyCodexLogin(snapshot.legacyRaw) ? snapshot.legacyRaw : null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function readRaw(file) {
|
|
32
|
+
try {
|
|
33
|
+
return fs.readFileSync(file, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return null;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function readFiles(auth, legacy) {
|
|
42
|
+
try {
|
|
43
|
+
const snapshot = { authRaw: readRaw(auth), legacyRaw: readRaw(legacy) };
|
|
44
|
+
const status = snapshot.authRaw !== null
|
|
45
|
+
? isCodexLogin(snapshot.authRaw)
|
|
46
|
+
? 'ok'
|
|
47
|
+
: 'corrupt'
|
|
48
|
+
: isLegacyCodexLogin(snapshot.legacyRaw)
|
|
49
|
+
? 'ok'
|
|
50
|
+
: 'absent';
|
|
51
|
+
return { status, snapshot };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { status: 'error' };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function readCodexCredentialStore(home = getSystemCodexHome()) {
|
|
58
|
+
return readFiles(path.join(home, 'auth.json'), path.join(home, '.credentials.json'));
|
|
59
|
+
}
|
|
60
|
+
export function readCodexBackup(id) {
|
|
61
|
+
return readFiles(getCodexBackupPath(id), getCodexBackupPath(id, true));
|
|
62
|
+
}
|
|
63
|
+
export function codexSnapshotFingerprint(snapshot) {
|
|
64
|
+
return createHash('sha256').update(JSON.stringify(snapshot)).digest('hex');
|
|
65
|
+
}
|
|
66
|
+
/** The whole snapshot, including absence, must agree before destructive operations. */
|
|
67
|
+
export function codexStoreMatches(read, snapshot) {
|
|
68
|
+
return (read.status !== 'error' &&
|
|
69
|
+
codexSnapshotFingerprint(read.snapshot) === codexSnapshotFingerprint(snapshot));
|
|
70
|
+
}
|
|
71
|
+
export function writeCodexBackup(id, snapshot) {
|
|
72
|
+
const login = codexLoginOnly(snapshot);
|
|
73
|
+
if (!isCodexLogin(login.authRaw) && !isLegacyCodexLogin(login.legacyRaw)) {
|
|
74
|
+
throw new Error('Refusing to back up an invalid Codex login.');
|
|
75
|
+
}
|
|
76
|
+
for (const [file, raw] of [
|
|
77
|
+
[getCodexBackupPath(id), login.authRaw],
|
|
78
|
+
[getCodexBackupPath(id, true), login.legacyRaw],
|
|
79
|
+
]) {
|
|
80
|
+
if (raw !== null)
|
|
81
|
+
atomicWriteFileSync(file, raw);
|
|
82
|
+
else
|
|
83
|
+
fs.rmSync(file, { force: true });
|
|
84
|
+
}
|
|
85
|
+
if (!codexStoreMatches(readCodexBackup(id), login))
|
|
86
|
+
throw new Error('The private Codex backup did not verify.');
|
|
87
|
+
}
|
|
88
|
+
/** Resolve existing parents too, so nonexistent children of a symlink cannot bypass ownership. */
|
|
89
|
+
export function canonicalCodexPath(value) {
|
|
90
|
+
const resolved = path.resolve(value);
|
|
91
|
+
try {
|
|
92
|
+
return fs.realpathSync(resolved);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
const parent = path.dirname(resolved);
|
|
96
|
+
return parent === resolved
|
|
97
|
+
? resolved
|
|
98
|
+
: path.join(canonicalCodexPath(parent), path.basename(resolved));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function isSidekickCodexHome(home) {
|
|
102
|
+
const root = canonicalCodexPath(path.join(getAccountsDir(), 'codex'));
|
|
103
|
+
const resolved = canonicalCodexPath(home);
|
|
104
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
105
|
+
}
|
|
106
|
+
/** Install only account data; an unrelated MCP credential file is never copied or deleted. */
|
|
107
|
+
export function codexInstalledSnapshot(before, candidate) {
|
|
108
|
+
const login = codexLoginOnly(candidate);
|
|
109
|
+
if (login.legacyRaw && before.legacyRaw && !isLegacyCodexLogin(before.legacyRaw)) {
|
|
110
|
+
throw new Error('Legacy Codex credentials would replace MCP data; sign in with Codex again.');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
authRaw: login.authRaw,
|
|
114
|
+
legacyRaw: before.legacyRaw && !isLegacyCodexLogin(before.legacyRaw)
|
|
115
|
+
? before.legacyRaw
|
|
116
|
+
: login.legacyRaw,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function writeFiles(home, snapshot) {
|
|
120
|
+
for (const [name, raw] of [
|
|
121
|
+
['auth.json', snapshot.authRaw],
|
|
122
|
+
['.credentials.json', snapshot.legacyRaw],
|
|
123
|
+
]) {
|
|
124
|
+
const file = path.join(home, name);
|
|
125
|
+
// Do not rewrite even identical MCP data; its own refresh writer owns it.
|
|
126
|
+
if (readRaw(file) === raw)
|
|
127
|
+
continue;
|
|
128
|
+
if (raw === null)
|
|
129
|
+
fs.rmSync(file, { force: true });
|
|
130
|
+
else
|
|
131
|
+
atomicWriteFileSync(file, raw);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function writeInactiveCodexCredentialStore(home, candidate) {
|
|
135
|
+
if (canonicalCodexPath(home) === canonicalCodexPath(getSystemCodexHome())) {
|
|
136
|
+
throw new Error('A profile credential writer cannot write the live Codex home.');
|
|
137
|
+
}
|
|
138
|
+
const before = readCodexCredentialStore(home);
|
|
139
|
+
if (before.status === 'error')
|
|
140
|
+
throw new Error('The Codex profile could not be read.');
|
|
141
|
+
const expected = codexInstalledSnapshot(before.snapshot, candidate);
|
|
142
|
+
writeFiles(home, expected);
|
|
143
|
+
if (!codexStoreMatches(readCodexCredentialStore(home), expected))
|
|
144
|
+
throw new Error('The Codex profile write did not verify.');
|
|
145
|
+
}
|
|
146
|
+
/** Internal capability: only the guarded installer in codexProfiles may call this. */
|
|
147
|
+
export function writeLiveCodexCredentialStoreUnguarded(snapshot) {
|
|
148
|
+
const home = getSystemCodexHome();
|
|
149
|
+
if (isSidekickCodexHome(home))
|
|
150
|
+
throw new Error('Unset CODEX_HOME before changing the live login; it names a Sidekick-owned home.');
|
|
151
|
+
writeFiles(home, snapshot);
|
|
152
|
+
}
|
|
153
|
+
export function removeCodexProfileCredentials(home, expected) {
|
|
154
|
+
if (canonicalCodexPath(home) === canonicalCodexPath(getSystemCodexHome()))
|
|
155
|
+
throw new Error('Refusing to remove live Codex credentials.');
|
|
156
|
+
if (!codexStoreMatches(readCodexCredentialStore(home), expected))
|
|
157
|
+
throw new Error('The Codex profile changed; its credential was kept.');
|
|
158
|
+
if (expected.authRaw !== null)
|
|
159
|
+
fs.rmSync(path.join(home, 'auth.json'), { force: true });
|
|
160
|
+
if (isLegacyCodexLogin(expected.legacyRaw))
|
|
161
|
+
fs.rmSync(path.join(home, '.credentials.json'), { force: true });
|
|
162
|
+
if (readCodexCredentialStore(home).status !== 'absent')
|
|
163
|
+
throw new Error('The Codex profile credential could not be removed.');
|
|
164
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** A bounded, inference-free official CLI refresh. Never expose child output. */
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
export const runCodexKeepAlive = (command, args, env, timeoutMs) => new Promise((resolve) => {
|
|
4
|
+
let child;
|
|
5
|
+
try {
|
|
6
|
+
child = spawn(command, args, {
|
|
7
|
+
env,
|
|
8
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
cwd: env.CODEX_HOME,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
resolve({ status: null, stdout: '' });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let buffer = '';
|
|
18
|
+
let bytes = 0;
|
|
19
|
+
let initialized = false;
|
|
20
|
+
let result = null;
|
|
21
|
+
let finishing = false;
|
|
22
|
+
let settled = false;
|
|
23
|
+
let killTimer;
|
|
24
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
25
|
+
function finish(status) {
|
|
26
|
+
if (finishing)
|
|
27
|
+
return;
|
|
28
|
+
finishing = true;
|
|
29
|
+
result = status;
|
|
30
|
+
child.stdin?.end();
|
|
31
|
+
// The lease stays held until the child exits, including timeout/error paths.
|
|
32
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), Math.min(1000, timeoutMs));
|
|
33
|
+
if (status !== 0)
|
|
34
|
+
child.kill('SIGKILL');
|
|
35
|
+
}
|
|
36
|
+
function settle() {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (killTimer)
|
|
42
|
+
clearTimeout(killTimer);
|
|
43
|
+
child.stdin?.destroy();
|
|
44
|
+
child.stdout?.destroy();
|
|
45
|
+
child.stderr?.destroy();
|
|
46
|
+
resolve({ status: result, stdout: '' });
|
|
47
|
+
}
|
|
48
|
+
const send = (message) => {
|
|
49
|
+
if (!child.stdin?.writable) {
|
|
50
|
+
finish(null);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
child.stdin.write(JSON.stringify(message) + '\n');
|
|
54
|
+
};
|
|
55
|
+
child.once('error', settle);
|
|
56
|
+
child.once('close', settle);
|
|
57
|
+
child.once('exit', settle);
|
|
58
|
+
child.stdin?.on('error', () => finish(null));
|
|
59
|
+
child.stderr?.on('data', () => {
|
|
60
|
+
/* Drain, never log possibly sensitive diagnostics. */
|
|
61
|
+
});
|
|
62
|
+
child.stdout?.setEncoding('utf8');
|
|
63
|
+
child.stdout?.on('data', (chunk) => {
|
|
64
|
+
if (finishing)
|
|
65
|
+
return;
|
|
66
|
+
bytes += Buffer.byteLength(chunk);
|
|
67
|
+
if (bytes > 1024 * 1024) {
|
|
68
|
+
finish(null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
buffer += chunk;
|
|
72
|
+
let newline;
|
|
73
|
+
while (!finishing && (newline = buffer.indexOf('\n')) >= 0) {
|
|
74
|
+
const line = buffer.slice(0, newline);
|
|
75
|
+
buffer = buffer.slice(newline + 1);
|
|
76
|
+
if (!line.trim())
|
|
77
|
+
continue;
|
|
78
|
+
let message;
|
|
79
|
+
try {
|
|
80
|
+
message = JSON.parse(line);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
finish(null);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!message || typeof message !== 'object') {
|
|
87
|
+
finish(null);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (message.id === 0 && !initialized) {
|
|
91
|
+
if (message.error || !message.result) {
|
|
92
|
+
finish(1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
initialized = true;
|
|
96
|
+
send({ method: 'initialized', params: {} });
|
|
97
|
+
send({ method: 'account/read', id: 1, params: { refreshToken: true } });
|
|
98
|
+
}
|
|
99
|
+
else if (message.id === 1 && initialized) {
|
|
100
|
+
finish(!message.error && message.result?.account?.type === 'chatgpt' ? 0 : 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
send({
|
|
105
|
+
method: 'initialize',
|
|
106
|
+
id: 0,
|
|
107
|
+
params: {
|
|
108
|
+
clientInfo: { name: 'sidekick_agent_hub', title: 'Sidekick Agent Hub', version: '1' },
|
|
109
|
+
capabilities: null,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
});
|