@tduyng/pi-session 0.0.1 → 0.0.2
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 +4 -1
- package/src/autoresume.ts +9 -5
- package/src/usage.ts +109 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tduyng/pi-session",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Session lifecycle: name an unnamed session after its first message, and resume once a provider quota resets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"@earendil-works/pi-ai": "*",
|
|
32
32
|
"@earendil-works/pi-coding-agent": "*"
|
|
33
33
|
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ata-validator": "^1.62.1"
|
|
36
|
+
},
|
|
34
37
|
"publishConfig": {
|
|
35
38
|
"access": "public"
|
|
36
39
|
},
|
package/src/autoresume.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
2
|
-
import { claudeToken, fetchAnthropicUsage } from '
|
|
3
|
-
import { exhaustedResetAt } from '../../pi-cost/src/cost/logic.js'
|
|
2
|
+
import { claudeToken, exhaustedResetAt, fetchAnthropicUsage } from './usage.js'
|
|
4
3
|
|
|
5
4
|
const SAFETY_DELAY_MS = 5_000
|
|
6
5
|
const MAX_DELAY_MS = 24 * 60 * 60 * 1000
|
|
@@ -33,6 +32,7 @@ export default function autoResume(pi: ExtensionAPI) {
|
|
|
33
32
|
pendingError = undefined
|
|
34
33
|
|
|
35
34
|
if (!enabled || !error || resumeAt) return
|
|
35
|
+
if (!isQuotaError(error)) return
|
|
36
36
|
|
|
37
37
|
const reportedTarget = parseReset(error)
|
|
38
38
|
const providerTarget = ctx.model?.provider === 'claude-bridge' ? await usageResetAt(pi, ctx) : undefined
|
|
@@ -78,9 +78,9 @@ export default function autoResume(pi: ExtensionAPI) {
|
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
pi.on('session_shutdown', async (_event, ctx) => cancel(ctx))
|
|
81
|
-
pi.on('session_start', () => {
|
|
81
|
+
pi.on('session_start', (_event, ctx) => {
|
|
82
82
|
pendingError = undefined
|
|
83
|
-
|
|
83
|
+
cancel(ctx)
|
|
84
84
|
})
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -244,10 +244,14 @@ function parseRetryAfter(input: string, now: number): number | undefined {
|
|
|
244
244
|
return match ? now + Number(match[1]) * 1000 : undefined
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
function isQuotaError(input: string): boolean {
|
|
248
|
+
return /\b(?:usage|quota|limit|rate[- ]?limit|too many requests|429)\b/i.test(input)
|
|
249
|
+
}
|
|
250
|
+
|
|
247
251
|
function parseClock(input: string, now: number): number | undefined {
|
|
248
252
|
const match = input.match(/\breset(?:s)?(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?(?:\s*\(([^)]+)\))?/i)
|
|
249
253
|
|
|
250
|
-
if (!match) return
|
|
254
|
+
if (!match || (match[2] === undefined && match[3] === undefined)) return
|
|
251
255
|
|
|
252
256
|
let hour = Number(match[1])
|
|
253
257
|
const minute = Number(match[2] ?? '0')
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
5
|
+
import { Validator } from 'ata-validator'
|
|
6
|
+
import { t } from 'ata-validator/t'
|
|
7
|
+
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 10_000
|
|
9
|
+
const KEYCHAIN_TIMEOUT_MS = 5_000
|
|
10
|
+
|
|
11
|
+
const WindowSchema = t.object({
|
|
12
|
+
utilization: t.optional(t.union([t.number(), t.null()])),
|
|
13
|
+
resets_at: t.optional(t.union([t.string(), t.number(), t.null()]))
|
|
14
|
+
})
|
|
15
|
+
const UsageSchema = t.object({
|
|
16
|
+
five_hour: t.optional(t.union([WindowSchema, t.null()])),
|
|
17
|
+
seven_day: t.optional(t.union([WindowSchema, t.null()])),
|
|
18
|
+
seven_day_opus: t.optional(t.union([WindowSchema, t.null()])),
|
|
19
|
+
seven_day_sonnet: t.optional(t.union([WindowSchema, t.null()]))
|
|
20
|
+
})
|
|
21
|
+
const CredentialsSchema = t.object(
|
|
22
|
+
{
|
|
23
|
+
claudeAiOauth: t.optional(t.object({ accessToken: t.optional(t.string()) }, { additionalProperties: true }))
|
|
24
|
+
},
|
|
25
|
+
{ additionalProperties: true }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const isUsage = new Validator(UsageSchema).isValidObject
|
|
29
|
+
const isCredentials = new Validator(CredentialsSchema).isValidObject
|
|
30
|
+
|
|
31
|
+
export type AnthropicWindow = { utilization?: number | null; resets_at?: string | number | null }
|
|
32
|
+
|
|
33
|
+
export type AnthropicUsagePayload = {
|
|
34
|
+
five_hour?: AnthropicWindow | null
|
|
35
|
+
seven_day?: AnthropicWindow | null
|
|
36
|
+
seven_day_opus?: AnthropicWindow | null
|
|
37
|
+
seven_day_sonnet?: AnthropicWindow | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function finite(value: number | null | undefined): number | undefined {
|
|
41
|
+
const numeric = value ?? Number.NaN
|
|
42
|
+
return Number.isFinite(numeric) ? numeric : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resetAtMs(value: string | number | null | undefined): number | undefined {
|
|
46
|
+
if (value === null || value === undefined) return undefined
|
|
47
|
+
const numeric = Number(value)
|
|
48
|
+
const parsed = Number.isFinite(numeric) ? numeric : Date.parse(String(value))
|
|
49
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined
|
|
50
|
+
return parsed < 1e12 ? parsed * 1000 : parsed
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function exhaustedResetAt(payload: AnthropicUsagePayload): number | undefined {
|
|
54
|
+
const windows = [payload.five_hour, payload.seven_day, payload.seven_day_opus, payload.seven_day_sonnet]
|
|
55
|
+
let latest: number | undefined
|
|
56
|
+
for (const window of windows) {
|
|
57
|
+
const utilization = finite(window?.utilization)
|
|
58
|
+
const resetAt = resetAtMs(window?.resets_at)
|
|
59
|
+
if (utilization === undefined || utilization < 100 || resetAt === undefined) continue
|
|
60
|
+
latest = latest === undefined ? resetAt : Math.max(latest, resetAt)
|
|
61
|
+
}
|
|
62
|
+
return latest
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function claudeAccessToken(raw: string | undefined): string | undefined {
|
|
66
|
+
if (!raw) return undefined
|
|
67
|
+
try {
|
|
68
|
+
const parsed: unknown = JSON.parse(raw)
|
|
69
|
+
if (!isCredentials(parsed)) return undefined
|
|
70
|
+
const token = parsed.claudeAiOauth?.accessToken?.trim()
|
|
71
|
+
return token || undefined
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function claudeToken(
|
|
78
|
+
pi: ExtensionAPI,
|
|
79
|
+
platform: NodeJS.Platform = process.platform
|
|
80
|
+
): Promise<string | undefined> {
|
|
81
|
+
let raw: string | undefined
|
|
82
|
+
if (platform === 'darwin') {
|
|
83
|
+
const result = await pi.exec('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'], {
|
|
84
|
+
cwd: os.homedir(),
|
|
85
|
+
timeout: KEYCHAIN_TIMEOUT_MS
|
|
86
|
+
})
|
|
87
|
+
if (result.code && result.code !== 44) throw new Error(`keychain lookup failed (exit ${result.code})`)
|
|
88
|
+
raw = result.stdout
|
|
89
|
+
}
|
|
90
|
+
if (!raw) {
|
|
91
|
+
raw = await fs.readFile(path.join(os.homedir(), '.claude', '.credentials.json'), 'utf8').catch(() => undefined)
|
|
92
|
+
}
|
|
93
|
+
return claudeAccessToken(raw)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function fetchAnthropicUsage(token: string): Promise<AnthropicUsagePayload> {
|
|
97
|
+
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
98
|
+
headers: {
|
|
99
|
+
authorization: `Bearer ${token}`,
|
|
100
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
101
|
+
accept: 'application/json'
|
|
102
|
+
},
|
|
103
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
104
|
+
})
|
|
105
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
106
|
+
const payload: unknown = await response.json()
|
|
107
|
+
if (!isUsage(payload)) throw new Error('usage response did not match the Anthropic schema')
|
|
108
|
+
return payload
|
|
109
|
+
}
|