Merge branch 'openchamber:main' into github-usage-rework
This commit is contained in:
@@ -674,7 +674,16 @@ export async function resolveGitHubPrStatus({ octokit, directory, branch, remote
|
||||
};
|
||||
}
|
||||
|
||||
const sourceCandidates = resolvedTargets.slice();
|
||||
// Only the repo this branch actually pushes to (the ranked-first remote)
|
||||
// and its fork network can be the SOURCE of the branch's PRs. Other
|
||||
// configured remotes — a maintainer's checkout often carries contributor
|
||||
// forks — are places to look for an open PR, but their `owner:branch`
|
||||
// heads are unrelated branches that merely share a name; treating them as
|
||||
// sources made a fork's closed `main` PR show up on the local main.
|
||||
const primaryRemoteName = resolvedTargets[0]?.remoteName ?? null;
|
||||
const sourceCandidates = resolvedTargets.filter(
|
||||
(target) => target.remoteName === primaryRemoteName,
|
||||
);
|
||||
// When every consulted repo list was complete, a no-PR result is
|
||||
// authoritative and the expensive Search API fallback is pointless.
|
||||
const coverage = { authoritative: true };
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { readAuthFile } from '../../opencode/auth.js';
|
||||
import { asObject, buildResult, getAuthEntry, normalizeAuthEntry, toNumber, toUsageWindow } from '../utils/index.js';
|
||||
|
||||
export const providerId = 'command-code';
|
||||
export const providerName = 'Command Code';
|
||||
export const aliases = ['command-code', 'commandcode', 'command_code', 'command code'];
|
||||
|
||||
const API_BASE_URL = 'https://api.commandcode.ai';
|
||||
|
||||
const getApiKey = (auth = readAuthFile()) => {
|
||||
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
||||
const stored = entry?.key ?? entry?.access ?? entry?.token;
|
||||
return (typeof stored === 'string' ? stored.trim() : '') || process.env.COMMAND_CODE_API_KEY?.trim() || null;
|
||||
};
|
||||
|
||||
const requestJson = async (path, apiKey, fetchImpl) => {
|
||||
const response = await fetchImpl(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'User-Agent': 'OpenChamber quota provider',
|
||||
},
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
if (response.status === 401 || response.status === 403) throw new Error('Command Code authentication failed');
|
||||
if (!response.ok) throw new Error(`Command Code usage API returned HTTP ${response.status}`);
|
||||
return response.json().catch(() => null);
|
||||
};
|
||||
|
||||
const formatCredits = (value) => String(Math.round((value + Number.EPSILON) * 100) / 100);
|
||||
|
||||
const toBalanceWindow = (value) => toUsageWindow({
|
||||
usedPercent: null,
|
||||
windowSeconds: null,
|
||||
resetAt: null,
|
||||
valueLabel: formatCredits(value),
|
||||
});
|
||||
|
||||
export const parseCommandCodeCredits = (payload) => {
|
||||
const root = asObject(payload);
|
||||
const credits = asObject(root?.credits);
|
||||
const limits = asObject(root?.windowLimits);
|
||||
const windows = {};
|
||||
|
||||
for (const [label, field] of [['monthly_credits', 'monthlyCredits'], ['purchased_credits', 'purchasedCredits'], ['free_credits', 'freeCredits']]) {
|
||||
const value = toNumber(credits?.[field]);
|
||||
if (value !== null) windows[label] = toBalanceWindow(value);
|
||||
}
|
||||
|
||||
for (const [label, field, windowSeconds] of [['5h', 'fiveHour', 5 * 60 * 60], ['weekly', 'weekly', 7 * 24 * 60 * 60]]) {
|
||||
const limit = asObject(limits?.[field]);
|
||||
const used = toNumber(limit?.used);
|
||||
const cap = toNumber(limit?.cap);
|
||||
if (used === null || cap === null || cap <= 0) continue;
|
||||
const resetAt = toNumber(limit?.resetAt);
|
||||
windows[label] = toUsageWindow({
|
||||
usedPercent: Math.min(100, Math.max(0, used / cap * 100)),
|
||||
windowSeconds,
|
||||
resetAt: resetAt === null ? null : resetAt < 1_000_000_000_000 ? resetAt * 1000 : resetAt,
|
||||
valueLabel: `${formatCredits(used)} / ${formatCredits(cap)}`,
|
||||
});
|
||||
}
|
||||
|
||||
return windows;
|
||||
};
|
||||
|
||||
export const fetchCommandCodeUsage = async (apiKey, fetchImpl = fetch) => {
|
||||
const identity = asObject(await requestJson('/alpha/whoami', apiKey, fetchImpl));
|
||||
const org = asObject(identity?.org);
|
||||
const orgId = typeof org?.id === 'string' ? org.id.trim() : '';
|
||||
const creditsPath = orgId
|
||||
? `/alpha/billing/credits?orgId=${encodeURIComponent(orgId)}`
|
||||
: '/alpha/billing/credits';
|
||||
const credits = await requestJson(creditsPath, apiKey, fetchImpl);
|
||||
const windows = parseCommandCodeCredits(credits);
|
||||
if (Object.keys(windows).length === 0) throw new Error('Command Code usage data could not be parsed');
|
||||
return windows;
|
||||
};
|
||||
|
||||
export const isConfigured = () => Boolean(getApiKey());
|
||||
|
||||
export const fetchQuota = async (auth = readAuthFile()) => {
|
||||
const apiKey = getApiKey(auth);
|
||||
if (!apiKey) return buildResult({ providerId, providerName, ok: false, configured: false, error: 'Not configured' });
|
||||
try {
|
||||
return buildResult({ providerId, providerName, ok: true, configured: true, usage: { windows: await fetchCommandCodeUsage(apiKey) } });
|
||||
} catch (error) {
|
||||
return buildResult({ providerId, providerName, ok: false, configured: true, error: error instanceof Error ? error.message : 'Request failed' });
|
||||
}
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { fetchCommandCodeUsage, fetchQuota, parseCommandCodeCredits } from './command-code.js';
|
||||
|
||||
const creditsPayload = {
|
||||
credits: { monthlyCredits: 120, purchasedCredits: 30, freeCredits: 5 },
|
||||
windowLimits: {
|
||||
fiveHour: { used: 25, cap: 100, resetAt: 1_776_000_000 },
|
||||
weekly: { used: 70, cap: 200, resetAt: 1_776_604_800 },
|
||||
},
|
||||
};
|
||||
|
||||
describe('Command Code quota provider', () => {
|
||||
it('parses balances and rate-limit windows', () => {
|
||||
const windows = parseCommandCodeCredits(creditsPayload);
|
||||
expect(windows.monthly_credits).toMatchObject({ usedPercent: null, valueLabel: '120' });
|
||||
expect(windows.purchased_credits).toMatchObject({ usedPercent: null, valueLabel: '30' });
|
||||
expect(windows.free_credits).toMatchObject({ usedPercent: null, valueLabel: '5' });
|
||||
expect(windows['5h']).toMatchObject({ usedPercent: 25, valueLabel: '25 / 100', resetAt: 1_776_000_000_000 });
|
||||
expect(windows.weekly.usedPercent).toBe(35);
|
||||
});
|
||||
|
||||
it('formats fractional credit values for display', () => {
|
||||
const windows = parseCommandCodeCredits({
|
||||
credits: { monthlyCredits: 69.7947070034 },
|
||||
windowLimits: { fiveHour: { used: 0.2052929966, cap: 14 } },
|
||||
});
|
||||
expect(windows.monthly_credits.valueLabel).toBe('69.79');
|
||||
expect(windows['5h'].valueLabel).toBe('0.21 / 14');
|
||||
});
|
||||
|
||||
it('resolves the organization before fetching credits', async () => {
|
||||
const requests = [];
|
||||
const windows = await fetchCommandCodeUsage('secret', async (url, options) => {
|
||||
requests.push({ url, options });
|
||||
return new Response(JSON.stringify(url.endsWith('/alpha/whoami') ? { org: { id: 'org/a' } } : creditsPayload));
|
||||
});
|
||||
expect(requests.map(({ url }) => url)).toEqual([
|
||||
'https://api.commandcode.ai/alpha/whoami',
|
||||
'https://api.commandcode.ai/alpha/billing/credits?orgId=org%2Fa',
|
||||
]);
|
||||
expect(requests[0].options.headers.Authorization).toBe('Bearer secret');
|
||||
expect(windows['5h'].usedPercent).toBe(25);
|
||||
});
|
||||
|
||||
it('fetches account-scoped credits without orgId for personal accounts', async () => {
|
||||
const urls = [];
|
||||
await fetchCommandCodeUsage('secret', async (url) => {
|
||||
urls.push(url);
|
||||
return new Response(JSON.stringify(url.endsWith('/alpha/whoami') ? { user: { id: 'user-1' }, org: null } : creditsPayload));
|
||||
});
|
||||
expect(urls).toEqual([
|
||||
'https://api.commandcode.ai/alpha/whoami',
|
||||
'https://api.commandcode.ai/alpha/billing/credits',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not expose credentials in authentication errors', async () => {
|
||||
await expect(fetchCommandCodeUsage('secret', async () => new Response('', { status: 401 }))).rejects.toThrow('authentication failed');
|
||||
});
|
||||
|
||||
it('reads OAuth access credentials from the OpenCode auth file', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ org: { id: 'org-1' } })))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify(creditsPayload)));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const result = await fetchQuota({ 'command-code': { type: 'oauth', access: 'test-token' } });
|
||||
expect(result).toMatchObject({ providerId: 'command-code', ok: true, configured: true });
|
||||
expect(fetchMock.mock.calls[0][1].headers.Authorization).toBe('Bearer test-token');
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('recognizes Command Code auth entries under supported provider ID variants', async () => {
|
||||
for (const providerId of ['commandcode', 'command_code', 'command code']) {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ org: { id: 'org-1' } })))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify(creditsPayload)));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const result = await fetchQuota({ [providerId]: { type: 'oauth', access: 'test-token' } });
|
||||
expect(result).toMatchObject({ providerId: 'command-code', ok: true, configured: true });
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,6 @@ import { buildResult } from '../utils/index.js';
|
||||
|
||||
import * as claude from './claude/index.js';
|
||||
import * as codex from './codex.js';
|
||||
import * as commandCode from './command-code.js';
|
||||
import * as copilot from './copilot.js';
|
||||
import * as crof from './crof.js';
|
||||
import * as cursor from './cursor.js';
|
||||
@@ -30,12 +29,6 @@ import * as opencodeGo from './opencode-go.js';
|
||||
import * as xai from './xai.js';
|
||||
|
||||
const registry = {
|
||||
'command-code': {
|
||||
providerId: commandCode.providerId,
|
||||
providerName: commandCode.providerName,
|
||||
isConfigured: commandCode.isConfigured,
|
||||
fetchQuota: commandCode.fetchQuota
|
||||
},
|
||||
claude: {
|
||||
providerId: claude.providerId,
|
||||
providerName: claude.providerName,
|
||||
@@ -160,12 +153,6 @@ const registry = {
|
||||
|
||||
const pendingFetches = new Map();
|
||||
|
||||
const normalizeQuotaProviderId = (providerId) => {
|
||||
if (typeof providerId !== 'string') return providerId;
|
||||
return ['command-code', 'commandcode', 'command_code', 'command code'].includes(providerId.trim().toLowerCase())
|
||||
? 'command-code'
|
||||
: providerId;
|
||||
};
|
||||
|
||||
export const listConfiguredQuotaProviders = () => {
|
||||
const configured = [];
|
||||
@@ -210,14 +197,13 @@ const fetchQuotaForProviderUncoalesced = async (providerId) => {
|
||||
};
|
||||
|
||||
export const fetchQuotaForProvider = (providerId) => {
|
||||
const normalizedProviderId = normalizeQuotaProviderId(providerId);
|
||||
const existing = pendingFetches.get(normalizedProviderId);
|
||||
const existing = pendingFetches.get(providerId);
|
||||
if (existing) return existing;
|
||||
|
||||
const pending = fetchQuotaForProviderUncoalesced(normalizedProviderId).finally(() => {
|
||||
if (pendingFetches.get(normalizedProviderId) === pending) pendingFetches.delete(normalizedProviderId);
|
||||
const pending = fetchQuotaForProviderUncoalesced(providerId).finally(() => {
|
||||
if (pendingFetches.get(providerId) === pending) pendingFetches.delete(providerId);
|
||||
});
|
||||
pendingFetches.set(normalizedProviderId, pending);
|
||||
pendingFetches.set(providerId, pending);
|
||||
return pending;
|
||||
};
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ export const createSessionGoalRuntime = ({
|
||||
getOpenCodeAuthHeaders,
|
||||
getSmallModelService,
|
||||
emitGoalNotification,
|
||||
isEnabled = isSessionGoalEnabled,
|
||||
idleQuietMs = IDLE_QUIET_MS,
|
||||
kickoffQuietMs = KICKOFF_QUIET_MS,
|
||||
maxAutoTurns = MAX_AUTO_TURNS,
|
||||
@@ -444,7 +445,7 @@ export const createSessionGoalRuntime = ({
|
||||
};
|
||||
|
||||
const tick = async (sessionId, directory) => {
|
||||
if (!isSessionGoalEnabled()) return;
|
||||
if (!isEnabled()) return;
|
||||
|
||||
const session = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, { directory })
|
||||
.catch((error) => {
|
||||
|
||||
@@ -35,13 +35,14 @@ const startIdleTick = async (fetchImpl) => {
|
||||
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
||||
getOpenCodeAuthHeaders: () => ({}),
|
||||
getSmallModelService,
|
||||
isEnabled: () => true,
|
||||
idleQuietMs: 10,
|
||||
});
|
||||
runtime.processPayload({
|
||||
type: 'session.status',
|
||||
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
return { runtime, getSmallModelService };
|
||||
};
|
||||
|
||||
@@ -156,6 +157,7 @@ describe('session goal live activity gate', () => {
|
||||
buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
|
||||
getOpenCodeAuthHeaders: () => ({}),
|
||||
getSmallModelService: async () => service,
|
||||
isEnabled: () => true,
|
||||
idleQuietMs: 10,
|
||||
});
|
||||
|
||||
@@ -163,7 +165,7 @@ describe('session goal live activity gate', () => {
|
||||
type: 'session.status',
|
||||
properties: { sessionID: SESSION_ID, status: { type: 'idle' }, directory: DIRECTORY },
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(service.generateSmallModelText).toHaveBeenCalledOnce();
|
||||
const patch = requests.find((request) => request.pathname === `/session/${SESSION_ID}` && request.method === 'PATCH');
|
||||
|
||||
Reference in New Issue
Block a user