quotacap 0.0.41 → 0.0.43
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/dist/adapters/claude.js +16 -5
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +19 -8
- package/dist/adapters/goldens.d.ts +4 -0
- package/dist/adapters/goldens.js +20 -0
- package/dist/adapters/grok.d.ts +2 -1
- package/dist/adapters/grok.js +26 -12
- package/dist/adapters/index.d.ts +20 -6
- package/dist/adapters/index.js +96 -37
- package/dist/adapters/kimi.d.ts +2 -1
- package/dist/adapters/kimi.js +24 -13
- package/dist/adapters/muse.d.ts +2 -1
- package/dist/adapters/muse.js +30 -15
- package/dist/adapters/opencode-go.d.ts +1 -1
- package/dist/adapters/opencode-go.js +8 -1
- package/dist/adapters/pty.d.ts +7 -0
- package/dist/adapters/pty.js +32 -2
- package/dist/adapters/vt.d.ts +19 -0
- package/dist/adapters/vt.js +506 -0
- package/dist/cli/providers.js +2 -1
- package/dist/config.d.ts +12 -23
- package/dist/config.js +24 -0
- package/dist/diagnostics/bundle.d.ts +22 -0
- package/dist/diagnostics/bundle.js +125 -0
- package/dist/diagnostics/failure.d.ts +8 -1
- package/dist/diagnostics/failure.js +43 -2
- package/dist/format/rows.d.ts +1 -1
- package/dist/format/rows.js +9 -6
- package/dist/mcp/server.d.ts +2 -2
- package/dist/runtime/poll.d.ts +4 -0
- package/dist/runtime/poll.js +30 -1
- package/dist/runtime/service.js +3 -1
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +1 -1
- package/dist/web/src/components/AdviceDrawer.js +1 -1
- package/dist/web/src/components/OpencodeGoBanner.d.ts +1 -1
- package/dist/web/src/components/OpencodeGoBanner.js +9 -2
- package/dist/web/src/components/PaceBar.d.ts +9 -4
- package/dist/web/src/components/PaceBar.js +26 -7
- package/dist/web/src/components/ProviderCard.js +2 -3
- package/dist/web/src/components/ProviderDrawer.js +3 -3
- package/dist/web/src/components/ProviderIcon.js +1 -1
- package/dist/web/src/components/ProviderRow.d.ts +1 -1
- package/dist/web/src/components/ProviderRow.js +2 -4
- package/dist/web/src/components/Recommendation.js +1 -1
- package/dist/web/src/components/ResetRail.js +1 -1
- package/dist/web/src/components/WindowsRow.d.ts +1 -1
- package/dist/web/src/components/WindowsRow.js +4 -4
- package/dist/web/src/components/settings/SettingsDrawer.js +1 -1
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +5 -5
- package/web/dist/assets/{index-Dk6l5h27.css → index-C6ltEeGL.css} +1 -1
- package/web/dist/assets/index-DRDDTtxF.js +13 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BUK8xUHm.js +0 -12
package/dist/config.d.ts
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare function getConfigPath(p?: string): string;
|
|
3
3
|
export declare function getDbPath(p?: string): string;
|
|
4
|
-
export declare const ProviderDisplayNameSchema: z.
|
|
4
|
+
export declare const ProviderDisplayNameSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
5
5
|
declare const ConfigSchema: z.ZodObject<{
|
|
6
6
|
port: z.ZodDefault<z.ZodNumber>;
|
|
7
7
|
pollMinutes: z.ZodDefault<z.ZodNumber>;
|
|
8
8
|
catalogTtlHours: z.ZodDefault<z.ZodNumber>;
|
|
9
|
-
enabledProviders: z.ZodDefault<z.ZodArray<z.ZodString
|
|
10
|
-
knownProviders: z.ZodDefault<z.ZodArray<z.ZodString
|
|
11
|
-
providerNames: z.ZodDefault<z.ZodRecord<z.ZodString, z.
|
|
9
|
+
enabledProviders: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
10
|
+
knownProviders: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
11
|
+
providerNames: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
|
|
12
12
|
opencodeGoConsentAt: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
13
13
|
experimentalIngest: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
pollMinutes: number;
|
|
17
|
-
catalogTtlHours: number;
|
|
18
|
-
enabledProviders: string[];
|
|
19
|
-
knownProviders: string[];
|
|
20
|
-
providerNames: Record<string, string>;
|
|
21
|
-
opencodeGoConsentAt: string | null;
|
|
22
|
-
experimentalIngest?: boolean | undefined;
|
|
23
|
-
}, {
|
|
24
|
-
port?: number | undefined;
|
|
25
|
-
pollMinutes?: number | undefined;
|
|
26
|
-
catalogTtlHours?: number | undefined;
|
|
27
|
-
enabledProviders?: string[] | undefined;
|
|
28
|
-
knownProviders?: string[] | undefined;
|
|
29
|
-
providerNames?: Record<string, string> | undefined;
|
|
30
|
-
opencodeGoConsentAt?: string | null | undefined;
|
|
31
|
-
experimentalIngest?: boolean | undefined;
|
|
32
|
-
}>;
|
|
14
|
+
debugFailureBundles: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
}, z.core.$strip>;
|
|
33
16
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
34
17
|
/**
|
|
35
18
|
* Manual ingest (CLI `ingest`, POST /api/ingest) is off unless this returns
|
|
@@ -37,6 +20,12 @@ export type Config = z.infer<typeof ConfigSchema>;
|
|
|
37
20
|
* config key. Do not document this as a supported product flag.
|
|
38
21
|
*/
|
|
39
22
|
export declare function isExperimentalIngestEnabled(config?: Pick<Config, "experimentalIngest">, env?: NodeJS.Dict<string>): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Failure evidence bundles (per-provider redacted transcripts under
|
|
25
|
+
* <dataDir>/failures/) are off unless this returns true. Env
|
|
26
|
+
* `QUOTACAP_DEBUG_FAILURES` wins; otherwise the optional config key.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isFailureBundlesEnabled(config?: Pick<Config, "debugFailureBundles">, env?: NodeJS.Dict<string>): boolean;
|
|
40
29
|
export declare function readConfig(p?: string): Promise<Config>;
|
|
41
30
|
export declare function defaultConfig(): Config;
|
|
42
31
|
export interface EnsureConfigResult {
|
package/dist/config.js
CHANGED
|
@@ -41,6 +41,10 @@ const ConfigSchema = z.object({
|
|
|
41
41
|
// Optional, omitted from defaults and `init` output. Manual ingest stays
|
|
42
42
|
// in-tree but is not a public surface until the product design lands.
|
|
43
43
|
experimentalIngest: z.boolean().optional(),
|
|
44
|
+
// Opt-in failure forensics: on poll failure, write a redacted evidence
|
|
45
|
+
// bundle (transcript, error, versions, timings) under <dataDir>/failures/.
|
|
46
|
+
// Local-only, never uploaded. Env QUOTACAP_DEBUG_FAILURES wins (see below).
|
|
47
|
+
debugFailureBundles: z.boolean().optional(),
|
|
44
48
|
});
|
|
45
49
|
function envFlag(raw) {
|
|
46
50
|
if (raw === undefined)
|
|
@@ -71,6 +75,25 @@ export function isExperimentalIngestEnabled(config, env = process.env) {
|
|
|
71
75
|
return false;
|
|
72
76
|
}
|
|
73
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Failure evidence bundles (per-provider redacted transcripts under
|
|
80
|
+
* <dataDir>/failures/) are off unless this returns true. Env
|
|
81
|
+
* `QUOTACAP_DEBUG_FAILURES` wins; otherwise the optional config key.
|
|
82
|
+
*/
|
|
83
|
+
export function isFailureBundlesEnabled(config, env = process.env) {
|
|
84
|
+
const fromEnv = envFlag(env.QUOTACAP_DEBUG_FAILURES);
|
|
85
|
+
if (fromEnv !== undefined)
|
|
86
|
+
return fromEnv;
|
|
87
|
+
if (config)
|
|
88
|
+
return config.debugFailureBundles === true;
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(fsSync.readFileSync(getConfigPath(), "utf8"));
|
|
91
|
+
return parsed?.debugFailureBundles === true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
74
97
|
export async function readConfig(p) {
|
|
75
98
|
try {
|
|
76
99
|
const raw = await fs.readFile(getConfigPath(p), "utf8");
|
|
@@ -106,6 +129,7 @@ const ServiceConfigSchema = z.object({
|
|
|
106
129
|
providerNames: z.record(z.string(), ProviderDisplayNameSchema).default({}),
|
|
107
130
|
opencodeGoConsentAt: z.string().nullable().default(null),
|
|
108
131
|
experimentalIngest: z.boolean().optional(),
|
|
132
|
+
debugFailureBundles: z.boolean().optional(),
|
|
109
133
|
});
|
|
110
134
|
export function defaultConfig() {
|
|
111
135
|
return ConfigSchema.parse({});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DiagnosticCode } from "./failure.js";
|
|
2
|
+
export declare const BUNDLE_KEEP_PER_PROVIDER = 5;
|
|
3
|
+
export declare function redactText(s: string): string;
|
|
4
|
+
export declare function providerBin(provider: string, providerPaths?: Record<string, string | null>): string | null;
|
|
5
|
+
/** Best-effort `<bin> --version` first line, or null. Never throws. */
|
|
6
|
+
export declare function getCliVersion(bin: string, timeoutMs?: number): Promise<string | null>;
|
|
7
|
+
export interface FailureBundleInput {
|
|
8
|
+
dataDir: string;
|
|
9
|
+
provider: string;
|
|
10
|
+
attemptedAt: string;
|
|
11
|
+
reason: unknown;
|
|
12
|
+
diagnosticCode: DiagnosticCode;
|
|
13
|
+
summary: string;
|
|
14
|
+
errorDetail: string;
|
|
15
|
+
attemptDurationMs?: number;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
quotacapVersion?: string;
|
|
18
|
+
cliBin?: string | null;
|
|
19
|
+
cliVersion?: string | null;
|
|
20
|
+
}
|
|
21
|
+
/** Write one bundle. Never throws: returns the dir path, or null. */
|
|
22
|
+
export declare function writeFailureBundle(input: FailureBundleInput): Promise<string | null>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Opt-in failure forensics: on a poll failure, persist a redacted evidence
|
|
2
|
+
// bundle under <dataDir>/failures/<provider>-<timestamp>-<rand>/. Bundles
|
|
3
|
+
// are local-only (never uploaded) and exist so the next "bad resets
|
|
4
|
+
// timestamp" arrives with the transcript that explains it. Off by default;
|
|
5
|
+
// enabled via config `debugFailureBundles` or env QUOTACAP_DEBUG_FAILURES.
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { getErrorEvidence } from "./failure.js";
|
|
10
|
+
import { renderScreen } from "../adapters/vt.js";
|
|
11
|
+
import { isGoldenCovered } from "../adapters/goldens.js";
|
|
12
|
+
export const BUNDLE_KEEP_PER_PROVIDER = 5;
|
|
13
|
+
const RAW_CAP = 64 * 1024;
|
|
14
|
+
const RENDERED_CAP = 32 * 1024;
|
|
15
|
+
const STDERR_CAP = 8 * 1024;
|
|
16
|
+
const MESSAGE_CAP = 2048;
|
|
17
|
+
// Emails, URLs, bearer tokens, and long token-like runs. Percents, dates,
|
|
18
|
+
// and short identifiers pass through: parse debugging needs them.
|
|
19
|
+
export function redactText(s) {
|
|
20
|
+
return s
|
|
21
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "[email]")
|
|
22
|
+
.replace(/https?:\/\/\S+/g, "[url]")
|
|
23
|
+
.replace(/(bearer\s+)[A-Za-z0-9._~+/-]+/gi, "$1[token]")
|
|
24
|
+
.replace(/\b[A-Za-z0-9_~+\/=.-]{32,}\b/g, "[token]");
|
|
25
|
+
}
|
|
26
|
+
function cap(s, n) {
|
|
27
|
+
return s.length > n ? s.slice(0, n) + `\n…[truncated, ${s.length} chars total]` : s;
|
|
28
|
+
}
|
|
29
|
+
// Providers without a version-reporting CLI (credential stores, virtual).
|
|
30
|
+
const NO_CLI_PROVIDERS = new Set(["opencode-go", "manual", "all"]);
|
|
31
|
+
export function providerBin(provider, providerPaths) {
|
|
32
|
+
if (NO_CLI_PROVIDERS.has(provider))
|
|
33
|
+
return null;
|
|
34
|
+
const recorded = providerPaths?.[provider];
|
|
35
|
+
if (typeof recorded === "string" && recorded.length > 0)
|
|
36
|
+
return recorded;
|
|
37
|
+
return provider;
|
|
38
|
+
}
|
|
39
|
+
/** Best-effort `<bin> --version` first line, or null. Never throws. */
|
|
40
|
+
export function getCliVersion(bin, timeoutMs = 3000) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
execFile(bin, ["--version"], { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
43
|
+
if (err) {
|
|
44
|
+
resolve(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const first = `${stdout ?? ""}\n${stderr ?? ""}`
|
|
48
|
+
.split("\n")
|
|
49
|
+
.map((l) => l.trim())
|
|
50
|
+
.find((l) => l.length > 0);
|
|
51
|
+
resolve(first ? first.slice(0, 120) : null);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function pruneOldBundles(failuresDir, provider) {
|
|
56
|
+
try {
|
|
57
|
+
const entries = await fs.readdir(failuresDir);
|
|
58
|
+
const ours = entries.filter((e) => e === provider || e.startsWith(`${provider}-`)).sort();
|
|
59
|
+
const stale = ours.slice(0, Math.max(0, ours.length - BUNDLE_KEEP_PER_PROVIDER));
|
|
60
|
+
await Promise.all(stale.map((e) => fs.rm(path.join(failuresDir, e), { recursive: true, force: true }).catch(() => { })));
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
}
|
|
64
|
+
/** Write one bundle. Never throws: returns the dir path, or null. */
|
|
65
|
+
export async function writeFailureBundle(input) {
|
|
66
|
+
try {
|
|
67
|
+
const stamp = input.attemptedAt.replace(/[:.]/g, "-");
|
|
68
|
+
const rand = Math.floor(Math.random() * 0x10000).toString(16).padStart(4, "0");
|
|
69
|
+
const dir = path.join(input.dataDir, "failures", `${input.provider}-${stamp}-${rand}`);
|
|
70
|
+
await fs.mkdir(dir, { recursive: true });
|
|
71
|
+
const evidence = getErrorEvidence(input.reason);
|
|
72
|
+
const raw = typeof evidence?.stdout === "string" ? evidence.stdout : "";
|
|
73
|
+
const message = input.reason instanceof Error ? input.reason.message : String(input.reason?.message ?? input.reason);
|
|
74
|
+
const files = [];
|
|
75
|
+
if (raw) {
|
|
76
|
+
files.push(["transcript-raw.txt", redactText(cap(raw, RAW_CAP))]);
|
|
77
|
+
if (evidence?.source === "pty") {
|
|
78
|
+
files.push(["transcript.txt", redactText(cap(renderScreen(raw), RENDERED_CAP))]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const errLines = [
|
|
82
|
+
`summary: ${input.summary}`,
|
|
83
|
+
`code: ${input.diagnosticCode}`,
|
|
84
|
+
`detail: ${input.errorDetail}`,
|
|
85
|
+
`message: ${redactText(cap(message, MESSAGE_CAP))}`,
|
|
86
|
+
];
|
|
87
|
+
if (evidence?.checkpoint)
|
|
88
|
+
errLines.push(`checkpoint: ${evidence.checkpoint}`);
|
|
89
|
+
if (evidence?.exitCode !== undefined)
|
|
90
|
+
errLines.push(`exitCode: ${evidence.exitCode}`);
|
|
91
|
+
if (evidence?.durationMs !== undefined)
|
|
92
|
+
errLines.push(`durationMs: ${evidence.durationMs}`);
|
|
93
|
+
if (evidence?.timings)
|
|
94
|
+
errLines.push(`timings: ${JSON.stringify(evidence.timings)}`);
|
|
95
|
+
const stderr = typeof evidence?.stderr === "string" ? evidence.stderr.trim() : "";
|
|
96
|
+
if (stderr)
|
|
97
|
+
errLines.push(`stderr:\n${redactText(cap(stderr, STDERR_CAP))}`);
|
|
98
|
+
files.push(["error.txt", errLines.join("\n") + "\n"]);
|
|
99
|
+
const meta = {
|
|
100
|
+
provider: input.provider,
|
|
101
|
+
attemptedAt: input.attemptedAt,
|
|
102
|
+
quotacapVersion: input.quotacapVersion ?? null,
|
|
103
|
+
cliBin: input.cliBin ?? null,
|
|
104
|
+
cliVersion: input.cliVersion ?? null,
|
|
105
|
+
goldenCovered: input.cliVersion ? isGoldenCovered(input.provider, input.cliVersion) : null,
|
|
106
|
+
diagnosticCode: input.diagnosticCode,
|
|
107
|
+
summary: input.summary,
|
|
108
|
+
errorDetail: input.errorDetail,
|
|
109
|
+
attemptDurationMs: input.attemptDurationMs ?? null,
|
|
110
|
+
timeoutMs: input.timeoutMs ?? null,
|
|
111
|
+
checkpoint: evidence?.checkpoint ?? null,
|
|
112
|
+
exitCode: evidence?.exitCode ?? null,
|
|
113
|
+
durationMs: evidence?.durationMs ?? null,
|
|
114
|
+
timings: evidence?.timings ?? null,
|
|
115
|
+
hasTranscript: raw.length > 0,
|
|
116
|
+
};
|
|
117
|
+
files.push(["meta.json", JSON.stringify(meta, null, 2) + "\n"]);
|
|
118
|
+
await Promise.all(files.map(([name, content]) => fs.writeFile(path.join(dir, name), content, "utf8")));
|
|
119
|
+
await pruneOldBundles(path.join(input.dataDir, "failures"), input.provider);
|
|
120
|
+
return dir;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -7,6 +7,8 @@ export interface FailureEvidence {
|
|
|
7
7
|
durationMs?: number;
|
|
8
8
|
stdout?: string;
|
|
9
9
|
stderr?: string;
|
|
10
|
+
/** Phase marks (ms since attempt start): spawn, input, completion, end. */
|
|
11
|
+
timings?: Record<string, number>;
|
|
10
12
|
}
|
|
11
13
|
export interface SafeDiagnostic {
|
|
12
14
|
diagnosticCode: DiagnosticCode;
|
|
@@ -19,8 +21,13 @@ export interface ClassifiedFailure extends SafeDiagnostic {
|
|
|
19
21
|
}
|
|
20
22
|
export declare class DiagnosticError extends Error {
|
|
21
23
|
readonly diagnostic: SafeDiagnostic;
|
|
22
|
-
|
|
24
|
+
/** Raw failure evidence (transcript, timings). Memory-only: classification
|
|
25
|
+
* and logs use `diagnostic`; bundles read this. */
|
|
26
|
+
readonly evidence?: FailureEvidence;
|
|
27
|
+
constructor(diagnostic: SafeDiagnostic, evidence?: FailureEvidence);
|
|
23
28
|
}
|
|
29
|
+
export declare function attachEvidence(error: unknown, evidence: FailureEvidence): void;
|
|
30
|
+
export declare function getErrorEvidence(reason: unknown): FailureEvidence | undefined;
|
|
24
31
|
export declare function formatProviderName(provider: string): string;
|
|
25
32
|
export declare function diagnosticError(reason: unknown, evidence?: FailureEvidence): DiagnosticError;
|
|
26
33
|
export declare function classifyFailure(provider: string, reason: unknown): ClassifiedFailure;
|
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
export class DiagnosticError extends Error {
|
|
2
2
|
diagnostic;
|
|
3
|
-
|
|
3
|
+
/** Raw failure evidence (transcript, timings). Memory-only: classification
|
|
4
|
+
* and logs use `diagnostic`; bundles read this. */
|
|
5
|
+
evidence;
|
|
6
|
+
constructor(diagnostic, evidence) {
|
|
4
7
|
super(diagnostic.errorDetail);
|
|
5
8
|
this.name = "DiagnosticError";
|
|
6
9
|
this.diagnostic = Object.freeze({ ...diagnostic });
|
|
10
|
+
// Non-enumerable: evidence must never leak through JSON.stringify or
|
|
11
|
+
// spreads — serialized errors carry the diagnosis only. Readable via
|
|
12
|
+
// getErrorEvidence.
|
|
13
|
+
if (evidence) {
|
|
14
|
+
Object.defineProperty(this, "evidence", {
|
|
15
|
+
value: Object.freeze({ ...evidence }),
|
|
16
|
+
enumerable: false,
|
|
17
|
+
writable: false,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
7
20
|
}
|
|
8
21
|
}
|
|
22
|
+
// Side-channel evidence for plain Errors: adapter polls attach the transcript
|
|
23
|
+
// a parse throw would otherwise discard, without touching the message that
|
|
24
|
+
// classification and existing logs depend on.
|
|
25
|
+
const EVIDENCE_KEY = "__quotacapEvidence";
|
|
26
|
+
export function attachEvidence(error, evidence) {
|
|
27
|
+
if (typeof error === "object" && error !== null) {
|
|
28
|
+
try {
|
|
29
|
+
// Non-enumerable, like DiagnosticError.evidence: serialization-safe.
|
|
30
|
+
Object.defineProperty(error, EVIDENCE_KEY, {
|
|
31
|
+
value: evidence,
|
|
32
|
+
enumerable: false,
|
|
33
|
+
writable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function getErrorEvidence(reason) {
|
|
41
|
+
if (reason instanceof DiagnosticError && reason.evidence)
|
|
42
|
+
return reason.evidence;
|
|
43
|
+
if (typeof reason === "object" && reason !== null) {
|
|
44
|
+
const ev = reason[EVIDENCE_KEY];
|
|
45
|
+
if (ev && typeof ev === "object" && (ev.source === "pty" || ev.source === "exec"))
|
|
46
|
+
return ev;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
9
50
|
export function formatProviderName(provider) {
|
|
10
51
|
switch (provider) {
|
|
11
52
|
case "claude":
|
|
@@ -426,7 +467,7 @@ export function diagnosticError(reason, evidence) {
|
|
|
426
467
|
return new DiagnosticError({
|
|
427
468
|
diagnosticCode: match.code,
|
|
428
469
|
errorDetail: detail,
|
|
429
|
-
});
|
|
470
|
+
}, evidence);
|
|
430
471
|
}
|
|
431
472
|
function mapCategory(code) {
|
|
432
473
|
switch (code) {
|
package/dist/format/rows.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare function weeklyPctOf(q: {
|
|
|
21
21
|
usedPct?: number;
|
|
22
22
|
} | null | undefined): number | undefined;
|
|
23
23
|
export declare function fmtDuration(ms: number): string;
|
|
24
|
-
export declare function
|
|
24
|
+
export declare function dayOf(iso: string | undefined): string;
|
|
25
25
|
export declare function countdownText(ps: ProviderSnapshot, now: Date): string;
|
|
26
26
|
export declare function elapsedPct(ps: ProviderSnapshot, now: Date): number | null;
|
|
27
27
|
export declare function usedPctOf(ps: ProviderSnapshot): number | null;
|
package/dist/format/rows.js
CHANGED
|
@@ -23,11 +23,14 @@ export function fmtDuration(ms) {
|
|
|
23
23
|
return `${Math.floor(ms / HOUR_MS)}h`;
|
|
24
24
|
return `${Math.max(1, Math.floor(ms / MIN_MS))}m`;
|
|
25
25
|
}
|
|
26
|
-
// Short weekday of an ISO instant in the locked English
|
|
27
|
-
// Pinned to en-US: the sentence it feeds
|
|
28
|
-
// a non-English machine must not
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// Short weekday plus calendar date of an ISO instant in the locked English
|
|
27
|
+
// copy, e.g. "Tue Sep 15". Pinned to en-US: the sentence it feeds
|
|
28
|
+
// ("unused until Tue Sep 15") is English, so a non-English machine must not
|
|
29
|
+
// print localized parts inside it. A bare weekday scans as this coming week,
|
|
30
|
+
// which misreads a monthly reset sitting weeks out.
|
|
31
|
+
export function dayOf(iso) {
|
|
32
|
+
const when = new Date(iso ?? "");
|
|
33
|
+
return `${when.toLocaleDateString("en-US", { weekday: "short" })} ${when.toLocaleDateString("en-US", { day: "numeric", month: "short" })}`;
|
|
31
34
|
}
|
|
32
35
|
export function countdownText(ps, now) {
|
|
33
36
|
const resetsMs = msOf(ps.quota?.resetsAt);
|
|
@@ -140,7 +143,7 @@ export function forecastText(ps, now) {
|
|
|
140
143
|
return "no readings yet";
|
|
141
144
|
let text;
|
|
142
145
|
if (adv.bindingWindow === "monthly" && adv.bindingRemaining <= 0) {
|
|
143
|
-
text = `unused until ${
|
|
146
|
+
text = `unused until ${dayOf(ps.quota?.monthlyResetsAt)}`;
|
|
144
147
|
}
|
|
145
148
|
else if (adv.bindingWindow === "monthly") {
|
|
146
149
|
text = `${Math.round(adv.bindingRemaining)}% of month left`;
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -29,10 +29,10 @@ export declare const tools: ({
|
|
|
29
29
|
inputSchema: {
|
|
30
30
|
type: string;
|
|
31
31
|
properties: {
|
|
32
|
+
task?: undefined;
|
|
32
33
|
provider: {
|
|
33
34
|
type: string;
|
|
34
35
|
};
|
|
35
|
-
task?: undefined;
|
|
36
36
|
};
|
|
37
37
|
required?: undefined;
|
|
38
38
|
};
|
|
@@ -42,10 +42,10 @@ export declare const tools: ({
|
|
|
42
42
|
inputSchema: {
|
|
43
43
|
type: string;
|
|
44
44
|
properties: {
|
|
45
|
+
task?: undefined;
|
|
45
46
|
provider: {
|
|
46
47
|
type: string;
|
|
47
48
|
};
|
|
48
|
-
task?: undefined;
|
|
49
49
|
};
|
|
50
50
|
required: string[];
|
|
51
51
|
};
|
package/dist/runtime/poll.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ export interface CoordinatorOptions {
|
|
|
44
44
|
onOwnershipLost?: () => void;
|
|
45
45
|
/** Fired after each scheduled tick settles (success or failure). The daemon refreshes its daily update cache here. */
|
|
46
46
|
onSettled?: () => void;
|
|
47
|
+
/** Data dir for failure bundles. Required with debugFailureBundles. */
|
|
48
|
+
dataDir?: string;
|
|
49
|
+
/** Opt-in forensics: write a redacted evidence bundle per poll failure. */
|
|
50
|
+
debugFailureBundles?: boolean;
|
|
47
51
|
}
|
|
48
52
|
export interface Coordinator {
|
|
49
53
|
refresh(): Promise<RefreshResult>;
|
package/dist/runtime/poll.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Poll coordinator: scheduled and manual refresh share one in-flight poll
|
|
2
2
|
// with a completion-measured cooldown (decisions D3/D4).
|
|
3
|
-
import { pollAll } from "../adapters/index.js";
|
|
3
|
+
import { pollAll, ADAPTER_TIMEOUTS } from "../adapters/index.js";
|
|
4
4
|
import { getAllLatest, upsertQuota } from "../store/quotas.js";
|
|
5
|
+
import { readServiceMetadata } from "../config.js";
|
|
6
|
+
import { writeFailureBundle, providerBin, getCliVersion } from "../diagnostics/bundle.js";
|
|
7
|
+
import { VERSION } from "../version.js";
|
|
5
8
|
import { recordAttempt, getAttempt, } from "../store/attempts.js";
|
|
6
9
|
import { classifyFailure, } from "../diagnostics/failure.js";
|
|
7
10
|
export class ServiceClosing extends Error {
|
|
@@ -51,6 +54,8 @@ export function createCoordinator(opts) {
|
|
|
51
54
|
const ownershipVerify = opts.ownershipVerify;
|
|
52
55
|
const onOwnershipLost = opts.onOwnershipLost ?? (() => process.exit(1));
|
|
53
56
|
const onSettled = opts.onSettled;
|
|
57
|
+
const dataDir = opts.dataDir;
|
|
58
|
+
const debugFailureBundles = opts.debugFailureBundles === true;
|
|
54
59
|
let inFlight = null;
|
|
55
60
|
let lastResult = null;
|
|
56
61
|
let lastCompletedPollAt = null;
|
|
@@ -171,6 +176,30 @@ export function createCoordinator(opts) {
|
|
|
171
176
|
errorDetail: diagnosis.errorDetail,
|
|
172
177
|
});
|
|
173
178
|
console.log(`[${completedAt}] [quotacap] [${row.provider}] poll failed (${diagnosis.diagnosticCode}): ${diagnosis.summary}: ${diagnosis.errorDetail}`);
|
|
179
|
+
if (debugFailureBundles && dataDir) {
|
|
180
|
+
try {
|
|
181
|
+
const bin = providerBin(row.provider, readServiceMetadata(dataDir)?.providerPaths);
|
|
182
|
+
const cliVersion = bin ? await getCliVersion(bin) : null;
|
|
183
|
+
const bundleDir = await writeFailureBundle({
|
|
184
|
+
dataDir,
|
|
185
|
+
provider: row.provider,
|
|
186
|
+
attemptedAt,
|
|
187
|
+
reason: row.reason,
|
|
188
|
+
diagnosticCode: diagnosis.diagnosticCode,
|
|
189
|
+
summary: diagnosis.summary,
|
|
190
|
+
errorDetail: diagnosis.errorDetail,
|
|
191
|
+
attemptDurationMs: new Date(completedAt).getTime() - new Date(attemptedAt).getTime(),
|
|
192
|
+
timeoutMs: ADAPTER_TIMEOUTS[row.provider],
|
|
193
|
+
quotacapVersion: VERSION,
|
|
194
|
+
cliBin: bin,
|
|
195
|
+
cliVersion,
|
|
196
|
+
});
|
|
197
|
+
if (bundleDir) {
|
|
198
|
+
console.log(`[${completedAt}] [quotacap] [${row.provider}] failure bundle: ${bundleDir}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
}
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
lastResult = result;
|
package/dist/runtime/service.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createCoordinator } from "./poll.js";
|
|
|
11
11
|
import { killAll } from "./spawn.js";
|
|
12
12
|
import { ensureToken } from "./token.js";
|
|
13
13
|
import { openDb, migrate } from "../store/db.js";
|
|
14
|
-
import { autoEnableNewProviders, ensureConfig, getConfigPath, getDbPath, isExperimentalIngestEnabled, readServiceConfig, readServiceMetadata } from "../config.js";
|
|
14
|
+
import { autoEnableNewProviders, ensureConfig, getConfigPath, getDbPath, isExperimentalIngestEnabled, isFailureBundlesEnabled, readServiceConfig, readServiceMetadata } from "../config.js";
|
|
15
15
|
import { buildApp } from "../http/server.js";
|
|
16
16
|
import { detectChannel, refreshUpdateCache } from "./updates.js";
|
|
17
17
|
import { VERSION } from "../version.js";
|
|
@@ -183,6 +183,8 @@ export async function startService(opts) {
|
|
|
183
183
|
coordinator = createCoordinator({
|
|
184
184
|
db,
|
|
185
185
|
enabledProviders: config.enabledProviders,
|
|
186
|
+
dataDir,
|
|
187
|
+
debugFailureBundles: isFailureBundlesEnabled(config),
|
|
186
188
|
ownershipVerify: () => claim.verify(),
|
|
187
189
|
// Passive daily update signal: refresh the shared cache on schedule.
|
|
188
190
|
// Background and best-effort — never blocks or fails a poll.
|
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.43";
|
package/dist/web/src/App.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import "./theme.css";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { paceBadge, timeLeft } from "./PaceBar.js";
|
|
4
4
|
import { ProviderIcon, providerTint } from "./ProviderIcon.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ProviderView, RuntimeView } from "../state.js";
|
|
3
|
-
export declare const OPENCODE_GO_CONSENT_COPY
|
|
3
|
+
export declare const OPENCODE_GO_CONSENT_COPY: string;
|
|
4
4
|
export declare function showOpenCodeGoBanner(runtime: Pick<RuntimeView, "detectedProviders"> | undefined, providers: Array<Pick<ProviderView, "id" | "enabled">>, suppressed: boolean): boolean;
|
|
5
5
|
export declare function OpencodeGoBanner({ onEnable }: {
|
|
6
6
|
onEnable: () => void;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
-
export const OPENCODE_GO_CONSENT_COPY =
|
|
3
|
+
export const OPENCODE_GO_CONSENT_COPY = [
|
|
4
|
+
"• What is read: API key OpenCode already stored when you ran `opencode auth login -p opencode-go` (`~/.local/share/opencode/auth.json`, `opencode-go` → `opencode` fallback).",
|
|
5
|
+
"• Read frequency: in-memory, read-only once per poll (every 15 minutes by default).",
|
|
6
|
+
"• Request destination: sent only as `Authorization: Bearer` to `https://opencode.ai/zen/go/v1/usage` to fetch `rolling/weekly/monthly` `percent` + `resetsAt`.",
|
|
7
|
+
"• Never-list: never used to make chat or model requests, never written or rotated, never stored in the `quotacap` DB, never returned by the API or MCP, and never logged (errors show `[redacted]`).",
|
|
8
|
+
"• Revocation instructions: revoke at `https://opencode.ai/auth` or `opencode providers logout opencode-go` and run `quotacap providers disable opencode-go` to stop polling.",
|
|
9
|
+
"• Optional environment override: if you prefer no file access, set `OPENCODE_API_KEY` instead.",
|
|
10
|
+
].join("\n");
|
|
4
11
|
export function showOpenCodeGoBanner(runtime, providers, suppressed) {
|
|
5
12
|
const detected = runtime?.detectedProviders?.includes("opencode-go") ?? false;
|
|
6
13
|
const row = providers.find((p) => p.id === "opencode-go");
|
|
@@ -32,5 +39,5 @@ export function ConsentModal({ onConfirm, onCancel, busy, }) {
|
|
|
32
39
|
padding: "var(--s4) var(--s5)",
|
|
33
40
|
maxWidth: 560,
|
|
34
41
|
margin: "var(--s4)",
|
|
35
|
-
}, children: [_jsx("h2", { id: "opencode-go-consent-title", style: { fontSize: "var(--t-4)", margin: "0 0 var(--s3)" }, children: "Enable OpenCode Go?" }), _jsx("p", { style: { fontSize: "var(--t-2)", color: "var(--ink-soft)", margin: "0 0 var(--s4)" }, children: OPENCODE_GO_CONSENT_COPY }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: "var(--s2)" }, children: [_jsx("button", { type: "button", className: "btn btn-quiet", onClick: onCancel, disabled: busy, children: "Cancel" }), _jsx("button", { type: "button", className: "btn btn-primary", onClick: onConfirm, disabled: busy, children: busy ? "Enabling…" : "Enable" })] })] }) }));
|
|
42
|
+
}, children: [_jsx("h2", { id: "opencode-go-consent-title", style: { fontSize: "var(--t-4)", margin: "0 0 var(--s3)" }, children: "Enable OpenCode Go?" }), _jsx("p", { style: { fontSize: "var(--t-2)", color: "var(--ink-soft)", margin: "0 0 var(--s4)", whiteSpace: "pre-line" }, children: OPENCODE_GO_CONSENT_COPY }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: "var(--s2)" }, children: [_jsx("button", { type: "button", className: "btn btn-quiet", onClick: onCancel, disabled: busy, children: "Cancel" }), _jsx("button", { type: "button", className: "btn btn-primary", onClick: onConfirm, disabled: busy, children: busy ? "Enabling…" : "Enable" })] })] }) }));
|
|
36
43
|
}
|
|
@@ -63,10 +63,15 @@ export declare function exclusionDetail(reason: ExclusionReason): string | null;
|
|
|
63
63
|
export declare function isEstimated(provider: ProviderView): boolean;
|
|
64
64
|
/** Weekday plus 24h clock, e.g. "Thu 21:00". Null when unparseable. */
|
|
65
65
|
export declare function resetClock(resetsAt: string, locale?: string): string | null;
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
export declare function
|
|
66
|
+
/** Weekday, calendar date and 24h clock, e.g. "Thu Oct 22 23:05". Monthly
|
|
67
|
+
* resets sit weeks out, where resetClock's weekday-only reading ("Thu 23:05")
|
|
68
|
+
* scans as this coming Thursday. Null when unparseable. */
|
|
69
|
+
export declare function resetDateClock(resetsAt: string, locale?: string): string | null;
|
|
70
|
+
/** Short weekday plus calendar date in the locked English copy, e.g.
|
|
71
|
+
* "Tue Sep 15". Mirrors dayOf in src/format/rows.ts, including its pinned
|
|
72
|
+
* en-US locale, so the CLI and the dashboard word the same sentence
|
|
73
|
+
* identically on any machine. */
|
|
74
|
+
export declare function resetDay(iso: string | undefined): string;
|
|
70
75
|
/** "6d 12h" style time left from server timestamps. Null when unparseable. */
|
|
71
76
|
export declare function timeLeft(resetsAt: string, asOfMs: number): string | null;
|
|
72
77
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ageDuration } from "../state.js";
|
|
3
3
|
/**
|
|
4
4
|
* Pace-state badge from the mapping table. Exclusion always wins; a blocked
|
|
@@ -112,11 +112,30 @@ export function resetClock(resetsAt, locale) {
|
|
|
112
112
|
});
|
|
113
113
|
return `${day} ${time}`;
|
|
114
114
|
}
|
|
115
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
export function
|
|
119
|
-
|
|
115
|
+
/** Weekday, calendar date and 24h clock, e.g. "Thu Oct 22 23:05". Monthly
|
|
116
|
+
* resets sit weeks out, where resetClock's weekday-only reading ("Thu 23:05")
|
|
117
|
+
* scans as this coming Thursday. Null when unparseable. */
|
|
118
|
+
export function resetDateClock(resetsAt, locale) {
|
|
119
|
+
const ms = Date.parse(resetsAt);
|
|
120
|
+
if (!Number.isFinite(ms))
|
|
121
|
+
return null;
|
|
122
|
+
const when = new Date(ms);
|
|
123
|
+
const day = when.toLocaleDateString(locale, { weekday: "short" });
|
|
124
|
+
const date = when.toLocaleDateString(locale, { day: "numeric", month: "short" });
|
|
125
|
+
const time = when.toLocaleTimeString(locale, {
|
|
126
|
+
hour: "2-digit",
|
|
127
|
+
minute: "2-digit",
|
|
128
|
+
hour12: false,
|
|
129
|
+
});
|
|
130
|
+
return `${day} ${date} ${time}`;
|
|
131
|
+
}
|
|
132
|
+
/** Short weekday plus calendar date in the locked English copy, e.g.
|
|
133
|
+
* "Tue Sep 15". Mirrors dayOf in src/format/rows.ts, including its pinned
|
|
134
|
+
* en-US locale, so the CLI and the dashboard word the same sentence
|
|
135
|
+
* identically on any machine. */
|
|
136
|
+
export function resetDay(iso) {
|
|
137
|
+
const when = new Date(iso ?? "");
|
|
138
|
+
return `${when.toLocaleDateString("en-US", { weekday: "short" })} ${when.toLocaleDateString("en-US", { day: "numeric", month: "short" })}`;
|
|
120
139
|
}
|
|
121
140
|
/** "6d 12h" style time left from server timestamps. Null when unparseable. */
|
|
122
141
|
export function timeLeft(resetsAt, asOfMs) {
|
|
@@ -360,7 +379,7 @@ export function forecastLine(provider) {
|
|
|
360
379
|
if (!advisory)
|
|
361
380
|
return null;
|
|
362
381
|
if (advisory.bindingWindow === "monthly" && advisory.bindingRemaining <= 0) {
|
|
363
|
-
return `unused until ${
|
|
382
|
+
return `unused until ${resetDay(provider.quota.monthlyResetsAt)}`;
|
|
364
383
|
}
|
|
365
384
|
if (advisory.bindingWindow === "monthly") {
|
|
366
385
|
return `${Math.round(advisory.bindingRemaining)}% of month left`;
|