sidekick-shared 0.23.0 → 0.24.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/README.md +139 -57
- package/dist/accountManager.js +10 -0
- package/dist/aggregation/EventAggregator.d.ts +8 -1
- package/dist/aggregation/EventAggregator.js +82 -136
- package/dist/aggregation/snapshot.d.ts +1 -1
- package/dist/aggregation/snapshot.js +2 -1
- package/dist/aggregation/types.d.ts +5 -0
- package/dist/autoSwitch.d.ts +1 -1
- package/dist/autoSwitch.js +14 -3
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +10 -1
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +27 -0
- package/dist/codexQuotaWatcher.d.ts +7 -0
- package/dist/codexQuotaWatcher.js +37 -19
- package/dist/context/sessionContext.js +43 -15
- package/dist/credentialIO.js +19 -5
- package/dist/errorHistory.d.ts +1 -0
- package/dist/errorHistory.js +3 -2
- package/dist/extractors/tokenUsage.js +1 -1
- package/dist/extractors/toolCall.js +2 -2
- package/dist/index.d.ts +14 -4
- package/dist/index.js +25 -7
- package/dist/modelInfo.d.ts +17 -7
- package/dist/modelInfo.js +11 -2
- package/dist/multiProviderQuotaService.js +13 -2
- package/dist/node.d.ts +4 -0
- package/dist/node.js +8 -1
- package/dist/observedSessionCollector.d.ts +68 -0
- package/dist/observedSessionCollector.js +178 -0
- package/dist/parsers/codexParser.js +24 -6
- package/dist/parsers/openCodeParser.js +22 -1
- package/dist/parsers/sessionActivityDetector.js +2 -0
- package/dist/parsers/sessionPathResolver.js +19 -16
- package/dist/parsers/subagentScanner.d.ts +1 -1
- package/dist/parsers/subagentScanner.js +4 -5
- package/dist/parsers/subagentTraceParser.js +15 -12
- package/dist/providerStatus.js +13 -2
- package/dist/providers/claudeCode.js +63 -5
- package/dist/providers/codex.js +56 -17
- package/dist/providers/detect.js +32 -3
- package/dist/providers/openCodeDatabase.js +12 -3
- package/dist/quota.js +8 -0
- package/dist/quotaPoller.js +17 -6
- package/dist/quotaSnapshots.js +80 -26
- package/dist/readers/decisions.js +3 -1
- package/dist/readers/handoff.js +1 -1
- package/dist/readers/notes.js +9 -4
- package/dist/readers/plans.js +2 -4
- package/dist/readers/tasks.js +3 -1
- package/dist/report/htmlHelpers.js +6 -1
- package/dist/report/openBrowser.js +3 -3
- package/dist/report/transcriptParser.d.ts +3 -11
- package/dist/report/transcriptParser.js +69 -154
- package/dist/schemas/index.d.ts +7 -1
- package/dist/schemas/index.js +18 -2
- package/dist/schemas/observedSessionV1.d.ts +32 -1
- package/dist/schemas/observedSessionV1.js +11 -1
- package/dist/schemas/sessionEvent.d.ts +353 -12
- package/dist/schemas/sessionEvent.js +76 -14
- package/dist/schemas/tokenEstimation.d.ts +14 -0
- package/dist/schemas/tokenEstimation.js +10 -0
- package/dist/schemas/transcript.d.ts +24 -0
- package/dist/schemas/transcript.js +81 -0
- package/dist/schemas/usageNormalization.d.ts +55 -0
- package/dist/schemas/usageNormalization.js +41 -0
- package/dist/sessionTranscripts.d.ts +16 -0
- package/dist/sessionTranscripts.js +47 -0
- package/dist/statusline/formatter.js +8 -1
- package/dist/terminalSync.js +16 -7
- package/dist/tokenEstimation.d.ts +24 -0
- package/dist/tokenEstimation.js +62 -0
- package/dist/transcript.d.ts +92 -0
- package/dist/transcript.js +240 -0
- package/dist/types/historicalData.d.ts +14 -0
- package/dist/types/historicalData.js +22 -0
- package/dist/types/observedSessionV1.d.ts +3 -1
- package/dist/types/observedSessionV1.js +20 -6
- package/dist/types/sessionEvent.d.ts +66 -7
- package/dist/usageNormalization.d.ts +69 -0
- package/dist/usageNormalization.js +100 -0
- package/dist/watchers/factory.js +14 -3
- package/dist/watchers/jsonlTail.js +34 -14
- package/dist/watchers/jsonlWatcher.js +5 -3
- package/dist/watchers/providerReaderWatcher.js +4 -2
- package/dist/watchers/sqliteWatcher.d.ts +2 -0
- package/dist/watchers/sqliteWatcher.js +27 -14
- package/dist/writers/atomic.js +100 -13
- package/dist/zaiQuotaWatcher.js +6 -0
- package/package.json +1 -1
|
@@ -46,11 +46,14 @@ exports.ClaudeCodeProvider = void 0;
|
|
|
46
46
|
const fs = __importStar(require("fs"));
|
|
47
47
|
const os = __importStar(require("os"));
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
|
+
const string_decoder_1 = require("string_decoder");
|
|
49
50
|
const sessionContext_1 = require("../context/sessionContext");
|
|
50
51
|
const jsonl_1 = require("../parsers/jsonl");
|
|
51
52
|
const sessionPathResolver_1 = require("../parsers/sessionPathResolver");
|
|
52
53
|
const subagentScanner_1 = require("../parsers/subagentScanner");
|
|
53
54
|
const modelContext_1 = require("../modelContext");
|
|
55
|
+
const sessionEvent_1 = require("../schemas/sessionEvent");
|
|
56
|
+
const usageNormalization_1 = require("../usageNormalization");
|
|
54
57
|
/** Type guard for content blocks with a `type` string property */
|
|
55
58
|
function isTypedBlock(block) {
|
|
56
59
|
return (block !== null &&
|
|
@@ -97,10 +100,15 @@ class ClaudeCodeReader {
|
|
|
97
100
|
filePosition = 0;
|
|
98
101
|
events = [];
|
|
99
102
|
_wasTruncated = false;
|
|
103
|
+
decoder = new string_decoder_1.StringDecoder('utf8');
|
|
100
104
|
constructor(sessionPath) {
|
|
101
105
|
this.sessionPath = sessionPath;
|
|
102
106
|
this.parser = new jsonl_1.JsonlParser({
|
|
103
|
-
onEvent: (
|
|
107
|
+
onEvent: (raw) => {
|
|
108
|
+
for (const event of (0, sessionEvent_1.extractSessionEvents)(raw)) {
|
|
109
|
+
this.events.push(normalizeClaudeUsage(event));
|
|
110
|
+
}
|
|
111
|
+
},
|
|
104
112
|
onError: (_err, _line) => {
|
|
105
113
|
// Silently skip parse errors — no logging framework dependency
|
|
106
114
|
},
|
|
@@ -120,6 +128,7 @@ class ClaudeCodeReader {
|
|
|
120
128
|
this._wasTruncated = true;
|
|
121
129
|
this.filePosition = 0;
|
|
122
130
|
this.parser.reset();
|
|
131
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
123
132
|
}
|
|
124
133
|
// No new content
|
|
125
134
|
if (currentSize <= this.filePosition) {
|
|
@@ -129,11 +138,16 @@ class ClaudeCodeReader {
|
|
|
129
138
|
const fd = fs.openSync(this.sessionPath, 'r');
|
|
130
139
|
const bufferSize = currentSize - this.filePosition;
|
|
131
140
|
const buffer = Buffer.alloc(bufferSize);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
let bytesRead = 0;
|
|
142
|
+
try {
|
|
143
|
+
bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
fs.closeSync(fd);
|
|
147
|
+
}
|
|
148
|
+
const chunk = this.decoder.write(buffer.subarray(0, bytesRead));
|
|
135
149
|
this.parser.processChunk(chunk);
|
|
136
|
-
this.filePosition
|
|
150
|
+
this.filePosition += bytesRead;
|
|
137
151
|
}
|
|
138
152
|
catch (error) {
|
|
139
153
|
console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
|
|
@@ -147,13 +161,18 @@ class ClaudeCodeReader {
|
|
|
147
161
|
reset() {
|
|
148
162
|
this.filePosition = 0;
|
|
149
163
|
this.parser.reset();
|
|
164
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
150
165
|
this._wasTruncated = false;
|
|
151
166
|
}
|
|
152
167
|
exists() {
|
|
153
168
|
return fs.existsSync(this.sessionPath);
|
|
154
169
|
}
|
|
155
170
|
flush() {
|
|
171
|
+
const finalChunk = this.decoder.end();
|
|
172
|
+
if (finalChunk)
|
|
173
|
+
this.parser.processChunk(finalChunk);
|
|
156
174
|
this.parser.flush();
|
|
175
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
157
176
|
}
|
|
158
177
|
getPosition() {
|
|
159
178
|
return this.filePosition;
|
|
@@ -161,11 +180,50 @@ class ClaudeCodeReader {
|
|
|
161
180
|
seekTo(position) {
|
|
162
181
|
this.filePosition = position;
|
|
163
182
|
this.parser.reset();
|
|
183
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
164
184
|
}
|
|
165
185
|
wasTruncated() {
|
|
166
186
|
return this._wasTruncated;
|
|
167
187
|
}
|
|
168
188
|
}
|
|
189
|
+
function normalizeClaudeUsage(event) {
|
|
190
|
+
const message = event.message;
|
|
191
|
+
const usage = message?.usage;
|
|
192
|
+
if (!message || !usage) {
|
|
193
|
+
return {
|
|
194
|
+
...event,
|
|
195
|
+
providerMetadata: {
|
|
196
|
+
...event.providerMetadata,
|
|
197
|
+
providerId: 'claude-code',
|
|
198
|
+
source: 'claude-code-jsonl',
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
...event,
|
|
204
|
+
message: {
|
|
205
|
+
...message,
|
|
206
|
+
normalizedUsage: (0, usageNormalization_1.normalizeProviderUsage)({
|
|
207
|
+
semantics: 'anthropic',
|
|
208
|
+
provider: 'anthropic',
|
|
209
|
+
source: 'claude-code-jsonl',
|
|
210
|
+
model: message.model,
|
|
211
|
+
inputTokens: usage.input_tokens,
|
|
212
|
+
outputTokens: usage.output_tokens,
|
|
213
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
214
|
+
cacheWriteTokens: usage.cache_creation_input_tokens,
|
|
215
|
+
reasoningTokens: usage.reasoning_tokens,
|
|
216
|
+
reasoningIncludedInOutput: false,
|
|
217
|
+
reportedCostUsd: usage.reported_cost,
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
providerMetadata: {
|
|
221
|
+
...event.providerMetadata,
|
|
222
|
+
providerId: 'claude-code',
|
|
223
|
+
source: 'claude-code-jsonl',
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
169
227
|
/**
|
|
170
228
|
* Session provider for Claude Code CLI.
|
|
171
229
|
*
|
package/dist/providers/codex.js
CHANGED
|
@@ -46,12 +46,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
46
46
|
exports.CodexProvider = void 0;
|
|
47
47
|
const fs = __importStar(require("fs"));
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
|
+
const string_decoder_1 = require("string_decoder");
|
|
49
50
|
const EventAggregator_1 = require("../aggregation/EventAggregator");
|
|
50
51
|
const sessionContext_1 = require("../context/sessionContext");
|
|
51
52
|
const codexParser_1 = require("../parsers/codexParser");
|
|
52
53
|
const codexDatabase_1 = require("./codexDatabase");
|
|
53
54
|
const codexProfiles_1 = require("../codexProfiles");
|
|
54
55
|
const modelContext_1 = require("../modelContext");
|
|
56
|
+
const usageNormalization_1 = require("../usageNormalization");
|
|
55
57
|
// ---------------------------------------------------------------------------
|
|
56
58
|
// Helper functions
|
|
57
59
|
// ---------------------------------------------------------------------------
|
|
@@ -131,6 +133,23 @@ function findRolloutFiles(dir) {
|
|
|
131
133
|
}
|
|
132
134
|
return results;
|
|
133
135
|
}
|
|
136
|
+
function hasRolloutFile(dir) {
|
|
137
|
+
try {
|
|
138
|
+
if (!fs.existsSync(dir))
|
|
139
|
+
return false;
|
|
140
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
141
|
+
const fullPath = path.join(dir, entry.name);
|
|
142
|
+
if (entry.isDirectory() && hasRolloutFile(fullPath))
|
|
143
|
+
return true;
|
|
144
|
+
if (entry.isFile() && isRolloutFile(entry.name))
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
134
153
|
function findRolloutFilesInConfiguredHomes() {
|
|
135
154
|
const results = [];
|
|
136
155
|
const seen = new Set();
|
|
@@ -148,7 +167,7 @@ function findRolloutFilesInConfiguredHomes() {
|
|
|
148
167
|
function getSessionsDir() {
|
|
149
168
|
const dirs = getSessionsDirs();
|
|
150
169
|
for (const dir of dirs) {
|
|
151
|
-
if (
|
|
170
|
+
if (hasRolloutFile(dir)) {
|
|
152
171
|
return dir;
|
|
153
172
|
}
|
|
154
173
|
}
|
|
@@ -430,6 +449,7 @@ class CodexReader {
|
|
|
430
449
|
filePosition = 0;
|
|
431
450
|
lineBuffer = '';
|
|
432
451
|
_wasTruncated = false;
|
|
452
|
+
decoder = new string_decoder_1.StringDecoder('utf8');
|
|
433
453
|
constructor(rolloutPath, onContextWindowLimit, onRateLimits, onTokenUsage) {
|
|
434
454
|
this.rolloutPath = rolloutPath;
|
|
435
455
|
this.onContextWindowLimit = onContextWindowLimit;
|
|
@@ -455,6 +475,7 @@ class CodexReader {
|
|
|
455
475
|
this.filePosition = 0;
|
|
456
476
|
this.lineBuffer = '';
|
|
457
477
|
this.parser.reset();
|
|
478
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
458
479
|
}
|
|
459
480
|
// No new content
|
|
460
481
|
if (currentSize <= this.filePosition)
|
|
@@ -463,10 +484,15 @@ class CodexReader {
|
|
|
463
484
|
const fd = fs.openSync(this.rolloutPath, 'r');
|
|
464
485
|
const bufferSize = currentSize - this.filePosition;
|
|
465
486
|
const buffer = Buffer.alloc(bufferSize);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
487
|
+
let bytesRead = 0;
|
|
488
|
+
try {
|
|
489
|
+
bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
fs.closeSync(fd);
|
|
493
|
+
}
|
|
494
|
+
const chunk = this.decoder.write(buffer.subarray(0, bytesRead));
|
|
495
|
+
this.filePosition += bytesRead;
|
|
470
496
|
// Process lines
|
|
471
497
|
const text = this.lineBuffer + chunk;
|
|
472
498
|
const lines = text.split('\n');
|
|
@@ -497,12 +523,22 @@ class CodexReader {
|
|
|
497
523
|
// Propagate last token usage snapshot to the provider
|
|
498
524
|
const lastUsage = this.parser.getLastTokenUsage();
|
|
499
525
|
if (lastUsage && this.onTokenUsage) {
|
|
526
|
+
const normalized = (0, usageNormalization_1.normalizeProviderUsage)({
|
|
527
|
+
semantics: 'openai',
|
|
528
|
+
provider: 'openai',
|
|
529
|
+
source: 'codex-reader-snapshot',
|
|
530
|
+
model: this.parser.getCurrentModel() || undefined,
|
|
531
|
+
inputTokens: lastUsage.input_tokens,
|
|
532
|
+
outputTokens: lastUsage.output_tokens,
|
|
533
|
+
cacheReadTokens: lastUsage.cached_input_tokens,
|
|
534
|
+
reasoningTokens: lastUsage.reasoning_output_tokens,
|
|
535
|
+
});
|
|
500
536
|
this.onTokenUsage({
|
|
501
|
-
inputTokens:
|
|
502
|
-
outputTokens:
|
|
503
|
-
cacheWriteTokens:
|
|
504
|
-
cacheReadTokens:
|
|
505
|
-
reasoningTokens:
|
|
537
|
+
inputTokens: normalized.uncachedInputTokens,
|
|
538
|
+
outputTokens: normalized.outputTokens,
|
|
539
|
+
cacheWriteTokens: normalized.cacheWriteTokens,
|
|
540
|
+
cacheReadTokens: normalized.cacheReadTokens,
|
|
541
|
+
reasoningTokens: normalized.reasoningTokens,
|
|
506
542
|
model: this.parser.getCurrentModel() || 'unknown',
|
|
507
543
|
timestamp: new Date(),
|
|
508
544
|
});
|
|
@@ -521,12 +557,14 @@ class CodexReader {
|
|
|
521
557
|
this.filePosition = 0;
|
|
522
558
|
this.lineBuffer = '';
|
|
523
559
|
this.parser.reset();
|
|
560
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
524
561
|
this._wasTruncated = false;
|
|
525
562
|
}
|
|
526
563
|
exists() {
|
|
527
564
|
return fs.existsSync(this.rolloutPath);
|
|
528
565
|
}
|
|
529
566
|
flush() {
|
|
567
|
+
this.lineBuffer += this.decoder.end();
|
|
530
568
|
// Process any remaining data in the line buffer
|
|
531
569
|
if (this.lineBuffer.trim()) {
|
|
532
570
|
try {
|
|
@@ -537,6 +575,7 @@ class CodexReader {
|
|
|
537
575
|
// Not a complete line yet
|
|
538
576
|
}
|
|
539
577
|
}
|
|
578
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
540
579
|
}
|
|
541
580
|
getPosition() {
|
|
542
581
|
return this.filePosition;
|
|
@@ -544,6 +583,7 @@ class CodexReader {
|
|
|
544
583
|
seekTo(position) {
|
|
545
584
|
this.filePosition = position;
|
|
546
585
|
this.lineBuffer = '';
|
|
586
|
+
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
547
587
|
}
|
|
548
588
|
wasTruncated() {
|
|
549
589
|
return this._wasTruncated;
|
|
@@ -675,9 +715,10 @@ class CodexProvider {
|
|
|
675
715
|
const db = this.ensureDb();
|
|
676
716
|
if (db) {
|
|
677
717
|
const cwdStats = db.getAllDistinctCwds();
|
|
718
|
+
const sessionsDir = getSessionsDir();
|
|
678
719
|
for (const stat of cwdStats) {
|
|
679
720
|
seenCwds.set(stat.cwd, {
|
|
680
|
-
dir:
|
|
721
|
+
dir: sessionsDir,
|
|
681
722
|
name: stat.cwd,
|
|
682
723
|
encodedName: stat.cwd,
|
|
683
724
|
sessionCount: stat.count,
|
|
@@ -837,7 +878,7 @@ class CodexProvider {
|
|
|
837
878
|
}
|
|
838
879
|
}
|
|
839
880
|
// Accumulate tokens from usage events
|
|
840
|
-
if (event.message
|
|
881
|
+
if (event.message?.usage) {
|
|
841
882
|
stats.inputTokens += event.message.usage.input_tokens || 0;
|
|
842
883
|
stats.outputTokens += event.message.usage.output_tokens || 0;
|
|
843
884
|
}
|
|
@@ -971,11 +1012,9 @@ class CodexProvider {
|
|
|
971
1012
|
return this.lastTokenUsageData;
|
|
972
1013
|
}
|
|
973
1014
|
computeContextSize(usage) {
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
|
|
977
|
-
// cacheWrite + cacheRead because Anthropic reports them separately.
|
|
978
|
-
return usage.inputTokens;
|
|
1015
|
+
// Codex usage is normalized to the disjoint TokenUsage shape at the reader
|
|
1016
|
+
// boundary, so reconstruct the full prompt/context from uncached + cached.
|
|
1017
|
+
return usage.inputTokens + usage.cacheReadTokens;
|
|
979
1018
|
}
|
|
980
1019
|
// --- Rate limits (for vscode wrapper to build QuotaState) ---
|
|
981
1020
|
/** Get the last rate limits data received from token_count events. */
|
package/dist/providers/detect.js
CHANGED
|
@@ -100,6 +100,35 @@ function getMostRecentMtime(dir) {
|
|
|
100
100
|
return 0;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
function getMostRecentFileMtime(dir, isCandidate, depth) {
|
|
104
|
+
if (depth < 0)
|
|
105
|
+
return 0;
|
|
106
|
+
let latest = 0;
|
|
107
|
+
try {
|
|
108
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
109
|
+
const filePath = path.join(dir, entry);
|
|
110
|
+
try {
|
|
111
|
+
const stats = fs.statSync(filePath);
|
|
112
|
+
if (stats.isDirectory?.()) {
|
|
113
|
+
latest = Math.max(latest, getMostRecentFileMtime(filePath, isCandidate, depth - 1));
|
|
114
|
+
}
|
|
115
|
+
else if (isCandidate(filePath)) {
|
|
116
|
+
latest = Math.max(latest, stats.mtime.getTime());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* skip */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
return latest;
|
|
128
|
+
}
|
|
129
|
+
function getClaudeActivityMtime(claudeBase) {
|
|
130
|
+
return getMostRecentFileMtime(claudeBase, (filePath) => filePath.endsWith('.jsonl'), 2);
|
|
131
|
+
}
|
|
103
132
|
function getOpenCodeActivityMtime() {
|
|
104
133
|
const dataDir = (0, openCode_1.getOpenCodeDataDir)();
|
|
105
134
|
const dbPath = path.join(dataDir, 'opencode.db');
|
|
@@ -123,7 +152,7 @@ function getCodexActivityMtime() {
|
|
|
123
152
|
const dbMtime = getCodexStateDbMtime(codexHome);
|
|
124
153
|
if (dbMtime > latest)
|
|
125
154
|
latest = dbMtime;
|
|
126
|
-
const sessionsMtime =
|
|
155
|
+
const sessionsMtime = getMostRecentFileMtime(path.join(codexHome, 'sessions'), (filePath) => path.basename(filePath).startsWith('rollout-') && filePath.endsWith('.jsonl'), 4);
|
|
127
156
|
if (sessionsMtime > latest)
|
|
128
157
|
latest = sessionsMtime;
|
|
129
158
|
}
|
|
@@ -151,7 +180,7 @@ function getAllDetectedProviders() {
|
|
|
151
180
|
const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
|
|
152
181
|
const available = [];
|
|
153
182
|
if (hasClaude)
|
|
154
|
-
available.push({ id: 'claude-code', mtime:
|
|
183
|
+
available.push({ id: 'claude-code', mtime: getClaudeActivityMtime(claudeBase) });
|
|
155
184
|
if (hasOpenCode)
|
|
156
185
|
available.push({ id: 'opencode', mtime: getOpenCodeActivityMtime() });
|
|
157
186
|
if (hasCodex)
|
|
@@ -172,7 +201,7 @@ function detectProvider(override) {
|
|
|
172
201
|
const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
|
|
173
202
|
const available = [];
|
|
174
203
|
if (hasClaude) {
|
|
175
|
-
available.push({ id: 'claude-code', mtime:
|
|
204
|
+
available.push({ id: 'claude-code', mtime: getClaudeActivityMtime(claudeBase) });
|
|
176
205
|
}
|
|
177
206
|
if (hasOpenCode) {
|
|
178
207
|
available.push({ id: 'opencode', mtime: getOpenCodeActivityMtime() });
|
|
@@ -64,8 +64,12 @@ class OpenCodeDatabase {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
open() {
|
|
67
|
-
if (this.runtimeStatus)
|
|
68
|
-
return
|
|
67
|
+
if (this.runtimeStatus?.kind === 'available')
|
|
68
|
+
return true;
|
|
69
|
+
if (this.runtimeStatus &&
|
|
70
|
+
['db_missing', 'sqlite_missing', 'sqlite_blocked'].includes(this.runtimeStatus.kind)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
69
73
|
if (!this.isAvailable()) {
|
|
70
74
|
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
71
75
|
return false;
|
|
@@ -114,7 +118,12 @@ class OpenCodeDatabase {
|
|
|
114
118
|
return JSON.parse(trimmed);
|
|
115
119
|
}
|
|
116
120
|
catch (error) {
|
|
117
|
-
this.runtimeStatus =
|
|
121
|
+
this.runtimeStatus = {
|
|
122
|
+
...toRuntimeStatus(error, 'query_failed'),
|
|
123
|
+
// The database and sqlite binary were successfully opened. A failed
|
|
124
|
+
// query is transient and must not gate the next attempt.
|
|
125
|
+
available: true,
|
|
126
|
+
};
|
|
118
127
|
return [];
|
|
119
128
|
}
|
|
120
129
|
}
|
package/dist/quota.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.withQuotaProjections = withQuotaProjections;
|
|
|
12
12
|
exports.fetchQuota = fetchQuota;
|
|
13
13
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
14
14
|
const BETA_HEADER = 'oauth-2025-04-20';
|
|
15
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
15
16
|
exports.FIVE_HOUR_WINDOW_MS = 5 * 3_600_000;
|
|
16
17
|
exports.SEVEN_DAY_WINDOW_MS = 7 * 86_400_000;
|
|
17
18
|
/**
|
|
@@ -94,9 +95,13 @@ function parseRetryAfterMs(retryAfter) {
|
|
|
94
95
|
* @returns QuotaState with utilization data and elapsed-time projections
|
|
95
96
|
*/
|
|
96
97
|
async function fetchQuota(accessToken) {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
100
|
+
timeout.unref();
|
|
97
101
|
try {
|
|
98
102
|
const res = await fetch(USAGE_URL, {
|
|
99
103
|
method: 'GET',
|
|
104
|
+
signal: controller.signal,
|
|
100
105
|
headers: {
|
|
101
106
|
Authorization: `Bearer ${accessToken}`,
|
|
102
107
|
'anthropic-beta': BETA_HEADER,
|
|
@@ -152,4 +157,7 @@ async function fetchQuota(accessToken) {
|
|
|
152
157
|
catch {
|
|
153
158
|
return unavailableState('Network error', { failureKind: 'network' });
|
|
154
159
|
}
|
|
160
|
+
finally {
|
|
161
|
+
clearTimeout(timeout);
|
|
162
|
+
}
|
|
155
163
|
}
|
package/dist/quotaPoller.js
CHANGED
|
@@ -111,10 +111,12 @@ class QuotaPoller {
|
|
|
111
111
|
this.notify(state);
|
|
112
112
|
}
|
|
113
113
|
else if (state.failureKind === 'auth') {
|
|
114
|
-
//
|
|
114
|
+
// Keep polling at the idle cadence so signing in again recovers
|
|
115
|
+
// without an extension/CLI restart.
|
|
115
116
|
this.latest = state;
|
|
116
117
|
this.notify(state);
|
|
117
|
-
this.
|
|
118
|
+
this.consecutiveFailures = 0;
|
|
119
|
+
this.scheduleNext(this.idleIntervalMs);
|
|
118
120
|
return;
|
|
119
121
|
}
|
|
120
122
|
else {
|
|
@@ -134,17 +136,26 @@ class QuotaPoller {
|
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
|
-
catch {
|
|
139
|
+
catch (error) {
|
|
138
140
|
this.consecutiveFailures++;
|
|
141
|
+
const state = {
|
|
142
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
143
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
144
|
+
available: false,
|
|
145
|
+
error: error instanceof Error ? error.message : String(error),
|
|
146
|
+
failureKind: 'auth',
|
|
147
|
+
};
|
|
148
|
+
this.latest = state;
|
|
149
|
+
this.notify(state);
|
|
139
150
|
}
|
|
140
151
|
this.scheduleNext();
|
|
141
152
|
}
|
|
142
|
-
scheduleNext() {
|
|
153
|
+
scheduleNext(overrideMs) {
|
|
143
154
|
if (this.stopped)
|
|
144
155
|
return;
|
|
145
156
|
const baseInterval = this.isActive ? this.activeIntervalMs : this.idleIntervalMs;
|
|
146
|
-
const backoff = Math.min(baseInterval * Math.pow(2, this.consecutiveFailures), this.maxBackoffMs);
|
|
147
|
-
const delay = this.consecutiveFailures > 0 ? backoff : baseInterval;
|
|
157
|
+
const backoff = Math.min(baseInterval * Math.pow(2, this.consecutiveFailures), Math.max(this.maxBackoffMs, baseInterval));
|
|
158
|
+
const delay = overrideMs ?? (this.consecutiveFailures > 0 ? backoff : baseInterval);
|
|
148
159
|
this.timer = setTimeout(() => void this.poll(), delay);
|
|
149
160
|
}
|
|
150
161
|
notify(state) {
|
package/dist/quotaSnapshots.js
CHANGED
|
@@ -40,6 +40,10 @@ const fs = __importStar(require("fs"));
|
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const paths_1 = require("./paths");
|
|
42
42
|
const codex_1 = require("./types/codex");
|
|
43
|
+
const LOCK_WAIT_MS = 15;
|
|
44
|
+
const LOCK_STALE_MS = 10_000;
|
|
45
|
+
const LOCK_TIMEOUT_MS = 15_000;
|
|
46
|
+
const lockSleepView = new Int32Array(new SharedArrayBuffer(4));
|
|
43
47
|
function getQuotaSnapshotPath() {
|
|
44
48
|
return path.join((0, paths_1.getConfigDir)(), 'quota-snapshots.json');
|
|
45
49
|
}
|
|
@@ -84,6 +88,61 @@ function writeStore(store) {
|
|
|
84
88
|
ensureConfigDir();
|
|
85
89
|
atomicWriteJson(getQuotaSnapshotPath(), store);
|
|
86
90
|
}
|
|
91
|
+
function withSnapshotStoreLock(operation) {
|
|
92
|
+
ensureConfigDir();
|
|
93
|
+
const lockPath = `${getQuotaSnapshotPath()}.lock`;
|
|
94
|
+
const token = `${process.pid}:${crypto.randomBytes(16).toString('hex')}`;
|
|
95
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
96
|
+
while (true) {
|
|
97
|
+
try {
|
|
98
|
+
const descriptor = fs.openSync(lockPath, 'wx', 0o600);
|
|
99
|
+
fs.writeFileSync(descriptor, token, 'utf8');
|
|
100
|
+
fs.fsyncSync(descriptor);
|
|
101
|
+
fs.closeSync(descriptor);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code !== 'EEXIST')
|
|
106
|
+
throw error;
|
|
107
|
+
try {
|
|
108
|
+
const stat = fs.statSync(lockPath);
|
|
109
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
110
|
+
const owner = fs.readFileSync(lockPath, 'utf8');
|
|
111
|
+
const ownerPid = Number.parseInt(owner.split(':', 1)[0], 10);
|
|
112
|
+
let ownerAlive = Number.isInteger(ownerPid) && ownerPid > 0;
|
|
113
|
+
if (ownerAlive) {
|
|
114
|
+
try {
|
|
115
|
+
process.kill(ownerPid, 0);
|
|
116
|
+
}
|
|
117
|
+
catch (ownerError) {
|
|
118
|
+
ownerAlive = ownerError.code === 'EPERM';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!ownerAlive)
|
|
122
|
+
fs.rmSync(lockPath, { force: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// The owner may have released the lock between checks.
|
|
127
|
+
}
|
|
128
|
+
if (Date.now() >= deadline)
|
|
129
|
+
throw new Error(`Timed out waiting for quota lock: ${lockPath}`);
|
|
130
|
+
Atomics.wait(lockSleepView, 0, 0, LOCK_WAIT_MS);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return operation();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
try {
|
|
138
|
+
if (fs.readFileSync(lockPath, 'utf8') === token)
|
|
139
|
+
fs.rmSync(lockPath, { force: true });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Never remove a lock that has already been released or replaced.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
87
146
|
function snapshotTimeMs(quota) {
|
|
88
147
|
const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : NaN;
|
|
89
148
|
return Number.isFinite(capturedAt) ? capturedAt : 0;
|
|
@@ -119,32 +178,27 @@ function shouldKeepExistingSnapshot(existing, next) {
|
|
|
119
178
|
return snapshotTimeMs(existing) > snapshotTimeMs(next);
|
|
120
179
|
}
|
|
121
180
|
function writeQuotaSnapshot(providerId, accountId, quota) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
store.snapshots.push(record);
|
|
146
|
-
}
|
|
147
|
-
writeStore(store);
|
|
181
|
+
withSnapshotStoreLock(() => {
|
|
182
|
+
const store = readStore();
|
|
183
|
+
const index = store.snapshots.findIndex((item) => item.providerId === providerId && item.accountId === accountId);
|
|
184
|
+
const existingQuota = index >= 0 ? store.snapshots[index].quota : undefined;
|
|
185
|
+
const snapshot = {
|
|
186
|
+
...quota,
|
|
187
|
+
providerId,
|
|
188
|
+
capturedAt: quota.capturedAt ?? new Date().toISOString(),
|
|
189
|
+
source: quota.source ?? 'session',
|
|
190
|
+
stale: false,
|
|
191
|
+
resetCredits: quota.resetCredits ?? (providerId === 'codex' ? existingQuota?.resetCredits : undefined),
|
|
192
|
+
};
|
|
193
|
+
if (index >= 0 && shouldKeepExistingSnapshot(store.snapshots[index].quota, snapshot))
|
|
194
|
+
return;
|
|
195
|
+
const record = { providerId, accountId, quota: snapshot };
|
|
196
|
+
if (index >= 0)
|
|
197
|
+
store.snapshots[index] = record;
|
|
198
|
+
else
|
|
199
|
+
store.snapshots.push(record);
|
|
200
|
+
writeStore(store);
|
|
201
|
+
});
|
|
148
202
|
}
|
|
149
203
|
function readQuotaSnapshot(providerId, accountId) {
|
|
150
204
|
const store = readStore();
|
|
@@ -9,7 +9,9 @@ async function readDecisions(project, opts) {
|
|
|
9
9
|
const store = await (0, helpers_1.readProjectJsonStore)(project, 'decisions');
|
|
10
10
|
if (!store)
|
|
11
11
|
return [];
|
|
12
|
-
let decisions =
|
|
12
|
+
let decisions = store.decisions && typeof store.decisions === 'object' && !Array.isArray(store.decisions)
|
|
13
|
+
? Object.values(store.decisions)
|
|
14
|
+
: [];
|
|
13
15
|
// Sort by timestamp descending
|
|
14
16
|
decisions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
15
17
|
if (opts?.search) {
|
package/dist/readers/handoff.js
CHANGED
|
@@ -42,7 +42,7 @@ const paths_1 = require("../paths");
|
|
|
42
42
|
async function readLatestHandoff(project) {
|
|
43
43
|
const candidates = typeof project === 'string' ? [project] : project.candidates;
|
|
44
44
|
for (const slug of candidates) {
|
|
45
|
-
const filePath = (0, paths_1.getProjectDataPath)(slug, 'handoffs').replace(
|
|
45
|
+
const filePath = (0, paths_1.getProjectDataPath)(slug, 'handoffs').replace(/\.json$/, '-latest.md');
|
|
46
46
|
try {
|
|
47
47
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
48
48
|
if (content.trim())
|
package/dist/readers/notes.js
CHANGED
|
@@ -10,21 +10,26 @@ async function readNotes(project, opts) {
|
|
|
10
10
|
if (!store)
|
|
11
11
|
return [];
|
|
12
12
|
let notes = [];
|
|
13
|
+
const notesByFile = store.notesByFile && typeof store.notesByFile === 'object' && !Array.isArray(store.notesByFile)
|
|
14
|
+
? store.notesByFile
|
|
15
|
+
: {};
|
|
13
16
|
if (opts?.file) {
|
|
14
17
|
// Normalize file path for comparison
|
|
15
18
|
const normalizedFile = opts.file.replace(/\\/g, '/');
|
|
16
|
-
for (const [filePath, fileNotes] of Object.entries(
|
|
19
|
+
for (const [filePath, fileNotes] of Object.entries(notesByFile)) {
|
|
17
20
|
const normalizedKey = filePath.replace(/\\/g, '/');
|
|
18
21
|
if (normalizedKey === normalizedFile ||
|
|
19
22
|
normalizedKey.endsWith('/' + normalizedFile) ||
|
|
20
23
|
normalizedFile.endsWith('/' + normalizedKey)) {
|
|
21
|
-
|
|
24
|
+
if (Array.isArray(fileNotes))
|
|
25
|
+
notes.push(...fileNotes);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
else {
|
|
26
|
-
for (const fileNotes of Object.values(
|
|
27
|
-
|
|
30
|
+
for (const fileNotes of Object.values(notesByFile)) {
|
|
31
|
+
if (Array.isArray(fileNotes))
|
|
32
|
+
notes.push(...fileNotes);
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
if (opts?.type) {
|