march-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/bin/march.mjs +13 -0
  2. package/package.json +36 -0
  3. package/src/agent/command-exec-tool.mjs +91 -0
  4. package/src/agent/context-stats-tool.mjs +57 -0
  5. package/src/agent/editing/diff-apply.mjs +28 -0
  6. package/src/agent/editing/diff-format.mjs +57 -0
  7. package/src/agent/file-edit-tool.mjs +276 -0
  8. package/src/agent/find-tool.mjs +112 -0
  9. package/src/agent/model-payload-dumper.mjs +201 -0
  10. package/src/agent/pi-session/pi-session-sidecar-failure.mjs +10 -0
  11. package/src/agent/provider/payload-messages.mjs +138 -0
  12. package/src/agent/read-file-tool.mjs +112 -0
  13. package/src/agent/runner/fast-model.mjs +36 -0
  14. package/src/agent/runner/runner-cleanup.mjs +12 -0
  15. package/src/agent/runner/runner-init.mjs +15 -0
  16. package/src/agent/runner/runner-session-state.mjs +40 -0
  17. package/src/agent/runner.mjs +266 -0
  18. package/src/agent/runtime/runner-runtime-host.mjs +73 -0
  19. package/src/agent/runtime/runtime-factory.mjs +42 -0
  20. package/src/agent/runtime/runtime-host.mjs +34 -0
  21. package/src/agent/session/session-auto-name.mjs +41 -0
  22. package/src/agent/session/session-binding.mjs +12 -0
  23. package/src/agent/session/session-options.mjs +46 -0
  24. package/src/agent/tool-names.mjs +1 -0
  25. package/src/agent/tool-result.mjs +3 -0
  26. package/src/agent/tools.mjs +54 -0
  27. package/src/agent/turn/turn-events.mjs +64 -0
  28. package/src/agent/turn/turn-runner.mjs +103 -0
  29. package/src/auth/login-command.mjs +90 -0
  30. package/src/auth/storage.mjs +33 -0
  31. package/src/cli/args.mjs +71 -0
  32. package/src/cli/commands/copy-command.mjs +73 -0
  33. package/src/cli/commands/export-command.mjs +206 -0
  34. package/src/cli/commands/extensions-command.mjs +53 -0
  35. package/src/cli/commands/help-command.mjs +7 -0
  36. package/src/cli/commands/model-command.mjs +110 -0
  37. package/src/cli/commands/paste-image-command.mjs +43 -0
  38. package/src/cli/commands/provider-command.mjs +55 -0
  39. package/src/cli/commands/status-command.mjs +157 -0
  40. package/src/cli/commands/thinking-command.mjs +80 -0
  41. package/src/cli/fallback-ui.mjs +156 -0
  42. package/src/cli/input/attachment-tokens.mjs +20 -0
  43. package/src/cli/input/autocomplete.mjs +106 -0
  44. package/src/cli/input/external-editor.mjs +39 -0
  45. package/src/cli/input/history-store.mjs +35 -0
  46. package/src/cli/input/image-clipboard.mjs +55 -0
  47. package/src/cli/input/keybinding-dispatch.mjs +76 -0
  48. package/src/cli/input/keybindings.mjs +96 -0
  49. package/src/cli/input/mode-state.mjs +43 -0
  50. package/src/cli/input/prompt-templates.mjs +84 -0
  51. package/src/cli/input/select-with-keyboard.mjs +67 -0
  52. package/src/cli/permissions.mjs +103 -0
  53. package/src/cli/repl-commands.mjs +86 -0
  54. package/src/cli/repl-loop.mjs +157 -0
  55. package/src/cli/selector-list.mjs +21 -0
  56. package/src/cli/session/pi-session-switch-command.mjs +41 -0
  57. package/src/cli/session/session-command.mjs +23 -0
  58. package/src/cli/session/session-list-command.mjs +68 -0
  59. package/src/cli/session/session-name-command.mjs +26 -0
  60. package/src/cli/session/session-source-command.mjs +89 -0
  61. package/src/cli/session/session-switch-command.mjs +1 -0
  62. package/src/cli/shell/shell-command.mjs +55 -0
  63. package/src/cli/shell/shell-drawer-controls.mjs +33 -0
  64. package/src/cli/shell/shell-drawer.mjs +192 -0
  65. package/src/cli/shell/shell-split-layout.mjs +70 -0
  66. package/src/cli/slash-commands.mjs +176 -0
  67. package/src/cli/startup/startup-banner.mjs +17 -0
  68. package/src/cli/startup/startup-session.mjs +51 -0
  69. package/src/cli/status-line-updater.mjs +74 -0
  70. package/src/cli/tool-output.mjs +9 -0
  71. package/src/cli/tui/editor/external-editor-runner.mjs +24 -0
  72. package/src/cli/tui/input/mouse-selection-controller.mjs +89 -0
  73. package/src/cli/tui/input/mouse-tracking.mjs +20 -0
  74. package/src/cli/tui/layout/main-pane-layout.mjs +38 -0
  75. package/src/cli/tui/layout/safe-render-boundary.mjs +46 -0
  76. package/src/cli/tui/markdown-renderer.mjs +279 -0
  77. package/src/cli/tui/output/scroll-state.mjs +79 -0
  78. package/src/cli/tui/output/tool-card-renderer.mjs +59 -0
  79. package/src/cli/tui/output-buffer.mjs +297 -0
  80. package/src/cli/tui/permission-request-ui.mjs +18 -0
  81. package/src/cli/tui/recall-rendering.mjs +25 -0
  82. package/src/cli/tui/select/editor-select-list.mjs +111 -0
  83. package/src/cli/tui/selection-screen.mjs +212 -0
  84. package/src/cli/tui/status/retry-status.mjs +72 -0
  85. package/src/cli/tui/status/spinner-status.mjs +42 -0
  86. package/src/cli/tui/status/status-bar.mjs +88 -0
  87. package/src/cli/tui/syntax/highlighting.mjs +277 -0
  88. package/src/cli/tui/syntax/languages.mjs +91 -0
  89. package/src/cli/tui/syntax/tree-sitter/bash.highlights.scm +261 -0
  90. package/src/cli/tui/syntax/tree-sitter/c.highlights.scm +341 -0
  91. package/src/cli/tui/syntax/tree-sitter/cpp.highlights.scm +268 -0
  92. package/src/cli/tui/syntax/tree-sitter/csharp.highlights.scm +577 -0
  93. package/src/cli/tui/syntax/tree-sitter/css.highlights.scm +109 -0
  94. package/src/cli/tui/syntax/tree-sitter/diff.highlights.scm +49 -0
  95. package/src/cli/tui/syntax/tree-sitter/go.highlights.scm +254 -0
  96. package/src/cli/tui/syntax/tree-sitter/html.highlights.scm +13 -0
  97. package/src/cli/tui/syntax/tree-sitter/java.highlights.scm +330 -0
  98. package/src/cli/tui/syntax/tree-sitter/json.highlights.scm +38 -0
  99. package/src/cli/tui/syntax/tree-sitter/php.highlights.scm +203 -0
  100. package/src/cli/tui/syntax/tree-sitter/python.highlights.scm +137 -0
  101. package/src/cli/tui/syntax/tree-sitter/ruby.highlights.scm +309 -0
  102. package/src/cli/tui/syntax/tree-sitter/rust.highlights.scm +531 -0
  103. package/src/cli/tui/syntax/tree-sitter/toml.highlights.scm +39 -0
  104. package/src/cli/tui/syntax/tree-sitter/tree-sitter-bash.wasm +0 -0
  105. package/src/cli/tui/syntax/tree-sitter/tree-sitter-c-sharp.wasm +0 -0
  106. package/src/cli/tui/syntax/tree-sitter/tree-sitter-c.wasm +0 -0
  107. package/src/cli/tui/syntax/tree-sitter/tree-sitter-cpp.wasm +0 -0
  108. package/src/cli/tui/syntax/tree-sitter/tree-sitter-css.wasm +0 -0
  109. package/src/cli/tui/syntax/tree-sitter/tree-sitter-diff.wasm +0 -0
  110. package/src/cli/tui/syntax/tree-sitter/tree-sitter-go.wasm +0 -0
  111. package/src/cli/tui/syntax/tree-sitter/tree-sitter-html.wasm +0 -0
  112. package/src/cli/tui/syntax/tree-sitter/tree-sitter-java.wasm +0 -0
  113. package/src/cli/tui/syntax/tree-sitter/tree-sitter-json.wasm +0 -0
  114. package/src/cli/tui/syntax/tree-sitter/tree-sitter-php.wasm +0 -0
  115. package/src/cli/tui/syntax/tree-sitter/tree-sitter-python.wasm +0 -0
  116. package/src/cli/tui/syntax/tree-sitter/tree-sitter-ruby.wasm +0 -0
  117. package/src/cli/tui/syntax/tree-sitter/tree-sitter-rust.wasm +0 -0
  118. package/src/cli/tui/syntax/tree-sitter/tree-sitter-toml.wasm +0 -0
  119. package/src/cli/tui/syntax/tree-sitter/tree-sitter-tsx.wasm +0 -0
  120. package/src/cli/tui/syntax/tree-sitter/tree-sitter-typescript.wasm +0 -0
  121. package/src/cli/tui/syntax/tree-sitter/tree-sitter-yaml.wasm +0 -0
  122. package/src/cli/tui/syntax/tree-sitter/tsx.highlights.scm +35 -0
  123. package/src/cli/tui/syntax/tree-sitter/typescript.highlights.scm +35 -0
  124. package/src/cli/tui/syntax/tree-sitter/yaml.highlights.scm +99 -0
  125. package/src/cli/tui/tool-rendering.mjs +194 -0
  126. package/src/cli/tui/tui-diff-rendering.mjs +157 -0
  127. package/src/cli/tui/tui-handlers.mjs +110 -0
  128. package/src/cli/tui/tui-input-controller.mjs +61 -0
  129. package/src/cli/tui/ui-theme.mjs +148 -0
  130. package/src/cli/ui.mjs +299 -0
  131. package/src/config/config-json.mjs +73 -0
  132. package/src/config/dotenv.mjs +20 -0
  133. package/src/config/features.mjs +75 -0
  134. package/src/config/loader.mjs +109 -0
  135. package/src/config/settings-command.mjs +97 -0
  136. package/src/context/diagnostics.mjs +70 -0
  137. package/src/context/engine.mjs +148 -0
  138. package/src/context/injections.mjs +26 -0
  139. package/src/context/project-context.mjs +20 -0
  140. package/src/context/session-status.mjs +15 -0
  141. package/src/context/shell-layers.mjs +23 -0
  142. package/src/context/system-core/base.md +60 -0
  143. package/src/context/system-core/prompts/deepseek-v4-pro.md +3 -0
  144. package/src/context/system-core/prompts/default.md +3 -0
  145. package/src/context/system-core.mjs +35 -0
  146. package/src/debug/model-context-dumper.mjs +52 -0
  147. package/src/extensions/discovery.mjs +40 -0
  148. package/src/extensions/lifecycle-adapter.mjs +210 -0
  149. package/src/extensions/lifecycle-manifest.mjs +69 -0
  150. package/src/image-gen/index.mjs +7 -0
  151. package/src/image-gen/provider.mjs +231 -0
  152. package/src/image-gen/tool.mjs +84 -0
  153. package/src/lsp/client.mjs +204 -0
  154. package/src/lsp/diagnostic-store.mjs +39 -0
  155. package/src/lsp/servers.mjs +212 -0
  156. package/src/lsp/service.mjs +65 -0
  157. package/src/main.mjs +294 -0
  158. package/src/mcp/client.mjs +195 -0
  159. package/src/mcp/config.mjs +130 -0
  160. package/src/mcp/index.mjs +48 -0
  161. package/src/mcp/tools.mjs +98 -0
  162. package/src/memory/database.mjs +219 -0
  163. package/src/memory/glossary.mjs +124 -0
  164. package/src/memory/graph/graph-cascades.mjs +109 -0
  165. package/src/memory/graph/graph-diagnostics.mjs +73 -0
  166. package/src/memory/graph/graph-path-removal.mjs +50 -0
  167. package/src/memory/graph/graph-path-utils.mjs +17 -0
  168. package/src/memory/graph/graph-primitives.mjs +103 -0
  169. package/src/memory/graph/graph-read.mjs +159 -0
  170. package/src/memory/graph.mjs +282 -0
  171. package/src/memory/markdown/markdown-delete.mjs +23 -0
  172. package/src/memory/markdown/markdown-format.mjs +128 -0
  173. package/src/memory/markdown/markdown-recall.mjs +28 -0
  174. package/src/memory/markdown/ripgrep.mjs +16 -0
  175. package/src/memory/markdown/sqlite-index.mjs +87 -0
  176. package/src/memory/markdown-store.mjs +286 -0
  177. package/src/memory/markdown-tools.mjs +103 -0
  178. package/src/memory/search.mjs +142 -0
  179. package/src/memory/snapshot.mjs +86 -0
  180. package/src/memory/system-views.mjs +120 -0
  181. package/src/memory/tools.mjs +282 -0
  182. package/src/notification/desktop-notifier.mjs +85 -0
  183. package/src/platform/open-file.mjs +28 -0
  184. package/src/provider/config-command.mjs +129 -0
  185. package/src/provider/presets.mjs +72 -0
  186. package/src/session/attachment-display.mjs +16 -0
  187. package/src/session/attachment-references.mjs +65 -0
  188. package/src/session/attachments.mjs +140 -0
  189. package/src/session/persist.mjs +1 -0
  190. package/src/session/pi-manager.mjs +34 -0
  191. package/src/session/session-utils.mjs +16 -0
  192. package/src/session/sidecar-sync.mjs +19 -0
  193. package/src/session/sidecar.mjs +68 -0
  194. package/src/session/transcript.mjs +83 -0
  195. package/src/session/tree.mjs +42 -0
  196. package/src/shell/cli-runtime.mjs +11 -0
  197. package/src/shell/hints.mjs +12 -0
  198. package/src/shell/node-pty-adapter.mjs +81 -0
  199. package/src/shell/runtime-state.mjs +126 -0
  200. package/src/shell/runtime.mjs +244 -0
  201. package/src/shell/screen-buffer.mjs +136 -0
  202. package/src/shell/tool-read.mjs +74 -0
  203. package/src/shell/tools.mjs +299 -0
  204. package/src/supergrok/actions/image-generate.mjs +60 -0
  205. package/src/supergrok/actions/search.mjs +78 -0
  206. package/src/supergrok/auth.mjs +36 -0
  207. package/src/supergrok/constants.mjs +18 -0
  208. package/src/supergrok/oauth-provider.mjs +278 -0
  209. package/src/supergrok/provider.mjs +36 -0
  210. package/src/supergrok/response.mjs +76 -0
  211. package/src/supergrok/tool.mjs +61 -0
  212. package/src/text/ansi.mjs +3 -0
  213. package/src/web/config-command.mjs +43 -0
  214. package/src/web/fetch.mjs +78 -0
  215. package/src/web/presets.mjs +16 -0
  216. package/src/web/search.mjs +83 -0
  217. package/src/web/tools.mjs +107 -0
@@ -0,0 +1,148 @@
1
+ // ── SGR constants ────────────────────────────────────────────────────
2
+ const R = "\x1b[0m";
3
+ const B = "\x1b[1m";
4
+ const D = "\x1b[2m";
5
+
6
+ // Standard 16 colors
7
+ const black = (s) => `\x1b[30m${s}${R}`;
8
+ const red = (s) => `\x1b[31m${s}${R}`;
9
+ const green = (s) => `\x1b[32m${s}${R}`;
10
+ const yellow = (s) => `\x1b[33m${s}${R}`;
11
+ const blue = (s) => `\x1b[34m${s}${R}`;
12
+ const magenta = (s) => `\x1b[35m${s}${R}`;
13
+ const cyan = (s) => `\x1b[36m${s}${R}`;
14
+ const white = (s) => `\x1b[37m${s}${R}`;
15
+ const brightBlack = (s) => `\x1b[90m${s}${R}`;
16
+ const brightRed = (s) => `\x1b[91m${s}${R}`;
17
+ const brightGreen = (s) => `\x1b[92m${s}${R}`;
18
+ const orange = (s) => `\x1b[38;2;245;167;66m${s}${R}`;
19
+ const softGreen = (s) => `\x1b[38;2;127;216;143m${s}${R}`;
20
+
21
+ // ── Formatters ───────────────────────────────────────────────────────
22
+ const bold = (s) => `${B}${s}${R}`;
23
+ const dim = (s) => `${D}${s}${R}`;
24
+ const inverse = (s) => `\x1b[7m${s}${R}`;
25
+
26
+ // ── 256-color helpers ────────────────────────────────────────────────
27
+ const fg256 = (n) => (s) => `\x1b[38;5;${n}m${s}${R}`;
28
+ const bg256 = (n) => (s) => `\x1b[48;5;${n}m${s}${R}`;
29
+
30
+ // ── Semantic tokens ──────────────────────────────────────────────────
31
+ const text = {
32
+ primary: white,
33
+ secondary: (s) => fg256(250)(s), // light gray
34
+ muted: brightBlack,
35
+ inverse: black,
36
+ };
37
+
38
+ const surface = {
39
+ base: (s) => s, // default terminal bg
40
+ raised: bg256(236), // dark gray bg
41
+ overlay: bg256(238),
42
+ };
43
+
44
+ const accent = {
45
+ primary: cyan,
46
+ success: green,
47
+ error: red,
48
+ warning: yellow,
49
+ info: blue,
50
+ };
51
+
52
+ const border = {
53
+ default: brightBlack,
54
+ focus: cyan,
55
+ };
56
+
57
+ // ── Component tokens ─────────────────────────────────────────────────
58
+ const diff = {
59
+ add: green,
60
+ del: red,
61
+ ctx: brightBlack,
62
+ header: bold,
63
+ gutter: brightBlack,
64
+ };
65
+
66
+ const tool = {
67
+ name: brightBlack,
68
+ args: brightBlack,
69
+ result: dim,
70
+ error: red,
71
+ expand: brightBlack,
72
+ };
73
+
74
+ const message = {
75
+ user: bold,
76
+ assistant: (s) => s,
77
+ system: brightBlack,
78
+ separator: brightBlack,
79
+ };
80
+
81
+ const statusBar = {
82
+ background: bg256(236),
83
+ text: fg256(250),
84
+ accent: cyan,
85
+ };
86
+
87
+ const shell = {
88
+ header: bold,
89
+ divider: fg256(238),
90
+ prompt: green,
91
+ scrollInfo: brightBlack,
92
+ };
93
+
94
+ const spinner = {
95
+ frame: cyan,
96
+ };
97
+
98
+ const selectList = {
99
+ selectedPrefix: cyan,
100
+ selectedText: white,
101
+ description: brightBlack,
102
+ scrollInfo: brightBlack,
103
+ noMatch: brightBlack,
104
+ };
105
+
106
+ // ── Editor theme (consumed by pi-tui Editor component) ──────────────
107
+ const EDITOR_THEME = {
108
+ borderColor: border.default,
109
+ selectList,
110
+ };
111
+
112
+ // ── Raw ANSI prefixes (for template literal composition) ─────────────
113
+ const PREFIX = {
114
+ reset: R,
115
+ bold: B,
116
+ dim: D,
117
+ fg250: "\x1b[38;5;250m", // header / secondary text
118
+ fg238: "\x1b[38;5;238m", // border / divider
119
+ brightBlack: "\x1b[90m", // muted text
120
+ cyan: "\x1b[36m", // active / accent
121
+ };
122
+
123
+ // ── Public API ───────────────────────────────────────────────────────
124
+ export {
125
+ PREFIX,
126
+ // SGR primitives
127
+ R, B, D,
128
+ black, red, green, yellow, blue, magenta, cyan, white,
129
+ brightBlack, brightRed, brightGreen,
130
+ orange, softGreen,
131
+ bold, dim, inverse,
132
+ fg256, bg256,
133
+ // Semantic
134
+ text,
135
+ surface,
136
+ accent,
137
+ border,
138
+ // Components
139
+ diff,
140
+ tool,
141
+ message,
142
+ statusBar,
143
+ shell,
144
+ spinner,
145
+ selectList,
146
+ // Editor
147
+ EDITOR_THEME,
148
+ };
package/src/cli/ui.mjs ADDED
@@ -0,0 +1,299 @@
1
+ import { stdout } from "node:process";
2
+ import { Editor, ProcessTerminal, TUI } from "@earendil-works/pi-tui";
3
+ import { writeSystemClipboardAsync } from "./commands/copy-command.mjs";
4
+ import { buildMarchCommands, MarchAutocompleteProvider } from "./input/autocomplete.mjs";
5
+ import { createJsonUI, createPlainUI } from "./fallback-ui.mjs";
6
+ import { createKeybindingDispatcher } from "./input/keybinding-dispatch.mjs";
7
+ import { OutputBuffer } from "./tui/output-buffer.mjs";
8
+ import { requestToolPermission } from "./tui/permission-request-ui.mjs";
9
+ import { runTuiExternalEditor } from "./tui/editor/external-editor-runner.mjs";
10
+ import { createRetryStatusController } from "./tui/status/retry-status.mjs";
11
+ import { createShellDrawerControls } from "./shell/shell-drawer-controls.mjs";
12
+ import { ShellDrawer } from "./shell/shell-drawer.mjs";
13
+ import { ShellSplitLayout } from "./shell/shell-split-layout.mjs";
14
+ import { createSpinnerStatusController } from "./tui/status/spinner-status.mjs";
15
+ import { showEditorSelectList } from "./tui/select/editor-select-list.mjs";
16
+ import { StatusBar } from "./tui/status/status-bar.mjs";
17
+ import { MainPaneLayout } from "./tui/layout/main-pane-layout.mjs";
18
+ import { SafeRenderBoundary } from "./tui/layout/safe-render-boundary.mjs";
19
+ import { createMouseSelectionController } from "./tui/input/mouse-selection-controller.mjs";
20
+ import { ScreenSelection } from "./tui/selection-screen.mjs";
21
+ import { writeEditDiff } from "./tui/tui-diff-rendering.mjs";
22
+ import { createTuiInputController } from "./tui/tui-input-controller.mjs";
23
+ import { writeMemoryHint } from "./tui/recall-rendering.mjs";
24
+ import { writeToolEnd, writeToolStart } from "./tui/tool-rendering.mjs";
25
+ import { EDITOR_THEME, brightBlack } from "./tui/ui-theme.mjs";
26
+ import { writeTranscriptToOutput } from "../session/transcript.mjs";
27
+
28
+ export { buildMarchCommands, MarchAutocompleteProvider } from "./input/autocomplete.mjs";
29
+
30
+ export function createTuiUI({
31
+ cwd = process.cwd(),
32
+ keybindings,
33
+ promptTemplates = [],
34
+ shellRuntime = null,
35
+ historyStore = null,
36
+ terminal = new ProcessTerminal(),
37
+ writeClipboard = writeSystemClipboardAsync,
38
+ } = {}) {
39
+ const tui = new TUI(terminal);
40
+ const output = new OutputBuffer();
41
+ const shellDrawer = new ShellDrawer({ shellRuntime });
42
+ const statusBar = new StatusBar();
43
+ const editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
44
+ const selection = new ScreenSelection();
45
+ const mainPane = new MainPaneLayout({ output, statusBar, editor, terminal, selection });
46
+ const shellSplitLayout = new ShellSplitLayout({
47
+ mainChildren: [mainPane],
48
+ shellPane: shellDrawer,
49
+ selection,
50
+ });
51
+ const autocomplete = new MarchAutocompleteProvider(buildMarchCommands(promptTemplates), cwd);
52
+ editor.setAutocompleteProvider(autocomplete);
53
+ editor.history = historyStore?.load?.() ?? [];
54
+
55
+ tui.addChild(new SafeRenderBoundary(shellSplitLayout));
56
+ tui.setFocus(editor);
57
+
58
+ let started = false;
59
+ let mouseOn = true;
60
+ let toolsExpanded = false;
61
+ const activeToolBlocks = [];
62
+
63
+ function requestRender() {
64
+ tui.requestRender();
65
+ }
66
+
67
+ const spinnerStatus = createSpinnerStatusController({ output, requestRender });
68
+ const retryStatus = createRetryStatusController({ output, requestRender, stopSpinner: spinnerStatus.stop });
69
+ const shellDrawerControls = createShellDrawerControls({ shellDrawer, output, requestRender });
70
+ const mouseSelectionController = createMouseSelectionController({ terminal, output, shellDrawer, shellDrawerControls, selection, writeClipboard, requestRender });
71
+
72
+ let onEscapeHandler = null, onCtrlCHandler = null, onShiftTabHandler = null;
73
+ let onCtrlTHandler = null, onCtrlLHandler = null, onPasteImageHandler = null, onToggleModeHandler = null;
74
+ const keybindingDispatcher = createKeybindingDispatcher({
75
+ keybindings,
76
+ handlers: {
77
+ abort: () => onEscapeHandler?.(),
78
+ interrupt: () => onCtrlCHandler?.(),
79
+ toggleMode: () => onToggleModeHandler?.(),
80
+ cycleThinking: () => onShiftTabHandler?.(),
81
+ thinkingSelector: () => onCtrlTHandler?.(),
82
+ modelSelector: () => onCtrlLHandler?.(),
83
+ externalEditor: () => openExternalEditor(),
84
+ toggleToolOutput: () => toggleToolOutput(),
85
+ toggleShellDrawer: () => shellDrawerControls.toggle(),
86
+ nextShell: () => shellDrawerControls.selectNext(),
87
+ shellScrollUp: () => shellDrawerControls.scroll(-1),
88
+ shellScrollDown: () => shellDrawerControls.scroll(1),
89
+ outputScrollUp: () => { output.scroll(-1); requestRender(); },
90
+ outputScrollDown: () => { output.scroll(1); requestRender(); },
91
+ pasteImage: () => onPasteImageHandler?.(),
92
+ },
93
+ isAutocompleteOpen: () => editor.isShowingAutocomplete(),
94
+ hasOverlay: () => tui.hasOverlay(),
95
+ });
96
+
97
+ function ensureStarted() {
98
+ if (!started) {
99
+ tui.addInputListener((data) => {
100
+ const mouseResult = mouseSelectionController.handleMouseInput(data, mouseOn);
101
+ if (mouseResult) return mouseResult;
102
+ const copyKeyResult = mouseSelectionController.handleCopyKey(data);
103
+ if (copyKeyResult) return copyKeyResult;
104
+ const dispatched = keybindingDispatcher.dispatch(data);
105
+ if (dispatched) return dispatched;
106
+ // When output is scrolled up, the next render has fewer lines.
107
+ // On new input, reset scroll to tail so the editor stays at bottom.
108
+ if (output.scrollOffset > 0) {
109
+ output.resetScroll();
110
+ requestRender();
111
+ }
112
+ if (shellDrawer.isInputActive()) {
113
+ shellDrawer.sendInput(data);
114
+ requestRender();
115
+ return { consume: true };
116
+ }
117
+ });
118
+ terminal.write("\x1b[?1049h");
119
+ terminal.write("\x1b[?1002h\x1b[?1006h");
120
+ tui.start();
121
+ started = true;
122
+ }
123
+ }
124
+
125
+ function openExternalEditor() {
126
+ runTuiExternalEditor({ terminal, tui, editor, output, requestRender, mouseOn: () => mouseOn });
127
+ }
128
+
129
+ function toggleToolOutput() {
130
+ toolsExpanded = !toolsExpanded;
131
+ output.setToolCardsExpanded(toolsExpanded);
132
+ output.writeln(brightBlack(`● tool output: ${toolsExpanded ? "expanded" : "collapsed"}`));
133
+ requestRender();
134
+ return toolsExpanded;
135
+ }
136
+
137
+ function selectList({ items, selectedIndex = 0, maxVisible = 8, ...options }) {
138
+ ensureStarted();
139
+ return showEditorSelectList({ tui, editor, items, selectedIndex, maxVisible, requestRender, ...options });
140
+ }
141
+
142
+ function retryStart({ attempt, maxAttempts, delayMs, errorMessage }) {
143
+ ensureStarted();
144
+ retryStatus.start({ attempt, maxAttempts, delayMs, errorMessage });
145
+ }
146
+
147
+ function retryEnd({ success, attempt, finalError }) {
148
+ ensureStarted();
149
+ retryStatus.end({ success, attempt, finalError });
150
+ }
151
+
152
+ const inputController = createTuiInputController({ editor, requestRender, historyStore });
153
+
154
+ return {
155
+ readline: (_prompt) => {
156
+ ensureStarted();
157
+ return inputController.readline();
158
+ },
159
+ write: (text) => {
160
+ ensureStarted();
161
+ output.write(text);
162
+ requestRender();
163
+ },
164
+ writeln: (text) => {
165
+ ensureStarted();
166
+ output.writeln(text);
167
+ requestRender();
168
+ },
169
+ thinkingStart: () => {
170
+ retryStatus.stop(); output.startThinking(); requestRender();
171
+ },
172
+
173
+ thinkingDelta: (delta) => {
174
+ output.appendThinking(delta);
175
+ requestRender();
176
+ },
177
+
178
+ thinkingEnd: (tokens) => {
179
+ output.endThinking(tokens);
180
+ requestRender();
181
+ },
182
+
183
+ thinkingBlock: (tokens, content) => {
184
+ retryStatus.stop(); output.addThinkingBlock(tokens, content); requestRender();
185
+ },
186
+
187
+ toggleLastThinking: () => false,
188
+
189
+ toolStart: (name, args) => {
190
+ ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); activeToolBlocks.push(writeToolStart({ output, name, args })); requestRender();
191
+ },
192
+
193
+ toolEnd: (name, isError, result) => {
194
+ if (writeToolEnd({ output, name, isError, result, toolsExpanded, toolBlock: activeToolBlocks.pop() })) requestRender();
195
+ },
196
+
197
+ textDelta: (delta) => {
198
+ ensureStarted(); retryStatus.stop(); spinnerStatus.stop();
199
+ output.writeMarkdown(delta);
200
+ requestRender();
201
+ },
202
+ assistantReplyEnd: () => {
203
+ ensureStarted();
204
+ const changed = output.ensureNewline();
205
+ if (output.sealCurrentText() || changed) requestRender();
206
+ },
207
+ status: (text) => {
208
+ ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
209
+ },
210
+ memoryHint: ({ hints }) => {
211
+ ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeMemoryHint({ output, hints }); requestRender();
212
+ },
213
+
214
+ clearOutput: () => {
215
+ ensureStarted(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); requestRender();
216
+ },
217
+
218
+ restoreTranscript: (turns) => {
219
+ ensureStarted(); spinnerStatus.stop(); retryStatus.stop(); output.clear();
220
+ writeTranscriptToOutput(output, turns);
221
+ requestRender();
222
+ },
223
+
224
+ setStatusBar: (text) => {
225
+ if (statusBar.setText(text)) requestRender();
226
+ },
227
+
228
+ turnStart: () => {
229
+ ensureStarted();
230
+ },
231
+
232
+ turnEnd: () => {
233
+ const changed = output.ensureNewline();
234
+ if (output.sealCurrentText() || changed) requestRender();
235
+ },
236
+
237
+ retryStart,
238
+ retryEnd,
239
+
240
+ editDiff: (path, diffLines) => {
241
+ ensureStarted();
242
+ spinnerStatus.stop();
243
+ writeEditDiff({ output, path, diffLines });
244
+ requestRender();
245
+ },
246
+
247
+ toggleMouse: () => {
248
+ if (mouseOn) {
249
+ terminal.write("\x1b[?1002l\x1b[?1006l");
250
+ mouseOn = false;
251
+ return false;
252
+ } else {
253
+ terminal.write("\x1b[?1002h\x1b[?1006h");
254
+ mouseOn = true;
255
+ return true;
256
+ }
257
+ },
258
+
259
+ requestPermission: async ({ toolName, params, category }) => {
260
+ ensureStarted();
261
+ spinnerStatus.stop();
262
+ return requestToolPermission({ toolName, params, category, output, selectList, requestRender });
263
+ },
264
+
265
+ setEscapeHandler: (fn) => { onEscapeHandler = fn; },
266
+ setCtrlCHandler: (fn) => { onCtrlCHandler = fn; },
267
+ setShiftTabHandler: (fn) => { onShiftTabHandler = fn; },
268
+ setCtrlTHandler: (fn) => { onCtrlTHandler = fn; },
269
+ setCtrlLHandler: (fn) => { onCtrlLHandler = fn; },
270
+ setPasteImageHandler: (fn) => { onPasteImageHandler = fn; },
271
+ setToggleModeHandler: (fn) => { onToggleModeHandler = fn; },
272
+
273
+ selectList,
274
+ getInputText: () => inputController.getInputText(),
275
+ insertTextAtCursor: (text) => inputController.insertTextAtCursor(text),
276
+ insertAttachmentAtCursor: (attachment) => inputController.insertAttachmentAtCursor(attachment),
277
+ openExternalEditor: () => { openExternalEditor(); },
278
+ toggleToolOutput,
279
+ toggleShellDrawer: () => shellDrawerControls.toggle(),
280
+ requestExit: () => inputController.requestExit(),
281
+
282
+ close: async () => {
283
+ spinnerStatus.stop();
284
+ retryStatus.stop();
285
+ if (started) {
286
+ await terminal.drainInput?.();
287
+ if (mouseOn) terminal.write("\x1b[?1002l\x1b[?1006l");
288
+ tui.stop();
289
+ terminal.write("\x1b[?1049l");
290
+ }
291
+ },
292
+ };
293
+ }
294
+
295
+ export function createUI({ json, cwd = process.cwd(), keybindings, promptTemplates = [], shellRuntime = null, historyStore = null } = {}) {
296
+ if (json) return createJsonUI();
297
+ if (!stdout.isTTY) return createPlainUI();
298
+ return createTuiUI({ cwd, keybindings, promptTemplates, shellRuntime, historyStore });
299
+ }
@@ -0,0 +1,73 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export function globalConfigJsonPath(homeDir = homedir()) {
6
+ return join(homeDir, ".march", "config.json");
7
+ }
8
+
9
+ export function readConfigJson(path) {
10
+ if (!existsSync(path)) return {};
11
+ try {
12
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
13
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ export function writeConfigJson(path, data) {
20
+ mkdirSync(dirname(path), { recursive: true });
21
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
22
+ }
23
+
24
+ export function upsertProviderProfile({ path = globalConfigJsonPath(), id, type, auth }) {
25
+ const config = readConfigJson(path);
26
+ const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers)
27
+ ? config.providers
28
+ : {};
29
+ providers[id] = {
30
+ ...(providers[id] ?? {}),
31
+ type,
32
+ auth,
33
+ };
34
+ config.providers = providers;
35
+ delete config.provider;
36
+ delete config.model;
37
+ writeConfigJson(path, config);
38
+ return config;
39
+ }
40
+
41
+ export function upsertModelSelection({ path = globalConfigJsonPath(), provider, model, serviceTier }) {
42
+ const config = readConfigJson(path);
43
+ config.provider = provider;
44
+ config.model = model;
45
+ if (serviceTier) {
46
+ config.serviceTier = serviceTier;
47
+ } else {
48
+ delete config.serviceTier;
49
+ }
50
+ writeConfigJson(path, config);
51
+ return config;
52
+ }
53
+
54
+ export function upsertWebSearchProvider({ path = globalConfigJsonPath(), id, apiKey }) {
55
+ const config = readConfigJson(path);
56
+ const webSearch = config.webSearch && typeof config.webSearch === "object" && !Array.isArray(config.webSearch)
57
+ ? config.webSearch
58
+ : {};
59
+ const providers = webSearch.providers && typeof webSearch.providers === "object" && !Array.isArray(webSearch.providers)
60
+ ? webSearch.providers
61
+ : {};
62
+ providers[id] = {
63
+ ...(providers[id] ?? {}),
64
+ apiKey,
65
+ };
66
+ config.webSearch = {
67
+ ...webSearch,
68
+ provider: id,
69
+ providers,
70
+ };
71
+ writeConfigJson(path, config);
72
+ return config;
73
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export function loadDotEnv(cwd, { homeDir = homedir(), sourceDir = dirname(dirname(fileURLToPath(import.meta.url))) } = {}) {
7
+ for (const dir of [cwd, join(homeDir, ".march"), sourceDir]) {
8
+ const path = join(dir, ".env");
9
+ if (!existsSync(path)) continue;
10
+ for (const line of readFileSync(path, "utf-8").split(/\r?\n/)) {
11
+ const trimmed = line.trim();
12
+ if (!trimmed || trimmed.startsWith("#")) continue;
13
+ const eq = trimmed.indexOf("=");
14
+ if (eq < 1) continue;
15
+ const key = trimmed.slice(0, eq).trim();
16
+ const val = trimmed.slice(eq + 1).trim();
17
+ if (!process.env[key]) process.env[key] = val;
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,75 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ const DEFAULTS = Object.freeze({
6
+ "experimental.mcp": true,
7
+ "experimental.web_search": true,
8
+ "experimental.web_fetch": true,
9
+ "experimental.shell": true,
10
+ "experimental.permissions": true,
11
+ "ui.markdown_rendering": false,
12
+ "ui.tool_expand_per_card": false,
13
+ "agent.plan_mode": false,
14
+ "agent.sub_agents": false,
15
+ "agent.background_tasks": false,
16
+ "agent.auto_retry": true,
17
+ });
18
+
19
+ /**
20
+ * Load feature flags from ~/.march/features.toml.
21
+ * Returns a frozen object with all known flags resolved to booleans.
22
+ * Unknown keys in the file are ignored.
23
+ *
24
+ * TOML format (subset):
25
+ * # comment
26
+ * [section]
27
+ * key = true
28
+ * key = false
29
+ */
30
+ export function loadFeatureFlags({
31
+ homeDir = homedir(),
32
+ readFileSyncImpl = readFileSync,
33
+ existsSyncImpl = existsSync,
34
+ } = {}) {
35
+ const path = join(homeDir, ".march", "features.toml");
36
+ const overrides = loadToml(path, { readFileSyncImpl, existsSyncImpl });
37
+ const resolved = { ...DEFAULTS };
38
+ for (const [key, value] of Object.entries(overrides)) {
39
+ if (key in resolved) resolved[key] = Boolean(value);
40
+ }
41
+ return Object.freeze(resolved);
42
+ }
43
+
44
+ export function isEnabled(flags, flag) {
45
+ return Boolean(flags?.[flag]);
46
+ }
47
+
48
+ // ── Minimal TOML subset parser (sections, booleans, comments) ──
49
+ function loadToml(path, { readFileSyncImpl, existsSyncImpl }) {
50
+ if (!existsSyncImpl(path)) return {};
51
+ const src = readFileSyncImpl(path, "utf8");
52
+ const result = {};
53
+ let section = "";
54
+ for (const raw of src.split(/\r?\n/)) {
55
+ const line = raw.trim();
56
+ if (!line || line.startsWith("#")) continue;
57
+
58
+ // [section]
59
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
60
+ if (sectionMatch) {
61
+ section = sectionMatch[1].trim() + ".";
62
+ continue;
63
+ }
64
+
65
+ // key = value
66
+ const eq = line.indexOf("=");
67
+ if (eq < 1) continue;
68
+ const key = (section + line.slice(0, eq).trim());
69
+ const value = line.slice(eq + 1).trim();
70
+ if (value === "true") result[key] = true;
71
+ else if (value === "false") result[key] = false;
72
+ // ignore non-boolean values
73
+ }
74
+ return result;
75
+ }