sidekick-shared 0.23.0 → 0.23.1
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 +24 -20
- package/dist/accountManager.js +10 -0
- package/dist/aggregation/EventAggregator.d.ts +4 -1
- package/dist/aggregation/EventAggregator.js +22 -9
- package/dist/aggregation/snapshot.d.ts +1 -1
- package/dist/aggregation/snapshot.js +2 -1
- package/dist/aggregation/types.d.ts +2 -0
- package/dist/autoSwitch.d.ts +1 -1
- package/dist/autoSwitch.js +14 -3
- 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 +35 -9
- package/dist/credentialIO.js +19 -5
- package/dist/errorHistory.d.ts +1 -0
- package/dist/errorHistory.js +3 -2
- package/dist/extractors/toolCall.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -7
- package/dist/modelInfo.js +6 -0
- package/dist/multiProviderQuotaService.js +13 -2
- package/dist/parsers/codexParser.js +4 -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 +4 -7
- package/dist/providerStatus.js +13 -2
- package/dist/providers/claudeCode.js +18 -4
- package/dist/providers/codex.js +42 -13
- 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/statusline/formatter.js +8 -1
- package/dist/terminalSync.js +16 -7
- package/dist/types/historicalData.d.ts +14 -0
- package/dist/types/historicalData.js +22 -0
- package/dist/types/observedSessionV1.js +6 -5
- package/dist/types/sessionEvent.d.ts +1 -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
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) {
|
package/dist/readers/plans.js
CHANGED
|
@@ -48,6 +48,7 @@ const plan_1 = require("../types/plan");
|
|
|
48
48
|
const paths_1 = require("../paths");
|
|
49
49
|
const helpers_1 = require("./helpers");
|
|
50
50
|
const planExtractor_1 = require("../parsers/planExtractor");
|
|
51
|
+
const atomic_1 = require("../writers/atomic");
|
|
51
52
|
async function readPlans(slug, opts) {
|
|
52
53
|
const filePath = (0, paths_1.getProjectDataPath)(slug, 'plans');
|
|
53
54
|
const store = await (0, helpers_1.readJsonStore)(filePath);
|
|
@@ -170,10 +171,7 @@ async function writePlans(slug, plans) {
|
|
|
170
171
|
plans: capped,
|
|
171
172
|
lastSaved: new Date().toISOString(),
|
|
172
173
|
};
|
|
173
|
-
|
|
174
|
-
const dir = filePath.replace(/[/\\][^/\\]+$/, '');
|
|
175
|
-
await fs.promises.mkdir(dir, { recursive: true });
|
|
176
|
-
await fs.promises.writeFile(filePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
174
|
+
await (0, atomic_1.atomicWriteJson)(filePath, store);
|
|
177
175
|
}
|
|
178
176
|
/**
|
|
179
177
|
* Reads raw plan markdown files from ~/.claude/plans/ that belong to
|
package/dist/readers/tasks.js
CHANGED
|
@@ -9,7 +9,9 @@ async function readTasks(project, opts) {
|
|
|
9
9
|
const store = await (0, helpers_1.readProjectJsonStore)(project, 'tasks');
|
|
10
10
|
if (!store)
|
|
11
11
|
return [];
|
|
12
|
-
let tasks =
|
|
12
|
+
let tasks = store.tasks && typeof store.tasks === 'object' && !Array.isArray(store.tasks)
|
|
13
|
+
? Object.values(store.tasks)
|
|
14
|
+
: [];
|
|
13
15
|
const status = opts?.status ?? 'all';
|
|
14
16
|
if (status !== 'all') {
|
|
15
17
|
tasks = tasks.filter((t) => t.status === status);
|
|
@@ -53,7 +53,12 @@ function simpleMarkdownToHtml(text) {
|
|
|
53
53
|
// Inline code (after escaping so backtick content is already safe)
|
|
54
54
|
result = result.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
|
55
55
|
// Links [text](url)
|
|
56
|
-
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
56
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, url) => {
|
|
57
|
+
const trimmedUrl = url.trim();
|
|
58
|
+
if (!/^(?:https?:|mailto:|#|\/)/i.test(trimmedUrl))
|
|
59
|
+
return match;
|
|
60
|
+
return `<a href="${trimmedUrl}" target="_blank" rel="noopener">${label}</a>`;
|
|
61
|
+
});
|
|
57
62
|
// Unordered lists
|
|
58
63
|
result = result.replace(/^(\s*)[-*] (.+)$/gm, '$1<li>$2</li>');
|
|
59
64
|
// Ordered lists
|
|
@@ -10,13 +10,13 @@ function openInBrowser(filePath) {
|
|
|
10
10
|
const url = `file://${filePath}`;
|
|
11
11
|
switch (process.platform) {
|
|
12
12
|
case 'darwin':
|
|
13
|
-
(0, child_process_1.
|
|
13
|
+
(0, child_process_1.execFile)('open', [url]);
|
|
14
14
|
break;
|
|
15
15
|
case 'win32':
|
|
16
|
-
(0, child_process_1.
|
|
16
|
+
(0, child_process_1.execFile)('rundll32.exe', ['url.dll,FileProtocolHandler', url]);
|
|
17
17
|
break;
|
|
18
18
|
default:
|
|
19
|
-
(0, child_process_1.
|
|
19
|
+
(0, child_process_1.execFile)('xdg-open', [url]);
|
|
20
20
|
break;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -42,13 +42,20 @@ function estimateWindowEta(quota, now) {
|
|
|
42
42
|
const resetAt = Date.parse(quota.fiveHour.resetsAt);
|
|
43
43
|
if (!Number.isFinite(resetAt))
|
|
44
44
|
return null;
|
|
45
|
+
const nowMs = now.getTime();
|
|
46
|
+
if (resetAt <= nowMs)
|
|
47
|
+
return null;
|
|
45
48
|
const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : now.getTime();
|
|
46
49
|
const sampleAt = Number.isFinite(capturedAt) ? capturedAt : now.getTime();
|
|
50
|
+
if (sampleAt >= resetAt)
|
|
51
|
+
return null;
|
|
47
52
|
const elapsedMinutes = (sampleAt - (resetAt - quota_1.FIVE_HOUR_WINDOW_MS)) / 60_000;
|
|
48
53
|
if (elapsedMinutes <= 0 || quota.fiveHour.utilization <= 0)
|
|
49
54
|
return null;
|
|
50
55
|
const burnRate = quota.fiveHour.utilization / elapsedMinutes;
|
|
51
|
-
|
|
56
|
+
const eta = (0, BurnRateCalculator_1.estimateTimeToQuota)(quota.fiveHour.utilization, 100, burnRate);
|
|
57
|
+
const minutesUntilReset = (resetAt - nowMs) / 60_000;
|
|
58
|
+
return eta !== null && eta < minutesUntilReset ? eta : null;
|
|
52
59
|
}
|
|
53
60
|
function compactLabel(value) {
|
|
54
61
|
const trimmed = value.trim();
|
package/dist/terminalSync.js
CHANGED
|
@@ -107,20 +107,28 @@ function buildShellHookBlock() {
|
|
|
107
107
|
function getShellRcPaths() {
|
|
108
108
|
const zshrc = path.join(os.homedir(), '.zshrc');
|
|
109
109
|
const bashrc = path.join(os.homedir(), '.bashrc');
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
const existing = [zshrc, bashrc].filter((filePath) => fs.existsSync(filePath));
|
|
111
|
+
if (existing.length > 0)
|
|
112
|
+
return existing;
|
|
113
|
+
const shell = path.basename(process.env.SHELL ?? 'zsh');
|
|
114
|
+
return shell === 'bash' ? [bashrc] : [zshrc];
|
|
115
|
+
}
|
|
116
|
+
function resolveShellRcTarget(filePath) {
|
|
117
|
+
if (!fs.existsSync(filePath))
|
|
118
|
+
return { filePath, mode: 0o600 };
|
|
119
|
+
const realPath = fs.realpathSync(filePath);
|
|
120
|
+
return { filePath: realPath, mode: fs.statSync(realPath).mode & 0o777 };
|
|
114
121
|
}
|
|
115
122
|
function stripShellHook(content) {
|
|
116
123
|
const pattern = new RegExp(`${HOOK_START}[\\s\\S]*?${HOOK_END}\\n?`, 'g');
|
|
117
124
|
return content.replace(pattern, '').replace(/\n{3,}/g, '\n\n');
|
|
118
125
|
}
|
|
119
126
|
function installHookInFile(filePath) {
|
|
120
|
-
const
|
|
127
|
+
const target = resolveShellRcTarget(filePath);
|
|
128
|
+
const existing = fs.existsSync(target.filePath) ? fs.readFileSync(target.filePath, 'utf8') : '';
|
|
121
129
|
const cleaned = stripShellHook(existing).replace(/\s*$/, '');
|
|
122
130
|
const next = cleaned ? `${cleaned}\n\n${buildShellHookBlock()}` : buildShellHookBlock();
|
|
123
|
-
atomicWriteFile(filePath, next,
|
|
131
|
+
atomicWriteFile(target.filePath, next, target.mode);
|
|
124
132
|
}
|
|
125
133
|
function installShellHook() {
|
|
126
134
|
for (const rcPath of getShellRcPaths()) {
|
|
@@ -130,7 +138,8 @@ function installShellHook() {
|
|
|
130
138
|
function uninstallHookInFile(filePath) {
|
|
131
139
|
if (!fs.existsSync(filePath))
|
|
132
140
|
return;
|
|
133
|
-
|
|
141
|
+
const target = resolveShellRcTarget(filePath);
|
|
142
|
+
atomicWriteFile(target.filePath, stripShellHook(fs.readFileSync(target.filePath, 'utf8')), target.mode);
|
|
134
143
|
}
|
|
135
144
|
function uninstallShellHook() {
|
|
136
145
|
for (const rcPath of getShellRcPaths()) {
|
|
@@ -51,6 +51,13 @@ export interface DailyData {
|
|
|
51
51
|
toolUsage: ToolUsageRecord[];
|
|
52
52
|
updatedAt: string;
|
|
53
53
|
}
|
|
54
|
+
export interface HourlyData {
|
|
55
|
+
hour: number;
|
|
56
|
+
tokens: TokenTotals;
|
|
57
|
+
totalCost: number;
|
|
58
|
+
messageCount: number;
|
|
59
|
+
sessionCount: number;
|
|
60
|
+
}
|
|
54
61
|
export interface MonthlyData {
|
|
55
62
|
month: string;
|
|
56
63
|
tokens: TokenTotals;
|
|
@@ -75,6 +82,8 @@ export interface AllTimeStats {
|
|
|
75
82
|
export interface HistoricalDataStore {
|
|
76
83
|
schemaVersion: number;
|
|
77
84
|
daily: Record<string, DailyData>;
|
|
85
|
+
/** Hourly buckets keyed by local YYYY-MM-DD. */
|
|
86
|
+
hourly?: Record<string, HourlyData[]>;
|
|
78
87
|
monthly: Record<string, MonthlyData>;
|
|
79
88
|
allTime: AllTimeStats;
|
|
80
89
|
lastSaved: string;
|
|
@@ -92,6 +101,10 @@ export interface SessionHistoryRecord {
|
|
|
92
101
|
tokens: TokenTotals;
|
|
93
102
|
totalCost: number;
|
|
94
103
|
messageCount: number;
|
|
104
|
+
/** Schema v3 additive fields used to make aggregate replacement idempotent. */
|
|
105
|
+
modelUsage?: ModelUsageRecord[];
|
|
106
|
+
toolUsage?: ToolUsageRecord[];
|
|
107
|
+
unpricedModelIds?: string[];
|
|
95
108
|
qualityScore: number;
|
|
96
109
|
qualityFactors: Array<{
|
|
97
110
|
id: string;
|
|
@@ -132,3 +145,4 @@ export interface SessionSummary {
|
|
|
132
145
|
costPerChangedLine?: number | null;
|
|
133
146
|
}
|
|
134
147
|
export declare function createEmptyTokenTotals(): TokenTotals;
|
|
148
|
+
export declare function createEmptyDataStore(): HistoricalDataStore;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.HISTORICAL_SESSION_RETENTION_LIMIT = exports.HISTORICAL_DATA_SCHEMA_VERSION = void 0;
|
|
20
20
|
exports.createEmptyTokenTotals = createEmptyTokenTotals;
|
|
21
|
+
exports.createEmptyDataStore = createEmptyDataStore;
|
|
21
22
|
exports.HISTORICAL_DATA_SCHEMA_VERSION = 3;
|
|
22
23
|
exports.HISTORICAL_SESSION_RETENTION_LIMIT = 500;
|
|
23
24
|
function createEmptyTokenTotals() {
|
|
@@ -28,3 +29,24 @@ function createEmptyTokenTotals() {
|
|
|
28
29
|
cacheReadTokens: 0,
|
|
29
30
|
};
|
|
30
31
|
}
|
|
32
|
+
function createEmptyDataStore() {
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: exports.HISTORICAL_DATA_SCHEMA_VERSION,
|
|
36
|
+
daily: {},
|
|
37
|
+
monthly: {},
|
|
38
|
+
sessions: [],
|
|
39
|
+
allTime: {
|
|
40
|
+
tokens: createEmptyTokenTotals(),
|
|
41
|
+
totalCost: 0,
|
|
42
|
+
messageCount: 0,
|
|
43
|
+
sessionCount: 0,
|
|
44
|
+
firstDate: '',
|
|
45
|
+
lastDate: '',
|
|
46
|
+
modelUsage: [],
|
|
47
|
+
toolUsage: [],
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
},
|
|
50
|
+
lastSaved: now,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -10,7 +10,7 @@ function evidence(provider, sessionId, sourcePath, event, eventIndex) {
|
|
|
10
10
|
sessionId,
|
|
11
11
|
sourcePath,
|
|
12
12
|
eventIndex,
|
|
13
|
-
eventId: event?.message
|
|
13
|
+
eventId: event?.message?.id,
|
|
14
14
|
timestamp: event?.timestamp,
|
|
15
15
|
},
|
|
16
16
|
];
|
|
@@ -29,7 +29,7 @@ function derivePendingUserRequestV1(provider, sessionId, sourcePath, events) {
|
|
|
29
29
|
const refs = evidence(provider, sessionId, sourcePath, event, index);
|
|
30
30
|
return {
|
|
31
31
|
schemaVersion: 1,
|
|
32
|
-
id: `${provider}:${sessionId}:${request.id ?? event.message
|
|
32
|
+
id: `${provider}:${sessionId}:${request.id ?? event.message?.id ?? index}`,
|
|
33
33
|
provider,
|
|
34
34
|
sessionId,
|
|
35
35
|
kind: observed(request.kind, 'inferred', request.kind === 'question' ? 0.95 : 0.9, refs),
|
|
@@ -45,11 +45,12 @@ function derivePendingUserRequestV1(provider, sessionId, sourcePath, events) {
|
|
|
45
45
|
}
|
|
46
46
|
function waitingToolRequest(event) {
|
|
47
47
|
const calls = [];
|
|
48
|
+
const message = event.message;
|
|
48
49
|
if (event.type === 'tool_use') {
|
|
49
|
-
calls.push({ id:
|
|
50
|
+
calls.push({ id: message?.id, name: event.tool?.name, input: event.tool?.input });
|
|
50
51
|
}
|
|
51
|
-
if (Array.isArray(
|
|
52
|
-
for (const block of
|
|
52
|
+
if (Array.isArray(message?.content)) {
|
|
53
|
+
for (const block of message.content) {
|
|
53
54
|
if (!block || typeof block !== 'object')
|
|
54
55
|
continue;
|
|
55
56
|
const value = block;
|
package/dist/watchers/factory.js
CHANGED
|
@@ -56,14 +56,25 @@ function createWatcher(options) {
|
|
|
56
56
|
}
|
|
57
57
|
let sessionPath;
|
|
58
58
|
if (sessionId) {
|
|
59
|
-
const
|
|
60
|
-
|
|
59
|
+
const matches = sessions.map((sessionPath) => ({
|
|
60
|
+
sessionPath,
|
|
61
|
+
id: provider.getSessionId(sessionPath),
|
|
62
|
+
}));
|
|
63
|
+
const exact = matches.filter((candidate) => candidate.id === sessionId);
|
|
64
|
+
const candidates = exact.length > 0 ? exact : matches.filter((candidate) => candidate.id.startsWith(sessionId));
|
|
65
|
+
if (candidates.length === 0) {
|
|
61
66
|
throw new Error(`Session ${sessionId} not found. Available: ${sessions
|
|
62
67
|
.slice(0, 5)
|
|
63
68
|
.map((s) => path.basename(s))
|
|
64
69
|
.join(', ')}`);
|
|
65
70
|
}
|
|
66
|
-
|
|
71
|
+
if (candidates.length > 1) {
|
|
72
|
+
throw new Error(`Session ${sessionId} is ambiguous. Matches: ${candidates
|
|
73
|
+
.slice(0, 5)
|
|
74
|
+
.map((candidate) => candidate.id)
|
|
75
|
+
.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
sessionPath = candidates[0].sessionPath;
|
|
67
78
|
}
|
|
68
79
|
else {
|
|
69
80
|
sessionPath = sessions[0]; // most recent
|