sidekick-shared 0.21.2 → 0.21.4

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.
Files changed (77) hide show
  1. package/README.md +60 -45
  2. package/dist/accountManager.js +9 -7
  3. package/dist/accountRegistry.d.ts +17 -0
  4. package/dist/accountRegistry.js +18 -14
  5. package/dist/accountStatus.js +7 -7
  6. package/dist/accounts.d.ts +13 -0
  7. package/dist/accounts.js +110 -25
  8. package/dist/aggregation/EventAggregator.d.ts +14 -11
  9. package/dist/aggregation/EventAggregator.js +68 -31
  10. package/dist/aggregation/PatternExtractor.js +5 -4
  11. package/dist/autoSwitch.js +6 -12
  12. package/dist/claudeProfiles.js +3 -3
  13. package/dist/codexProfiles.d.ts +15 -1
  14. package/dist/codexProfiles.js +96 -22
  15. package/dist/codexQuota.js +44 -16
  16. package/dist/codexQuotaWatcher.js +19 -4
  17. package/dist/context/composer.js +8 -6
  18. package/dist/context/sessionContext.js +12 -9
  19. package/dist/credentialIO.js +10 -8
  20. package/dist/extractors/sessionAssets.js +9 -1
  21. package/dist/extractors/sources/claudeAssets.js +7 -1
  22. package/dist/extractors/sources/codexAssets.js +14 -3
  23. package/dist/extractors/toolCall.js +1 -4
  24. package/dist/formatters/eventHighlighter.js +148 -28
  25. package/dist/formatters/noiseClassifier.js +3 -11
  26. package/dist/formatters/sessionDump.js +22 -13
  27. package/dist/formatters/toolSummary.js +1 -3
  28. package/dist/index.d.ts +26 -26
  29. package/dist/index.js +10 -4
  30. package/dist/modelContext.js +10 -8
  31. package/dist/modelInfo.js +11 -6
  32. package/dist/multiProviderQuotaService.d.ts +2 -2
  33. package/dist/multiProviderQuotaService.js +14 -10
  34. package/dist/parsers/codexParser.js +170 -103
  35. package/dist/parsers/debugLogParser.js +5 -4
  36. package/dist/parsers/openCodeParser.js +66 -48
  37. package/dist/parsers/planExtractor.js +3 -3
  38. package/dist/parsers/sessionActivityDetector.js +10 -6
  39. package/dist/parsers/sessionPathResolver.js +29 -34
  40. package/dist/parsers/subagentScanner.js +21 -20
  41. package/dist/parsers/subagentTraceParser.js +7 -4
  42. package/dist/phrases.js +11 -11
  43. package/dist/providerStatus.js +3 -4
  44. package/dist/providers/claudeCode.js +3 -1
  45. package/dist/providers/codex.js +35 -14
  46. package/dist/providers/codexDatabase.js +14 -5
  47. package/dist/providers/detect.js +10 -6
  48. package/dist/providers/openCode.js +91 -63
  49. package/dist/providers/openCodeDatabase.js +44 -17
  50. package/dist/quota.d.ts +19 -0
  51. package/dist/quota.js +50 -8
  52. package/dist/quotaHistory.js +10 -3
  53. package/dist/quotaSnapshots.js +2 -2
  54. package/dist/readers/decisions.js +1 -1
  55. package/dist/readers/notes.js +5 -3
  56. package/dist/readers/plans.js +43 -23
  57. package/dist/readers/tasks.js +1 -1
  58. package/dist/report/htmlHelpers.js +135 -16
  59. package/dist/report/htmlReportGenerator.js +38 -12
  60. package/dist/report/transcriptParser.js +8 -2
  61. package/dist/schemas/index.d.ts +4 -4
  62. package/dist/schemas/quota.js +2 -10
  63. package/dist/schemas/sessionEvent.js +20 -10
  64. package/dist/search/advancedFilter.js +3 -3
  65. package/dist/search/sessionSearch.js +10 -4
  66. package/dist/terminalSync.js +4 -2
  67. package/dist/turns/assistantTurn.js +5 -1
  68. package/dist/watchers/eventBridge.js +56 -26
  69. package/dist/watchers/factory.js +5 -2
  70. package/dist/watchers/jsonlTail.js +3 -1
  71. package/dist/watchers/jsonlWatcher.js +128 -52
  72. package/dist/watchers/providerReaderWatcher.js +3 -1
  73. package/dist/watchers/sqliteWatcher.js +52 -22
  74. package/dist/zaiQuota.js +6 -10
  75. package/dist/zaiQuotaApi.js +13 -5
  76. package/dist/zaiQuotaWatcher.js +1 -1
  77. package/package.json +5 -2
@@ -82,7 +82,9 @@ function getOpenCodeDataDir() {
82
82
  candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'opencode'));
83
83
  }
84
84
  else if (process.platform === 'win32') {
85
- candidates.push(path.join(process.env.LOCALAPPDATA || process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'opencode'));
85
+ candidates.push(path.join(process.env.LOCALAPPDATA ||
86
+ process.env.APPDATA ||
87
+ path.join(os.homedir(), 'AppData', 'Local'), 'opencode'));
86
88
  }
87
89
  for (const candidate of candidates) {
88
90
  try {
@@ -99,7 +101,9 @@ function getOpenCodeDataDir() {
99
101
  return path.join(os.homedir(), 'Library', 'Application Support', 'opencode');
100
102
  }
101
103
  if (process.platform === 'win32') {
102
- return path.join(process.env.LOCALAPPDATA || process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'opencode');
104
+ return path.join(process.env.LOCALAPPDATA ||
105
+ process.env.APPDATA ||
106
+ path.join(os.homedir(), 'AppData', 'Local'), 'opencode');
103
107
  }
104
108
  return path.join(os.homedir(), '.local', 'share', 'opencode');
105
109
  }
@@ -132,7 +136,10 @@ function detectAgentTypeFromDescription(desc) {
132
136
  if (lower.includes('plan') || lower.includes('architect') || lower.includes('design')) {
133
137
  return 'Plan';
134
138
  }
135
- if (lower.includes('task') || lower.includes('execute') || lower.includes('implement') || lower.includes('build')) {
139
+ if (lower.includes('task') ||
140
+ lower.includes('execute') ||
141
+ lower.includes('implement') ||
142
+ lower.includes('build')) {
136
143
  return 'Task';
137
144
  }
138
145
  return undefined;
@@ -189,9 +196,7 @@ function extractRoleFromDbMessage(row) {
189
196
  function extractParentIdFromDbMessage(row) {
190
197
  try {
191
198
  const data = JSON.parse(row.data);
192
- return typeof data.parentID === 'string' && data.parentID.length > 0
193
- ? data.parentID
194
- : null;
199
+ return typeof data.parentID === 'string' && data.parentID.length > 0 ? data.parentID : null;
195
200
  }
196
201
  catch {
197
202
  return null;
@@ -224,7 +229,7 @@ function resolveProjectIdFromFiles(workspacePath) {
224
229
  const projectDir = path.join(getStorageDir(), 'project');
225
230
  if (!fs.existsSync(projectDir))
226
231
  return resolveProjectIdFromGit(workspacePath);
227
- const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.json'));
232
+ const files = fs.readdirSync(projectDir).filter((f) => f.endsWith('.json'));
228
233
  const matches = [];
229
234
  for (const file of files) {
230
235
  try {
@@ -263,9 +268,12 @@ function resolveProjectIdFromGit(workspacePath) {
263
268
  const hash = (0, child_process_1.execSync)('git rev-list --max-parents=0 HEAD', {
264
269
  cwd: workspacePath,
265
270
  encoding: 'utf-8',
266
- timeout: 5000,
267
- stdio: ['pipe', 'pipe', 'pipe']
268
- }).trim().split('\n')[0];
271
+ timeout: 4000,
272
+ killSignal: 'SIGKILL',
273
+ stdio: ['pipe', 'pipe', 'pipe'],
274
+ })
275
+ .trim()
276
+ .split('\n')[0];
269
277
  if (hash && /^[a-f0-9]+$/i.test(hash)) {
270
278
  return hash;
271
279
  }
@@ -325,7 +333,7 @@ class OpenCodeFileReader {
325
333
  return [];
326
334
  let messageFiles;
327
335
  try {
328
- messageFiles = fs.readdirSync(messageDir).filter(f => f.endsWith('.json'));
336
+ messageFiles = fs.readdirSync(messageDir).filter((f) => f.endsWith('.json'));
329
337
  }
330
338
  catch {
331
339
  return [];
@@ -348,16 +356,17 @@ class OpenCodeFileReader {
348
356
  let parts = [];
349
357
  if (fs.existsSync(partDir)) {
350
358
  try {
351
- parts = fs.readdirSync(partDir)
352
- .filter(f => f.endsWith('.json'))
353
- .map(f => readJsonSafe(path.join(partDir, f)))
359
+ parts = fs
360
+ .readdirSync(partDir)
361
+ .filter((f) => f.endsWith('.json'))
362
+ .map((f) => readJsonSafe(path.join(partDir, f)))
354
363
  .filter((p) => p !== null);
355
364
  }
356
365
  catch {
357
366
  // Skip unreadable part directories
358
367
  }
359
368
  }
360
- const partIds = new Set(parts.map(part => part.id));
369
+ const partIds = new Set(parts.map((part) => part.id));
361
370
  const previous = this.seenMessages.get(msgId);
362
371
  const isNewMessage = !previous;
363
372
  let hasNewParts = false;
@@ -463,23 +472,26 @@ class OpenCodeDbReader {
463
472
  messages.sort((a, b) => a.time_created - b.time_created);
464
473
  // Filter user messages that haven't been processed yet
465
474
  const userMessageIds = messages
466
- .filter(m => extractRoleFromDbMessage(m) === 'user')
467
- .map(m => m.id);
475
+ .filter((m) => extractRoleFromDbMessage(m) === 'user')
476
+ .map((m) => m.id);
468
477
  const processedUserMessageIds = new Set(this.db.getProcessedUserMessageIds(this.sessionId, userMessageIds));
469
478
  for (const msgRow of messages) {
470
479
  try {
471
- if (extractRoleFromDbMessage(msgRow) === 'user' && !processedUserMessageIds.has(msgRow.id)) {
480
+ if (extractRoleFromDbMessage(msgRow) === 'user' &&
481
+ !processedUserMessageIds.has(msgRow.id)) {
472
482
  continue;
473
483
  }
474
484
  const message = (0, openCodeParser_1.parseDbMessageData)(msgRow);
475
- const msgParts = (partsByMessage.get(msgRow.id) || []).map(row => {
485
+ const msgParts = (partsByMessage.get(msgRow.id) || [])
486
+ .map((row) => {
476
487
  try {
477
488
  return (0, openCodeParser_1.parseDbPartData)(row);
478
489
  }
479
490
  catch {
480
491
  return null;
481
492
  }
482
- }).filter((p) => p !== null);
493
+ })
494
+ .filter((p) => p !== null);
483
495
  events.push(...(0, openCodeParser_1.convertOpenCodeMessage)(message, msgParts));
484
496
  }
485
497
  catch {
@@ -512,7 +524,7 @@ class OpenCodeDbReader {
512
524
  return [];
513
525
  }
514
526
  // Build set of affected message IDs from both message and part changes
515
- const affectedMessageIds = new Set(messages.map(m => m.id));
527
+ const affectedMessageIds = new Set(messages.map((m) => m.id));
516
528
  for (const part of parts) {
517
529
  affectedMessageIds.add(part.message_id);
518
530
  }
@@ -525,21 +537,21 @@ class OpenCodeDbReader {
525
537
  }
526
538
  }
527
539
  // Fetch missing message rows for part-only updates
528
- const messageMap = new Map(messages.map(m => [m.id, m]));
529
- const missingMessageIds = [...affectedMessageIds].filter(id => !messageMap.has(id));
540
+ const messageMap = new Map(messages.map((m) => [m.id, m]));
541
+ const missingMessageIds = [...affectedMessageIds].filter((id) => !messageMap.has(id));
530
542
  if (missingMessageIds.length > 0) {
531
543
  const missingRows = this.db.getMessagesByIds(this.sessionId, missingMessageIds);
532
544
  for (const row of missingRows) {
533
545
  messageMap.set(row.id, row);
534
546
  }
535
- const unresolved = missingMessageIds.filter(id => !messageMap.has(id));
547
+ const unresolved = missingMessageIds.filter((id) => !messageMap.has(id));
536
548
  if (unresolved.length > 0) {
537
549
  // Keep cursor unchanged so we can retry on next polling cycle.
538
550
  return [];
539
551
  }
540
552
  }
541
553
  const targetMessages = [...messageMap.values()];
542
- const targetMessageIds = targetMessages.map(m => m.id);
554
+ const targetMessageIds = targetMessages.map((m) => m.id);
543
555
  // Fetch complete part sets for all affected messages in one batched query
544
556
  const allParts = this.db.getPartsForMessages(this.sessionId, targetMessageIds);
545
557
  const partsByMessage = new Map();
@@ -566,23 +578,26 @@ class OpenCodeDbReader {
566
578
  // Sort messages by creation time
567
579
  targetMessages.sort((a, b) => a.time_created - b.time_created);
568
580
  const userMessageIds = targetMessages
569
- .filter(m => extractRoleFromDbMessage(m) === 'user')
570
- .map(m => m.id);
581
+ .filter((m) => extractRoleFromDbMessage(m) === 'user')
582
+ .map((m) => m.id);
571
583
  const processedUserMessageIds = new Set(this.db.getProcessedUserMessageIds(this.sessionId, userMessageIds));
572
584
  for (const msgRow of targetMessages) {
573
585
  try {
574
- if (extractRoleFromDbMessage(msgRow) === 'user' && !processedUserMessageIds.has(msgRow.id)) {
586
+ if (extractRoleFromDbMessage(msgRow) === 'user' &&
587
+ !processedUserMessageIds.has(msgRow.id)) {
575
588
  continue;
576
589
  }
577
590
  const message = (0, openCodeParser_1.parseDbMessageData)(msgRow);
578
- const msgParts = (partsByMessage.get(msgRow.id) || []).map(row => {
591
+ const msgParts = (partsByMessage.get(msgRow.id) || [])
592
+ .map((row) => {
579
593
  try {
580
594
  return (0, openCodeParser_1.parseDbPartData)(row);
581
595
  }
582
596
  catch {
583
597
  return null;
584
598
  }
585
- }).filter((p) => p !== null);
599
+ })
600
+ .filter((p) => p !== null);
586
601
  events.push(...(0, openCodeParser_1.convertOpenCodeMessage)(message, msgParts));
587
602
  }
588
603
  catch {
@@ -673,7 +688,7 @@ class OpenCodeProvider {
673
688
  return [];
674
689
  const sessions = db.getSessionsForProject(projectId);
675
690
  const dataDir = getOpenCodeDataDir();
676
- return sessions.map(session => {
691
+ return sessions.map((session) => {
677
692
  const syntheticPath = makeDbSessionPath(dataDir, projectId, session.id);
678
693
  this.sessionMetaCache.set(syntheticPath, {
679
694
  title: session.title,
@@ -750,7 +765,7 @@ class OpenCodeProvider {
750
765
  let bestPath = null;
751
766
  let bestMtime = 0;
752
767
  try {
753
- const files = fs.readdirSync(sessionDir).filter(f => f.endsWith('.json'));
768
+ const files = fs.readdirSync(sessionDir).filter((f) => f.endsWith('.json'));
754
769
  for (const file of files) {
755
770
  const fullPath = path.join(sessionDir, file);
756
771
  try {
@@ -773,7 +788,7 @@ class OpenCodeProvider {
773
788
  const messageDir = path.join(getStorageDir(), 'message', sessionId);
774
789
  if (fs.existsSync(messageDir)) {
775
790
  try {
776
- const messageFiles = fs.readdirSync(messageDir).filter(f => f.endsWith('.json'));
791
+ const messageFiles = fs.readdirSync(messageDir).filter((f) => f.endsWith('.json'));
777
792
  for (const mf of messageFiles) {
778
793
  try {
779
794
  const mstat = fs.statSync(path.join(messageDir, mf));
@@ -843,9 +858,10 @@ class OpenCodeProvider {
843
858
  try {
844
859
  if (!fs.existsSync(dir))
845
860
  return [];
846
- return fs.readdirSync(dir)
847
- .filter(f => f.endsWith('.json'))
848
- .map(f => {
861
+ return fs
862
+ .readdirSync(dir)
863
+ .filter((f) => f.endsWith('.json'))
864
+ .map((f) => {
849
865
  const fullPath = path.join(dir, f);
850
866
  try {
851
867
  const stats = fs.statSync(fullPath);
@@ -857,7 +873,7 @@ class OpenCodeProvider {
857
873
  })
858
874
  .filter((f) => f !== null && f.size > 0)
859
875
  .sort((a, b) => b.mtime - a.mtime)
860
- .map(f => f.path);
876
+ .map((f) => f.path);
861
877
  }
862
878
  catch {
863
879
  return [];
@@ -871,7 +887,7 @@ class OpenCodeProvider {
871
887
  if (db) {
872
888
  const projects = db.getAllProjects();
873
889
  const stats = db.getProjectSessionStats();
874
- const statsMap = new Map(stats.map(s => [s.projectId, s]));
890
+ const statsMap = new Map(stats.map((s) => [s.projectId, s]));
875
891
  let currentProjectId = null;
876
892
  if (workspacePath) {
877
893
  currentProjectId = resolveProjectId(workspacePath, db, dbStatus);
@@ -916,7 +932,7 @@ class OpenCodeProvider {
916
932
  try {
917
933
  if (!fs.existsSync(sessionBase))
918
934
  return [];
919
- const projectIds = fs.readdirSync(sessionBase).filter(name => {
935
+ const projectIds = fs.readdirSync(sessionBase).filter((name) => {
920
936
  try {
921
937
  return fs.statSync(path.join(sessionBase, name)).isDirectory();
922
938
  }
@@ -928,7 +944,7 @@ class OpenCodeProvider {
928
944
  const projectNames = new Map();
929
945
  if (fs.existsSync(projectDir)) {
930
946
  try {
931
- for (const file of fs.readdirSync(projectDir).filter(f => f.endsWith('.json'))) {
947
+ for (const file of fs.readdirSync(projectDir).filter((f) => f.endsWith('.json'))) {
932
948
  const proj = readJsonSafe(path.join(projectDir, file));
933
949
  if (proj) {
934
950
  projectNames.set(proj.id, proj.path || proj.name || proj.id);
@@ -948,7 +964,7 @@ class OpenCodeProvider {
948
964
  let sessionCount = 0;
949
965
  let lastModified = new Date(0);
950
966
  try {
951
- const sessions = fs.readdirSync(projSessionDir).filter(f => f.endsWith('.json'));
967
+ const sessions = fs.readdirSync(projSessionDir).filter((f) => f.endsWith('.json'));
952
968
  for (const session of sessions) {
953
969
  try {
954
970
  const fstats = fs.statSync(path.join(projSessionDir, session));
@@ -972,7 +988,7 @@ class OpenCodeProvider {
972
988
  name: projectNames.get(projectId) || projectId,
973
989
  encodedName: projectId,
974
990
  sessionCount,
975
- lastModified
991
+ lastModified,
976
992
  });
977
993
  }
978
994
  folders.sort((a, b) => {
@@ -1032,15 +1048,16 @@ class OpenCodeProvider {
1032
1048
  if (!fs.existsSync(messageDir))
1033
1049
  return null;
1034
1050
  try {
1035
- const files = fs.readdirSync(messageDir)
1036
- .filter(f => f.endsWith('.json'))
1051
+ const files = fs
1052
+ .readdirSync(messageDir)
1053
+ .filter((f) => f.endsWith('.json'))
1037
1054
  .slice(0, 5);
1038
1055
  for (const file of files) {
1039
1056
  const msg = readJsonSafe(path.join(messageDir, file));
1040
1057
  if (msg?.role === 'user') {
1041
1058
  const partDir = path.join(getStorageDir(), 'part', msg.id);
1042
1059
  if (fs.existsSync(partDir)) {
1043
- const partFiles = fs.readdirSync(partDir).filter(f => f.endsWith('.json'));
1060
+ const partFiles = fs.readdirSync(partDir).filter((f) => f.endsWith('.json'));
1044
1061
  for (const pf of partFiles) {
1045
1062
  const part = readJsonSafe(path.join(partDir, pf));
1046
1063
  if (part?.type === 'text' && part.text.trim().length > 0) {
@@ -1139,8 +1156,12 @@ class OpenCodeProvider {
1139
1156
  let duration;
1140
1157
  const timeInfo = state?.time;
1141
1158
  if (timeInfo?.start && timeInfo?.end) {
1142
- const startMs = typeof timeInfo.start === 'number' ? timeInfo.start : new Date(timeInfo.start).getTime();
1143
- const endMs = typeof timeInfo.end === 'number' ? timeInfo.end : new Date(timeInfo.end).getTime();
1159
+ const startMs = typeof timeInfo.start === 'number'
1160
+ ? timeInfo.start
1161
+ : new Date(timeInfo.start).getTime();
1162
+ const endMs = typeof timeInfo.end === 'number'
1163
+ ? timeInfo.end
1164
+ : new Date(timeInfo.end).getTime();
1144
1165
  if (endMs > startMs)
1145
1166
  duration = endMs - startMs;
1146
1167
  }
@@ -1168,8 +1189,8 @@ class OpenCodeProvider {
1168
1189
  else {
1169
1190
  startTime = new Date(child.time_created);
1170
1191
  }
1171
- const rawAgentType = subtask?.agent || (child.title || undefined);
1172
- const description = subtask?.description || (child.title || undefined);
1192
+ const rawAgentType = subtask?.agent || child.title || undefined;
1193
+ const description = subtask?.description || child.title || undefined;
1173
1194
  results.push({
1174
1195
  agentId: subtask?.id || child.id,
1175
1196
  agentType: normalizeAgentType(rawAgentType, description),
@@ -1223,7 +1244,7 @@ class OpenCodeProvider {
1223
1244
  try {
1224
1245
  const parts = db.getPartsForSession(sessionId);
1225
1246
  const messages = db.getMessagesForSession(sessionId);
1226
- const messageMap = new Map(messages.map(m => [m.id, m]));
1247
+ const messageMap = new Map(messages.map((m) => [m.id, m]));
1227
1248
  for (const partRow of parts) {
1228
1249
  if (results.length >= maxResults)
1229
1250
  break;
@@ -1262,7 +1283,7 @@ class OpenCodeProvider {
1262
1283
  try {
1263
1284
  const session = readJsonSafe(sessionPath);
1264
1285
  const projectPath = session?.projectID || sessionId;
1265
- const messageFiles = fs.readdirSync(messageDir).filter(f => f.endsWith('.json'));
1286
+ const messageFiles = fs.readdirSync(messageDir).filter((f) => f.endsWith('.json'));
1266
1287
  for (const file of messageFiles) {
1267
1288
  if (results.length >= maxResults)
1268
1289
  break;
@@ -1273,7 +1294,7 @@ class OpenCodeProvider {
1273
1294
  if (!fs.existsSync(partDir))
1274
1295
  continue;
1275
1296
  try {
1276
- const partFiles = fs.readdirSync(partDir).filter(f => f.endsWith('.json'));
1297
+ const partFiles = fs.readdirSync(partDir).filter((f) => f.endsWith('.json'));
1277
1298
  for (const pf of partFiles) {
1278
1299
  if (results.length >= maxResults)
1279
1300
  break;
@@ -1305,7 +1326,7 @@ class OpenCodeProvider {
1305
1326
  line: snippet.replace(/\n/g, ' '),
1306
1327
  eventType: msg.role,
1307
1328
  timestamp: String(msg.time.created),
1308
- projectPath
1329
+ projectPath,
1309
1330
  });
1310
1331
  }
1311
1332
  }
@@ -1357,10 +1378,13 @@ class OpenCodeProvider {
1357
1378
  if (!modelUsage[modelId])
1358
1379
  modelUsage[modelId] = { calls: 0, tokens: 0 };
1359
1380
  modelUsage[modelId].calls++;
1360
- modelUsage[modelId].tokens += (msgTokens.input || 0) + (msgTokens.output || 0);
1381
+ modelUsage[modelId].tokens +=
1382
+ (msgTokens.input || 0) + (msgTokens.output || 0);
1361
1383
  }
1362
1384
  }
1363
- catch { /* skip */ }
1385
+ catch {
1386
+ /* skip */
1387
+ }
1364
1388
  if (!startTime)
1365
1389
  startTime = new Date(msg.time_created).toISOString();
1366
1390
  endTime = new Date(msg.time_updated || msg.time_created).toISOString();
@@ -1373,7 +1397,9 @@ class OpenCodeProvider {
1373
1397
  toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
1374
1398
  }
1375
1399
  }
1376
- catch { /* skip */ }
1400
+ catch {
1401
+ /* skip */
1402
+ }
1377
1403
  }
1378
1404
  }
1379
1405
  return {
@@ -1546,9 +1572,13 @@ class OpenCodeProvider {
1546
1572
  attribution.toolOutputs += toolTokens;
1547
1573
  attribution.userMessages -= toolTokens;
1548
1574
  }
1549
- const total = attribution.systemPrompt + attribution.userMessages +
1550
- attribution.assistantResponses + attribution.toolInputs +
1551
- attribution.toolOutputs + attribution.thinking + attribution.other;
1575
+ const total = attribution.systemPrompt +
1576
+ attribution.userMessages +
1577
+ attribution.assistantResponses +
1578
+ attribution.toolInputs +
1579
+ attribution.toolOutputs +
1580
+ attribution.thinking +
1581
+ attribution.other;
1552
1582
  return total > 0 ? attribution : null;
1553
1583
  }
1554
1584
  catch {
@@ -1600,7 +1630,7 @@ class OpenCodeProvider {
1600
1630
  }
1601
1631
  if (rawRows.length === 0)
1602
1632
  return null;
1603
- const turns = rawRows.map(row => ({
1633
+ const turns = rawRows.map((row) => ({
1604
1634
  timestampMs: row.timeCreated,
1605
1635
  model: row.modelId,
1606
1636
  inputTokens: row.inputTokens || 0,
@@ -1612,9 +1642,7 @@ class OpenCodeProvider {
1612
1642
  const accumulated = (0, zaiQuota_1.accumulateZaiUsage)(turns, Date.now());
1613
1643
  // Default to 'max' on auto when usage is non-zero — overestimating the
1614
1644
  // tier gives a lower (safer) utilization % until calibration runs.
1615
- const resolvedTier = tier === 'auto'
1616
- ? (accumulated.weeklyTurns > 0 ? 'max' : 'lite')
1617
- : tier;
1645
+ const resolvedTier = tier === 'auto' ? (accumulated.weeklyTurns > 0 ? 'max' : 'lite') : tier;
1618
1646
  // Walk rows again to trap any captured z.ai business errors.
1619
1647
  let authoritativeFiveHourResetAt;
1620
1648
  let authoritativeWeeklyResetAt;
@@ -47,7 +47,9 @@ class OpenCodeDatabase {
47
47
  constructor(dataDir) {
48
48
  this.dbPath = path.join(dataDir, 'opencode.db');
49
49
  }
50
- isAvailable() { return fs.existsSync(this.dbPath); }
50
+ isAvailable() {
51
+ return fs.existsSync(this.dbPath);
52
+ }
51
53
  getRuntimeStatus() {
52
54
  if (this.runtimeStatus)
53
55
  return this.runtimeStatus;
@@ -55,7 +57,11 @@ class OpenCodeDatabase {
55
57
  this.runtimeStatus = { available: false, kind: 'db_missing' };
56
58
  return this.runtimeStatus;
57
59
  }
58
- return { available: false, kind: 'query_failed', message: 'OpenCode database has not been initialized yet.' };
60
+ return {
61
+ available: false,
62
+ kind: 'query_failed',
63
+ message: 'OpenCode database has not been initialized yet.',
64
+ };
59
65
  }
60
66
  open() {
61
67
  if (this.runtimeStatus)
@@ -65,7 +71,12 @@ class OpenCodeDatabase {
65
71
  return false;
66
72
  }
67
73
  try {
68
- (0, child_process_1.execFileSync)('sqlite3', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
74
+ (0, child_process_1.execFileSync)('sqlite3', ['--version'], {
75
+ encoding: 'utf-8',
76
+ timeout: 4000,
77
+ killSignal: 'SIGKILL',
78
+ stdio: ['pipe', 'pipe', 'pipe'],
79
+ });
69
80
  this.runtimeStatus = { available: true, kind: 'available' };
70
81
  return true;
71
82
  }
@@ -89,7 +100,11 @@ class OpenCodeDatabase {
89
100
  }
90
101
  try {
91
102
  const result = (0, child_process_1.execFileSync)('sqlite3', ['-json', '-readonly', this.dbPath, query], {
92
- encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 50 * 1024 * 1024,
103
+ encoding: 'utf-8',
104
+ timeout: 4000,
105
+ killSignal: 'SIGKILL',
106
+ stdio: ['pipe', 'pipe', 'pipe'],
107
+ maxBuffer: 50 * 1024 * 1024,
93
108
  });
94
109
  this.runtimeStatus = { available: true, kind: 'available' };
95
110
  const trimmed = result.trim();
@@ -141,17 +156,17 @@ class OpenCodeDatabase {
141
156
  }
142
157
  if (!bestProjectId)
143
158
  return null;
144
- return this.getAllProjects().find(project => project.id === bestProjectId) ?? null;
159
+ return this.getAllProjects().find((project) => project.id === bestProjectId) ?? null;
145
160
  }
146
161
  getAllProjects() {
147
- return this.query('SELECT id, worktree, name, sandboxes, time_created, time_updated FROM project')
148
- .map(project => ({
162
+ return this.query('SELECT id, worktree, name, sandboxes, time_created, time_updated FROM project').map((project) => ({
149
163
  ...project,
150
164
  sandboxes: parseStringArray(project.sandboxes),
151
165
  }));
152
166
  }
153
167
  hasProject(projectId) {
154
- return this.queryOne('SELECT id FROM project WHERE id = ? LIMIT 1', [projectId]) !== null;
168
+ return (this.queryOne('SELECT id FROM project WHERE id = ? LIMIT 1', [projectId]) !==
169
+ null);
155
170
  }
156
171
  getSessionsForProject(projectId) {
157
172
  return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC', [projectId]);
@@ -197,7 +212,7 @@ class OpenCodeDatabase {
197
212
  AND json_extract(data, '$.role') = 'assistant'
198
213
  AND json_extract(data, '$.parentID') IN (${placeholders})`, [sessionId, ...userMessageIds]);
199
214
  return rows
200
- .map(r => r.parentId)
215
+ .map((r) => r.parentId)
201
216
  .filter((id) => typeof id === 'string' && id.length > 0);
202
217
  }
203
218
  /** Get the latest assistant token usage row with non-zero context signal. */
@@ -259,7 +274,9 @@ class OpenCodeDatabase {
259
274
  getAssistantMessagesByProviderId(providerIds, sinceMs) {
260
275
  if (providerIds.length === 0)
261
276
  return [];
262
- const providerClauses = providerIds.map(() => 'json_extract(data, \'$.providerID\') = ?').join(' OR ');
277
+ const providerClauses = providerIds
278
+ .map(() => "json_extract(data, '$.providerID') = ?")
279
+ .join(' OR ');
263
280
  return this.query(`SELECT
264
281
  time_created AS timeCreated,
265
282
  COALESCE(json_extract(data, '$.modelID'), 'unknown') AS modelId,
@@ -302,7 +319,9 @@ function parseStringArray(value) {
302
319
  try {
303
320
  const parsed = JSON.parse(value);
304
321
  return Array.isArray(parsed)
305
- ? parsed.filter((entry) => typeof entry === 'string' && entry.length > 0).map(normalizePath)
322
+ ? parsed
323
+ .filter((entry) => typeof entry === 'string' && entry.length > 0)
324
+ .map(normalizePath)
306
325
  : [];
307
326
  }
308
327
  catch {
@@ -334,17 +353,25 @@ function pathMatchScore(candidate, workspacePath) {
334
353
  }
335
354
  function toRuntimeStatus(error, fallback = 'sqlite_missing') {
336
355
  if (isErrno(error, 'ENOENT')) {
337
- return { available: false, kind: 'sqlite_missing', message: 'sqlite3 executable not found in PATH.' };
356
+ return {
357
+ available: false,
358
+ kind: 'sqlite_missing',
359
+ message: 'sqlite3 executable not found in PATH.',
360
+ };
338
361
  }
339
362
  if (isErrno(error, 'EPERM') || isErrno(error, 'EACCES')) {
340
- return { available: false, kind: 'sqlite_blocked', message: 'sqlite3 exists but could not be executed.' };
363
+ return {
364
+ available: false,
365
+ kind: 'sqlite_blocked',
366
+ message: 'sqlite3 exists but could not be executed.',
367
+ };
341
368
  }
342
369
  const message = error instanceof Error ? error.message : String(error);
343
370
  return { available: false, kind: fallback, message };
344
371
  }
345
372
  function isErrno(error, code) {
346
- return typeof error === 'object'
347
- && error !== null
348
- && 'code' in error
349
- && error.code === code;
373
+ return (typeof error === 'object' &&
374
+ error !== null &&
375
+ 'code' in error &&
376
+ error.code === code);
350
377
  }
package/dist/quota.d.ts CHANGED
@@ -50,6 +50,25 @@ export interface QuotaState {
50
50
  /** Provider-specific rate-limit reached reason */
51
51
  rateLimitReachedType?: string;
52
52
  }
53
+ export declare const FIVE_HOUR_WINDOW_MS: number;
54
+ export declare const SEVEN_DAY_WINDOW_MS: number;
55
+ export interface QuotaProjectionInput {
56
+ utilization: number;
57
+ resetsAt: string;
58
+ windowMs: number;
59
+ capturedAt?: string;
60
+ }
61
+ /**
62
+ * Projects utilization at end of window based on elapsed time.
63
+ *
64
+ * Formula: `current * (windowDuration / elapsed)`, capped at 200%.
65
+ */
66
+ export declare function projectQuotaWindow(input: QuotaProjectionInput): number | undefined;
67
+ export declare function withQuotaProjections<T extends QuotaState>(state: T, options?: {
68
+ fiveHourWindowMs?: number;
69
+ sevenDayWindowMs?: number;
70
+ capturedAt?: string;
71
+ }): T;
53
72
  /**
54
73
  * Fetch current quota utilization from the Anthropic OAuth usage API.
55
74
  *