@ai-devkit/agent-manager 0.34.0 → 0.36.0

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.
Files changed (211) hide show
  1. package/dist/AgentManager.d.ts +2 -0
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +25 -0
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +74 -0
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js +381 -0
  8. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js.map +1 -0
  9. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +28 -0
  10. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  11. package/dist/__tests__/adapters/CodexAdapter.test.js +7 -0
  12. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  13. package/dist/__tests__/adapters/KiroAdapter.test.js +607 -0
  14. package/dist/__tests__/adapters/KiroAdapter.test.js.map +1 -0
  15. package/dist/__tests__/capacity/claude.test.js +485 -0
  16. package/dist/__tests__/capacity/claude.test.js.map +1 -0
  17. package/dist/__tests__/capacity/openai.test.js +672 -0
  18. package/dist/__tests__/capacity/openai.test.js.map +1 -0
  19. package/dist/__tests__/capacity/wham.test.js +144 -0
  20. package/dist/__tests__/capacity/wham.test.js.map +1 -0
  21. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js +18 -0
  22. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -1
  23. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js +36 -0
  24. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -1
  25. package/dist/__tests__/providers/grok/GrokAgentMapper.test.js +56 -0
  26. package/dist/__tests__/providers/grok/GrokAgentMapper.test.js.map +1 -0
  27. package/dist/__tests__/{adapters → providers/grok}/GrokCliAdapter.test.js +19 -6
  28. package/dist/__tests__/providers/grok/GrokCliAdapter.test.js.map +1 -0
  29. package/dist/__tests__/providers/grok/GrokSessionLocator.test.js +133 -0
  30. package/dist/__tests__/providers/grok/GrokSessionLocator.test.js.map +1 -0
  31. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +24 -0
  32. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -1
  33. package/dist/__tests__/providers/pi/PiAdapter.test.js +30 -0
  34. package/dist/__tests__/providers/pi/PiAdapter.test.js.map +1 -1
  35. package/dist/__tests__/utils/agents.test.js +6 -0
  36. package/dist/__tests__/utils/agents.test.js.map +1 -1
  37. package/dist/__tests__/utils/session.test.js +18 -1
  38. package/dist/__tests__/utils/session.test.js.map +1 -1
  39. package/dist/adapters/AgentAdapter.d.ts +9 -1
  40. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  41. package/dist/adapters/AgentAdapter.js.map +1 -1
  42. package/dist/adapters/AntigravityCliAdapter.d.ts +57 -0
  43. package/dist/adapters/AntigravityCliAdapter.d.ts.map +1 -0
  44. package/dist/adapters/AntigravityCliAdapter.js +273 -0
  45. package/dist/adapters/AntigravityCliAdapter.js.map +1 -0
  46. package/dist/adapters/KiroAdapter.d.ts +46 -0
  47. package/dist/adapters/KiroAdapter.d.ts.map +1 -0
  48. package/dist/adapters/KiroAdapter.js +344 -0
  49. package/dist/adapters/KiroAdapter.js.map +1 -0
  50. package/dist/adapters/index.d.ts +3 -1
  51. package/dist/adapters/index.d.ts.map +1 -1
  52. package/dist/adapters/index.js +3 -1
  53. package/dist/adapters/index.js.map +1 -1
  54. package/dist/capacity/claude.d.ts +17 -0
  55. package/dist/capacity/claude.d.ts.map +1 -0
  56. package/dist/capacity/claude.js +271 -0
  57. package/dist/capacity/claude.js.map +1 -0
  58. package/dist/capacity/codex.d.ts +1 -2
  59. package/dist/capacity/codex.d.ts.map +1 -1
  60. package/dist/capacity/codex.js +5 -47
  61. package/dist/capacity/codex.js.map +1 -1
  62. package/dist/capacity/index.d.ts +11 -0
  63. package/dist/capacity/index.d.ts.map +1 -1
  64. package/dist/capacity/index.js +17 -0
  65. package/dist/capacity/index.js.map +1 -1
  66. package/dist/capacity/openai.d.ts +48 -0
  67. package/dist/capacity/openai.d.ts.map +1 -0
  68. package/dist/capacity/openai.js +312 -0
  69. package/dist/capacity/openai.js.map +1 -0
  70. package/dist/capacity/wham.d.ts +16 -0
  71. package/dist/capacity/wham.d.ts.map +1 -0
  72. package/dist/capacity/wham.js +66 -0
  73. package/dist/capacity/wham.js.map +1 -0
  74. package/dist/index.d.ts +5 -3
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +4 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/providers/claude/ClaudeCodeAdapter.d.ts +2 -0
  79. package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -1
  80. package/dist/providers/claude/ClaudeCodeAdapter.js +22 -19
  81. package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -1
  82. package/dist/providers/claude/ClaudeSessionLocator.d.ts +4 -0
  83. package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -1
  84. package/dist/providers/claude/ClaudeSessionLocator.js +18 -1
  85. package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -1
  86. package/dist/providers/codex/CodexAdapter.d.ts +1 -0
  87. package/dist/providers/codex/CodexAdapter.d.ts.map +1 -1
  88. package/dist/providers/codex/CodexAdapter.js +8 -0
  89. package/dist/providers/codex/CodexAdapter.js.map +1 -1
  90. package/dist/providers/copilot/CopilotAdapter.d.ts +2 -0
  91. package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -1
  92. package/dist/providers/copilot/CopilotAdapter.js +24 -9
  93. package/dist/providers/copilot/CopilotAdapter.js.map +1 -1
  94. package/dist/providers/copilot/CopilotSessionLocator.d.ts +1 -0
  95. package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -1
  96. package/dist/providers/copilot/CopilotSessionLocator.js +9 -1
  97. package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -1
  98. package/dist/providers/gemini/GeminiCliAdapter.d.ts +1 -0
  99. package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -1
  100. package/dist/providers/gemini/GeminiCliAdapter.js +9 -0
  101. package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -1
  102. package/dist/providers/grok/GrokAgentMapper.d.ts +14 -0
  103. package/dist/providers/grok/GrokAgentMapper.d.ts.map +1 -0
  104. package/dist/providers/grok/GrokAgentMapper.js +36 -0
  105. package/dist/providers/grok/GrokAgentMapper.js.map +1 -0
  106. package/dist/providers/grok/GrokCliAdapter.d.ts +36 -0
  107. package/dist/providers/grok/GrokCliAdapter.d.ts.map +1 -0
  108. package/dist/providers/grok/GrokCliAdapter.js +98 -0
  109. package/dist/providers/grok/GrokCliAdapter.js.map +1 -0
  110. package/dist/providers/grok/GrokSessionLocator.d.ts +62 -0
  111. package/dist/providers/grok/GrokSessionLocator.d.ts.map +1 -0
  112. package/dist/providers/grok/GrokSessionLocator.js +140 -0
  113. package/dist/providers/grok/GrokSessionLocator.js.map +1 -0
  114. package/dist/providers/grok/GrokSessionParser.d.ts +54 -0
  115. package/dist/providers/grok/GrokSessionParser.d.ts.map +1 -0
  116. package/dist/providers/grok/GrokSessionParser.js +122 -0
  117. package/dist/providers/grok/GrokSessionParser.js.map +1 -0
  118. package/dist/providers/opencode/OpenCodeAdapter.d.ts +1 -0
  119. package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -1
  120. package/dist/providers/opencode/OpenCodeAdapter.js +3 -0
  121. package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -1
  122. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +2 -0
  123. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -1
  124. package/dist/providers/opencode/OpenCodeSessionLocator.js +31 -12
  125. package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -1
  126. package/dist/providers/pi/PiAdapter.d.ts +1 -0
  127. package/dist/providers/pi/PiAdapter.d.ts.map +1 -1
  128. package/dist/providers/pi/PiAdapter.js +10 -0
  129. package/dist/providers/pi/PiAdapter.js.map +1 -1
  130. package/dist/providers/pi/PiSessionLocator.d.ts +1 -0
  131. package/dist/providers/pi/PiSessionLocator.d.ts.map +1 -1
  132. package/dist/providers/pi/PiSessionLocator.js +8 -1
  133. package/dist/providers/pi/PiSessionLocator.js.map +1 -1
  134. package/dist/readiness/AgentReadiness.d.ts +1 -1
  135. package/dist/readiness/AgentReadiness.d.ts.map +1 -1
  136. package/dist/readiness/AgentReadiness.js.map +1 -1
  137. package/dist/runtime/ManagedAgentRuntime.js +3 -1
  138. package/dist/runtime/ManagedAgentRuntime.js.map +1 -1
  139. package/dist/runtime/herdr/HerdrAgentDiscovery.js +10 -0
  140. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -1
  141. package/dist/utils/agents.d.ts +1 -1
  142. package/dist/utils/agents.d.ts.map +1 -1
  143. package/dist/utils/agents.js +11 -0
  144. package/dist/utils/agents.js.map +1 -1
  145. package/dist/utils/session.d.ts +2 -0
  146. package/dist/utils/session.d.ts.map +1 -1
  147. package/dist/utils/session.js +3 -0
  148. package/dist/utils/session.js.map +1 -1
  149. package/package.json +1 -1
  150. package/src/AgentManager.ts +34 -0
  151. package/src/__tests__/AgentManager.test.ts +60 -0
  152. package/src/__tests__/adapters/AntigravityCliAdapter.test.ts +324 -0
  153. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +23 -0
  154. package/src/__tests__/adapters/CodexAdapter.test.ts +7 -0
  155. package/src/__tests__/adapters/KiroAdapter.test.ts +534 -0
  156. package/src/__tests__/capacity/claude.test.ts +467 -0
  157. package/src/__tests__/capacity/fixtures/claude-credentials.json +7 -0
  158. package/src/__tests__/capacity/fixtures/claude-usage-malformed.json +1 -0
  159. package/src/__tests__/capacity/fixtures/claude-usage-partial.json +15 -0
  160. package/src/__tests__/capacity/fixtures/claude-usage.json +63 -0
  161. package/src/__tests__/capacity/fixtures/openai-auth.json +6 -0
  162. package/src/__tests__/capacity/fixtures/openai-codex-auth.json +9 -0
  163. package/src/__tests__/capacity/fixtures/openai-usage-completions.json +67 -0
  164. package/src/__tests__/capacity/fixtures/openai-usage-responses.json +20 -0
  165. package/src/__tests__/capacity/fixtures/wham-usage.json +32 -0
  166. package/src/__tests__/capacity/openai.test.ts +570 -0
  167. package/src/__tests__/capacity/wham.test.ts +104 -0
  168. package/src/__tests__/providers/copilot/CopilotAdapter.test.ts +12 -0
  169. package/src/__tests__/providers/gemini/GeminiCliAdapter.test.ts +21 -0
  170. package/src/__tests__/providers/grok/GrokAgentMapper.test.ts +57 -0
  171. package/src/__tests__/{adapters → providers/grok}/GrokCliAdapter.test.ts +18 -9
  172. package/src/__tests__/providers/grok/GrokSessionLocator.test.ts +106 -0
  173. package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +18 -0
  174. package/src/__tests__/providers/pi/PiAdapter.test.ts +15 -0
  175. package/src/__tests__/utils/agents.test.ts +7 -0
  176. package/src/__tests__/utils/session.test.ts +11 -1
  177. package/src/adapters/AgentAdapter.ts +11 -0
  178. package/src/adapters/AntigravityCliAdapter.ts +356 -0
  179. package/src/adapters/KiroAdapter.ts +456 -0
  180. package/src/adapters/index.ts +3 -1
  181. package/src/capacity/claude.ts +313 -0
  182. package/src/capacity/codex.ts +25 -134
  183. package/src/capacity/index.ts +33 -4
  184. package/src/capacity/openai.ts +361 -0
  185. package/src/capacity/wham.ts +84 -0
  186. package/src/index.ts +11 -2
  187. package/src/providers/claude/ClaudeCodeAdapter.ts +28 -23
  188. package/src/providers/claude/ClaudeSessionLocator.ts +20 -0
  189. package/src/providers/codex/CodexAdapter.ts +8 -0
  190. package/src/providers/copilot/CopilotAdapter.ts +26 -10
  191. package/src/providers/copilot/CopilotSessionLocator.ts +7 -1
  192. package/src/providers/gemini/GeminiCliAdapter.ts +8 -0
  193. package/src/providers/grok/GrokAgentMapper.ts +41 -0
  194. package/src/providers/grok/GrokCliAdapter.ts +125 -0
  195. package/src/providers/grok/GrokSessionLocator.ts +179 -0
  196. package/src/providers/grok/GrokSessionParser.ts +166 -0
  197. package/src/providers/opencode/OpenCodeAdapter.ts +4 -0
  198. package/src/providers/opencode/OpenCodeSessionLocator.ts +43 -15
  199. package/src/providers/pi/PiAdapter.ts +12 -0
  200. package/src/providers/pi/PiSessionLocator.ts +16 -1
  201. package/src/readiness/AgentReadiness.ts +4 -1
  202. package/src/runtime/ManagedAgentRuntime.ts +2 -0
  203. package/src/runtime/herdr/HerdrAgentDiscovery.ts +5 -1
  204. package/src/utils/agents.ts +11 -1
  205. package/src/utils/session.ts +11 -0
  206. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +0 -1
  207. package/dist/adapters/GrokCliAdapter.d.ts +0 -80
  208. package/dist/adapters/GrokCliAdapter.d.ts.map +0 -1
  209. package/dist/adapters/GrokCliAdapter.js +0 -310
  210. package/dist/adapters/GrokCliAdapter.js.map +0 -1
  211. package/src/adapters/GrokCliAdapter.ts +0 -408
@@ -0,0 +1,361 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { parseWhamUsage } from "./wham.js";
5
+ import type { CapacityReport, CapacityWindow } from "./types.js";
6
+
7
+ const OPENAI_USAGE_BASE_URL = "https://api.openai.com/v1/organization/usage";
8
+ const OPENAI_MODELS_URL = "https://api.openai.com/v1/models";
9
+ const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
10
+ const OPENAI_USAGE_ENDPOINTS = ["completions", "responses"] as const;
11
+ const USAGE_WINDOW_DAYS = 7;
12
+ const DAY_MS = 24 * 60 * 60 * 1000;
13
+ const MISSING_API_KEY_MESSAGE =
14
+ "OpenAI credentials not found; set OPENAI_API_KEY, configure the Pi openai provider, or log in to OpenAI via Pi";
15
+ const EPOCH_MS_FLOOR = 1e12;
16
+
17
+ export type OpenAiUsageBucket = { startMs: number; tokens: number };
18
+ type UnknownRecord = Record<string, unknown>;
19
+
20
+ /**
21
+ * Resolved OpenAI credential: a platform API key (env or Pi `openai` entry)
22
+ * or the OAuth login Pi stores as `openai-codex` for ChatGPT-backed access.
23
+ */
24
+ export type OpenAiCredential =
25
+ | { kind: "platform"; key: string }
26
+ | { kind: "oauth"; access: string; accountId: string; expiresMs: number | null };
27
+
28
+ export type OpenAiCredentialOptions = {
29
+ env?: NodeJS.ProcessEnv;
30
+ readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
31
+ };
32
+
33
+ export type OpenAiProbeOptions = OpenAiCredentialOptions & {
34
+ checkedAt: string;
35
+ fetch?: typeof globalThis.fetch;
36
+ timeoutMs?: number;
37
+ };
38
+ type OpenAiRequestContext = OpenAiProbeOptions & { key: string };
39
+ type OAuthCredential = Extract<OpenAiCredential, { kind: "oauth" }>;
40
+
41
+ function record(value: unknown): UnknownRecord | null {
42
+ return value !== null && typeof value === "object" && !Array.isArray(value)
43
+ ? (value as UnknownRecord)
44
+ : null;
45
+ }
46
+
47
+ function nonEmptyText(value: unknown): string | null {
48
+ return typeof value === "string" && value.trim() ? value.trim() : null;
49
+ }
50
+
51
+ function finiteNumber(value: unknown): number | null {
52
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
53
+ }
54
+
55
+ /** Pi stores `expires` in epoch milliseconds, codex-style tokens use seconds. */
56
+ function toEpochMs(value: unknown): number | null {
57
+ const raw = finiteNumber(value);
58
+ if (raw === null || raw <= 0) return null;
59
+ return raw > EPOCH_MS_FLOOR ? raw : raw * 1000;
60
+ }
61
+
62
+ async function readPiAuthRoot(options: OpenAiCredentialOptions): Promise<UnknownRecord> {
63
+ const env = options.env ?? process.env;
64
+ const authPath = join(env.HOME || homedir(), ".pi", "agent", "auth.json");
65
+ let parsed: UnknownRecord;
66
+ try {
67
+ const contents = await (options.readFile ?? readFile)(authPath, "utf8");
68
+ const root = record(JSON.parse(contents));
69
+ if (!root) throw new Error("invalid");
70
+ parsed = root;
71
+ } catch (error) {
72
+ if (record(error)?.code === "ENOENT") {
73
+ throw new Error(MISSING_API_KEY_MESSAGE);
74
+ }
75
+ throw new Error("OpenAI Pi auth file is malformed");
76
+ }
77
+ return parsed;
78
+ }
79
+
80
+ /**
81
+ * Resolves the OpenAI credential in priority order: `OPENAI_API_KEY` env,
82
+ * then the Pi `openai` platform-key entry, then the Pi `openai-codex` OAuth
83
+ * login written by `pi login` (no manual setup required).
84
+ */
85
+ export async function resolveOpenAiCredential(
86
+ options: OpenAiCredentialOptions = {},
87
+ ): Promise<OpenAiCredential> {
88
+ const env = options.env ?? process.env;
89
+ const environmentKey = nonEmptyText(env.OPENAI_API_KEY);
90
+ if (environmentKey) return { kind: "platform", key: environmentKey };
91
+
92
+ const parsed = await readPiAuthRoot(options);
93
+
94
+ const platform = record(parsed.openai);
95
+ if (platform) {
96
+ const key = platform.type === "api_key" ? nonEmptyText(platform.key) : null;
97
+ if (!key) {
98
+ throw new Error("OpenAI Pi auth file has a malformed openai credential");
99
+ }
100
+ return { kind: "platform", key };
101
+ }
102
+
103
+ const oauth = record(parsed["openai-codex"]);
104
+ const access = oauth?.type === "oauth" ? nonEmptyText(oauth.access) : null;
105
+ const accountId = nonEmptyText(oauth?.accountId);
106
+ if (oauth && access && accountId) {
107
+ return { kind: "oauth", access, accountId, expiresMs: toEpochMs(oauth.expires) };
108
+ }
109
+ throw new Error(MISSING_API_KEY_MESSAGE);
110
+ }
111
+
112
+ export async function resolveOpenAiApiKey(options: OpenAiCredentialOptions = {}): Promise<string> {
113
+ const credential = await resolveOpenAiCredential(options);
114
+ if (credential.kind === "platform") return credential.key;
115
+ throw new Error(MISSING_API_KEY_MESSAGE);
116
+ }
117
+
118
+ function bucketTokens(value: unknown): number {
119
+ const results = Array.isArray(value) ? value : [];
120
+ return results.reduce(
121
+ (total, entry) =>
122
+ total +
123
+ (finiteNumber(record(entry)?.input_tokens) ?? 0) +
124
+ (finiteNumber(record(entry)?.output_tokens) ?? 0),
125
+ 0,
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Maps one OpenAI Usage API payload (day buckets of per-model token counts)
131
+ * into summed UTC-day buckets. Entries without a numeric
132
+ * aggregation_timestamp are skipped because they cannot be placed in time.
133
+ */
134
+ export function parseOpenAiUsage(raw: unknown): OpenAiUsageBucket[] {
135
+ const data = record(raw)?.data;
136
+ if (!Array.isArray(data)) throw new Error("Invalid OpenAI usage response");
137
+ const buckets: OpenAiUsageBucket[] = [];
138
+ for (const entry of data) {
139
+ const seconds = finiteNumber(record(entry)?.aggregation_timestamp);
140
+ if (seconds === null) continue;
141
+ buckets.push({ startMs: seconds * 1000, tokens: bucketTokens(record(entry)?.results) });
142
+ }
143
+ return buckets;
144
+ }
145
+
146
+ function openAiCapacityReport(
147
+ windows: CapacityWindow[],
148
+ checkedAt: string,
149
+ authenticated: boolean,
150
+ ): CapacityReport {
151
+ return {
152
+ harness: "pi",
153
+ provider: "openai",
154
+ generatedAt: checkedAt,
155
+ authenticated,
156
+ available: windows.length > 0 ? "yes" : "unknown",
157
+ windows,
158
+ creditsRemaining: null,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Builds the capacity report from merged day buckets: trailing-7-day and
164
+ * current-UTC-day token usage. The platform is pay-as-you-go and exposes no
165
+ * queryable limit, so windows report consumption only — no faked percentages.
166
+ */
167
+ export function buildOpenAiReport(buckets: OpenAiUsageBucket[], checkedAt: string): CapacityReport {
168
+ const nowMs = Date.parse(checkedAt);
169
+ const todayStartMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
170
+ const weekStartMs = todayStartMs - (USAGE_WINDOW_DAYS - 1) * DAY_MS;
171
+ let todayTokens = 0;
172
+ let weekTokens = 0;
173
+ for (const bucket of buckets) {
174
+ if (bucket.startMs === todayStartMs) todayTokens += bucket.tokens;
175
+ if (bucket.startMs >= weekStartMs) weekTokens += bucket.tokens;
176
+ }
177
+ const windows: CapacityWindow[] = [
178
+ {
179
+ id: "openai:tokens:today",
180
+ label: "Tokens · today (UTC)",
181
+ limitType: "TOKENS_LIMIT",
182
+ durationMinutes: 24 * 60,
183
+ usedPercent: null,
184
+ resetsAt: new Date(todayStartMs + DAY_MS).toISOString(),
185
+ total: null,
186
+ current: todayTokens,
187
+ remaining: null,
188
+ },
189
+ {
190
+ id: "openai:tokens:week",
191
+ label: "Tokens · 7 days",
192
+ limitType: "TOKENS_LIMIT",
193
+ durationMinutes: USAGE_WINDOW_DAYS * 24 * 60,
194
+ usedPercent: null,
195
+ resetsAt: null,
196
+ total: null,
197
+ current: weekTokens,
198
+ remaining: null,
199
+ },
200
+ ];
201
+ return openAiCapacityReport(windows, checkedAt, true);
202
+ }
203
+
204
+ async function fetchUsage(context: OpenAiRequestContext): Promise<unknown[] | number | null> {
205
+ const fetcher = context.fetch ?? globalThis.fetch;
206
+ const endSeconds = Math.floor(Date.parse(context.checkedAt) / 1000);
207
+ const startSeconds = endSeconds - USAGE_WINDOW_DAYS * 24 * 60 * 60;
208
+ const query = `start_time=${startSeconds}&end_time=${endSeconds}`;
209
+ const payloads: unknown[] = [];
210
+ for (const endpoint of OPENAI_USAGE_ENDPOINTS) {
211
+ const controller = new AbortController();
212
+ const timer = setTimeout(() => controller.abort(), context.timeoutMs ?? 5000);
213
+ let response: Response;
214
+ try {
215
+ response = await fetcher(`${OPENAI_USAGE_BASE_URL}/${endpoint}?${query}`, {
216
+ method: "GET",
217
+ headers: {
218
+ Authorization: `Bearer ${context.key}`,
219
+ },
220
+ signal: controller.signal,
221
+ });
222
+ } catch {
223
+ throw new Error("OpenAI usage request failed");
224
+ } finally {
225
+ clearTimeout(timer);
226
+ }
227
+ if (response.status === 401) return null;
228
+ if (response.status === 403) return response.status;
229
+ if (response.status !== 200) {
230
+ throw new Error(`OpenAI usage request failed: HTTP ${response.status}`);
231
+ }
232
+ try {
233
+ payloads.push(await response.json());
234
+ } catch {
235
+ throw new Error("OpenAI usage response is not valid JSON");
236
+ }
237
+ }
238
+ return payloads;
239
+ }
240
+
241
+ async function verifyKey(context: OpenAiRequestContext): Promise<boolean> {
242
+ const fetcher = context.fetch ?? globalThis.fetch;
243
+ const controller = new AbortController();
244
+ const timer = setTimeout(() => controller.abort(), context.timeoutMs ?? 5000);
245
+ try {
246
+ const response = await fetcher(OPENAI_MODELS_URL, {
247
+ method: "GET",
248
+ headers: { Authorization: `Bearer ${context.key}` },
249
+ signal: controller.signal,
250
+ });
251
+ return response.status !== 401;
252
+ } catch {
253
+ throw new Error("OpenAI usage request failed");
254
+ } finally {
255
+ clearTimeout(timer);
256
+ }
257
+ }
258
+
259
+ export async function probeOpenAiCapacity(options: OpenAiProbeOptions): Promise<CapacityReport> {
260
+ const credential = await resolveOpenAiCredential(options);
261
+ if (credential.kind === "oauth") {
262
+ return probeOpenAiOauthCapacity(credential, options);
263
+ }
264
+ const probe: OpenAiProbeOptions & { key: string } = { ...options, key: credential.key };
265
+ const result = await fetchUsage(probe);
266
+ if (result === null) {
267
+ return openAiCapacityReport([], options.checkedAt, false);
268
+ }
269
+ if (typeof result === "number") {
270
+ const authenticated = await verifyKey(probe);
271
+ return openAiCapacityReport([], options.checkedAt, authenticated);
272
+ }
273
+ const buckets = result.flatMap(parseOpenAiUsage);
274
+ return buildOpenAiReport(buckets, options.checkedAt);
275
+ }
276
+
277
+ /** Mirrors codex.ts: JWT `exp` (epoch seconds) as expiry fallback. */
278
+ function jwtExpiryMs(token: string): number | null {
279
+ const part = token.split(".")[1];
280
+ if (!part) return null;
281
+ try {
282
+ const exp = finiteNumber(
283
+ record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp,
284
+ );
285
+ return exp === null ? null : exp * 1000;
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+
291
+ function oauthExpired(credential: OAuthCredential, nowMs: number): boolean {
292
+ const expiryMs = credential.expiresMs ?? jwtExpiryMs(credential.access);
293
+ return expiryMs !== null && expiryMs <= nowMs;
294
+ }
295
+
296
+ async function fetchWhamUsage(
297
+ credential: OAuthCredential,
298
+ options: OpenAiProbeOptions,
299
+ ): Promise<unknown | null> {
300
+ const fetcher = options.fetch ?? globalThis.fetch;
301
+ const controller = new AbortController();
302
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5000);
303
+ let response: Response;
304
+ try {
305
+ response = await fetcher(WHAM_USAGE_URL, {
306
+ method: "GET",
307
+ headers: {
308
+ Authorization: `Bearer ${credential.access}`,
309
+ "ChatGPT-Account-Id": credential.accountId,
310
+ },
311
+ signal: controller.signal,
312
+ });
313
+ } catch {
314
+ throw new Error("OpenAI usage request failed");
315
+ } finally {
316
+ clearTimeout(timer);
317
+ }
318
+ if (response.status === 401 || response.status === 403) return null;
319
+ if (response.status !== 200) {
320
+ throw new Error(`OpenAI usage request failed: HTTP ${response.status}`);
321
+ }
322
+ try {
323
+ return await response.json();
324
+ } catch {
325
+ throw new Error("OpenAI usage response is not valid JSON");
326
+ }
327
+ }
328
+
329
+ /**
330
+ * OAuth path for Pi's OpenAI login: consumes the same wham/usage endpoint as
331
+ * the codex harness. Stale logins are never fetched (no refresh, matching
332
+ * codex.ts); they render as authenticated-but-stale instead.
333
+ */
334
+ async function probeOpenAiOauthCapacity(
335
+ credential: OAuthCredential,
336
+ options: OpenAiProbeOptions,
337
+ ): Promise<CapacityReport> {
338
+ const staleReport: CapacityReport = {
339
+ harness: "pi",
340
+ provider: "openai",
341
+ generatedAt: options.checkedAt,
342
+ authenticated: true,
343
+ available: "unknown",
344
+ windows: [],
345
+ creditsRemaining: null,
346
+ };
347
+ const nowMs = Date.parse(options.checkedAt);
348
+ if (Number.isNaN(nowMs) || oauthExpired(credential, nowMs)) return staleReport;
349
+ const raw = await fetchWhamUsage(credential, options);
350
+ if (raw === null) {
351
+ return { ...staleReport, authenticated: false };
352
+ }
353
+ const snapshot = parseWhamUsage(raw);
354
+ const hasUsage = snapshot.windows.some((window) => window.usedPercent !== null);
355
+ return {
356
+ ...staleReport,
357
+ available: hasUsage ? "yes" : "unknown",
358
+ windows: snapshot.windows,
359
+ creditsRemaining: snapshot.creditsRemaining,
360
+ };
361
+ }
@@ -0,0 +1,84 @@
1
+ import type { CapacityWindow } from "./types.js";
2
+
3
+ type WhamUsageSnapshot = {
4
+ windows: CapacityWindow[];
5
+ creditsRemaining: number | null;
6
+ };
7
+
8
+ type UnknownRecord = Record<string, unknown>;
9
+
10
+ function record(value: unknown): UnknownRecord | null {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value)
12
+ ? (value as UnknownRecord)
13
+ : null;
14
+ }
15
+
16
+ function finiteNumber(value: unknown): number | null {
17
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
18
+ }
19
+
20
+ function nonEmptyText(value: unknown): string | null {
21
+ return typeof value === "string" && value.length > 0 ? value : null;
22
+ }
23
+
24
+ export function resetTime(value: unknown): string | null {
25
+ const seconds = finiteNumber(value);
26
+ if (seconds !== null) return new Date(seconds * 1000).toISOString();
27
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value)))
28
+ return new Date(value).toISOString();
29
+ return null;
30
+ }
31
+
32
+ export function safeIdentifier(value: unknown): string | null {
33
+ const candidate = nonEmptyText(value);
34
+ if (!candidate || !/^[a-z][a-z0-9_-]{0,63}$/i.test(candidate)) return null;
35
+ if (/(?:account|token|secret|key)[_-]?\d{6,}/i.test(candidate)) return null;
36
+ return candidate;
37
+ }
38
+
39
+ export function toRateWindow(value: unknown, id: string, label: string): CapacityWindow | null {
40
+ const input = record(value);
41
+ if (!input) return null;
42
+ const used = finiteNumber(input.used_percent);
43
+ const seconds = finiteNumber(input.limit_window_seconds);
44
+ return {
45
+ id,
46
+ label,
47
+ durationMinutes: seconds === null ? null : seconds / 60,
48
+ usedPercent: used,
49
+ resetsAt: resetTime(input.reset_at),
50
+ };
51
+ }
52
+
53
+ function extraWindows(value: unknown): CapacityWindow[] {
54
+ if (!Array.isArray(value)) return [];
55
+ return value.flatMap((entry, index) => {
56
+ const limit = record(entry);
57
+ if (!limit) return [];
58
+ const scope = safeIdentifier(limit.limit_name) ?? `extra-${index + 1}`;
59
+ const windows = record(limit.rate_limit) ?? limit;
60
+ return [
61
+ toRateWindow(windows.primary_window, `${scope}:primary`, `${scope} primary`),
62
+ toRateWindow(windows.secondary_window, `${scope}:secondary`, `${scope} secondary`),
63
+ ].filter((window): window is CapacityWindow => window !== null);
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Parses the chatgpt.com backend wham/usage payload shared by every
69
+ * OAuth-backed OpenAI consumer (codex harness and pi login) into generic
70
+ * capacity windows plus the credit balance.
71
+ */
72
+ export function parseWhamUsage(raw: unknown): WhamUsageSnapshot {
73
+ const response = record(raw) ?? {};
74
+ const limits = record(response.rate_limit) ?? {};
75
+ const credits = record(response.credits) ?? {};
76
+ return {
77
+ windows: [
78
+ toRateWindow(limits.primary_window, "session", "Session"),
79
+ toRateWindow(limits.secondary_window, "weekly", "Weekly"),
80
+ ...extraWindows(response.additional_rate_limits),
81
+ ].filter((window): window is CapacityWindow => window !== null),
82
+ creditsRemaining: finiteNumber(credits.balance),
83
+ };
84
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  export { AgentManager, AgentNotRunningError } from "./AgentManager.js";
2
- export { getCodexCapacityReport, getZaiCapacityReport } from "./capacity/index.js";
2
+ export {
3
+ getClaudeCapacityReport,
4
+ getCodexCapacityReport,
5
+ getOpenAiCapacityReport,
6
+ getZaiCapacityReport,
7
+ } from "./capacity/index.js";
3
8
  export type {
9
+ ClaudeCapacityOptions,
4
10
  CapacityProbeOptions,
5
11
  CapacityReport,
6
12
  CapacityWindow,
13
+ OpenAiCapacityOptions,
7
14
  ZaiCapacityOptions,
8
15
  } from "./capacity/index.js";
9
16
  export {
@@ -29,7 +36,9 @@ export { ClaudeCodeAdapter } from "./providers/claude/ClaudeCodeAdapter.js";
29
36
  export { CodexAdapter } from "./providers/codex/CodexAdapter.js";
30
37
  export { CopilotAdapter } from "./providers/copilot/CopilotAdapter.js";
31
38
  export { GeminiCliAdapter } from "./providers/gemini/GeminiCliAdapter.js";
32
- export { GrokCliAdapter } from "./adapters/GrokCliAdapter.js";
39
+ export { GrokCliAdapter } from "./providers/grok/GrokCliAdapter.js";
40
+ export { AntigravityCliAdapter } from "./adapters/AntigravityCliAdapter.js";
41
+ export { KiroAdapter } from "./adapters/KiroAdapter.js";
33
42
  export { OpenCodeAdapter } from "./providers/opencode/OpenCodeAdapter.js";
34
43
  export { PiAdapter } from "./providers/pi/PiAdapter.js";
35
44
  export { AgentStatus } from "./adapters/AgentAdapter.js";
@@ -138,31 +138,36 @@ export class ClaudeCodeAdapter implements AgentAdapter {
138
138
  const summaries: SessionSummary[] = [];
139
139
 
140
140
  for (const { filePath, defaultCwd } of candidates) {
141
- const session = this.parser.readSession(filePath, defaultCwd);
142
- if (!session) continue;
143
-
144
- // Drop sessions whose JSONL had no parseable conversation entries.
145
- // readSession is permissive (returns a shell record even when every
146
- // line fails to parse); listSessions needs at least one real entry
147
- // so we don't surface garbage files.
148
- if (!session.lastEntryType) continue;
149
-
150
- const recordedCwd = session.lastCwd || defaultCwd;
151
- if (filterCwd !== undefined && recordedCwd !== filterCwd) continue;
152
-
153
- const stat = safeStat(filePath);
154
-
155
- summaries.push({
156
- type: "claude",
157
- sessionId: session.sessionId,
158
- cwd: recordedCwd,
159
- firstUserMessage: session.firstUserMessage || "",
160
- lastActive: session.lastActive ?? stat?.mtime ?? new Date(),
161
- startedAt: session.sessionStart ?? stat?.birthtime ?? stat?.mtime ?? new Date(),
162
- sessionFilePath: filePath,
163
- });
141
+ const summary = this.toSessionSummary(filePath, defaultCwd);
142
+ if (!summary) continue;
143
+ if (filterCwd !== undefined && summary.cwd !== filterCwd) continue;
144
+ summaries.push(summary);
164
145
  }
165
146
 
166
147
  return summaries;
167
148
  }
149
+
150
+ async findSessionsById(sessionId: string): Promise<SessionSummary[]> {
151
+ return this.createLocator()
152
+ .findHistoricalSessionFilesById(sessionId)
153
+ .map(({ filePath, defaultCwd }) => this.toSessionSummary(filePath, defaultCwd))
154
+ .filter((summary): summary is SessionSummary => summary?.sessionId === sessionId);
155
+ }
156
+
157
+ private toSessionSummary(filePath: string, defaultCwd: string): SessionSummary | null {
158
+ const session = this.parser.readSession(filePath, defaultCwd);
159
+ if (!session?.lastEntryType) return null;
160
+
161
+ const recordedCwd = session.lastCwd || defaultCwd;
162
+ const stat = safeStat(filePath);
163
+ return {
164
+ type: "claude",
165
+ sessionId: session.sessionId,
166
+ cwd: recordedCwd,
167
+ firstUserMessage: session.firstUserMessage || "",
168
+ lastActive: session.lastActive ?? stat?.mtime ?? new Date(),
169
+ startedAt: session.sessionStart ?? stat?.birthtime ?? stat?.mtime ?? new Date(),
170
+ sessionFilePath: filePath,
171
+ };
172
+ }
168
173
  }
@@ -6,6 +6,7 @@ import { matchProcessesToSessions, type MatchResult } from "../../utils/matching
6
6
  import {
7
7
  batchGetSessionFileBirthtimes,
8
8
  isDirectory,
9
+ isSafePathSegment,
9
10
  listJsonl,
10
11
  safeReaddir,
11
12
  safeStat,
@@ -130,6 +131,25 @@ export class ClaudeSessionLocator {
130
131
  return out;
131
132
  }
132
133
 
134
+ findHistoricalSessionFilesById(
135
+ sessionId: string,
136
+ ): Array<{ filePath: string; defaultCwd: string }> {
137
+ if (!isSafePathSegment(sessionId) || !isDirectory(this.projectsDir)) {
138
+ return [];
139
+ }
140
+
141
+ const matches: Array<{ filePath: string; defaultCwd: string }> = [];
142
+ for (const dirName of safeReaddir(this.projectsDir)) {
143
+ const projectDir = path.join(this.projectsDir, dirName);
144
+ if (!isDirectory(projectDir)) continue;
145
+
146
+ const filePath = path.join(projectDir, `${sessionId}.jsonl`);
147
+ if (!safeStat(filePath)?.isFile()) continue;
148
+ matches.push({ filePath, defaultCwd: dirName.replace(/-/g, "/") });
149
+ }
150
+ return matches;
151
+ }
152
+
133
153
  /**
134
154
  * Derive the Claude Code project directory for a given CWD.
135
155
  *
@@ -95,6 +95,14 @@ export class CodexAdapter implements AgentAdapter {
95
95
  return summaries;
96
96
  }
97
97
 
98
+ async findSessionsById(sessionId: string): Promise<SessionSummary[]> {
99
+ const sessionFile = this.createLocator().findSessionFileById(sessionId);
100
+ if (!sessionFile) return [];
101
+
102
+ const summary = this.parser.fileToSessionSummary(sessionFile.filePath);
103
+ return summary?.sessionId === sessionId ? [summary] : [];
104
+ }
105
+
98
106
  private async getCodexProcesses(context?: AgentDetectionContext): Promise<ProcessInfo[]> {
99
107
  const snapshot = context?.processes ?? (await captureProcessSnapshot(this.processNames));
100
108
  const relevant = filterByProcessNames(snapshot, this.processNames);
@@ -29,7 +29,7 @@ import {
29
29
  import { AgentRegistry, type RegistryEntry } from "../../utils/AgentRegistry.js";
30
30
  import { CopilotAgentMapper } from "./CopilotAgentMapper.js";
31
31
  import { CopilotSessionLocator } from "./CopilotSessionLocator.js";
32
- import { CopilotSessionParser } from "./CopilotSessionParser.js";
32
+ import { CopilotSessionParser, type CopilotSession } from "./CopilotSessionParser.js";
33
33
 
34
34
  export interface CopilotAdapterOptions {
35
35
  sessionStateDir?: string;
@@ -111,20 +111,36 @@ export class CopilotAdapter implements AgentAdapter {
111
111
  if (!session) continue;
112
112
  if (opts?.cwd !== undefined && session.projectPath !== opts.cwd) continue;
113
113
 
114
- summaries.push({
115
- type: this.type,
116
- sessionId: session.sessionId,
117
- cwd: session.projectPath,
118
- firstUserMessage: session.firstUserMessage,
119
- lastActive: session.lastActive,
120
- startedAt: session.sessionStart,
121
- sessionFilePath: session.eventsFilePath,
122
- });
114
+ summaries.push(this.toSessionSummary(session));
123
115
  }
124
116
 
125
117
  return summaries;
126
118
  }
127
119
 
120
+ async findSessionsById(sessionId: string): Promise<SessionSummary[]> {
121
+ const match = this.locator.findSessionDirById(sessionId);
122
+ if (match) {
123
+ const session = this.parser.readSessionDir(match.sessionDir, match.sessionId);
124
+ if (session?.sessionId === sessionId) {
125
+ return [this.toSessionSummary(session)];
126
+ }
127
+ }
128
+
129
+ return (await this.listSessions()).filter((session) => session.sessionId === sessionId);
130
+ }
131
+
132
+ private toSessionSummary(session: CopilotSession): SessionSummary {
133
+ return {
134
+ type: this.type,
135
+ sessionId: session.sessionId,
136
+ cwd: session.projectPath,
137
+ firstUserMessage: session.firstUserMessage,
138
+ lastActive: session.lastActive,
139
+ startedAt: session.sessionStart,
140
+ sessionFilePath: session.eventsFilePath,
141
+ };
142
+ }
143
+
128
144
  private applyWrapperRegistryName(
129
145
  agent: AgentInfo,
130
146
  processInfo: ProcessInfo,
@@ -1,5 +1,5 @@
1
1
  import * as path from "path";
2
- import { isDirectory, safeReaddir } from "../../utils/session.js";
2
+ import { isDirectory, isSafePathSegment, safeReaddir } from "../../utils/session.js";
3
3
 
4
4
  export interface CopilotLock {
5
5
  sessionDir: string;
@@ -39,6 +39,12 @@ export class CopilotSessionLocator {
39
39
  return sessionDirs;
40
40
  }
41
41
 
42
+ findSessionDirById(sessionId: string): CopilotSessionDir | null {
43
+ if (!isSafePathSegment(sessionId)) return null;
44
+ const sessionDir = path.join(this.sessionStateDir, sessionId);
45
+ return isDirectory(sessionDir) ? { sessionDir, sessionId } : null;
46
+ }
47
+
42
48
  discoverActiveLocks(): CopilotLock[] {
43
49
  const locks: CopilotLock[] = [];
44
50
  for (const { sessionDir, sessionId } of this.listSessionDirs()) {
@@ -108,6 +108,14 @@ export class GeminiCliAdapter implements AgentAdapter {
108
108
  return summaries;
109
109
  }
110
110
 
111
+ async findSessionsById(sessionId: string): Promise<SessionSummary[]> {
112
+ for (const filePath of this.locator.discoverHistoricalSessionFiles()) {
113
+ const summary = this.parser.fileToSessionSummary(filePath);
114
+ if (summary?.sessionId === sessionId) return [summary];
115
+ }
116
+ return [];
117
+ }
118
+
111
119
  private async getGeminiProcesses(context?: AgentDetectionContext): Promise<ProcessInfo[]> {
112
120
  const snapshot = context?.processes ?? (await captureProcessSnapshot(this.processNames));
113
121
  const relevant = filterByProcessNames(snapshot, this.processNames);