feat: enhanced usage providers to extract account identifiers from headers
- Enhanced usage providers to extract account identifiers from response headers. - Fixed GitHub Copilot usage provider token handling and account identifier resolution. - Added usage report deduplication and provider sorting features. - Added isolated task execution with git worktree management.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added usage tracking system with normalized schema for provider quota/limit endpoints
|
||||
@@ -12,8 +11,16 @@
|
||||
- Added OpenAI Codex usage provider for primary and secondary rate limit windows
|
||||
- Added ZAI usage provider for token and request quota tracking
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Claude usage provider to extract account identifiers from response headers
|
||||
- Updated GitHub Copilot usage provider to include account identifiers in usage reports
|
||||
- Updated Google Gemini CLI usage provider to handle missing reset time gracefully
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed GitHub Copilot usage provider to simplify token handling and improve reliability
|
||||
- Fixed GitHub Copilot usage provider to properly resolve account identifiers for OAuth credentials
|
||||
- Fixed API validation errors when sending empty user messages (resume with `.`) across all providers:
|
||||
- Google Cloud Code Assist (google-shared.ts)
|
||||
- OpenAI Responses API (openai-responses.ts)
|
||||
@@ -21,6 +28,10 @@
|
||||
- Cursor (cursor.ts)
|
||||
- Amazon Bedrock (amazon-bedrock.ts)
|
||||
- Clamped OpenAI Codex reasoning effort "minimal" to "low" for gpt-5.2 models to avoid API errors
|
||||
- Fixed GitHub Copilot usage fallback to internal quota endpoints when billing usage is unavailable
|
||||
- Fixed GitHub Copilot usage metadata to include account identifiers for report dedupe
|
||||
- Fixed Anthropic usage metadata extraction to include account identifiers when provided by the usage endpoint
|
||||
- Fixed Gemini CLI usage windows to consistently label quota windows for display suppression
|
||||
|
||||
## [6.9.69] - 2026-01-21
|
||||
### Added
|
||||
|
||||
@@ -73,10 +73,20 @@ function getModelTier(modelId: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseWindow(resetTime: string | undefined, now: number): UsageWindow | undefined {
|
||||
if (!resetTime) return undefined;
|
||||
function parseWindow(resetTime: string | undefined, now: number): UsageWindow {
|
||||
if (!resetTime) {
|
||||
return {
|
||||
id: "quota",
|
||||
label: "Quota window",
|
||||
};
|
||||
}
|
||||
const resetsAt = Date.parse(resetTime);
|
||||
if (Number.isNaN(resetsAt)) return undefined;
|
||||
if (Number.isNaN(resetsAt)) {
|
||||
return {
|
||||
id: "quota",
|
||||
label: "Quota window",
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: `reset-${resetsAt}`,
|
||||
label: "Quota window",
|
||||
|
||||
@@ -62,6 +62,11 @@ interface ClaudeUsageResponse {
|
||||
seven_day_sonnet?: ClaudeUsageBucket | null;
|
||||
}
|
||||
|
||||
type ClaudeUsagePayload = {
|
||||
payload: ClaudeUsageResponse;
|
||||
orgId?: string;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -94,6 +99,28 @@ function parseBucket(bucket: unknown): ParsedUsageBucket | undefined {
|
||||
return { utilization, resetsAt };
|
||||
}
|
||||
|
||||
function getPayloadString(payload: Record<string, unknown>, key: string): string | undefined {
|
||||
const value = payload[key];
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function extractUsageIdentity(payload: ClaudeUsageResponse, orgId?: string): { accountId?: string; email?: string } {
|
||||
if (!isRecord(payload)) return { accountId: orgId };
|
||||
const accountId =
|
||||
getPayloadString(payload, "account_id") ??
|
||||
getPayloadString(payload, "accountId") ??
|
||||
getPayloadString(payload, "user_id") ??
|
||||
getPayloadString(payload, "userId") ??
|
||||
getPayloadString(payload, "org_id") ??
|
||||
getPayloadString(payload, "orgId") ??
|
||||
orgId;
|
||||
const email =
|
||||
getPayloadString(payload, "email") ??
|
||||
getPayloadString(payload, "user_email") ??
|
||||
getPayloadString(payload, "userEmail");
|
||||
return { accountId, email };
|
||||
}
|
||||
|
||||
function hasUsageData(payload: ClaudeUsageResponse): boolean {
|
||||
return Boolean(payload.five_hour || payload.seven_day || payload.seven_day_opus || payload.seven_day_sonnet);
|
||||
}
|
||||
@@ -103,8 +130,9 @@ async function fetchUsagePayload(
|
||||
headers: Record<string, string>,
|
||||
ctx: UsageFetchContext,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ClaudeUsageResponse | null> {
|
||||
): Promise<ClaudeUsagePayload | null> {
|
||||
let lastPayload: ClaudeUsageResponse | null = null;
|
||||
let lastOrgId: string | undefined;
|
||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await ctx.fetch(url, { headers, signal });
|
||||
@@ -114,8 +142,10 @@ async function fetchUsagePayload(
|
||||
}
|
||||
const payload = (await response.json()) as ClaudeUsageResponse;
|
||||
lastPayload = payload;
|
||||
const orgId = response.headers.get("anthropic-organization-id")?.trim() || undefined;
|
||||
lastOrgId = orgId ?? lastOrgId;
|
||||
if (payload && isRecord(payload) && hasUsageData(payload)) {
|
||||
return payload;
|
||||
return { payload, orgId };
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger?.warn("Claude usage fetch error", { error: String(error) });
|
||||
@@ -127,7 +157,7 @@ async function fetchUsagePayload(
|
||||
}
|
||||
}
|
||||
|
||||
return lastPayload;
|
||||
return lastPayload ? { payload: lastPayload, orgId: lastOrgId } : null;
|
||||
}
|
||||
|
||||
function buildUsageAmount(utilization: number | undefined): UsageAmount | undefined {
|
||||
@@ -240,8 +270,9 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
||||
authorization: `Bearer ${credential.accessToken}`,
|
||||
};
|
||||
|
||||
const payload = await fetchUsagePayload(url, headers, ctx, params.signal);
|
||||
if (!payload || !isRecord(payload)) return cachedValue;
|
||||
const payloadResult = await fetchUsagePayload(url, headers, ctx, params.signal);
|
||||
if (!payloadResult || !isRecord(payloadResult.payload)) return cachedValue;
|
||||
const { payload, orgId } = payloadResult;
|
||||
|
||||
const fiveHour = parseBucket(payload.five_hour);
|
||||
const sevenDay = parseBucket(payload.seven_day);
|
||||
@@ -296,14 +327,17 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
||||
].filter((limit): limit is UsageLimit => limit !== null);
|
||||
|
||||
if (limits.length === 0) return cachedValue;
|
||||
const identity = extractUsageIdentity(payload, orgId);
|
||||
const accountId = identity.accountId ?? credential.accountId;
|
||||
const email = identity.email ?? credential.email;
|
||||
|
||||
const report: UsageReport = {
|
||||
provider: params.provider,
|
||||
fetchedAt: now,
|
||||
limits,
|
||||
metadata: {
|
||||
accountId: credential.accountId,
|
||||
email: credential.email,
|
||||
accountId,
|
||||
email,
|
||||
endpoint: url,
|
||||
},
|
||||
raw: payload,
|
||||
|
||||
@@ -49,11 +49,6 @@ type CopilotUsageResponse = {
|
||||
quota_snapshots: CopilotQuotaSnapshots;
|
||||
};
|
||||
|
||||
type CopilotTokenResponse = {
|
||||
token: string;
|
||||
expires_at: number;
|
||||
};
|
||||
|
||||
type BillingUsageItem = {
|
||||
product: string;
|
||||
sku: string;
|
||||
@@ -101,13 +96,6 @@ function resolveGitHubApiBaseUrl(params: UsageFetchParams): string {
|
||||
return `https://api.${enterpriseUrl}`;
|
||||
}
|
||||
|
||||
function resolveCopilotApiBaseUrl(params: UsageFetchParams): string {
|
||||
if (params.baseUrl) return params.baseUrl.replace(/\/$/, "");
|
||||
const enterpriseUrl = params.credential.enterpriseUrl?.trim();
|
||||
if (enterpriseUrl) return `https://api.${enterpriseUrl}`;
|
||||
return "https://api.individual.githubcopilot.com";
|
||||
}
|
||||
|
||||
function buildCacheKey(params: UsageFetchParams): string {
|
||||
const parts: string[] = [params.provider];
|
||||
const { credential } = params;
|
||||
@@ -221,68 +209,21 @@ async function resolveGitHubUsername(
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeForCopilotToken(
|
||||
ctx: UsageFetchContext,
|
||||
baseUrl: string,
|
||||
oauthToken: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CopilotTokenResponse | null> {
|
||||
try {
|
||||
const data = await fetchJson(ctx, `${baseUrl}/copilot_internal/v2/token`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${oauthToken}`,
|
||||
...COPILOT_HEADERS,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!isRecord(data)) return null;
|
||||
const token = typeof data.token === "string" ? data.token : undefined;
|
||||
const expiresAt = toNumber(data.expires_at);
|
||||
if (!token || !expiresAt) return null;
|
||||
return { token, expires_at: expiresAt };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchInternalUsage(
|
||||
ctx: UsageFetchContext,
|
||||
baseUrl: string,
|
||||
oauthToken: string,
|
||||
accessToken: string | undefined,
|
||||
expiresAt: number | undefined,
|
||||
githubApiBaseUrl: string,
|
||||
token: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CopilotUsageResponse> {
|
||||
const requestWithToken = async (token: string, legacy: boolean) => {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: legacy ? `token ${token}` : `Bearer ${token}`,
|
||||
...COPILOT_HEADERS,
|
||||
};
|
||||
const data = await fetchJson(ctx, `${baseUrl}/copilot_internal/user`, { headers, signal });
|
||||
if (!isRecord(data)) throw new Error("Invalid Copilot usage response");
|
||||
return data as CopilotUsageResponse;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
...COPILOT_HEADERS,
|
||||
};
|
||||
|
||||
const now = ctx.now();
|
||||
if (accessToken && expiresAt && accessToken !== oauthToken && expiresAt > now) {
|
||||
try {
|
||||
return await requestWithToken(accessToken, false);
|
||||
} catch {
|
||||
// Ignore and try other strategies.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await requestWithToken(oauthToken, true);
|
||||
} catch {
|
||||
const exchanged = await exchangeForCopilotToken(ctx, baseUrl, oauthToken, signal);
|
||||
if (!exchanged) throw new Error("Copilot usage token exchange failed");
|
||||
return requestWithToken(exchanged.token, false);
|
||||
}
|
||||
const data = await fetchJson(ctx, `${githubApiBaseUrl}/copilot_internal/user`, { headers, signal });
|
||||
if (!isRecord(data)) throw new Error("Invalid Copilot usage response");
|
||||
return data as CopilotUsageResponse;
|
||||
}
|
||||
|
||||
async function fetchBillingUsage(
|
||||
@@ -315,6 +256,7 @@ function buildLimitFromQuota(
|
||||
quota: CopilotQuotaDetail,
|
||||
plan: string,
|
||||
window: UsageWindow | undefined,
|
||||
accountId?: string,
|
||||
): UsageLimit {
|
||||
const used = quota.unlimited ? undefined : Math.max(0, quota.entitlement - quota.remaining);
|
||||
const limit = quota.unlimited ? undefined : quota.entitlement;
|
||||
@@ -329,6 +271,7 @@ function buildLimitFromQuota(
|
||||
label,
|
||||
scope: {
|
||||
provider: "github-copilot",
|
||||
accountId,
|
||||
tier: plan,
|
||||
windowId: window?.id,
|
||||
},
|
||||
@@ -342,21 +285,22 @@ function buildLimitFromQuota(
|
||||
function normalizeQuotaSnapshots(
|
||||
data: CopilotUsageResponse,
|
||||
now: number,
|
||||
accountId?: string,
|
||||
): { limits: UsageLimit[]; window?: UsageWindow } {
|
||||
const window = buildWindow(data.quota_reset_date, now);
|
||||
const snapshots = data.quota_snapshots ?? {};
|
||||
const limits: UsageLimit[] = [];
|
||||
const premium = parseQuotaDetail(snapshots.premium_interactions);
|
||||
if (premium) {
|
||||
limits.push(buildLimitFromQuota("premium", "Premium Requests", premium, data.copilot_plan, window));
|
||||
limits.push(buildLimitFromQuota("premium", "Premium Requests", premium, data.copilot_plan, window, accountId));
|
||||
}
|
||||
const chat = parseQuotaDetail(snapshots.chat);
|
||||
if (chat && !chat.unlimited) {
|
||||
limits.push(buildLimitFromQuota("chat", "Chat Requests", chat, data.copilot_plan, window));
|
||||
limits.push(buildLimitFromQuota("chat", "Chat Requests", chat, data.copilot_plan, window, accountId));
|
||||
}
|
||||
const completions = parseQuotaDetail(snapshots.completions);
|
||||
if (completions && !completions.unlimited) {
|
||||
limits.push(buildLimitFromQuota("completions", "Completions", completions, data.copilot_plan, window));
|
||||
limits.push(buildLimitFromQuota("completions", "Completions", completions, data.copilot_plan, window, accountId));
|
||||
}
|
||||
return { limits, window };
|
||||
}
|
||||
@@ -437,26 +381,36 @@ export const githubCopilotUsageProvider: UsageProvider = {
|
||||
const cached = await ctx.cache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > now) return cached.value;
|
||||
|
||||
const baseUrl =
|
||||
params.credential.type === "api_key" ? resolveGitHubApiBaseUrl(params) : resolveCopilotApiBaseUrl(params);
|
||||
const githubApiBaseUrl = resolveGitHubApiBaseUrl(params);
|
||||
let report: UsageReport | null = null;
|
||||
|
||||
if (params.credential.type === "api_key") {
|
||||
let username =
|
||||
let username: string | undefined;
|
||||
const candidate =
|
||||
params.credential.accountId || params.credential.metadata?.username || params.credential.metadata?.user;
|
||||
if ((!username || typeof username !== "string" || !username.trim()) && params.credential.apiKey) {
|
||||
username = await resolveGitHubUsername(ctx, baseUrl, params.credential.apiKey, params.signal);
|
||||
if (typeof candidate === "string" && candidate.trim()) {
|
||||
username = candidate.trim();
|
||||
}
|
||||
if (typeof username !== "string" || !username.trim()) {
|
||||
if (!username && params.credential.apiKey) {
|
||||
username = await resolveGitHubUsername(ctx, githubApiBaseUrl, params.credential.apiKey, params.signal);
|
||||
}
|
||||
if (!username) {
|
||||
ctx.logger?.warn("Copilot usage requires username for billing API", { provider: params.provider });
|
||||
} else if (params.credential.apiKey) {
|
||||
try {
|
||||
const billing = await fetchBillingUsage(ctx, baseUrl, username, params.credential.apiKey, params.signal);
|
||||
const billing = await fetchBillingUsage(
|
||||
ctx,
|
||||
githubApiBaseUrl,
|
||||
username,
|
||||
params.credential.apiKey,
|
||||
params.signal,
|
||||
);
|
||||
report = {
|
||||
provider: "github-copilot",
|
||||
fetchedAt: now,
|
||||
limits: normalizeBillingUsage(billing),
|
||||
metadata: {
|
||||
accountId: billing.user,
|
||||
account: billing.user,
|
||||
period: billing.timePeriod,
|
||||
},
|
||||
@@ -465,26 +419,49 @@ export const githubCopilotUsageProvider: UsageProvider = {
|
||||
ctx.logger?.warn("Copilot usage fetch failed", { error: String(error) });
|
||||
}
|
||||
}
|
||||
if (!report && params.credential.apiKey) {
|
||||
try {
|
||||
const usage = await fetchInternalUsage(ctx, githubApiBaseUrl, params.credential.apiKey, params.signal);
|
||||
const normalized = normalizeQuotaSnapshots(usage, now, username);
|
||||
report = {
|
||||
provider: "github-copilot",
|
||||
fetchedAt: now,
|
||||
limits: normalized.limits,
|
||||
metadata: {
|
||||
accountId: username,
|
||||
plan: usage.copilot_plan,
|
||||
quotaResetDate: usage.quota_reset_date,
|
||||
},
|
||||
raw: usage,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.logger?.warn("Copilot usage fetch failed", { error: String(error) });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { refreshToken, accessToken, expiresAt } = params.credential;
|
||||
const { refreshToken, accessToken } = params.credential;
|
||||
if (!refreshToken && !accessToken) return null;
|
||||
const oauthToken = refreshToken || accessToken;
|
||||
if (!oauthToken) return null;
|
||||
const githubToken = refreshToken ?? accessToken;
|
||||
if (!githubToken) return null;
|
||||
try {
|
||||
const usage = await fetchInternalUsage(
|
||||
ctx,
|
||||
baseUrl,
|
||||
oauthToken,
|
||||
accessToken ?? undefined,
|
||||
expiresAt ?? undefined,
|
||||
params.signal,
|
||||
);
|
||||
const normalized = normalizeQuotaSnapshots(usage, now);
|
||||
const usage = await fetchInternalUsage(ctx, githubApiBaseUrl, githubToken, params.signal);
|
||||
let accountId = params.credential.accountId;
|
||||
if (!accountId && refreshToken) {
|
||||
accountId = await resolveGitHubUsername(ctx, githubApiBaseUrl, refreshToken, params.signal);
|
||||
}
|
||||
if (!accountId && accessToken) {
|
||||
accountId = await resolveGitHubUsername(ctx, githubApiBaseUrl, accessToken, params.signal);
|
||||
}
|
||||
const normalized = normalizeQuotaSnapshots(usage, now, accountId);
|
||||
report = {
|
||||
provider: "github-copilot",
|
||||
fetchedAt: now,
|
||||
limits: normalized.limits,
|
||||
metadata: {
|
||||
accountId,
|
||||
email: params.credential.email,
|
||||
plan: usage.copilot_plan,
|
||||
quotaResetDate: usage.quota_reset_date,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added usage report deduplication to prevent duplicate account entries
|
||||
- Added debug logging for usage fetch operations to aid diagnostics
|
||||
- Added provider sorting in usage display by total usage amount
|
||||
- Added `isolated` parameter to task tool for running each task in separate git worktrees
|
||||
- Added git worktree management for isolated task execution with patch generation
|
||||
- Added patch application system that applies changes only when all patches are valid
|
||||
- Added working directory information to environment info display
|
||||
- Added `/usage` command to display provider usage and limits
|
||||
- Added support for multiple usage providers beyond Codex
|
||||
- Added usage report caching with configurable TTL
|
||||
@@ -14,9 +20,16 @@
|
||||
- Added support for jq-like queries when reading JSON outputs
|
||||
- Added offset and limit parameters for reading specific line ranges from outputs
|
||||
- Added "." and "c" shortcuts to continue agent without sending visible message
|
||||
- Added debug logging for usage fetch results to aid /usage diagnostics
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated discoverSkills function to return object with skills property
|
||||
- Enhanced usage report merging to combine limits and metadata from duplicate accounts
|
||||
- Improved OAuth credential handling to preserve existing fields when updating
|
||||
- Removed cd function from Python prelude to encourage using cwd parameter
|
||||
- Updated task tool to generate and apply patches when running in isolated mode
|
||||
- Enhanced task tool rendering to display isolated execution status and patch paths
|
||||
- Updated system prompt structure and formatting for better readability
|
||||
- Reorganized tool hierarchy and discipline sections
|
||||
- Added parallel work guidance for task-based workflows
|
||||
@@ -32,12 +45,17 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed TypeScript error in bash executor by properly typing caught exception
|
||||
- Fixed usage display ordering to show providers with lowest usage first
|
||||
- Fixed task tool result rendering to show fallback text when no results are available
|
||||
- Fixed external editor to work properly on Unix systems by correctly handling terminal I/O
|
||||
- Fixed external editor to show warning message when it fails to open instead of silently failing
|
||||
- Fixed find tool to properly handle no matches case without treating as error
|
||||
- Fixed find tool to wait for fd exit so error messages no longer report exit null
|
||||
- Fixed read tool to properly handle no matches case without treating as error
|
||||
- Fixed orphaned Python kernel gateway processes not being killed on process exit
|
||||
- Fixed /usage provider ordering to sort by aggregate usage (most used last)
|
||||
- Fixed /usage account dedupe to collapse identical accounts using usage metadata
|
||||
|
||||
## [6.9.69] - 2026-01-21
|
||||
|
||||
@@ -81,6 +99,7 @@
|
||||
- Fixed patch indentation normalization for fuzzy matches, tab/space diffs, and ambiguous context alignment
|
||||
|
||||
## [6.9.0] - 2026-01-21
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed Git tool and all related functionality
|
||||
@@ -91,6 +110,7 @@
|
||||
- Removed @oh-my-pi/pi-git-tool dependency
|
||||
|
||||
## [6.8.5] - 2026-01-21
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Changed timeout parameter from seconds to milliseconds in Python tool
|
||||
@@ -102,6 +122,7 @@
|
||||
- Improved streaming output handling and buffer management
|
||||
|
||||
## [6.8.4] - 2026-01-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated output sink to properly handle large outputs
|
||||
@@ -183,6 +204,7 @@
|
||||
- Updated temporary file cleanup to use secure async removal methods
|
||||
|
||||
## [6.7.67] - 2026-01-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added normative rewrite setting to control tool call argument normalization in session history
|
||||
@@ -231,7 +253,7 @@
|
||||
|
||||
- Patch application handles repeated context blocks, preserves original indentation on fuzzy match
|
||||
- Ambiguous context matching resolves duplicates using adjacent @@ anchor positioning
|
||||
- Patch parser handles bare *** terminators, model hallucination markers, line hint ranges
|
||||
- Patch parser handles bare \*\*\* terminators, model hallucination markers, line hint ranges
|
||||
- Function context matching handles signatures with and without empty parentheses
|
||||
- Fixed session title generation to respect OMP_NO_TITLE environment variable
|
||||
- Fixed Python module discovery to use import.meta.dir for ES module compatibility
|
||||
@@ -243,6 +265,7 @@
|
||||
- Fixed MCP tool path formatting to correctly display provider information
|
||||
|
||||
## [6.2.0] - 2026-01-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved LSP batching to coalesce formatting and diagnostics for parallel edits
|
||||
@@ -280,12 +303,14 @@
|
||||
- Fixed TTSR abbreviation expansion from TTSR to Time Traveling Stream Rules
|
||||
|
||||
## [5.8.0] - 2026-01-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated WASM loading to use streaming for development environments with base64 fallback
|
||||
- Added scripts directory to published package files
|
||||
|
||||
## [5.7.68] - 2026-01-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated WASM loading to use base64-encoded WASM for better compatibility with compiled binaries
|
||||
@@ -295,12 +320,14 @@
|
||||
- Fixed WASM loading issues in compiled binary builds
|
||||
|
||||
## [5.7.67] - 2026-01-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced external photon-node dependency with vendored WebAssembly implementation
|
||||
- Updated image processing to use local photon library for better performance
|
||||
|
||||
## [5.6.70] - 2026-01-18
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for loading Python prelude extension modules from user and project directories
|
||||
@@ -373,7 +400,7 @@
|
||||
- Enhanced Python kernel availability checking with faster validation
|
||||
- Optimized Python environment warming to avoid blocking during tool initialization
|
||||
- Reorganized settings interface into behavior, tools, display, voice, status, lsp, and exa tabs
|
||||
- Migrated environment variables from PI_ to OMP_ prefix with automatic migration
|
||||
- Migrated environment variables from PI* to OMP* prefix with automatic migration
|
||||
- Updated model selector to use TabBar component for provider navigation
|
||||
- Changed role badges to inverted style with colored backgrounds
|
||||
- Added support for /models command alias in addition to /model
|
||||
@@ -406,11 +433,13 @@
|
||||
- Enhanced Python gateway environment filtering to exclude sensitive API keys and Windows system paths
|
||||
|
||||
## [5.5.0] - 2026-01-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated task execution guidelines to improve prompt framing and parallelization instructions
|
||||
|
||||
## [5.4.2] - 2026-01-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated model resolution to accept pre-serialized settings for better performance
|
||||
@@ -418,11 +447,13 @@
|
||||
- Enhanced edit tool documentation with clear use cases for bash alternatives
|
||||
|
||||
## [5.3.0] - 2026-01-15
|
||||
|
||||
### Changed
|
||||
|
||||
- Expanded bash tool guidance to explicitly list appropriate use cases including file operations, build commands, and process management
|
||||
|
||||
## [5.2.1] - 2026-01-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed stale diagnostic results by tracking diagnostic versions before file sync operations
|
||||
@@ -466,6 +497,7 @@
|
||||
- Fixed session selector page up/down navigation
|
||||
|
||||
## [5.0.1] - 2026-01-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced wasm-vips with Photon for more stable WASM image processing
|
||||
@@ -492,6 +524,7 @@
|
||||
- Move `sharp` to optional dependencies with all platform binaries to fix arm64 runtime errors
|
||||
|
||||
## [4.7.0] - 2026-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add `omp config` subcommand for managing settings (`list`, `get`, `set`, `reset`, `path`)
|
||||
@@ -511,6 +544,7 @@
|
||||
## [4.6.0] - 2026-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add `/skill:name` slash commands for quick skill access (toggle via `skills.enableSkillCommands` setting)
|
||||
- Add `cwd` to SessionInfo for session list display
|
||||
- Add custom summarization instructions option in tree selector
|
||||
@@ -518,10 +552,12 @@
|
||||
- Add `shutdownRequested` and `checkShutdownRequested()` for extension-initiated shutdown
|
||||
|
||||
### Fixed
|
||||
|
||||
- Component `invalidate()` now properly rebuilds content on theme changes
|
||||
- Force full re-render after returning from external editor
|
||||
|
||||
## [4.4.8] - 2026-01-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed review finding priority format from numeric (0-3) to string labels (P0-P3) for clearer severity indication
|
||||
@@ -541,6 +577,7 @@
|
||||
- Fixed frontmatter parsing to properly report source location when YAML parsing fails
|
||||
|
||||
## [4.4.4] - 2026-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added `todo_write` tool for creating and managing structured task lists during coding sessions
|
||||
@@ -571,6 +608,7 @@
|
||||
- Fixed prompt template loading to strip leading HTML comment metadata blocks
|
||||
|
||||
## [4.3.2] - 2026-01-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Increased default bash output preview from 5 to 10 lines when collapsed
|
||||
@@ -610,6 +648,7 @@
|
||||
- Fixed serialized auth storage initialization so OAuth refreshes in subagents don't crash
|
||||
|
||||
## [4.2.2] - 2026-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added persistent cache storage for Codex usage data that survives application restarts
|
||||
@@ -630,6 +669,7 @@
|
||||
- Removed `planner` agent command template, consolidating planning functionality into the `plan` agent
|
||||
|
||||
## [4.2.1] - 2026-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added automatic discovery and listing of AGENTS.md files in the system prompt, providing agents with an authoritative list of project-specific instruction files without runtime searching
|
||||
@@ -696,6 +736,7 @@
|
||||
- Hardened file permissions on agent database directory (700) and database file (600) to restrict access
|
||||
|
||||
## [4.1.0] - 2026-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added persistent prompt history with SQLite-backed storage and Ctrl+R search
|
||||
@@ -705,6 +746,7 @@
|
||||
- Fixed credential blocking logic to correctly check for remaining available credentials instead of always returning true
|
||||
|
||||
## [4.0.1] - 2026-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added usage limit error detection to enable automatic credential switching when Codex accounts hit rate limits
|
||||
@@ -808,11 +850,13 @@
|
||||
- Extension directories in `settings.json` respect `package.json` manifests
|
||||
|
||||
## [3.37.0] - 2026-01-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved bash command display to show relative paths for working directories within the current directory, and hide redundant `cd` prefix when working directory matches current directory
|
||||
|
||||
## [3.36.0] - 2026-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added `calc` tool for basic mathematical calculations with support for arithmetic operators, parentheses, and hex/binary/octal literals
|
||||
@@ -831,6 +875,7 @@
|
||||
- Improved completion notification message to include session title when available
|
||||
|
||||
## [3.35.0] - 2026-01-09
|
||||
|
||||
### Added
|
||||
|
||||
- Added retry logic with exponential backoff for auto-compaction failures
|
||||
@@ -1908,15 +1953,14 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
- **Credential storage refactored**: API keys and OAuth tokens are now stored in `~/.omp/agent/auth.json` instead of `oauth.json` and `settings.json`. Existing credentials are automatically migrated on first run. ([#296](https://github.com/badlogic/pi-mono/issues/296))
|
||||
|
||||
- **SDK API changes** ([#296](https://github.com/badlogic/pi-mono/issues/296)):
|
||||
|
||||
- Added `AuthStorage` class for credential management (API keys and OAuth tokens)
|
||||
- Added `ModelRegistry` class for model discovery and API key resolution
|
||||
- Added `discoverAuthStorage()` and `discoverModels()` discovery functions
|
||||
- `createAgentSession()` now accepts `authStorage` and `modelRegistry` options
|
||||
- Removed `configureOAuthStorage()`, `defaultGetApiKey()`, `findModel()`, `discoverAvailableModels()`
|
||||
- Removed `getApiKey` callback option (use `AuthStorage.setRuntimeApiKey()` for runtime overrides)
|
||||
- Use `getModel()` from `@oh-my-pi/pi-ai` for built-in models, `modelRegistry.find()` for custom models + built-in models
|
||||
- See updated [SDK documentation](docs/sdk.md) and [README](README.md)
|
||||
- Added `AuthStorage` class for credential management (API keys and OAuth tokens)
|
||||
- Added `ModelRegistry` class for model discovery and API key resolution
|
||||
- Added `discoverAuthStorage()` and `discoverModels()` discovery functions
|
||||
- `createAgentSession()` now accepts `authStorage` and `modelRegistry` options
|
||||
- Removed `configureOAuthStorage()`, `defaultGetApiKey()`, `findModel()`, `discoverAvailableModels()`
|
||||
- Removed `getApiKey` callback option (use `AuthStorage.setRuntimeApiKey()` for runtime overrides)
|
||||
- Use `getModel()` from `@oh-my-pi/pi-ai` for built-in models, `modelRegistry.find()` for custom models + built-in models
|
||||
- See updated [SDK documentation](docs/sdk.md) and [README](README.md)
|
||||
|
||||
- **Settings changes**: Removed `apiKeys` from `settings.json`. Use `auth.json` instead. ([#296](https://github.com/badlogic/pi-mono/issues/296))
|
||||
|
||||
@@ -1947,16 +1991,15 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
### Added
|
||||
|
||||
- **Compaction hook improvements**: The `before_compact` session event now includes:
|
||||
|
||||
- `previousSummary`: Summary from the last compaction (if any), so hooks can preserve accumulated context
|
||||
- `messagesToKeep`: Messages that will be kept after the summary (recent turns), in addition to `messagesToSummarize`
|
||||
- `resolveApiKey`: Function to resolve API keys for any model (checks settings, OAuth, env vars)
|
||||
- Removed `apiKey` string in favor of `resolveApiKey` for more flexibility
|
||||
- `previousSummary`: Summary from the last compaction (if any), so hooks can preserve accumulated context
|
||||
- `messagesToKeep`: Messages that will be kept after the summary (recent turns), in addition to `messagesToSummarize`
|
||||
- `resolveApiKey`: Function to resolve API keys for any model (checks settings, OAuth, env vars)
|
||||
- Removed `apiKey` string in favor of `resolveApiKey` for more flexibility
|
||||
|
||||
- **SessionManager API cleanup**:
|
||||
- Renamed `loadSessionFromEntries()` to `buildSessionContext()` (builds LLM context from entries, handling compaction)
|
||||
- Renamed `loadEntries()` to `getEntries()` (returns defensive copy of all session entries)
|
||||
- Added `buildSessionContext()` method to SessionManager
|
||||
- Renamed `loadSessionFromEntries()` to `buildSessionContext()` (builds LLM context from entries, handling compaction)
|
||||
- Renamed `loadEntries()` to `getEntries()` (returns defensive copy of all session entries)
|
||||
- Added `buildSessionContext()` method to SessionManager
|
||||
|
||||
## [0.27.5] - 2025-12-24
|
||||
|
||||
@@ -2175,13 +2218,13 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
- **Custom tools now require `index.ts` entry point**: Auto-discovered custom tools must be in a subdirectory with an `index.ts` file. The old pattern `~/.omp/agent/tools/mytool.ts` must become `~/.omp/agent/tools/mytool/index.ts`. This allows multi-file tools to import helper modules. Explicit paths via `--tool` or `settings.json` still work with any `.ts` file.
|
||||
|
||||
- **Hook `tool_result` event restructured**: The `ToolResultEvent` now exposes full tool result data instead of just text. ([#233](https://github.com/badlogic/pi-mono/pull/233))
|
||||
- Removed: `result: string` field
|
||||
- Added: `content: (TextContent | ImageContent)[]` - full content array
|
||||
- Added: `details: unknown` - tool-specific details (typed per tool via discriminated union on `toolName`)
|
||||
- `ToolResultEventResult.result` renamed to `ToolResultEventResult.text` (removed), use `content` instead
|
||||
- Hook handlers returning `{ result: "..." }` must change to `{ content: [{ type: "text", text: "..." }] }`
|
||||
- Built-in tool details types exported: `BashToolDetails`, `ReadToolDetails`, `GrepToolDetails`, `FindToolDetails`, `LsToolDetails`, `TruncationResult`
|
||||
- Type guards exported for narrowing: `isBashToolResult`, `isReadToolResult`, `isEditToolResult`, `isWriteToolResult`, `isGrepToolResult`, `isFindToolResult`, `isLsToolResult`
|
||||
- Removed: `result: string` field
|
||||
- Added: `content: (TextContent | ImageContent)[]` - full content array
|
||||
- Added: `details: unknown` - tool-specific details (typed per tool via discriminated union on `toolName`)
|
||||
- `ToolResultEventResult.result` renamed to `ToolResultEventResult.text` (removed), use `content` instead
|
||||
- Hook handlers returning `{ result: "..." }` must change to `{ content: [{ type: "text", text: "..." }] }`
|
||||
- Built-in tool details types exported: `BashToolDetails`, `ReadToolDetails`, `GrepToolDetails`, `FindToolDetails`, `LsToolDetails`, `TruncationResult`
|
||||
- Type guards exported for narrowing: `isBashToolResult`, `isReadToolResult`, `isEditToolResult`, `isWriteToolResult`, `isGrepToolResult`, `isFindToolResult`, `isLsToolResult`
|
||||
|
||||
## [0.23.4] - 2025-12-18
|
||||
|
||||
@@ -2210,13 +2253,12 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo
|
||||
- Improved system prompt documentation section with clearer pointers to specific doc files for custom models, themes, skills, hooks, custom tools, and RPC.
|
||||
|
||||
- Cleaned up documentation:
|
||||
|
||||
- `theme.md`: Added missing color tokens (`thinkingXhigh`, `bashMode`)
|
||||
- `skills.md`: Rewrote with better framing and examples
|
||||
- `hooks.md`: Fixed timeout/error handling docs, added import aliases section
|
||||
- `custom-tools.md`: Added intro with use cases and comparison table
|
||||
- `rpc.md`: Added missing `hook_error` event documentation
|
||||
- `README.md`: Complete settings table, condensed philosophy section, standardized OAuth docs
|
||||
- `theme.md`: Added missing color tokens (`thinkingXhigh`, `bashMode`)
|
||||
- `skills.md`: Rewrote with better framing and examples
|
||||
- `hooks.md`: Fixed timeout/error handling docs, added import aliases section
|
||||
- `custom-tools.md`: Added intro with use cases and comparison table
|
||||
- `rpc.md`: Added missing `hook_error` event documentation
|
||||
- `README.md`: Complete settings table, condensed philosophy section, standardized OAuth docs
|
||||
|
||||
- Hooks loader now supports same import aliases as custom tools (`@sinclair/typebox`, `@oh-my-pi/pi-ai`, `@oh-my-pi/pi-tui`, `@oh-my-pi/pi-coding-agent`).
|
||||
|
||||
@@ -2473,12 +2515,12 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
||||
### Changed
|
||||
|
||||
- **Tool output truncation**: All tools now enforce consistent truncation limits with actionable notices for the LLM. ([#134](https://github.com/badlogic/pi-mono/issues/134))
|
||||
- **Limits**: 2000 lines OR 50KB (whichever hits first), never partial lines
|
||||
- **read**: Shows `[Showing lines X-Y of Z. Use offset=N to continue]`. If first line exceeds 50KB, suggests bash command
|
||||
- **bash**: Tail truncation with temp file. Shows `[Showing lines X-Y of Z. Full output: /tmp/...]`
|
||||
- **grep**: Pre-truncates match lines to 500 chars. Shows match limit and line truncation notices
|
||||
- **find/ls**: Shows result/entry limit notices
|
||||
- TUI displays truncation warnings in yellow at bottom of tool output (visible even when collapsed)
|
||||
- **Limits**: 2000 lines OR 50KB (whichever hits first), never partial lines
|
||||
- **read**: Shows `[Showing lines X-Y of Z. Use offset=N to continue]`. If first line exceeds 50KB, suggests bash command
|
||||
- **bash**: Tail truncation with temp file. Shows `[Showing lines X-Y of Z. Full output: /tmp/...]`
|
||||
- **grep**: Pre-truncates match lines to 500 chars. Shows match limit and line truncation notices
|
||||
- **find/ls**: Shows result/entry limit notices
|
||||
- TUI displays truncation warnings in yellow at bottom of tool output (visible even when collapsed)
|
||||
|
||||
## [0.13.1] - 2025-12-06
|
||||
|
||||
@@ -2565,13 +2607,13 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
||||
### Added
|
||||
|
||||
- **Context Compaction**: Long sessions can now be compacted to reduce context usage while preserving recent conversation history. ([#92](https://github.com/badlogic/pi-mono/issues/92), [docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md#context-compaction))
|
||||
- `/compact [instructions]`: Manually compact context with optional custom instructions for the summary
|
||||
- `/autocompact`: Toggle automatic compaction when context exceeds threshold
|
||||
- Compaction summarizes older messages while keeping recent messages (default 20k tokens) verbatim
|
||||
- Auto-compaction triggers when context reaches `contextWindow - reserveTokens` (default 16k reserve)
|
||||
- Compacted sessions show a collapsible summary in the TUI (toggle with `o` key)
|
||||
- HTML exports include compaction summaries as collapsible sections
|
||||
- RPC mode supports `{"type":"compact"}` command and auto-compaction (emits compaction events)
|
||||
- `/compact [instructions]`: Manually compact context with optional custom instructions for the summary
|
||||
- `/autocompact`: Toggle automatic compaction when context exceeds threshold
|
||||
- Compaction summarizes older messages while keeping recent messages (default 20k tokens) verbatim
|
||||
- Auto-compaction triggers when context reaches `contextWindow - reserveTokens` (default 16k reserve)
|
||||
- Compacted sessions show a collapsible summary in the TUI (toggle with `o` key)
|
||||
- HTML exports include compaction summaries as collapsible sections
|
||||
- RPC mode supports `{"type":"compact"}` command and auto-compaction (emits compaction events)
|
||||
- **Branch Source Tracking**: Branched sessions now store `branchedFrom` in the session header, containing the path to the original session file. Useful for tracing session lineage.
|
||||
|
||||
## [0.12.5] - 2025-12-03
|
||||
@@ -2608,11 +2650,11 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
||||
### Added
|
||||
|
||||
- **Models**: Added support for OpenAI's new models:
|
||||
- `gpt-4.1` (128K context)
|
||||
- `gpt-4.1-mini` (128K context)
|
||||
- `gpt-4.1-nano` (128K context)
|
||||
- `o3` (200K context, reasoning model)
|
||||
- `o4-mini` (200K context, reasoning model)
|
||||
- `gpt-4.1` (128K context)
|
||||
- `gpt-4.1-mini` (128K context)
|
||||
- `gpt-4.1-nano` (128K context)
|
||||
- `o3` (200K context, reasoning model)
|
||||
- `o4-mini` (200K context, reasoning model)
|
||||
|
||||
## [0.12.0] - 2025-12-02
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
// Discover all skills from cwd/.omp/skills, ~/.omp/agent/skills, etc.
|
||||
const allSkills = discoverSkills();
|
||||
const { skills: allSkills } = await discoverSkills();
|
||||
console.log(
|
||||
"Discovered skills:",
|
||||
allSkills.map((s) => s.name),
|
||||
|
||||
@@ -801,6 +801,112 @@ export class AuthStorage {
|
||||
};
|
||||
}
|
||||
|
||||
private getUsageReportMetadataValue(report: UsageReport, key: string): string | undefined {
|
||||
const metadata = report.metadata;
|
||||
if (!metadata || typeof metadata !== "object") return undefined;
|
||||
const value = metadata[key];
|
||||
return typeof value === "string" ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
private getUsageReportScopeAccountId(report: UsageReport): string | undefined {
|
||||
const ids = new Set<string>();
|
||||
for (const limit of report.limits) {
|
||||
const accountId = limit.scope.accountId?.trim();
|
||||
if (accountId) ids.add(accountId);
|
||||
}
|
||||
if (ids.size === 1) return [...ids][0];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getUsageReportIdentifiers(report: UsageReport): string[] {
|
||||
const identifiers: string[] = [];
|
||||
const email = this.getUsageReportMetadataValue(report, "email");
|
||||
if (email) identifiers.push(`email:${email.toLowerCase()}`);
|
||||
const accountId = this.getUsageReportMetadataValue(report, "accountId");
|
||||
if (accountId) identifiers.push(`account:${accountId}`);
|
||||
const account = this.getUsageReportMetadataValue(report, "account");
|
||||
if (account) identifiers.push(`account:${account}`);
|
||||
const user = this.getUsageReportMetadataValue(report, "user");
|
||||
if (user) identifiers.push(`account:${user}`);
|
||||
const username = this.getUsageReportMetadataValue(report, "username");
|
||||
if (username) identifiers.push(`account:${username}`);
|
||||
const scopeAccountId = this.getUsageReportScopeAccountId(report);
|
||||
if (scopeAccountId) identifiers.push(`account:${scopeAccountId}`);
|
||||
return identifiers.map((identifier) => `${report.provider}:${identifier.toLowerCase()}`);
|
||||
}
|
||||
|
||||
private mergeUsageReportGroup(reports: UsageReport[]): UsageReport {
|
||||
if (reports.length === 1) return reports[0];
|
||||
const sorted = [...reports].sort((a, b) => {
|
||||
const limitDiff = b.limits.length - a.limits.length;
|
||||
if (limitDiff !== 0) return limitDiff;
|
||||
return (b.fetchedAt ?? 0) - (a.fetchedAt ?? 0);
|
||||
});
|
||||
const base = sorted[0];
|
||||
const mergedLimits = [...base.limits];
|
||||
const limitIds = new Set(mergedLimits.map((limit) => limit.id));
|
||||
const mergedMetadata: Record<string, unknown> = { ...(base.metadata ?? {}) };
|
||||
let fetchedAt = base.fetchedAt;
|
||||
|
||||
for (const report of sorted.slice(1)) {
|
||||
fetchedAt = Math.max(fetchedAt, report.fetchedAt);
|
||||
for (const limit of report.limits) {
|
||||
if (!limitIds.has(limit.id)) {
|
||||
limitIds.add(limit.id);
|
||||
mergedLimits.push(limit);
|
||||
}
|
||||
}
|
||||
if (report.metadata) {
|
||||
for (const [key, value] of Object.entries(report.metadata)) {
|
||||
if (mergedMetadata[key] === undefined) {
|
||||
mergedMetadata[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
fetchedAt,
|
||||
limits: mergedLimits,
|
||||
metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private dedupeUsageReports(reports: UsageReport[]): UsageReport[] {
|
||||
const groups: UsageReport[][] = [];
|
||||
const idToGroup = new Map<string, number>();
|
||||
|
||||
for (const report of reports) {
|
||||
const identifiers = this.getUsageReportIdentifiers(report);
|
||||
let groupIndex: number | undefined;
|
||||
for (const identifier of identifiers) {
|
||||
const existing = idToGroup.get(identifier);
|
||||
if (existing !== undefined) {
|
||||
groupIndex = existing;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (groupIndex === undefined) {
|
||||
groupIndex = groups.length;
|
||||
groups.push([]);
|
||||
}
|
||||
groups[groupIndex].push(report);
|
||||
for (const identifier of identifiers) {
|
||||
idToGroup.set(identifier, groupIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const deduped = groups.map((group) => this.mergeUsageReportGroup(group));
|
||||
if (deduped.length !== reports.length) {
|
||||
this.usageLogger?.debug("Usage reports deduped", {
|
||||
before: reports.length,
|
||||
after: deduped.length,
|
||||
});
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
private isUsageLimitExhausted(limit: UsageLimit): boolean {
|
||||
if (limit.status === "exhausted") return true;
|
||||
const amount = limit.amount;
|
||||
@@ -883,6 +989,9 @@ export class AuthStorage {
|
||||
...this.data.keys(),
|
||||
...DEFAULT_USAGE_PROVIDERS.map((provider) => provider.id),
|
||||
]);
|
||||
this.usageLogger?.debug("Usage fetch requested", {
|
||||
providers: Array.from(providers).sort(),
|
||||
});
|
||||
for (const provider of providers) {
|
||||
const providerImpl = resolver(provider as Provider);
|
||||
if (!providerImpl) continue;
|
||||
@@ -911,6 +1020,11 @@ export class AuthStorage {
|
||||
if (providerImpl.supports && !providerImpl.supports(params)) {
|
||||
continue;
|
||||
}
|
||||
this.usageLogger?.debug("Usage fetch queued", {
|
||||
provider,
|
||||
credentialType: "api_key",
|
||||
baseUrl,
|
||||
});
|
||||
tasks.push(
|
||||
providerImpl
|
||||
.fetchUsage(params, {
|
||||
@@ -946,6 +1060,14 @@ export class AuthStorage {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.usageLogger?.debug("Usage fetch queued", {
|
||||
provider,
|
||||
credentialType: usageCredential.type,
|
||||
baseUrl,
|
||||
accountId: usageCredential.accountId,
|
||||
email: usageCredential.email,
|
||||
});
|
||||
|
||||
tasks.push(
|
||||
providerImpl
|
||||
.fetchUsage(params, {
|
||||
@@ -967,7 +1089,25 @@ export class AuthStorage {
|
||||
|
||||
if (tasks.length === 0) return [];
|
||||
const results = await Promise.all(tasks);
|
||||
return results.filter((report): report is UsageReport => report !== null);
|
||||
const reports = results.filter((report): report is UsageReport => report !== null);
|
||||
const deduped = this.dedupeUsageReports(reports);
|
||||
this.usageLogger?.debug("Usage fetch resolved", {
|
||||
reports: deduped.map((report) => {
|
||||
const accountLabel =
|
||||
this.getUsageReportMetadataValue(report, "email") ??
|
||||
this.getUsageReportMetadataValue(report, "accountId") ??
|
||||
this.getUsageReportMetadataValue(report, "account") ??
|
||||
this.getUsageReportMetadataValue(report, "user") ??
|
||||
this.getUsageReportMetadataValue(report, "username") ??
|
||||
this.getUsageReportScopeAccountId(report);
|
||||
return {
|
||||
provider: report.provider,
|
||||
limits: report.limits.length,
|
||||
account: accountLabel,
|
||||
};
|
||||
}),
|
||||
});
|
||||
return deduped;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1093,7 +1233,16 @@ export class AuthStorage {
|
||||
const result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);
|
||||
if (!result) return undefined;
|
||||
|
||||
const updated: OAuthCredential = { type: "oauth", ...result.newCredentials };
|
||||
const updated: OAuthCredential = {
|
||||
type: "oauth",
|
||||
access: result.newCredentials.access,
|
||||
refresh: result.newCredentials.refresh,
|
||||
expires: result.newCredentials.expires,
|
||||
accountId: result.newCredentials.accountId ?? selection.credential.accountId,
|
||||
email: result.newCredentials.email ?? selection.credential.email,
|
||||
projectId: result.newCredentials.projectId ?? selection.credential.projectId,
|
||||
enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
|
||||
};
|
||||
this.replaceCredentialAt(provider, selection.index, updated);
|
||||
|
||||
if (checkUsage) {
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
||||
cancelled: false,
|
||||
...(await sink.dump()),
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
// Exception covers NonZeroExitError, AbortError, TimeoutError
|
||||
if (err instanceof Exception) {
|
||||
if (err.aborted) {
|
||||
|
||||
@@ -24,14 +24,6 @@ if "__omp_prelude_loaded__" not in globals():
|
||||
_emit_status("pwd", path=str(p))
|
||||
return p
|
||||
|
||||
@_category("Navigation")
|
||||
def cd(path: str | Path) -> Path:
|
||||
"""Change directory."""
|
||||
p = Path(path).expanduser().resolve()
|
||||
os.chdir(p)
|
||||
_emit_status("cd", path=str(p))
|
||||
return p
|
||||
|
||||
@_category("Shell")
|
||||
def env(key: str | None = None, value: str | None = None):
|
||||
"""Get/set environment variables."""
|
||||
|
||||
@@ -684,6 +684,13 @@ function resolveFraction(limit: UsageLimit): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveProviderUsageTotal(reports: UsageReport[]): number {
|
||||
return reports
|
||||
.flatMap((report) => report.limits)
|
||||
.map((limit) => resolveFraction(limit) ?? 0)
|
||||
.reduce((sum, value) => sum + value, 0);
|
||||
}
|
||||
|
||||
function formatLimitTitle(limit: UsageLimit): string {
|
||||
const tier = limit.scope.tier;
|
||||
if (tier && !limit.label.toLowerCase().includes(tier.toLowerCase())) {
|
||||
@@ -841,8 +848,18 @@ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs
|
||||
list.push(report);
|
||||
grouped.set(report.provider, list);
|
||||
}
|
||||
const providerEntries = Array.from(grouped.entries())
|
||||
.map(([provider, providerReports]) => ({
|
||||
provider,
|
||||
providerReports,
|
||||
totalUsage: resolveProviderUsageTotal(providerReports),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.totalUsage !== b.totalUsage) return a.totalUsage - b.totalUsage;
|
||||
return a.provider.localeCompare(b.provider);
|
||||
});
|
||||
|
||||
for (const [provider, providerReports] of grouped.entries()) {
|
||||
for (const { provider, providerReports } of providerEntries) {
|
||||
lines.push("");
|
||||
const providerName = formatProviderName(provider);
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ cols(read("data.tsv"), 0, 2, sep="\t")
|
||||
|
||||
- Code executes as IPython cells; users see the full cell output (including rendered figures, tables, etc.)
|
||||
- Kernel persists for the session by default; per-call mode uses a fresh kernel each call. Use `reset: true` to clear state when session mode is active
|
||||
- Use `cwd` parameter instead of `os.chdir()` in tool call
|
||||
- Use `plt.show()` to display figures
|
||||
- Use `display()` from IPython.display for rich output (HTML, Markdown, images, etc.)
|
||||
- Output streams in real time, truncated after 50KB
|
||||
|
||||
+17
-17
@@ -11,27 +11,27 @@ const THEMES_DIR = join(process.cwd(), "packages/coding-agent/src/modes/interact
|
||||
const INDEX_FILE = join(THEMES_DIR, "index.ts");
|
||||
|
||||
async function main() {
|
||||
const files = await readdir(THEMES_DIR);
|
||||
const jsonFiles = files.filter(f => f.endsWith(".json")).sort();
|
||||
const files = await readdir(THEMES_DIR);
|
||||
const jsonFiles = files.filter((f) => f.endsWith(".json")).sort();
|
||||
|
||||
const imports: string[] = [];
|
||||
const exportEntries: string[] = [];
|
||||
const imports: string[] = [];
|
||||
const exportEntries: string[] = [];
|
||||
|
||||
for (const file of jsonFiles) {
|
||||
const name = file.replace(".json", "");
|
||||
const varName = name.replace(/-/g, "_");
|
||||
|
||||
imports.push(`import ${varName} from "./${file}" with { type: "json" };`);
|
||||
exportEntries.push(` "${name}": ${varName},`);
|
||||
}
|
||||
for (const file of jsonFiles) {
|
||||
const name = file.replace(".json", "");
|
||||
const varName = name.replace(/-/g, "_");
|
||||
|
||||
let content = imports.join("\n");
|
||||
content += "\n\nexport const defaultThemes = {\n";
|
||||
content += exportEntries.join("\n");
|
||||
content += "\n};\n";
|
||||
imports.push(`import ${varName} from "./${file}" with { type: "json" };`);
|
||||
exportEntries.push(` "${name}": ${varName},`);
|
||||
}
|
||||
|
||||
await Bun.write(INDEX_FILE, content);
|
||||
console.log(`Updated ${INDEX_FILE} with ${jsonFiles.length} themes.`);
|
||||
let content = imports.join("\n");
|
||||
content += "\n\nexport const defaultThemes = {\n";
|
||||
content += exportEntries.join("\n");
|
||||
content += "\n};\n";
|
||||
|
||||
await Bun.write(INDEX_FILE, content);
|
||||
console.log(`Updated ${INDEX_FILE} with ${jsonFiles.length} themes.`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user