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 +1 -1
- package/src/agent/runner.mjs +2 -2
- package/src/agent/turn/turn-runner.mjs +2 -1
- package/src/cli/repl-loop.mjs +20 -2
- package/src/cli/tui/markdown-renderer.mjs +6 -0
- package/src/config/loader.mjs +1 -1
- package/src/context/engine.mjs +46 -2
- package/src/context/session-status.mjs +3 -1
- package/src/context/system-core/base.md +1 -1
- package/src/main.mjs +1 -0
- package/src/notification/desktop-notifier.mjs +33 -1
- package/src/session/sidecar.mjs +1 -0
package/package.json
CHANGED
package/src/agent/runner.mjs
CHANGED
|
@@ -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),
|
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -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);
|
package/src/config/loader.mjs
CHANGED
package/src/context/engine.mjs
CHANGED
|
@@ -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,
|
|
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
|
@@ -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 =
|
|
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("&", "&")
|
|
90
|
+
.replaceAll("<", "<")
|
|
91
|
+
.replaceAll(">", ">");
|
|
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
|
}
|
package/src/session/sidecar.mjs
CHANGED
|
@@ -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
|
}
|