useful-pi-extensions 1.8.0 → 1.10.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "useful-pi-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A small collection of pi extensions, installed with one command — a labelled status line with context pressure, cache, cost, effort, TTFT and tokens/sec.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -55,7 +55,7 @@ Configuration lives at the top of [`render.ts`](render.ts):
|
|
|
55
55
|
|
|
56
56
|
pi prices every model in USD and its `cost` field carries no unit at all, so the footer cannot know
|
|
57
57
|
what you were actually billed. The currency and the rate live in `~/.pi/agent/statusline/config.json` — everything this
|
|
58
|
-
extension keeps (config plus the
|
|
58
|
+
extension keeps (config plus the per-session state files that survive /reload) sits in that one folder:
|
|
59
59
|
|
|
60
60
|
```json
|
|
61
61
|
{
|
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
* stepStartTime; decodeMs = completedTime - firstTokenTime; tok/s = usage.output / (decodeMs / 1000)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises'
|
|
22
|
-
import { stat } from 'node:fs/promises'
|
|
21
|
+
import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
23
22
|
import { homedir } from 'node:os'
|
|
24
|
-
import { dirname, join } from 'node:path'
|
|
23
|
+
import { basename, dirname, join } from 'node:path'
|
|
25
24
|
|
|
26
25
|
import type {
|
|
27
26
|
ExtensionAPI,
|
|
@@ -71,8 +70,11 @@ const RATES_URL = 'https://open.er-api.com/v6/latest/USD'
|
|
|
71
70
|
/** Every session on this machine, for the day-cost total that spans projects and models. */
|
|
72
71
|
const SESSIONS_DIR = join(homedir(), '.pi', 'agent', 'sessions')
|
|
73
72
|
/** Where throughput metrics wait out a /reload: keyed by session file, so a reload restores. */
|
|
74
|
-
|
|
73
|
+
/** One state file per session, so two concurrent sessions cannot clobber each other. */
|
|
74
|
+
const STATE_DIR = join(STATUSLINE_DIR, 'state')
|
|
75
75
|
const LEGACY_STATE = join(homedir(), '.pi', 'agent', 'statusline-state.json')
|
|
76
|
+
/** The 1.8 single-file state: one bucket every session shared, also migrated to per-session. */
|
|
77
|
+
const LEGACY_SHARED_STATE = join(STATUSLINE_DIR, 'state.json')
|
|
76
78
|
const FETCH_TIMEOUT_MS = 5000
|
|
77
79
|
|
|
78
80
|
function today(): string {
|
|
@@ -133,6 +135,40 @@ async function migrateLegacyFile(legacy: string, current: string): Promise<void>
|
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** Per-session state path: the session file's name is unique and stable across reloads. */
|
|
139
|
+
function statePath(sessionFile: string): string {
|
|
140
|
+
return join(STATE_DIR, `${basename(sessionFile)}.json`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Moves a shared-bucket state file to its per-session name; garbage in, ignored out. */
|
|
144
|
+
async function migrateLegacyState(legacy: string): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
const parsed: unknown = JSON.parse(await readFile(legacy, 'utf8'))
|
|
147
|
+
if (!isRecord(parsed) || typeof parsed.sessionFile !== 'string') return
|
|
148
|
+
await mkdir(STATE_DIR, { recursive: true })
|
|
149
|
+
await rename(legacy, statePath(parsed.sessionFile))
|
|
150
|
+
} catch {
|
|
151
|
+
// Nothing to migrate, or the file was not ours: leave it alone.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Drops per-session state untouched for a week; older files restore nothing useful. */
|
|
156
|
+
async function pruneState(): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
const files = await readdir(STATE_DIR)
|
|
159
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000
|
|
160
|
+
await Promise.all(
|
|
161
|
+
files.map(async (name) => {
|
|
162
|
+
const filePath = join(STATE_DIR, name)
|
|
163
|
+
const stats = await stat(filePath).catch(() => null)
|
|
164
|
+
if (stats !== null && stats.mtimeMs < cutoff) await unlink(filePath).catch(() => {})
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
} catch {
|
|
168
|
+
// No state dir yet: nothing to prune.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
136
172
|
/** Today's provider cost of one session-file line, or null when the line bills nothing today. */
|
|
137
173
|
function entryCost(line: string, since: number): number | null {
|
|
138
174
|
if (!line.includes('"usage"')) return null
|
|
@@ -414,19 +450,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
414
450
|
ttftCount,
|
|
415
451
|
last: reading,
|
|
416
452
|
}
|
|
417
|
-
void
|
|
453
|
+
void mkdir(STATE_DIR, { recursive: true })
|
|
454
|
+
.then(() => writeFile(statePath(sessionFile), `${JSON.stringify(state, null, 2)}\n`))
|
|
455
|
+
.catch(() => {})
|
|
418
456
|
}
|
|
419
457
|
|
|
420
|
-
/** Restores
|
|
458
|
+
/** Restores this session's own state file, written by persist before the reload. */
|
|
421
459
|
async function restore(sessionFile: string | null): Promise<void> {
|
|
422
460
|
if (sessionFile === null) return
|
|
423
461
|
let state: unknown
|
|
424
462
|
try {
|
|
425
|
-
state = JSON.parse(await readFile(
|
|
463
|
+
state = JSON.parse(await readFile(statePath(sessionFile), 'utf8'))
|
|
426
464
|
} catch {
|
|
427
465
|
return
|
|
428
466
|
}
|
|
429
|
-
if (!isRecord(state)
|
|
467
|
+
if (!isRecord(state)) return
|
|
430
468
|
totalDecodeMs = num(state.totalDecodeMs)
|
|
431
469
|
totalMeasuredOutput = num(state.totalMeasuredOutput)
|
|
432
470
|
totalTtftMs = num(state.totalTtftMs)
|
|
@@ -483,33 +521,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
483
521
|
const separator = theme.fg('dim', ' · ')
|
|
484
522
|
const wall = theme.fg('dim', ' | ')
|
|
485
523
|
const identity = [theme.fg('accent', model)]
|
|
486
|
-
if (ctx.thinkingLevel) identity.push(pair(theme, 'Effort', ctx.thinkingLevel
|
|
524
|
+
if (ctx.thinkingLevel) identity.push(pair(theme, 'Effort', ctx.thinkingLevel))
|
|
487
525
|
|
|
488
526
|
const ttft: string[] = []
|
|
489
527
|
const waiting = ttftDisplay(requestAt, firstTokenAt, Date.now())
|
|
490
528
|
if (waiting !== null) {
|
|
491
529
|
// The clock is running: this wait has no reading yet, so the previous turn's
|
|
492
530
|
// numbers would only be mistaken for the current one.
|
|
493
|
-
ttft.push(pair(theme, 'TTFT', waiting.text
|
|
531
|
+
ttft.push(pair(theme, 'TTFT', waiting.text))
|
|
494
532
|
} else if (reading) {
|
|
495
533
|
if (reading.ttftMs !== null)
|
|
496
|
-
ttft.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs)
|
|
534
|
+
ttft.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs)))
|
|
497
535
|
}
|
|
498
536
|
const avgTtft = avgMs(totalTtftMs, ttftCount)
|
|
499
|
-
if (avgTtft !== null) ttft.push(pair(theme, 'Avg TTFT', formatLatency(avgTtft)
|
|
537
|
+
if (avgTtft !== null) ttft.push(pair(theme, 'Avg TTFT', formatLatency(avgTtft)))
|
|
500
538
|
|
|
501
539
|
const throughput: string[] = []
|
|
502
540
|
if (reading) {
|
|
503
541
|
throughput.push(
|
|
504
|
-
pair(
|
|
505
|
-
theme,
|
|
506
|
-
'Last',
|
|
507
|
-
`${reading.exact ? '' : '~'}${formatTps(reading.rate)} tok/s`,
|
|
508
|
-
reading.exact ? 'success' : 'dim',
|
|
509
|
-
),
|
|
542
|
+
pair(theme, 'Last', `${reading.exact ? '' : '~'}${formatTps(reading.rate)} tok/s`),
|
|
510
543
|
)
|
|
511
544
|
}
|
|
512
|
-
if (avg !== null) throughput.push(pair(theme, 'Avg', `${formatTps(avg)} tok/s
|
|
545
|
+
if (avg !== null) throughput.push(pair(theme, 'Avg', `${formatTps(avg)} tok/s`))
|
|
513
546
|
|
|
514
547
|
const row2Right = [identity, ttft, throughput]
|
|
515
548
|
.filter((group) => group.length > 0)
|
|
@@ -579,7 +612,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
579
612
|
currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
|
|
580
613
|
lastKnownUsage = null
|
|
581
614
|
await migrateLegacyFile(LEGACY_CONFIG, CONFIG_PATH)
|
|
582
|
-
await
|
|
615
|
+
await migrateLegacyState(LEGACY_STATE)
|
|
616
|
+
await migrateLegacyState(LEGACY_SHARED_STATE)
|
|
617
|
+
void pruneState()
|
|
583
618
|
const file = ctx.sessionManager.getSessionFile()
|
|
584
619
|
await restore(file ?? null)
|
|
585
620
|
todayBase = await sumOtherTodaysCost(file ?? null, startOfToday())
|
|
@@ -41,7 +41,9 @@ export const ANSI = /\u001b\[[0-9;]*m/g
|
|
|
41
41
|
* an unrelated extension that happens to use the same words is left alone.
|
|
42
42
|
*/
|
|
43
43
|
export const QUIET_STATUS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
44
|
-
|
|
44
|
+
// pi-lens reports language-server health here. Useful when diagnostics break; permanent
|
|
45
|
+
// noise once they work, so the whole LSP line stays out of the footer.
|
|
46
|
+
['pi-lens-lsp', /^LSP/i],
|
|
45
47
|
]
|
|
46
48
|
|
|
47
49
|
export function formatTokens(count: number): string {
|