march-cli 0.1.27 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -13,7 +13,7 @@ export function createRemoteRuntimeUiClient(peer) {
13
13
  retryEnd: (event) => peer.notify("uiEvent", { type: "retry_end", ...event }),
14
14
  status: (text) => peer.notify("uiEvent", { type: "status", text }),
15
15
  debugLines: (lines) => peer.notify("uiEvent", { type: "debug_lines", lines }),
16
- memoryHint: ({ source, hints }) => peer.notify("uiEvent", { type: "memory_hint", source, hints }),
16
+ recall: ({ source, hints }) => peer.notify("uiEvent", { type: "recall", source, hints }),
17
17
  editDiff: (path, diffLines) => peer.notify("uiEvent", { type: "edit_diff", path, diffLines }),
18
18
  requestPermission: (request) => peer.call("uiRequest", { type: "permission_request", ...request }),
19
19
  };
@@ -50,7 +50,7 @@ export function createRuntimeUiClient(eventBus) {
50
50
  retryEnd: (event) => eventBus.emit({ type: "retry_end", ...event }),
51
51
  status: (text) => eventBus.emit({ type: "status", text }),
52
52
  debugLines: (lines) => eventBus.emit({ type: "debug_lines", lines }),
53
- memoryHint: ({ source, hints }) => eventBus.emit({ type: "memory_hint", source, hints }),
53
+ recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
54
54
  editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
55
55
  requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
56
56
  };
@@ -71,7 +71,7 @@ export function dispatchRuntimeUiEvent(ui, event) {
71
71
  case "retry_end": return ui.retryEnd?.(pickRetryEnd(event));
72
72
  case "status": return ui.status?.(event.text);
73
73
  case "debug_lines": return writeDebugLines(ui, event.lines);
74
- case "memory_hint": return ui.memoryHint?.({ source: event.source, hints: event.hints });
74
+ case "recall": return ui.recall?.({ source: event.source, hints: event.hints });
75
75
  case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
76
76
  case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
77
77
  default: return undefined;
@@ -42,7 +42,7 @@ export async function runRunnerTurn({
42
42
  if (hints.length > 0) {
43
43
  midTurnRecallHints.push(...hints);
44
44
  queueMidTurnRecallHints(activeSession, hints, logger);
45
- ui.memoryHint?.({ source: "assistant", hints });
45
+ ui.recall?.({ source: "assistant", hints });
46
46
  }
47
47
  }
48
48
  });
@@ -93,7 +93,7 @@ function queueMidTurnRecallHints(session, hints, logger) {
93
93
  const content = formatRecallHints("assistant", hints);
94
94
  if (!content) return;
95
95
  const injected = session.sendCustomMessage?.({
96
- customType: "march.memory_hint",
96
+ customType: "march.recall",
97
97
  content,
98
98
  display: false,
99
99
  details: { source: "assistant" },
@@ -1,6 +1,6 @@
1
1
  import { stdout } from "node:process";
2
2
  import { extractToolOutput } from "./tool-output.mjs";
3
- import { formatMemoryHintLines } from "./tui/recall-rendering.mjs";
3
+ import { formatRecallLines } from "./tui/recall-rendering.mjs";
4
4
  import { formatToolStartLine } from "./tui/tool-rendering.mjs";
5
5
  import { brightBlack, dim, red, green, yellow } from "./tui/ui-theme.mjs";
6
6
 
@@ -32,7 +32,7 @@ export function createJsonUI() {
32
32
  stdout.write(delta);
33
33
  },
34
34
  status: () => {},
35
- memoryHint: () => {},
35
+ recall: () => {},
36
36
  clearOutput: () => {},
37
37
  restoreTranscript: () => {},
38
38
  setStatusBar: () => {},
@@ -111,9 +111,9 @@ export function createPlainUI() {
111
111
  },
112
112
  textDelta: writeText,
113
113
  status: (text) => { ensureNewline(); stdout.write(`${brightBlack(`● ${text}`)}\n`); },
114
- memoryHint: ({ hints }) => {
114
+ recall: ({ hints }) => {
115
115
  ensureNewline();
116
- for (const line of formatMemoryHintLines(hints)) stdout.write(`${brightBlack(line)}\n`);
116
+ for (const line of formatRecallLines(hints)) stdout.write(`${brightBlack(line)}\n`);
117
117
  },
118
118
  clearOutput: () => {},
119
119
  restoreTranscript: () => {},
@@ -27,8 +27,8 @@ export async function runSingleShotPrompt({
27
27
  const modePrompt = appendModeReminder(prompt, modeState?.get?.());
28
28
  const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
29
29
  ui.writeln(formatUserDisplayMessage(prompt));
30
- ui.memoryHint?.({ source: "user", hints: userRecallHints });
31
- if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.memoryHint?.({ source: "assistant", hints: carryoverRecallHints });
30
+ ui.recall?.({ source: "user", hints: userRecallHints });
31
+ if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.recall?.({ source: "assistant", hints: carryoverRecallHints });
32
32
  refreshStatusBar.startWorking?.();
33
33
  try {
34
34
  await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
@@ -149,8 +149,8 @@ async function runReplTurn({ prompt, args, runner, memoryStore, currentProject,
149
149
  const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
150
150
  try {
151
151
  ui.writeln(formatUserDisplayMessage(prompt));
152
- ui.memoryHint?.({ source: "user", hints: userRecallHints });
153
- if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.memoryHint?.({ source: "assistant", hints: carryoverRecallHints });
152
+ ui.recall?.({ source: "user", hints: userRecallHints });
153
+ if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.recall?.({ source: "assistant", hints: carryoverRecallHints });
154
154
  setTurnRunning(true);
155
155
  refreshStatusBar.startWorking?.();
156
156
  await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
@@ -178,6 +178,6 @@ function renderPendingAssistantRecallPreview({ runner, ui }) {
178
178
  if (runner.engine.hasRenderedPendingAssistantRecallHints?.()) return;
179
179
  const hints = runner.engine.peekPendingAssistantRecallHints?.() ?? [];
180
180
  if (hints.length === 0) return;
181
- ui.memoryHint?.({ source: "assistant", hints });
181
+ ui.recall?.({ source: "assistant", hints });
182
182
  runner.engine.markPendingAssistantRecallHintsRendered?.();
183
183
  }
@@ -10,7 +10,8 @@ export class MainPaneLayout {
10
10
  render(width) {
11
11
  const safeWidth = Math.max(1, Math.trunc(width));
12
12
  const statusTopLines = this.statusBar.renderTop?.(safeWidth) ?? this.statusBar.render(safeWidth);
13
- const rawEditorLines = this.editor.render(safeWidth);
13
+ const editorWidth = this.statusBar.inputContentWidth?.(safeWidth) ?? safeWidth;
14
+ const rawEditorLines = this.editor.render(Math.max(1, editorWidth));
14
15
  const editorLines = this.statusBar.renderInputLines?.(rawEditorLines, safeWidth)
15
16
  ?? rawEditorLines.map((line) => this.statusBar.renderInputLine?.(line, safeWidth) ?? line);
16
17
  const statusBottomLines = this.statusBar.renderBottom?.(safeWidth) ?? [];
@@ -1,5 +1,5 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
- import { R, brightBlack, dim, red } from "../ui-theme.mjs";
2
+ import { R, brightBlack, dim, red, warmAmber } from "../ui-theme.mjs";
3
3
 
4
4
  export function renderToolCardBlock(block, width) {
5
5
  const lines = [];
@@ -7,7 +7,7 @@ export function renderToolCardBlock(block, width) {
7
7
  const marker = block.state === "running" ? "▶" : block.expanded ? "▾" : "▸";
8
8
  const summary = block.summary ? ` · ${block.summary}` : "";
9
9
  const head = `${marker} ${block.title}${summary}`;
10
- appendCardWrapped(lines, border, (block.isError ? red : dim)(head), width);
10
+ appendCardWrapped(lines, border, colorToolHead(block)(head), width);
11
11
 
12
12
  if (block.expanded && block.bodyLines?.length) {
13
13
  lines.push(border);
@@ -16,6 +16,12 @@ export function renderToolCardBlock(block, width) {
16
16
  return lines;
17
17
  }
18
18
 
19
+ function colorToolHead(block) {
20
+ if (block.isError) return red;
21
+ if (block.name === "memory_open") return warmAmber;
22
+ return dim;
23
+ }
24
+
19
25
  function appendCardWrapped(lines, border, text, width, indent = "") {
20
26
  const prefix = `${border} ${indent}`;
21
27
  const contentWidth = Math.max(8, width - visibleWidth(prefix));
@@ -1,8 +1,8 @@
1
- import { brightBlack, warmAmber } from "./ui-theme.mjs";
1
+ import { brightBlack } from "./ui-theme.mjs";
2
2
 
3
3
  const RECALL_ICON = "✦";
4
4
 
5
- export function formatMemoryHintLines(hints = []) {
5
+ export function formatRecallLines(hints = []) {
6
6
  if (!hints.length) return [];
7
7
  const noun = hints.length === 1 ? "note" : "notes";
8
8
  return [
@@ -11,11 +11,10 @@ export function formatMemoryHintLines(hints = []) {
11
11
  ];
12
12
  }
13
13
 
14
- export function writeMemoryHint({ output, hints = [] }) {
15
- const lines = formatMemoryHintLines(hints);
16
- lines.forEach((line, index) => {
17
- if (index === 0) output.writeln(warmAmber(line));
18
- else if (line.startsWith(" ")) output.writeln(brightBlack(line));
14
+ export function writeRecall({ output, hints = [] }) {
15
+ const lines = formatRecallLines(hints);
16
+ lines.forEach((line) => {
17
+ if (line.startsWith(" ")) output.writeln(brightBlack(line));
19
18
  else output.writeln(line);
20
19
  });
21
20
  }
@@ -45,6 +45,10 @@ export class StatusBar {
45
45
  return [`${left}${line}${right}`, ""];
46
46
  }
47
47
 
48
+ inputContentWidth(width) {
49
+ return maxInputContentWidth(width);
50
+ }
51
+
48
52
  renderInputLines(lines, width) {
49
53
  if (width <= 0) return [""];
50
54
  const { left, innerWidth, right } = insetForWidth(width);
@@ -62,9 +66,7 @@ export class StatusBar {
62
66
  if (width <= 0) return "";
63
67
  const paintWidth = inputPaintWidth(width);
64
68
  const prompt = isFirst ? statusBar.prompt(INPUT_PROMPT) : " ";
65
- const promptWidth = visibleWidth(stripAnsi(INPUT_PROMPT));
66
- const maxContentWidth = Math.max(0, paintWidth - promptWidth - 2);
67
- const content = clipToWidth(line, maxContentWidth);
69
+ const content = clipToWidth(line, maxInputContentWidth(width));
68
70
  return applyInputBackground(padToWidth(`${prompt}${content}`, paintWidth));
69
71
  }
70
72
 
@@ -108,6 +110,12 @@ function inputPaintWidth(width) {
108
110
  return safeWidth > 1 ? safeWidth - 1 : safeWidth;
109
111
  }
110
112
 
113
+ function maxInputContentWidth(width) {
114
+ const paintWidth = inputPaintWidth(width);
115
+ const promptWidth = visibleWidth(stripAnsi(INPUT_PROMPT));
116
+ return Math.max(0, paintWidth - promptWidth - 2);
117
+ }
118
+
111
119
  function applyInputBackground(line) {
112
120
  return `${INPUT_BG}${String(line).replaceAll(R, `${R}${INPUT_BG}`)}${R}`;
113
121
  }
@@ -1,6 +1,6 @@
1
1
  import { resolveAttachmentTokens, uniqueAttachmentToken, withLeadingSpace } from "../input/attachment-tokens.mjs";
2
2
 
3
- export function createTuiInputController({ editor, requestRender, historyStore = null }) {
3
+ export function createTuiInputController({ editor, requestRender, historyStore = null, onSubmit = null }) {
4
4
  let onSubmitResolve = null;
5
5
  const attachmentTokens = new Map();
6
6
 
@@ -17,6 +17,7 @@ export function createTuiInputController({ editor, requestRender, historyStore =
17
17
  }
18
18
  clearSubmitState();
19
19
  attachmentTokens.clear();
20
+ onSubmit?.();
20
21
  resolve(resolvedText);
21
22
  };
22
23
  });
package/src/cli/ui.mjs CHANGED
@@ -20,7 +20,7 @@ import { createMouseSelectionController } from "./tui/input/mouse-selection-cont
20
20
  import { ScreenSelection } from "./tui/selection-screen.mjs";
21
21
  import { writeEditDiff } from "./tui/tui-diff-rendering.mjs";
22
22
  import { createTuiInputController } from "./tui/tui-input-controller.mjs";
23
- import { writeMemoryHint } from "./tui/recall-rendering.mjs";
23
+ import { writeRecall } from "./tui/recall-rendering.mjs";
24
24
  import { writeToolEnd, writeToolStart } from "./tui/tool-rendering.mjs";
25
25
  import { EDITOR_THEME, brightBlack } from "./tui/ui-theme.mjs";
26
26
  import { createRenderScheduler } from "./tui/render/render-scheduler.mjs";
@@ -104,12 +104,6 @@ export function createTuiUI({
104
104
  if (copyKeyResult) return copyKeyResult;
105
105
  const dispatched = keybindingDispatcher.dispatch(data);
106
106
  if (dispatched) return dispatched;
107
- // When output is scrolled up, the next render has fewer lines.
108
- // On new input, reset scroll to tail so the editor stays at bottom.
109
- if (output.scrollOffset > 0) {
110
- output.resetScroll();
111
- requestRender();
112
- }
113
107
  if (shellDrawer.isInputActive()) {
114
108
  shellDrawer.sendInput(data);
115
109
  requestRender();
@@ -150,7 +144,12 @@ export function createTuiUI({
150
144
  retryStatus.end({ success, attempt, finalError });
151
145
  }
152
146
 
153
- const inputController = createTuiInputController({ editor, requestRender, historyStore });
147
+ const resetOutputScrollOnSubmit = () => {
148
+ if (output.scrollOffset <= 0) return;
149
+ output.resetScroll();
150
+ requestRender();
151
+ };
152
+ const inputController = createTuiInputController({ editor, requestRender, historyStore, onSubmit: resetOutputScrollOnSubmit });
154
153
 
155
154
  return {
156
155
  readline: (_prompt) => {
@@ -205,8 +204,8 @@ export function createTuiUI({
205
204
  status: (text) => {
206
205
  ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
207
206
  },
208
- memoryHint: ({ hints }) => {
209
- ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeMemoryHint({ output, hints }); requestRender();
207
+ recall: ({ hints }) => {
208
+ ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeRecall({ output, hints }); requestRender();
210
209
  },
211
210
 
212
211
  clearOutput: () => {
@@ -91,13 +91,14 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
91
91
  </git_contract>
92
92
 
93
93
  <memory_system>
94
- - [memory_hint source="..."] blocks in recent_chat are lightweight recall hints matched from prior thinking output. Treat them as possibly relevant pointers, not as complete facts.
95
- - If a memory hint may help the current task, use memory_open(id) to read the full memory before relying on it. Ignore hints that are clearly unrelated or too low-value for the task.
94
+ - [recall source="..."] blocks in recent_chat are lightweight recall hints matched from prior thinking output. Treat them as possibly relevant pointers, not as complete facts.
95
+ - A recall hint's description may record key operational constraints, including when the full memory must be opened; factor those constraints into relevance before acting.
96
+ - If a recall hint may help the current task, use memory_open(id) to read the full memory before relying on it. Ignore hints that are clearly unrelated or too low-value for the task.
96
97
  - Use memory_search(query) for full-text search across all memories.
97
98
  - To edit an existing memory, use memory_open(id) to get its path, then edit_file with mode="patch" for targeted edits.
98
99
  - Use memory_save() to create memories or update whole fields. Before creating a new memory, first search/open related memories and merge updates into an existing memory when they share the same topic, project, or decision thread; prefer modifying the existing memory file over creating a scattered new one. Tags are the primary retrieval key for future recall. Prefer lowercase kebab-case tags like 'march-cli', 'tooling', 'permissions'.
99
100
  - When learning multiple related external workflows or skills, maintain memory as an evolving domain library: start with the specific source name when only one item exists, then rename and rewrite the memory title/description as the scope grows; merge new related learnings into the same memory, preserving each source's unique traits while distilling reusable principles.
100
101
  - Distinguish "migrating a Skill to memory" from "learning a Skill": migration preserves the complete Skill folder under memory_root/skills/ and creates a memory entry as its index; that memory should describe what the Skill is for and reference the copied Skill folder path so future recall knows how to use it. Learning only reads and internalizes the Skill's methods, scenarios, and principles into ordinary memory without copying source files. Infer the action from the user's wording, and ask when ambiguous.
101
- - Unlike memory hints, this system-core center is always visible in every model call. Only update the center for instructions that must always be followed; use memory for contextual, project-specific, or recall-dependent knowledge.
102
+ - Unlike recall blocks, this system-core center is always visible in every model call. Only update the center for instructions that must always be followed; use memory for contextual, project-specific, or recall-dependent knowledge.
102
103
  - If execution takes a meaningful detour, create or update a memory after the task. A detour means the initial plan or assumption failed, multiple approaches were tried, and the final successful path contains reusable project knowledge. Record the failed assumption, what was tried, and the successful approach. Prefer updating an existing related memory over creating a new one.
103
104
  </memory_system>
@@ -2,7 +2,7 @@ import { expandTags, normalizeText } from "./markdown-format.mjs";
2
2
 
3
3
  export function formatRecallHints(source, hints = []) {
4
4
  if (!hints.length) return "";
5
- const lines = [`[memory_hint source="${source}"]`];
5
+ const lines = [`[recall source="${source}"]`];
6
6
  for (const hint of hints) {
7
7
  lines.push(`- ${hint.id} | ${hint.name} | ${hint.description}`);
8
8
  }
@@ -59,7 +59,7 @@ export class MarkdownMemoryStore {
59
59
  continue;
60
60
  }
61
61
  if (!parsed.frontmatter.description) {
62
- diagnostics.push({ type: "warning", path, message: "Memory file is missing description; excluded from memory hint recall" });
62
+ diagnostics.push({ type: "warning", path, message: "Memory file is missing description; excluded from passive recall" });
63
63
  }
64
64
  const tags = normalizeTags(parsed.frontmatter.tags ?? []);
65
65
  const entry = {
@@ -37,9 +37,9 @@ export function createMarkdownMemoryTools(store, { remoteSources = [] } = {}) {
37
37
  defineTool({
38
38
  name: "memory_open",
39
39
  label: "Memory Open",
40
- description: "Open a Markdown memory by id or by path. Use this after memory hint or memory_search when you need more context. Local memories may be edited with edit_file; remote memories are read-only.",
40
+ description: "Open a Markdown memory by id or by path. Use this after recall hint or memory_search when you need more context. Local memories may be edited with edit_file; remote memories are read-only.",
41
41
  parameters: Type.Object({
42
- id: Type.Optional(Type.String({ description: "Local memory id from memory hint, e.g. mem_..." })),
42
+ id: Type.Optional(Type.String({ description: "Local memory id from recall hint, e.g. mem_..." })),
43
43
  source: Type.Optional(Type.String({ description: "Memory source: omitted/local or a remote memory name" })),
44
44
  path: Type.Optional(Type.String({ description: "Path returned by memory_search" })),
45
45
  line: Type.Optional(Type.Number({ description: "Open around this 1-based line number" })),
@@ -70,13 +70,13 @@ export function createMarkdownMemoryTools(store, { remoteSources = [] } = {}) {
70
70
  name: "memory_save",
71
71
  label: "Memory Save",
72
72
  description:
73
- "Create a Markdown memory or update whole fields on an existing memory. For targeted edits to an existing memory body or frontmatter, use memory_open to get the path, then edit_file. Before creating a new memory, merge related updates into an existing memory when they share the same topic or decision thread. New memories require name, description, body, and at least one tag because memory hints only use tags. When updating by id, omitted fields keep their existing values; passing tags replaces the full tag list.",
73
+ "Create a Markdown memory or update whole fields on an existing memory. For targeted edits to an existing memory body or frontmatter, use memory_open to get the path, then edit_file. Before creating a new memory, merge related updates into an existing memory when they share the same topic or decision thread. New memories require name, description, body, and at least one tag because recall hints only use tags. When updating by id, omitted fields keep their existing values; passing tags replaces the full tag list.",
74
74
  parameters: Type.Object({
75
75
  id: Type.Optional(Type.String({ description: "Existing memory id to update. Omit to create a new memory." })),
76
76
  name: Type.Optional(Type.String({ description: "Memory name. Required when creating." })),
77
- description: Type.Optional(Type.String({ description: "Short natural-language summary shown in memory hint. Required when creating." })),
77
+ description: Type.Optional(Type.String({ description: "Short natural-language summary shown in recall hint. Required when creating." })),
78
78
  body: Type.Optional(Type.String({ description: "Markdown memory body. Required when creating." })),
79
- tags: Type.Optional(Type.Array(Type.String(), { description: "Tags used for memory hint. Required and non-empty when creating; replaces tags when updating. Prefer stable retrieval keys: project name, technology, feature/domain, user/person, and decision topic. Use lowercase kebab-case when possible. Examples: ['march-cli', 'tooling', 'permissions'], ['memory', 'sqlite-index']." })),
79
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Tags used for recall hint. Required and non-empty when creating; replaces tags when updating. Prefer stable retrieval keys: project name, technology, feature/domain, user/person, and decision topic. Use lowercase kebab-case when possible. Examples: ['march-cli', 'tooling', 'permissions'], ['memory', 'sqlite-index']." })),
80
80
  }),
81
81
  execute: async (_toolCallId, params) => {
82
82
  try {