@ai-devkit/agent-manager 0.33.0 → 0.34.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 (174) hide show
  1. package/dist/AgentManager.d.ts +11 -2
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +37 -6
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +212 -5
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/capacity/codex.test.js +2 -1
  8. package/dist/__tests__/capacity/codex.test.js.map +1 -1
  9. package/dist/__tests__/capacity/index.test.js +6 -3
  10. package/dist/__tests__/capacity/index.test.js.map +1 -1
  11. package/dist/__tests__/capacity/zai.test.js +213 -0
  12. package/dist/__tests__/capacity/zai.test.js.map +1 -0
  13. package/dist/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.js +19 -14
  14. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -0
  15. package/dist/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.js +37 -29
  16. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -0
  17. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +189 -0
  18. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -0
  19. package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js +89 -0
  20. package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js.map +1 -0
  21. package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js +94 -0
  22. package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js.map +1 -0
  23. package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js +147 -0
  24. package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js.map +1 -0
  25. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js +164 -0
  26. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js.map +1 -0
  27. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +37 -1
  28. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -1
  29. package/dist/__tests__/utils/AgentRegistry.test.js +20 -13
  30. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  31. package/dist/adapters/index.d.ts +3 -3
  32. package/dist/adapters/index.d.ts.map +1 -1
  33. package/dist/adapters/index.js +3 -3
  34. package/dist/adapters/index.js.map +1 -1
  35. package/dist/capacity/codex.d.ts +1 -0
  36. package/dist/capacity/codex.d.ts.map +1 -1
  37. package/dist/capacity/codex.js +9 -4
  38. package/dist/capacity/codex.js.map +1 -1
  39. package/dist/capacity/index.d.ts +8 -0
  40. package/dist/capacity/index.d.ts.map +1 -1
  41. package/dist/capacity/index.js +10 -9
  42. package/dist/capacity/index.js.map +1 -1
  43. package/dist/capacity/types.d.ts +8 -0
  44. package/dist/capacity/types.d.ts.map +1 -1
  45. package/dist/capacity/types.js.map +1 -1
  46. package/dist/capacity/zai.d.ts +14 -0
  47. package/dist/capacity/zai.d.ts.map +1 -0
  48. package/dist/capacity/zai.js +203 -0
  49. package/dist/capacity/zai.js.map +1 -0
  50. package/dist/index.d.ts +8 -6
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -4
  53. package/dist/index.js.map +1 -1
  54. package/dist/{adapters → providers/copilot}/CopilotAdapter.d.ts +11 -25
  55. package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -0
  56. package/dist/providers/copilot/CopilotAdapter.js +108 -0
  57. package/dist/providers/copilot/CopilotAdapter.js.map +1 -0
  58. package/dist/providers/copilot/CopilotAgentMapper.d.ts +9 -0
  59. package/dist/providers/copilot/CopilotAgentMapper.d.ts.map +1 -0
  60. package/dist/providers/copilot/CopilotAgentMapper.js +37 -0
  61. package/dist/providers/copilot/CopilotAgentMapper.js.map +1 -0
  62. package/dist/providers/copilot/CopilotSessionLocator.d.ts +19 -0
  63. package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -0
  64. package/dist/providers/copilot/CopilotSessionLocator.js +41 -0
  65. package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -0
  66. package/dist/providers/copilot/CopilotSessionParser.d.ts +30 -0
  67. package/dist/providers/copilot/CopilotSessionParser.d.ts.map +1 -0
  68. package/dist/providers/copilot/CopilotSessionParser.js +181 -0
  69. package/dist/providers/copilot/CopilotSessionParser.js.map +1 -0
  70. package/dist/providers/gemini/GeminiAgentMapper.d.ts +9 -0
  71. package/dist/providers/gemini/GeminiAgentMapper.d.ts.map +1 -0
  72. package/dist/providers/gemini/GeminiAgentMapper.js +37 -0
  73. package/dist/providers/gemini/GeminiAgentMapper.js.map +1 -0
  74. package/dist/providers/gemini/GeminiCliAdapter.d.ts +52 -0
  75. package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -0
  76. package/dist/providers/gemini/GeminiCliAdapter.js +188 -0
  77. package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -0
  78. package/dist/providers/gemini/GeminiSessionLocator.d.ts +30 -0
  79. package/dist/providers/gemini/GeminiSessionLocator.d.ts.map +1 -0
  80. package/dist/providers/gemini/GeminiSessionLocator.js +142 -0
  81. package/dist/providers/gemini/GeminiSessionLocator.js.map +1 -0
  82. package/dist/providers/gemini/GeminiSessionParser.d.ts +94 -0
  83. package/dist/providers/gemini/GeminiSessionParser.d.ts.map +1 -0
  84. package/dist/providers/gemini/GeminiSessionParser.js +166 -0
  85. package/dist/providers/gemini/GeminiSessionParser.js.map +1 -0
  86. package/dist/{adapters → providers/opencode}/OpenCodeAdapter.d.ts +6 -12
  87. package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -0
  88. package/dist/providers/opencode/OpenCodeAdapter.js +86 -0
  89. package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -0
  90. package/dist/providers/opencode/OpenCodeAgentMapper.d.ts +11 -0
  91. package/dist/providers/opencode/OpenCodeAgentMapper.d.ts.map +1 -0
  92. package/dist/providers/opencode/OpenCodeAgentMapper.js +50 -0
  93. package/dist/providers/opencode/OpenCodeAgentMapper.js.map +1 -0
  94. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +21 -0
  95. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -0
  96. package/dist/providers/opencode/OpenCodeSessionLocator.js +99 -0
  97. package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -0
  98. package/dist/providers/opencode/OpenCodeSessionParser.d.ts +18 -0
  99. package/dist/providers/opencode/OpenCodeSessionParser.d.ts.map +1 -0
  100. package/dist/providers/opencode/OpenCodeSessionParser.js +104 -0
  101. package/dist/providers/opencode/OpenCodeSessionParser.js.map +1 -0
  102. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts +18 -0
  103. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -0
  104. package/dist/runtime/herdr/HerdrAgentDiscovery.js +183 -0
  105. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -0
  106. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +5 -0
  107. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -1
  108. package/dist/runtime/herdr/HerdrAgentRuntime.js +26 -8
  109. package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -1
  110. package/dist/runtime/herdr/HerdrResponseParsers.d.ts +2 -0
  111. package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -1
  112. package/dist/runtime/herdr/HerdrResponseParsers.js +12 -1
  113. package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -1
  114. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  115. package/dist/utils/AgentRegistry.js +5 -5
  116. package/dist/utils/AgentRegistry.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/AgentManager.ts +67 -5
  119. package/src/__tests__/AgentManager.test.ts +231 -5
  120. package/src/__tests__/capacity/codex.test.ts +94 -26
  121. package/src/__tests__/capacity/fixtures/zai-auth.json +6 -0
  122. package/src/__tests__/capacity/fixtures/zai-quota-invalid.json +7 -0
  123. package/src/__tests__/capacity/fixtures/zai-quota.json +38 -0
  124. package/src/__tests__/capacity/index.test.ts +10 -3
  125. package/src/__tests__/capacity/zai.test.ts +186 -0
  126. package/src/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.ts +13 -16
  127. package/src/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.ts +39 -31
  128. package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +197 -0
  129. package/src/__tests__/providers/opencode/OpenCodeAgentMapper.test.ts +83 -0
  130. package/src/__tests__/providers/opencode/OpenCodeSessionLocator.test.ts +84 -0
  131. package/src/__tests__/providers/opencode/OpenCodeSessionParser.test.ts +148 -0
  132. package/src/__tests__/runtime/HerdrAgentDiscovery.test.ts +131 -0
  133. package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +20 -1
  134. package/src/__tests__/utils/AgentRegistry.test.ts +20 -10
  135. package/src/adapters/index.ts +3 -3
  136. package/src/capacity/codex.ts +96 -28
  137. package/src/capacity/index.ts +29 -11
  138. package/src/capacity/types.ts +8 -0
  139. package/src/capacity/zai.ts +237 -0
  140. package/src/index.ts +18 -6
  141. package/src/providers/copilot/CopilotAdapter.ts +145 -0
  142. package/src/providers/copilot/CopilotAgentMapper.ts +36 -0
  143. package/src/providers/copilot/CopilotSessionLocator.ts +57 -0
  144. package/src/providers/copilot/CopilotSessionParser.ts +289 -0
  145. package/src/providers/gemini/GeminiAgentMapper.ts +36 -0
  146. package/src/providers/gemini/GeminiCliAdapter.ts +266 -0
  147. package/src/providers/gemini/GeminiSessionLocator.ts +183 -0
  148. package/src/providers/gemini/GeminiSessionParser.ts +271 -0
  149. package/src/providers/opencode/OpenCodeAdapter.ts +112 -0
  150. package/src/providers/opencode/OpenCodeAgentMapper.ts +61 -0
  151. package/src/providers/opencode/OpenCodeSessionLocator.ts +123 -0
  152. package/src/providers/opencode/OpenCodeSessionParser.ts +145 -0
  153. package/src/runtime/herdr/HerdrAgentDiscovery.ts +178 -0
  154. package/src/runtime/herdr/HerdrAgentRuntime.ts +33 -7
  155. package/src/runtime/herdr/HerdrResponseParsers.ts +11 -1
  156. package/src/utils/AgentRegistry.ts +8 -6
  157. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +0 -1
  158. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +0 -1
  159. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +0 -635
  160. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +0 -1
  161. package/dist/adapters/CopilotAdapter.d.ts.map +0 -1
  162. package/dist/adapters/CopilotAdapter.js +0 -329
  163. package/dist/adapters/CopilotAdapter.js.map +0 -1
  164. package/dist/adapters/GeminiCliAdapter.d.ts +0 -98
  165. package/dist/adapters/GeminiCliAdapter.d.ts.map +0 -1
  166. package/dist/adapters/GeminiCliAdapter.js +0 -513
  167. package/dist/adapters/GeminiCliAdapter.js.map +0 -1
  168. package/dist/adapters/OpenCodeAdapter.d.ts.map +0 -1
  169. package/dist/adapters/OpenCodeAdapter.js +0 -286
  170. package/dist/adapters/OpenCodeAdapter.js.map +0 -1
  171. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +0 -543
  172. package/src/adapters/CopilotAdapter.ts +0 -483
  173. package/src/adapters/GeminiCliAdapter.ts +0 -699
  174. package/src/adapters/OpenCodeAdapter.ts +0 -369
@@ -92,24 +92,34 @@ describe("AgentRegistry", () => {
92
92
  });
93
93
  });
94
94
 
95
- it("lets a Herdr managed start replace an existing same-pid name", () => {
95
+ it("preserves existing identity and routing fields for same-pid refreshes", () => {
96
96
  const runtimeRef = { session: "default", paneId: "w1:p2", agentName: "requested-name" };
97
97
 
98
- registry.register(makeEntry({ name: "old-name", pid: process.pid }));
98
+ registry.register(
99
+ makeEntry({
100
+ name: "old-name",
101
+ pid: process.pid,
102
+ runtime: "tmux",
103
+ runtimeRef: { session: "old-name" },
104
+ cwd: "/original",
105
+ }),
106
+ );
99
107
  registry.register(
100
108
  makeEntry({
101
109
  name: "requested-name",
102
110
  pid: process.pid,
103
111
  runtime: "herdr",
104
112
  runtimeRef,
113
+ cwd: "/incoming",
105
114
  }),
106
115
  );
107
116
 
108
- expect(registry.lookup("old-name")).toBeNull();
109
- expect(registry.lookup("requested-name")).toMatchObject({
117
+ expect(registry.lookup("requested-name")).toBeNull();
118
+ expect(registry.lookup("old-name")).toMatchObject({
110
119
  pid: process.pid,
111
- runtime: "herdr",
112
- runtimeRef,
120
+ runtime: "tmux",
121
+ runtimeRef: { session: "old-name" },
122
+ cwd: "/original",
113
123
  });
114
124
  });
115
125
 
@@ -127,11 +137,11 @@ describe("AgentRegistry", () => {
127
137
  expect(registry.lookup("a")?.runtimeRef).toBeNull();
128
138
  });
129
139
 
130
- it("lets a managed start entry replace a generated fallback for the same pid", () => {
140
+ it("preserves an existing generated fallback for the same pid", () => {
131
141
  registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, runtimeRef: null }));
132
142
  registry.register(makeEntry({ name: "custom-name", runtimeRef: { session: "custom-name" } }));
133
- expect(registry.lookup("custom-name")?.runtimeRef).toEqual({ session: "custom-name" });
134
- expect(registry.lookup(`ai-devkit-${process.pid}`)).toBeNull();
143
+ expect(registry.lookup("custom-name")).toBeNull();
144
+ expect(registry.lookup(`ai-devkit-${process.pid}`)?.runtimeRef).toBeNull();
135
145
  expect(registry.list()).toHaveLength(1);
136
146
  });
137
147
 
@@ -186,7 +196,7 @@ describe("AgentRegistry", () => {
186
196
  registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, runtimeRef: null }));
187
197
 
188
198
  expect(registry.list()).toHaveLength(1);
189
- expect(registry.lookup("custom-name")?.pid).toBe(process.pid);
199
+ expect(registry.lookup(`ai-devkit-${process.pid}`)?.pid).toBe(process.pid);
190
200
  });
191
201
 
192
202
  it("cleans up a cross-type row when its pid has been reused", () => {
@@ -1,9 +1,9 @@
1
1
  export { ClaudeCodeAdapter } from "../providers/claude/ClaudeCodeAdapter.js";
2
2
  export { CodexAdapter } from "../providers/codex/CodexAdapter.js";
3
- export { CopilotAdapter } from "./CopilotAdapter.js";
4
- export { GeminiCliAdapter } from "./GeminiCliAdapter.js";
3
+ export { CopilotAdapter } from "../providers/copilot/CopilotAdapter.js";
4
+ export { GeminiCliAdapter } from "../providers/gemini/GeminiCliAdapter.js";
5
5
  export { GrokCliAdapter } from "./GrokCliAdapter.js";
6
- export { OpenCodeAdapter } from "./OpenCodeAdapter.js";
6
+ export { OpenCodeAdapter } from "../providers/opencode/OpenCodeAdapter.js";
7
7
  export { PiAdapter } from "../providers/pi/PiAdapter.js";
8
8
  export { AgentStatus } from "./AgentAdapter.js";
9
9
  export type {
@@ -15,7 +15,13 @@ type RpcMessage = { id?: number; method: string; params?: UnknownRecord };
15
15
  type CliResponses = { rateLimits: unknown; account: unknown };
16
16
  type CodexRpc = (messages: RpcMessage[]) => Promise<CliResponses>;
17
17
 
18
- export const CODEX_APP_SERVER_ARGS = ["-s", "read-only", "-a", "untrusted", "app-server"] as const;
18
+ export const CODEX_APP_SERVER_ARGS = [
19
+ "-s",
20
+ "read-only",
21
+ "-a",
22
+ "untrusted",
23
+ "app-server",
24
+ ] as const;
19
25
 
20
26
  type CodexProbeOptions = {
21
27
  installed: boolean;
@@ -57,12 +63,18 @@ function safeIdentifier(value: unknown): string | null {
57
63
  return candidate;
58
64
  }
59
65
 
60
- export function resolveCodexAuthPath(env: NodeJS.ProcessEnv = process.env): string {
66
+ export function resolveCodexAuthPath(
67
+ env: NodeJS.ProcessEnv = process.env,
68
+ ): string {
61
69
  const root = env.CODEX_HOME || join(env.HOME || "", ".codex");
62
70
  return join(root, "auth.json");
63
71
  }
64
72
 
65
- export function toRateWindow(value: unknown, id: string, label: string): CapacityWindow | null {
73
+ export function toRateWindow(
74
+ value: unknown,
75
+ id: string,
76
+ label: string,
77
+ ): CapacityWindow | null {
66
78
  const input = record(value);
67
79
  if (!input) return null;
68
80
  const used = finiteNumber(input.used_percent);
@@ -84,13 +96,24 @@ function extraWindows(value: unknown): CapacityWindow[] {
84
96
  const scope = safeIdentifier(limit.limit_name) ?? `extra-${index + 1}`;
85
97
  const windows = record(limit.rate_limit) ?? limit;
86
98
  return [
87
- toRateWindow(windows.primary_window, `${scope}:primary`, `${scope} primary`),
88
- toRateWindow(windows.secondary_window, `${scope}:secondary`, `${scope} secondary`),
99
+ toRateWindow(
100
+ windows.primary_window,
101
+ `${scope}:primary`,
102
+ `${scope} primary`,
103
+ ),
104
+ toRateWindow(
105
+ windows.secondary_window,
106
+ `${scope}:secondary`,
107
+ `${scope} secondary`,
108
+ ),
89
109
  ].filter((window): window is CapacityWindow => window !== null);
90
110
  });
91
111
  }
92
112
 
93
- export function parseUsage(raw: unknown, source: "pat" | "oauth"): UsageSnapshot {
113
+ export function parseUsage(
114
+ raw: unknown,
115
+ source: "pat" | "oauth",
116
+ ): UsageSnapshot {
94
117
  const response = record(raw) ?? {};
95
118
  const limits = record(response.rate_limit) ?? {};
96
119
  const credits = record(response.credits) ?? {};
@@ -105,7 +128,11 @@ export function parseUsage(raw: unknown, source: "pat" | "oauth"): UsageSnapshot
105
128
  };
106
129
  }
107
130
 
108
- function cliWindow(value: unknown, id: string, label: string): CapacityWindow | null {
131
+ function cliWindow(
132
+ value: unknown,
133
+ id: string,
134
+ label: string,
135
+ ): CapacityWindow | null {
109
136
  const input = record(value);
110
137
  if (!input) return null;
111
138
  return {
@@ -117,10 +144,14 @@ function cliWindow(value: unknown, id: string, label: string): CapacityWindow |
117
144
  };
118
145
  }
119
146
 
120
- function cliSnapshotWindows(value: unknown, fallbackId: string): CapacityWindow[] {
147
+ function cliSnapshotWindows(
148
+ value: unknown,
149
+ fallbackId: string,
150
+ ): CapacityWindow[] {
121
151
  const snapshot = record(value);
122
152
  if (!snapshot) return [];
123
- const scope = safeIdentifier(snapshot.limitId) ?? safeIdentifier(fallbackId) ?? "codex";
153
+ const scope =
154
+ safeIdentifier(snapshot.limitId) ?? safeIdentifier(fallbackId) ?? "codex";
124
155
  return [
125
156
  cliWindow(snapshot.primary, `${scope}:primary`, `${scope} primary`),
126
157
  cliWindow(snapshot.secondary, `${scope}:secondary`, `${scope} secondary`),
@@ -136,7 +167,9 @@ export function parseCliUsage(raw: unknown): UsageSnapshot {
136
167
  for (const [id, snapshot] of Object.entries(buckets))
137
168
  windows.push(...cliSnapshotWindows(snapshot, id));
138
169
  }
139
- const unique = [...new Map(windows.map((window) => [window.id, window])).values()];
170
+ const unique = [
171
+ ...new Map(windows.map((window) => [window.id, window])).values(),
172
+ ];
140
173
  return { windows: unique, creditsRemaining: null, source: "cli" };
141
174
  }
142
175
 
@@ -145,18 +178,23 @@ function capacityFromSnapshot(
145
178
  context: CodexProbeOptions,
146
179
  raw?: unknown,
147
180
  ): CapacityReport {
148
- const hasUsage = snapshot.windows.some((window) => window.usedPercent !== null);
181
+ const hasUsage = snapshot.windows.some(
182
+ (window) => window.usedPercent !== null,
183
+ );
149
184
  const rateLimits = record(record(raw)?.rateLimits);
150
185
  const reached = nonEmptyText(rateLimits?.rateLimitReachedType);
151
186
  const resetCredits =
152
- record(record(raw)?.rateLimitResetCredits) ?? record(record(raw)?.usageLimitResetCredits);
187
+ record(record(raw)?.rateLimitResetCredits) ??
188
+ record(record(raw)?.usageLimitResetCredits);
153
189
  return {
154
- provider: "codex",
190
+ harness: "codex",
191
+ provider: "openai",
155
192
  generatedAt: context.checkedAt,
156
193
  authenticated: true,
157
194
  available: reached ? "no" : hasUsage ? "yes" : "unknown",
158
195
  windows: snapshot.windows,
159
- creditsRemaining: snapshot.creditsRemaining ?? finiteNumber(resetCredits?.availableCount),
196
+ creditsRemaining:
197
+ snapshot.creditsRemaining ?? finiteNumber(resetCredits?.availableCount),
160
198
  };
161
199
  }
162
200
 
@@ -164,7 +202,9 @@ function jwtExpiry(token: string): number | null {
164
202
  const part = token.split(".")[1];
165
203
  if (!part) return null;
166
204
  try {
167
- return finiteNumber(record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp);
205
+ return finiteNumber(
206
+ record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp,
207
+ );
168
208
  } catch {
169
209
  return null;
170
210
  }
@@ -191,7 +231,10 @@ async function fetchJson(
191
231
  const timer = setTimeout(() => controller.abort(), timeoutMs);
192
232
  try {
193
233
  const response = await fetcher(url, { ...init, signal: controller.signal });
194
- if (!response.ok) throw new Error(response.status === 401 ? "unauthorized" : "request failed");
234
+ if (!response.ok)
235
+ throw new Error(
236
+ response.status === 401 ? "unauthorized" : "request failed",
237
+ );
195
238
  return await response.json();
196
239
  } finally {
197
240
  clearTimeout(timer);
@@ -209,14 +252,20 @@ async function apiSnapshot(
209
252
  fetcher,
210
253
  "https://chatgpt.com/backend-api/wham/usage",
211
254
  {
212
- headers: { Authorization: `Bearer ${token}`, "ChatGPT-Account-Id": accountId },
255
+ headers: {
256
+ Authorization: `Bearer ${token}`,
257
+ "ChatGPT-Account-Id": accountId,
258
+ },
213
259
  },
214
260
  options.timeoutMs ?? 5000,
215
261
  );
216
262
  return parseUsage(raw, source);
217
263
  }
218
264
 
219
- function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResponses> {
265
+ function appServerRpc(
266
+ messages: RpcMessage[],
267
+ timeoutMs = 5000,
268
+ ): Promise<CliResponses> {
220
269
  return new Promise((resolve, reject) => {
221
270
  const child = spawn("codex", CODEX_APP_SERVER_ARGS, {
222
271
  stdio: ["pipe", "pipe", "ignore"],
@@ -232,8 +281,13 @@ function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResp
232
281
  if (error) reject(error);
233
282
  else resolve(results as CliResponses);
234
283
  };
235
- const timer = setTimeout(() => finish(new Error("codex probe timed out")), timeoutMs);
236
- child.once("error", () => finish(new Error("codex app-server unavailable")));
284
+ const timer = setTimeout(
285
+ () => finish(new Error("codex probe timed out")),
286
+ timeoutMs,
287
+ );
288
+ child.once("error", () =>
289
+ finish(new Error("codex app-server unavailable")),
290
+ );
237
291
  child.once("exit", () => {
238
292
  if (!settled) finish(new Error("codex app-server exited"));
239
293
  });
@@ -256,7 +310,8 @@ function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResp
256
310
  for (const request of messages.slice(1))
257
311
  child.stdin.write(`${JSON.stringify(request)}\n`);
258
312
  } else if (message.id === 2) {
259
- if (message.error) finish(new Error("codex rate-limit method failed"));
313
+ if (message.error)
314
+ finish(new Error("codex rate-limit method failed"));
260
315
  else results.rateLimits = message.result;
261
316
  } else if (message.id === 3) {
262
317
  if (message.error) finish(new Error("codex account method failed"));
@@ -269,10 +324,11 @@ function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResp
269
324
  });
270
325
  }
271
326
 
272
- function unavailable(options: CodexProbeOptions): CapacityReport {
327
+ export function codexUnavailableReport(checkedAt: string): CapacityReport {
273
328
  return {
274
- provider: "codex",
275
- generatedAt: options.checkedAt,
329
+ harness: "codex",
330
+ provider: "openai",
331
+ generatedAt: checkedAt,
276
332
  authenticated: null,
277
333
  available: "unknown",
278
334
  windows: [],
@@ -280,7 +336,13 @@ function unavailable(options: CodexProbeOptions): CapacityReport {
280
336
  };
281
337
  }
282
338
 
283
- async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport> {
339
+ function unavailable(options: CodexProbeOptions): CapacityReport {
340
+ return codexUnavailableReport(options.checkedAt);
341
+ }
342
+
343
+ async function cliFallback(
344
+ options: CodexProbeOptions,
345
+ ): Promise<CapacityReport> {
284
346
  if (!options.installed) return unavailable(options);
285
347
  const messages: RpcMessage[] = [
286
348
  {
@@ -296,7 +358,8 @@ async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport>
296
358
  { id: 3, method: "account/read" },
297
359
  ];
298
360
  try {
299
- const rpc = options.rpc ?? ((requests) => appServerRpc(requests, options.timeoutMs));
361
+ const rpc =
362
+ options.rpc ?? ((requests) => appServerRpc(requests, options.timeoutMs));
300
363
  const response = await rpc(messages);
301
364
  const result = capacityFromSnapshot(
302
365
  parseCliUsage(response.rateLimits),
@@ -318,7 +381,9 @@ async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport>
318
381
  }
319
382
  }
320
383
 
321
- export async function probeCodexCapacity(options: CodexProbeOptions): Promise<CapacityReport> {
384
+ export async function probeCodexCapacity(
385
+ options: CodexProbeOptions,
386
+ ): Promise<CapacityReport> {
322
387
  let parsed: UnknownRecord | null = null;
323
388
  try {
324
389
  const contents = await (options.readFile ?? readFile)(
@@ -345,7 +410,10 @@ export async function probeCodexCapacity(options: CodexProbeOptions): Promise<Ca
345
410
  );
346
411
  const accountId = nonEmptyText(whoami?.chatgpt_account_id);
347
412
  if (!accountId) throw new Error("account unavailable");
348
- return capacityFromSnapshot(await apiSnapshot(pat, accountId, "pat", options), options);
413
+ return capacityFromSnapshot(
414
+ await apiSnapshot(pat, accountId, "pat", options),
415
+ options,
416
+ );
349
417
  } catch {
350
418
  // Continue to a separately available OAuth credential before using the CLI.
351
419
  }
@@ -1,7 +1,8 @@
1
1
  import { constants } from "node:fs";
2
2
  import { access as fsAccess } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { probeCodexCapacity } from "./codex.js";
4
+ import { codexUnavailableReport, probeCodexCapacity } from "./codex.js";
5
+ import { probeZaiCapacity } from "./zai.js";
5
6
  import type { CapacityReport } from "./types.js";
6
7
 
7
8
  export type { CapacityReport, CapacityWindow } from "./types.js";
@@ -13,6 +14,14 @@ export type CapacityProbeOptions = {
13
14
  probe?: typeof probeCodexCapacity;
14
15
  };
15
16
 
17
+ export type ZaiCapacityOptions = {
18
+ now?: () => Date;
19
+ env?: NodeJS.ProcessEnv;
20
+ readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
21
+ fetch?: typeof globalThis.fetch;
22
+ timeoutMs?: number;
23
+ };
24
+
16
25
  async function canAccess(target: string, mode: number): Promise<boolean> {
17
26
  try {
18
27
  await fsAccess(target, mode);
@@ -46,17 +55,26 @@ export async function getCodexCapacityReport(
46
55
  options: CapacityProbeOptions = {},
47
56
  ): Promise<CapacityReport> {
48
57
  const generatedAt = (options.now?.() ?? new Date()).toISOString();
49
- const installed = await isCodexInstalled(options.path ?? process.env.PATH ?? "", options.access);
58
+ const installed = await isCodexInstalled(
59
+ options.path ?? process.env.PATH ?? "",
60
+ options.access,
61
+ );
50
62
  try {
51
- return await (options.probe ?? probeCodexCapacity)({ installed, checkedAt: generatedAt });
63
+ return await (options.probe ?? probeCodexCapacity)({
64
+ installed,
65
+ checkedAt: generatedAt,
66
+ });
52
67
  } catch {
53
- return {
54
- provider: "codex",
55
- generatedAt,
56
- authenticated: null,
57
- available: "unknown",
58
- windows: [],
59
- creditsRemaining: null,
60
- };
68
+ return codexUnavailableReport(generatedAt);
61
69
  }
62
70
  }
71
+
72
+ export async function getZaiCapacityReport(
73
+ options: ZaiCapacityOptions = {},
74
+ ): Promise<CapacityReport> {
75
+ const { now, ...probeOptions } = options;
76
+ return probeZaiCapacity({
77
+ ...probeOptions,
78
+ checkedAt: (now?.() ?? new Date()).toISOString(),
79
+ });
80
+ }
@@ -1,4 +1,5 @@
1
1
  export type Availability = "yes" | "no" | "unknown";
2
+ export type CapacityLimitType = "TOKENS_LIMIT" | "CREDIT_LIMIT" | "TIME_LIMIT";
2
3
 
3
4
  export interface CapacityWindow {
4
5
  id: string;
@@ -6,9 +7,16 @@ export interface CapacityWindow {
6
7
  durationMinutes: number | null;
7
8
  usedPercent: number | null;
8
9
  resetsAt: string | null;
10
+ limitType?: CapacityLimitType;
11
+ total?: number | null;
12
+ current?: number | null;
13
+ remaining?: number | null;
9
14
  }
10
15
 
11
16
  export interface CapacityReport {
17
+ /** Harness whose quota pool is measured, e.g. "pi" or "codex". */
18
+ harness: string;
19
+ /** Model provider backing the harness, e.g. "zai" or "openai". */
12
20
  provider: string;
13
21
  generatedAt: string;
14
22
  authenticated: boolean | null;
@@ -0,0 +1,237 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type {
5
+ Availability,
6
+ CapacityLimitType,
7
+ CapacityReport,
8
+ CapacityWindow,
9
+ } from "./types.js";
10
+
11
+ const ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
12
+ const MISSING_API_KEY_MESSAGE =
13
+ "z.ai API key not found; set Z_AI_API_KEY or configure Pi provider zai";
14
+ const LIMIT_METADATA = {
15
+ TOKENS_LIMIT: { id: "tokens", label: "Tokens" },
16
+ CREDIT_LIMIT: { id: "credit", label: "Credit" },
17
+ TIME_LIMIT: { id: "time", label: "Time" },
18
+ } as const satisfies Record<CapacityLimitType, { id: string; label: string }>;
19
+ const UNIT_METADATA = new Map<number, { minutes: number; label: string }>([
20
+ [1, { minutes: 24 * 60, label: "day" }],
21
+ [3, { minutes: 60, label: "hour" }],
22
+ [5, { minutes: 1, label: "minute" }],
23
+ [6, { minutes: 7 * 24 * 60, label: "week" }],
24
+ ]);
25
+ type UnknownRecord = Record<string, unknown>;
26
+
27
+ export type ZaiCredentialOptions = {
28
+ env?: NodeJS.ProcessEnv;
29
+ readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
30
+ };
31
+
32
+ export type ZaiProbeOptions = ZaiCredentialOptions & {
33
+ checkedAt: string;
34
+ fetch?: typeof globalThis.fetch;
35
+ timeoutMs?: number;
36
+ };
37
+
38
+ function record(value: unknown): UnknownRecord | null {
39
+ return value !== null && typeof value === "object" && !Array.isArray(value)
40
+ ? (value as UnknownRecord)
41
+ : null;
42
+ }
43
+
44
+ function nonEmptyText(value: unknown): string | null {
45
+ return typeof value === "string" && value.trim() ? value.trim() : null;
46
+ }
47
+
48
+ function optionalNumber(value: unknown, field: string): number | null {
49
+ if (value === undefined || value === null) return null;
50
+ if (typeof value !== "number" || !Number.isFinite(value)) {
51
+ throw new Error(`Invalid z.ai quota ${field}`);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ function clampPercent(value: number): number {
57
+ return Math.max(0, Math.min(100, value));
58
+ }
59
+
60
+ function integer(value: unknown): number | null {
61
+ return typeof value === "number" && Number.isInteger(value) ? value : null;
62
+ }
63
+
64
+ function isCapacityLimitType(value: string): value is CapacityLimitType {
65
+ return Object.hasOwn(LIMIT_METADATA, value);
66
+ }
67
+
68
+ function durationMinutes(
69
+ unit: number,
70
+ number: number,
71
+ type: CapacityLimitType,
72
+ ): number | null {
73
+ if (type === "TIME_LIMIT" && unit === 5 && number === 1) return 30 * 24 * 60;
74
+ const metadata = UNIT_METADATA.get(unit);
75
+ return metadata === undefined || number <= 0
76
+ ? null
77
+ : metadata.minutes * number;
78
+ }
79
+
80
+ function windowLabel(
81
+ type: CapacityLimitType,
82
+ unit: number,
83
+ number: number,
84
+ ): string {
85
+ if (type === "TIME_LIMIT" && unit === 5 && number === 1)
86
+ return "MCP · monthly";
87
+ const kind = LIMIT_METADATA[type].label;
88
+ const unitLabel = UNIT_METADATA.get(unit)?.label;
89
+ if (!unitLabel) return kind;
90
+ return `${kind} · ${number} ${unitLabel}${number === 1 ? "" : "s"}`;
91
+ }
92
+
93
+ function resetTime(value: unknown): string | null {
94
+ const milliseconds = optionalNumber(value, "limit nextResetTime");
95
+ if (milliseconds === null) return null;
96
+ const date = new Date(milliseconds);
97
+ if (Number.isNaN(date.getTime()))
98
+ throw new Error("Invalid z.ai quota limit nextResetTime");
99
+ return date.toISOString();
100
+ }
101
+
102
+ function availability(windows: CapacityWindow[]): Availability {
103
+ if (windows.length === 0) return "unknown";
104
+ if (
105
+ windows.some(
106
+ (window) => window.usedPercent !== null && window.usedPercent < 100,
107
+ )
108
+ ) {
109
+ return "yes";
110
+ }
111
+ return "no";
112
+ }
113
+
114
+ function parseLimit(value: unknown, index: number): CapacityWindow | null {
115
+ const limit = record(value);
116
+ if (!limit) throw new Error("Invalid z.ai quota limit entry");
117
+ const unit = integer(limit.unit);
118
+ const count = integer(limit.number);
119
+ if (typeof limit.type !== "string" || unit === null || count === null) {
120
+ throw new Error("Invalid z.ai quota limit entry");
121
+ }
122
+ const serverPercent = optionalNumber(limit.percentage, "limit percentage");
123
+ if (serverPercent === null)
124
+ throw new Error("Invalid z.ai quota limit percentage");
125
+ if (!isCapacityLimitType(limit.type)) return null;
126
+
127
+ const total = optionalNumber(limit.usage, "limit usage");
128
+ const current = optionalNumber(limit.currentValue, "limit currentValue");
129
+ const remaining = optionalNumber(limit.remaining, "limit remaining");
130
+ let consumed =
131
+ total !== null && remaining !== null ? total - remaining : null;
132
+ if (current !== null) consumed = Math.max(consumed ?? current, current);
133
+
134
+ let usedPercent = serverPercent;
135
+ if (total !== null && total > 0 && consumed !== null) {
136
+ usedPercent = (consumed / total) * 100;
137
+ }
138
+ const metadata = LIMIT_METADATA[limit.type];
139
+
140
+ return {
141
+ id: `zai:${metadata.id}:${index + 1}`,
142
+ label: windowLabel(limit.type, unit, count),
143
+ limitType: limit.type,
144
+ durationMinutes: durationMinutes(unit, count, limit.type),
145
+ usedPercent: clampPercent(usedPercent),
146
+ resetsAt: resetTime(limit.nextResetTime),
147
+ total,
148
+ current,
149
+ remaining,
150
+ };
151
+ }
152
+
153
+ export async function resolveZaiApiKey(
154
+ options: ZaiCredentialOptions = {},
155
+ ): Promise<string> {
156
+ const env = options.env ?? process.env;
157
+ const environmentKey = nonEmptyText(env.Z_AI_API_KEY);
158
+ if (environmentKey) return environmentKey;
159
+
160
+ const authPath = join(env.HOME || homedir(), ".pi", "agent", "auth.json");
161
+ let parsed: UnknownRecord;
162
+ try {
163
+ const contents = await (options.readFile ?? readFile)(authPath, "utf8");
164
+ const root = record(JSON.parse(contents));
165
+ if (!root) throw new Error("invalid");
166
+ parsed = root;
167
+ } catch (error) {
168
+ if (record(error)?.code === "ENOENT") {
169
+ throw new Error(MISSING_API_KEY_MESSAGE);
170
+ }
171
+ throw new Error("z.ai Pi auth file is malformed");
172
+ }
173
+
174
+ const credential = record(parsed.zai);
175
+ if (!credential) {
176
+ throw new Error(MISSING_API_KEY_MESSAGE);
177
+ }
178
+ const key =
179
+ credential.type === "api_key" ? nonEmptyText(credential.key) : null;
180
+ if (!key) throw new Error("z.ai Pi auth file has a malformed zai credential");
181
+ return key;
182
+ }
183
+
184
+ export function parseZaiQuota(raw: unknown, checkedAt: string): CapacityReport {
185
+ const root = record(raw);
186
+ const data = record(root?.data);
187
+ if (
188
+ root?.success !== true ||
189
+ root.code !== 200 ||
190
+ !data ||
191
+ !Array.isArray(data.limits)
192
+ ) {
193
+ throw new Error("Invalid z.ai quota response");
194
+ }
195
+ const windows = data.limits
196
+ .map(parseLimit)
197
+ .filter((window): window is CapacityWindow => window !== null);
198
+ return {
199
+ harness: "pi",
200
+ provider: "zai",
201
+ generatedAt: checkedAt,
202
+ authenticated: true,
203
+ available: availability(windows),
204
+ windows,
205
+ creditsRemaining: null,
206
+ };
207
+ }
208
+
209
+ export async function probeZaiCapacity(
210
+ options: ZaiProbeOptions,
211
+ ): Promise<CapacityReport> {
212
+ const key = await resolveZaiApiKey(options);
213
+ const controller = new AbortController();
214
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5000);
215
+ let response: Response;
216
+ try {
217
+ response = await (options.fetch ?? globalThis.fetch)(ZAI_QUOTA_URL, {
218
+ method: "GET",
219
+ headers: { Authorization: `Bearer ${key}` },
220
+ signal: controller.signal,
221
+ });
222
+ } catch {
223
+ throw new Error("z.ai quota request failed");
224
+ } finally {
225
+ clearTimeout(timer);
226
+ }
227
+ if (response.status !== 200) {
228
+ throw new Error(`z.ai quota request failed: HTTP ${response.status}`);
229
+ }
230
+ let raw: unknown;
231
+ try {
232
+ raw = await response.json();
233
+ } catch {
234
+ throw new Error("z.ai quota response is not valid JSON");
235
+ }
236
+ return parseZaiQuota(raw, options.checkedAt);
237
+ }