quotacap 0.0.19 → 0.0.22
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 +93 -68
- package/dist/adapters/agy.d.ts +3 -0
- package/dist/adapters/agy.js +81 -0
- package/dist/adapters/claude.d.ts +7 -5
- package/dist/adapters/claude.js +6 -6
- package/dist/adapters/codex.d.ts +3 -3
- package/dist/adapters/codex.js +128 -49
- package/dist/adapters/grok.d.ts +3 -3
- package/dist/adapters/grok.js +114 -70
- package/dist/adapters/index.d.ts +5 -2
- package/dist/adapters/index.js +36 -9
- package/dist/adapters/kimi.d.ts +3 -3
- package/dist/adapters/kimi.js +64 -86
- package/dist/adapters/manual.d.ts +4 -4
- package/dist/adapters/manual.js +2 -2
- package/dist/adapters/pty.d.ts +23 -0
- package/dist/adapters/pty.js +487 -0
- package/dist/adapters/types.d.ts +7 -3
- package/dist/advisory/engine.d.ts +19 -2
- package/dist/advisory/engine.js +130 -10
- package/dist/advisory/snapshot.d.ts +16 -0
- package/dist/advisory/snapshot.js +213 -0
- package/dist/advisory/types.d.ts +46 -4
- package/dist/advisory/validate.d.ts +5 -0
- package/dist/advisory/validate.js +18 -0
- package/dist/cli/advise.d.ts +3 -0
- package/dist/cli/advise.js +62 -0
- package/dist/cli/clients.d.ts +18 -0
- package/dist/cli/clients.js +11 -0
- package/dist/cli/index.js +4 -84
- package/dist/cli/ingest.d.ts +3 -0
- package/dist/cli/ingest.js +31 -0
- package/dist/cli/runtime.d.ts +12 -0
- package/dist/cli/runtime.js +125 -0
- package/dist/cli/snapshot-source.d.ts +25 -0
- package/dist/cli/snapshot-source.js +223 -0
- package/dist/cli/status.d.ts +3 -0
- package/dist/cli/status.js +84 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +126 -1
- package/dist/daemon.d.ts +4 -29
- package/dist/daemon.js +12 -67
- package/dist/format/markdown.d.ts +3 -0
- package/dist/format/markdown.js +24 -0
- package/dist/format/rows.d.ts +27 -0
- package/dist/format/rows.js +241 -0
- package/dist/format/terminal.d.ts +12 -0
- package/dist/format/terminal.js +144 -0
- package/dist/http/server.d.ts +24 -3
- package/dist/http/server.js +236 -106
- package/dist/mcp/server.d.ts +0 -6
- package/dist/mcp/server.js +78 -25
- package/dist/runtime/client.d.ts +24 -0
- package/dist/runtime/client.js +72 -0
- package/dist/runtime/owner.d.ts +35 -0
- package/dist/runtime/owner.js +393 -0
- package/dist/runtime/poll.d.ts +48 -0
- package/dist/runtime/poll.js +222 -0
- package/dist/runtime/service.d.ts +24 -0
- package/dist/runtime/service.js +251 -0
- package/dist/runtime/spawn.d.ts +17 -0
- package/dist/runtime/spawn.js +97 -0
- package/dist/runtime/token.d.ts +3 -0
- package/dist/runtime/token.js +64 -0
- package/dist/service/index.d.ts +4 -0
- package/dist/service/index.js +47 -0
- package/dist/service/macos.d.ts +96 -0
- package/dist/service/macos.js +551 -0
- package/dist/src/adapters/agy.d.ts +3 -0
- package/dist/src/adapters/agy.js +81 -0
- package/dist/src/adapters/claude.d.ts +7 -5
- package/dist/src/adapters/claude.js +6 -6
- package/dist/src/adapters/codex.d.ts +3 -3
- package/dist/src/adapters/codex.js +128 -49
- package/dist/src/adapters/grok.d.ts +3 -3
- package/dist/src/adapters/grok.js +114 -70
- package/dist/src/adapters/index.d.ts +5 -2
- package/dist/src/adapters/index.js +36 -9
- package/dist/src/adapters/kimi.d.ts +3 -3
- package/dist/src/adapters/kimi.js +64 -86
- package/dist/src/adapters/manual.d.ts +4 -4
- package/dist/src/adapters/manual.js +2 -2
- package/dist/src/adapters/pty.d.ts +23 -0
- package/dist/src/adapters/pty.js +487 -0
- package/dist/src/adapters/types.d.ts +7 -3
- package/dist/src/advisory/engine.d.ts +19 -2
- package/dist/src/advisory/engine.js +130 -10
- package/dist/src/advisory/snapshot.d.ts +16 -0
- package/dist/src/advisory/snapshot.js +213 -0
- package/dist/src/advisory/types.d.ts +46 -4
- package/dist/src/advisory/validate.d.ts +5 -0
- package/dist/src/advisory/validate.js +18 -0
- package/dist/src/cli/advise.d.ts +3 -0
- package/dist/src/cli/advise.js +62 -0
- package/dist/src/cli/clients.d.ts +18 -0
- package/dist/src/cli/clients.js +11 -0
- package/dist/src/cli/index.js +4 -84
- package/dist/src/cli/ingest.d.ts +3 -0
- package/dist/src/cli/ingest.js +31 -0
- package/dist/src/cli/runtime.d.ts +12 -0
- package/dist/src/cli/runtime.js +125 -0
- package/dist/src/cli/snapshot-source.d.ts +25 -0
- package/dist/src/cli/snapshot-source.js +223 -0
- package/dist/src/cli/status.d.ts +3 -0
- package/dist/src/cli/status.js +84 -0
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +126 -1
- package/dist/src/daemon.d.ts +4 -29
- package/dist/src/daemon.js +12 -67
- package/dist/src/format/markdown.d.ts +3 -0
- package/dist/src/format/markdown.js +24 -0
- package/dist/src/format/rows.d.ts +27 -0
- package/dist/src/format/rows.js +241 -0
- package/dist/src/format/terminal.d.ts +12 -0
- package/dist/src/format/terminal.js +144 -0
- package/dist/src/http/server.d.ts +24 -3
- package/dist/src/http/server.js +236 -106
- package/dist/src/mcp/server.d.ts +0 -6
- package/dist/src/mcp/server.js +78 -25
- package/dist/src/runtime/client.d.ts +24 -0
- package/dist/src/runtime/client.js +72 -0
- package/dist/src/runtime/owner.d.ts +35 -0
- package/dist/src/runtime/owner.js +393 -0
- package/dist/src/runtime/poll.d.ts +48 -0
- package/dist/src/runtime/poll.js +222 -0
- package/dist/src/runtime/service.d.ts +24 -0
- package/dist/src/runtime/service.js +251 -0
- package/dist/src/runtime/spawn.d.ts +17 -0
- package/dist/src/runtime/spawn.js +97 -0
- package/dist/src/runtime/token.d.ts +3 -0
- package/dist/src/runtime/token.js +64 -0
- package/dist/src/service/index.d.ts +4 -0
- package/dist/src/service/index.js +47 -0
- package/dist/src/service/macos.d.ts +96 -0
- package/dist/src/service/macos.js +551 -0
- package/dist/src/store/attempts.d.ts +12 -0
- package/dist/src/store/attempts.js +29 -0
- package/dist/src/store/db.d.ts +1 -1
- package/dist/src/store/db.js +58 -2
- package/dist/src/store/quotas.js +52 -10
- package/dist/src/version.js +1 -1
- package/dist/src/webAssets.js +1 -1
- package/dist/src/webHtml.js +1 -1
- package/dist/store/attempts.d.ts +12 -0
- package/dist/store/attempts.js +29 -0
- package/dist/store/db.d.ts +1 -1
- package/dist/store/db.js +58 -2
- package/dist/store/quotas.js +52 -10
- package/dist/tests/adapters/agy.test.js +337 -0
- package/dist/tests/adapters/claude.test.js +41 -1
- package/dist/tests/adapters/codex.test.js +231 -91
- package/dist/tests/adapters/credential-free.test.js +211 -0
- package/dist/tests/adapters/grok.test.js +239 -135
- package/dist/tests/adapters/kimi.test.js +219 -104
- package/dist/tests/adapters/pty.test.d.ts +1 -0
- package/dist/tests/adapters/pty.test.js +240 -0
- package/dist/tests/adapters/registry.test.js +5 -5
- package/dist/tests/advisory/engine.test.js +194 -6
- package/dist/tests/advisory/pace.test.d.ts +1 -0
- package/dist/tests/advisory/pace.test.js +60 -0
- package/dist/tests/advisory/snapshot.test.d.ts +1 -0
- package/dist/tests/advisory/snapshot.test.js +65 -0
- package/dist/tests/advisory/validate.test.d.ts +1 -0
- package/dist/tests/advisory/validate.test.js +19 -0
- package/dist/tests/cli/advise.test.d.ts +1 -0
- package/dist/tests/cli/advise.test.js +294 -0
- package/dist/tests/cli/cli.test.js +39 -19
- package/dist/tests/cli/helpers.d.ts +9 -0
- package/dist/tests/cli/helpers.js +93 -0
- package/dist/tests/cli/ingest.test.d.ts +1 -0
- package/dist/tests/cli/ingest.test.js +188 -0
- package/dist/tests/cli/parity.test.d.ts +1 -0
- package/dist/tests/cli/parity.test.js +225 -0
- package/dist/tests/cli/snapshot-source.test.d.ts +1 -0
- package/dist/tests/cli/snapshot-source.test.js +305 -0
- package/dist/tests/cli/status.test.d.ts +1 -0
- package/dist/tests/cli/status.test.js +423 -0
- package/dist/tests/daemon.test.js +140 -38
- package/dist/tests/fixtures/stable-state.d.ts +22 -0
- package/dist/tests/fixtures/stable-state.js +152 -0
- package/dist/tests/fixtures/stable-state.test.d.ts +1 -0
- package/dist/tests/fixtures/stable-state.test.js +17 -0
- package/dist/tests/format/markdown.test.d.ts +1 -0
- package/dist/tests/format/markdown.test.js +49 -0
- package/dist/tests/format/terminal.test.d.ts +1 -0
- package/dist/tests/format/terminal.test.js +476 -0
- package/dist/tests/http/api.test.js +224 -2
- package/dist/tests/integration.test.js +20 -7
- package/dist/tests/mcp/protocol.test.d.ts +1 -0
- package/dist/tests/mcp/protocol.test.js +134 -0
- package/dist/tests/mcp/quotas.test.js +38 -6
- package/dist/tests/mcp/tools.test.d.ts +1 -0
- package/dist/tests/mcp/tools.test.js +291 -0
- package/dist/tests/runtime/cancel.test.d.ts +1 -0
- package/dist/tests/runtime/cancel.test.js +264 -0
- package/dist/tests/runtime/cli.test.d.ts +1 -0
- package/dist/tests/runtime/cli.test.js +226 -0
- package/dist/tests/runtime/config.test.d.ts +1 -0
- package/dist/tests/runtime/config.test.js +144 -0
- package/dist/tests/runtime/http.test.d.ts +1 -0
- package/dist/tests/runtime/http.test.js +267 -0
- package/dist/tests/runtime/lifecycle.test.d.ts +1 -0
- package/dist/tests/runtime/lifecycle.test.js +303 -0
- package/dist/tests/runtime/ownership.test.d.ts +1 -0
- package/dist/tests/runtime/ownership.test.js +355 -0
- package/dist/tests/runtime/poll.test.d.ts +1 -0
- package/dist/tests/runtime/poll.test.js +410 -0
- package/dist/tests/runtime/status.test.d.ts +1 -0
- package/dist/tests/runtime/status.test.js +232 -0
- package/dist/tests/service/macos.test.d.ts +1 -0
- package/dist/tests/service/macos.test.js +349 -0
- package/dist/tests/store/attempts.test.d.ts +1 -0
- package/dist/tests/store/attempts.test.js +29 -0
- package/dist/tests/store/db.test.js +61 -6
- package/dist/tests/store/migration.test.d.ts +1 -0
- package/dist/tests/store/migration.test.js +41 -0
- package/dist/tests/web/advice-drawer.test.d.ts +1 -0
- package/dist/tests/web/advice-drawer.test.js +45 -0
- package/dist/tests/web/dashboard.e2e.d.ts +1 -0
- package/dist/tests/web/dashboard.e2e.js +670 -0
- package/dist/tests/web/header.test.d.ts +1 -0
- package/dist/tests/web/header.test.js +56 -0
- package/dist/tests/web/names.test.d.ts +1 -0
- package/dist/tests/web/names.test.js +19 -0
- package/dist/tests/web/onboarding.test.d.ts +1 -0
- package/dist/tests/web/onboarding.test.js +21 -0
- package/dist/tests/web/pacing.test.d.ts +1 -0
- package/dist/tests/web/pacing.test.js +62 -0
- package/dist/tests/web/playwright.config.d.ts +2 -0
- package/dist/tests/web/playwright.config.js +10 -0
- package/dist/tests/web/provider-drawer.test.d.ts +1 -0
- package/dist/tests/web/provider-drawer.test.js +68 -0
- package/dist/tests/web/recommendation.test.d.ts +1 -0
- package/dist/tests/web/recommendation.test.js +38 -0
- package/dist/tests/web/reset-rail.test.d.ts +1 -0
- package/dist/tests/web/reset-rail.test.js +63 -0
- package/dist/tests/web/responsive.test.d.ts +1 -0
- package/dist/tests/web/responsive.test.js +21 -0
- package/dist/tests/web/settings.test.d.ts +1 -0
- package/dist/tests/web/settings.test.js +32 -0
- package/dist/tests/web/state-catalog.test.d.ts +1 -0
- package/dist/tests/web/state-catalog.test.js +206 -0
- package/dist/tests/web/state.test.d.ts +1 -0
- package/dist/tests/web/state.test.js +27 -0
- package/dist/tests/web/stub-server.d.ts +37 -0
- package/dist/tests/web/stub-server.js +132 -0
- package/dist/tests/web/theme.test.d.ts +1 -0
- package/dist/tests/web/theme.test.js +12 -0
- package/dist/version.js +1 -1
- package/dist/web/src/App.d.ts +1 -0
- package/dist/web/src/App.js +106 -78
- package/dist/web/src/Theme.d.ts +11 -0
- package/dist/web/src/Theme.js +53 -0
- package/dist/web/src/api.d.ts +27 -2
- package/dist/web/src/api.js +102 -6
- package/dist/web/src/components/AdviceDrawer.d.ts +9 -0
- package/dist/web/src/components/AdviceDrawer.js +77 -0
- package/dist/web/src/components/FaultBanner.d.ts +17 -0
- package/dist/web/src/components/FaultBanner.js +60 -0
- package/dist/web/src/components/Header.d.ts +22 -0
- package/dist/web/src/components/Header.js +46 -0
- package/dist/web/src/components/PaceBar.d.ts +51 -0
- package/dist/web/src/components/PaceBar.js +251 -0
- package/dist/web/src/components/PacingLegend.d.ts +3 -0
- package/dist/web/src/components/PacingLegend.js +43 -0
- package/dist/web/src/components/ProviderCard.d.ts +8 -0
- package/dist/web/src/components/ProviderCard.js +27 -0
- package/dist/web/src/components/ProviderDrawer.d.ts +52 -0
- package/dist/web/src/components/ProviderDrawer.js +270 -0
- package/dist/web/src/components/ProviderIcon.d.ts +7 -0
- package/dist/web/src/components/ProviderIcon.js +27 -0
- package/dist/web/src/components/ProviderRow.d.ts +8 -0
- package/dist/web/src/components/ProviderRow.js +14 -0
- package/dist/web/src/components/Recommendation.d.ts +27 -0
- package/dist/web/src/components/Recommendation.js +77 -0
- package/dist/web/src/components/ResetRail.d.ts +38 -0
- package/dist/web/src/components/ResetRail.js +188 -0
- package/dist/web/src/components/SubscriptionList.d.ts +19 -0
- package/dist/web/src/components/SubscriptionList.js +107 -0
- package/dist/web/src/components/settings/CliIntegrationPanel.d.ts +2 -0
- package/dist/web/src/components/settings/CliIntegrationPanel.js +49 -0
- package/dist/web/src/components/settings/DaemonPanel.d.ts +6 -0
- package/dist/web/src/components/settings/DaemonPanel.js +45 -0
- package/dist/web/src/components/settings/ProvidersPanel.d.ts +14 -0
- package/dist/web/src/components/settings/ProvidersPanel.js +69 -0
- package/dist/web/src/components/settings/SettingsDrawer.d.ts +9 -0
- package/dist/web/src/components/settings/SettingsDrawer.js +55 -0
- package/dist/web/src/names.d.ts +3 -0
- package/dist/web/src/names.js +19 -0
- package/dist/web/src/pages/Onboarding.d.ts +17 -0
- package/dist/web/src/pages/Onboarding.js +96 -0
- package/dist/web/src/router.d.ts +11 -0
- package/dist/web/src/router.js +31 -0
- package/dist/web/src/state.d.ts +97 -0
- package/dist/web/src/state.js +80 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +33 -8
- package/scripts/fix-pty-perms.mjs +21 -0
- package/web/dist/assets/index-CyUkWdwJ.css +1 -0
- package/web/dist/assets/index-PN4RpMKG.js +12 -0
- package/web/dist/index.html +19 -2
- package/dist/adapters/core.d.ts +0 -9
- package/dist/adapters/core.js +0 -90
- package/dist/format/table.d.ts +0 -2
- package/dist/format/table.js +0 -56
- package/dist/src/adapters/core.d.ts +0 -9
- package/dist/src/adapters/core.js +0 -90
- package/dist/src/format/table.d.ts +0 -2
- package/dist/src/format/table.js +0 -56
- package/dist/tests/adapters/core.test.js +0 -110
- package/dist/tests/format/table.test.js +0 -98
- package/web/dist/assets/index-Dpiqzcc8.js +0 -40
- /package/dist/tests/adapters/{core.test.d.ts → agy.test.d.ts} +0 -0
- /package/dist/tests/{format/table.test.d.ts → adapters/credential-free.test.d.ts} +0 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { registerTrackedChild } from "../runtime/spawn.js";
|
|
6
|
+
export function stripAnsi(s) {
|
|
7
|
+
return s
|
|
8
|
+
.replace(/\x1b\[[^A-Za-z]*[A-Za-z]/g, "")
|
|
9
|
+
.replace(/\x1b\][^\x07]*\x07/g, "")
|
|
10
|
+
.replace(/\x1b\][^\x1b]*\x1b\\/g, "")
|
|
11
|
+
.replace(/\x1b\([A-B0-2]/g, "")
|
|
12
|
+
.replace(/\x1b[=>]|\x1b7|\x1b8|\x1bD|\x1bM/g, "");
|
|
13
|
+
}
|
|
14
|
+
function delay(ms) {
|
|
15
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
}
|
|
17
|
+
function regexTest(re, s) {
|
|
18
|
+
const fresh = new RegExp(re.source, re.flags.replace("g", ""));
|
|
19
|
+
return fresh.test(s);
|
|
20
|
+
}
|
|
21
|
+
let ptyMod = null;
|
|
22
|
+
function getPty() {
|
|
23
|
+
if (ptyMod)
|
|
24
|
+
return ptyMod;
|
|
25
|
+
try {
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
ptyMod = require("node-pty");
|
|
28
|
+
if (ptyMod?.spawn)
|
|
29
|
+
return ptyMod;
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
// Compiled binary (bun --compile) has no node_modules; try sidecar locations
|
|
33
|
+
const execDir = path.dirname(process.execPath);
|
|
34
|
+
const platformArch = `${process.platform}-${process.arch}`;
|
|
35
|
+
const candidates = [
|
|
36
|
+
path.join(execDir, "pty", "node-pty"),
|
|
37
|
+
path.join(execDir, "node-pty"),
|
|
38
|
+
path.join(execDir, "..", "share", "quotacap", "pty", "node-pty"),
|
|
39
|
+
path.join(os.homedir(), ".local", "share", "quotacap", "pty", "node-pty"),
|
|
40
|
+
path.join(os.homedir(), ".local", "share", "quotacap", "pty", platformArch, "node-pty"),
|
|
41
|
+
path.join(os.homedir(), ".quotacap", "pty", "node-pty"),
|
|
42
|
+
];
|
|
43
|
+
for (const c of candidates) {
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.existsSync(c))
|
|
46
|
+
continue;
|
|
47
|
+
const require2 = createRequire(import.meta.url);
|
|
48
|
+
const mod = require2(c);
|
|
49
|
+
if (mod?.spawn) {
|
|
50
|
+
ptyMod = mod;
|
|
51
|
+
return ptyMod;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
try {
|
|
56
|
+
const libIndex = path.join(c, "lib", "index.js");
|
|
57
|
+
if (!fs.existsSync(libIndex))
|
|
58
|
+
continue;
|
|
59
|
+
const require3 = createRequire(import.meta.url);
|
|
60
|
+
const mod = require3(libIndex);
|
|
61
|
+
if (mod?.spawn) {
|
|
62
|
+
ptyMod = mod;
|
|
63
|
+
return ptyMod;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
}
|
|
68
|
+
throw new Error("pty: node-pty not available — install with build tools (Xcode on macOS, build-essential + python3 on Linux) or use exec-based adapters only; for compiled binaries, ensure the pty sidecar is installed alongside the binary (see install.sh)");
|
|
69
|
+
}
|
|
70
|
+
function isBunRuntime() {
|
|
71
|
+
return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.spawn === "function";
|
|
72
|
+
}
|
|
73
|
+
async function runPtyBun(opts) {
|
|
74
|
+
const maxBytes = opts.maxBytes ?? 256 * 1024;
|
|
75
|
+
const cols = opts.cols ?? 140;
|
|
76
|
+
const rows = opts.rows ?? 50;
|
|
77
|
+
const readyTimeoutMs = opts.readyTimeoutMs ?? 6000;
|
|
78
|
+
if (opts.timeoutMs <= 0)
|
|
79
|
+
throw new Error("pty: timeoutMs must be > 0");
|
|
80
|
+
if (opts.signal?.aborted)
|
|
81
|
+
throw new Error("pty aborted");
|
|
82
|
+
let aborted = false;
|
|
83
|
+
const onAbort = () => {
|
|
84
|
+
aborted = true;
|
|
85
|
+
};
|
|
86
|
+
if (opts.signal)
|
|
87
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
const checkAborted = () => {
|
|
89
|
+
if (aborted)
|
|
90
|
+
throw new Error("pty aborted");
|
|
91
|
+
};
|
|
92
|
+
const teardownAbort = () => {
|
|
93
|
+
if (opts.signal)
|
|
94
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
95
|
+
};
|
|
96
|
+
const BunGlobal = globalThis.Bun;
|
|
97
|
+
let transcript = "";
|
|
98
|
+
let exited = false;
|
|
99
|
+
let exitCode;
|
|
100
|
+
const proc = BunGlobal.spawn([opts.file, ...(opts.args ?? [])], {
|
|
101
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
102
|
+
env: {
|
|
103
|
+
...process.env,
|
|
104
|
+
...(opts.env ?? {}),
|
|
105
|
+
TERM: "xterm-256color",
|
|
106
|
+
},
|
|
107
|
+
stdin: "pipe",
|
|
108
|
+
pty: { cols, rows },
|
|
109
|
+
});
|
|
110
|
+
// Capture stdout (pty merges stdout+stderr)
|
|
111
|
+
const decoder = new TextDecoder();
|
|
112
|
+
const reader = proc.stdout.getReader();
|
|
113
|
+
let reading = true;
|
|
114
|
+
const readLoop = (async () => {
|
|
115
|
+
try {
|
|
116
|
+
while (reading) {
|
|
117
|
+
const { value, done } = await reader.read();
|
|
118
|
+
if (done)
|
|
119
|
+
break;
|
|
120
|
+
if (value)
|
|
121
|
+
transcript += decoder.decode(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
})();
|
|
126
|
+
// Poll for exit
|
|
127
|
+
const exitPoll = (async () => {
|
|
128
|
+
try {
|
|
129
|
+
const code = await proc.exited;
|
|
130
|
+
exited = true;
|
|
131
|
+
exitCode = code;
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
})();
|
|
135
|
+
const checkCap = () => {
|
|
136
|
+
if (Buffer.byteLength(transcript, "utf8") > maxBytes) {
|
|
137
|
+
throw new Error(`pty transcript exceeds ${maxBytes} bytes`);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const checkAbort = (clean) => {
|
|
141
|
+
if (opts.abortOn && regexTest(opts.abortOn, clean)) {
|
|
142
|
+
throw new Error(`pty: untrusted workspace — trust prompt detected in ${opts.cwd ?? process.cwd()} — run \`${opts.file}\` there and select Trust this folder`);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const kill = async () => {
|
|
146
|
+
if (exited)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
proc.kill();
|
|
150
|
+
}
|
|
151
|
+
catch { }
|
|
152
|
+
for (let i = 0; i < 6; i++) {
|
|
153
|
+
await delay(100);
|
|
154
|
+
if (exited)
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
proc.kill(9);
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
for (let i = 0; i < 5; i++) {
|
|
162
|
+
await delay(100);
|
|
163
|
+
if (exited)
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const unregisterChild = registerTrackedChild(() => {
|
|
168
|
+
try {
|
|
169
|
+
proc.kill();
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
}, opts.label ?? opts.file);
|
|
173
|
+
try {
|
|
174
|
+
if (opts.readyRegex) {
|
|
175
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
176
|
+
let matched = false;
|
|
177
|
+
while (Date.now() < deadline) {
|
|
178
|
+
checkCap();
|
|
179
|
+
checkAborted();
|
|
180
|
+
if (exited)
|
|
181
|
+
throw new Error(`pty exited before ready (code ${exitCode})`);
|
|
182
|
+
const clean = stripAnsi(transcript);
|
|
183
|
+
checkAbort(clean);
|
|
184
|
+
if (regexTest(opts.readyRegex, clean)) {
|
|
185
|
+
matched = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
await delay(40);
|
|
189
|
+
}
|
|
190
|
+
if (!matched) {
|
|
191
|
+
await kill();
|
|
192
|
+
throw new Error(`pty ready timeout after ${readyTimeoutMs}ms`);
|
|
193
|
+
}
|
|
194
|
+
await delay(200);
|
|
195
|
+
checkCap();
|
|
196
|
+
checkAborted();
|
|
197
|
+
if (exited)
|
|
198
|
+
throw new Error(`pty exited before input (code ${exitCode})`);
|
|
199
|
+
const postReadyClean = stripAnsi(transcript);
|
|
200
|
+
checkAbort(postReadyClean);
|
|
201
|
+
}
|
|
202
|
+
else if (opts.settleDelayMs) {
|
|
203
|
+
await delay(opts.settleDelayMs);
|
|
204
|
+
checkCap();
|
|
205
|
+
checkAborted();
|
|
206
|
+
if (exited)
|
|
207
|
+
throw new Error(`pty exited during settle (code ${exitCode})`);
|
|
208
|
+
const c = stripAnsi(transcript);
|
|
209
|
+
checkAbort(c);
|
|
210
|
+
}
|
|
211
|
+
if (exited)
|
|
212
|
+
throw new Error(`pty exited before input (code ${exitCode})`);
|
|
213
|
+
try {
|
|
214
|
+
proc.stdin.write(opts.input);
|
|
215
|
+
if (typeof proc.stdin.flush === "function")
|
|
216
|
+
proc.stdin.flush();
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
throw new Error(`pty write failed: ${e.message}`);
|
|
220
|
+
}
|
|
221
|
+
if (opts.completionRegex) {
|
|
222
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
223
|
+
let done = false;
|
|
224
|
+
while (Date.now() < deadline) {
|
|
225
|
+
checkCap();
|
|
226
|
+
checkAborted();
|
|
227
|
+
const clean = stripAnsi(transcript);
|
|
228
|
+
checkAbort(clean);
|
|
229
|
+
if (regexTest(opts.completionRegex, clean)) {
|
|
230
|
+
done = true;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (exited) {
|
|
234
|
+
if (!regexTest(opts.completionRegex, clean)) {
|
|
235
|
+
throw new Error(`pty exited before completion (code ${exitCode})`);
|
|
236
|
+
}
|
|
237
|
+
done = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
await delay(40);
|
|
241
|
+
}
|
|
242
|
+
if (!done) {
|
|
243
|
+
await kill();
|
|
244
|
+
throw new Error(`pty completion timeout after ${opts.timeoutMs}ms`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
249
|
+
while (Date.now() < deadline) {
|
|
250
|
+
checkCap();
|
|
251
|
+
checkAborted();
|
|
252
|
+
const c = stripAnsi(transcript);
|
|
253
|
+
checkAbort(c);
|
|
254
|
+
if (exited)
|
|
255
|
+
break;
|
|
256
|
+
await delay(40);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
await kill();
|
|
260
|
+
reading = false;
|
|
261
|
+
try {
|
|
262
|
+
reader.cancel();
|
|
263
|
+
}
|
|
264
|
+
catch { }
|
|
265
|
+
await Promise.race([readLoop, delay(200)]);
|
|
266
|
+
checkCap();
|
|
267
|
+
checkAborted();
|
|
268
|
+
const finalClean = stripAnsi(transcript);
|
|
269
|
+
checkAbort(finalClean);
|
|
270
|
+
unregisterChild();
|
|
271
|
+
teardownAbort();
|
|
272
|
+
return transcript;
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
reading = false;
|
|
276
|
+
try {
|
|
277
|
+
reader.cancel();
|
|
278
|
+
}
|
|
279
|
+
catch { }
|
|
280
|
+
await kill();
|
|
281
|
+
unregisterChild();
|
|
282
|
+
teardownAbort();
|
|
283
|
+
throw e;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function runPtyNode(opts) {
|
|
287
|
+
const maxBytes = opts.maxBytes ?? 256 * 1024;
|
|
288
|
+
const cols = opts.cols ?? 140;
|
|
289
|
+
const rows = opts.rows ?? 50;
|
|
290
|
+
const readyTimeoutMs = opts.readyTimeoutMs ?? 6000;
|
|
291
|
+
if (opts.timeoutMs <= 0)
|
|
292
|
+
throw new Error("pty: timeoutMs must be > 0");
|
|
293
|
+
if (opts.signal?.aborted)
|
|
294
|
+
throw new Error("pty aborted");
|
|
295
|
+
let aborted = false;
|
|
296
|
+
const onAbort = () => {
|
|
297
|
+
aborted = true;
|
|
298
|
+
};
|
|
299
|
+
if (opts.signal)
|
|
300
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
301
|
+
const checkAborted = () => {
|
|
302
|
+
if (aborted)
|
|
303
|
+
throw new Error("pty aborted");
|
|
304
|
+
};
|
|
305
|
+
const teardownAbort = () => {
|
|
306
|
+
if (opts.signal)
|
|
307
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
308
|
+
};
|
|
309
|
+
let transcript = "";
|
|
310
|
+
let exited = false;
|
|
311
|
+
let exitCode;
|
|
312
|
+
const pty = getPty();
|
|
313
|
+
let ptyProcess;
|
|
314
|
+
try {
|
|
315
|
+
ptyProcess = pty.spawn(opts.file, opts.args ?? [], {
|
|
316
|
+
name: "xterm-color",
|
|
317
|
+
cols,
|
|
318
|
+
rows,
|
|
319
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
320
|
+
env: {
|
|
321
|
+
...process.env,
|
|
322
|
+
...(opts.env ?? {}),
|
|
323
|
+
TERM: "xterm-256color",
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
throw new Error(`pty spawn failed for ${opts.file}: ${e.message}`);
|
|
329
|
+
}
|
|
330
|
+
const unregisterChild = registerTrackedChild(() => {
|
|
331
|
+
try {
|
|
332
|
+
ptyProcess.kill("SIGTERM");
|
|
333
|
+
}
|
|
334
|
+
catch { }
|
|
335
|
+
}, opts.label ?? opts.file);
|
|
336
|
+
const disposables = [];
|
|
337
|
+
disposables.push(ptyProcess.onData((data) => {
|
|
338
|
+
transcript += data;
|
|
339
|
+
}));
|
|
340
|
+
disposables.push(ptyProcess.onExit(({ exitCode: c }) => {
|
|
341
|
+
exited = true;
|
|
342
|
+
exitCode = c;
|
|
343
|
+
}));
|
|
344
|
+
const cleanup = () => {
|
|
345
|
+
for (const d of disposables) {
|
|
346
|
+
try {
|
|
347
|
+
d.dispose();
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const kill = async () => {
|
|
353
|
+
if (exited)
|
|
354
|
+
return;
|
|
355
|
+
try {
|
|
356
|
+
ptyProcess.kill("SIGINT");
|
|
357
|
+
}
|
|
358
|
+
catch { }
|
|
359
|
+
for (let i = 0; i < 6; i++) {
|
|
360
|
+
await delay(100);
|
|
361
|
+
if (exited)
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
ptyProcess.kill("SIGKILL");
|
|
366
|
+
}
|
|
367
|
+
catch { }
|
|
368
|
+
for (let i = 0; i < 5; i++) {
|
|
369
|
+
await delay(100);
|
|
370
|
+
if (exited)
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
const checkCap = () => {
|
|
375
|
+
if (Buffer.byteLength(transcript, "utf8") > maxBytes) {
|
|
376
|
+
throw new Error(`pty transcript exceeds ${maxBytes} bytes`);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const checkAbort = (clean) => {
|
|
380
|
+
if (opts.abortOn && regexTest(opts.abortOn, clean)) {
|
|
381
|
+
throw new Error(`pty: untrusted workspace — trust prompt detected in ${opts.cwd ?? process.cwd()} — run \`${opts.file}\` there and select Trust this folder`);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
if (opts.readyRegex) {
|
|
386
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
387
|
+
let matched = false;
|
|
388
|
+
while (Date.now() < deadline) {
|
|
389
|
+
checkCap();
|
|
390
|
+
checkAborted();
|
|
391
|
+
if (exited)
|
|
392
|
+
throw new Error(`pty exited before ready (code ${exitCode})`);
|
|
393
|
+
const clean = stripAnsi(transcript);
|
|
394
|
+
checkAbort(clean);
|
|
395
|
+
if (regexTest(opts.readyRegex, clean)) {
|
|
396
|
+
matched = true;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
await delay(40);
|
|
400
|
+
}
|
|
401
|
+
if (!matched) {
|
|
402
|
+
await kill();
|
|
403
|
+
throw new Error(`pty ready timeout after ${readyTimeoutMs}ms`);
|
|
404
|
+
}
|
|
405
|
+
await delay(200);
|
|
406
|
+
checkCap();
|
|
407
|
+
checkAborted();
|
|
408
|
+
if (exited)
|
|
409
|
+
throw new Error(`pty exited before input (code ${exitCode})`);
|
|
410
|
+
const postReadyClean = stripAnsi(transcript);
|
|
411
|
+
checkAbort(postReadyClean);
|
|
412
|
+
}
|
|
413
|
+
else if (opts.settleDelayMs) {
|
|
414
|
+
await delay(opts.settleDelayMs);
|
|
415
|
+
checkCap();
|
|
416
|
+
checkAborted();
|
|
417
|
+
if (exited)
|
|
418
|
+
throw new Error(`pty exited during settle (code ${exitCode})`);
|
|
419
|
+
const c = stripAnsi(transcript);
|
|
420
|
+
checkAbort(c);
|
|
421
|
+
}
|
|
422
|
+
if (exited)
|
|
423
|
+
throw new Error(`pty exited before input (code ${exitCode})`);
|
|
424
|
+
ptyProcess.write(opts.input);
|
|
425
|
+
if (opts.completionRegex) {
|
|
426
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
427
|
+
let done = false;
|
|
428
|
+
while (Date.now() < deadline) {
|
|
429
|
+
checkCap();
|
|
430
|
+
checkAborted();
|
|
431
|
+
const clean = stripAnsi(transcript);
|
|
432
|
+
checkAbort(clean);
|
|
433
|
+
if (regexTest(opts.completionRegex, clean)) {
|
|
434
|
+
done = true;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
if (exited) {
|
|
438
|
+
if (!regexTest(opts.completionRegex, clean)) {
|
|
439
|
+
throw new Error(`pty exited before completion (code ${exitCode})`);
|
|
440
|
+
}
|
|
441
|
+
done = true;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
await delay(40);
|
|
445
|
+
}
|
|
446
|
+
if (!done) {
|
|
447
|
+
await kill();
|
|
448
|
+
throw new Error(`pty completion timeout after ${opts.timeoutMs}ms`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
453
|
+
while (Date.now() < deadline) {
|
|
454
|
+
checkCap();
|
|
455
|
+
checkAborted();
|
|
456
|
+
const c = stripAnsi(transcript);
|
|
457
|
+
checkAbort(c);
|
|
458
|
+
if (exited)
|
|
459
|
+
break;
|
|
460
|
+
await delay(40);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
await kill();
|
|
464
|
+
checkCap();
|
|
465
|
+
checkAborted();
|
|
466
|
+
const finalClean = stripAnsi(transcript);
|
|
467
|
+
checkAbort(finalClean);
|
|
468
|
+
unregisterChild();
|
|
469
|
+
teardownAbort();
|
|
470
|
+
return transcript;
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
await kill();
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
cleanup();
|
|
478
|
+
unregisterChild();
|
|
479
|
+
teardownAbort();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
export async function runPty(opts) {
|
|
483
|
+
if (isBunRuntime()) {
|
|
484
|
+
return runPtyBun(opts);
|
|
485
|
+
}
|
|
486
|
+
return runPtyNode(opts);
|
|
487
|
+
}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -5,13 +5,17 @@ export interface Quota {
|
|
|
5
5
|
sessionPct?: number;
|
|
6
6
|
resetsAt: string;
|
|
7
7
|
periodStart: string;
|
|
8
|
-
|
|
9
|
-
source: "cli" | "api" | "scrape" | "manual";
|
|
8
|
+
source: "cli" | "api" | "scrape" | "manual" | "tui";
|
|
10
9
|
fetchedAt: string;
|
|
10
|
+
creditsUsd?: number;
|
|
11
|
+
resetsAtEstimated?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedQuota extends Quota {
|
|
14
|
+
raw?: string;
|
|
11
15
|
}
|
|
12
16
|
export interface Adapter {
|
|
13
17
|
id: string;
|
|
14
18
|
requiresAuth: string;
|
|
15
|
-
poll(): Promise<
|
|
19
|
+
poll(): Promise<ParsedQuota | ParsedQuota[]>;
|
|
16
20
|
}
|
|
17
21
|
export declare const UNIMPLEMENTED: (id: string) => Error;
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import type { Quota } from "../adapters/types.js";
|
|
2
|
-
import type { Advisory } from "./types.js";
|
|
3
|
-
|
|
2
|
+
import type { Advisory, PaceSource, RecommendationBasis } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pace from a single reading.
|
|
5
|
+
*
|
|
6
|
+
* `usedPct` is a running total since the window opened, and `periodStart` is
|
|
7
|
+
* a required field every adapter sets, so a pace is always computable from
|
|
8
|
+
* one poll. Returns null only when `periodStart` is missing or unparseable.
|
|
9
|
+
*/
|
|
10
|
+
export declare function averagePace(q: Quota, now?: Date): number | null;
|
|
11
|
+
export declare function computeAdvisory(q: Quota, burnRate: number | null, now?: Date, paceSourceOrMeasured?: PaceSource | boolean): Advisory;
|
|
4
12
|
export declare function recommend(quotas: Quota[], _task: string, burnByProvider?: Map<string, number>, now?: Date): {
|
|
5
13
|
use: string;
|
|
6
14
|
reason: string;
|
|
7
15
|
wastePct: number;
|
|
8
16
|
idealRate: number;
|
|
17
|
+
recommendationBasis: RecommendationBasis;
|
|
18
|
+
alternatives: Quota[];
|
|
19
|
+
advisories: Advisory[];
|
|
20
|
+
} | {
|
|
21
|
+
use: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
wastePct: null;
|
|
24
|
+
idealRate: number;
|
|
25
|
+
recommendationBasis: RecommendationBasis;
|
|
9
26
|
alternatives: Quota[];
|
|
10
27
|
advisories: Advisory[];
|
|
11
28
|
};
|
package/dist/advisory/engine.js
CHANGED
|
@@ -1,8 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
const DAY_MS = 86400000;
|
|
2
|
+
/**
|
|
3
|
+
* Pace from a single reading.
|
|
4
|
+
*
|
|
5
|
+
* `usedPct` is a running total since the window opened, and `periodStart` is
|
|
6
|
+
* a required field every adapter sets, so a pace is always computable from
|
|
7
|
+
* one poll. Returns null only when `periodStart` is missing or unparseable.
|
|
8
|
+
*/
|
|
9
|
+
export function averagePace(q, now = new Date()) {
|
|
10
|
+
if (!q.periodStart)
|
|
11
|
+
return null;
|
|
12
|
+
const start = new Date(q.periodStart).getTime();
|
|
13
|
+
if (Number.isNaN(start))
|
|
14
|
+
return null;
|
|
15
|
+
const elapsedDays = (now.getTime() - start) / DAY_MS;
|
|
16
|
+
if (elapsedDays < 1 / 24)
|
|
17
|
+
return null; // under an hour in: not yet meaningful
|
|
18
|
+
const pace = q.usedPct / elapsedDays;
|
|
19
|
+
if (!Number.isFinite(pace))
|
|
20
|
+
return null;
|
|
21
|
+
return Math.max(0, pace);
|
|
22
|
+
}
|
|
23
|
+
export function computeAdvisory(q, burnRate, now = new Date(), paceSourceOrMeasured = "unknown") {
|
|
2
24
|
const resets = new Date(q.resetsAt);
|
|
3
|
-
const daysLeft = Math.max(0.1, (resets.getTime() - now.getTime()) /
|
|
25
|
+
const daysLeft = Math.max(0.1, (resets.getTime() - now.getTime()) / DAY_MS);
|
|
4
26
|
const remaining = 100 - q.usedPct;
|
|
5
27
|
const idealRate = remaining / daysLeft;
|
|
28
|
+
let paceSource;
|
|
29
|
+
let burnMeasured;
|
|
30
|
+
if (burnRate === null) {
|
|
31
|
+
paceSource = "unknown";
|
|
32
|
+
burnMeasured = false;
|
|
33
|
+
}
|
|
34
|
+
else if (typeof paceSourceOrMeasured === "string") {
|
|
35
|
+
paceSource = paceSourceOrMeasured;
|
|
36
|
+
burnMeasured = paceSource === "recent";
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
burnMeasured = paceSourceOrMeasured;
|
|
40
|
+
paceSource = burnMeasured ? "recent" : "window-average";
|
|
41
|
+
}
|
|
42
|
+
if (burnRate === null) {
|
|
43
|
+
return {
|
|
44
|
+
provider: q.provider,
|
|
45
|
+
daysLeft,
|
|
46
|
+
remaining,
|
|
47
|
+
idealRate,
|
|
48
|
+
burnRate: null,
|
|
49
|
+
burnMeasured: false,
|
|
50
|
+
paceSource: "unknown",
|
|
51
|
+
daysToExhaust: null,
|
|
52
|
+
status: "unknown",
|
|
53
|
+
wastePct: null,
|
|
54
|
+
urgency: "on track",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
6
57
|
const wastePct = Math.max(0, remaining - burnRate * daysLeft);
|
|
7
58
|
// burn > ideal is exactly "quota exhausts before reset": remaining/burn < daysLeft
|
|
8
59
|
const daysToExhaust = burnRate > 0 ? remaining / burnRate : Infinity;
|
|
@@ -16,14 +67,83 @@ export function computeAdvisory(q, burnRate, now = new Date(), burnMeasured = fa
|
|
|
16
67
|
urgency = "slow down";
|
|
17
68
|
else if (wastePct > 10)
|
|
18
69
|
urgency = "save";
|
|
19
|
-
return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, burnMeasured, daysToExhaust, status, wastePct, urgency };
|
|
70
|
+
return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, burnMeasured, paceSource, daysToExhaust, status, wastePct, urgency };
|
|
20
71
|
}
|
|
21
72
|
export function recommend(quotas, _task, burnByProvider = new Map(), now = new Date()) {
|
|
22
|
-
if (!quotas.length)
|
|
23
|
-
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
73
|
+
if (!quotas.length) {
|
|
74
|
+
return {
|
|
75
|
+
use: "none",
|
|
76
|
+
reason: "no quotas yet",
|
|
77
|
+
wastePct: 0,
|
|
78
|
+
idealRate: 0,
|
|
79
|
+
recommendationBasis: "none",
|
|
80
|
+
alternatives: [],
|
|
81
|
+
advisories: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Build advisories for every quota with honest pace sources.
|
|
85
|
+
const advisories = quotas.map(q => {
|
|
86
|
+
const recent = burnByProvider.get(q.provider);
|
|
87
|
+
if (recent !== undefined) {
|
|
88
|
+
return computeAdvisory(q, recent, now, "recent");
|
|
89
|
+
}
|
|
90
|
+
const avg = averagePace(q, now);
|
|
91
|
+
if (avg !== null) {
|
|
92
|
+
return computeAdvisory(q, avg, now, "window-average");
|
|
93
|
+
}
|
|
94
|
+
return computeAdvisory(q, null, now, "unknown");
|
|
95
|
+
});
|
|
96
|
+
// 1. Prefer measured providers with positive avoidable waste (actionable "Use more" candidates).
|
|
97
|
+
const positiveWaste = advisories.filter((a) => a.paceSource !== "unknown" && a.wastePct !== null && a.wastePct > 0);
|
|
98
|
+
if (positiveWaste.length > 0) {
|
|
99
|
+
const burnNow = positiveWaste.filter(a => a.urgency === "burn now");
|
|
100
|
+
const pool = burnNow.length ? burnNow : positiveWaste;
|
|
101
|
+
const use = pool.sort((a, b) => b.wastePct - a.wastePct)[0];
|
|
102
|
+
return {
|
|
103
|
+
use: use.provider,
|
|
104
|
+
reason: `${Math.round(use.wastePct)}% waste in ${use.daysLeft.toFixed(1)}d`,
|
|
105
|
+
wastePct: use.wastePct,
|
|
106
|
+
idealRate: use.idealRate,
|
|
107
|
+
recommendationBasis: "known-waste",
|
|
108
|
+
alternatives: quotas,
|
|
109
|
+
advisories,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// 2. Low-confidence fallback: rank unknown providers by remaining headroom (remaining / daysLeft).
|
|
113
|
+
const unknownProviders = advisories.filter(a => a.paceSource === "unknown" && a.remaining > 0);
|
|
114
|
+
if (unknownProviders.length > 0) {
|
|
115
|
+
const use = unknownProviders.sort((a, b) => (b.remaining / b.daysLeft) - (a.remaining / a.daysLeft))[0];
|
|
116
|
+
return {
|
|
117
|
+
use: use.provider,
|
|
118
|
+
reason: `Measuring pace; ${Math.round(use.remaining)}% remains with ${use.daysLeft.toFixed(1)}d until reset`,
|
|
119
|
+
wastePct: null,
|
|
120
|
+
idealRate: use.idealRate,
|
|
121
|
+
recommendationBasis: "unknown-headroom",
|
|
122
|
+
alternatives: quotas,
|
|
123
|
+
advisories,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// 3. Check if all measured providers are healthy and on pace (status "on track").
|
|
127
|
+
const onTrack = advisories.filter(a => a.paceSource !== "unknown" && a.status === "on track" && a.remaining > 0);
|
|
128
|
+
if (onTrack.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
use: "none",
|
|
131
|
+
reason: "No subscription needs priority",
|
|
132
|
+
wastePct: 0,
|
|
133
|
+
idealRate: 0,
|
|
134
|
+
recommendationBasis: "none",
|
|
135
|
+
alternatives: quotas,
|
|
136
|
+
advisories,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// 4. No meaningful candidate (e.g. all quotas at risk or exhausted).
|
|
140
|
+
return {
|
|
141
|
+
use: "none",
|
|
142
|
+
reason: "all quotas at risk or exhausted",
|
|
143
|
+
wastePct: 0,
|
|
144
|
+
idealRate: 0,
|
|
145
|
+
recommendationBasis: "none",
|
|
146
|
+
alternatives: quotas,
|
|
147
|
+
advisories,
|
|
148
|
+
};
|
|
29
149
|
}
|