clauddy 1.28.0 → 1.28.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 +1 -1
- package/cursor.js +24 -13
- package/package.json +1 -1
- package/usage.js +97 -15
package/README.md
CHANGED
|
@@ -286,7 +286,7 @@ Settings saved from the UI live in `~/.claude-usage-monitor/config.json`, so you
|
|
|
286
286
|
"alertThresholds": [80, 95], // notify when session/week cross these % (two levels)
|
|
287
287
|
"fireThreshold": 90, // session % at which the pet catches fire (maxed out stays 100)
|
|
288
288
|
"pollIntervalMs": 4000, // how often local logs are re-read
|
|
289
|
-
"activeThresholdMs": 20000, // "active"
|
|
289
|
+
"activeThresholdMs": 20000, // fallback "active" window, for a log whose tail doesn't say whether a turn is open
|
|
290
290
|
"sleepThresholdMs": 300000, // "sleeping" after this much idle time (5 min)
|
|
291
291
|
"codex": { "enabled": false }, // set by Settings → Codex → Connect
|
|
292
292
|
"cursor": { "enabled": false }, // set by Settings → Cursor → Connect
|
package/cursor.js
CHANGED
|
@@ -8,7 +8,10 @@ const os = require('node:os')
|
|
|
8
8
|
// comes from the agent transcripts under ~/.cursor/projects, which log each
|
|
9
9
|
// tool call much like Claude Code's own logs.
|
|
10
10
|
const API = 'https://api2.cursor.sh/aiserver.v1.DashboardService/'
|
|
11
|
-
const ACTIVE_MS = 60000 // a transcript
|
|
11
|
+
const ACTIVE_MS = 60000 // a transcript that says nothing counts only while it's this fresh
|
|
12
|
+
// an unfinished turn this long without a write was abandoned (Cursor closed mid-run)
|
|
13
|
+
const OPEN_TURN_MS = 30 * 60000
|
|
14
|
+
const RECENT_TRANSCRIPTS = 8
|
|
12
15
|
const FETCH_MS = 5 * 60000 // a monthly budget moves slowly; don't hammer the API
|
|
13
16
|
const RETRY_MS = 60000
|
|
14
17
|
const TAIL_BYTES = 64 * 1024
|
|
@@ -169,7 +172,8 @@ const ACTIVITY = {
|
|
|
169
172
|
AskQuestion: 'waiting',
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
// what the newest transcript lines say: a finished turn means nothing is
|
|
175
|
+
// what the newest transcript lines say: a finished turn means nothing is
|
|
176
|
+
// running (null), an open one names its scene, and undefined means no telling
|
|
173
177
|
function activityOf(lines) {
|
|
174
178
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
175
179
|
let o
|
|
@@ -186,7 +190,7 @@ function activityOf(lines) {
|
|
|
186
190
|
}
|
|
187
191
|
if (o.role === 'user') return 'working' // just asked: the agent is thinking
|
|
188
192
|
}
|
|
189
|
-
return
|
|
193
|
+
return undefined
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
const dirs = (dir) => {
|
|
@@ -232,22 +236,29 @@ function tailLines(file, size) {
|
|
|
232
236
|
return lines.filter(Boolean)
|
|
233
237
|
}
|
|
234
238
|
|
|
239
|
+
// A turn stays open while a long command runs or the model thinks, without a
|
|
240
|
+
// write, so an open turn in any recent transcript is Cursor at work. A
|
|
241
|
+
// transcript with nothing to say counts only while it's fresh.
|
|
235
242
|
function readActivity(now) {
|
|
236
|
-
|
|
243
|
+
const recent = []
|
|
244
|
+
let lastSeen = null
|
|
237
245
|
for (const file of transcripts()) {
|
|
238
246
|
try {
|
|
239
247
|
const st = fs.statSync(file)
|
|
240
|
-
if (
|
|
248
|
+
if (lastSeen == null || st.mtimeMs > lastSeen) lastSeen = st.mtimeMs
|
|
249
|
+
if (now - st.mtimeMs < OPEN_TURN_MS) recent.push({ file, st })
|
|
241
250
|
} catch {}
|
|
242
251
|
}
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
recent.sort((a, b) => b.st.mtimeMs - a.st.mtimeMs)
|
|
253
|
+
for (const { file, st } of recent.slice(0, RECENT_TRANSCRIPTS)) {
|
|
254
|
+
let said
|
|
255
|
+
try {
|
|
256
|
+
said = activityOf(tailLines(file, st.size))
|
|
257
|
+
} catch {}
|
|
258
|
+
const activity = said === undefined ? (now - st.mtimeMs < ACTIVE_MS ? 'working' : null) : said
|
|
259
|
+
if (activity) return { active: true, activity, lastSeen }
|
|
260
|
+
}
|
|
261
|
+
return { active: false, activity: null, lastSeen }
|
|
251
262
|
}
|
|
252
263
|
|
|
253
264
|
// The poll's view of Cursor: activity from disk every tick, limits from the
|
package/package.json
CHANGED
package/usage.js
CHANGED
|
@@ -224,23 +224,30 @@ const ACTIVITY_BY_TOOL = {
|
|
|
224
224
|
AskUserQuestion: 'waiting',
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
// (permission-mode) wins over everything. Returns null when there's no signal.
|
|
230
|
-
function detectActivity(file) {
|
|
231
|
-
let raw
|
|
227
|
+
// the last lines of a session log (the recent tail is plenty), or null
|
|
228
|
+
function tailLines(file) {
|
|
232
229
|
try {
|
|
233
230
|
const fd = fs.openSync(file, 'r')
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
231
|
+
try {
|
|
232
|
+
const size = fs.fstatSync(fd).size
|
|
233
|
+
const len = Math.min(size, 65536)
|
|
234
|
+
const buf = Buffer.alloc(len)
|
|
235
|
+
fs.readSync(fd, buf, 0, len, size - len)
|
|
236
|
+
return buf.toString('utf8').split('\n').filter(Boolean)
|
|
237
|
+
} finally {
|
|
238
|
+
fs.closeSync(fd)
|
|
239
|
+
}
|
|
240
240
|
} catch {
|
|
241
241
|
return null
|
|
242
242
|
}
|
|
243
|
-
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Classify what Claude Code is doing right now by reading the tail of the most
|
|
246
|
+
// recently touched session log: the latest tool_use → an activity, plan mode
|
|
247
|
+
// (permission-mode) wins over everything. Returns null when there's no signal.
|
|
248
|
+
function detectActivity(file) {
|
|
249
|
+
const lines = tailLines(file)
|
|
250
|
+
if (!lines) return null
|
|
244
251
|
let activity = null
|
|
245
252
|
let permMode = null
|
|
246
253
|
for (let i = lines.length - 1; i >= 0 && i >= lines.length - 80; i--) {
|
|
@@ -265,6 +272,60 @@ function detectActivity(file) {
|
|
|
265
272
|
return activity
|
|
266
273
|
}
|
|
267
274
|
|
|
275
|
+
// User lines that open no turn: slash commands and their output, `!` shell
|
|
276
|
+
// input, background-task notices, the compaction summary, meta attachments
|
|
277
|
+
const NO_TURN =
|
|
278
|
+
/^\s*(<command-name>|<command-message>|<local-command-|<bash-|<task-notification>|This session is being continued)/
|
|
279
|
+
const DONE_STOPS = new Set(['end_turn', 'stop_sequence', 'refusal'])
|
|
280
|
+
const textOf = (content) =>
|
|
281
|
+
typeof content === 'string'
|
|
282
|
+
? content
|
|
283
|
+
: Array.isArray(content)
|
|
284
|
+
? content.map((b) => (b && b.type === 'text' ? b.text : '')).join(' ')
|
|
285
|
+
: ''
|
|
286
|
+
|
|
287
|
+
// Is a turn still open? Claude Code writes nothing while a command runs or the
|
|
288
|
+
// model thinks, so a quiet log proves nothing; how it ends does. A final answer
|
|
289
|
+
// (and the turn_duration line after it) or an interruption closes the turn; a
|
|
290
|
+
// tool call, a tool result or a prompt leaves it open. Returns true, false, or
|
|
291
|
+
// null when the tail doesn't say.
|
|
292
|
+
function turnOpen(lines) {
|
|
293
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
294
|
+
let o
|
|
295
|
+
try {
|
|
296
|
+
o = JSON.parse(lines[i])
|
|
297
|
+
} catch {
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
if (o.isSidechain) continue
|
|
301
|
+
if (o.type === 'system') {
|
|
302
|
+
if (o.subtype === 'turn_duration') return false
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
if (o.type === 'assistant') {
|
|
306
|
+
const stop = o.message?.stop_reason
|
|
307
|
+
if (!stop) continue
|
|
308
|
+
return !DONE_STOPS.has(stop)
|
|
309
|
+
}
|
|
310
|
+
if (o.type === 'user') {
|
|
311
|
+
const content = o.message?.content
|
|
312
|
+
if (Array.isArray(content) && content.some((b) => b?.type === 'tool_result')) return true
|
|
313
|
+
if (o.isMeta || o.isCompactSummary) continue
|
|
314
|
+
const text = textOf(content)
|
|
315
|
+
if (text.includes('[Request interrupted by user')) return false
|
|
316
|
+
if (NO_TURN.test(text)) continue
|
|
317
|
+
return true
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return null
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// a turn left open this long without a write was abandoned (a closed
|
|
324
|
+
// terminal, a crash) — or a permission prompt nobody answered
|
|
325
|
+
const OPEN_TURN_MS = 30 * 60000
|
|
326
|
+
// parallel sessions and subagents each keep a log; a handful covers them
|
|
327
|
+
const RECENT_LOGS = 8
|
|
328
|
+
|
|
268
329
|
// Plan presets (token budgets ~= 100%). Calibrated for Max 5x from the official
|
|
269
330
|
// panel (5h ~24% at 152M tokens, weekly ~62% at 2.14B); Pro/Max20x scaled by the
|
|
270
331
|
// plan multiplier. ESTIMATES — Anthropic doesn't publish exact numbers.
|
|
@@ -371,11 +432,31 @@ function getUsage(config) {
|
|
|
371
432
|
const weekPct = weeklyBudget ? Math.min(100, (weekTokens / weeklyBudget) * 100) : 0
|
|
372
433
|
|
|
373
434
|
const lastActivityMs = lastMtime ? now - lastMtime : Infinity
|
|
374
|
-
|
|
375
|
-
|
|
435
|
+
// Working means a turn is open in one of the recent logs, even when a long
|
|
436
|
+
// command or a slow answer has kept it quiet for minutes. A log with nothing
|
|
437
|
+
// to say falls back to the old rule: written in the last few seconds.
|
|
438
|
+
let openFile = null
|
|
439
|
+
const recentLogs = files
|
|
440
|
+
.filter((f) => now - f.st.mtimeMs <= OPEN_TURN_MS)
|
|
441
|
+
.sort((a, b) => b.st.mtimeMs - a.st.mtimeMs)
|
|
442
|
+
.slice(0, RECENT_LOGS)
|
|
443
|
+
let newestSays = null
|
|
444
|
+
for (const [i, f] of recentLogs.entries()) {
|
|
445
|
+
const lines = tailLines(f.full)
|
|
446
|
+
const open = lines ? turnOpen(lines) : null
|
|
447
|
+
if (i === 0) newestSays = open
|
|
448
|
+
if (open) {
|
|
449
|
+
openFile = f.full
|
|
450
|
+
break
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const quietRule = newestSays === null && lastActivityMs <= (config.activeThresholdMs || 20000)
|
|
454
|
+
const active = !!openFile || quietRule
|
|
455
|
+
const sleeping = !active && lastActivityMs >= (config.sleepThresholdMs || 300000)
|
|
376
456
|
|
|
377
457
|
// what Claude is doing right now (only meaningful while active)
|
|
378
|
-
const
|
|
458
|
+
const activeFile = openFile || (quietRule ? newestFile : null)
|
|
459
|
+
const activity = activeFile ? detectActivity(activeFile) || (openFile ? 'working' : null) : null
|
|
379
460
|
|
|
380
461
|
const byModelArr = [...byModel.entries()]
|
|
381
462
|
.map(([label, tokens]) => ({ label, tokens }))
|
|
@@ -422,5 +503,6 @@ module.exports = {
|
|
|
422
503
|
projectLabel,
|
|
423
504
|
tokensOf,
|
|
424
505
|
detectActivity,
|
|
506
|
+
turnOpen,
|
|
425
507
|
PLAN_BUDGETS,
|
|
426
508
|
}
|