sidekick-shared 0.27.5 → 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 +58 -0
- package/README.md +93 -52
- package/dist/accountHealth.d.ts +14 -0
- package/dist/accountHealth.js +61 -14
- package/dist/accountLaunch.d.ts +6 -0
- package/dist/accountLaunch.js +61 -4
- package/dist/accountLease.d.ts +5 -0
- package/dist/accountLease.js +98 -0
- package/dist/accountManager.d.ts +7 -0
- package/dist/accountManager.js +226 -89
- package/dist/accountSync.d.ts +1 -5
- package/dist/accountSync.js +22 -6
- package/dist/accountSyncTypes.d.ts +9 -0
- package/dist/accounts.d.ts +75 -18
- package/dist/accounts.js +712 -189
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudeCredentialDoctor.d.ts +50 -0
- package/dist/claudeCredentialDoctor.js +277 -0
- package/dist/claudeCredentialLedger.d.ts +15 -0
- package/dist/claudeCredentialLedger.js +130 -0
- package/dist/claudeCredentials.d.ts +20 -3
- package/dist/claudeCredentials.js +50 -3
- package/dist/claudeLiveGuard.d.ts +33 -0
- package/dist/claudeLiveGuard.js +46 -0
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +15 -8
- package/dist/claudeProfiles.js +30 -23
- package/dist/claudeRefreshLock.d.ts +15 -0
- package/dist/claudeRefreshLock.js +145 -0
- package/dist/codexAuth.d.ts +4 -0
- package/dist/codexAuth.js +18 -0
- package/dist/codexCredentialDoctor.d.ts +33 -0
- package/dist/codexCredentialDoctor.js +302 -0
- package/dist/codexCredentialStore.d.ts +29 -0
- package/dist/codexCredentialStore.js +213 -0
- package/dist/codexKeepAlive.d.ts +2 -0
- package/dist/codexKeepAlive.js +116 -0
- package/dist/codexPaths.d.ts +30 -2
- package/dist/codexPaths.js +118 -7
- package/dist/codexProfiles.d.ts +38 -35
- package/dist/codexProfiles.js +848 -715
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/doctor.js +2 -2
- package/dist/ensureDefaultAccounts.js +1 -8
- 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 +30 -11
- package/dist/index.js +72 -15
- package/dist/keepAlive.d.ts +2 -1
- package/dist/keepAlive.js +116 -29
- package/dist/liveCredentialGuard.d.ts +28 -0
- package/dist/liveCredentialGuard.js +36 -0
- 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/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- 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/accountManager.d.ts +22 -0
- package/dist/schemas/accountManager.js +4 -0
- package/dist/schemas/assistantTurn.d.ts +2 -2
- 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/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- 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
package/dist/credentialIO.d.ts
CHANGED
|
@@ -7,30 +7,90 @@
|
|
|
7
7
|
* Keychain is unavailable (locked, SSH) Claude Code falls back to the file on
|
|
8
8
|
* macOS too, so reads consult the file after a Keychain miss.
|
|
9
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
|
+
*
|
|
10
17
|
* Both accounts.ts and credentials.ts consume this module so that all
|
|
11
18
|
* credential access is platform-correct in one place.
|
|
12
19
|
*/
|
|
13
20
|
export declare const KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
21
|
+
/**
|
|
22
|
+
* Claude Code's ceiling for a `security -i` command line (the tool reads at
|
|
23
|
+
* most 4095 bytes per line). Longer commands go through argv instead.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SECURITY_INTERACTIVE_LINE_LIMIT = 4032;
|
|
26
|
+
/**
|
|
27
|
+
* Written as the item comment (`-j`) on every Keychain item Sidekick creates,
|
|
28
|
+
* so ownership is provable from the item itself even after its directory and
|
|
29
|
+
* the ledger that described it are gone.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SIDEKICK_KEYCHAIN_COMMENT_PREFIX = "sidekick-agent-hub:";
|
|
32
|
+
export type ClaudeCredentialStoreSource = 'keychain' | 'file';
|
|
33
|
+
/**
|
|
34
|
+
* Outcome of reading one credential store. `error` (timeout, locked Keychain)
|
|
35
|
+
* is never treated like `absent`: guard decisions that may overwrite a store
|
|
36
|
+
* only proceed on a definite answer.
|
|
37
|
+
*/
|
|
38
|
+
export type ClaudeCredentialStoreRead = {
|
|
39
|
+
status: 'ok';
|
|
40
|
+
blob: Record<string, unknown>;
|
|
41
|
+
source: ClaudeCredentialStoreSource;
|
|
42
|
+
} | {
|
|
43
|
+
status: 'absent';
|
|
44
|
+
} | {
|
|
45
|
+
status: 'corrupt';
|
|
46
|
+
source: ClaudeCredentialStoreSource;
|
|
47
|
+
length: number;
|
|
48
|
+
} | {
|
|
49
|
+
status: 'error';
|
|
50
|
+
reason: string;
|
|
51
|
+
};
|
|
52
|
+
/** A credential write that did not land, or could not be verified. */
|
|
53
|
+
export declare class ClaudeCredentialWriteError extends Error {
|
|
54
|
+
constructor(message: string);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read one Claude credential store with a definite outcome. On macOS the
|
|
58
|
+
* Keychain item is authoritative; the file is consulted only when the item is
|
|
59
|
+
* absent (Claude Code's own fallback), unless `keychainOnly` is set.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readClaudeCredentialStore(configDir?: string, options?: {
|
|
62
|
+
keychainOnly?: boolean;
|
|
63
|
+
}): ClaudeCredentialStoreRead;
|
|
14
64
|
/**
|
|
15
65
|
* Read the active Claude Code credentials as a parsed object.
|
|
16
66
|
*
|
|
17
67
|
* - macOS: reads from system Keychain via the `security` CLI, then the file
|
|
18
68
|
* - Linux / WSL / Windows: reads `<home>/.credentials.json`
|
|
19
69
|
*
|
|
20
|
-
* Returns `null` when credentials are absent or unreadable.
|
|
70
|
+
* Returns `null` when credentials are absent or unreadable. Callers that must
|
|
71
|
+
* tell "absent" from "could not read" use {@link readClaudeCredentialStore}.
|
|
21
72
|
*/
|
|
22
73
|
export declare function readActiveCredentials(configDir?: string): unknown;
|
|
23
74
|
/**
|
|
24
|
-
* Write credentials to
|
|
75
|
+
* Write credentials to a profile home's Claude Code credential store.
|
|
25
76
|
*
|
|
26
|
-
* - macOS:
|
|
27
|
-
* - Linux / WSL / Windows: atomic-writes `<
|
|
77
|
+
* - macOS: the suffixed Keychain item for `configDir`
|
|
78
|
+
* - Linux / WSL / Windows: atomic-writes `<configDir>/.credentials.json`
|
|
28
79
|
*
|
|
29
|
-
*
|
|
80
|
+
* `configDir` is required: the live store is written only through the guarded
|
|
81
|
+
* installer in accounts.ts, which refuses downgrades and captures the outgoing
|
|
82
|
+
* login first. Throws {@link ClaudeCredentialWriteError} when the write did
|
|
83
|
+
* not land intact.
|
|
30
84
|
*/
|
|
31
85
|
export declare function writeActiveCredentials(credentials: unknown, configDir?: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* Write the live store with no guard. Internal: the only caller is
|
|
88
|
+
* `installIntoLiveClaudeStore` in accounts.ts (a test enforces this).
|
|
89
|
+
*/
|
|
90
|
+
export declare function writeLiveClaudeCredentialStoreUnguarded(credentials: unknown): void;
|
|
32
91
|
/**
|
|
33
92
|
* Remove the credential store of a config directory (an abandoned isolated
|
|
34
|
-
* login, a removed profile
|
|
93
|
+
* login, a removed profile, the profile home of the account that became
|
|
94
|
+
* live). Never throws; returns true when the store is gone afterwards.
|
|
35
95
|
*/
|
|
36
|
-
export declare function deleteStoredCredentials(configDir: string):
|
|
96
|
+
export declare function deleteStoredCredentials(configDir: string): boolean;
|
package/dist/credentialIO.js
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* Keychain is unavailable (locked, SSH) Claude Code falls back to the file on
|
|
9
9
|
* macOS too, so reads consult the file after a Keychain miss.
|
|
10
10
|
*
|
|
11
|
+
* Keychain writes mirror Claude Code 2.1.x exactly: the payload is hex-encoded
|
|
12
|
+
* (`-X`) and sent through `security -i` only while the command line fits the
|
|
13
|
+
* tool's line buffer; longer payloads go through argv. `security -i` silently
|
|
14
|
+
* truncates longer lines, which once stored half a credential (the `mcpOAuth`
|
|
15
|
+
* tokens make blobs of 4 KB and more) — a login Claude Code then reports as
|
|
16
|
+
* "Not logged in". Every write is read back and compared before it counts.
|
|
17
|
+
*
|
|
11
18
|
* Both accounts.ts and credentials.ts consume this module so that all
|
|
12
19
|
* credential access is platform-correct in one place.
|
|
13
20
|
*/
|
|
@@ -45,17 +52,41 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
52
|
};
|
|
46
53
|
})();
|
|
47
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.KEYCHAIN_SERVICE = void 0;
|
|
55
|
+
exports.ClaudeCredentialWriteError = exports.SIDEKICK_KEYCHAIN_COMMENT_PREFIX = exports.SECURITY_INTERACTIVE_LINE_LIMIT = exports.KEYCHAIN_SERVICE = void 0;
|
|
56
|
+
exports.readClaudeCredentialStore = readClaudeCredentialStore;
|
|
49
57
|
exports.readActiveCredentials = readActiveCredentials;
|
|
50
58
|
exports.writeActiveCredentials = writeActiveCredentials;
|
|
59
|
+
exports.writeLiveClaudeCredentialStoreUnguarded = writeLiveClaudeCredentialStoreUnguarded;
|
|
51
60
|
exports.deleteStoredCredentials = deleteStoredCredentials;
|
|
52
61
|
const fs = __importStar(require("fs"));
|
|
53
62
|
const path = __importStar(require("path"));
|
|
54
63
|
const child_process_1 = require("child_process");
|
|
55
64
|
const claudeProfiles_1 = require("./claudeProfiles");
|
|
65
|
+
const claudeCredentialLedger_1 = require("./claudeCredentialLedger");
|
|
56
66
|
const atomic_1 = require("./writers/atomic");
|
|
57
67
|
exports.KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
58
68
|
const SECURITY_TIMEOUT_MS = 4000;
|
|
69
|
+
/** `security` exits with this status when no item matches. */
|
|
70
|
+
const SECURITY_ITEM_NOT_FOUND = 44;
|
|
71
|
+
/**
|
|
72
|
+
* Claude Code's ceiling for a `security -i` command line (the tool reads at
|
|
73
|
+
* most 4095 bytes per line). Longer commands go through argv instead.
|
|
74
|
+
*/
|
|
75
|
+
exports.SECURITY_INTERACTIVE_LINE_LIMIT = 4032;
|
|
76
|
+
/**
|
|
77
|
+
* Written as the item comment (`-j`) on every Keychain item Sidekick creates,
|
|
78
|
+
* so ownership is provable from the item itself even after its directory and
|
|
79
|
+
* the ledger that described it are gone.
|
|
80
|
+
*/
|
|
81
|
+
exports.SIDEKICK_KEYCHAIN_COMMENT_PREFIX = 'sidekick-agent-hub:';
|
|
82
|
+
/** A credential write that did not land, or could not be verified. */
|
|
83
|
+
class ClaudeCredentialWriteError extends Error {
|
|
84
|
+
constructor(message) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = 'ClaudeCredentialWriteError';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.ClaudeCredentialWriteError = ClaudeCredentialWriteError;
|
|
59
90
|
function quoteSecurityInteractiveArg(value) {
|
|
60
91
|
// `security -i` is not a POSIX shell: adjacent quoted segments do NOT
|
|
61
92
|
// concatenate (so the shell-style '\'' splice splits the token), and
|
|
@@ -65,27 +96,69 @@ function quoteSecurityInteractiveArg(value) {
|
|
|
65
96
|
function getCredentialsFilePath(configDir) {
|
|
66
97
|
return path.join(configDir ?? (0, claudeProfiles_1.getLiveClaudeHome)(), '.credentials.json');
|
|
67
98
|
}
|
|
68
|
-
function
|
|
99
|
+
function parseStoredText(raw, source) {
|
|
100
|
+
const text = raw.trim();
|
|
101
|
+
if (!text)
|
|
102
|
+
return { status: 'corrupt', source, length: 0 };
|
|
103
|
+
// `security -w` prints hex when the stored bytes are not plain text.
|
|
104
|
+
const decoded = source === 'keychain' && !text.startsWith('{') && /^(?:[0-9a-f]{2})+$/i.test(text)
|
|
105
|
+
? Buffer.from(text, 'hex').toString('utf8')
|
|
106
|
+
: text;
|
|
69
107
|
try {
|
|
70
|
-
|
|
108
|
+
const parsed = JSON.parse(decoded);
|
|
109
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
110
|
+
return { status: 'ok', blob: parsed, source };
|
|
111
|
+
}
|
|
71
112
|
}
|
|
72
113
|
catch {
|
|
73
|
-
|
|
114
|
+
/* fall through */
|
|
74
115
|
}
|
|
116
|
+
return { status: 'corrupt', source, length: decoded.length };
|
|
75
117
|
}
|
|
76
|
-
function
|
|
118
|
+
function readCredentialsFileStore(configDir) {
|
|
119
|
+
let raw;
|
|
77
120
|
try {
|
|
78
|
-
|
|
121
|
+
raw = fs.readFileSync(getCredentialsFilePath(configDir), 'utf8');
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return error.code === 'ENOENT'
|
|
125
|
+
? { status: 'absent' }
|
|
126
|
+
: { status: 'error', reason: String(error.message ?? error) };
|
|
127
|
+
}
|
|
128
|
+
return parseStoredText(raw, 'file');
|
|
129
|
+
}
|
|
130
|
+
function securityErrorStatus(error) {
|
|
131
|
+
const status = error.status;
|
|
132
|
+
return typeof status === 'number' ? status : null;
|
|
133
|
+
}
|
|
134
|
+
function readKeychainStore(service) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = (0, child_process_1.execFileSync)('security', ['find-generic-password', '-a', (0, claudeProfiles_1.claudeKeychainAccountName)(), '-s', service, '-w'], {
|
|
79
137
|
encoding: 'utf8',
|
|
80
138
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
139
|
timeout: SECURITY_TIMEOUT_MS,
|
|
82
140
|
killSignal: 'SIGKILL',
|
|
83
141
|
});
|
|
84
|
-
return
|
|
142
|
+
return parseStoredText(String(raw), 'keychain');
|
|
85
143
|
}
|
|
86
|
-
catch {
|
|
87
|
-
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND)
|
|
146
|
+
return { status: 'absent' };
|
|
147
|
+
return { status: 'error', reason: String(error?.message ?? error) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Read one Claude credential store with a definite outcome. On macOS the
|
|
152
|
+
* Keychain item is authoritative; the file is consulted only when the item is
|
|
153
|
+
* absent (Claude Code's own fallback), unless `keychainOnly` is set.
|
|
154
|
+
*/
|
|
155
|
+
function readClaudeCredentialStore(configDir, options = {}) {
|
|
156
|
+
if (process.platform === 'darwin') {
|
|
157
|
+
const keychain = readKeychainStore((0, claudeProfiles_1.claudeKeychainService)(configDir));
|
|
158
|
+
if (keychain.status !== 'absent' || options.keychainOnly)
|
|
159
|
+
return keychain;
|
|
88
160
|
}
|
|
161
|
+
return readCredentialsFileStore(configDir);
|
|
89
162
|
}
|
|
90
163
|
/**
|
|
91
164
|
* Read the active Claude Code credentials as a parsed object.
|
|
@@ -93,69 +166,160 @@ function readKeychainItem(service) {
|
|
|
93
166
|
* - macOS: reads from system Keychain via the `security` CLI, then the file
|
|
94
167
|
* - Linux / WSL / Windows: reads `<home>/.credentials.json`
|
|
95
168
|
*
|
|
96
|
-
* Returns `null` when credentials are absent or unreadable.
|
|
169
|
+
* Returns `null` when credentials are absent or unreadable. Callers that must
|
|
170
|
+
* tell "absent" from "could not read" use {@link readClaudeCredentialStore}.
|
|
97
171
|
*/
|
|
98
172
|
function readActiveCredentials(configDir) {
|
|
99
173
|
if (process.platform === 'darwin') {
|
|
100
|
-
|
|
174
|
+
const keychain = readKeychainStore((0, claudeProfiles_1.claudeKeychainService)(configDir));
|
|
175
|
+
if (keychain.status === 'ok')
|
|
176
|
+
return keychain.blob;
|
|
101
177
|
}
|
|
102
|
-
|
|
178
|
+
const file = readCredentialsFileStore(configDir);
|
|
179
|
+
return file.status === 'ok' ? file.blob : null;
|
|
180
|
+
}
|
|
181
|
+
function runKeychainWrite(service, hex, comment) {
|
|
182
|
+
const account = (0, claudeProfiles_1.claudeKeychainAccountName)();
|
|
183
|
+
const args = ['add-generic-password', '-U', '-a', account, '-s', service];
|
|
184
|
+
const interactive = [
|
|
185
|
+
'add-generic-password',
|
|
186
|
+
'-U',
|
|
187
|
+
'-a',
|
|
188
|
+
quoteSecurityInteractiveArg(account),
|
|
189
|
+
'-s',
|
|
190
|
+
quoteSecurityInteractiveArg(service),
|
|
191
|
+
];
|
|
192
|
+
if (comment) {
|
|
193
|
+
args.push('-j', comment);
|
|
194
|
+
interactive.push('-j', quoteSecurityInteractiveArg(comment));
|
|
195
|
+
}
|
|
196
|
+
args.push('-X', hex);
|
|
197
|
+
interactive.push('-X', quoteSecurityInteractiveArg(hex));
|
|
198
|
+
const line = `${interactive.join(' ')}\n`;
|
|
199
|
+
if (Buffer.byteLength(line, 'utf8') <= exports.SECURITY_INTERACTIVE_LINE_LIMIT) {
|
|
200
|
+
(0, child_process_1.execFileSync)('security', ['-i'], {
|
|
201
|
+
input: line,
|
|
202
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
203
|
+
timeout: SECURITY_TIMEOUT_MS,
|
|
204
|
+
killSignal: 'SIGKILL',
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// The same trade-off Claude Code makes for large payloads: the hex is on the
|
|
209
|
+
// command line (visible to `ps`) for the duration of this call.
|
|
210
|
+
(0, child_process_1.execFileSync)('security', args, {
|
|
211
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
212
|
+
timeout: SECURITY_TIMEOUT_MS,
|
|
213
|
+
killSignal: 'SIGKILL',
|
|
214
|
+
});
|
|
103
215
|
}
|
|
104
216
|
/**
|
|
105
|
-
* Write
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* - Linux / WSL / Windows: atomic-writes `<home>/.credentials.json`
|
|
109
|
-
*
|
|
110
|
-
* Throws on failure.
|
|
217
|
+
* Write a credential blob to a store and verify it by reading it back.
|
|
218
|
+
* `configDir` undefined targets the live home; only the guarded live installer
|
|
219
|
+
* in accounts.ts may do that (see {@link writeLiveClaudeCredentialStoreUnguarded}).
|
|
111
220
|
*/
|
|
112
|
-
function
|
|
221
|
+
function writeClaudeCredentialStore(credentials, configDir) {
|
|
113
222
|
const json = JSON.stringify(credentials);
|
|
114
|
-
|
|
223
|
+
let expected;
|
|
224
|
+
try {
|
|
225
|
+
expected = JSON.stringify(JSON.parse(json));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
throw new ClaudeCredentialWriteError('Refusing to store a credential that is not JSON.');
|
|
229
|
+
}
|
|
115
230
|
if (process.platform === 'darwin') {
|
|
116
231
|
const service = (0, claudeProfiles_1.claudeKeychainService)(configDir);
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
232
|
+
const comment = configDir
|
|
233
|
+
? `${exports.SIDEKICK_KEYCHAIN_COMMENT_PREFIX}${path.resolve(configDir)}`
|
|
234
|
+
: null;
|
|
235
|
+
try {
|
|
236
|
+
runKeychainWrite(service, Buffer.from(json, 'utf8').toString('hex'), comment);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
throw new ClaudeCredentialWriteError(`Keychain write failed: ${error?.message ?? error}`);
|
|
240
|
+
}
|
|
241
|
+
const readBack = readKeychainStore(service);
|
|
242
|
+
if (readBack.status === 'error') {
|
|
243
|
+
throw new ClaudeCredentialWriteError('The Keychain write could not be verified (read-back failed); treating it as not written.');
|
|
244
|
+
}
|
|
245
|
+
if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
|
|
246
|
+
throw new ClaudeCredentialWriteError('The Keychain item did not hold the credential after writing it.');
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Linux / WSL / Windows — file-based (tmp + fsync + rename, retried on Windows)
|
|
251
|
+
(0, atomic_1.atomicWriteFileSync)(getCredentialsFilePath(configDir), json, 0o600);
|
|
252
|
+
const readBack = readCredentialsFileStore(configDir);
|
|
253
|
+
if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
|
|
254
|
+
throw new ClaudeCredentialWriteError('The credentials file did not hold the credential after writing it.');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Write credentials to a profile home's Claude Code credential store.
|
|
259
|
+
*
|
|
260
|
+
* - macOS: the suffixed Keychain item for `configDir`
|
|
261
|
+
* - Linux / WSL / Windows: atomic-writes `<configDir>/.credentials.json`
|
|
262
|
+
*
|
|
263
|
+
* `configDir` is required: the live store is written only through the guarded
|
|
264
|
+
* installer in accounts.ts, which refuses downgrades and captures the outgoing
|
|
265
|
+
* login first. Throws {@link ClaudeCredentialWriteError} when the write did
|
|
266
|
+
* not land intact.
|
|
267
|
+
*/
|
|
268
|
+
function writeActiveCredentials(credentials, configDir) {
|
|
269
|
+
if (!configDir) {
|
|
270
|
+
throw new ClaudeCredentialWriteError('writeActiveCredentials() needs a config directory; the live Claude login is only written through a guarded account switch.');
|
|
271
|
+
}
|
|
272
|
+
(0, claudeCredentialLedger_1.recordClaudeCredentialHome)(configDir, 'profile');
|
|
273
|
+
writeClaudeCredentialStore(credentials, configDir);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Write the live store with no guard. Internal: the only caller is
|
|
277
|
+
* `installIntoLiveClaudeStore` in accounts.ts (a test enforces this).
|
|
278
|
+
*/
|
|
279
|
+
function writeLiveClaudeCredentialStoreUnguarded(credentials) {
|
|
280
|
+
writeClaudeCredentialStore(credentials, undefined);
|
|
281
|
+
}
|
|
282
|
+
function deleteKeychainItem(service, withAccount) {
|
|
283
|
+
try {
|
|
284
|
+
(0, child_process_1.execFileSync)('security', [
|
|
285
|
+
'delete-generic-password',
|
|
286
|
+
...(withAccount ? ['-a', (0, claudeProfiles_1.claudeKeychainAccountName)()] : []),
|
|
120
287
|
'-s',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'-w',
|
|
125
|
-
quoteSecurityInteractiveArg(json),
|
|
126
|
-
].join(' ');
|
|
127
|
-
(0, child_process_1.execFileSync)('security', ['-i'], {
|
|
128
|
-
input: command + '\n',
|
|
129
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
288
|
+
service,
|
|
289
|
+
], {
|
|
290
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
130
291
|
timeout: SECURITY_TIMEOUT_MS,
|
|
131
292
|
killSignal: 'SIGKILL',
|
|
132
293
|
});
|
|
133
|
-
return;
|
|
294
|
+
return 'deleted';
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
return securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND ? 'absent' : 'error';
|
|
134
298
|
}
|
|
135
|
-
// Linux / WSL / Windows — file-based (tmp + fsync + rename, retried on Windows)
|
|
136
|
-
(0, atomic_1.atomicWriteFileSync)(getCredentialsFilePath(configDir), json, 0o600);
|
|
137
299
|
}
|
|
138
300
|
/**
|
|
139
301
|
* Remove the credential store of a config directory (an abandoned isolated
|
|
140
|
-
* login, a removed profile
|
|
302
|
+
* login, a removed profile, the profile home of the account that became
|
|
303
|
+
* live). Never throws; returns true when the store is gone afterwards.
|
|
141
304
|
*/
|
|
142
305
|
function deleteStoredCredentials(configDir) {
|
|
306
|
+
let gone = true;
|
|
143
307
|
if (process.platform === 'darwin') {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
catch {
|
|
152
|
-
/* no item, or keychain unavailable */
|
|
153
|
-
}
|
|
308
|
+
const service = (0, claudeProfiles_1.claudeKeychainService)(configDir);
|
|
309
|
+
let outcome = deleteKeychainItem(service, true);
|
|
310
|
+
// Items written before the account name was pinned may carry another one.
|
|
311
|
+
if (outcome === 'absent')
|
|
312
|
+
outcome = deleteKeychainItem(service, false);
|
|
313
|
+
if (outcome === 'error')
|
|
314
|
+
gone = false;
|
|
154
315
|
}
|
|
155
316
|
try {
|
|
156
317
|
fs.rmSync(getCredentialsFilePath(configDir), { force: true });
|
|
157
318
|
}
|
|
158
319
|
catch {
|
|
159
|
-
|
|
320
|
+
gone = false;
|
|
160
321
|
}
|
|
322
|
+
if (gone)
|
|
323
|
+
(0, claudeCredentialLedger_1.forgetClaudeCredentialHome)(configDir);
|
|
324
|
+
return gone;
|
|
161
325
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -37,7 +37,6 @@ exports.getSessionDiagnostics = getSessionDiagnostics;
|
|
|
37
37
|
exports.runDoctor = runDoctor;
|
|
38
38
|
exports.formatHealthReport = formatHealthReport;
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
|
-
const os = __importStar(require("os"));
|
|
41
40
|
const path = __importStar(require("path"));
|
|
42
41
|
const accountStatus_1 = require("./accountStatus");
|
|
43
42
|
const paths_1 = require("./paths");
|
|
@@ -48,11 +47,12 @@ const openCode_1 = require("./providers/openCode");
|
|
|
48
47
|
const factory_1 = require("./providers/factory");
|
|
49
48
|
const detect_1 = require("./providers/detect");
|
|
50
49
|
const openCodeDatabase_1 = require("./providers/openCodeDatabase");
|
|
50
|
+
const claudePaths_1 = require("./claudePaths");
|
|
51
51
|
/** Collects the Claude Code path-resolution detail used by both host surfaces. */
|
|
52
52
|
function getSessionDiagnostics(workspacePath) {
|
|
53
53
|
const encodedPath = (0, sessionPathResolver_1.encodeWorkspacePath)(workspacePath);
|
|
54
54
|
const expectedSessionDir = (0, sessionPathResolver_1.getSessionDirectory)(workspacePath);
|
|
55
|
-
const projectsDir =
|
|
55
|
+
const projectsDir = (0, claudePaths_1.getClaudeProjectsDir)();
|
|
56
56
|
let existingProjectDirs = [];
|
|
57
57
|
let expectedDirExists = false;
|
|
58
58
|
try {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ensureDefaultAccounts = ensureDefaultAccounts;
|
|
4
4
|
const accounts_1 = require("./accounts");
|
|
5
|
-
const codexProfiles_1 = require("./codexProfiles");
|
|
6
5
|
const accountSync_1 = require("./accountSync");
|
|
7
6
|
function logFailure(options, message, error) {
|
|
8
7
|
try {
|
|
@@ -35,13 +34,7 @@ async function ensureDefaultAccounts(options) {
|
|
|
35
34
|
logFailure(options, 'Claude auth reconciliation failed.', error);
|
|
36
35
|
}
|
|
37
36
|
try {
|
|
38
|
-
await (0,
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
logFailure(options, 'Codex auth reconciliation failed.', error);
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
(0, accountSync_1.cleanupAbandonedLogins)();
|
|
37
|
+
await (0, accountSync_1.cleanupAbandonedLoginsAsync)();
|
|
45
38
|
}
|
|
46
39
|
catch (error) {
|
|
47
40
|
logFailure(options, 'Abandoned login cleanup failed.', error);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const listeners = new Set();
|
|
2
|
+
export function _onRawAccountsChanged(listener) {
|
|
3
|
+
listeners.add(listener);
|
|
4
|
+
return () => listeners.delete(listener);
|
|
5
|
+
}
|
|
6
|
+
export function _emitAccountsChanged(reason) {
|
|
7
|
+
for (const listener of listeners) {
|
|
8
|
+
try {
|
|
9
|
+
listener(reason);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Account notifications are observational and cannot break mutations.
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { _emitAccountsChanged, _onRawAccountsChanged, } from './accountChangeSignal';
|
|
5
|
+
import { getActiveAccountStatus } from './accountStatus';
|
|
6
|
+
import { getAccountsDir } from './accountRegistry';
|
|
7
|
+
import { syncLiveAccountStateSync } from './accountSync';
|
|
8
|
+
import { getLiveClaudeHome } from './claudeProfiles';
|
|
9
|
+
import { getSystemCodexHome } from './codexPaths';
|
|
10
|
+
const listeners = new Set();
|
|
11
|
+
let watchers = [];
|
|
12
|
+
let pollTimer;
|
|
13
|
+
let stopRawListener;
|
|
14
|
+
let lastFingerprint;
|
|
15
|
+
let debounceTimer;
|
|
16
|
+
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
17
|
+
let activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
18
|
+
/**
|
|
19
|
+
* Observe login, logout, and active-account switches. Local Sidekick mutations
|
|
20
|
+
* signal immediately; filesystem watchers cover external CLIs; a low-frequency
|
|
21
|
+
* catch-up poll protects network mounts and watcher-hostile filesystems.
|
|
22
|
+
*/
|
|
23
|
+
export function onAccountsChanged(listener, options = {}) {
|
|
24
|
+
listeners.add(listener);
|
|
25
|
+
// The fastest interval any live subscriber asked for wins; a later, faster
|
|
26
|
+
// subscriber restarts the timer rather than being silently ignored.
|
|
27
|
+
const requestedPollMs = Math.max(1_000, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
28
|
+
if (!stopRawListener) {
|
|
29
|
+
activePollIntervalMs = requestedPollMs;
|
|
30
|
+
ensureMonitoring();
|
|
31
|
+
}
|
|
32
|
+
else if (requestedPollMs < activePollIntervalMs) {
|
|
33
|
+
activePollIntervalMs = requestedPollMs;
|
|
34
|
+
restartPollTimer();
|
|
35
|
+
}
|
|
36
|
+
if (options.emitCurrent)
|
|
37
|
+
emitIfChanged('poll', true, listener);
|
|
38
|
+
return {
|
|
39
|
+
dispose: () => {
|
|
40
|
+
listeners.delete(listener);
|
|
41
|
+
if (listeners.size === 0)
|
|
42
|
+
stopMonitoring();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function ensureMonitoring() {
|
|
47
|
+
if (stopRawListener)
|
|
48
|
+
return;
|
|
49
|
+
lastFingerprint = fingerprint(getActiveAccountStatus());
|
|
50
|
+
stopRawListener = _onRawAccountsChanged((reason) => scheduleCheck(reason));
|
|
51
|
+
for (const target of watchTargets()) {
|
|
52
|
+
try {
|
|
53
|
+
const watcher = fs.watch(target, { persistent: false }, () => {
|
|
54
|
+
scheduleCheck('filesystem');
|
|
55
|
+
});
|
|
56
|
+
watcher.on('error', () => {
|
|
57
|
+
watchers = watchers.filter((candidate) => candidate !== watcher);
|
|
58
|
+
try {
|
|
59
|
+
watcher.close();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// The catch-up poll remains active.
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
watchers.push(watcher);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Missing/unwatchable directories are covered by the catch-up poll.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
startPollTimer();
|
|
72
|
+
}
|
|
73
|
+
function startPollTimer() {
|
|
74
|
+
pollTimer = setInterval(() => emitIfChanged('poll'), activePollIntervalMs);
|
|
75
|
+
pollTimer.unref?.();
|
|
76
|
+
}
|
|
77
|
+
function restartPollTimer() {
|
|
78
|
+
if (pollTimer)
|
|
79
|
+
clearInterval(pollTimer);
|
|
80
|
+
startPollTimer();
|
|
81
|
+
}
|
|
82
|
+
function stopMonitoring() {
|
|
83
|
+
stopRawListener?.();
|
|
84
|
+
stopRawListener = undefined;
|
|
85
|
+
for (const watcher of watchers)
|
|
86
|
+
watcher.close();
|
|
87
|
+
watchers = [];
|
|
88
|
+
if (pollTimer)
|
|
89
|
+
clearInterval(pollTimer);
|
|
90
|
+
pollTimer = undefined;
|
|
91
|
+
if (debounceTimer)
|
|
92
|
+
clearTimeout(debounceTimer);
|
|
93
|
+
debounceTimer = undefined;
|
|
94
|
+
lastFingerprint = undefined;
|
|
95
|
+
activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
96
|
+
}
|
|
97
|
+
function watchTargets() {
|
|
98
|
+
return Array.from(new Set([
|
|
99
|
+
getAccountsDir(),
|
|
100
|
+
getLiveClaudeHome(),
|
|
101
|
+
path.join(os.homedir(), '.claude'),
|
|
102
|
+
getSystemCodexHome(),
|
|
103
|
+
]));
|
|
104
|
+
}
|
|
105
|
+
function scheduleCheck(reason) {
|
|
106
|
+
if (debounceTimer)
|
|
107
|
+
clearTimeout(debounceTimer);
|
|
108
|
+
debounceTimer = setTimeout(() => {
|
|
109
|
+
debounceTimer = undefined;
|
|
110
|
+
// Never force: watch targets include ~/.claude and the Codex home, whose
|
|
111
|
+
// history files churn on every prompt. Only a real status change may wake
|
|
112
|
+
// subscribers, or dormant quota pollers would poll on unrelated writes.
|
|
113
|
+
emitIfChanged(reason);
|
|
114
|
+
}, 25);
|
|
115
|
+
debounceTimer.unref?.();
|
|
116
|
+
}
|
|
117
|
+
function emitIfChanged(reason, force = false, singleListener) {
|
|
118
|
+
// External CLIs write the live homes directly: fold their logins into the
|
|
119
|
+
// saved profiles before reporting status, so a native `claude /login` is
|
|
120
|
+
// registered and its rotated tokens are backed up without a host round-trip.
|
|
121
|
+
// `local` signals come from our own registry writes and need no sync.
|
|
122
|
+
if (reason === 'filesystem' || reason === 'poll') {
|
|
123
|
+
try {
|
|
124
|
+
syncLiveAccountStateSync({ reason: reason === 'poll' ? 'poll' : 'watch' });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Sync problems surface through the health model, never through the watcher.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const status = getActiveAccountStatus();
|
|
131
|
+
const nextFingerprint = fingerprint(status);
|
|
132
|
+
if (!force && nextFingerprint === lastFingerprint)
|
|
133
|
+
return;
|
|
134
|
+
lastFingerprint = nextFingerprint;
|
|
135
|
+
const event = { reason, status };
|
|
136
|
+
const targets = singleListener ? [singleListener] : [...listeners];
|
|
137
|
+
for (const listener of targets) {
|
|
138
|
+
try {
|
|
139
|
+
listener(event);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A host callback cannot stop monitoring.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function fingerprint(status) {
|
|
147
|
+
return JSON.stringify(status);
|
|
148
|
+
}
|
|
149
|
+
/** Internal bridge for account mutations outside the registry module. */
|
|
150
|
+
export function _notifyAccountsChanged() {
|
|
151
|
+
_emitAccountsChanged('local');
|
|
152
|
+
}
|