march-cli 0.1.34 → 0.1.36

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 (72) hide show
  1. package/package.json +12 -1
  2. package/src/agent/code-search/cache.mjs +133 -0
  3. package/src/agent/code-search/chunk-rules.mjs +107 -0
  4. package/src/agent/code-search/chunker.mjs +125 -0
  5. package/src/agent/code-search/engine.mjs +109 -0
  6. package/src/agent/code-search/languages.mjs +25 -0
  7. package/src/agent/code-search/parser-pool.mjs +29 -0
  8. package/src/agent/code-search/rerank.mjs +43 -0
  9. package/src/agent/code-search/retrieval/bm25.mjs +47 -0
  10. package/src/agent/code-search/retrieval/fusion.mjs +18 -0
  11. package/src/agent/code-search/retrieval/model2vec.mjs +96 -0
  12. package/src/agent/code-search/retrieval/safetensors.mjs +49 -0
  13. package/src/agent/code-search/retrieval/vector.mjs +107 -0
  14. package/src/agent/code-search/retrieval/wordpiece.mjs +82 -0
  15. package/src/agent/code-search/scanner.mjs +84 -0
  16. package/src/agent/code-search/tokenize.mjs +16 -0
  17. package/src/agent/code-search/tool.mjs +75 -0
  18. package/src/agent/lifecycle/runner-lifecycle.mjs +16 -0
  19. package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
  20. package/src/agent/runner/provider-quota-runtime.mjs +38 -0
  21. package/src/agent/runner.mjs +14 -14
  22. package/src/agent/runtime/remote-runner-client.mjs +9 -15
  23. package/src/agent/runtime/runner-ipc-target.mjs +10 -22
  24. package/src/agent/runtime/runner-process-client.mjs +101 -24
  25. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  26. package/src/agent/runtime/state/runner-state.mjs +81 -0
  27. package/src/agent/runtime/ui-event-bridge.mjs +2 -0
  28. package/src/agent/session/session-options.mjs +2 -1
  29. package/src/agent/tools.mjs +6 -1
  30. package/src/cli/args.mjs +14 -3
  31. package/src/cli/commands/catalog/visible-commands.mjs +5 -0
  32. package/src/cli/commands/help-command.mjs +1 -7
  33. package/src/cli/commands/registry/slash-command-registry.mjs +296 -0
  34. package/src/cli/commands/status-command.mjs +61 -35
  35. package/src/cli/input/autocomplete.mjs +2 -25
  36. package/src/cli/repl-loop.mjs +24 -41
  37. package/src/cli/slash-commands.mjs +19 -185
  38. package/src/cli/startup/app-runtime.mjs +201 -0
  39. package/src/cli/startup/configured-command.mjs +9 -0
  40. package/src/cli/startup/early-command.mjs +29 -0
  41. package/src/cli/turn/turn-input-preparer.mjs +41 -0
  42. package/src/context/system-core/base.md +5 -0
  43. package/src/main.mjs +47 -242
  44. package/src/provider/quota/codex.mjs +278 -0
  45. package/src/provider/quota/index.mjs +46 -0
  46. package/src/provider/quota/transport-observer.mjs +99 -0
  47. package/src/web-ui/command.mjs +112 -0
  48. package/src/web-ui/index.html +12 -0
  49. package/src/web-ui/runtime-host.mjs +188 -0
  50. package/src/web-ui/server.mjs +140 -0
  51. package/src/web-ui/session-manager.mjs +111 -0
  52. package/src/web-ui/src/App.tsx +7 -0
  53. package/src/web-ui/src/components/AppShell.tsx +48 -0
  54. package/src/web-ui/src/components/Composer.tsx +47 -0
  55. package/src/web-ui/src/components/FileExplorer.tsx +46 -0
  56. package/src/web-ui/src/components/RightSidebar.tsx +115 -0
  57. package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
  58. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
  59. package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
  60. package/src/web-ui/src/fileTreeAdapter.ts +51 -0
  61. package/src/web-ui/src/main.tsx +11 -0
  62. package/src/web-ui/src/mockData.ts +87 -0
  63. package/src/web-ui/src/model.ts +82 -0
  64. package/src/web-ui/src/runtime/client.ts +81 -0
  65. package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
  66. package/src/web-ui/src/runtime/useWebRuntime.ts +144 -0
  67. package/src/web-ui/src/styles/shell.css +166 -0
  68. package/src/web-ui/src/styles/tokens.css +116 -0
  69. package/src/web-ui/src/timelineAdapter.ts +43 -0
  70. package/src/web-ui/src/vite-env.d.ts +1 -0
  71. package/src/web-ui/tsconfig.json +20 -0
  72. package/src/web-ui/vite.config.mjs +11 -0
package/src/main.mjs CHANGED
@@ -1,46 +1,18 @@
1
1
  import { homedir } from "node:os";
2
- import { join, resolve, basename, relative } from "node:path";
3
- import { existsSync, mkdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
5
4
  import { parseCliArgs, showHelp } from "./cli/args.mjs";
6
- import { createUI } from "./cli/ui.mjs";
7
- import { createPermissionController, MODE } from "./cli/permissions.mjs";
8
- import { loadKeybindings } from "./cli/input/keybindings.mjs";
9
- import { createInputHistoryStore } from "./cli/input/history-store.mjs";
10
- import { createModeState } from "./cli/input/mode-state.mjs";
11
- import { loadPromptTemplates } from "./cli/input/prompt-templates.mjs";
12
5
  import { runInteractiveRepl, runSingleShotPrompt } from "./cli/repl-loop.mjs";
13
6
  import { closeMarchRuntime } from "./cli/startup/runtime-close.mjs";
14
- import { createStatusLineUpdater } from "./cli/status-line-updater.mjs";
15
- import { wireTuiHandlers } from "./cli/tui/tui-handlers.mjs";
16
- import { createMarchAuthStorage } from "./auth/storage.mjs";
17
- import { runLoginCommand } from "./auth/login-command.mjs";
18
- import { createRuntimeRunner } from "./cli/startup/create-runtime-runner.mjs";
19
- import { createCliShellRuntime } from "./shell/cli-runtime.mjs";
20
- import { MarkdownMemoryStore } from "./memory/markdown-store.mjs";
21
- import { createMarkdownMemoryTools } from "./memory/markdown-tools.mjs";
7
+ import { createCliAppRuntime } from "./cli/startup/app-runtime.mjs";
8
+ import { formatStartupBanner } from "./cli/startup/startup-banner.mjs";
22
9
  import { loadDotEnv } from "./config/dotenv.mjs";
23
10
  import { loadConfig } from "./config/loader.mjs";
24
- import { discoverProjectExtensionPaths } from "./extensions/discovery.mjs";
25
- import { loadProjectLifecycleHookManifests } from "./extensions/lifecycle-manifest.mjs";
26
- import { loadOrCreateProjectId, resumeStartupSession } from "./cli/startup/startup-session.mjs";
27
- import { formatStartupBanner } from "./cli/startup/startup-banner.mjs";
28
- import { initializeMcp } from "./mcp/index.mjs";
29
- import { createWebToolsFromConfig } from "./web/tools.mjs";
30
- import { createModelContextDumper } from "./debug/model-context-dumper.mjs";
31
- import { createLogger, installProcessLogHandlers } from "./debug/logger.mjs";
32
- import { defaultProfilePaths, ensureProfileFiles } from "./context/profiles.mjs";
33
- import { runProviderCommand } from "./provider/command.mjs";
34
- import { runWebSearchConfigCommand } from "./web/config-command.mjs";
35
- import { createDesktopTurnNotifier } from "./notification/desktop-notifier.mjs";
36
11
  import { registerSuperGrokOAuthProvider } from "./supergrok/oauth-provider.mjs";
37
12
  import { installNetworkEnvironment } from "./network/environment.mjs";
38
- import { runMemoryCommand } from "./memory/command.mjs";
39
- import { normalizeRemoteMemorySources } from "./memory/remote/config.mjs";
40
- import { resolveMemoryRoot } from "./memory/root.mjs";
41
- import { runConfiguredCliCommand } from "./cli/startup/configured-command.mjs";
13
+ import { runEarlyCliCommand } from "./cli/startup/early-command.mjs";
42
14
  import { maybeRunGatewayDaemonCommand } from "./cli/startup/gateway-daemon-command.mjs";
43
- import { ensureBrowserDaemon } from "./browser/client/lifecycle.mjs";
15
+
44
16
  export async function run(argv) {
45
17
  const cwd = process.cwd();
46
18
  loadDotEnv(cwd);
@@ -56,239 +28,72 @@ export async function run(argv) {
56
28
  const stateRoot = join(homedir(), ".march");
57
29
  const useRuntimeProcess = process.env.MARCH_RUNTIME_PROCESS !== "0";
58
30
  installNetworkEnvironment(config.network);
59
- if (args.command?.name === "login") {
60
- try {
61
- return await runLoginCommand({
62
- providerId: args.command.args[0] ?? args.provider,
63
- });
64
- } catch (err) {
65
- process.stderr.write(`Error: ${err.message}\n`);
66
- return 1;
67
- }
68
- }
69
- if (args.command?.name === "provider") {
70
- return await runProviderCommand(args);
71
- }
72
- if (args.command?.name === "websearch") {
73
- if (args.providerConfig) return await runWebSearchConfigCommand({ homeDir: homedir() });
74
- process.stderr.write("Usage: march websearch --config\n");
75
- return 1;
76
- }
77
- if (args.command?.name === "memory") {
78
- args.memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
79
- return await runMemoryCommand(args, { homeDir: homedir() });
80
- }
81
- const configuredCommand = await runConfiguredCliCommand(args, { config, cwd, stateRoot });
82
- if (configuredCommand.handled) return configuredCommand.code;
83
- if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
84
- await ensureBrowserDaemon({ stateRoot }).catch(() => {});
85
- const logger = createLogger({ logDir: join(stateRoot, "logs") });
86
- installProcessLogHandlers(logger);
87
- logger.event("process.start", {
88
- cwd,
89
- argv,
90
- version: process.version,
91
- platform: process.platform,
92
- logPath: logger.path,
93
- });
94
31
 
95
- const provider = args.provider ?? config.provider ?? null;
96
- const serviceTier = config.serviceTier ?? null;
97
- const model = args.model ?? config.model ?? null;
98
- const extensionPaths = [
99
- ...discoverProjectExtensionPaths(cwd),
100
- ...args.extensions.map((extensionPath) => resolve(cwd, extensionPath)),
101
- ];
102
- const lifecycleManifests = loadProjectLifecycleHookManifests(cwd);
103
- const keybindingConfig = loadKeybindings(cwd);
104
- const promptTemplateConfig = loadPromptTemplates(cwd);
105
- const authConfig = createMarchAuthStorage({ provider: provider ?? "deepseek", providers: config.providers, cwd });
32
+ const earlyCommand = await runEarlyCliCommand(args, { config, cwd, stateRoot });
33
+ if (earlyCommand.handled) return earlyCommand.code;
106
34
 
107
- if (!authConfig.hasAuth) {
108
- process.stderr.write("Error: no providers configured. Run: march provider --config\n");
109
- return 1;
110
- }
111
-
112
- const projectMarchDir = resolve(cwd, ".march");
113
- if (!existsSync(projectMarchDir)) mkdirSync(projectMarchDir, { recursive: true });
114
- const inputHistoryStore = createInputHistoryStore({ path: join(projectMarchDir, "input-history.json") });
115
- const modeState = createModeState();
116
- const namespace = loadOrCreateProjectId(projectMarchDir);
117
- const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
118
- const profilePaths = defaultProfilePaths();
119
- ensureProfileFiles(profilePaths);
120
- const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
121
- const remoteMemorySources = normalizeRemoteMemorySources(config);
122
- const memoryTools = createMarkdownMemoryTools(memoryStore, { remoteSources: remoteMemorySources });
123
- const currentProject = basename(cwd);
124
- const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
35
+ const app = await createCliAppRuntime({ args, config, cwd, argv, stateRoot, useRuntimeProcess });
36
+ if (!app.ok) return app.code;
125
37
 
126
- const mcpInit = useRuntimeProcess
127
- ? { clientManager: null, mcpTools: [], mcpInjections: [], errors: [] }
128
- : await initializeMcp({ projectDir: cwd });
129
- const { clientManager: mcpClientManager, mcpTools, mcpInjections } = mcpInit;
130
- for (const { server, error } of mcpInit.errors) {
131
- if (args.json) {
132
- // errors will be surfaced in diagnostics via runner status
133
- } else {
134
- process.stderr.write(`[mcp] ${server}: ${error}\n`);
135
- }
136
- }
137
-
138
- const webTools = createWebToolsFromConfig(config);
139
- const turnNotifier = createDesktopTurnNotifier({ enabled: Boolean(config.notifications?.turnEnd), config: config.notifications });
140
- const permissionMode = args.permissionMode ?? MODE.BYPASS;
141
- const permissionController = createPermissionController({ mode: permissionMode });
142
- const usePiSessions = true;
143
- const usePiRuntimeHost = true;
144
- const sessionSource = "pi";
145
- const sessionsRoot = join(projectMarchDir, "sessions");
146
- const sessionState = {
147
- sessionId: args.resume ?? Date.now().toString(36),
148
- sessionDir: null,
149
- };
150
- sessionState.sessionDir = join(sessionsRoot, sessionState.sessionId);
151
- const contextDumpRoot = resolve(projectMarchDir, "context-dumps", sessionState.sessionId);
152
- const modelContextDumper = createModelContextDumper({
153
- enabled: args.dumpContext,
154
- rootDir: contextDumpRoot,
155
- });
156
-
157
- const ui = createUI({
158
- json: args.json,
159
- cwd,
160
- keybindings: keybindingConfig.keybindings,
161
- promptTemplates: promptTemplateConfig.templates,
162
- shellRuntime,
163
- historyStore: inputHistoryStore,
164
- });
165
-
166
- // Esc to abort current turn
167
- let turnRunning = false;
168
- let refreshStatusBar = null;
169
-
170
- const runnerOptions = {
171
- cwd,
172
- modelId: model,
173
- provider,
174
- serviceTier,
175
- providers: config.providers,
38
+ const gatewayDaemonCommand = await maybeRunGatewayDaemonCommand(args, {
176
39
  config,
177
- stateRoot,
178
- memoryRoot,
179
- profilePaths,
180
- namespace,
181
- projectMarchDir,
182
- extensionPaths,
183
- permissionMode,
184
- shellRuntime: Boolean(shellRuntime),
185
- lifecycleHooks: lifecycleManifests.hooks,
186
- lifecycleDiagnostics: lifecycleManifests.diagnostics,
187
- modelContextDumper: { enabled: args.dumpContext, rootDir: contextDumpRoot },
188
- remoteMemorySources,
189
- };
190
- const runner = await createRuntimeRunner({
191
- useRuntimeProcess,
192
- runnerOptions,
193
- ui,
194
- memoryStore,
195
- memoryTools,
196
- shellRuntime,
197
- mcpTools,
198
- mcpInjections,
199
- mcpClientManager,
200
- webTools,
201
- usePiSessions,
202
- usePiRuntimeHost,
203
- authStorage: authConfig.authStorage,
204
- permissionController,
205
- modelContextDumper,
206
- turnNotifier,
207
- logger,
208
- refreshStatusBar: (...args) => refreshStatusBar?.(...args),
209
- });
210
-
211
- refreshStatusBar = createStatusLineUpdater({
212
- ui,
213
- runner,
214
- sessionState,
215
- sessionSource,
216
- getMode: () => modeState.get(),
217
- });
218
- const initialContextTokens = typeof runner.estimateContextTokens === "function"
219
- ? await runner.estimateContextTokens("")
220
- : null;
221
- refreshStatusBar(initialContextTokens ? { contextTokens: initialContextTokens } : undefined);
222
-
223
- wireTuiHandlers({
224
- ui,
225
- runner,
226
- sessionState,
227
- projectMarchDir,
228
- refreshStatusBar,
229
- isTurnRunning: () => turnRunning,
230
- modeState,
231
- });
232
-
233
- const startupResume = await resumeStartupSession({
234
- resumeId: args.resume,
235
- runner,
236
- sessionState,
237
- projectMarchDir,
238
- ui,
40
+ cwd,
41
+ runner: app.runner,
42
+ currentProject: app.currentProject,
43
+ memoryStore: app.memoryStore,
44
+ ui: app.ui,
45
+ logger: app.logger,
239
46
  });
240
- refreshStatusBar();
241
- const gatewayDaemonCommand = await maybeRunGatewayDaemonCommand(args, { config, cwd, runner, currentProject, memoryStore, ui, logger });
242
47
  if (gatewayDaemonCommand.handled) return gatewayDaemonCommand.code;
243
48
 
244
49
  if (args.prompt) {
245
- turnRunning = true;
50
+ app.setTurnRunning(true);
246
51
  try {
247
52
  await runSingleShotPrompt({
248
53
  prompt: args.prompt,
249
- runner,
250
- memoryStore,
251
- currentProject,
252
- ui,
253
- sessionState,
254
- refreshStatusBar,
255
- modeState,
54
+ runner: app.runner,
55
+ memoryStore: app.memoryStore,
56
+ currentProject: app.currentProject,
57
+ ui: app.ui,
58
+ sessionState: app.sessionState,
59
+ refreshStatusBar: app.refreshStatusBar,
60
+ modeState: app.modeState,
256
61
  });
257
62
  } finally {
258
- turnRunning = false;
259
- await closeMarchRuntime({ runner, memoryStore, ui, logger, blankLine: true });
63
+ app.setTurnRunning(false);
64
+ await closeMarchRuntime({ runner: app.runner, memoryStore: app.memoryStore, ui: app.ui, logger: app.logger, blankLine: true });
260
65
  }
261
- logger.event("process.exit", { code: 0 });
66
+ app.logger.event("process.exit", { code: 0 });
262
67
  return 0;
263
68
  }
264
69
 
265
- const dumpContextPath = args.dumpContext ? relative(cwd, contextDumpRoot) : null;
266
- if (startupResume.transcriptTurns?.length > 0) ui.restoreTranscript?.(startupResume.transcriptTurns);
267
- for (const line of formatStartupBanner({ cwd, modelId: runner.engine.modelId, thinkingLevel: runner.engine.thinkingLevel, mode: modeState.get(), dumpContextPath })) ui.writeln(line);
70
+ const dumpContextPath = args.dumpContext ? relative(cwd, app.contextDumpRoot) : null;
71
+ if (app.startupResume.transcriptTurns?.length > 0) app.ui.restoreTranscript?.(app.startupResume.transcriptTurns);
72
+ for (const line of formatStartupBanner({ cwd, modelId: app.runner.engine.modelId, thinkingLevel: app.runner.engine.thinkingLevel, mode: app.modeState.get(), dumpContextPath })) app.ui.writeln(line);
268
73
  try {
269
74
  await runInteractiveRepl({
270
75
  cwd,
271
76
  args,
272
- ui,
273
- runner,
274
- memoryStore,
275
- currentProject,
276
- sessionState,
277
- sessionsRoot,
278
- projectMarchDir,
279
- sessionSource,
280
- extensionPaths,
281
- keybindingConfig,
282
- promptTemplateConfig,
283
- renderStartupBanner: () => formatStartupBanner({ cwd, modelId: runner.engine.modelId, thinkingLevel: runner.engine.thinkingLevel, mode: modeState.get(), dumpContextPath }),
284
- refreshStatusBar,
285
- setTurnRunning: (value) => { turnRunning = value; },
286
- modeState,
77
+ ui: app.ui,
78
+ runner: app.runner,
79
+ memoryStore: app.memoryStore,
80
+ currentProject: app.currentProject,
81
+ sessionState: app.sessionState,
82
+ sessionsRoot: app.sessionsRoot,
83
+ projectMarchDir: app.projectMarchDir,
84
+ sessionSource: app.sessionSource,
85
+ extensionPaths: app.extensionPaths,
86
+ keybindingConfig: app.keybindingConfig,
87
+ promptTemplateConfig: app.promptTemplateConfig,
88
+ renderStartupBanner: () => formatStartupBanner({ cwd, modelId: app.runner.engine.modelId, thinkingLevel: app.runner.engine.thinkingLevel, mode: app.modeState.get(), dumpContextPath }),
89
+ refreshStatusBar: app.refreshStatusBar,
90
+ setTurnRunning: app.setTurnRunning,
91
+ modeState: app.modeState,
287
92
  });
288
93
  } finally {
289
- await closeMarchRuntime({ runner, memoryStore, ui, logger });
94
+ await closeMarchRuntime({ runner: app.runner, memoryStore: app.memoryStore, ui: app.ui, logger: app.logger });
290
95
  }
291
- logger.event("process.exit", { code: 0 });
96
+ app.logger.event("process.exit", { code: 0 });
292
97
  return 0;
293
98
  }
294
99
 
@@ -0,0 +1,278 @@
1
+ import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
2
+
3
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
4
+ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
5
+
6
+ export async function fetchOpenAICodexQuota({ authStorage, model, fetchImpl = fetch, now = new Date() } = {}) {
7
+ const token = await resolveCodexAccessToken(authStorage);
8
+ const accountId = resolveCodexAccountId(authStorage, token);
9
+ const response = await fetchImpl(CODEX_USAGE_URL, {
10
+ method: "GET",
11
+ headers: buildCodexUsageHeaders(token, accountId),
12
+ });
13
+ if (!response.ok) {
14
+ const text = await response.text().catch(() => "");
15
+ throw new Error(`Codex quota request failed (${response.status}): ${text || response.statusText}`);
16
+ }
17
+ const payload = await response.json();
18
+ return normalizeCodexQuotaPayload(payload, { model, capturedAt: now.toISOString() });
19
+ }
20
+
21
+ export function normalizeCodexQuotaPayload(payload, { model = null, capturedAt = new Date().toISOString() } = {}) {
22
+ const snapshots = Array.isArray(payload) ? payload : snapshotsFromCodexUsagePayload(payload);
23
+ return normalizeCodexQuotaSnapshots(snapshots, { model, capturedAt });
24
+ }
25
+
26
+ export function normalizeCodexQuotaSnapshots(snapshots, { model = null, capturedAt = new Date().toISOString() } = {}) {
27
+ const limits = snapshots.map(normalizeSnapshot).filter((limit) => limit.windows.length > 0);
28
+ if (limits.length === 0) return null;
29
+ return {
30
+ providerId: "openai-codex",
31
+ modelId: model?.id ?? null,
32
+ label: "GPT usage",
33
+ planType: firstNonEmpty(snapshots.map((snapshot) => readField(snapshot, "planType", "plan_type"))),
34
+ capturedAt,
35
+ limits,
36
+ };
37
+ }
38
+
39
+ export function parseOpenAICodexQuotaHeaders(headers, { model = null, capturedAt = new Date().toISOString() } = {}) {
40
+ const normalized = normalizeHeaders(headers);
41
+ const limitIds = new Set(["codex"]);
42
+ for (const name of Object.keys(normalized)) {
43
+ const prefix = name.endsWith("-primary-used-percent") ? name.slice(2, -"-primary-used-percent".length) : null;
44
+ if (prefix) limitIds.add(prefix.replaceAll("-", "_"));
45
+ }
46
+ const snapshots = [...limitIds]
47
+ .map((limitId) => snapshotFromHeaders(normalized, limitId))
48
+ .filter((snapshot) => snapshot.primary || snapshot.secondary || snapshot.credits);
49
+ return normalizeCodexQuotaSnapshots(snapshots, { model, capturedAt });
50
+ }
51
+
52
+ export function parseOpenAICodexQuotaEvent(payload, { model = null, capturedAt = new Date().toISOString() } = {}) {
53
+ const event = typeof payload === "string" ? parseJson(payload) : payload;
54
+ if (!event || event.type !== "codex.rate_limits") return null;
55
+ const snapshot = {
56
+ limitId: readField(event, "metered_limit_name", "limit_name") ?? "codex",
57
+ limitName: null,
58
+ primary: mapWindow(readField(readField(event, "rate_limits", "rateLimits"), "primary")),
59
+ secondary: mapWindow(readField(readField(event, "rate_limits", "rateLimits"), "secondary")),
60
+ credits: readField(event, "credits") ?? null,
61
+ planType: readField(event, "plan_type", "planType") ?? null,
62
+ rateLimitReachedType: null,
63
+ };
64
+ return normalizeCodexQuotaSnapshots([snapshot], { model, capturedAt });
65
+ }
66
+
67
+ async function resolveCodexAccessToken(authStorage) {
68
+ const token = await authStorage?.getApiKey?.("openai-codex", { includeFallback: false });
69
+ if (token) return token;
70
+ const credentials = authStorage?.get?.("openai-codex");
71
+ if (!credentials) throw new Error("OpenAI Codex not authenticated. Run: march login openai-codex");
72
+ const provider = getOAuthProvider("openai-codex");
73
+ if (!provider) throw new Error("OpenAI Codex OAuth provider is not available");
74
+ return provider.getApiKey(credentials);
75
+ }
76
+
77
+ function resolveCodexAccountId(authStorage, token) {
78
+ const credentials = authStorage?.get?.("openai-codex");
79
+ return credentials?.accountId ?? credentials?.chatgpt_account_id ?? extractAccountId(token);
80
+ }
81
+
82
+ function extractAccountId(token) {
83
+ try {
84
+ const [, payload] = String(token).split(".");
85
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
86
+ const accountId = parsed?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
87
+ if (typeof accountId === "string" && accountId) return accountId;
88
+ } catch {}
89
+ throw new Error("Failed to extract Codex account ID from token");
90
+ }
91
+
92
+ function buildCodexUsageHeaders(token, accountId) {
93
+ return {
94
+ authorization: `Bearer ${token}`,
95
+ "chatgpt-account-id": accountId,
96
+ originator: "march",
97
+ "user-agent": "march-cli",
98
+ accept: "application/json",
99
+ };
100
+ }
101
+
102
+ function snapshotsFromCodexUsagePayload(payload) {
103
+ if (!payload || typeof payload !== "object") return [];
104
+ const planType = readField(payload, "planType", "plan_type") ?? null;
105
+ const reached = readField(payload, "rateLimitReachedType", "rate_limit_reached_type") ?? null;
106
+ const snapshots = [makeSnapshot({
107
+ limitId: "codex",
108
+ limitName: null,
109
+ rateLimit: unwrap(readField(payload, "rateLimit", "rate_limit")),
110
+ credits: unwrap(readField(payload, "credits")),
111
+ planType,
112
+ rateLimitReachedType: unwrap(reached)?.kind ?? reached,
113
+ })];
114
+ const additional = readField(payload, "additionalRateLimits", "additional_rate_limits");
115
+ if (Array.isArray(additional)) {
116
+ for (const item of additional) {
117
+ snapshots.push(makeSnapshot({
118
+ limitId: readField(item, "meteredFeature", "metered_feature"),
119
+ limitName: readField(item, "limitName", "limit_name"),
120
+ rateLimit: unwrap(readField(item, "rateLimit", "rate_limit")),
121
+ credits: null,
122
+ planType,
123
+ rateLimitReachedType: null,
124
+ }));
125
+ }
126
+ }
127
+ return snapshots;
128
+ }
129
+
130
+ function makeSnapshot({ limitId, limitName, rateLimit, credits, planType, rateLimitReachedType }) {
131
+ return {
132
+ limitId: limitId ?? null,
133
+ limitName: limitName ?? null,
134
+ primary: mapWindow(readField(rateLimit, "primary", "primaryWindow", "primary_window")),
135
+ secondary: mapWindow(readField(rateLimit, "secondary", "secondaryWindow", "secondary_window")),
136
+ credits: credits ?? null,
137
+ planType,
138
+ rateLimitReachedType,
139
+ };
140
+ }
141
+
142
+ function snapshotFromHeaders(headers, limitId) {
143
+ const headerPrefix = `x-${limitId.replaceAll("_", "-")}`;
144
+ return {
145
+ limitId,
146
+ limitName: readHeader(headers, `${headerPrefix}-limit-name`),
147
+ primary: windowFromHeaders(headers, headerPrefix, "primary"),
148
+ secondary: windowFromHeaders(headers, headerPrefix, "secondary"),
149
+ credits: limitId === "codex" ? creditsFromHeaders(headers) : null,
150
+ planType: null,
151
+ rateLimitReachedType: null,
152
+ };
153
+ }
154
+
155
+ function windowFromHeaders(headers, prefix, windowId) {
156
+ const usedPercent = readHeader(headers, `${prefix}-${windowId}-used-percent`);
157
+ if (usedPercent === undefined) return null;
158
+ return {
159
+ usedPercent,
160
+ windowDurationMins: readHeader(headers, `${prefix}-${windowId}-window-minutes`),
161
+ resetsAt: readHeader(headers, `${prefix}-${windowId}-reset-at`),
162
+ };
163
+ }
164
+
165
+ function creditsFromHeaders(headers) {
166
+ const hasCredits = parseHeaderBool(readHeader(headers, "x-codex-credits-has-credits"));
167
+ const unlimited = parseHeaderBool(readHeader(headers, "x-codex-credits-unlimited"));
168
+ if (hasCredits === null || unlimited === null) return null;
169
+ return { hasCredits, unlimited, balance: readHeader(headers, "x-codex-credits-balance") ?? null };
170
+ }
171
+
172
+ function normalizeSnapshot(snapshot) {
173
+ const id = readField(snapshot, "limitId", "limit_id") ?? "quota";
174
+ return {
175
+ id,
176
+ name: readField(snapshot, "limitName", "limit_name") ?? id,
177
+ windows: [
178
+ normalizeWindow("primary", readField(snapshot, "primary")),
179
+ normalizeWindow("secondary", readField(snapshot, "secondary")),
180
+ ].filter(Boolean),
181
+ rateLimitReachedType: readField(snapshot, "rateLimitReachedType", "rate_limit_reached_type") ?? null,
182
+ };
183
+ }
184
+
185
+ function normalizeWindow(id, window) {
186
+ const normalized = mapWindow(window);
187
+ if (!normalized) return null;
188
+ const label = formatWindowLabel(normalized.windowDurationMins, id);
189
+ return {
190
+ id,
191
+ label,
192
+ usedPercent: normalized.usedPercent,
193
+ remainingPercent: Math.max(0, 100 - normalized.usedPercent),
194
+ windowDurationMins: normalized.windowDurationMins,
195
+ resetsAt: normalized.resetsAt,
196
+ };
197
+ }
198
+
199
+ function mapWindow(window) {
200
+ const unwrapped = unwrap(window);
201
+ if (!unwrapped || typeof unwrapped !== "object") return null;
202
+ const rawUsed = readField(unwrapped, "usedPercent", "used_percent");
203
+ const usedPercent = Number(rawUsed);
204
+ if (!Number.isFinite(usedPercent)) return null;
205
+ const rawMinutes = readField(unwrapped, "windowDurationMins", "window_minutes", "windowDurationMinutes");
206
+ const rawSeconds = readField(unwrapped, "limitWindowSeconds", "limit_window_seconds");
207
+ return {
208
+ usedPercent,
209
+ windowDurationMins: normalizeWindowMinutes(rawMinutes, rawSeconds),
210
+ resetsAt: normalizeResetTime(readField(unwrapped, "resetsAt", "resets_at", "resetAt", "reset_at")),
211
+ };
212
+ }
213
+
214
+ function normalizeWindowMinutes(minutes, seconds) {
215
+ const minuteValue = Number(minutes);
216
+ if (Number.isFinite(minuteValue) && minuteValue > 0) return minuteValue;
217
+ const secondValue = Number(seconds);
218
+ return Number.isFinite(secondValue) && secondValue > 0 ? Math.round(secondValue / 60) : null;
219
+ }
220
+
221
+ function normalizeResetTime(value) {
222
+ const numeric = Number(value);
223
+ if (Number.isFinite(numeric) && numeric > 0) {
224
+ const millis = numeric < 10_000_000_000 ? numeric * 1000 : numeric;
225
+ return new Date(millis).toISOString();
226
+ }
227
+ const parsed = Date.parse(String(value));
228
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
229
+ }
230
+
231
+ function formatWindowLabel(minutes, fallback) {
232
+ if (!Number.isFinite(minutes) || minutes <= 0) return fallback;
233
+ if (minutes % (60 * 24 * 7) === 0) return "weekly";
234
+ if (minutes % 60 === 0) return `${minutes / 60}h`;
235
+ return `${minutes}m`;
236
+ }
237
+
238
+ function unwrap(value) {
239
+ return Array.isArray(value) && value.length === 1 ? value[0] : value;
240
+ }
241
+
242
+ function readField(object, ...keys) {
243
+ if (!object || typeof object !== "object") return undefined;
244
+ for (const key of keys) if (Object.hasOwn(object, key)) return object[key];
245
+ return undefined;
246
+ }
247
+
248
+ function normalizeHeaders(headers) {
249
+ if (!headers) return {};
250
+ if (typeof headers.entries === "function") {
251
+ return Object.fromEntries([...headers.entries()].map(([key, value]) => [key.toLowerCase(), String(value)]));
252
+ }
253
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), String(value)]));
254
+ }
255
+
256
+ function readHeader(headers, name) {
257
+ const value = headers[name.toLowerCase()];
258
+ return value === undefined || value === "" ? undefined : value;
259
+ }
260
+
261
+ function parseHeaderBool(value) {
262
+ if (value === undefined) return null;
263
+ if (value === "1" || value.toLowerCase() === "true") return true;
264
+ if (value === "0" || value.toLowerCase() === "false") return false;
265
+ return null;
266
+ }
267
+
268
+ function parseJson(value) {
269
+ try {
270
+ return JSON.parse(value);
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ function firstNonEmpty(values) {
277
+ return values.find((value) => value !== null && value !== undefined && value !== "") ?? null;
278
+ }
@@ -0,0 +1,46 @@
1
+ import { fetchOpenAICodexQuota, parseOpenAICodexQuotaEvent, parseOpenAICodexQuotaHeaders } from "./codex.mjs";
2
+
3
+ const quotaAdapters = new Map([
4
+ ["openai-codex", {
5
+ refresh: fetchOpenAICodexQuota,
6
+ observeHeaders: parseOpenAICodexQuotaHeaders,
7
+ observeEvent: parseOpenAICodexQuotaEvent,
8
+ }],
9
+ ]);
10
+
11
+ export function supportsProviderQuota(providerId) {
12
+ return quotaAdapters.has(providerId);
13
+ }
14
+
15
+ export async function getProviderQuotaSnapshot({ providerId, model, authStorage, fetchImpl, now } = {}) {
16
+ const adapter = quotaAdapters.get(providerId);
17
+ if (!adapter) return null;
18
+ return adapter.refresh({ authStorage, model, fetchImpl, now });
19
+ }
20
+
21
+ export function observeProviderQuotaHeaders({ providerId, headers, model, capturedAt } = {}) {
22
+ const adapter = quotaAdapters.get(providerId);
23
+ return adapter?.observeHeaders?.(headers, { model, capturedAt }) ?? null;
24
+ }
25
+
26
+ export function observeProviderQuotaEvent({ providerId, payload, model, capturedAt } = {}) {
27
+ const adapter = quotaAdapters.get(providerId);
28
+ return adapter?.observeEvent?.(payload, { model, capturedAt }) ?? null;
29
+ }
30
+
31
+ export function createProviderQuotaService({ authStorage, fetchImpl = fetch, now = () => new Date() } = {}) {
32
+ return {
33
+ supports(providerId) {
34
+ return supportsProviderQuota(providerId);
35
+ },
36
+ refresh(model) {
37
+ return getProviderQuotaSnapshot({ providerId: model?.provider, model, authStorage, fetchImpl, now: now() });
38
+ },
39
+ observeHeaders(headers, model) {
40
+ return observeProviderQuotaHeaders({ providerId: model?.provider, headers, model, capturedAt: now().toISOString() });
41
+ },
42
+ observeEvent(payload, model) {
43
+ return observeProviderQuotaEvent({ providerId: model?.provider, payload, model, capturedAt: now().toISOString() });
44
+ },
45
+ };
46
+ }