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.
- package/package.json +12 -1
- package/src/agent/code-search/cache.mjs +133 -0
- package/src/agent/code-search/chunk-rules.mjs +107 -0
- package/src/agent/code-search/chunker.mjs +125 -0
- package/src/agent/code-search/engine.mjs +109 -0
- package/src/agent/code-search/languages.mjs +25 -0
- package/src/agent/code-search/parser-pool.mjs +29 -0
- package/src/agent/code-search/rerank.mjs +43 -0
- package/src/agent/code-search/retrieval/bm25.mjs +47 -0
- package/src/agent/code-search/retrieval/fusion.mjs +18 -0
- package/src/agent/code-search/retrieval/model2vec.mjs +96 -0
- package/src/agent/code-search/retrieval/safetensors.mjs +49 -0
- package/src/agent/code-search/retrieval/vector.mjs +107 -0
- package/src/agent/code-search/retrieval/wordpiece.mjs +82 -0
- package/src/agent/code-search/scanner.mjs +84 -0
- package/src/agent/code-search/tokenize.mjs +16 -0
- package/src/agent/code-search/tool.mjs +75 -0
- package/src/agent/lifecycle/runner-lifecycle.mjs +16 -0
- package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
- package/src/agent/runner/provider-quota-runtime.mjs +38 -0
- package/src/agent/runner.mjs +14 -14
- package/src/agent/runtime/remote-runner-client.mjs +9 -15
- package/src/agent/runtime/runner-ipc-target.mjs +10 -22
- package/src/agent/runtime/runner-process-client.mjs +101 -24
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/runtime/state/runner-state.mjs +81 -0
- package/src/agent/runtime/ui-event-bridge.mjs +2 -0
- package/src/agent/session/session-options.mjs +2 -1
- package/src/agent/tools.mjs +6 -1
- package/src/cli/args.mjs +14 -3
- package/src/cli/commands/catalog/visible-commands.mjs +5 -0
- package/src/cli/commands/help-command.mjs +1 -7
- package/src/cli/commands/registry/slash-command-registry.mjs +296 -0
- package/src/cli/commands/status-command.mjs +61 -35
- package/src/cli/input/autocomplete.mjs +2 -25
- package/src/cli/repl-loop.mjs +24 -41
- package/src/cli/slash-commands.mjs +19 -185
- package/src/cli/startup/app-runtime.mjs +201 -0
- package/src/cli/startup/configured-command.mjs +9 -0
- package/src/cli/startup/early-command.mjs +29 -0
- package/src/cli/turn/turn-input-preparer.mjs +41 -0
- package/src/context/system-core/base.md +5 -0
- package/src/main.mjs +47 -242
- package/src/provider/quota/codex.mjs +278 -0
- package/src/provider/quota/index.mjs +46 -0
- package/src/provider/quota/transport-observer.mjs +99 -0
- package/src/web-ui/command.mjs +112 -0
- package/src/web-ui/index.html +12 -0
- package/src/web-ui/runtime-host.mjs +188 -0
- package/src/web-ui/server.mjs +140 -0
- package/src/web-ui/session-manager.mjs +111 -0
- package/src/web-ui/src/App.tsx +7 -0
- package/src/web-ui/src/components/AppShell.tsx +48 -0
- package/src/web-ui/src/components/Composer.tsx +47 -0
- package/src/web-ui/src/components/FileExplorer.tsx +46 -0
- package/src/web-ui/src/components/RightSidebar.tsx +115 -0
- package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
- package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
- package/src/web-ui/src/fileTreeAdapter.ts +51 -0
- package/src/web-ui/src/main.tsx +11 -0
- package/src/web-ui/src/mockData.ts +87 -0
- package/src/web-ui/src/model.ts +82 -0
- package/src/web-ui/src/runtime/client.ts +81 -0
- package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
- package/src/web-ui/src/runtime/useWebRuntime.ts +144 -0
- package/src/web-ui/src/styles/shell.css +166 -0
- package/src/web-ui/src/styles/tokens.css +116 -0
- package/src/web-ui/src/timelineAdapter.ts +43 -0
- package/src/web-ui/src/vite-env.d.ts +1 -0
- package/src/web-ui/tsconfig.json +20 -0
- 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,
|
|
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 {
|
|
15
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|