quotacap 0.0.35 → 0.0.37
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 +2 -2
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/muse.d.ts +28 -0
- package/dist/adapters/muse.js +124 -33
- package/dist/diagnostics/failure.js +5 -3
- package/dist/version.js +1 -1
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-NtT9XTrm.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DiOyUuQc.css +0 -1
- /package/web/dist/assets/{index-Cm_QtC0G.js → index-gDYktkri.js} +0 -0
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ QuotaCap helps you get more from the AI coding subscriptions you already pay for
|
|
|
23
23
|
| Codex | `pty` — `codex --no-alt-screen` then `/status`, parse `Weekly/5h limit: X% left` | Live |
|
|
24
24
|
| Kimi Code | `pty` — `kimi` then `/usage`, parse `Weekly/5h limit: Y% used` | Live |
|
|
25
25
|
| Grok | `pty` — `grok` then `/usage`, parse `Weekly limit (plan)` + `Credits: $X` | Live |
|
|
26
|
-
| Muse Code | `pty` — `muse --trust-workspace` then `/usage`, parse `Subscription · Muse Code <plan>` + `Weekly/Current N% used` | Live |
|
|
26
|
+
| Muse Code | `pty` — `muse --trust-workspace` then `/usage`, parse `Subscription · Muse Code <plan>` + `Weekly/Current N% used`; on `Currently unavailable`, up to three headless `muse exec` warm turns and re-reads inside the poll | Live |
|
|
27
27
|
|
|
28
|
-
Exec adapters run via `execFile` with an argv list. PTY adapters run via `node-pty` (`src/adapters/pty.ts`). They are TUI-fragile: a vendor text change breaks the parser and the row degrades fail-closed until the regex is fixed. Poll latency is 2–10 s per PTY provider (settle plus completion). It dominates `POST /api/refresh` and the first poll, not the steady-state 15 m timer.
|
|
28
|
+
Exec adapters run via `execFile` with an argv list. PTY adapters run via `node-pty` (`src/adapters/pty.ts`). They are TUI-fragile: a vendor text change breaks the parser and the row degrades fail-closed until the regex is fixed. Poll latency is 2–10 s per PTY provider (settle plus completion); `muse` can reach about 75 s when it has to warm an unavailable subscription. It dominates `POST /api/refresh` and the first poll, not the steady-state 15 m timer.
|
|
29
29
|
|
|
30
30
|
Live adapters invoke the CLIs you already logged into. No API keys. No token files are read.
|
|
31
31
|
|
package/dist/adapters/index.js
CHANGED
package/dist/adapters/muse.d.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import type { ParsedQuota } from "./types.js";
|
|
2
|
+
/** Canonical unavailability error: parseMuseTui throws it, poll() recognises it
|
|
3
|
+
* to start recovery, and failure.ts classifies its text as service_unavailable. */
|
|
4
|
+
export declare const MUSE_UNAVAILABLE_MESSAGE = "muse: subscription currently unavailable in TUI output";
|
|
5
|
+
export declare const WARM_ATTEMPTS = 3;
|
|
6
|
+
export declare const WARM_SETTLE_MS = 3000;
|
|
7
|
+
export declare const WARM_EXEC_TIMEOUT_MS = 20000;
|
|
8
|
+
export declare const FALLBACK_BUDGET_MS = 60000;
|
|
9
|
+
export interface RecoveryDeps {
|
|
10
|
+
unavailable: Error;
|
|
11
|
+
usagePass: (timeoutMs: number) => Promise<ParsedQuota>;
|
|
12
|
+
warmTurn: (timeoutMs: number) => Promise<void>;
|
|
13
|
+
sleep?: (ms: number) => Promise<void>;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
aborted?: () => boolean;
|
|
16
|
+
log?: (message: string) => void;
|
|
17
|
+
attempts?: number;
|
|
18
|
+
settleMs?: number;
|
|
19
|
+
budgetMs?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Recovery loop for an unavailable subscription, entered only on
|
|
23
|
+
* MUSE_UNAVAILABLE_MESSAGE. Attempt = settle, headless warm turn, settle,
|
|
24
|
+
* /usage re-read. The budget is checked before every step, and the warm turn,
|
|
25
|
+
* sleep and readiness/completion waits are clamped to what remains; the
|
|
26
|
+
* adapter's 90 s timeout is the backstop for the fixed per-pass overheads.
|
|
27
|
+
* Exhaustion rethrows the last unavailability error.
|
|
28
|
+
*/
|
|
29
|
+
export declare function recoverSubscription(deps: RecoveryDeps): Promise<ParsedQuota>;
|
|
2
30
|
export declare function museProbeDir(): string;
|
|
3
31
|
export declare function parseMuseTui(text: string, now?: Date): ParsedQuota;
|
|
4
32
|
export declare const museAdapter: {
|
package/dist/adapters/muse.js
CHANGED
|
@@ -3,7 +3,71 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseResetText } from "./parse.js";
|
|
5
5
|
import { runPty, stripAnsi } from "./pty.js";
|
|
6
|
-
import { adapterSignal } from "../runtime/spawn.js";
|
|
6
|
+
import { adapterSignal, trackedExecFile } from "../runtime/spawn.js";
|
|
7
|
+
/** Canonical unavailability error: parseMuseTui throws it, poll() recognises it
|
|
8
|
+
* to start recovery, and failure.ts classifies its text as service_unavailable. */
|
|
9
|
+
export const MUSE_UNAVAILABLE_MESSAGE = "muse: subscription currently unavailable in TUI output";
|
|
10
|
+
export const WARM_ATTEMPTS = 3;
|
|
11
|
+
export const WARM_SETTLE_MS = 3000;
|
|
12
|
+
export const WARM_EXEC_TIMEOUT_MS = 20000;
|
|
13
|
+
export const FALLBACK_BUDGET_MS = 60000;
|
|
14
|
+
const USAGE_TIMEOUT_MS = 14000;
|
|
15
|
+
const MIN_STEP_MS = 1000;
|
|
16
|
+
function delay(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Recovery loop for an unavailable subscription, entered only on
|
|
21
|
+
* MUSE_UNAVAILABLE_MESSAGE. Attempt = settle, headless warm turn, settle,
|
|
22
|
+
* /usage re-read. The budget is checked before every step, and the warm turn,
|
|
23
|
+
* sleep and readiness/completion waits are clamped to what remains; the
|
|
24
|
+
* adapter's 90 s timeout is the backstop for the fixed per-pass overheads.
|
|
25
|
+
* Exhaustion rethrows the last unavailability error.
|
|
26
|
+
*/
|
|
27
|
+
export async function recoverSubscription(deps) {
|
|
28
|
+
const sleep = deps.sleep ?? delay;
|
|
29
|
+
const now = deps.now ?? Date.now;
|
|
30
|
+
const aborted = deps.aborted ?? (() => adapterSignal("muse")?.aborted === true);
|
|
31
|
+
const log = deps.log ?? ((message) => console.warn(`[quotacap] muse: ${message}`));
|
|
32
|
+
const attempts = deps.attempts ?? WARM_ATTEMPTS;
|
|
33
|
+
const settleMs = deps.settleMs ?? WARM_SETTLE_MS;
|
|
34
|
+
const deadline = now() + (deps.budgetMs ?? FALLBACK_BUDGET_MS);
|
|
35
|
+
const remaining = () => deadline - now();
|
|
36
|
+
let unavailable = deps.unavailable;
|
|
37
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
38
|
+
if (remaining() < MIN_STEP_MS)
|
|
39
|
+
break;
|
|
40
|
+
await sleep(Math.min(settleMs, remaining()));
|
|
41
|
+
if (remaining() < MIN_STEP_MS)
|
|
42
|
+
break;
|
|
43
|
+
try {
|
|
44
|
+
await deps.warmTurn(Math.min(WARM_EXEC_TIMEOUT_MS, remaining()));
|
|
45
|
+
}
|
|
46
|
+
catch (warmError) {
|
|
47
|
+
if (aborted())
|
|
48
|
+
throw warmError;
|
|
49
|
+
log(`warm attempt ${attempt} failed: ${warmError?.message}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (remaining() < MIN_STEP_MS)
|
|
53
|
+
break;
|
|
54
|
+
await sleep(Math.min(settleMs, remaining()));
|
|
55
|
+
if (remaining() < MIN_STEP_MS)
|
|
56
|
+
break;
|
|
57
|
+
try {
|
|
58
|
+
const quota = await deps.usagePass(Math.min(USAGE_TIMEOUT_MS, remaining()));
|
|
59
|
+
log(`subscription unavailable; recovered after ${attempt} warm prompt(s)`);
|
|
60
|
+
return quota;
|
|
61
|
+
}
|
|
62
|
+
catch (readError) {
|
|
63
|
+
if (readError?.message !== MUSE_UNAVAILABLE_MESSAGE)
|
|
64
|
+
throw readError;
|
|
65
|
+
unavailable = readError;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
log("subscription still unavailable; warm recovery exhausted");
|
|
69
|
+
throw unavailable;
|
|
70
|
+
}
|
|
7
71
|
// QuotaCap-owned empty probe dir, beside the config and database. Muse loads
|
|
8
72
|
// project-local skills, rules, hooks and plugin config from cwd, so the probe
|
|
9
73
|
// never runs in $HOME or a user project — an empty dir grants nothing.
|
|
@@ -15,7 +79,7 @@ export function parseMuseTui(text, now = new Date()) {
|
|
|
15
79
|
// newlines), so no pattern here may anchor on ^, $ or \n.
|
|
16
80
|
const cleaned = stripAnsi(text);
|
|
17
81
|
if (/currently unavailable|subscriptions aren't currently available|subscription_unavailable/i.test(cleaned)) {
|
|
18
|
-
throw new Error(
|
|
82
|
+
throw new Error(MUSE_UNAVAILABLE_MESSAGE);
|
|
19
83
|
}
|
|
20
84
|
const weekly = cleaned.match(/Weekly\s+(\d+)%\s+used/i);
|
|
21
85
|
if (!weekly)
|
|
@@ -60,40 +124,67 @@ export function parseMuseTui(text, now = new Date()) {
|
|
|
60
124
|
raw: cleaned.slice(0, 4096),
|
|
61
125
|
};
|
|
62
126
|
}
|
|
127
|
+
/** One `/usage` read: the TUI scrape, shared by the first attempt and every
|
|
128
|
+
* recovery re-read. */
|
|
129
|
+
async function usagePass(timeoutMs = USAGE_TIMEOUT_MS) {
|
|
130
|
+
const cwd = museProbeDir();
|
|
131
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
132
|
+
const transcript = await runPty({
|
|
133
|
+
file: "muse",
|
|
134
|
+
args: ["--trust-workspace"],
|
|
135
|
+
cwd,
|
|
136
|
+
// Without this, a missing binary triggers a foreground 248 MB download
|
|
137
|
+
// on every poll; with it the launcher dies fast with a clear message.
|
|
138
|
+
env: { MUSE_NO_AUTO_UPDATE: "1" },
|
|
139
|
+
cols: 140,
|
|
140
|
+
rows: 50,
|
|
141
|
+
readyRegex: /muse-spark|Muse Code \d/,
|
|
142
|
+
readyTimeoutMs: 8000,
|
|
143
|
+
settleDelayMs: 1000,
|
|
144
|
+
// Two-phase submit: "/usage" first, CR 1500ms later. A single combined
|
|
145
|
+
// write is swallowed by the slash-command autocomplete and never runs.
|
|
146
|
+
input: "/usage",
|
|
147
|
+
submitInput: "\r",
|
|
148
|
+
submitAfterMs: 1500,
|
|
149
|
+
completionRegex: /Subscription/i,
|
|
150
|
+
// Trust prompt and the accidental-turn guard stay fail-closed. The
|
|
151
|
+
// unavailable-subscription message is deliberately not here: parseMuseTui
|
|
152
|
+
// owns detection so the poll can recover from it.
|
|
153
|
+
abortOn: /Do you trust this workspace|Working \(\d+s/,
|
|
154
|
+
timeoutMs,
|
|
155
|
+
respondToQueries: true,
|
|
156
|
+
maxBytes: 256 * 1024,
|
|
157
|
+
signal: adapterSignal("muse"),
|
|
158
|
+
label: "muse",
|
|
159
|
+
});
|
|
160
|
+
return parseMuseTui(transcript);
|
|
161
|
+
}
|
|
63
162
|
export const museAdapter = {
|
|
64
163
|
id: "muse",
|
|
65
164
|
requiresAuth: "muse login (CLI owns credentials)",
|
|
66
165
|
async poll() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
abortOn: /Do you trust this workspace|Working \(\d+s|Subscriptions aren't currently available|subscription_unavailable/,
|
|
91
|
-
timeoutMs: 14000,
|
|
92
|
-
respondToQueries: true,
|
|
93
|
-
maxBytes: 256 * 1024,
|
|
94
|
-
signal: adapterSignal("muse"),
|
|
95
|
-
label: "muse",
|
|
96
|
-
});
|
|
97
|
-
return parseMuseTui(transcript);
|
|
166
|
+
// Captured before the first pass: pollAll clears the adapter signal map
|
|
167
|
+
// entry in a microtask when its gate rejects, but clearing the map does
|
|
168
|
+
// not reset the signal object, so the loop can still observe the abort.
|
|
169
|
+
const signal = adapterSignal("muse");
|
|
170
|
+
try {
|
|
171
|
+
return await usagePass();
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (e?.message !== MUSE_UNAVAILABLE_MESSAGE)
|
|
175
|
+
throw e;
|
|
176
|
+
return recoverSubscription({
|
|
177
|
+
unavailable: e,
|
|
178
|
+
usagePass: (timeoutMs) => usagePass(timeoutMs),
|
|
179
|
+
warmTurn: async (timeoutMs) => {
|
|
180
|
+
await trackedExecFile("muse", "muse", ["exec", "hi"], {
|
|
181
|
+
cwd: museProbeDir(),
|
|
182
|
+
env: { ...process.env, MUSE_NO_AUTO_UPDATE: "1" },
|
|
183
|
+
timeout: timeoutMs,
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
aborted: () => signal?.aborted === true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
98
189
|
},
|
|
99
190
|
};
|
|
@@ -408,7 +408,7 @@ function mapCategory(code) {
|
|
|
408
408
|
return "unknown";
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
-
function getSummaryAndAction(code, providerName) {
|
|
411
|
+
function getSummaryAndAction(code, providerName, provider) {
|
|
412
412
|
switch (code) {
|
|
413
413
|
case "terminal_error":
|
|
414
414
|
return {
|
|
@@ -443,7 +443,9 @@ function getSummaryAndAction(code, providerName) {
|
|
|
443
443
|
case "service_unavailable":
|
|
444
444
|
return {
|
|
445
445
|
summary: `${providerName} usage currently unavailable`,
|
|
446
|
-
action:
|
|
446
|
+
action: provider === "muse"
|
|
447
|
+
? `${providerName} reported that subscription usage is currently unavailable. QuotaCap already retried automatically with a warm-up prompt. If this persists, send a prompt in ${providerName}, or wait for the next scheduled poll.`
|
|
448
|
+
: `${providerName} reported that subscription usage is currently unavailable. Send a prompt in ${providerName} to refresh its session limits, or wait for the next scheduled poll.`,
|
|
447
449
|
};
|
|
448
450
|
case "timeout":
|
|
449
451
|
return {
|
|
@@ -473,7 +475,7 @@ export function classifyFailure(provider, reason) {
|
|
|
473
475
|
}
|
|
474
476
|
const category = mapCategory(diag.diagnosticCode);
|
|
475
477
|
const pName = formatProviderName(provider);
|
|
476
|
-
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName);
|
|
478
|
+
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName, provider);
|
|
477
479
|
return {
|
|
478
480
|
diagnosticCode: diag.diagnosticCode,
|
|
479
481
|
errorDetail: diag.errorDetail,
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.37";
|