Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Setup details live in the [Provider setup guide](docs/readme/providers.md#custom

If quota or token data looks wrong:

1. Run `/quota_status`, or start with `opencode-quota show` for a terminal quota summary.
1. Run `/quota_status`, or run `opencode-quota status` from your terminal for the same diagnostics without launching OpenCode.
2. Confirm the expected provider appears in the detected provider list.
3. Confirm companion auth plugins are before `@slkiser/opencode-quota` in `opencode.json`.
4. If token reports are empty, start OpenCode once so it creates `opencode.db`, then run a session with model usage.
Expand Down
23 changes: 18 additions & 5 deletions docs/readme/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ Start with `/quota_status`. It shows which config, providers, authentication, an

## First checks

1. Run `/quota_status`.
2. Find the provider or feature that is failing.
3. Follow the matching fix below.
4. Restart OpenCode after changing config or authentication.
1. Run `/quota_status`, or run `opencode-quota status` from your terminal for the same diagnostics without launching OpenCode.
2. Confirm the expected provider appears in the detected provider list.
3. Confirm companion auth plugins are before `@slkiser/opencode-quota` in `opencode.json`.
4. If token reports are empty, start OpenCode once so it creates `opencode.db`, then run a session with model usage.
5. Use the provider-specific table below for the failing provider.

If every provider is missing, confirm OpenCode Quota is listed in `opencode.jsonc` or `.json`. For TUI commands and displays, also confirm it is listed in `tui.jsonc` or `.json`.

Expand Down Expand Up @@ -37,7 +38,19 @@ npx @slkiser/opencode-quota@latest update --dry-run
npx @slkiser/opencode-quota@latest update
```

The updater preserves unrelated settings, comments, and plugins. Restart OpenCode when it finishes.
| Symptom | Try this |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/quota` or other slash commands do not appear | Confirm `opencode.json` includes `@slkiser/opencode-quota`, then restart OpenCode. The server plugin registers each command once for both TUI and Desktop/server; `tui.json` only enables the visual TUI surfaces. |
| `/quota` shows no providers | Run `/quota_status` (or `opencode-quota status` in your terminal), then check provider detection and auth. You can also use `opencode-quota show` for a terminal quota summary. |
| Sidebar panel does not appear | Confirm `tui.json` includes `@slkiser/opencode-quota`, restart OpenCode, and check `tuiSidebarPanel.enabled`. |
| Compact status line does not appear anywhere | Confirm `tui.json` includes `@slkiser/opencode-quota`, restart OpenCode, check `tuiCompactStatus.enabled`, and check whether `tuiCompactStatus.suppressWhenNativeProviderQuota` is hiding it because OpenCode exposes native provider-quota support. |
| Compact status appears on home but not in chat/session | Check `tuiCompactStatus.sessionPrompt`; set it to `true` to show the chat/session prompt line. |
| Popup toasts do not appear | Check `enableToast`, `showOnIdle`, `showOnQuestion`, and `showOnCompact`. |
| Announcement home notice does not appear | Confirm `tui.json` includes `@slkiser/opencode-quota`, restart OpenCode, then check `maintainerAnnouncements.enabled`, `maintainerAnnouncements.home`, and the active count in the `maintainer_announcements` section of `/quota_status`. |
| Token reports are empty | Start OpenCode once so `opencode.db` exists, then run a session with model usage. |
| Pricing looks stale | Run `/pricing_refresh`. |
| `/tokens_between` needs dates | Run `/tokens_between YYYY-MM-DD YYYY-MM-DD`. Missing or invalid dates produce inline usage output; no date dialog opens. |
| Desktop shows HTTP 500/error toast after correct command output | OpenCode 1.17.18 has no successful command-cancellation contract. The deterministic output was already injected as one ignored/no-reply message, and the handled sentinel prevents model continuation and context pollution. This is the accepted upstream limitation tracked by anomalyco/opencode#18554 and anomalyco/opencode#18559. |

## Provider fixes

Expand Down
9 changes: 9 additions & 0 deletions src/bin/opencode-quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const USAGE = [
"Usage:",
" npx @slkiser/opencode-quota init [--dry-run] [--sync-legacy-config]",
" npx @slkiser/opencode-quota show [--provider <provider-id>] [--json] [--threshold <pct>]",
" npx @slkiser/opencode-quota status [--provider <provider-id>] [--json]",
" npx @slkiser/opencode-quota update [--dry-run] [--yes]",
" npx @slkiser/opencode-quota provider add [--dry-run]",
" npx @slkiser/opencode-quota --help",
Expand All @@ -22,6 +23,9 @@ const USAGE = [
" --json Machine-readable JSON output (reads from cache)",
" --threshold <pct> With --json, exit 1 if below <pct>%, 2 if incomplete/not comparable",
" --provider <id> Filter to one provider",
" status Print Quota Status diagnostics (same data as /quota_status)",
" --json Machine-readable JSON output",
" --provider <id> Filter to one provider",
" update Safely refresh only OpenCode Quota config and verified cache entries",
" --dry-run Preview without changing config or cache",
" --yes Apply noninteractively after printing the preview",
Expand Down Expand Up @@ -81,6 +85,11 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
return await runCliShowCommand({ argv: rest });
}

if (command === "status") {
const { runCliStatusCommand } = await import("../lib/cli-status.js");
return await runCliStatusCommand({ argv: rest });
}

if (command === "update") {
const { runScopedUpdateCommand } = await import("../lib/scoped-update.js");
return await runScopedUpdateCommand({ argv: rest });
Expand Down
4 changes: 2 additions & 2 deletions src/lib/cli-show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function cloneCliConfig(config: QuotaToastConfig): QuotaToastConfig {
};
}

function resolveCliRoots(cwd: string): {
export function resolveCliRoots(cwd: string): {
workspaceRoot: string;
configRoot: string;
fallbackDirectory: string;
Expand All @@ -150,7 +150,7 @@ function resolveCliRoots(cwd: string): {
};
}

function createCliQuotaClient(params: { configRootDir: string }): QuotaRuntimeClient {
export function createCliQuotaClient(params: { configRootDir: string }): QuotaRuntimeClient {
let configPromise: Promise<Record<string, unknown>> | undefined;
let providerIdsPromise: Promise<string[]> | undefined;

Expand Down
166 changes: 166 additions & 0 deletions src/lib/cli-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { QuotaRuntimeClient } from "./quota-runtime-context.js";

import { getQuotaProviderShape } from "./provider-metadata.js";
import { resolveQuotaRuntimeContext } from "./quota-runtime-context.js";
import { buildStatusReportData, type QuotaStatusReportPayload } from "./quota-dialog-commands.js";
import { createCliQuotaClient, resolveCliRoots } from "./cli-show.js";

export interface RunCliStatusCommandOptions {
argv?: string[];
cwd?: string;
stdout?: Pick<NodeJS.WriteStream, "write">;
stderr?: Pick<NodeJS.WriteStream, "write">;
}

type ParsedStatusArgs =
| { ok: true; providerId?: string; help: boolean; json: boolean }
| { ok: false; error: string };

const STATUS_USAGE = [
"Usage:",
" npx @slkiser/opencode-quota status [--provider <provider-id>] [--json]",
"",
"Print the same Quota Status diagnostics as the /quota_status slash command,",
"without launching OpenCode.",
"",
"Options:",
" --provider <provider-id> Restrict provider availability and live probes to one provider",
" --json Machine-readable JSON output:",
" { version, generatedAt, config, providers, pricing, liveProbes }",
" --help, -h Show help",
"",
"Exit codes:",
" 0 success",
" 1 error or quota disabled (enabled: false)",
" 2 no comparable provider data (with --json only)",
].join("\n");

const THRESHOLD_REDIRECT =
"--threshold is not supported by status. For threshold exit codes, use: opencode-quota show --json --threshold <pct>";

function parseStatusArgs(argv: string[]): ParsedStatusArgs {
let providerId: string | undefined;
let json = false;

for (let index = 0; index < argv.length; index++) {
const arg = argv[index];

if (arg === "--help" || arg === "-h") {
return { ok: true, help: true, json: false };
}

if (arg === "--json") {
json = true;
continue;
}

if (arg === "--threshold" || arg.startsWith("--threshold=")) {
return { ok: false, error: THRESHOLD_REDIRECT };
}

if (arg === "--provider") {
const value = argv[index + 1];
if (!value || value.startsWith("-")) {
return { ok: false, error: "Missing value for --provider." };
}
if (providerId) {
return { ok: false, error: "Specify --provider only once." };
}
providerId = value;
index += 1;
continue;
}

if (arg.startsWith("--provider=")) {
const value = arg.slice("--provider=".length).trim();
if (!value) {
return { ok: false, error: "Missing value for --provider." };
}
if (providerId) {
return { ok: false, error: "Specify --provider only once." };
}
providerId = value;
continue;
}

if (arg.startsWith("-")) {
return { ok: false, error: `Unknown option: ${arg}` };
}

return { ok: false, error: `Unexpected argument: ${arg}` };
}

return { ok: true, providerId, help: false, json };
}

function writeLine(stream: Pick<NodeJS.WriteStream, "write">, message: string): void {
stream.write(message.endsWith("\n") ? message : `${message}\n`);
}

function hasComparableProviderData(payload: QuotaStatusReportPayload): boolean {
return payload.liveProbes.length > 0;
}

export async function runCliStatusCommand(
options: RunCliStatusCommandOptions = {},
): Promise<number> {
const argv = options.argv ?? process.argv.slice(3);
const stdout = options.stdout ?? process.stdout;
const stderr = options.stderr ?? process.stderr;

const parsed = parseStatusArgs(argv);
if (!parsed.ok) {
writeLine(stderr, parsed.error);
writeLine(stderr, STATUS_USAGE);
return 1;
}

if (parsed.help) {
writeLine(stdout, STATUS_USAGE);
return 0;
}

const providerId = parsed.providerId ? getQuotaProviderShape(parsed.providerId)?.id : undefined;
if (parsed.providerId && !providerId) {
writeLine(stderr, `Unknown provider: ${parsed.providerId}`);
return 1;
}

try {
const roots = resolveCliRoots(options.cwd ?? process.cwd());
const client: QuotaRuntimeClient = createCliQuotaClient({ configRootDir: roots.configRoot });
const runtime = await resolveQuotaRuntimeContext({
client,
roots,
includeSessionMeta: false,
});

if (!runtime.config.enabled) {
writeLine(stderr, "Quota disabled in config (enabled: false).");
return 1;
}

const data = await buildStatusReportData({
runtime,
generatedAtMs: Date.now(),
providerFilterId: providerId,
});

if (!data.output || !data.payload) {
writeLine(stderr, "Quota disabled in config (enabled: false).");
return 1;
}

if (parsed.json) {
writeLine(stdout, JSON.stringify(data.payload, null, 2));
return hasComparableProviderData(data.payload) ? 0 : 2;
}

writeLine(stdout, data.output);
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
writeLine(stderr, `Failed to generate quota status: ${message}`);
return 1;
}
}
Loading