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
@@ -0,0 +1,99 @@
1
+ const FETCH_INSTALLED = Symbol.for("march.providerQuota.fetchObserverInstalled");
2
+ const WEBSOCKET_INSTALLED = Symbol.for("march.providerQuota.websocketObserverInstalled");
3
+ const LISTENERS = Symbol.for("march.providerQuota.transportListeners");
4
+
5
+ export function installProviderQuotaTransportObserver() {
6
+ installFetchObserver();
7
+ installWebSocketObserver();
8
+ }
9
+
10
+ export function subscribeProviderQuotaTransport(listener) {
11
+ const listeners = ensureListeners();
12
+ listeners.add(listener);
13
+ return () => listeners.delete(listener);
14
+ }
15
+
16
+ function installFetchObserver() {
17
+ if (globalThis[FETCH_INSTALLED]) return;
18
+ const originalFetch = globalThis.fetch;
19
+ if (typeof originalFetch !== "function") return;
20
+ globalThis.fetch = async function marchProviderQuotaFetch(input, init = {}) {
21
+ const response = await originalFetch.call(this, input, init);
22
+ if (isCodexResponsesHttpRequest(input, init)) {
23
+ notifyTransportListeners({ providerId: "openai-codex", source: "headers", headers: response.headers });
24
+ }
25
+ return response;
26
+ };
27
+ globalThis[FETCH_INSTALLED] = true;
28
+ }
29
+
30
+ function installWebSocketObserver() {
31
+ if (globalThis[WEBSOCKET_INSTALLED]) return;
32
+ const OriginalWebSocket = globalThis.WebSocket;
33
+ if (typeof OriginalWebSocket !== "function") return;
34
+
35
+ class MarchProviderQuotaWebSocket extends OriginalWebSocket {
36
+ constructor(url, protocolsOrOptions, maybeOptions) {
37
+ if (!isCodexResponsesWebSocketUrl(url)) {
38
+ return new OriginalWebSocket(url, protocolsOrOptions, maybeOptions);
39
+ }
40
+ super(url, protocolsOrOptions, maybeOptions);
41
+ this.addEventListener?.("message", async (event) => {
42
+ const payload = await decodeWebSocketData(event?.data).catch(() => null);
43
+ if (payload?.includes?.('"codex.rate_limits"')) {
44
+ notifyTransportListeners({ providerId: "openai-codex", source: "event", payload });
45
+ }
46
+ });
47
+ }
48
+ }
49
+ copyReadyStateConstants(MarchProviderQuotaWebSocket, OriginalWebSocket);
50
+ globalThis.WebSocket = MarchProviderQuotaWebSocket;
51
+ globalThis[WEBSOCKET_INSTALLED] = true;
52
+ }
53
+
54
+ function ensureListeners() {
55
+ globalThis[LISTENERS] = globalThis[LISTENERS] ?? new Set();
56
+ return globalThis[LISTENERS];
57
+ }
58
+
59
+ function notifyTransportListeners(event) {
60
+ for (const listener of [...ensureListeners()]) {
61
+ try {
62
+ listener(event);
63
+ } catch {}
64
+ }
65
+ }
66
+
67
+ function isCodexResponsesHttpRequest(input, init) {
68
+ const url = getRequestUrl(input);
69
+ if (!url || !url.includes("/codex/responses")) return false;
70
+ const method = init?.method ?? input?.method ?? "GET";
71
+ return String(method).toUpperCase() === "POST";
72
+ }
73
+
74
+ function isCodexResponsesWebSocketUrl(url) {
75
+ const raw = getRequestUrl(url);
76
+ return Boolean(raw?.includes("/codex/responses") && (raw.startsWith("ws://") || raw.startsWith("wss://")));
77
+ }
78
+
79
+ function getRequestUrl(input) {
80
+ if (typeof input === "string") return input;
81
+ if (input instanceof URL) return input.toString();
82
+ if (input && typeof input.url === "string") return input.url;
83
+ return "";
84
+ }
85
+
86
+ async function decodeWebSocketData(data) {
87
+ if (typeof data === "string") return data;
88
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(data));
89
+ if (ArrayBuffer.isView(data)) return new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
90
+ if (data && typeof data.arrayBuffer === "function") return new TextDecoder().decode(new Uint8Array(await data.arrayBuffer()));
91
+ return null;
92
+ }
93
+
94
+ function copyReadyStateConstants(Target, OriginalWebSocket) {
95
+ for (const key of ["CONNECTING", "OPEN", "CLOSING", "CLOSED"]) {
96
+ const value = OriginalWebSocket[key];
97
+ if (typeof value === "number") Object.defineProperty(Target, key, { value, enumerable: true });
98
+ }
99
+ }
@@ -0,0 +1,112 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { createWebUiServer } from "./server.mjs";
4
+ import { createWebSessionManager, resolveWorkspace } from "./session-manager.mjs";
5
+
6
+ const DEFAULT_HOST = "127.0.0.1";
7
+ const DEFAULT_PORT = 4174;
8
+
9
+ export async function runWebUiCommand(args, { config, cwd, stateRoot, useRuntimeProcess = true } = {}) {
10
+ const host = args.host ?? DEFAULT_HOST;
11
+ assertLoopbackHost(host);
12
+ const port = Number.parseInt(args.port ?? "", 10) || DEFAULT_PORT;
13
+ const runtime = createWebSessionManager({ args, config, launchCwd: cwd, stateRoot, useRuntimeProcess });
14
+ const initialWorkspace = resolveInitialWorkspace(args, cwd);
15
+ if (initialWorkspace) await runtime.createSession(initialWorkspace);
16
+
17
+ if (args.dev) return runWebUiDevCommand({ args, host, port, runtime, initialWorkspace });
18
+ assertWebBuildReady();
19
+ const server = createWebUiServer({ runtime });
20
+ await listen(server, port, host);
21
+ process.stdout.write(`March Web running at http://${host}:${port}\n`);
22
+ if (initialWorkspace) process.stdout.write(`Workspace: ${initialWorkspace}\n`);
23
+ await waitForShutdown({ servers: [server], runtime });
24
+ return 0;
25
+ }
26
+
27
+ export function resolveInitialWorkspace(args, launchCwd) {
28
+ const positional = args.command?.args ?? [];
29
+ if (positional.length > 1) throw new Error("Usage: march web [workspace] [--host <host>] [--port <port>]");
30
+ if (args.workspace && positional.length > 0) throw new Error("Use either march web <workspace> or --workspace <path>, not both");
31
+ const requested = args.workspace ?? positional[0];
32
+ return requested ? resolveWorkspace(requested, launchCwd) : null;
33
+ }
34
+
35
+ function assertLoopbackHost(host) {
36
+ if (["127.0.0.1", "localhost", "::1"].includes(host)) return;
37
+ throw new Error("march web only exposes local filesystem APIs on 127.0.0.1/localhost");
38
+ }
39
+
40
+ function assertWebBuildReady() {
41
+ if (existsSync(new URL("./dist/index.html", import.meta.url))) return;
42
+ throw new Error("Web UI build not found. Run: npm run web:build or use march web --dev");
43
+ }
44
+
45
+ async function runWebUiDevCommand({ args, host, port, runtime, initialWorkspace }) {
46
+ const apiPort = Number.parseInt(args.apiPort ?? "", 10) || port + 1;
47
+ const apiServer = createWebUiServer({ runtime });
48
+ let apiStarted = false;
49
+ try {
50
+ await listen(apiServer, apiPort, host);
51
+ apiStarted = true;
52
+ const vite = await createViteDevServer({ host, port, apiPort });
53
+ process.stdout.write(`March Web dev running at http://${host}:${port}\n`);
54
+ process.stdout.write(`March Web API running at http://${host}:${apiPort}\n`);
55
+ if (initialWorkspace) process.stdout.write(`Workspace: ${initialWorkspace}\n`);
56
+ await waitForShutdown({ servers: [apiServer], runtime, vite });
57
+ return 0;
58
+ } catch (err) {
59
+ if (apiStarted) await closeServer(apiServer);
60
+ await runtime.dispose?.();
61
+ throw err;
62
+ }
63
+ }
64
+
65
+ async function createViteDevServer({ host, port, apiPort }) {
66
+ let createServer;
67
+ try {
68
+ ({ createServer } = await import("vite"));
69
+ } catch {
70
+ throw new Error("Vite is required for march web --dev. Run npm install in the March repo.");
71
+ }
72
+ const vite = await createServer({
73
+ configFile: fileURLToPath(new URL("./vite.config.mjs", import.meta.url)),
74
+ server: {
75
+ host,
76
+ port,
77
+ strictPort: true,
78
+ proxy: { "/api": `http://${host}:${apiPort}` },
79
+ },
80
+ });
81
+ await vite.listen();
82
+ return vite;
83
+ }
84
+
85
+ function listen(server, port, host) {
86
+ return new Promise((resolve, reject) => {
87
+ server.once("error", reject);
88
+ server.listen(port, host, () => {
89
+ server.off("error", reject);
90
+ resolve();
91
+ });
92
+ });
93
+ }
94
+
95
+ function closeServer(server) {
96
+ return new Promise((resolve) => server.close(resolve));
97
+ }
98
+
99
+ function waitForShutdown({ servers, runtime, vite = null }) {
100
+ return new Promise((resolve) => {
101
+ const shutdown = async () => {
102
+ process.off("SIGINT", shutdown);
103
+ process.off("SIGTERM", shutdown);
104
+ if (vite) await vite.close();
105
+ await Promise.all(servers.map(closeServer));
106
+ await runtime.dispose?.();
107
+ resolve();
108
+ };
109
+ process.once("SIGINT", shutdown);
110
+ process.once("SIGTERM", shutdown);
111
+ });
112
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <title>March Web</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,188 @@
1
+ import { homedir } from "node:os";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
4
+ import { createMarchAuthStorage } from "../auth/storage.mjs";
5
+ import { createRuntimeRunner } from "../cli/startup/create-runtime-runner.mjs";
6
+ import { createPermissionController, MODE } from "../cli/permissions.mjs";
7
+ import { createCliShellRuntime } from "../shell/cli-runtime.mjs";
8
+ import { MarkdownMemoryStore } from "../memory/markdown-store.mjs";
9
+ import { createMarkdownMemoryTools } from "../memory/markdown-tools.mjs";
10
+ import { resolveMemoryRoot } from "../memory/root.mjs";
11
+ import { defaultProfilePaths, ensureProfileFiles } from "../context/profiles.mjs";
12
+ import { loadOrCreateProjectId } from "../cli/startup/startup-session.mjs";
13
+ import { createWebToolsFromConfig } from "../web/tools.mjs";
14
+ import { createLogger, installProcessLogHandlers } from "../debug/logger.mjs";
15
+ import { createModelContextDumper } from "../debug/model-context-dumper.mjs";
16
+ import { createDesktopTurnNotifier } from "../notification/desktop-notifier.mjs";
17
+ import { discoverProjectExtensionPaths } from "../extensions/discovery.mjs";
18
+ import { loadProjectLifecycleHookManifests } from "../extensions/lifecycle-manifest.mjs";
19
+ import { normalizeRemoteMemorySources } from "../memory/remote/config.mjs";
20
+ import { prepareTurnInput } from "../cli/turn/turn-input-preparer.mjs";
21
+
22
+ const MAX_WORKSPACE_DEPTH = 3;
23
+ const MAX_WORKSPACE_ENTRIES = 200;
24
+
25
+ export async function createWebRuntimeHost({ args, config, cwd, stateRoot, useRuntimeProcess = true } = {}) {
26
+ stateRoot ??= join(homedir(), ".march");
27
+ if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
28
+ const logger = createLogger({ logDir: join(stateRoot, "logs") });
29
+ installProcessLogHandlers(logger);
30
+ const provider = args.provider ?? config.provider ?? null;
31
+ const model = args.model ?? config.model ?? null;
32
+ const authConfig = createMarchAuthStorage({ provider: provider ?? "deepseek", providers: config.providers, cwd });
33
+ if (!authConfig.hasAuth) throw new Error("No providers configured. Run: march provider --config");
34
+
35
+ const projectMarchDir = resolve(cwd, ".march");
36
+ if (!existsSync(projectMarchDir)) mkdirSync(projectMarchDir, { recursive: true });
37
+ const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
38
+ const profilePaths = defaultProfilePaths();
39
+ ensureProfileFiles(profilePaths);
40
+
41
+ const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
42
+ const remoteMemorySources = normalizeRemoteMemorySources(config);
43
+ const memoryTools = createMarkdownMemoryTools(memoryStore, { remoteSources: remoteMemorySources });
44
+ const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
45
+ const extensionPaths = discoverProjectExtensionPaths(cwd);
46
+ const lifecycleManifests = loadProjectLifecycleHookManifests(cwd);
47
+ const contextDumpRoot = resolve(projectMarchDir, "context-dumps", Date.now().toString(36));
48
+ const modelContextDumper = createModelContextDumper({ enabled: args.dumpContext, rootDir: contextDumpRoot });
49
+ const permissionController = createPermissionController({ mode: args.permissionMode ?? MODE.BYPASS });
50
+ const turnNotifier = createDesktopTurnNotifier({ enabled: Boolean(config.notifications?.turnEnd), config: config.notifications });
51
+ const ui = createHeadlessWebUi();
52
+ const currentProject = basename(cwd);
53
+ const namespace = loadOrCreateProjectId(projectMarchDir);
54
+ const runnerOptions = {
55
+ cwd,
56
+ modelId: model,
57
+ provider,
58
+ serviceTier: config.serviceTier ?? null,
59
+ providers: config.providers,
60
+ config,
61
+ stateRoot,
62
+ memoryRoot,
63
+ profilePaths,
64
+ namespace,
65
+ projectMarchDir,
66
+ extensionPaths,
67
+ permissionMode: args.permissionMode,
68
+ shellRuntime: Boolean(shellRuntime),
69
+ lifecycleHooks: lifecycleManifests.hooks,
70
+ lifecycleDiagnostics: lifecycleManifests.diagnostics,
71
+ modelContextDumper: { enabled: args.dumpContext, rootDir: contextDumpRoot },
72
+ remoteMemorySources,
73
+ };
74
+ const runner = await createRuntimeRunner({
75
+ useRuntimeProcess,
76
+ runnerOptions,
77
+ ui,
78
+ memoryStore,
79
+ memoryTools,
80
+ shellRuntime,
81
+ webTools: createWebToolsFromConfig(config),
82
+ usePiSessions: true,
83
+ usePiRuntimeHost: true,
84
+ authStorage: authConfig.authStorage,
85
+ permissionController,
86
+ modelContextDumper,
87
+ turnNotifier,
88
+ logger,
89
+ });
90
+ let turnRunning = false;
91
+
92
+ return {
93
+ runner,
94
+ memoryStore,
95
+ logger,
96
+ currentProject,
97
+ snapshot: () => createWebSnapshot({ cwd, runner, currentProject }),
98
+ subscribe: (listener) => runner.runtimeUiEvents.on(listener),
99
+ refreshProviderQuota: () => runner.getProviderQuotaSnapshot?.({ emit: true }) ?? null,
100
+ async runTurn(prompt) {
101
+ if (turnRunning) throw new Error("A turn is already running");
102
+ turnRunning = true;
103
+ const input = prepareTurnInput({ prompt, runner, memoryStore, currentProject });
104
+ runner.runtimeUiEvents.emit({ type: "web_user_message", text: input.userMessage });
105
+ try {
106
+ return await runner.runTurn(input.fullPrompt, input.userMessage, input.runOptions);
107
+ } finally {
108
+ turnRunning = false;
109
+ memoryStore.endTurn();
110
+ }
111
+ },
112
+ abort: () => runner.abort(),
113
+ async dispose() {
114
+ await runner.dispose?.();
115
+ memoryStore.close?.();
116
+ },
117
+ };
118
+ }
119
+
120
+ export function createHeadlessWebUi() {
121
+ return {
122
+ readline: () => Promise.resolve(null), write: () => {}, writeln: () => {},
123
+ thinkingStart: () => {}, thinkingDelta: () => {}, thinkingEnd: () => {},
124
+ thinkingBlock: () => {}, toggleLastThinking: () => false,
125
+ toolStart: () => {}, toolEnd: () => {}, textDelta: () => {},
126
+ assistantReplyEnd: () => {}, status: () => {}, recall: () => {},
127
+ providerQuotaSnapshot: () => {},
128
+ clearOutput: () => {}, restoreTranscript: () => {}, setStatusBar: () => {},
129
+ turnStart: () => {}, turnEnd: () => {}, retryStart: () => {}, retryEnd: () => {},
130
+ editDiff: () => {}, requestPermission: async () => true,
131
+ setEscapeHandler: () => {}, setCtrlCHandler: () => {}, setShiftTabHandler: () => {},
132
+ setCtrlTHandler: () => {}, setCtrlLHandler: () => {}, setPasteImageHandler: () => {},
133
+ getInputText: () => "", insertTextAtCursor: () => {}, openExternalEditor: () => {},
134
+ toggleToolOutput: () => false, requestExit: () => {}, close: () => {},
135
+ };
136
+ }
137
+
138
+ export function createWebSnapshot({ cwd, runner, currentProject = basename(cwd) }) {
139
+ const model = runner.getCurrentModel?.();
140
+ return {
141
+ workspace: readWorkspaceTree(cwd),
142
+ timeline: { title: currentProject, meta: runtimeMeta(model), events: [] },
143
+ providerQuota: runner.getCachedProviderQuotaSnapshot?.() ?? null,
144
+ sessions: [{ id: "current", title: runner.engine?.sessionName ?? currentProject, time: "now", active: true }],
145
+ activity: [{ id: "runtime", action: "runner connected", time: "now" }],
146
+ composer: { mode: "Chat", placeholder: "Message March…" },
147
+ };
148
+ }
149
+
150
+ function runtimeMeta(model) {
151
+ return [model?.provider, model?.id].filter(Boolean).join(" · ") || "runner connected";
152
+ }
153
+
154
+ function readWorkspaceTree(rootPath) {
155
+ let count = 0;
156
+ const rootName = basename(rootPath) || rootPath;
157
+ return readNode(rootPath, rootName, 0, true);
158
+
159
+ function readNode(path, name, depth, selected = false) {
160
+ const stat = safeStat(path);
161
+ const kind = stat?.isDirectory() ? "folder" : "file";
162
+ const node = { id: path, name, kind, selected };
163
+ if (kind !== "folder" || depth >= MAX_WORKSPACE_DEPTH || count >= MAX_WORKSPACE_ENTRIES) return node;
164
+ const children = safeReadDir(path)
165
+ .filter((entry) => !entry.name.startsWith(".git") && entry.name !== "node_modules")
166
+ .sort(compareEntries)
167
+ .slice(0, 80)
168
+ .map((entry) => {
169
+ count += 1;
170
+ return readNode(join(path, entry.name), entry.name, depth + 1);
171
+ });
172
+ if (children.length > 0) node.children = children;
173
+ return node;
174
+ }
175
+ }
176
+
177
+ function safeStat(path) {
178
+ try { return statSync(path); } catch { return null; }
179
+ }
180
+
181
+ function safeReadDir(path) {
182
+ try { return readdirSync(path, { withFileTypes: true }); } catch { return []; }
183
+ }
184
+
185
+ function compareEntries(a, b) {
186
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
187
+ return a.name.localeCompare(b.name);
188
+ }
@@ -0,0 +1,140 @@
1
+ import { createReadStream, existsSync, statSync } from "node:fs";
2
+ import { createServer } from "node:http";
3
+ import { extname, join, normalize, sep } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+
6
+ const STATIC_ROOT = fileURLToPath(new URL("./dist/", import.meta.url));
7
+ const DEFAULT_PORT = 4174;
8
+
9
+ const rootPrefix = (root) => `${normalize(root).replace(/[\\/]$/, "")}${sep}`;
10
+
11
+ const mimeTypes = new Map([
12
+ [".html", "text/html; charset=utf-8"],
13
+ [".css", "text/css; charset=utf-8"],
14
+ [".js", "text/javascript; charset=utf-8"],
15
+ [".svg", "image/svg+xml"],
16
+ ]);
17
+
18
+ export function createWebUiServer({ root = STATIC_ROOT, runtime = null } = {}) {
19
+ return createServer(async (req, res) => {
20
+ if (runtime && req.url?.startsWith("/api/")) {
21
+ await handleApiRequest(req, res, runtime);
22
+ return;
23
+ }
24
+ serveStaticRequest(req, res, root);
25
+ });
26
+ }
27
+
28
+ export async function handleApiRequest(req, res, runtime) {
29
+ const { pathname } = new URL(req.url ?? "/", "http://localhost");
30
+ try {
31
+ if (req.method === "GET" && pathname === "/api/snapshot") return sendJson(res, runtime.snapshot(getSessionId(req)));
32
+ if (req.method === "GET" && pathname === "/api/events") return streamRuntimeEvents(req, res, runtime);
33
+ if (req.method === "GET" && pathname === "/api/sessions") return sendJson(res, { sessions: runtime.listSessions() });
34
+ if (req.method === "POST" && pathname === "/api/sessions") return sendJson(res, await createRuntimeSession(req, runtime));
35
+ if (req.method === "GET" && pathname === "/api/fs/roots") return sendJson(res, { roots: runtime.fsRoots() });
36
+ if (req.method === "GET" && pathname === "/api/fs/list") return sendJson(res, { entries: runtime.fsList(getPathParam(req)) });
37
+ if (req.method === "GET" && pathname === "/api/provider-quota") return sendJson(res, { snapshot: await runtime.refreshProviderQuota(getSessionId(req)) });
38
+ if (req.method === "POST" && pathname === "/api/turn") return sendJson(res, await runRuntimeTurn(req, runtime));
39
+ if (req.method === "POST" && pathname === "/api/abort") return sendJson(res, { ok: true, result: runtime.abort(getSessionId(req)) });
40
+ sendJson(res, { error: "Not found" }, 404);
41
+ } catch (err) {
42
+ sendJson(res, { error: err?.message ?? String(err) }, 500);
43
+ }
44
+ }
45
+
46
+ export function resolveStaticPath(root, requestUrl) {
47
+ const url = new URL(requestUrl, "http://localhost");
48
+ const pathname = url.pathname === "/" ? "/index.html" : url.pathname;
49
+ const decoded = decodeURIComponent(pathname);
50
+ const candidate = normalize(join(root, decoded));
51
+ return candidate.startsWith(rootPrefix(root)) ? candidate : null;
52
+ }
53
+
54
+ function serveStaticRequest(req, res, root) {
55
+ const filePath = resolveStaticPath(root, req.url ?? "/");
56
+ if (!filePath) {
57
+ res.writeHead(403);
58
+ res.end("Forbidden");
59
+ return;
60
+ }
61
+
62
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
63
+ res.writeHead(404);
64
+ res.end("Not found");
65
+ return;
66
+ }
67
+
68
+ const type = mimeTypes.get(extname(filePath)) ?? "application/octet-stream";
69
+ res.writeHead(200, { "content-type": type });
70
+ createReadStream(filePath).pipe(res);
71
+ }
72
+
73
+ async function createRuntimeSession(req, runtime) {
74
+ const body = await readJsonBody(req);
75
+ const session = await runtime.createSession(body.workspacePath);
76
+ return { ok: true, session, snapshot: runtime.snapshot(session.id) };
77
+ }
78
+
79
+ async function runRuntimeTurn(req, runtime) {
80
+ const body = await readJsonBody(req);
81
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
82
+ if (!prompt) throw new Error("Missing prompt");
83
+ const result = await runtime.runTurn(body.sessionId, prompt);
84
+ return { ok: true, draft: result?.draft ?? "" };
85
+ }
86
+
87
+ function streamRuntimeEvents(req, res, runtime) {
88
+ res.writeHead(200, {
89
+ "content-type": "text/event-stream; charset=utf-8",
90
+ "cache-control": "no-cache, no-transform",
91
+ connection: "keep-alive",
92
+ });
93
+ writeSse(res, "ready", { ok: true });
94
+ const unsubscribe = runtime.subscribe(getSessionId(req), (event) => writeSse(res, "runtime", event));
95
+ res.on("close", unsubscribe);
96
+ }
97
+
98
+ function getSessionId(req) {
99
+ const url = new URL(req.url ?? "/", "http://localhost");
100
+ return url.searchParams.get("sessionId");
101
+ }
102
+
103
+ function getPathParam(req) {
104
+ const url = new URL(req.url ?? "/", "http://localhost");
105
+ const path = url.searchParams.get("path");
106
+ if (!path) throw new Error("Missing path");
107
+ return path;
108
+ }
109
+
110
+ function writeSse(res, event, data) {
111
+ res.write(`event: ${event}\n`);
112
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
113
+ }
114
+
115
+ function sendJson(res, data, status = 200) {
116
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
117
+ res.end(JSON.stringify(data));
118
+ }
119
+
120
+ function readJsonBody(req) {
121
+ return new Promise((resolve, reject) => {
122
+ let raw = "";
123
+ req.setEncoding("utf8");
124
+ req.on("data", (chunk) => {
125
+ raw += chunk;
126
+ if (raw.length > 1_000_000) reject(new Error("Request body too large"));
127
+ });
128
+ req.on("end", () => {
129
+ try { resolve(raw ? JSON.parse(raw) : {}); } catch { reject(new Error("Invalid JSON body")); }
130
+ });
131
+ req.on("error", reject);
132
+ });
133
+ }
134
+
135
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
136
+ const port = Number.parseInt(process.env.MARCH_WEB_PORT ?? "", 10) || DEFAULT_PORT;
137
+ createWebUiServer().listen(port, "127.0.0.1", () => {
138
+ console.log(`March Web preview running at http://127.0.0.1:${port}`);
139
+ });
140
+ }
@@ -0,0 +1,111 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, resolve } from "node:path";
4
+ import { createWebRuntimeHost } from "./runtime-host.mjs";
5
+
6
+ export function createWebSessionManager({ args, config, launchCwd, stateRoot, useRuntimeProcess = true } = {}) {
7
+ const sessions = new Map();
8
+ const activities = [];
9
+ let activeSessionId = null;
10
+ let nextSessionNumber = 1;
11
+
12
+ async function createSession(workspacePath) {
13
+ const workspace = resolveWorkspace(workspacePath, launchCwd);
14
+ const id = `session-${Date.now().toString(36)}-${nextSessionNumber++}`;
15
+ const runtime = await createWebRuntimeHost({ args, config, cwd: workspace, stateRoot, useRuntimeProcess });
16
+ const session = { id, workspace, title: basename(workspace) || workspace, runtime, createdAt: Date.now() };
17
+ sessions.set(id, session);
18
+ activeSessionId = id;
19
+ activities.unshift({ id: `activity:${id}`, action: `opened ${session.title}`, time: "now" });
20
+ return toSessionSummary(session, true);
21
+ }
22
+
23
+ const listSessions = () => Array.from(sessions.values()).map((session) => toSessionSummary(session, session.id === activeSessionId));
24
+
25
+ return {
26
+ async createSession(workspacePath) { return createSession(workspacePath); },
27
+ listSessions,
28
+ snapshot(sessionId = activeSessionId) {
29
+ const session = getOptionalSession(sessions, sessionId);
30
+ if (!session) return createEmptySnapshot({ sessions, activeSessionId, activities });
31
+ const model = session.runtime.snapshot();
32
+ return { ...model, sessions: listSessions(), activity: activities, activeSessionId: session.id };
33
+ },
34
+ subscribe(sessionId, listener) { return getSession(sessions, sessionId).runtime.subscribe(listener); },
35
+ runTurn(sessionId, prompt) { return getSession(sessions, sessionId).runtime.runTurn(prompt); },
36
+ refreshProviderQuota(sessionId) { return getSession(sessions, sessionId).runtime.refreshProviderQuota?.() ?? null; },
37
+ abort(sessionId) { return getSession(sessions, sessionId).runtime.abort?.(); },
38
+ fsRoots() { return listFsRoots(launchCwd); },
39
+ fsList(path) { return listFsDirectory(path); },
40
+ async dispose() {
41
+ await Promise.all(Array.from(sessions.values()).map((session) => session.runtime.dispose?.()));
42
+ sessions.clear();
43
+ },
44
+ };
45
+ }
46
+
47
+ export function resolveWorkspace(requested, launchCwd = process.cwd()) {
48
+ if (!requested || typeof requested !== "string") throw new Error("Missing workspace path");
49
+ const workspace = resolve(launchCwd, requested);
50
+ if (!isDirectory(workspace)) throw new Error(`Workspace does not exist or is not a directory: ${workspace}`);
51
+ return workspace;
52
+ }
53
+
54
+ function getSession(sessions, id) {
55
+ const session = getOptionalSession(sessions, id);
56
+ if (!session) throw new Error("Session not found");
57
+ return session;
58
+ }
59
+
60
+ function getOptionalSession(sessions, id) {
61
+ if (!id) return null;
62
+ return sessions.get(id) ?? null;
63
+ }
64
+
65
+ function toSessionSummary(session, active) {
66
+ return { id: session.id, title: session.title, workspacePath: session.workspace, time: "now", active };
67
+ }
68
+
69
+ function createEmptySnapshot({ sessions, activeSessionId, activities }) {
70
+ return {
71
+ workspace: { id: "no-workspace", name: "Choose workspace", kind: "folder", selected: true },
72
+ timeline: { title: "No session selected", meta: "Create a session and bind a workspace", events: [] },
73
+ sessions: Array.from(sessions.values()).map((session) => toSessionSummary(session, session.id === activeSessionId)),
74
+ providerQuota: null,
75
+ activity: activities,
76
+ activeSessionId: null,
77
+ composer: { mode: "No session", placeholder: "Choose a workspace to start…" },
78
+ };
79
+ }
80
+
81
+ function listFsRoots(launchCwd) {
82
+ const roots = uniquePaths([launchCwd, homedir(), ...windowsDriveRoots()]);
83
+ return roots.filter(isDirectory).map((path) => ({ name: path, path, kind: "root" }));
84
+ }
85
+
86
+ function windowsDriveRoots() {
87
+ if (process.platform !== "win32") return ["/"];
88
+ return Array.from({ length: 26 }, (_, index) => `${String.fromCharCode(65 + index)}:\\`);
89
+ }
90
+
91
+ function listFsDirectory(path) {
92
+ const root = resolve(path);
93
+ if (!isDirectory(root)) throw new Error(`Directory does not exist: ${root}`);
94
+ return safeReadDir(root)
95
+ .filter((entry) => entry.isDirectory())
96
+ .sort((a, b) => a.name.localeCompare(b.name))
97
+ .slice(0, 500)
98
+ .map((entry) => ({ name: entry.name, path: resolve(root, entry.name), kind: "directory" }));
99
+ }
100
+
101
+ function uniquePaths(paths) {
102
+ return Array.from(new Set(paths.filter(Boolean).map((path) => resolve(path))));
103
+ }
104
+
105
+ function isDirectory(path) {
106
+ try { return statSync(path).isDirectory(); } catch { return false; }
107
+ }
108
+
109
+ function safeReadDir(path) {
110
+ try { return readdirSync(path, { withFileTypes: true }); } catch { return []; }
111
+ }
@@ -0,0 +1,7 @@
1
+ import { AppShell } from "./components/AppShell";
2
+ import { useWebRuntime } from "./runtime/useWebRuntime";
3
+
4
+ export function App() {
5
+ const runtime = useWebRuntime();
6
+ return <AppShell runtime={runtime} />;
7
+ }