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,103 @@
1
+ // ── Permission categories ──────────────────────────────────────────────
2
+ export const PERM = Object.freeze({
3
+ READ_ONLY: "read_only",
4
+ FILE_WRITE: "file_write",
5
+ COMMAND_EXEC: "command_exec",
6
+ NETWORK_EXTERNAL: "network_external",
7
+ });
8
+
9
+ const CATEGORY_LABEL = {
10
+ [PERM.READ_ONLY]: "read-only",
11
+ [PERM.FILE_WRITE]: "file write",
12
+ [PERM.COMMAND_EXEC]: "command exec",
13
+ [PERM.NETWORK_EXTERNAL]: "network",
14
+ };
15
+
16
+ export function permissionLabel(cat) {
17
+ return CATEGORY_LABEL[cat] ?? cat;
18
+ }
19
+
20
+ // ── Permitted modes ────────────────────────────────────────────────────
21
+ export const MODE = Object.freeze({
22
+ DEFAULT: "default",
23
+ DONT_ASK: "dontAsk",
24
+ BYPASS: "bypassPermissions",
25
+ ACCEPT_EDITS: "acceptEdits",
26
+ });
27
+
28
+ // ── Default tool → category mapping ────────────────────────────────────
29
+ const DEFAULT_CATEGORIES = {
30
+ context_stats: PERM.READ_ONLY,
31
+ find: PERM.READ_ONLY,
32
+ edit_file: PERM.FILE_WRITE,
33
+ command_exec: PERM.COMMAND_EXEC,
34
+ terminal_spawn: PERM.COMMAND_EXEC,
35
+ terminal_send: PERM.COMMAND_EXEC,
36
+ terminal_list: PERM.COMMAND_EXEC,
37
+ terminal_kill: PERM.COMMAND_EXEC,
38
+ terminal_resize: PERM.COMMAND_EXEC,
39
+ terminal_clear: PERM.COMMAND_EXEC,
40
+ terminal_search: PERM.COMMAND_EXEC,
41
+ terminal_read: PERM.COMMAND_EXEC,
42
+ terminal_snapshot: PERM.COMMAND_EXEC,
43
+ web_search: PERM.NETWORK_EXTERNAL,
44
+ web_fetch: PERM.NETWORK_EXTERNAL,
45
+ };
46
+
47
+ export function createPermissionController({
48
+ mode = MODE.DEFAULT,
49
+ toolCategories = {},
50
+ onRequestApproval = null,
51
+ } = {}) {
52
+ const sessionApprovals = new Map();
53
+ const categories = { ...DEFAULT_CATEGORIES, ...toolCategories };
54
+
55
+ function getCategory(toolName) {
56
+ if (categories[toolName] !== undefined) return categories[toolName];
57
+ // MCP and unknown tools: default to most restrictive
58
+ if (toolName.startsWith("mcp__")) return PERM.NETWORK_EXTERNAL;
59
+ return PERM.COMMAND_EXEC;
60
+ }
61
+
62
+ function setCategory(toolName, category) {
63
+ categories[toolName] = category;
64
+ }
65
+
66
+ function check(toolName) {
67
+ const category = getCategory(toolName);
68
+
69
+ if (category === PERM.READ_ONLY) return { behavior: "allow" };
70
+ if (mode === MODE.BYPASS) return { behavior: "allow" };
71
+ if (mode === MODE.DONT_ASK) {
72
+ return { behavior: "deny", message: `Tool '${toolName}' requires ${permissionLabel(category)} permission, but permission mode is 'dontAsk'.` };
73
+ }
74
+ if (sessionApprovals.has(toolName)) return { behavior: "allow" };
75
+
76
+ return { behavior: "ask", category, toolName };
77
+ }
78
+
79
+ function approve(toolName) {
80
+ sessionApprovals.set(toolName, true);
81
+ }
82
+
83
+ function isApproved(toolName) {
84
+ return sessionApprovals.has(toolName);
85
+ }
86
+
87
+ async function requestApproval(toolName, params, requestFn) {
88
+ const decision = check(toolName);
89
+ if (decision.behavior !== "ask") return decision;
90
+ if (!requestFn) return decision;
91
+ const ok = await requestFn({ toolName, params, category: decision.category });
92
+ if (ok) {
93
+ approve(toolName);
94
+ return { behavior: "allow" };
95
+ }
96
+ return {
97
+ behavior: "deny",
98
+ message: `User denied ${toolName} (requires ${permissionLabel(decision.category)} permission).`,
99
+ };
100
+ }
101
+
102
+ return { check, approve, isApproved, getCategory, setCategory, requestApproval, get mode() { return mode; } };
103
+ }
@@ -0,0 +1,86 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { DEFAULT_KEYBINDINGS, KEYBINDING_ACTIONS } from "./input/keybindings.mjs";
3
+ import { dim, red } from "./tui/ui-theme.mjs";
4
+
5
+ const HOTKEY_GROUPS = Object.freeze([
6
+ ["Turn control", ["abort", "interrupt"]],
7
+ ["Model and thinking", ["cycleThinking", "thinkingSelector", "modelSelector"]],
8
+ ["Editor and output", ["externalEditor", "toggleToolOutput", "pasteImage"]],
9
+ ["Shell pane", ["toggleShellDrawer", "nextShell", "shellScrollUp", "shellScrollDown"]],
10
+ ["Output scroll", ["outputScrollUp", "outputScrollDown"]],
11
+ ]);
12
+
13
+ export function parseInlineShellInput(input, lastCommand = "") {
14
+ if (input === "!!") {
15
+ if (!lastCommand) return { type: "error", message: "No previous inline shell command." };
16
+ return { type: "command", command: lastCommand, repeated: true };
17
+ }
18
+ if (!input.startsWith("!")) return { type: "none" };
19
+ const command = input.slice(1).trim();
20
+ if (!command) return { type: "error", message: "Usage: ! <command>" };
21
+ return { type: "command", command, repeated: false };
22
+ }
23
+
24
+ export function runInlineShellCommand(command, { cwd = process.cwd(), ui } = {}) {
25
+ const shell = process.platform === "win32"
26
+ ? { bin: "powershell.exe", args: ["-NoProfile", "-Command", command] }
27
+ : { bin: "bash", args: ["-lc", command] };
28
+ ui?.writeln(dim(`$ ${command}`));
29
+ const result = spawnSync(shell.bin, shell.args, {
30
+ cwd,
31
+ encoding: "utf8",
32
+ windowsHide: true,
33
+ maxBuffer: 1024 * 1024,
34
+ });
35
+ if (result.stdout) {
36
+ for (const line of result.stdout.replace(/\s+$/, "").split("\n")) {
37
+ if (line) ui?.writeln(line);
38
+ }
39
+ }
40
+ if (result.stderr) {
41
+ for (const line of result.stderr.replace(/\s+$/, "").split("\n")) {
42
+ if (line) ui?.writeln(red(line));
43
+ }
44
+ }
45
+ if (result.error) {
46
+ ui?.writeln(red(`Error: ${result.error.message}`));
47
+ } else if (result.status !== 0) {
48
+ ui?.writeln(red(`exit ${result.status}`));
49
+ }
50
+ return result;
51
+ }
52
+
53
+ export function formatHotkeysPanel(keybindings = DEFAULT_KEYBINDINGS, diagnostics = []) {
54
+ return [
55
+ "Keyboard shortcuts:",
56
+ ...formatGroupedKeybindingLines(keybindings),
57
+ ...formatKeybindingDiagnostics(diagnostics),
58
+ "Input prefixes:",
59
+ " / Slash command autocomplete",
60
+ " /session Restore a previous session",
61
+ " /thinking Choose or list/set thinking level",
62
+ " @ File path autocomplete",
63
+ " ! cmd Run local shell command without sending to the model",
64
+ " !! Repeat previous local shell command",
65
+ ];
66
+ }
67
+
68
+ export function formatGroupedKeybindingLines(keybindings = DEFAULT_KEYBINDINGS) {
69
+ return HOTKEY_GROUPS.flatMap(([label, actions]) => [
70
+ ` ${label}:`,
71
+ ...actions.map((action) => formatKeybindingLine(action, keybindings)),
72
+ ]);
73
+ }
74
+
75
+ function formatKeybindingLine(action, keybindings) {
76
+ const key = keybindings[action] ?? DEFAULT_KEYBINDINGS[action];
77
+ return ` ${key.padEnd(10, " ")} ${KEYBINDING_ACTIONS[action]}`;
78
+ }
79
+
80
+ function formatKeybindingDiagnostics(diagnostics) {
81
+ if (!diagnostics || diagnostics.length === 0) return [];
82
+ return [
83
+ "Keybinding diagnostics:",
84
+ ...diagnostics.map((diagnostic) => ` - ${diagnostic.type ?? "warning"}: ${diagnostic.message}`),
85
+ ];
86
+ }
@@ -0,0 +1,157 @@
1
+ import { brightBlack, inverse } from "./tui/ui-theme.mjs";
2
+ import { handleSlashCommand } from "./slash-commands.mjs";
3
+ import { appendModeReminder } from "./input/mode-state.mjs";
4
+ import { expandPromptTemplate } from "./input/prompt-templates.mjs";
5
+ import { parseInlineShellInput, runInlineShellCommand } from "./repl-commands.mjs";
6
+ import { formatRecallHints } from "../memory/markdown-store.mjs";
7
+ import { formatMessageAttachmentsForDisplay } from "../session/attachment-display.mjs";
8
+ import { formatShellHints } from "../shell/hints.mjs";
9
+
10
+ export async function runSingleShotPrompt({
11
+ prompt,
12
+ runner,
13
+ memoryStore,
14
+ currentProject,
15
+ ui,
16
+ sessionState,
17
+ refreshStatusBar,
18
+ modeState = null,
19
+ }) {
20
+ memoryStore.beginTurn();
21
+ const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
22
+ const recallBlock = formatRecallHints("user", userRecallHints);
23
+ const shellHints = formatShellHints(runner.shellRuntime);
24
+ const modePrompt = appendModeReminder(prompt, modeState?.get?.());
25
+ const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, shellHints);
26
+ ui.writeln(formatUserDisplayMessage(prompt));
27
+ ui.memoryHint?.({ source: "user", hints: userRecallHints });
28
+ refreshStatusBar.startWorking?.();
29
+ try {
30
+ await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
31
+ } finally {
32
+ refreshStatusBar.stopWorking?.();
33
+ memoryStore.endTurn();
34
+ }
35
+ refreshStatusBar();
36
+ }
37
+
38
+ export async function runInteractiveRepl({
39
+ cwd,
40
+ args,
41
+ ui,
42
+ runner,
43
+ memoryStore,
44
+ currentProject,
45
+ sessionState,
46
+ sessionsRoot,
47
+ projectMarchDir,
48
+ sessionSource,
49
+ extensionPaths,
50
+ keybindingConfig,
51
+ promptTemplateConfig,
52
+ renderStartupBanner = null,
53
+ refreshStatusBar,
54
+ setTurnRunning,
55
+ modeState = null,
56
+ configHomeDir,
57
+ }) {
58
+ let lastInlineShellCommand = "";
59
+
60
+ for (;;) {
61
+ const line = await ui.readline("> ");
62
+ if (line === null) break;
63
+ let trimmed = line.trim();
64
+ if (!trimmed) continue;
65
+
66
+ const handledInline = handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand });
67
+ if (handledInline.type === "handled") {
68
+ lastInlineShellCommand = handledInline.lastInlineShellCommand;
69
+ continue;
70
+ }
71
+ if (handledInline.type === "error") continue;
72
+
73
+ const slashResult = await handleSlashCommand(trimmed, {
74
+ ui,
75
+ runner,
76
+ sessionState,
77
+ sessionsRoot,
78
+ projectMarchDir,
79
+ sessionSource,
80
+ extensionPaths,
81
+ keybindings: keybindingConfig.keybindings,
82
+ keybindingDiagnostics: keybindingConfig.diagnostics,
83
+ promptTemplates: promptTemplateConfig.templates,
84
+ promptTemplateDiagnostics: promptTemplateConfig.diagnostics,
85
+ renderStartupBanner,
86
+ configHomeDir,
87
+ });
88
+ if (slashResult.exit) break;
89
+ if (slashResult.handled) {
90
+ refreshStatusBar();
91
+ continue;
92
+ }
93
+
94
+ const templateResult = expandPromptTemplate(trimmed, promptTemplateConfig.templates);
95
+ if (templateResult.type === "template") {
96
+ ui.writeln(brightBlack(`● template: ${templateResult.name}`));
97
+ trimmed = templateResult.prompt;
98
+ }
99
+
100
+ await runReplTurn({
101
+ prompt: trimmed,
102
+ args,
103
+ runner,
104
+ memoryStore,
105
+ currentProject,
106
+ ui,
107
+ refreshStatusBar,
108
+ setTurnRunning,
109
+ modeState,
110
+ });
111
+ }
112
+ }
113
+
114
+ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
115
+ const inlineShell = parseInlineShellInput(trimmed, lastInlineShellCommand);
116
+ if (inlineShell.type === "error") {
117
+ ui.writeln(`Error: ${inlineShell.message}`);
118
+ return { type: "error" };
119
+ }
120
+ if (inlineShell.type === "command") {
121
+ runInlineShellCommand(inlineShell.command, { cwd, ui });
122
+ return { type: "handled", lastInlineShellCommand: inlineShell.command };
123
+ }
124
+ return { type: "none" };
125
+ }
126
+
127
+ async function runReplTurn({ prompt, args, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
128
+ memoryStore.beginTurn();
129
+ const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
130
+ const recallBlock = formatRecallHints("user", userRecallHints);
131
+ const shellHints = formatShellHints(runner.shellRuntime);
132
+ const modePrompt = appendModeReminder(prompt, modeState?.get?.());
133
+ const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, shellHints);
134
+ try {
135
+ ui.writeln(formatUserDisplayMessage(prompt));
136
+ ui.memoryHint?.({ source: "user", hints: userRecallHints });
137
+ setTurnRunning(true);
138
+ refreshStatusBar.startWorking?.();
139
+ await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
140
+ ui.writeln("");
141
+ } catch (err) {
142
+ ui.writeln(`Error: ${err.message}`);
143
+ } finally {
144
+ setTurnRunning(false);
145
+ refreshStatusBar.stopWorking?.();
146
+ memoryStore.endTurn();
147
+ refreshStatusBar();
148
+ }
149
+ }
150
+
151
+ export function formatUserDisplayMessage(prompt) {
152
+ return `${inverse(" USER ")} ${formatMessageAttachmentsForDisplay(prompt)}`;
153
+ }
154
+
155
+ function appendPromptBlocks(prompt, ...blocks) {
156
+ return [prompt, ...blocks.filter(Boolean)].join("\n\n");
157
+ }
@@ -0,0 +1,21 @@
1
+ export function formatSelectorList({
2
+ items = [],
3
+ currentIndex = -1,
4
+ emptyMessage = "(no items)",
5
+ instruction,
6
+ linePrefix = "",
7
+ formatItem = (item) => String(item),
8
+ }) {
9
+ if (!items.length) return [emptyMessage];
10
+ const lines = items.map((item, index) => {
11
+ const mark = index === currentIndex ? "*" : " ";
12
+ return `${linePrefix}${mark} ${index + 1}. ${formatItem(item)}`;
13
+ });
14
+ if (instruction) lines.push(instruction);
15
+ return lines;
16
+ }
17
+
18
+ export function findCurrentIndex(items, predicate) {
19
+ if (!Array.isArray(items) || typeof predicate !== "function") return -1;
20
+ return items.findIndex(predicate);
21
+ }
@@ -0,0 +1,41 @@
1
+ import { loadPiSessionSidecar } from "../../session/sidecar.mjs";
2
+
3
+ export async function resumePiSessionById(id, { runner, sessions, projectMarchDir }) {
4
+ if (!runner.canSwitchPiSession?.()) {
5
+ return ["Error: pi session resume requires the pi runtime host"];
6
+ }
7
+
8
+ const matches = sessions.filter((session) => session.id.startsWith(id));
9
+ if (matches.length === 0) return [`Error: pi session not found: ${id}`];
10
+ if (matches.length > 1) {
11
+ return [`Error: pi session id is ambiguous: ${id} (${matches.map((session) => session.id).join(", ")})`];
12
+ }
13
+
14
+ const session = matches[0];
15
+ let sidecar;
16
+ try {
17
+ sidecar = loadPiSessionSidecar({ projectMarchDir, sessionRef: session.path });
18
+ } catch (err) {
19
+ return [`Error: pi session sidecar is invalid for ${session.id}: ${err.message}`];
20
+ }
21
+ if (!sidecar) {
22
+ return [`Error: pi session sidecar not found for ${session.id}; refusing partial resume`];
23
+ }
24
+ if (sidecar.state.cwd && sidecar.state.cwd !== runner.engine.cwd) {
25
+ return [`Error: pi session sidecar cwd mismatch for ${session.id}: ${sidecar.state.cwd}`];
26
+ }
27
+
28
+ let result;
29
+ try {
30
+ result = await runner.switchPiSession(session.path);
31
+ } catch (err) {
32
+ return [`Error: failed to switch pi session ${session.id}: ${err.message}`];
33
+ }
34
+ if (result?.cancelled) return [`Resume pi session cancelled: ${session.id}`];
35
+ runner.engine.restoreSession(toContextSessionState(sidecar.state), null, { replace: true });
36
+ return [`Resumed pi session: ${session.id}`];
37
+ }
38
+
39
+ function toContextSessionState(sidecarState) {
40
+ return { ...sidecarState };
41
+ }
@@ -0,0 +1,23 @@
1
+ export function formatSessionStats(stats) {
2
+ const lines = [
3
+ `session: ${stats.sessionId}`,
4
+ `messages: ${stats.userMessages}u + ${stats.assistantMessages}a + ${stats.toolCalls}t = ${stats.totalMessages} total`,
5
+ `tokens: ${stats.tokens.input} in / ${stats.tokens.output} out (${stats.tokens.cacheRead} cache read, ${stats.tokens.cacheWrite} cache write)`,
6
+ `cost: $${stats.cost.toFixed(4)}`,
7
+ ];
8
+ if (typeof stats.persisted === "boolean") {
9
+ const mode = stats.persisted ? "pi-jsonl" : "in-memory";
10
+ const suffix = stats.sessionFile ? ` (${stats.sessionFile})` : "";
11
+ lines.splice(1, 0, `persistence: ${mode}${suffix}`);
12
+ }
13
+ if (typeof stats.runtimeHost === "boolean") {
14
+ const mode = stats.runtimeHost ? "pi-runtime-host" : "direct-agent-session";
15
+ const insertAt = typeof stats.persisted === "boolean" ? 2 : 1;
16
+ lines.splice(insertAt, 0, `runtime: ${mode}`);
17
+ }
18
+ return lines;
19
+ }
20
+
21
+ export function listSessionStats({ runner }) {
22
+ return formatSessionStats(runner.getSessionStats());
23
+ }
@@ -0,0 +1,68 @@
1
+ import { formatSessionTree } from "../../session/tree.mjs";
2
+
3
+ export function formatSessionList(sessions, currentSessionId = null) {
4
+ if (sessions.length === 0) return ["(no saved sessions)"];
5
+ const lines = sessions.map((session) => {
6
+ const marker = session.id === currentSessionId ? " *" : " ";
7
+ const parent = session.parentSessionId ? ` fork:${session.parentSessionId}` : "";
8
+ return `${marker} ${session.id} ${session.turnCount}t ${session.cwd} ${session.savedAt?.slice(0, 19) ?? "?"}${parent}`;
9
+ });
10
+ lines.push("(* = current session)");
11
+ return lines;
12
+ }
13
+
14
+ export function listSessionCommand({ sessions, currentSessionId, tree = false }) {
15
+ if (tree) return formatSessionTree(sessions, currentSessionId);
16
+ return formatSessionList(sessions, currentSessionId);
17
+ }
18
+
19
+ export function formatPiSessionList(sessions) {
20
+ if (sessions.length === 0) return ["(no pi sessions)"];
21
+ const lines = sessions.map((session) => {
22
+ const label = session.name || session.firstMessage || "(no messages)";
23
+ const savedAt = session.savedAt?.slice(0, 19) ?? "?";
24
+ return ` ${session.id} ${session.turnCount}m ${savedAt} ${label}`;
25
+ });
26
+ lines.push("(pi JSONL session files; use /session to restore a previous session)");
27
+ return lines;
28
+ }
29
+
30
+ export function formatPiSessionTree(sessions, currentSessionId = null) {
31
+ if (sessions.length === 0) return ["(no pi sessions)"];
32
+
33
+ const nodes = new Map();
34
+ const byPath = new Map();
35
+ for (const session of sessions) {
36
+ const node = { ...session, children: [] };
37
+ nodes.set(session.id, node);
38
+ if (session.path) byPath.set(session.path, node);
39
+ }
40
+
41
+ const roots = [];
42
+ for (const node of nodes.values()) {
43
+ const parent = node.parentSessionPath ? byPath.get(node.parentSessionPath) : null;
44
+ if (parent) parent.children.push(node);
45
+ else roots.push(node);
46
+ }
47
+
48
+ const bySavedAtDesc = (a, b) => (b.savedAt ?? "").localeCompare(a.savedAt ?? "");
49
+ const sortDeep = (items) => {
50
+ items.sort(bySavedAtDesc);
51
+ for (const item of items) sortDeep(item.children);
52
+ };
53
+ sortDeep(roots);
54
+
55
+ const lines = [];
56
+ const visit = (node, depth) => {
57
+ const marker = node.id === currentSessionId ? "*" : "-";
58
+ const savedAt = node.savedAt?.slice(0, 19) ?? "?";
59
+ const label = node.name || node.firstMessage || "(no messages)";
60
+ const indent = " ".repeat(depth);
61
+ lines.push(`${indent}${marker} ${node.id} ${node.turnCount ?? 0}m ${savedAt} ${label}`);
62
+ for (const child of node.children) visit(child, depth + 1);
63
+ };
64
+
65
+ for (const root of roots) visit(root, 0);
66
+ lines.push("(* = current pi session; file-level tree uses pi JSONL parentSessionPath)");
67
+ return lines;
68
+ }
@@ -0,0 +1,26 @@
1
+ export function parseSessionNameCommand(input) {
2
+ if (input !== "/name" && !input.startsWith("/name ")) return { type: "none" };
3
+ const name = input.slice("/name".length).trim();
4
+ if (!name) return { type: "show" };
5
+ if (name.length > 120) return { type: "error", message: "Session name must be 120 characters or less" };
6
+ return { type: "set", name };
7
+ }
8
+
9
+ export function handleSessionNameCommand(command, { runner } = {}) {
10
+ if (command.type === "error") return [`Error: ${command.message}`];
11
+ if (command.type === "show") return [`Session name: ${runner.engine.sessionName || "(unnamed)"}`];
12
+
13
+ const name = runner.setSessionName
14
+ ? runner.setSessionName(command.name)
15
+ : setEngineSessionName(runner.engine, command.name);
16
+ return [`Session named: ${name}`];
17
+ }
18
+
19
+ function setEngineSessionName(engine, name) {
20
+ if (typeof engine.setSessionName === "function") {
21
+ engine.setSessionName(name);
22
+ } else {
23
+ engine.sessionName = String(name || "").trim();
24
+ }
25
+ return engine.sessionName;
26
+ }
@@ -0,0 +1,89 @@
1
+ import { listPiSessionInfos } from "../../session/pi-manager.mjs";
2
+ import { loadPiSessionTranscriptTurns } from "../../session/transcript.mjs";
3
+ import { resumePiSessionById } from "./pi-session-switch-command.mjs";
4
+
5
+ export async function handleSessionSourceCommand(trimmed, {
6
+ ui,
7
+ runner,
8
+ sessionState,
9
+ sessionsRoot,
10
+ projectMarchDir,
11
+ }) {
12
+ if (trimmed === "/save") {
13
+ const stats = runner.getSessionStats?.();
14
+ ui.writeln(`Pi session auto-saved: ${stats?.sessionId ?? sessionState.sessionId}`);
15
+ return { handled: true };
16
+ }
17
+
18
+ if (trimmed === "/session") {
19
+ const sessions = await listPiSessionInfos({
20
+ cwd: runner.engine.cwd,
21
+ projectMarchDir,
22
+ });
23
+ if (sessions.length === 0) {
24
+ ui.writeln("No previous sessions.");
25
+ return { handled: true };
26
+ }
27
+ if (!ui.selectList) {
28
+ ui.writeln("Session selector is only available in TUI.");
29
+ return { handled: true };
30
+ }
31
+ const currentSessionId = runner.getSessionStats?.().sessionId ?? null;
32
+ const item = await ui.selectList({
33
+ items: buildSessionSelectItems(sessions, currentSessionId),
34
+ selectedIndex: Math.max(0, sessions.findIndex((session) => session.id === currentSessionId)),
35
+ width: 72,
36
+ suppressInitialConfirm: true,
37
+ searchable: true,
38
+ getSearchText: sessionSelectSearchText,
39
+ });
40
+ if (!item) {
41
+ ui.writeln("Session unchanged.");
42
+ return { handled: true };
43
+ }
44
+ const lines = await resumePiSessionById(item.session.id, { runner, sessions, projectMarchDir });
45
+ if (isResumeSuccess(lines)) restoreTranscriptFromSession(item.session, ui);
46
+ for (const line of lines) {
47
+ ui.writeln(line);
48
+ }
49
+ return { handled: true };
50
+ }
51
+
52
+ return { handled: false };
53
+ }
54
+
55
+ export function buildSessionSelectItems(sessions, currentSessionId = null) {
56
+ return sessions.map((session) => {
57
+ const label = session.name || session.firstMessage || "(no messages)";
58
+ const savedAt = formatSessionSelectTime(session.savedAt);
59
+ return {
60
+ value: session.id,
61
+ label,
62
+ description: savedAt,
63
+ session,
64
+ };
65
+ });
66
+ }
67
+
68
+ function sessionSelectSearchText(item) {
69
+ const session = item?.session;
70
+ return `${item?.label ?? ""} ${item?.description ?? ""} ${session?.id ?? ""} ${session?.name ?? ""} ${session?.firstMessage ?? ""} ${session?.turnCount ?? ""}`;
71
+ }
72
+
73
+ function restoreTranscriptFromSession(session, ui) {
74
+ if (typeof ui.restoreTranscript !== "function") return;
75
+ try {
76
+ ui.restoreTranscript(loadPiSessionTranscriptTurns(session.path));
77
+ } catch (err) {
78
+ ui.writeln(`Warning: failed to restore session transcript: ${err.message}`);
79
+ }
80
+ }
81
+
82
+ function isResumeSuccess(lines) {
83
+ return Array.isArray(lines) && lines.some((line) => String(line).startsWith("Resumed pi session:"));
84
+ }
85
+
86
+ function formatSessionSelectTime(value) {
87
+ if (!value) return "?";
88
+ return String(value).slice(0, 16).replace("T", " ");
89
+ }
@@ -0,0 +1 @@
1
+ // Legacy session switch command removed. Use /session to restore previous pi sessions.
@@ -0,0 +1,55 @@
1
+ export function parseShellCommand(input) {
2
+ if (input === "/shell") return { type: "list" };
3
+ if (input === "/shell spawn") return { type: "spawn", name: "" };
4
+ if (input.startsWith("/shell spawn ")) {
5
+ const name = input.slice("/shell spawn ".length).trim();
6
+ return name ? { type: "spawn", name } : { type: "spawn", name: "" };
7
+ }
8
+ if (input.startsWith("/shell ")) {
9
+ const idOrName = input.slice("/shell ".length).trim();
10
+ return idOrName ? { type: "show", idOrName } : { type: "list" };
11
+ }
12
+ return { type: "none" };
13
+ }
14
+
15
+ export function handleShellCommand(command, { shellRuntime = null } = {}) {
16
+ if (!shellRuntime) {
17
+ return ["Shell runtime is disabled. Restart without --no-shell-runtime to use /shell."];
18
+ }
19
+ if (command.type === "spawn") {
20
+ const shell = shellRuntime.spawnShell({ name: command.name || undefined });
21
+ return [
22
+ `Spawned shell: ${shell.id} ${shell.name} ${shell.status}`,
23
+ "Open the right-side shell pane with Alt+S, then type directly to send input.",
24
+ ];
25
+ }
26
+ if (command.type === "list") {
27
+ const shells = shellRuntime.listShells();
28
+ if (!shells.length) return ["No shells."];
29
+ return [
30
+ "Shells:",
31
+ ...shells.map(formatShellListItem),
32
+ "Use /shell <id-or-name> to inspect recent output, or /shell spawn [name] to start one.",
33
+ ];
34
+ }
35
+ if (command.type === "show") {
36
+ const shell = findShell(shellRuntime.listShells(), command.idOrName);
37
+ if (!shell) return [`Error: shell not found: ${command.idOrName}`];
38
+ const snapshot = shellRuntime.snapshotShell(shell.id);
39
+ return [
40
+ formatShellListItem(shell),
41
+ "Recent output:",
42
+ snapshot.screen?.plain || snapshot.plain || "(no output)",
43
+ ];
44
+ }
45
+ return [];
46
+ }
47
+
48
+ function findShell(shells, idOrName) {
49
+ return shells.find((shell) => shell.id === idOrName || shell.name === idOrName) ?? null;
50
+ }
51
+
52
+ function formatShellListItem(shell) {
53
+ const args = shell.args?.length ? ` ${shell.args.join(" ")}` : "";
54
+ return `${shell.id} ${shell.name} ${shell.status} ${shell.command}${args} ${shell.lineCount ?? 0} lines`;
55
+ }