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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-aware credential I/O for Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* On macOS, Claude Code stores OAuth credentials in the system Keychain
|
|
5
|
+
* (service "Claude Code-credentials", suffixed per config directory). On
|
|
6
|
+
* Linux/WSL/Windows, credentials live in `<home>/.credentials.json`. When the
|
|
7
|
+
* Keychain is unavailable (locked, SSH) Claude Code falls back to the file on
|
|
8
|
+
* macOS too, so reads consult the file after a Keychain miss.
|
|
9
|
+
*
|
|
10
|
+
* Keychain writes mirror Claude Code 2.1.x exactly: the payload is hex-encoded
|
|
11
|
+
* (`-X`) and sent through `security -i` only while the command line fits the
|
|
12
|
+
* tool's line buffer; longer payloads go through argv. `security -i` silently
|
|
13
|
+
* truncates longer lines, which once stored half a credential (the `mcpOAuth`
|
|
14
|
+
* tokens make blobs of 4 KB and more) — a login Claude Code then reports as
|
|
15
|
+
* "Not logged in". Every write is read back and compared before it counts.
|
|
16
|
+
*
|
|
17
|
+
* Both accounts.ts and credentials.ts consume this module so that all
|
|
18
|
+
* credential access is platform-correct in one place.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { execFileSync } from 'child_process';
|
|
23
|
+
import { claudeKeychainAccountName, claudeKeychainService, getLiveClaudeHome, } from './claudeProfiles';
|
|
24
|
+
import { forgetClaudeCredentialHome, recordClaudeCredentialHome } from './claudeCredentialLedger';
|
|
25
|
+
import { atomicWriteFileSync } from './writers/atomic';
|
|
26
|
+
export const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
27
|
+
const SECURITY_TIMEOUT_MS = 4000;
|
|
28
|
+
/** `security` exits with this status when no item matches. */
|
|
29
|
+
const SECURITY_ITEM_NOT_FOUND = 44;
|
|
30
|
+
/**
|
|
31
|
+
* Claude Code's ceiling for a `security -i` command line (the tool reads at
|
|
32
|
+
* most 4095 bytes per line). Longer commands go through argv instead.
|
|
33
|
+
*/
|
|
34
|
+
export const SECURITY_INTERACTIVE_LINE_LIMIT = 4032;
|
|
35
|
+
/**
|
|
36
|
+
* Written as the item comment (`-j`) on every Keychain item Sidekick creates,
|
|
37
|
+
* so ownership is provable from the item itself even after its directory and
|
|
38
|
+
* the ledger that described it are gone.
|
|
39
|
+
*/
|
|
40
|
+
export const SIDEKICK_KEYCHAIN_COMMENT_PREFIX = 'sidekick-agent-hub:';
|
|
41
|
+
/** A credential write that did not land, or could not be verified. */
|
|
42
|
+
export class ClaudeCredentialWriteError extends Error {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'ClaudeCredentialWriteError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function quoteSecurityInteractiveArg(value) {
|
|
49
|
+
// `security -i` is not a POSIX shell: adjacent quoted segments do NOT
|
|
50
|
+
// concatenate (so the shell-style '\'' splice splits the token), and
|
|
51
|
+
// backslash escapes the next character even inside single quotes.
|
|
52
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, `\\'`)}'`;
|
|
53
|
+
}
|
|
54
|
+
function getCredentialsFilePath(configDir) {
|
|
55
|
+
return path.join(configDir ?? getLiveClaudeHome(), '.credentials.json');
|
|
56
|
+
}
|
|
57
|
+
function parseStoredText(raw, source) {
|
|
58
|
+
const text = raw.trim();
|
|
59
|
+
if (!text)
|
|
60
|
+
return { status: 'corrupt', source, length: 0 };
|
|
61
|
+
// `security -w` prints hex when the stored bytes are not plain text.
|
|
62
|
+
const decoded = source === 'keychain' && !text.startsWith('{') && /^(?:[0-9a-f]{2})+$/i.test(text)
|
|
63
|
+
? Buffer.from(text, 'hex').toString('utf8')
|
|
64
|
+
: text;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(decoded);
|
|
67
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
68
|
+
return { status: 'ok', blob: parsed, source };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* fall through */
|
|
73
|
+
}
|
|
74
|
+
return { status: 'corrupt', source, length: decoded.length };
|
|
75
|
+
}
|
|
76
|
+
function readCredentialsFileStore(configDir) {
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = fs.readFileSync(getCredentialsFilePath(configDir), 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return error.code === 'ENOENT'
|
|
83
|
+
? { status: 'absent' }
|
|
84
|
+
: { status: 'error', reason: String(error.message ?? error) };
|
|
85
|
+
}
|
|
86
|
+
return parseStoredText(raw, 'file');
|
|
87
|
+
}
|
|
88
|
+
function securityErrorStatus(error) {
|
|
89
|
+
const status = error.status;
|
|
90
|
+
return typeof status === 'number' ? status : null;
|
|
91
|
+
}
|
|
92
|
+
function readKeychainStore(service) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = execFileSync('security', ['find-generic-password', '-a', claudeKeychainAccountName(), '-s', service, '-w'], {
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
timeout: SECURITY_TIMEOUT_MS,
|
|
98
|
+
killSignal: 'SIGKILL',
|
|
99
|
+
});
|
|
100
|
+
return parseStoredText(String(raw), 'keychain');
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND)
|
|
104
|
+
return { status: 'absent' };
|
|
105
|
+
return { status: 'error', reason: String(error?.message ?? error) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Read one Claude credential store with a definite outcome. On macOS the
|
|
110
|
+
* Keychain item is authoritative; the file is consulted only when the item is
|
|
111
|
+
* absent (Claude Code's own fallback), unless `keychainOnly` is set.
|
|
112
|
+
*/
|
|
113
|
+
export function readClaudeCredentialStore(configDir, options = {}) {
|
|
114
|
+
if (process.platform === 'darwin') {
|
|
115
|
+
const keychain = readKeychainStore(claudeKeychainService(configDir));
|
|
116
|
+
if (keychain.status !== 'absent' || options.keychainOnly)
|
|
117
|
+
return keychain;
|
|
118
|
+
}
|
|
119
|
+
return readCredentialsFileStore(configDir);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read the active Claude Code credentials as a parsed object.
|
|
123
|
+
*
|
|
124
|
+
* - macOS: reads from system Keychain via the `security` CLI, then the file
|
|
125
|
+
* - Linux / WSL / Windows: reads `<home>/.credentials.json`
|
|
126
|
+
*
|
|
127
|
+
* Returns `null` when credentials are absent or unreadable. Callers that must
|
|
128
|
+
* tell "absent" from "could not read" use {@link readClaudeCredentialStore}.
|
|
129
|
+
*/
|
|
130
|
+
export function readActiveCredentials(configDir) {
|
|
131
|
+
if (process.platform === 'darwin') {
|
|
132
|
+
const keychain = readKeychainStore(claudeKeychainService(configDir));
|
|
133
|
+
if (keychain.status === 'ok')
|
|
134
|
+
return keychain.blob;
|
|
135
|
+
}
|
|
136
|
+
const file = readCredentialsFileStore(configDir);
|
|
137
|
+
return file.status === 'ok' ? file.blob : null;
|
|
138
|
+
}
|
|
139
|
+
function runKeychainWrite(service, hex, comment) {
|
|
140
|
+
const account = claudeKeychainAccountName();
|
|
141
|
+
const args = ['add-generic-password', '-U', '-a', account, '-s', service];
|
|
142
|
+
const interactive = [
|
|
143
|
+
'add-generic-password',
|
|
144
|
+
'-U',
|
|
145
|
+
'-a',
|
|
146
|
+
quoteSecurityInteractiveArg(account),
|
|
147
|
+
'-s',
|
|
148
|
+
quoteSecurityInteractiveArg(service),
|
|
149
|
+
];
|
|
150
|
+
if (comment) {
|
|
151
|
+
args.push('-j', comment);
|
|
152
|
+
interactive.push('-j', quoteSecurityInteractiveArg(comment));
|
|
153
|
+
}
|
|
154
|
+
args.push('-X', hex);
|
|
155
|
+
interactive.push('-X', quoteSecurityInteractiveArg(hex));
|
|
156
|
+
const line = `${interactive.join(' ')}\n`;
|
|
157
|
+
if (Buffer.byteLength(line, 'utf8') <= SECURITY_INTERACTIVE_LINE_LIMIT) {
|
|
158
|
+
execFileSync('security', ['-i'], {
|
|
159
|
+
input: line,
|
|
160
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
161
|
+
timeout: SECURITY_TIMEOUT_MS,
|
|
162
|
+
killSignal: 'SIGKILL',
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// The same trade-off Claude Code makes for large payloads: the hex is on the
|
|
167
|
+
// command line (visible to `ps`) for the duration of this call.
|
|
168
|
+
execFileSync('security', args, {
|
|
169
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
170
|
+
timeout: SECURITY_TIMEOUT_MS,
|
|
171
|
+
killSignal: 'SIGKILL',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Write a credential blob to a store and verify it by reading it back.
|
|
176
|
+
* `configDir` undefined targets the live home; only the guarded live installer
|
|
177
|
+
* in accounts.ts may do that (see {@link writeLiveClaudeCredentialStoreUnguarded}).
|
|
178
|
+
*/
|
|
179
|
+
function writeClaudeCredentialStore(credentials, configDir) {
|
|
180
|
+
const json = JSON.stringify(credentials);
|
|
181
|
+
let expected;
|
|
182
|
+
try {
|
|
183
|
+
expected = JSON.stringify(JSON.parse(json));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw new ClaudeCredentialWriteError('Refusing to store a credential that is not JSON.');
|
|
187
|
+
}
|
|
188
|
+
if (process.platform === 'darwin') {
|
|
189
|
+
const service = claudeKeychainService(configDir);
|
|
190
|
+
const comment = configDir
|
|
191
|
+
? `${SIDEKICK_KEYCHAIN_COMMENT_PREFIX}${path.resolve(configDir)}`
|
|
192
|
+
: null;
|
|
193
|
+
try {
|
|
194
|
+
runKeychainWrite(service, Buffer.from(json, 'utf8').toString('hex'), comment);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
throw new ClaudeCredentialWriteError(`Keychain write failed: ${error?.message ?? error}`);
|
|
198
|
+
}
|
|
199
|
+
const readBack = readKeychainStore(service);
|
|
200
|
+
if (readBack.status === 'error') {
|
|
201
|
+
throw new ClaudeCredentialWriteError('The Keychain write could not be verified (read-back failed); treating it as not written.');
|
|
202
|
+
}
|
|
203
|
+
if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
|
|
204
|
+
throw new ClaudeCredentialWriteError('The Keychain item did not hold the credential after writing it.');
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Linux / WSL / Windows — file-based (tmp + fsync + rename, retried on Windows)
|
|
209
|
+
atomicWriteFileSync(getCredentialsFilePath(configDir), json, 0o600);
|
|
210
|
+
const readBack = readCredentialsFileStore(configDir);
|
|
211
|
+
if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
|
|
212
|
+
throw new ClaudeCredentialWriteError('The credentials file did not hold the credential after writing it.');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Write credentials to a profile home's Claude Code credential store.
|
|
217
|
+
*
|
|
218
|
+
* - macOS: the suffixed Keychain item for `configDir`
|
|
219
|
+
* - Linux / WSL / Windows: atomic-writes `<configDir>/.credentials.json`
|
|
220
|
+
*
|
|
221
|
+
* `configDir` is required: the live store is written only through the guarded
|
|
222
|
+
* installer in accounts.ts, which refuses downgrades and captures the outgoing
|
|
223
|
+
* login first. Throws {@link ClaudeCredentialWriteError} when the write did
|
|
224
|
+
* not land intact.
|
|
225
|
+
*/
|
|
226
|
+
export function writeActiveCredentials(credentials, configDir) {
|
|
227
|
+
if (!configDir) {
|
|
228
|
+
throw new ClaudeCredentialWriteError('writeActiveCredentials() needs a config directory; the live Claude login is only written through a guarded account switch.');
|
|
229
|
+
}
|
|
230
|
+
recordClaudeCredentialHome(configDir, 'profile');
|
|
231
|
+
writeClaudeCredentialStore(credentials, configDir);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Write the live store with no guard. Internal: the only caller is
|
|
235
|
+
* `installIntoLiveClaudeStore` in accounts.ts (a test enforces this).
|
|
236
|
+
*/
|
|
237
|
+
export function writeLiveClaudeCredentialStoreUnguarded(credentials) {
|
|
238
|
+
writeClaudeCredentialStore(credentials, undefined);
|
|
239
|
+
}
|
|
240
|
+
function deleteKeychainItem(service, withAccount) {
|
|
241
|
+
try {
|
|
242
|
+
execFileSync('security', [
|
|
243
|
+
'delete-generic-password',
|
|
244
|
+
...(withAccount ? ['-a', claudeKeychainAccountName()] : []),
|
|
245
|
+
'-s',
|
|
246
|
+
service,
|
|
247
|
+
], {
|
|
248
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
249
|
+
timeout: SECURITY_TIMEOUT_MS,
|
|
250
|
+
killSignal: 'SIGKILL',
|
|
251
|
+
});
|
|
252
|
+
return 'deleted';
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND ? 'absent' : 'error';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Remove the credential store of a config directory (an abandoned isolated
|
|
260
|
+
* login, a removed profile, the profile home of the account that became
|
|
261
|
+
* live). Never throws; returns true when the store is gone afterwards.
|
|
262
|
+
*/
|
|
263
|
+
export function deleteStoredCredentials(configDir) {
|
|
264
|
+
let gone = true;
|
|
265
|
+
if (process.platform === 'darwin') {
|
|
266
|
+
const service = claudeKeychainService(configDir);
|
|
267
|
+
let outcome = deleteKeychainItem(service, true);
|
|
268
|
+
// Items written before the account name was pinned may carry another one.
|
|
269
|
+
if (outcome === 'absent')
|
|
270
|
+
outcome = deleteKeychainItem(service, false);
|
|
271
|
+
if (outcome === 'error')
|
|
272
|
+
gone = false;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
fs.rmSync(getCredentialsFilePath(configDir), { force: true });
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
gone = false;
|
|
279
|
+
}
|
|
280
|
+
if (gone)
|
|
281
|
+
forgetClaudeCredentialHome(configDir);
|
|
282
|
+
return gone;
|
|
283
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read Claude Max OAuth credentials from disk.
|
|
3
|
+
*
|
|
4
|
+
* Shared by sidekick-cli, sidekick-vscode, and any external consumer.
|
|
5
|
+
*/
|
|
6
|
+
import { parseClaudeCredentialBlob } from './claudeCredentials';
|
|
7
|
+
import { readActiveCredentials } from './credentialIO';
|
|
8
|
+
function readParsed() {
|
|
9
|
+
try {
|
|
10
|
+
return parseClaudeCredentialBlob(readActiveCredentials());
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reads Claude Max OAuth credentials.
|
|
18
|
+
*
|
|
19
|
+
* On macOS reads from system Keychain; on Linux/Windows reads from
|
|
20
|
+
* `~/.claude/.credentials.json`. Returns `null` if credentials are
|
|
21
|
+
* absent, the token is missing, or the token is expired. Never throws.
|
|
22
|
+
*/
|
|
23
|
+
export async function readClaudeMaxCredentials() {
|
|
24
|
+
const parsed = readParsed();
|
|
25
|
+
if (!parsed)
|
|
26
|
+
return null;
|
|
27
|
+
if (parsed.expiresAt && Date.now() > parsed.expiresAt)
|
|
28
|
+
return null;
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Like {@link readClaudeMaxCredentials} but returns the stored credential even
|
|
33
|
+
* when the access token has lapsed: the CLI refreshes it on next use, so an
|
|
34
|
+
* expired access token is still a valid login (until the refresh token expires).
|
|
35
|
+
*/
|
|
36
|
+
export function readClaudeMaxCredentialsRaw() {
|
|
37
|
+
return readParsed();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Synchronous convenience — returns just the access token or `null`.
|
|
41
|
+
*/
|
|
42
|
+
export function readClaudeMaxAccessTokenSync() {
|
|
43
|
+
const parsed = readParsed();
|
|
44
|
+
if (!parsed)
|
|
45
|
+
return null;
|
|
46
|
+
if (parsed.expiresAt && Date.now() > parsed.expiresAt)
|
|
47
|
+
return null;
|
|
48
|
+
return parsed.accessToken;
|
|
49
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getActiveAccountStatus } from './accountStatus';
|
|
4
|
+
import { resolveProjectIdentity } from './paths';
|
|
5
|
+
import { discoverSessionDirectory, encodeWorkspacePath, findSubdirectorySessionDirs, getMostRecentlyActiveSessionDir, getSessionDirectory, } from './parsers/sessionPathResolver';
|
|
6
|
+
import { toLegacyProviderStatus } from './providerStatus';
|
|
7
|
+
import { fetchProviderServiceStatus } from './providerServiceStatus';
|
|
8
|
+
import { getOpenCodeDataDir } from './providers/openCode';
|
|
9
|
+
import { createSessionProviders } from './providers/factory';
|
|
10
|
+
import { detectProvider } from './providers/detect';
|
|
11
|
+
import { OpenCodeDatabase } from './providers/openCodeDatabase';
|
|
12
|
+
import { getClaudeProjectsDir } from './claudePaths';
|
|
13
|
+
/** Collects the Claude Code path-resolution detail used by both host surfaces. */
|
|
14
|
+
export function getSessionDiagnostics(workspacePath) {
|
|
15
|
+
const encodedPath = encodeWorkspacePath(workspacePath);
|
|
16
|
+
const expectedSessionDir = getSessionDirectory(workspacePath);
|
|
17
|
+
const projectsDir = getClaudeProjectsDir();
|
|
18
|
+
let existingProjectDirs = [];
|
|
19
|
+
let expectedDirExists = false;
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(projectsDir)) {
|
|
22
|
+
existingProjectDirs = fs
|
|
23
|
+
.readdirSync(projectsDir)
|
|
24
|
+
.filter((name) => {
|
|
25
|
+
try {
|
|
26
|
+
return fs.statSync(path.join(projectsDir, name)).isDirectory();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
34
|
+
expectedDirExists = fs.existsSync(expectedSessionDir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Diagnostics remain useful even if a directory cannot be read.
|
|
38
|
+
}
|
|
39
|
+
const discoveredSessionDir = discoverSessionDirectory(workspacePath);
|
|
40
|
+
const workspaceBasename = path.basename(workspacePath).toLowerCase();
|
|
41
|
+
const similarDirs = existingProjectDirs.filter((directory) => {
|
|
42
|
+
const lower = directory.toLowerCase();
|
|
43
|
+
return (lower.includes(workspaceBasename) || workspaceBasename.includes(lower.split('-').pop() || ''));
|
|
44
|
+
});
|
|
45
|
+
const subdirectoryMatches = findSubdirectorySessionDirs(workspacePath);
|
|
46
|
+
return {
|
|
47
|
+
workspacePath,
|
|
48
|
+
encodedPath,
|
|
49
|
+
expectedSessionDir,
|
|
50
|
+
expectedDirExists,
|
|
51
|
+
discoveredSessionDir,
|
|
52
|
+
existingProjectDirs,
|
|
53
|
+
similarDirs,
|
|
54
|
+
platform: process.platform,
|
|
55
|
+
subdirectoryMatches,
|
|
56
|
+
selectedSubdirectoryMatch: subdirectoryMatches.length > 0 ? getMostRecentlyActiveSessionDir(subdirectoryMatches) : null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function statusUnavailable() {
|
|
60
|
+
return {
|
|
61
|
+
indicator: 'none',
|
|
62
|
+
description: 'Status unavailable',
|
|
63
|
+
affectedComponents: [],
|
|
64
|
+
activeIncident: null,
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function checkSessionProvider(providerId, workspacePath) {
|
|
69
|
+
const { providers } = createSessionProviders({ providerIds: [providerId] });
|
|
70
|
+
const provider = providers[0];
|
|
71
|
+
const title = `Session discovery (${provider?.displayName ?? providerId})`;
|
|
72
|
+
try {
|
|
73
|
+
if (!provider)
|
|
74
|
+
throw new Error(`${providerId} could not be initialized.`);
|
|
75
|
+
const sessions = provider.listSessionFilesAsync
|
|
76
|
+
? await provider.listSessionFilesAsync(workspacePath)
|
|
77
|
+
: provider.findAllSessions(workspacePath);
|
|
78
|
+
if (sessions.length > 0) {
|
|
79
|
+
return {
|
|
80
|
+
id: 'session_discovery',
|
|
81
|
+
status: 'ok',
|
|
82
|
+
title,
|
|
83
|
+
message: `Found ${sessions.length} ${provider.displayName} session${sessions.length === 1 ? '' : 's'} for ${workspacePath}.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const operation = provider.getLastOperationStatus?.();
|
|
87
|
+
const runtime = provider.getRuntimeStatus?.();
|
|
88
|
+
const issue = operation && !operation.usable
|
|
89
|
+
? operation.runtimeStatus
|
|
90
|
+
: runtime && !runtime.available
|
|
91
|
+
? runtime
|
|
92
|
+
: undefined;
|
|
93
|
+
return {
|
|
94
|
+
id: 'session_discovery',
|
|
95
|
+
status: 'warning',
|
|
96
|
+
title,
|
|
97
|
+
message: issue?.message ?? `No ${provider.displayName} sessions found for ${workspacePath}.`,
|
|
98
|
+
repair: issue?.kind === 'sqlite_missing'
|
|
99
|
+
? 'Install sqlite3 and ensure it is on PATH, then rerun sidekick doctor.'
|
|
100
|
+
: `Start ${provider.displayName} in this project, or inspect ${provider.getSessionDirectory(workspacePath)}.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
id: 'session_discovery',
|
|
106
|
+
status: 'warning',
|
|
107
|
+
title,
|
|
108
|
+
message: error instanceof Error ? error.message : String(error),
|
|
109
|
+
repair: `Check ${providerId} session storage permissions and rerun sidekick doctor.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
for (const candidate of providers)
|
|
114
|
+
candidate.dispose();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function runDoctor(options = {}) {
|
|
118
|
+
const project = resolveProjectIdentity(options.cwd);
|
|
119
|
+
const sessions = getSessionDiagnostics(project.cwd);
|
|
120
|
+
const accounts = getActiveAccountStatus(undefined, { selfHeal: false });
|
|
121
|
+
const sessionProvider = options.provider && options.provider !== 'auto' ? options.provider : detectProvider();
|
|
122
|
+
let openCode = options.openCodeStatus;
|
|
123
|
+
if (!openCode) {
|
|
124
|
+
const database = new OpenCodeDatabase(getOpenCodeDataDir());
|
|
125
|
+
database.open();
|
|
126
|
+
openCode = database.getRuntimeStatus();
|
|
127
|
+
database.close();
|
|
128
|
+
}
|
|
129
|
+
const serviceStatus = options.fetchServiceStatuses || !options.fetchStatuses
|
|
130
|
+
? await (options.fetchServiceStatuses ??
|
|
131
|
+
(async () => {
|
|
132
|
+
const [claude, openai] = await Promise.all([
|
|
133
|
+
fetchProviderServiceStatus('claude-code'),
|
|
134
|
+
fetchProviderServiceStatus('codex'),
|
|
135
|
+
]);
|
|
136
|
+
return { claude, openai };
|
|
137
|
+
}))().catch(() => {
|
|
138
|
+
const unavailable = (provider) => ({
|
|
139
|
+
availability: 'unavailable',
|
|
140
|
+
provider,
|
|
141
|
+
checkedAt: new Date().toISOString(),
|
|
142
|
+
sourceUrl: `https://status.${provider === 'codex' ? 'openai' : 'claude'}.com/api/v2/summary.json`,
|
|
143
|
+
reason: 'network_error',
|
|
144
|
+
});
|
|
145
|
+
return { claude: unavailable('claude-code'), openai: unavailable('codex') };
|
|
146
|
+
})
|
|
147
|
+
: undefined;
|
|
148
|
+
const providerStatus = serviceStatus
|
|
149
|
+
? {
|
|
150
|
+
claude: toLegacyProviderStatus(serviceStatus.claude),
|
|
151
|
+
openai: toLegacyProviderStatus(serviceStatus.openai),
|
|
152
|
+
}
|
|
153
|
+
: await options.fetchStatuses().catch(() => ({
|
|
154
|
+
claude: statusUnavailable(),
|
|
155
|
+
openai: statusUnavailable(),
|
|
156
|
+
}));
|
|
157
|
+
const checks = [];
|
|
158
|
+
checks.push(project.hasLegacyMismatch
|
|
159
|
+
? {
|
|
160
|
+
id: 'project_slug',
|
|
161
|
+
status: 'warning',
|
|
162
|
+
title: 'Project path resolves to a different slug',
|
|
163
|
+
message: `Legacy ${project.legacySlug}; canonical ${project.canonicalSlug}.`,
|
|
164
|
+
repair: `Open the resolved path (${project.resolvedCwd}) and merge any legacy project stores into the canonical slug. Sidekick readers keep the legacy fallback.`,
|
|
165
|
+
}
|
|
166
|
+
: {
|
|
167
|
+
id: 'project_slug',
|
|
168
|
+
status: 'ok',
|
|
169
|
+
title: 'Project identity',
|
|
170
|
+
message: `Canonical slug: ${project.canonicalSlug}`,
|
|
171
|
+
});
|
|
172
|
+
checks.push(await checkSessionProvider(sessionProvider, project.resolvedCwd));
|
|
173
|
+
const openCodeCheck = openCode.available
|
|
174
|
+
? {
|
|
175
|
+
id: 'opencode_sqlite',
|
|
176
|
+
status: 'ok',
|
|
177
|
+
title: 'OpenCode database',
|
|
178
|
+
message: 'OpenCode database and sqlite3 are available.',
|
|
179
|
+
}
|
|
180
|
+
: openCode.kind === 'db_missing'
|
|
181
|
+
? {
|
|
182
|
+
id: 'opencode_sqlite',
|
|
183
|
+
status: 'info',
|
|
184
|
+
title: 'OpenCode database',
|
|
185
|
+
message: 'No OpenCode database was found; this is expected if OpenCode is not used.',
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
id: 'opencode_sqlite',
|
|
189
|
+
status: 'warning',
|
|
190
|
+
title: 'OpenCode database',
|
|
191
|
+
message: openCode.message ?? `OpenCode database unavailable (${openCode.kind}).`,
|
|
192
|
+
repair: openCode.kind === 'sqlite_missing'
|
|
193
|
+
? 'Install sqlite3 and ensure the sqlite3 executable is on PATH, then rerun sidekick doctor.'
|
|
194
|
+
: 'Make sqlite3 executable and verify the OpenCode database is readable.',
|
|
195
|
+
};
|
|
196
|
+
if (sessionProvider !== 'opencode' && openCodeCheck.status === 'warning') {
|
|
197
|
+
openCodeCheck.status = 'info';
|
|
198
|
+
openCodeCheck.message += ' This does not affect the selected session provider.';
|
|
199
|
+
}
|
|
200
|
+
checks.push(openCodeCheck);
|
|
201
|
+
checks.push({
|
|
202
|
+
id: 'accounts',
|
|
203
|
+
status: accounts.ok ? 'ok' : 'info',
|
|
204
|
+
title: 'Accounts and credentials',
|
|
205
|
+
message: accounts.ok
|
|
206
|
+
? `Detected ${[accounts.claude.present && 'Claude', accounts.codex.present && 'Codex'].filter(Boolean).join(' and ')} credentials.`
|
|
207
|
+
: (accounts.error ?? 'No Claude or Codex credentials were detected.'),
|
|
208
|
+
...(!accounts.ok
|
|
209
|
+
? {
|
|
210
|
+
repair: 'Sign in with claude or codex to refresh account quota. Existing sessions can be monitored without saved credentials.',
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
213
|
+
});
|
|
214
|
+
const unavailableProviders = serviceStatus
|
|
215
|
+
? Object.entries(serviceStatus)
|
|
216
|
+
.filter(([, status]) => status.availability === 'unavailable')
|
|
217
|
+
.map(([name]) => name)
|
|
218
|
+
: Object.entries(providerStatus)
|
|
219
|
+
.filter(([, status]) => status.description === 'Status unavailable')
|
|
220
|
+
.map(([name]) => name);
|
|
221
|
+
const degradedProviders = serviceStatus
|
|
222
|
+
? Object.entries(serviceStatus)
|
|
223
|
+
.filter(([, status]) => status.availability === 'observed' && status.severity !== 'none')
|
|
224
|
+
.map(([name]) => name)
|
|
225
|
+
: Object.entries(providerStatus)
|
|
226
|
+
.filter(([, status]) => status.indicator !== 'none')
|
|
227
|
+
.map(([name]) => name);
|
|
228
|
+
const partialProviders = serviceStatus
|
|
229
|
+
? Object.entries(serviceStatus)
|
|
230
|
+
.filter(([, status]) => status.availability === 'observed' && status.incidents === null)
|
|
231
|
+
.map(([name]) => name)
|
|
232
|
+
: [];
|
|
233
|
+
const observations = [
|
|
234
|
+
...(degradedProviders.length
|
|
235
|
+
? [`Public vendor degradation reported for: ${degradedProviders.join(', ')}.`]
|
|
236
|
+
: []),
|
|
237
|
+
...(unavailableProviders.length
|
|
238
|
+
? [`Status pages unavailable for: ${unavailableProviders.join(', ')}.`]
|
|
239
|
+
: []),
|
|
240
|
+
...(partialProviders.length
|
|
241
|
+
? [`Incident information unavailable for: ${partialProviders.join(', ')}.`]
|
|
242
|
+
: []),
|
|
243
|
+
];
|
|
244
|
+
checks.push({
|
|
245
|
+
id: 'provider_api',
|
|
246
|
+
status: degradedProviders.includes(sessionProvider === 'codex' ? 'openai' : sessionProvider === 'claude-code' ? 'claude' : '')
|
|
247
|
+
? 'warning'
|
|
248
|
+
: observations.length
|
|
249
|
+
? 'info'
|
|
250
|
+
: 'ok',
|
|
251
|
+
title: 'Public provider service status',
|
|
252
|
+
message: (observations.length
|
|
253
|
+
? observations.join(' ')
|
|
254
|
+
: 'Claude and OpenAI status pages report normal operation.') +
|
|
255
|
+
' Public status does not establish request connectivity or authentication.',
|
|
256
|
+
});
|
|
257
|
+
const deprecatedSettings = options.deprecatedSettings ?? [];
|
|
258
|
+
checks.push({
|
|
259
|
+
id: 'deprecated_settings',
|
|
260
|
+
status: deprecatedSettings.length > 0 ? 'warning' : 'ok',
|
|
261
|
+
title: 'Deprecated settings',
|
|
262
|
+
message: deprecatedSettings.length > 0
|
|
263
|
+
? deprecatedSettings
|
|
264
|
+
.map((setting) => `${setting.key}${setting.replacement ? ` → ${setting.replacement}` : ''}`)
|
|
265
|
+
.join(', ')
|
|
266
|
+
: 'No deprecated Sidekick settings detected.',
|
|
267
|
+
...(deprecatedSettings.length > 0
|
|
268
|
+
? {
|
|
269
|
+
repair: 'Move each configured value to its replacement and remove the deprecated setting.',
|
|
270
|
+
}
|
|
271
|
+
: {}),
|
|
272
|
+
});
|
|
273
|
+
const status = checks.some((check) => check.status === 'error')
|
|
274
|
+
? 'unhealthy'
|
|
275
|
+
: checks.some((check) => check.status === 'warning')
|
|
276
|
+
? 'attention'
|
|
277
|
+
: 'healthy';
|
|
278
|
+
return {
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
generatedAt: new Date().toISOString(),
|
|
281
|
+
project,
|
|
282
|
+
status,
|
|
283
|
+
checks,
|
|
284
|
+
accounts,
|
|
285
|
+
openCode,
|
|
286
|
+
providerStatus,
|
|
287
|
+
...(serviceStatus ? { serviceStatus } : {}),
|
|
288
|
+
sessions,
|
|
289
|
+
sessionProvider,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
export function formatHealthReport(report) {
|
|
293
|
+
const icon = { ok: '✓', warning: '!', error: '✕', info: '·' };
|
|
294
|
+
const lines = [
|
|
295
|
+
`Sidekick Doctor — ${report.status}`,
|
|
296
|
+
`Project: ${report.project.resolvedCwd}`,
|
|
297
|
+
'',
|
|
298
|
+
];
|
|
299
|
+
for (const check of report.checks) {
|
|
300
|
+
lines.push(`${icon[check.status]} ${check.title}: ${check.message}`);
|
|
301
|
+
if (check.repair)
|
|
302
|
+
lines.push(` Repair: ${check.repair}`);
|
|
303
|
+
}
|
|
304
|
+
return lines.join('\n');
|
|
305
|
+
}
|