march-cli 0.1.1 → 0.1.3

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.1",
3
+ "version": "0.1.3",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -27,7 +27,7 @@ export { installModelPayloadDumper } from "./model-payload-dumper.mjs";
27
27
  export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
28
28
  export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
29
29
 
30
- export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryStore = null, memoryTools = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null }) {
30
+ export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, memoryStore = null, memoryTools = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null }) {
31
31
  if (!useRuntimeHost && extensionPaths.length > 0) {
32
32
  throw new Error("--extension requires the default pi runtime host path");
33
33
  }
@@ -47,7 +47,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
47
47
  retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
48
48
  });
49
49
  const lspService = new LspService({ cwd });
50
- const engine = new ContextEngine({ cwd, modelId, provider, namespace, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
50
+ const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
51
51
  const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
52
52
  const sessionBinding = createSessionBinding(null);
53
53
  let currentModelCallKind = "model";
@@ -29,6 +29,7 @@ export async function runRunnerTurn({
29
29
  if (hints.length > 0) {
30
30
  midTurnRecallHints.push(...hints);
31
31
  onMidTurnRecallHints?.(hints);
32
+ ui.memoryHint?.({ source: "assistant", hints });
32
33
  }
33
34
  }
34
35
  });
@@ -51,8 +52,8 @@ export async function runRunnerTurn({
51
52
 
52
53
  closeAssistantReply({ ui, state: turnState });
53
54
  const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
55
+ engine.setPendingAssistantRecallHints?.(assistantRecallHints);
54
56
  const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
55
- ui.memoryHint?.({ source: "assistant", hints: recordedAssistantRecallHints });
56
57
 
57
58
  engine.recordTurn({
58
59
  userMessage: userMessage ?? prompt.slice(0, 300),
@@ -18,16 +18,21 @@ export async function runSingleShotPrompt({
18
18
  modeState = null,
19
19
  }) {
20
20
  memoryStore.beginTurn();
21
+ const carryoverAlreadyRendered = runner.engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
22
+ const carryoverRecallHints = runner.engine.takePendingAssistantRecallHints?.() ?? [];
21
23
  const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
22
24
  const recallBlock = formatRecallHints("user", userRecallHints);
25
+ const carryoverRecallBlock = formatRecallHints("assistant", carryoverRecallHints);
23
26
  const shellHints = formatShellHints(runner.shellRuntime);
24
27
  const modePrompt = appendModeReminder(prompt, modeState?.get?.());
25
- const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, shellHints);
28
+ const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
26
29
  ui.writeln(formatUserDisplayMessage(prompt));
27
30
  ui.memoryHint?.({ source: "user", hints: userRecallHints });
31
+ if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.memoryHint?.({ source: "assistant", hints: carryoverRecallHints });
28
32
  refreshStatusBar.startWorking?.();
29
33
  try {
30
34
  await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
35
+ renderPendingAssistantRecallPreview({ runner, ui });
31
36
  } finally {
32
37
  refreshStatusBar.stopWorking?.();
33
38
  memoryStore.endTurn();
@@ -126,17 +131,22 @@ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
126
131
 
127
132
  async function runReplTurn({ prompt, args, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
128
133
  memoryStore.beginTurn();
134
+ const carryoverAlreadyRendered = runner.engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
135
+ const carryoverRecallHints = runner.engine.takePendingAssistantRecallHints?.() ?? [];
129
136
  const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
130
137
  const recallBlock = formatRecallHints("user", userRecallHints);
138
+ const carryoverRecallBlock = formatRecallHints("assistant", carryoverRecallHints);
131
139
  const shellHints = formatShellHints(runner.shellRuntime);
132
140
  const modePrompt = appendModeReminder(prompt, modeState?.get?.());
133
- const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, shellHints);
141
+ const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
134
142
  try {
135
143
  ui.writeln(formatUserDisplayMessage(prompt));
136
144
  ui.memoryHint?.({ source: "user", hints: userRecallHints });
145
+ if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.memoryHint?.({ source: "assistant", hints: carryoverRecallHints });
137
146
  setTurnRunning(true);
138
147
  refreshStatusBar.startWorking?.();
139
148
  await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
149
+ renderPendingAssistantRecallPreview({ runner, ui });
140
150
  ui.writeln("");
141
151
  } catch (err) {
142
152
  ui.writeln(`Error: ${err.message}`);
@@ -155,3 +165,11 @@ export function formatUserDisplayMessage(prompt) {
155
165
  function appendPromptBlocks(prompt, ...blocks) {
156
166
  return [prompt, ...blocks.filter(Boolean)].join("\n\n");
157
167
  }
168
+
169
+ function renderPendingAssistantRecallPreview({ runner, ui }) {
170
+ if (runner.engine.hasRenderedPendingAssistantRecallHints?.()) return;
171
+ const hints = runner.engine.peekPendingAssistantRecallHints?.() ?? [];
172
+ if (hints.length === 0) return;
173
+ ui.memoryHint?.({ source: "assistant", hints });
174
+ runner.engine.markPendingAssistantRecallHintsRendered?.();
175
+ }
@@ -167,6 +167,12 @@ function appendWrappedRuns(lines, runs, width, indent = 0, firstPrefix = null) {
167
167
  const maxWidth = Math.max(1, width);
168
168
  for (const run of runs) {
169
169
  for (const ch of run.text) {
170
+ if (ch === "\n") {
171
+ lines.push(current);
172
+ current = restPrefix;
173
+ currentWidth = visibleWidth(restPrefix);
174
+ continue;
175
+ }
170
176
  const charWidth = visibleWidth(ch);
171
177
  if (currentWidth + charWidth > maxWidth && currentWidth > visibleWidth(stripAnsi(prefix))) {
172
178
  lines.push(current);
@@ -49,7 +49,7 @@ function mergeLayers(layers) {
49
49
  maxTurns: null,
50
50
  trimBatch: null,
51
51
  memoryRoot: null,
52
- notifications: { turnEnd: false },
52
+ notifications: { turnEnd: true },
53
53
  };
54
54
 
55
55
  for (const layer of layers) {
@@ -6,12 +6,15 @@ import { buildProjectContext } from "./project-context.mjs";
6
6
  import { formatRecallHints } from "../memory/markdown-store.mjs";
7
7
 
8
8
  export class ContextEngine {
9
- constructor({ cwd, modelId, provider = "deepseek", thinkingLevel = "medium", namespace = "", shellRuntime = null, lspService = null, injections = [], maxTurns, trimBatch }) {
9
+ constructor({ cwd, modelId, provider = "deepseek", thinkingLevel = "medium", namespace = "", memoryRoot = null, shellRuntime = null, lspService = null, injections = [], maxTurns, trimBatch }) {
10
10
  this.cwd = cwd;
11
+ this.memoryRoot = memoryRoot;
11
12
  this.modelId = modelId;
12
13
  this.provider = provider;
13
14
  this.thinkingLevel = thinkingLevel;
14
15
  this.turns = [];
16
+ this.pendingAssistantRecallHints = [];
17
+ this.pendingAssistantRecallHintsRendered = false;
15
18
  this.sessionName = "";
16
19
  this.toolDefs = [];
17
20
  this.namespace = namespace;
@@ -85,6 +88,30 @@ export class ContextEngine {
85
88
  return ids;
86
89
  }
87
90
 
91
+ setPendingAssistantRecallHints(hints = []) {
92
+ this.pendingAssistantRecallHints = uniqueHints(hints);
93
+ this.pendingAssistantRecallHintsRendered = false;
94
+ }
95
+
96
+ peekPendingAssistantRecallHints() {
97
+ return this.pendingAssistantRecallHints;
98
+ }
99
+
100
+ hasRenderedPendingAssistantRecallHints() {
101
+ return this.pendingAssistantRecallHintsRendered;
102
+ }
103
+
104
+ markPendingAssistantRecallHintsRendered() {
105
+ if (this.pendingAssistantRecallHints.length > 0) this.pendingAssistantRecallHintsRendered = true;
106
+ }
107
+
108
+ takePendingAssistantRecallHints() {
109
+ const hints = this.pendingAssistantRecallHints;
110
+ this.pendingAssistantRecallHints = [];
111
+ this.pendingAssistantRecallHintsRendered = false;
112
+ return hints;
113
+ }
114
+
88
115
  resolvePath(raw) {
89
116
  return resolve(this.cwd, raw);
90
117
  }
@@ -106,9 +133,15 @@ export class ContextEngine {
106
133
  restoreSession(data, _pool, { replace = false } = {}) {
107
134
  if (replace) {
108
135
  this.turns = [];
136
+ this.pendingAssistantRecallHints = [];
137
+ this.pendingAssistantRecallHintsRendered = false;
109
138
  this.sessionName = "";
110
139
  }
111
140
  if (data.turns) this.turns = data.turns;
141
+ if (Array.isArray(data.pendingAssistantRecallHints)) {
142
+ this.pendingAssistantRecallHints = uniqueHints(data.pendingAssistantRecallHints);
143
+ this.pendingAssistantRecallHintsRendered = false;
144
+ }
112
145
  if (typeof data.sessionName === "string") this.sessionName = data.sessionName;
113
146
  this.setRuntimeState(data);
114
147
  }
@@ -116,7 +149,7 @@ export class ContextEngine {
116
149
  // ── Private layers ──────────────────────────────────────────────────
117
150
 
118
151
  #buildSessionIdentity() {
119
- return buildSessionIdentity({ cwd: this.cwd, workspaceRoot: this.cwd });
152
+ return buildSessionIdentity({ cwd: this.cwd, workspaceRoot: this.cwd, memoryRoot: this.memoryRoot });
120
153
  }
121
154
 
122
155
  #buildRecentChat() {
@@ -146,3 +179,14 @@ function appendCurrentUser(recentChat, userMessage) {
146
179
  const currentUser = String(userMessage ?? "").trimEnd();
147
180
  return `${recentChat}\n\n[current_user]\n${currentUser}`;
148
181
  }
182
+
183
+ function uniqueHints(hints = []) {
184
+ const seen = new Set();
185
+ const unique = [];
186
+ for (const hint of hints) {
187
+ if (!hint?.id || seen.has(hint.id)) continue;
188
+ seen.add(hint.id);
189
+ unique.push(hint);
190
+ }
191
+ return unique;
192
+ }
@@ -1,15 +1,17 @@
1
1
  export function buildSessionIdentity({
2
2
  cwd,
3
3
  workspaceRoot = cwd,
4
+ memoryRoot = null,
4
5
  platform = process.platform,
5
6
  } = {}) {
6
7
  const shellInfo = platform === "win32"
7
8
  ? "shells: powershell (recommended), bash (Git Bash)"
8
9
  : "shell: bash";
10
+ const memoryInfo = memoryRoot ? `memory_root: ${memoryRoot}\n` : "";
9
11
 
10
12
  return `[session_identity]
11
13
  cwd: ${cwd}
12
14
  workspace_root: ${workspaceRoot}
13
- platform: ${platform}
15
+ ${memoryInfo}platform: ${platform}
14
16
  ${shellInfo}`;
15
17
  }
@@ -56,5 +56,5 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
56
56
  - [memory_hint source="..."] blocks in recent_chat show memory hints matched from your thinking output. Use memory_open(id) to read the full content.
57
57
  - Use memory_search(query) for full-text search across all memories.
58
58
  - To edit an existing memory, use memory_open(id) to get its path, then edit_file with mode="patch" for targeted edits.
59
- - Use memory_save() to create memories or update whole fields. Before creating a new memory, merge related updates into an existing memory when they share the same topic or decision thread. Tags are the primary retrieval key for future recall. Prefer lowercase kebab-case tags like 'march-cli', 'tooling', 'permissions'.
59
+ - 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'.
60
60
  </memory_system>
package/src/main.mjs CHANGED
@@ -155,6 +155,7 @@ export async function run(argv) {
155
155
  providers: config.providers,
156
156
  stateRoot,
157
157
  ui,
158
+ memoryRoot,
158
159
  memoryStore,
159
160
  memoryTools,
160
161
  shellRuntime,
@@ -25,7 +25,7 @@ export async function sendDesktopNotification({ platform = process.platform, spa
25
25
 
26
26
  const safeTitle = normalizeNotificationText(title) || "March";
27
27
  const safeMessage = normalizeNotificationText(message) || "Turn finished";
28
- const script = buildWindowsBalloonScript({ title: safeTitle, message: safeMessage });
28
+ const script = buildWindowsNotificationScript({ title: safeTitle, message: safeMessage });
29
29
 
30
30
  try {
31
31
  const child = spawnProcess("powershell.exe", [
@@ -63,6 +63,38 @@ export function buildWindowsBalloonScript({ title, message, timeoutMs = DEFAULT_
63
63
  ].join("; ");
64
64
  }
65
65
 
66
+ export function buildWindowsNotificationScript({ title, message, timeoutMs = DEFAULT_BALLOON_TIMEOUT_MS }) {
67
+ const toastXml = escapePowerShellDoubleQuotedString(buildToastXml({ title, message }));
68
+ const balloonScript = buildWindowsBalloonScript({ title, message, timeoutMs });
69
+ return [
70
+ "try {",
71
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
72
+ "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null",
73
+ "$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
74
+ `$xml.LoadXml("${toastXml}")`,
75
+ "$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)",
76
+ "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('PowerShell').Show($toast)",
77
+ "} catch {",
78
+ balloonScript,
79
+ "}",
80
+ ].join("; ");
81
+ }
82
+
83
+ function buildToastXml({ title, message }) {
84
+ return `<toast><visual><binding template="ToastGeneric"><text>${escapeXmlText(title)}</text><text>${escapeXmlText(message)}</text></binding></visual></toast>`;
85
+ }
86
+
87
+ function escapeXmlText(text) {
88
+ return String(text)
89
+ .replaceAll("&", "&amp;")
90
+ .replaceAll("<", "&lt;")
91
+ .replaceAll(">", "&gt;");
92
+ }
93
+
94
+ function escapePowerShellDoubleQuotedString(text) {
95
+ return String(text).replace(/[`"$]/g, "`$&");
96
+ }
97
+
66
98
  function defaultTurnTitle(status) {
67
99
  return status === "error" ? "March turn failed" : "March is ready";
68
100
  }
@@ -22,6 +22,7 @@ export function captureContextSidecar(engine, metadata = {}) {
22
22
  sessionName: engine.sessionName ?? "",
23
23
  thinkingLevel: engine.thinkingLevel,
24
24
  namespace: engine.namespace,
25
+ pendingAssistantRecallHints: engine.pendingAssistantRecallHints ?? [],
25
26
  turns: engine.turns,
26
27
  };
27
28
  }