shraga 0.1.77 → 0.1.78
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 +1 -1
- package/src/server/claude-usage.ts +33 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// the Claude Code CLI maintains on this box. Fails CLOSED: every error path returns null, and the
|
|
3
3
|
// caller renders nothing — a broken or zeroed gauge is worse than no gauge.
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
5
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
|
+
import { dataPath } from './paths.ts';
|
|
10
11
|
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
12
13
|
|
|
@@ -71,6 +72,10 @@ export class ClaudeUsageOptions {
|
|
|
71
72
|
keychainService = 'Claude Code-credentials';
|
|
72
73
|
/** A locked keychain can block (or prompt) indefinitely — never let that stall a client poll. */
|
|
73
74
|
keychainTimeoutMs = 3_000;
|
|
75
|
+
/** Where the last known-good reading is mirrored, so a restart (deploy, self-upgrade) does not
|
|
76
|
+
* blank the gauge on a box whose upstream is rate-limited for the next hour. Identity + percentages
|
|
77
|
+
* only — never a token. */
|
|
78
|
+
cachePath = dataPath('claude-usage-last.json');
|
|
74
79
|
/** Seam: hands back the raw secret string, or null on ANY failure. Tests inject here so the suite
|
|
75
80
|
* never shells out to the real `security` binary. */
|
|
76
81
|
readKeychain: (options: ClaudeUsageOptions) => Promise<string | null> = readKeychainSecret;
|
|
@@ -144,6 +149,8 @@ export class ClaudeUsageReader {
|
|
|
144
149
|
* a widget that flickers in and out reads as a bug. We keep serving this, flagged `stale`, and the
|
|
145
150
|
* client shows how old it is. Only a box that never had a good reading answers null. */
|
|
146
151
|
private lastGood: { at: number; value: ClaudeUsage } | null = null;
|
|
152
|
+
/** One-shot rehydrate of `lastGood` from disk, awaited by the first get(). */
|
|
153
|
+
private restored: Promise<void> | null = null;
|
|
147
154
|
|
|
148
155
|
public constructor(options?: Partial<ClaudeUsageOptions>) {
|
|
149
156
|
this.options = { ...new ClaudeUsageOptions(), ...options };
|
|
@@ -151,6 +158,7 @@ export class ClaudeUsageReader {
|
|
|
151
158
|
|
|
152
159
|
/** null => this box is not on a Claude subscription, or we could not prove that it ever was. */
|
|
153
160
|
async get(): Promise<ClaudeUsage | null> {
|
|
161
|
+
await (this.restored ??= this.restore());
|
|
154
162
|
const now = Date.now();
|
|
155
163
|
if (now < this.cooldownUntil) return this.stale();
|
|
156
164
|
if (this.cache && now - this.cache.at < this.options.ttlMs) return this.cache.value ?? this.stale();
|
|
@@ -162,13 +170,36 @@ export class ClaudeUsageReader {
|
|
|
162
170
|
const at = Date.now();
|
|
163
171
|
const stamped = value ? { ...value, fetchedAt: new Date(at).toISOString() } : null;
|
|
164
172
|
this.cache = { at, value: stamped };
|
|
165
|
-
if (stamped) this.lastGood = { at, value: stamped };
|
|
173
|
+
if (stamped) { this.lastGood = { at, value: stamped }; void this.persist(this.lastGood); }
|
|
166
174
|
return stamped ?? this.stale();
|
|
167
175
|
})
|
|
168
176
|
.finally(() => { this.inflight = null; });
|
|
169
177
|
return this.inflight;
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
/** Rehydrate the last reading a previous process wrote. Never throws: a missing or corrupt file
|
|
181
|
+
* just means we start with nothing, exactly as before. */
|
|
182
|
+
private async restore(): Promise<void> {
|
|
183
|
+
try {
|
|
184
|
+
const saved = JSON.parse(await readFile(this.options.cachePath, 'utf8'));
|
|
185
|
+
if (Array.isArray(saved?.value?.limits) && saved.value.limits.length && typeof saved.at === 'number') {
|
|
186
|
+
this.lastGood = { at: saved.at, value: saved.value };
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
190
|
+
if (code !== 'ENOENT') console.debug(`${TAG} could not restore the last reading: ${(err as Error).message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Mirror a fresh reading to disk. Best-effort — a write failure must never break the response. */
|
|
195
|
+
private async persist(entry: { at: number; value: ClaudeUsage }) {
|
|
196
|
+
try {
|
|
197
|
+
await writeFile(this.options.cachePath, JSON.stringify(entry));
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.debug(`${TAG} could not persist the last reading: ${(err as Error).message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
172
203
|
/** Last known-good reading, marked stale. Never invents numbers — null when we never had any. */
|
|
173
204
|
private stale(): ClaudeUsage | null {
|
|
174
205
|
return this.lastGood ? { ...this.lastGood.value, stale: true } : null;
|