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,42 @@
1
+ export function buildSessionTree(sessions) {
2
+ const nodes = new Map();
3
+ for (const session of sessions) {
4
+ nodes.set(session.id, { ...session, children: [] });
5
+ }
6
+
7
+ const roots = [];
8
+ for (const node of nodes.values()) {
9
+ const parent = node.parentSessionId ? nodes.get(node.parentSessionId) : null;
10
+ if (parent) {
11
+ parent.children.push(node);
12
+ } else {
13
+ roots.push(node);
14
+ }
15
+ }
16
+
17
+ const bySavedAtDesc = (a, b) => (b.savedAt ?? "").localeCompare(a.savedAt ?? "");
18
+ const sortDeep = (items) => {
19
+ items.sort(bySavedAtDesc);
20
+ for (const item of items) sortDeep(item.children);
21
+ };
22
+ sortDeep(roots);
23
+ return roots;
24
+ }
25
+
26
+ export function formatSessionTree(sessions, currentSessionId = null) {
27
+ const roots = buildSessionTree(sessions);
28
+ if (roots.length === 0) return ["(no saved sessions)"];
29
+
30
+ const lines = [];
31
+ const visit = (node, depth) => {
32
+ const marker = node.id === currentSessionId ? "*" : "-";
33
+ const savedAt = node.savedAt?.slice(0, 19) ?? "?";
34
+ const indent = " ".repeat(depth);
35
+ lines.push(`${indent}${marker} ${node.id} ${node.turnCount ?? 0}t ${savedAt}`);
36
+ for (const child of node.children) visit(child, depth + 1);
37
+ };
38
+
39
+ for (const root of roots) visit(root, 0);
40
+ lines.push("(* = current session)");
41
+ return lines;
42
+ }
@@ -0,0 +1,11 @@
1
+ import { createShellRuntime } from "./runtime.mjs";
2
+ import { createNodePtyAdapterFactory } from "./node-pty-adapter.mjs";
3
+
4
+ export function createCliShellRuntime({ cwd = process.cwd(), env = process.env } = {}) {
5
+ return createShellRuntime({
6
+ createPty: createNodePtyAdapterFactory({
7
+ defaultCwd: cwd,
8
+ defaultEnv: env,
9
+ }),
10
+ });
11
+ }
@@ -0,0 +1,12 @@
1
+ export function formatShellHints(shellRuntime, { limit = 5 } = {}) {
2
+ const shells = (shellRuntime?.listShells?.() ?? []).filter((shell) => shell.status !== "killed").slice(0, limit);
3
+ if (shells.length === 0) return "";
4
+ const lines = ["[shell_hints]"];
5
+ for (const shell of shells) {
6
+ const args = shell.args?.length ? ` ${shell.args.join(" ")}` : "";
7
+ const count = shell.scrollbackLineCount ?? shell.lineCount ?? 0;
8
+ lines.push(`- ${shell.id} ${shell.name} ${shell.status} command: ${shell.command}${args} cwd: ${shell.cwd} lines: ${count}`);
9
+ }
10
+ lines.push("Use terminal_read or terminal_snapshot to inspect shell output.");
11
+ return lines.join("\n");
12
+ }
@@ -0,0 +1,81 @@
1
+ import pty from "node-pty";
2
+
3
+ export function createNodePtyAdapterFactory({
4
+ ptyModule = pty,
5
+ defaultCwd = process.cwd(),
6
+ defaultEnv = process.env,
7
+ platform = process.platform,
8
+ } = {}) {
9
+ return function createNodePtyAdapter({
10
+ command,
11
+ args = [],
12
+ cwd = defaultCwd,
13
+ env = defaultEnv,
14
+ cols = 80,
15
+ rows = 24,
16
+ onData,
17
+ onExit,
18
+ onError,
19
+ }) {
20
+ const resolved = resolveShellCommand({ command, args, platform });
21
+ let term;
22
+ try {
23
+ term = ptyModule.spawn(resolved.command, resolved.args, {
24
+ name: "xterm-color",
25
+ cols,
26
+ rows,
27
+ cwd,
28
+ env,
29
+ });
30
+ } catch (error) {
31
+ onError?.(error);
32
+ throw error;
33
+ }
34
+
35
+ let disposed = false;
36
+ const disposeTerminal = () => {
37
+ if (disposed) return;
38
+ disposed = true;
39
+ // node-pty's Windows kill path can emit noisy AttachConsole failures after
40
+ // a natural exit. Closing the backing socket releases Node handles without
41
+ // forcing the PTY helper down the kill path.
42
+ if (typeof term._socket?.destroy === "function") {
43
+ term._socket.destroy();
44
+ } else if (typeof term.destroy === "function") {
45
+ term.destroy();
46
+ } else if (typeof term.kill === "function") {
47
+ term.kill();
48
+ }
49
+ };
50
+
51
+ term.onData?.((chunk) => onData?.(chunk));
52
+ term.onExit?.((event) => {
53
+ onExit?.(event);
54
+ disposeTerminal();
55
+ });
56
+
57
+ return {
58
+ write: (text) => term.write(String(text ?? "")),
59
+ resize: (nextCols, nextRows) => term.resize?.(nextCols, nextRows),
60
+ kill: () => disposeTerminal(),
61
+ dispose: () => disposeTerminal(),
62
+ };
63
+ };
64
+ }
65
+
66
+ export function resolveShellCommand({ command, args = [], platform = process.platform }) {
67
+ if (command) return { command: normalizeWindowsShellCommand(command, platform), args: [...args] };
68
+ if (platform === "win32") {
69
+ return { command: "powershell.exe", args: ["-NoLogo", "-NoProfile"] };
70
+ }
71
+ return { command: process.env.SHELL || "sh", args: [] };
72
+ }
73
+
74
+ function normalizeWindowsShellCommand(command, platform) {
75
+ if (platform !== "win32") return command;
76
+ const normalized = String(command).trim().toLowerCase();
77
+ if (normalized === "powershell") return "powershell.exe";
78
+ if (normalized === "pwsh") return "pwsh.exe";
79
+ if (normalized === "cmd") return "cmd.exe";
80
+ return command;
81
+ }
@@ -0,0 +1,126 @@
1
+ const OSC_RE = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
2
+ const ANSI_RE = /\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
3
+
4
+ export function findShellIdByName(shells, name) {
5
+ for (const shell of shells.values()) {
6
+ if (shell.name === name) return shell.id;
7
+ }
8
+ return null;
9
+ }
10
+
11
+ export function closeShellForReplacement(shells, shell) {
12
+ try {
13
+ shell.pty?.kill?.();
14
+ shell.screen?.dispose?.();
15
+ } finally {
16
+ shells.delete(shell.id);
17
+ }
18
+ }
19
+
20
+ export function stripAnsi(text) {
21
+ return String(text ?? "").replace(ANSI_RE, "");
22
+ }
23
+
24
+ export function appendOutput(shell, chunk, maxScrollbackLines) {
25
+ const raw = stripControlPayloads(String(chunk ?? ""));
26
+ shell.screen?.write?.(raw);
27
+ shell.rawChunks.push(raw);
28
+ if (shell.rawChunks.length > maxScrollbackLines * 4) {
29
+ shell.rawChunks = shell.rawChunks.slice(-(maxScrollbackLines * 4));
30
+ }
31
+
32
+ const plain = stripAnsi(raw).replace(/\r/g, "");
33
+ for (const line of plain.split("\n")) {
34
+ if (line === "") continue;
35
+ shell.plainLines.push(line);
36
+ }
37
+ if (shell.plainLines.length > maxScrollbackLines) {
38
+ shell.plainLines = shell.plainLines.slice(-maxScrollbackLines);
39
+ }
40
+ }
41
+
42
+ export function isPromptNoise(line, shell) {
43
+ const text = String(line ?? "").trim();
44
+ if (!text) return false;
45
+ if (process.platform === "win32" || /powershell|pwsh|cmd/i.test(shell.command)) {
46
+ if (/^(PS [A-Za-z]:\\.*>|[A-Za-z]:\\.*>)\s*$/.test(text)) return true;
47
+ if (/^(PS [A-Za-z]:\\.*>|[A-Za-z]:\\.*>)\s+/.test(text)) return true;
48
+ }
49
+ return /^[^#$>\n]{0,80}[$#>]\s*$/.test(text);
50
+ }
51
+
52
+ export function markExited(shell, event) {
53
+ if (shell.status === "killed") return;
54
+ shell.status = "exited";
55
+ shell.exitCode = event.exitCode ?? null;
56
+ shell.signal = event.signal ?? null;
57
+ shell.updatedAt = new Date().toISOString();
58
+ }
59
+
60
+ export function markFailed(shell, error) {
61
+ shell.status = "failed";
62
+ shell.error = error?.message ?? String(error);
63
+ shell.updatedAt = new Date().toISOString();
64
+ }
65
+
66
+ export function touch(shell, now) {
67
+ shell.updatedAt = now().toISOString();
68
+ }
69
+
70
+ export function requireShell(shells, id) {
71
+ const shell = shells.get(id);
72
+ if (!shell) throw new Error(`shell not found: ${id}`);
73
+ return shell;
74
+ }
75
+
76
+ export function publicShell(shell) {
77
+ const screenSnapshot = shell.screen?.snapshot?.() ?? null;
78
+ const visibleLineCount = screenSnapshot?.plain
79
+ ? screenSnapshot.plain.split("\n").filter((line) => line.length > 0).length
80
+ : 0;
81
+ return {
82
+ id: shell.id,
83
+ name: shell.name,
84
+ command: shell.command,
85
+ args: [...shell.args],
86
+ cwd: shell.cwd,
87
+ status: shell.status,
88
+ exitCode: shell.exitCode,
89
+ signal: shell.signal,
90
+ error: shell.error,
91
+ cols: shell.cols,
92
+ rows: shell.rows,
93
+ createdAt: shell.createdAt,
94
+ updatedAt: shell.updatedAt,
95
+ lineCount: shell.plainLines.length,
96
+ scrollbackLineCount: shell.plainLines.length,
97
+ visibleLineCount,
98
+ };
99
+ }
100
+
101
+ export function normalizeSize({ cols, rows, fallbackCols, fallbackRows }) {
102
+ return {
103
+ cols: normalizePositiveInt(cols, fallbackCols),
104
+ rows: normalizePositiveInt(rows, fallbackRows),
105
+ };
106
+ }
107
+
108
+ export function uniqueName(baseName, shells) {
109
+ const normalized = String(baseName || "shell").trim() || "shell";
110
+ const names = new Set([...shells.values()].map((shell) => shell.name));
111
+ if (!names.has(normalized)) return normalized;
112
+ for (let index = 2; ; index++) {
113
+ const candidate = `${normalized}-${index}`;
114
+ if (!names.has(candidate)) return candidate;
115
+ }
116
+ }
117
+
118
+ function stripControlPayloads(text) {
119
+ return String(text ?? "").replace(OSC_RE, "");
120
+ }
121
+
122
+ function normalizePositiveInt(value, fallback) {
123
+ const number = Number(value);
124
+ if (!Number.isFinite(number) || number <= 0) return Math.max(1, Math.trunc(Number(fallback) || 1));
125
+ return Math.max(1, Math.trunc(number));
126
+ }
@@ -0,0 +1,244 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createTerminalScreenBuffer } from "./screen-buffer.mjs";
3
+ import {
4
+ appendOutput,
5
+ closeShellForReplacement,
6
+ findShellIdByName,
7
+ isPromptNoise,
8
+ markExited,
9
+ markFailed,
10
+ normalizeSize,
11
+ publicShell,
12
+ requireShell,
13
+ stripAnsi,
14
+ touch,
15
+ uniqueName,
16
+ } from "./runtime-state.mjs";
17
+
18
+ export { stripAnsi } from "./runtime-state.mjs";
19
+
20
+ export function createShellRuntime({
21
+ createPty,
22
+ now = () => new Date(),
23
+ maxScrollbackLines = 200,
24
+ idFactory = () => randomUUID().slice(0, 8),
25
+ defaultCommand = process.platform === "win32" ? "powershell.exe" : process.env.SHELL || "sh",
26
+ defaultArgs = process.platform === "win32" ? ["-NoLogo", "-NoProfile"] : [],
27
+ defaultCols = 120,
28
+ defaultRows = 24,
29
+ createScreenBuffer = createTerminalScreenBuffer,
30
+ } = {}) {
31
+ if (typeof createPty !== "function") {
32
+ throw new Error("createPty is required");
33
+ }
34
+
35
+ const shells = new Map();
36
+
37
+ function spawnShell({
38
+ name,
39
+ command,
40
+ args = [],
41
+ cwd = process.cwd(),
42
+ env = process.env,
43
+ cols = defaultCols,
44
+ rows = defaultRows,
45
+ nameConflict = "suffix",
46
+ } = {}) {
47
+ const resolvedCommand = command || defaultCommand;
48
+ const resolvedArgs = command ? args : (args.length ? args : defaultArgs);
49
+ const baseName = String(name || resolvedCommand || "shell").trim() || "shell";
50
+ const existing = shells.get(findShellIdByName(shells, baseName));
51
+ if (existing && nameConflict === "reuse") {
52
+ return publicShell(existing);
53
+ }
54
+ if (existing && nameConflict === "replace") {
55
+ closeShellForReplacement(shells, existing);
56
+ }
57
+ const size = normalizeSize({ cols, rows, fallbackCols: defaultCols, fallbackRows: defaultRows });
58
+ const id = idFactory();
59
+ const shell = {
60
+ id,
61
+ name: uniqueName(baseName, shells),
62
+ command: resolvedCommand,
63
+ args: [...resolvedArgs],
64
+ cwd,
65
+ status: "starting",
66
+ exitCode: null,
67
+ signal: null,
68
+ error: null,
69
+ cols: size.cols,
70
+ rows: size.rows,
71
+ createdAt: now().toISOString(),
72
+ updatedAt: now().toISOString(),
73
+ rawChunks: [],
74
+ plainLines: [],
75
+ screen: createScreenBuffer({ cols: size.cols, rows: size.rows }),
76
+ pty: null,
77
+ };
78
+ shells.set(id, shell);
79
+
80
+ try {
81
+ shell.pty = createPty({
82
+ command: resolvedCommand,
83
+ args: shell.args,
84
+ cwd,
85
+ env,
86
+ cols: shell.cols,
87
+ rows: shell.rows,
88
+ onData: (chunk) => appendOutput(shell, chunk, maxScrollbackLines),
89
+ onExit: (event = {}) => markExited(shell, event),
90
+ onError: (error) => markFailed(shell, error),
91
+ });
92
+ shell.status = "running";
93
+ touch(shell, now);
94
+ } catch (error) {
95
+ markFailed(shell, error);
96
+ }
97
+
98
+ return publicShell(shell);
99
+ }
100
+
101
+ function sendShell(id, text) {
102
+ const shell = requireShell(shells, id);
103
+ if (shell.status !== "running") {
104
+ return { ok: false, error: `shell ${id} is ${shell.status}`, shell: publicShell(shell) };
105
+ }
106
+ shell.pty.write(String(text ?? ""));
107
+ touch(shell, now);
108
+ return { ok: true, shell: publicShell(shell) };
109
+ }
110
+
111
+ function resizeShell(id, { cols, rows } = {}) {
112
+ const shell = requireShell(shells, id);
113
+ const size = normalizeSize({ cols, rows, fallbackCols: shell.cols, fallbackRows: shell.rows });
114
+ if (shell.cols === size.cols && shell.rows === size.rows) {
115
+ return { ok: true, changed: false, shell: publicShell(shell) };
116
+ }
117
+ if (shell.status !== "running" && shell.status !== "starting") {
118
+ return { ok: false, changed: false, error: `shell ${id} is ${shell.status}`, shell: publicShell(shell) };
119
+ }
120
+ if (typeof shell.pty?.resize !== "function") {
121
+ return { ok: false, changed: false, error: `shell ${id} does not support resize`, shell: publicShell(shell) };
122
+ }
123
+ try {
124
+ shell.pty.resize(size.cols, size.rows);
125
+ } catch (error) {
126
+ shell.status = "failed";
127
+ shell.error = `resize failed: ${error?.message ?? String(error)}`;
128
+ touch(shell, now);
129
+ return { ok: false, changed: true, error: shell.error, shell: publicShell(shell) };
130
+ }
131
+ shell.cols = size.cols;
132
+ shell.rows = size.rows;
133
+ shell.screen?.resize?.(shell.cols, shell.rows);
134
+ touch(shell, now);
135
+ return { ok: true, changed: true, shell: publicShell(shell) };
136
+ }
137
+
138
+ function killShell(id) {
139
+ const shell = requireShell(shells, id);
140
+ if (shell.status !== "running" && shell.status !== "starting") {
141
+ return { ok: false, error: `shell ${id} is ${shell.status}`, shell: publicShell(shell) };
142
+ }
143
+ shell.status = "killed";
144
+ touch(shell, now);
145
+ try {
146
+ shell.pty?.kill?.();
147
+ } catch (error) {
148
+ shell.status = "failed";
149
+ shell.error = `kill failed: ${error?.message ?? String(error)}`;
150
+ touch(shell, now);
151
+ return { ok: false, error: shell.error, shell: publicShell(shell) };
152
+ }
153
+ return { ok: true, shell: publicShell(shell) };
154
+ }
155
+
156
+ function listShells() {
157
+ return [...shells.values()].map(publicShell);
158
+ }
159
+
160
+ function getShell(id) {
161
+ const shell = shells.get(id);
162
+ return shell ? publicShell(shell) : null;
163
+ }
164
+
165
+ function searchShell(id, pattern, { source = "auto", includePrompts = false } = {}) {
166
+ const shell = requireShell(shells, id);
167
+ const needle = String(pattern ?? "");
168
+ const screenLines = shell.screen?.snapshot?.().plain?.split("\n") ?? [];
169
+ const scrollbackLines = shell.plainLines;
170
+ const lines = source === "screen" ? screenLines : source === "scrollback" ? scrollbackLines : screenLines;
171
+ let matches = findLineMatches(lines, needle, shell, includePrompts);
172
+ let resolvedSource = source === "auto" ? "screen" : source;
173
+ if (source === "auto" && matches.length === 0) {
174
+ matches = findLineMatches(scrollbackLines, needle, shell, includePrompts);
175
+ resolvedSource = "scrollback";
176
+ }
177
+ return { shell: publicShell(shell), matches, source: resolvedSource };
178
+ }
179
+
180
+ function findLineMatches(lines, needle, shell, includePrompts) {
181
+ return lines
182
+ .map((line, index) => ({ index, line }))
183
+ .filter(({ line }) => includePrompts || !isPromptNoise(line, shell))
184
+ .filter(({ line }) => line.includes(needle));
185
+ }
186
+
187
+ function snapshotShell(id) {
188
+ const shell = requireShell(shells, id);
189
+ return {
190
+ shell: publicShell(shell),
191
+ ansi: shell.rawChunks.join(""),
192
+ plain: shell.plainLines.join("\n"),
193
+ screen: shell.screen?.snapshot?.() ?? null,
194
+ };
195
+ }
196
+
197
+ function clearShell(id) {
198
+ const shell = requireShell(shells, id);
199
+ shell.rawChunks = [];
200
+ shell.plainLines = [];
201
+ shell.screen?.dispose?.();
202
+ shell.screen = createScreenBuffer({ cols: shell.cols, rows: shell.rows });
203
+ touch(shell, now);
204
+ return { ok: true, shell: publicShell(shell) };
205
+ }
206
+
207
+ function killAll() {
208
+ const results = [];
209
+ for (const shell of shells.values()) {
210
+ if (shell.status === "running" || shell.status === "starting") {
211
+ results.push(killShell(shell.id));
212
+ }
213
+ }
214
+ return results;
215
+ }
216
+
217
+ function dispose() {
218
+ const results = killAll();
219
+ for (const shell of shells.values()) {
220
+ try {
221
+ shell.pty?.dispose?.();
222
+ shell.screen?.dispose?.();
223
+ } catch (error) {
224
+ shell.error = `dispose failed: ${error?.message ?? String(error)}`;
225
+ touch(shell, now);
226
+ }
227
+ }
228
+ return results;
229
+ }
230
+
231
+ return {
232
+ spawnShell,
233
+ sendShell,
234
+ resizeShell,
235
+ killShell,
236
+ killAll,
237
+ listShells,
238
+ getShell,
239
+ searchShell,
240
+ snapshotShell,
241
+ clearShell,
242
+ dispose,
243
+ };
244
+ }
@@ -0,0 +1,136 @@
1
+ import headless from "@xterm/headless";
2
+
3
+ const { Terminal } = headless;
4
+
5
+ export function createTerminalScreenBuffer({ cols = 80, rows = 24 } = {}) {
6
+ const terminal = new Terminal({
7
+ allowProposedApi: true,
8
+ cols: normalizePositiveInt(cols, 80),
9
+ rows: normalizePositiveInt(rows, 24),
10
+ scrollback: 0,
11
+ });
12
+
13
+ let pendingWrites = 0;
14
+
15
+ return {
16
+ write(data) {
17
+ pendingWrites += 1;
18
+ terminal.write(String(data ?? ""), () => {
19
+ pendingWrites = Math.max(0, pendingWrites - 1);
20
+ });
21
+ },
22
+ resize(nextCols, nextRows) {
23
+ terminal.resize(
24
+ normalizePositiveInt(nextCols, terminal.cols),
25
+ normalizePositiveInt(nextRows, terminal.rows),
26
+ );
27
+ },
28
+ snapshot() {
29
+ const lines = readViewportLines(terminal);
30
+ return {
31
+ cols: terminal.cols,
32
+ rows: terminal.rows,
33
+ pendingWrites,
34
+ plain: lines.map((line) => line.plain).join("\n"),
35
+ ansi: lines.map((line) => line.ansi).join("\n"),
36
+ };
37
+ },
38
+ dispose() {
39
+ terminal.dispose();
40
+ },
41
+ };
42
+ }
43
+
44
+ function readViewportLines(terminal) {
45
+ const buffer = terminal.buffer.active;
46
+ const start = buffer.baseY;
47
+ const rows = [];
48
+ for (let y = start; y < start + terminal.rows; y++) {
49
+ const line = buffer.getLine(y);
50
+ rows.push(line ? lineToSnapshot(line) : { plain: "", ansi: "" });
51
+ }
52
+ return trimTrailingBlankRows(rows);
53
+ }
54
+
55
+ function lineToSnapshot(line) {
56
+ const end = findContentEnd(line);
57
+ if (end === 0) return { plain: "", ansi: "" };
58
+
59
+ const plain = line.translateToString(false, 0, end);
60
+ let ansi = "";
61
+ let activeStyle = "";
62
+ for (let x = 0; x < end; x++) {
63
+ const cell = line.getCell(x);
64
+ if (!cell || cell.getWidth() === 0) continue;
65
+ const chars = cell.getChars() || " ";
66
+ const style = cellToSgr(cell);
67
+ if (style !== activeStyle) {
68
+ if (activeStyle) ansi += "\x1b[0m";
69
+ if (style) ansi += style;
70
+ activeStyle = style;
71
+ }
72
+ ansi += chars;
73
+ }
74
+ if (activeStyle) ansi += "\x1b[0m";
75
+ return { plain, ansi };
76
+ }
77
+
78
+ function findContentEnd(line) {
79
+ for (let x = line.length - 1; x >= 0; x--) {
80
+ const cell = line.getCell(x);
81
+ if (cell?.getChars()) return x + cell.getWidth();
82
+ }
83
+ return 0;
84
+ }
85
+
86
+ function cellToSgr(cell) {
87
+ if (cell.isAttributeDefault()) return "";
88
+ const codes = [];
89
+ if (cell.isBold()) codes.push(1);
90
+ if (cell.isDim()) codes.push(2);
91
+ if (cell.isItalic()) codes.push(3);
92
+ if (cell.isUnderline()) codes.push(4);
93
+ if (cell.isBlink()) codes.push(5);
94
+ if (cell.isInverse()) codes.push(7);
95
+ if (cell.isInvisible()) codes.push(8);
96
+ if (cell.isStrikethrough()) codes.push(9);
97
+ if (cell.isOverline()) codes.push(53);
98
+ pushColorCodes(codes, cell, "fg");
99
+ pushColorCodes(codes, cell, "bg");
100
+ return codes.length ? `\x1b[${codes.join(";")}m` : "";
101
+ }
102
+
103
+ function pushColorCodes(codes, cell, target) {
104
+ const isFg = target === "fg";
105
+ const isDefault = isFg ? cell.isFgDefault() : cell.isBgDefault();
106
+ if (isDefault) return;
107
+
108
+ const color = isFg ? cell.getFgColor() : cell.getBgColor();
109
+ const base = isFg ? 30 : 40;
110
+ const brightBase = isFg ? 90 : 100;
111
+ if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {
112
+ codes.push(isFg ? 38 : 48, 2, (color >> 16) & 255, (color >> 8) & 255, color & 255);
113
+ return;
114
+ }
115
+ if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
116
+ if (color < 8) {
117
+ codes.push(base + color);
118
+ } else if (color < 16) {
119
+ codes.push(brightBase + color - 8);
120
+ } else {
121
+ codes.push(isFg ? 38 : 48, 5, color);
122
+ }
123
+ }
124
+ }
125
+
126
+ function trimTrailingBlankRows(rows) {
127
+ let end = rows.length;
128
+ while (end > 0 && rows[end - 1].plain === "") end -= 1;
129
+ return rows.slice(0, end);
130
+ }
131
+
132
+ function normalizePositiveInt(value, fallback) {
133
+ const number = Number(value);
134
+ if (!Number.isFinite(number) || number <= 0) return Math.max(1, Math.trunc(Number(fallback) || 1));
135
+ return Math.max(1, Math.trunc(number));
136
+ }